<?php
namespace App\Utils;
class ImageProcessor {
private string $srcPath;
private array $info = []; // [width, height, type, mime]
private resource $srcImg = null;
public function __construct(string $sourcePath) {
if (!is_file($sourcePath)) throw new \InvalidArgumentException("File not found: $sourcePath");
$this->srcPath = $sourcePath;
$this->loadInfo();
}
private function loadInfo(): void {
$this->info = getimagesize($this->srcPath);
if (!$this->info) throw new \RuntimeException('Not a valid image');
[$this->info['width'], $this->info['height'], $this->info['type']] = $this->info;
$this->info['mime'] = image_type_to_mime_type($this->info['type']);
}
private function createSource(): void {
if ($this->srcImg) return;
$create = 'imagecreatefrom' . ($this->info['type'] === IMAGETYPE_JPEG ? 'jpeg' : image_type_to_extension($this->info['type'], false));
$this->srcImg = $create($this->srcPath);
if (!$this->srcImg) throw new \RuntimeException('Failed to create image resource');
}
// ===== 公共 API =====
/**
* 生成縮略圖
* @param int $maxW 最大寬
* @param int $maxH 最大高
* @param int $mode 0=等比縮放(含邊框), 1=裁剪填滿, 2=等比縮放(無邊框)
* @return string 保存路徑
*/
public function thumb(int $maxW, int $maxH, int $mode = 0, string $saveDir = ''): string {
$this->createSource();
$srcW = $this->info['width'];
$srcH = $this->info['height'];
// 計算目標尺寸與裁剪座標
if ($mode === 1) { // 裁剪填滿
$scale = max($maxW / $srcW, $maxH / $srcH);
$dstW = $maxW;
$dstH = $maxH;
$srcX = ($srcW - $dstW / $scale) / 2;
$srcY = ($srcH - $dstH / $scale) / 2;
$srcW = $dstW / $scale;
$srcH = $dstH / $scale;
} elseif ($mode === 0) { // 等比含邊框
$scale = min($maxW / $srcW, $maxH / $srcH);
$dstW = (int)($srcW * $scale);
$dstH = (int)($srcH * $scale);
$srcX = $srcY = 0;
} else { // 等比無邊框
$scale = min($maxW / $srcW, $maxH / $srcH);
$dstW = (int)($srcW * $scale);
$dstH = (int)($srcH * $scale);
$srcX = $srcY = 0;
}
$dstImg = imagecreatetruecolor($dstW, $dstH);
// 保持透明度
if (in_array($this->info['type'], [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
imagealphablending($dstImg, false);
imagesavealpha($dstImg, true);
$transparent = imagecolorallocatealpha($dstImg, 0, 0, 0, 127);
imagefill($dstImg, 0, 0, $transparent);
}
imagecopyresampled($dstImg, $this->srcImg, 0, 0, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
return $this->save($dstImg, $saveDir, "thumb_{$maxW}x{$maxH}");
}
/**
* 添加圖片水印
* @param string $waterPath 水印圖片路徑
* @param int $position 0-8 九宮格位置
* @param int $alpha 0-100 透明度
* @return string
*/
public function watermark(string $waterPath, int $position = 4, int $alpha = 50): string {
$this->createSource();
$waterInfo = getimagesize($waterPath);
if (!$waterInfo) throw new \InvalidArgumentException('Invalid watermark image');
$waterImg = ('imagecreatefrom' . ($waterInfo[2] === IMAGETYPE_JPEG ? 'jpeg' : image_type_to_extension($waterInfo[2], false)))($waterPath);
$wW = $waterInfo[0];
$wH = $waterInfo[1];
// 計算位置
$srcW = $this->info['width'];
$srcH = $this->info['height'];
$positions = [
0 => [0, 0], // 左上
1 => [$srcW - $wW, 0], // 右上
2 => [0, $srcH - $wH], // 左下
3 => [$srcW - $wW, $srcH - $wH],// 右下
4 => [($srcW - $wW) / 2, ($srcH - $wH) / 2], // 居中
5 => [($srcW - $wW) / 2, 0], // 上中
6 => [($srcW - $wW) / 2, $srcH - $wH], // 下中
7 => [0, ($srcH - $wH) / 2], // 左中
8 => [$srcW - $wW, ($srcH - $wH) / 2], // 右中
];
[$dstX, $dstY] = $positions[$position] ?? $positions[4];
// 複製水印(支持透明通道)
imagealphablending($waterImg, true);
imagecopymerge($this->srcImg, $waterImg, $dstX, $dstY, 0, 0, $wW, $wH, $alpha);
return $this->save($this->srcImg, '', 'watermarked');
}
/**
* 添加文字水印
*/
public function textWatermark(string $text, string $fontPath, int $size = 20, array $color = [255,255,255], int $alpha = 50, int $angle = 0, int $position = 4): string {
$this->createSource();
$box = imagettfbbox($size, $angle, $fontPath, $text);
$tW = $box[2] - $box[0];
$tH = $box[1] - $box[7];
$srcW = $this->info['width'];
$srcH = $this->info['height'];
// 位置計算同 watermark()...
// 簡化:居中
$x = ($srcW - $tW) / 2;
$y = ($srcH + $tH) / 2;
$col = imagecolorallocatealpha($this->srcImg, $color[0], $color[1], $color[2], 127 - (int)($alpha * 1.27));
imagettftext($this->srcImg, $size, $angle, $x, $y, $col, $fontPath, $text);
return $this->save($this->srcImg, '', 'text_watermarked');
}
// ===== 內部保存 =====
private function save(resource $img, string $saveDir, string $suffix): string {
$ext = image_type_to_extension($this->info['type'], false);
$ext = $ext === 'jpeg' ? 'jpg' : $ext;
$baseName = pathinfo($this->srcPath, PATHINFO_FILENAME);
$fileName = $baseName . '_' . $suffix . '.' . $ext;
$dir = $saveDir ?: dirname($this->srcPath);
$dir = rtrim($dir, '/') . '/';
if (!is_dir($dir)) mkdir($dir, 0755, true);
$fullPath = $dir . $fileName;
$saveFunc = 'image' . ($ext === 'jpg' ? 'jpeg' : $ext);
$saveFunc($img, $fullPath, $ext === 'jpg' ? 90 : ($ext === 'png' ? 9 : 0));
imagedestroy($img);
return $fullPath;
}
public function __destruct() {
if ($this->srcImg) imagedestroy($this->srcImg);
}
}
require 'vendor/autoload.php';
use App\Utils\ImageProcessor;
$processor = new ImageProcessor('uploads/original/photo.jpg');
// 1. 生成三種規格縮略圖
$small = $processor->thumb(100, 100, 1); // 裁剪 100x100
$medium = $processor->thumb(400, 300, 0); // 等比 400x300 內
$large = $processor->thumb(800, 600, 2); // 等比 800x600 內
// 2. 添加圖片水印(右下角,透明度 30%)
$watermarked = $processor->watermark('assets/watermark.png', 3, 30);
// 3. 添加文字水印
$texted = $processor->textWatermark('© MySite', 'fonts/msyh.ttc', 24, [255,255,255], 50, 0, 4);
echo "完成:$small, $medium, $large, $watermarked, $texted";
GD 原生不支持 GIF 幀合成。需引入 `gifsicle` 命令行工具或 `php-gif-encoder` 庫逐幀處理:
// 簡易調用 gifsicle(需服務器安裝)
exec("gifsicle --multifile -O3 -i input.gif -o output.gif --watermark watermark.png --position bottom-right");