path($sourcePath); if (! file_exists($fullSrc)) { return null; } $data = @file_get_contents($fullSrc); if ($data === false) { return null; } // Prefer robust decode via GD from string (handles jpeg/png/webp if compiled) $src = @imagecreatefromstring($data); if (! $src) { return null; } $w = imagesx($src); $h = imagesy($src); if ($w === 0 || $h === 0) { imagedestroy($src); return null; } $scale = min(1.0, $maxEdge / max($w, $h)); $tw = (int) max(1, round($w * $scale)); $th = (int) max(1, round($h * $scale)); $dst = imagecreatetruecolor($tw, $th); imagecopyresampled($dst, $src, 0, 0, 0, 0, $tw, $th, $w, $h); // Ensure destination directory exists $destDir = dirname($destPath); Storage::disk($disk)->makeDirectory($destDir); $fullDest = Storage::disk($disk)->path($destPath); // Encode JPEG @imagejpeg($dst, $fullDest, $quality); imagedestroy($dst); imagedestroy($src); // Confirm file written if (! file_exists($fullDest)) { return null; } return $destPath; } catch (\Throwable $e) { // Silent failure; caller can fall back to original return null; } } /** * Apply a watermark in-place on the given disk/path. * Expects $config with keys: asset (path), position, opacity (0..1), scale (0..1), padding (px). */ public static function applyWatermarkOnDisk(string $disk, string $path, array $config): bool { $fullSrc = Storage::disk($disk)->path($path); if (! file_exists($fullSrc)) { return false; } $assetPath = $config['asset'] ?? null; if (! $assetPath) { return false; } $assetFull = null; if (Storage::disk('public')->exists($assetPath)) { $assetFull = Storage::disk('public')->path($assetPath); } elseif (file_exists($assetPath)) { $assetFull = $assetPath; } if (! $assetFull || ! file_exists($assetFull)) { return false; } try { $data = @file_get_contents($fullSrc); $src = $data !== false ? @imagecreatefromstring($data) : null; if (! $src) { return false; } $wmData = @file_get_contents($assetFull); $watermark = $wmData !== false ? @imagecreatefromstring($wmData) : null; if (! $watermark) { imagedestroy($src); return false; } imagesavealpha($src, true); imagesavealpha($watermark, true); $srcW = imagesx($src); $srcH = imagesy($src); $wmW = imagesx($watermark); $wmH = imagesy($watermark); if ($srcW <= 0 || $srcH <= 0 || $wmW <= 0 || $wmH <= 0) { imagedestroy($src); imagedestroy($watermark); return false; } $scale = max(0.05, min(1.0, (float) ($config['scale'] ?? 0.2))); $targetW = max(1, (int) round($srcW * $scale)); $targetH = max(1, (int) round($wmH * ($targetW / $wmW))); $resized = imagecreatetruecolor($targetW, $targetH); imagealphablending($resized, false); imagesavealpha($resized, true); imagecopyresampled($resized, $watermark, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH); imagedestroy($watermark); $padding = max(0, (int) ($config['padding'] ?? 0)); $position = $config['position'] ?? 'bottom-right'; $x = $padding; $y = $padding; switch ($position) { case 'top-right': $x = max(0, $srcW - $targetW - $padding); break; case 'top-center': $x = (int) max(0, ($srcW - $targetW) / 2); break; case 'middle-left': $y = (int) max(0, ($srcH - $targetH) / 2); break; case 'center': $x = (int) max(0, ($srcW - $targetW) / 2); $y = (int) max(0, ($srcH - $targetH) / 2); break; case 'middle-right': $x = max(0, $srcW - $targetW - $padding); $y = (int) max(0, ($srcH - $targetH) / 2); break; case 'bottom-left': $y = max(0, $srcH - $targetH - $padding); break; case 'bottom-center': $x = (int) max(0, ($srcW - $targetW) / 2); $y = max(0, $srcH - $targetH - $padding); break; case 'bottom-right': $x = max(0, $srcW - $targetW - $padding); $y = max(0, $srcH - $targetH - $padding); break; default: break; } $offsetX = (int) ($config['offset_x'] ?? 0); $offsetY = (int) ($config['offset_y'] ?? 0); $x = max(0, min($srcW - $targetW, $x + $offsetX)); $y = max(0, min($srcH - $targetH, $y + $offsetY)); $opacity = max(0.0, min(1.0, (float) ($config['opacity'] ?? 0.25))); if ($opacity < 1.0) { self::applyOpacity($resized, $opacity); } imagealphablending($src, true); imagecopy($src, $resized, $x, $y, 0, 0, $targetW, $targetH); imagedestroy($resized); // Overwrite original (respect mime: always JPEG for compatibility) @imagejpeg($src, $fullSrc, 90); imagedestroy($src); return true; } catch (\Throwable $e) { return false; } } /** * Copy a source to destination and apply watermark there. */ public static function copyWithWatermark(string $disk, string $sourcePath, string $destPath, array $config): ?string { if (! Storage::disk($disk)->exists($sourcePath)) { return null; } Storage::disk($disk)->makeDirectory(dirname($destPath)); Storage::disk($disk)->copy($sourcePath, $destPath); $applied = self::applyWatermarkOnDisk($disk, $destPath, $config); return $applied ? $destPath : null; } /** * @param \GdImage|resource $image */ private static function applyOpacity($image, float $opacity): void { $width = imagesx($image); $height = imagesy($image); if ($width <= 0 || $height <= 0) { return; } imagealphablending($image, false); imagesavealpha($image, true); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgba = imagecolorat($image, $x, $y); $alpha = ($rgba >> 24) & 0x7F; $red = ($rgba >> 16) & 0xFF; $green = ($rgba >> 8) & 0xFF; $blue = $rgba & 0xFF; $adjustedAlpha = (int) round(127 - (127 - $alpha) * $opacity); $color = imagecolorallocatealpha($image, $red, $green, $blue, max(0, min(127, $adjustedAlpha))); imagesetpixel($image, $x, $y, $color); } } } }