Files
admin/app/Jobs/AddWatermarkJob.php
Husanjonazamov e0f1989655 classify admin
2026-02-24 12:52:01 +05:00

395 lines
16 KiB
PHP

<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use App\Services\CachingService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class AddWatermarkJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $imagePath;
public string $extension;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public int $tries = 3;
/**
* The maximum number of seconds the job can run before timing out.
*
* @var int
*/
public int $timeout = 300; // 5 minutes
/**
* The number of seconds to wait before retrying the job.
*
* @var int
*/
public int $backoff = 60; // Wait 60 seconds before retry
/**
* Create a new job instance.
*
* @param string $imagePath
* @param string $extension
*/
public function __construct(string $imagePath, string $extension)
{
$this->imagePath = $imagePath;
$this->extension = $extension;
}
public function handle(): void
{
try {
// Log::info('AddWatermarkJob started', [
// 'imagePath' => $this->imagePath,
// 'extension' => $this->extension,
// 'file_exists' => file_exists($this->imagePath)
// ]);
$startTime = microtime(true);
// Get all settings from cache
$settings = CachingService::getSystemSettings()->toArray();
// Check if watermark is enabled
$watermarkEnabled = isset($settings['watermark_enabled']) && (int)$settings['watermark_enabled'] === 1;
if (!$watermarkEnabled) {
// Log::info('Watermark is disabled, skipping watermark job');
return;
}
// Log::info('Watermark is enabled, processing job');
// Helper function to extract relative path from URL or return path as is
$extractPathFromUrl = function($pathOrUrl) {
if (empty($pathOrUrl)) {
return null;
}
// If it's a URL, extract the path after /storage/
if (filter_var($pathOrUrl, FILTER_VALIDATE_URL)) {
$parsedUrl = parse_url($pathOrUrl);
$path = $parsedUrl['path'] ?? '';
// Extract path after /storage/
if (preg_match('#/storage/(.+)$#', $path, $matches)) {
return $matches[1];
}
// If /storage/ not found, try to extract from path
if (preg_match('#/([^/]+/.+)$#', $path, $matches)) {
return $matches[1];
}
}
// Return as is if it's already a relative path
return $pathOrUrl;
};
// Get watermark image path
$watermarkImage = $settings['watermark_image'] ?? null;
$watermarkPath = null;
$disk = config('filesystems.default');
if (!empty($watermarkImage)) {
// Extract relative path from URL if needed
$watermarkRelativePath = $extractPathFromUrl($watermarkImage);
// Try to get absolute path from storage
if ($watermarkRelativePath && Storage::disk($disk)->exists($watermarkRelativePath)) {
$watermarkPath = Storage::disk($disk)->path($watermarkRelativePath);
// Log::info('Watermark found in storage', ['path' => $watermarkPath]);
} else {
// Try direct path if it's a file path
if (file_exists($watermarkImage)) {
$watermarkPath = $watermarkImage;
// Log::info('Watermark found as direct path', ['path' => $watermarkPath]);
} else {
// Fallback to public path
$watermarkPath = public_path('assets/images/logo/' . basename($watermarkImage));
// Log::info('Trying public path fallback', ['path' => $watermarkPath]);
}
}
}
// If watermark image not found, try company logo
if (empty($watermarkPath) || !file_exists($watermarkPath)) {
// Log::info('Watermark image not found, trying company logo');
$companyLogo = $settings['company_logo'] ?? null;
if (!empty($companyLogo)) {
// Extract relative path from URL if needed
$companyLogoRelativePath = $extractPathFromUrl($companyLogo);
if ($companyLogoRelativePath && Storage::disk($disk)->exists($companyLogoRelativePath)) {
$watermarkPath = Storage::disk($disk)->path($companyLogoRelativePath);
// Log::info('Company logo found in storage', ['path' => $watermarkPath]);
} elseif (file_exists($companyLogo)) {
$watermarkPath = $companyLogo;
// Log::info('Company logo found as direct path', ['path' => $watermarkPath]);
} else {
$watermarkPath = public_path('assets/images/logo/' . basename($companyLogo));
// Log::info('Trying company logo public path', ['path' => $watermarkPath]);
}
} else {
$watermarkPath = public_path('assets/images/logo/logo.png');
// Log::info('Using default logo path', ['path' => $watermarkPath]);
}
}
if (!file_exists($watermarkPath)) {
return;
}
// Get watermark settings with validation
$opacity = isset($settings['watermark_opacity']) ? (int)$settings['watermark_opacity'] : 25;
$size = isset($settings['watermark_size']) ? (int)$settings['watermark_size'] : 10;
$style = $settings['watermark_style'] ?? 'tile';
$position = $settings['watermark_position'] ?? 'center';
$rotation = isset($settings['watermark_rotation']) ? (int)$settings['watermark_rotation'] : 0;
// Validate and clamp values to safe ranges
$opacity = max(0, min(100, $opacity)); // Clamp between 0-100
$size = max(1, min(100, $size)); // Clamp between 1-100
$rotation = max(-360, min(360, $rotation)); // Clamp between -360 to 360
// Normalize rotation to -180 to 180 range for efficiency
while ($rotation > 180) $rotation -= 360;
while ($rotation < -180) $rotation += 360;
// Validate style
if (!in_array($style, ['tile', 'single', 'center'])) {
$style = 'tile';
}
// Validate position
if (!in_array($position, ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'])) {
$position = 'center';
}
// If style is 'center', force position to center
if ($style === 'center') {
$position = 'center';
}
// Intervention/Image rotates in the opposite direction compared to our UI/CSS preview.
// To keep backend output consistent with the admin preview, invert the rotation sign.
$appliedRotation = -$rotation;
// Load image
if (!file_exists($this->imagePath)) {
return;
}
$image = Image::make($this->imagePath);
$originalWidth = $image->width();
$originalHeight = $image->height();
// Only resize very large images (over 3000px) to speed up processing
// This maintains quality for most images while improving performance
$maxDimension = 3000;
$needsResize = false;
if ($originalWidth > $maxDimension || $originalHeight > $maxDimension) {
$needsResize = true;
if ($originalWidth > $originalHeight) {
$image->resize($maxDimension, null, fn($c) => $c->aspectRatio());
} else {
$image->resize(null, $maxDimension, fn($c) => $c->aspectRatio());
}
}
// Load and prepare watermark
$watermark = Image::make($watermarkPath);
$watermarkOriginalWidth = $watermark->width();
$watermarkOriginalHeight = $watermark->height();
// Validate watermark dimensions
if ($watermarkOriginalWidth <= 0 || $watermarkOriginalHeight <= 0) {
return;
}
// Calculate watermark size based on percentage
// Ensure minimum size of 10px to prevent invisible watermarks
$watermarkWidth = max(10, $image->width() * ($size / 100));
// Also ensure it doesn't exceed image dimensions
$watermarkWidth = min($watermarkWidth, $image->width());
$watermark->resize($watermarkWidth, null, fn($c) => $c->aspectRatio());
// Store dimensions before rotation for tiling calculations
$watermarkWidthBeforeRotation = $watermark->width();
$watermarkHeightBeforeRotation = $watermark->height();
// Validate dimensions after resize
if ($watermarkWidthBeforeRotation <= 0 || $watermarkHeightBeforeRotation <= 0) {
return;
}
// Apply opacity (only if > 0, otherwise skip processing)
if ($opacity > 0) {
$watermark->opacity($opacity);
} else {
// Log::info('Watermark opacity is 0, watermark will be invisible');
}
// Rotate the watermark if needed
if ($appliedRotation != 0) {
$watermark->rotate($appliedRotation);
}
// Log::info('Watermark prepared', [
// 'original_size' => "{$watermarkOriginalWidth}x{$watermarkOriginalHeight}",
// 'resized_size' => "{$watermarkWidthBeforeRotation}x{$watermarkHeightBeforeRotation}",
// 'after_rotation_size' => "{$watermark->width()}x{$watermark->height()}",
// 'rotation' => $appliedRotation
// ]);
/**
* 🧩 Apply watermark based on style
*/
if ($style === 'tile') {
// Use dimensions before rotation for spacing calculation to ensure proper coverage
$baseSpacing = 1.5;
$xStep = (int)($watermarkWidthBeforeRotation * $baseSpacing);
$yStep = (int)($watermarkHeightBeforeRotation * $baseSpacing);
// Ensure minimum step size to prevent infinite loops
$xStep = max(1, $xStep);
$yStep = max(1, $yStep);
// Calculate number of tiles and optimize spacing if too many
$tilesX = (int)ceil($image->width() / max(1, $xStep));
$tilesY = (int)ceil($image->height() / max(1, $yStep));
$totalTiles = $tilesX * $tilesY;
// Limit to max 150 tiles for performance (adjust spacing if needed)
$maxTiles = 150;
if ($totalTiles > $maxTiles) {
$factor = sqrt($totalTiles / $maxTiles);
$xStep = max(1, (int)($xStep * $factor));
$yStep = max(1, (int)($yStep * $factor));
// Recalculate after adjustment
$tilesX = (int)ceil($image->width() / max(1, $xStep));
$tilesY = (int)ceil($image->height() / max(1, $yStep));
}
// ::info('Tiling calculation', [
// 'tiles_x' => $tilesX,
// 'tiles_y' => $tilesY,
// 'total_tiles' => $tilesX * $tilesY,
// 'x_step' => $xStep,
// Log'y_step' => $yStep
// ]);
// Apply tiles - Intervention Image can reuse the same watermark object
$tileCount = 0;
try {
for ($y = 0; $y < $image->height(); $y += $yStep) {
for ($x = 0; $x < $image->width(); $x += $xStep) {
$image->insert($watermark, 'top-left', $x, $y);
$tileCount++;
}
}
// Log::info('Tiles applied successfully', ['count' => $tileCount]);
} catch (\Exception $e) {
// Log::error('Error applying tiles', [
// 'error' => $e->getMessage(),
// 'tiles_applied' => $tileCount,
// 'trace' => $e->getTraceAsString()
// ]);
throw $e;
}
} else {
// Single watermark or center style
$padding = 10;
// Single watermark at specified position
switch ($position) {
case 'top-left':
$image->insert($watermark, 'top-left', $padding, $padding);
break;
case 'top-right':
$image->insert($watermark, 'top-right', $padding, $padding);
break;
case 'bottom-left':
$image->insert($watermark, 'bottom-left', $padding, $padding);
break;
case 'bottom-right':
$image->insert($watermark, 'bottom-right', $padding, $padding);
break;
case 'center':
default:
$image->insert($watermark, 'center');
break;
}
// ::info('Single watermark applied', [
// 'position' => $position,
// Log'style' => $style
// ]);
}
/**
* 💾 Save image (replace original)
*/
// Normalize extension for encoding (jpg -> jpeg)
$encodeFormat = strtolower($this->extension);
if ($encodeFormat === 'jpg') {
$encodeFormat = 'jpeg';
}
// Log::info('Saving watermarked image', [
// 'path' => $this->imagePath,
// 'format' => $encodeFormat,
// 'quality' => 85
// ]);
try {
$image->encode($encodeFormat, 85)->save($this->imagePath);
// Verify the file was saved
if (!file_exists($this->imagePath)) {
throw new \Exception('Image file was not saved successfully');
}
$fileSize = filesize($this->imagePath);
// Log::info('Watermark job completed successfully', [
// 'image_path' => $this->imagePath,
// 'file_size' => $fileSize,
// 'processing_time' => round(microtime(true) - $startTime, 2) . 's'
// ]);
} catch (\Exception $e) {
Log::error('Error saving watermarked image', [
'error' => $e->getMessage(),
'path' => $this->imagePath,
'format' => $encodeFormat,
'trace' => $e->getTraceAsString()
]);
throw $e;
}
} catch (\Throwable $e) {
Log::error('Error in AddWatermarkJob: ' . $e->getMessage(), [
'imagePath' => $this->imagePath,
'trace' => $e->getTraceAsString(),
]);
}
}
}