<?php
/**
* 為圖片添加文字水印
* @param string $srcPath 來源圖路徑
* @param string $dstPath 目標儲存路徑
* @param string $text 水印文字 (支援多行 \n)
* @param array $options 選項
* @return bool
*/
function addTextWatermark(string $srcPath, string $dstPath, string $text, array $options = []): bool {
$opts = array_merge([
'font_path' => __DIR__ . '/fonts/NotoSansTC-Regular.otf', // 必須提供支援中文的 TTF/OTF
'font_size' => 24,
'font_color' => [255, 255, 255, 128], // RGBA, 128=50% 透明
'position' => 'bottom-right', // top-left, top-right, bottom-left, bottom-right, center, tile
'margin' => 10,
'angle' => 0,
'shadow' => true,
'shadow_color' => [0, 0, 0, 100],
'shadow_offset' => [2, 2],
'line_height' => 1.4,
], $options);
if (!is_file($srcPath) || !is_file($opts['font_path'])) return false;
$info = getimagesize($srcPath);
if (!$info) return false;
[$w, $h, $mime] = $info;
$srcImg = match ($mime) {
IMAGETYPE_JPEG => imagecreatefromjpeg($srcPath),
IMAGETYPE_PNG => imagecreatefrompng($srcPath),
IMAGETYPE_GIF => imagecreatefromgif($srcPath),
IMAGETYPE_WEBP => imagecreatefromwebp($srcPath),
default => null,
};
if (!$srcImg) return false;
// 計算文字區塊大小
$lines = explode("\n", $text);
$box = [0, 0];
foreach ($lines as $line) {
$b = imagettfbbox($opts['font_size'], $opts['angle'], $opts['font_path'], $line);
$box[0] = max($box[0], $b[2] - $b[0]); // 寬
$box[1] += ($b[1] - $b[7]) * $opts['line_height']; // 高累加
}
$textW = $box[0];
$textH = $box[1];
// 計算起始座標
[$x, $y] = match ($opts['position']) {
'top-left' => [$opts['margin'], $opts['margin'] + $textH],
'top-right' => [$w - $textW - $opts['margin'], $opts['margin'] + $textH],
'bottom-left' => [$opts['margin'], $h - $opts['margin']],
'bottom-right' => [$w - $textW - $opts['margin'], $h - $opts['margin']],
'center' => [($w - $textW) / 2, ($h + $textH) / 2],
'tile' => null, // 特殊處理
default => [$opts['margin'], $h - $opts['margin']],
};
// 分配顏色
$color = imagecolorallocatealpha($srcImg, ...$opts['font_color']);
$shadowColor = $opts['shadow'] ? imagecolorallocatealpha($srcImg, ...$opts['shadow_color']) : null;
// 繪製 (支援 tile 平鋪)
if ($opts['position'] === 'tile') {
for ($ty = -$textH; $ty < $h + $textH; $ty += $textH * 2) {
for ($tx = -$textW; $tx < $w + $textW; $tx += $textW * 2) {
foreach ($lines as $i => $line) {
$ly = $ty + $i * ($textH / count($lines)) * $opts['line_height'];
if ($shadowColor) imagettftext($srcImg, $opts['font_size'], $opts['angle'], $tx + $opts['shadow_offset'][0], $ly + $opts['shadow_offset'][1], $shadowColor, $opts['font_path'], $line);
imagettftext($srcImg, $opts['font_size'], $opts['angle'], $tx, $ly, $color, $opts['font_path'], $line);
}
}
}
} else {
foreach ($lines as $i => $line) {
$ly = $y + $i * ($textH / count($lines)) * $opts['line_height'];
if ($shadowColor) imagettftext($srcImg, $opts['font_size'], $opts['angle'], $x + $opts['shadow_offset'][0], $ly + $opts['shadow_offset'][1], $shadowColor, $opts['font_path'], $line);
imagettftext($srcImg, $opts['font_size'], $opts['angle'], $x, $ly, $color, $opts['font_path'], $line);
}
}
// 輸出 (保持原格式)
$ok = match ($mime) {
IMAGETYPE_JPEG => imagejpeg($srcImg, $dstPath, 90),
IMAGETYPE_PNG => imagepng($srcImg, $dstPath, 9),
IMAGETYPE_GIF => imagegif($srcImg, $dstPath),
IMAGETYPE_WEBP => imagewebp($srcImg, $dstPath, 90),
default => false,
};
imagedestroy($srcImg);
return $ok;
}
?>
| 問題 | 解決 |
|---|---|
| 中文顯示方框/亂碼 | **必須指定支援中文的字體檔** (Noto Sans TC, Source Han Sans, 微軟正黑體)。`imagettftext` 不支援字體回退。 |
| 字體路徑錯誤 | 使用絕對路徑,`__DIR__ . '/fonts/...'`,檢查 `is_file()`。 |
| 透明度不生效 | PNG/GIF/WebP 需 `imagealphablending($img, true); imagesavealpha($img, true);`;JPEG 不支援透明度 (會變黑底),水印色建議不透明。 |
| 文字座標基準點 | `imagettftext` 的 `$y` 是 **baseline (基線)**,非文字頂部。需加上字體上升高度 (從 `imagettfbbox` 計算)。 |
| 記憶體耗盡 | 大圖處理前 `ini_set('memory_limit', '512M')`,或改用 Imagick。 |
composer require intervention/image
<?php
use Intervention\Image\ImageManagerStatic as Image;
Image::make('photo.jpg')
->text('© MySite 2024', 800, 600, function ($font) {
$font->file(__DIR__ . '/fonts/NotoSansTC-Regular.otf');
$font->size(24);
$font->color('#ffffff80'); // 8位十六進位含 Alpha
$font->align('right');
$font->valign('bottom');
})
->save('watermarked.jpg');
// 圖片水印 (Logo)
Image::make('photo.jpg')
->insert('logo.png', 'bottom-right', 10, 10)
->save('watermarked2.jpg');
?>
GD 原生水印需手動處理**字體載入、座標計算、Baseline 校正、透明度、多行、平鋪**,極易出錯。**生產環境請直接使用 Intervention Image**,一行鏈式呼叫解決所有細節,支援 GD/Imagick 雙驅動無縫切換。