前端 Canvas 可加水印,但用戶可繞過前端直接下載原圖。服務端(PHP GD/ImageMagick)處理上傳/輸出時強制添加水印,是保護圖片版權、品牌曝光的必要防線。本文提供圖片水印、文字水印、透明度控制、九宮格位置、批量處理的完整封裝類。
class Watermarker {
private $sourcePath;
private $waterPath; // 水印圖片路徑
private $waterText; // 水印文字
private $fontPath; // TTF 字體路徑
private $fontSize = 20;
private $fontColor = [255,255,255]; // RGB
private $opacity = 50; // 0-100
private $position = 8; // 1-9 九宮格,5=居中
private $offset = 10; // 邊距
private $outputPath;
private $quality = 90; // JPEG 質量
public function __construct($sourcePath) {
$this->sourcePath = $sourcePath;
}
// 鏈式設置
public function setWaterImage($path) { $this->waterPath = $path; return $this; }
public function setWaterText($text, $fontPath) { $this->waterText = $text; $this->fontPath = $fontPath; return $this; }
public function setOpacity($opacity) { $this->opacity = max(0, min(100, $opacity)); return $this; }
public function setPosition($pos) { $this->position = max(1, min(9, $pos)); return $this; }
public function setOffset($offset) { $this->offset = $offset; return $this; }
public function setFont($size, $color) { $this->fontSize = $size; $this->fontColor = $color; return $this; }
public function setOutput($path, $quality = 90) { $this->outputPath = $path; $this->quality = $quality; return $this; }
public function apply() {
// 1. 載入源圖(自動檢測類型)
$srcInfo = getimagesize($this->sourcePath);
if (!$srcInfo) throw new Exception('源圖不存在或格式不支持');
$srcMime = $srcInfo['mime'];
$createFunc = 'imagecreatefrom' . ($srcMime === 'image/jpeg' ? 'jpeg' : ($srcMime === 'image/png' ? 'png' : 'gif'));
if (!function_exists($createFunc)) throw new Exception('GD 不支持該格式: ' . $srcMime);
$srcImg = $createFunc($this->sourcePath);
$srcW = imagesx($srcImg); $srcH = imagesy($srcImg);
// 2. 準備水印圖層
if ($this->waterPath) {
$waterInfo = getimagesize($this->waterPath);
$waterCreate = 'imagecreatefrom' . ($waterInfo['mime'] === 'image/jpeg' ? 'jpeg' : ($waterInfo['mime'] === 'image/png' ? 'png' : 'gif'));
$waterImg = $waterCreate($this->waterPath);
$waterW = imagesx($waterImg); $waterH = imagesy($waterImg);
} elseif ($this->waterText) {
if (!$this->fontPath || !file_exists($this->fontPath)) throw new Exception('字體文件不存在');
// 計算文字邊界框
$box = imagettfbbox($this->fontSize, 0, $this->fontPath, $this->waterText);
$waterW = $box[2] - $box[0]; $waterH = $box[3] - $box[5];
// 創建透明畫布繪製文字
$waterImg = imagecreatetruecolor($waterW, $waterH);
imagesavealpha($waterImg, true);
$transparent = imagecolorallocatealpha($waterImg, 0, 0, 0, 127);
imagefill($waterImg, 0, 0, $transparent);
$textColor = imagecolorallocatealpha($waterImg, $this->fontColor[0], $this->fontColor[1], $this->fontColor[2], 127 * (100 - $this->opacity) / 100);
imagettftext($waterImg, $this->fontSize, 0, 0, $waterH - $box[5], $textColor, $this->fontPath, $this->waterText);
} else {
throw new Exception('必須設置水印圖片或文字');
}
// 3. 計算貼合位置 (九宮格)
$posMap = [
1 => [$this->offset, $this->offset], // 左上
2 => [($srcW - $waterW) / 2, $this->offset], // 頂中
3 => [$srcW - $waterW - $this->offset, $this->offset], // 右上
4 => [$this->offset, ($srcH - $waterH) / 2], // 左中
5 => [($srcW - $waterW) / 2, ($srcH - $waterH) / 2], // 居中
6 => [$srcW - $waterW - $this->offset, ($srcH - $waterH) / 2], // 右中
7 => [$this->offset, $srcH - $waterH - $this->offset], // 左下
8 => [($srcW - $waterW) / 2, $srcH - $waterH - $this->offset], // 底中 (默認)
9 => [$srcW - $waterW - $this->offset, $srcH - $waterH - $this->offset],// 右下
];
list($dstX, $dstY) = $posMap[$this->position];
$dstX = max(0, min($srcW - $waterW, (int)$dstX));
$dstY = max(0, min($srcH - $waterH, (int)$dstY));
// 4. 合成:開啟 Alpha 混合
imagealphablending($srcImg, true);
imagesavealpha($srcImg, true);
// 水印圖片自身透明度已在創建時設置,此處直接 copy
imagecopy($srcImg, $waterImg, $dstX, $dstY, 0, 0, $waterW, $waterH);
// 5. 輸出
$outPath = $this->outputPath ?? $this->sourcePath;
$outMime = $srcMime;
if (preg_match('/\.jpe?g$/i', $outPath)) $outMime = 'image/jpeg';
elseif (preg_match('/\.png$/i', $outPath)) $outMime = 'image/png';
elseif (preg_match('/\.gif$/i', $outPath)) $outMime = 'image/gif';
$outputFunc = 'image' . ($outMime === 'image/jpeg' ? 'jpeg' : ($outMime === 'image/png' ? 'png' : 'gif'));
if ($outMime === 'image/jpeg') {
$outputFunc($srcImg, $outPath, $this->quality);
} else {
$outputFunc($srcImg, $outPath);
}
// 6. 清理
imagedestroy($srcImg);
imagedestroy($waterImg);
return $outPath;
}
}
(new Watermarker('uploads/photo.jpg'))
->setWaterImage('assets/logo.png')
->setOpacity(30)
->setPosition(9) // 右下
->setOffset(20)
->setOutput('watermarked/photo.jpg')
->apply();
(new Watermarker('uploads/photo.jpg'))
->setWaterText('© 2024 MySite.com', 'fonts/NotoSansSC-Regular.otf')
->setFont(24, [255,255,255])
->setOpacity(50)
->setPosition(5) // 居中
->setOutput('watermarked/photo.jpg')
->apply();
foreach (glob('uploads/*.{jpg,png}', GLOB_BRACE) as $file) {
$out = 'watermarked/' . basename($file);
try {
(new Watermarker($file))
->setWaterImage('assets/watermark.png')
->setPosition(8)
->setOutput($out)
->apply();
echo "OK: $file\n";
} catch (Exception $e) {
echo "FAIL: $file - " . $e->getMessage() . "\n";
}
}
| 技術點 | 實現細節 | 避坑提示 |
|---|---|---|
| 透明度控制 | 水印圖層創建時用 imagecolorallocatealpha 設置 alpha 通道 (0-127),而非 imagecopymerge(後者不支持 Alpha 通道疊加) | 圖片水印需確保源文件含 Alpha 通道(PNG),JPEG 無透明度 |
| 九宮格定位 | 預計算 9 個坐標,邊界檢查防止越界 | 圖片較小時水印可能被裁剪,建議按比例縮放水印 |
| 文字繪製 | imagettfbbox 獲取邊界框 → 創建透明畫布 → imagettftext 繪製 → 作為水印層合成 | 字體路徑需絕對路徑;中文必須用支持 CJK 的 TTF/OTF 字體 |
| 格式自動識別 | getimagesize() 獲取 MIME,動態調用對應 imagecreatefrom* / image* | 輸出格式建議與源一致,或統一轉 WebP (需 GD 支持) |
| 內存管理 | 及時 imagedestroy() 釋放 GD 資源,防止大批量處理 OOM | 單張大圖 (如 4000x3000) 佔用 ~48MB 內存,注意 memory_limit |
// 需安裝 php-imagick 擴展
$imagick = new Imagick('photo.jpg');
$watermark = new Imagick('logo.png');
$watermark->evaluateImage(Imagick::EVALUATE_MULTIPLY, 0.3, Imagick::ALPHA_CHANNEL); // 30% 不透明度
$imagick->compositeImage($watermark, Imagick::COMPOSITE_OVER, 100, 100); // 底中
$imagick->writeImage('watermarked.jpg');
ImageMagick 支持向量水印 (SVG)、ICC 色彩配置文件、並行處理,高併發場景推薦。
PHP GD 庫雖屬基礎擴展,但功能完備,封裝得當即可構建生產級水印服務。核心在於Alpha 通道正確混合、九宮格靈活定位、資源及時釋放。建議將水印邏輯封裝為獨立服務/隊列任務,上傳時異步處理,返回原圖 URL,水印完成後通知前端更新,兼顧用戶體驗與版權保護。