247 lines
8.0 KiB
PHP
247 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class ImageHelper
|
|
{
|
|
/**
|
|
* Create a JPEG thumbnail for a file stored on a given disk.
|
|
* Returns the relative path (on the same disk) or null on failure.
|
|
*/
|
|
public static function makeThumbnailOnDisk(string $disk, string $sourcePath, string $destPath, int $maxEdge = 600, int $quality = 82): ?string
|
|
{
|
|
try {
|
|
$fullSrc = Storage::disk($disk)->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);
|
|
}
|
|
}
|
|
}
|
|
}
|