UEditor 後端預設不支援上傳圖片自動加水印。原文採用修改核心類 Uploader.class.php 並硬編碼水印參數的方式,存在侵入性強、升級困難、配置硬編碼、路徑耦合等工程化問題。
interface WatermarkStrategyInterface {
public function apply(string $sourcePath, string $targetPath, array $config): bool;
public function supports(string $mimeType): bool;
}
class GdTextWatermark implements WatermarkStrategyInterface {
public function apply(string $source, string $target, array $config): bool {
$image = $this->createImageFromFile($source);
if (!$image) return false;
$font = $config['font_path'] ?? __DIR__ . '/fonts/msyh.ttc';
$text = $config['text'] ?? 'Copyright';
$color = $config['color'] ?? [255, 0, 0];
$size = $config['size'] ?? 20;
$angle = $config['angle'] ?? 0;
$opacity = $config['opacity'] ?? 50;
$position = $config['position'] ?? 'bottom-right';
$margin = $config['margin'] ?? 20;
$box = imagettfbbox($size, $angle, $font, $text);
$textW = $box[2] - $box[0];
$textH = $box[7] - $box[1];
[$x, $y] = $this->calcPosition($position, imagesx($image), imagesy($image), $textW, $textH, $margin);
$colorAllocated = imagecolorallocatealpha($image, ...$color, 127 - (int)($opacity * 1.27));
imagettftext($image, $size, $angle, $x, $y, $colorAllocated, $font, $text);
return $this->saveImage($image, $target, $source);
}
// ... 私有輔助方法:createImageFromFile, calcPosition, saveImage
}
class GdImageWatermark implements WatermarkStrategyInterface {
public function apply(string $source, string $target, array $config): bool {
$main = $this->createImageFromFile($source);
$watermark = $this->createImageFromFile($config['watermark_image']);
if (!$main || !$watermark) return false;
$position = $config['position'] ?? 'bottom-right';
$opacity = $config['opacity'] ?? 50;
$margin = $config['margin'] ?? 20;
[$x, $y] = $this->calcPosition($position, imagesx($main), imagesy($main), imagesx($watermark), imagesy($watermark), $margin);
if ($config['blend'] ?? true) {
imagecopymerge($main, $watermark, $x, $y, 0, 0, imagesx($watermark), imagesy($watermark), $opacity);
} else {
imagecopy($main, $watermark, $x, $y, 0, 0, imagesx($watermark), imagesy($watermark));
}
return $this->saveImage($main, $target, $source);
}
}
class ImagickWatermark implements WatermarkStrategyInterface {
public function apply(string $source, string $target, array $config): bool {
try {
$img = new \Imagick($source);
$wm = new \Imagick($config['watermark_image']);
$wm->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $config['opacity'] / 100, \Imagick::CHANNEL_ALPHA);
$img->compositeImage($wm, \Imagick::COMPOSITE_OVER, ...$this->calcPosition($config, $img, $wm));
$img->writeImage($target);
return true;
} catch (\ImagickException $e) {
error_log("Watermark failed: {$e->getMessage()}");
return false;
}
}
}
// config.json 新增配置段
{
"imageWatermark": {
"enabled": true,
"strategy": "imagick", // gd_text, gd_image, imagick
"config": {
"watermark_image": "/static/watermark/logo.png",
"position": "bottom-right",
"opacity": 30,
"margin": 10
}
}
}
// 事件監聽器(不修改核心類)
class UEditorWatermarkListener {
public function onUploadComplete(array $fileInfo, array $config): void {
if (empty($config['imageWatermark']['enabled'])) return;
if (!in_array($fileInfo['mime'], ['image/jpeg', 'image/png', 'image/gif', 'image/webp'])) return;
$strategy = $this->resolveStrategy($config['imageWatermark']['strategy']);
$strategy->apply($fileInfo['path'], $fileInfo['path'], $config['imageWatermark']['config']);
}
}
// config/watermark.php
return [
'default' => env('WATERMARK_DRIVER', 'imagick'),
'drivers' => [
'gd_text' => [
'class' => GdTextWatermark::class,
'font_path' => base_path('resources/fonts/msyh.ttc'),
'text' => env('WATERMARK_TEXT', '© MySite'),
'color' => [255, 255, 255],
'size' => 18,
'opacity' => 40,
],
'imagick' => [
'class' => ImagickWatermark::class,
'watermark_image' => public_path('watermark/logo.png'),
'position' => 'bottom-right',
'opacity' => 25,
],
],
'positions' => [
'top-left' => [10, 10],
'top-right' => ['width - wm_width - 10', 10],
'center' => ['(width - wm_width) / 2', '(height - wm_height) / 2'],
'bottom-right' => ['width - wm_width - 10', 'height - wm_height - 10'],
],
];
filename_wm.ext,保留原圖供後續調整採用策略模式 + 事件監聽器 + 配置驅動的架構,實現零侵入式水印擴充。ImageMagick 為首選引擎,配合佇列非同步處理與原圖保留策略,滿足高併發、可維護、可擴充的生產級需求。