imagecopymerge():图片水印,支持透明度 (0-100)imagecopy():图片水印,保留源图 Alpha 通道 (PNG 透明)imagettftext():文字水印,需 TTF 字体<?php
/**
* GD 图片水印处理器
* 支持:图片水印、文字水印、九宫格位置、透明度、平铺
*/
class ImageWatermark
{
private $srcImage;
private $width;
private $height;
private $mime;
public function __construct(string $sourcePath)
{
if (!file_exists($sourcePath)) {
throw new \InvalidArgumentException("源文件不存在: $sourcePath");
}
$info = getimagesize($sourcePath);
$this->width = $info[0];
$this->height = $info[1];
$this->mime = $info['mime'];
$this->srcImage = $this->createFromSource($sourcePath);
if (!$this->srcImage) {
throw new \RuntimeException("无法创建图像资源: $sourcePath");
}
}
/** 添加图片水印 */
public function addImageWatermark(
string $watermarkPath,
int $position = 9, // 1-9 九宫格,5=中心
int $opacity = 50, // 0-100 透明度 (仅 imagecopymerge)
int $offsetX = 10,
int $offsetY = 10
): self {
$wmInfo = getimagesize($watermarkPath);
$wmWidth = $wmInfo[0];
$wmHeight = $wmInfo[1];
$wmMime = $wmInfo['mime'];
$wmResource = $this->createFromSource($watermarkPath);
if (!$wmResource) throw new \RuntimeException("水印图片加载失败");
// 计算坐标
[$destX, $destY] = $this->calcPosition($wmWidth, $wmHeight, $position, $offsetX, $offsetY);
// 判断水印是否带 Alpha 通道 (PNG/GIF)
$hasAlpha = in_array($wmMime, ['image/png', 'image/gif']);
if ($hasAlpha && $opacity >= 100) {
// 保留原透明度,直接复制
imagecopy($this->srcImage, $wmResource, $destX, $destY, 0, 0, $wmWidth, $wmHeight);
} else {
// 使用 merge 控制整体透明度
imagecopymerge($this->srcImage, $wmResource, $destX, $destY, 0, 0, $wmWidth, $wmHeight, $opacity);
}
imagedestroy($wmResource);
return $this;
}
/** 添加文字水印 */
public function addTextWatermark(
string $text,
string $fontPath,
int $fontSize = 20,
array $color = [255, 255, 255],
int $position = 9,
int $opacity = 80,
int $offsetX = 10,
int $offsetY = 10,
int $angle = 0
): self {
if (!file_exists($fontPath)) throw new \InvalidArgumentException("字体文件不存在: $fontPath");
// 创建临时图层绘制文字 (支持透明度)
$textBox = imagettfbbox($fontSize, $angle, $fontPath, $text);
$textWidth = $textBox[2] - $textBox[0];
$textHeight = $textBox[1] - $textBox[7];
[$destX, $destY] = $this->calcPosition($textWidth, $textHeight, $position, $offsetX, $offsetY);
// 分配颜色
$textColor = imagecolorallocatealpha($this->srcImage, $color[0], $color[1], $color[2], 127 - (int)($opacity * 1.27));
// 绘制 (基线坐标调整)
$baselineY = $destY + $textHeight;
imagettftext($this->srcImage, $fontSize, $angle, $destX, $baselineY, $textColor, $fontPath, $text);
return $this;
}
/** 平铺水印 (防盗图) */
public function addTiledWatermark(string $watermarkPath, int $opacity = 30, int $spacing = 50): self
{
$wmInfo = getimagesize($watermarkPath);
$wmWidth = $wmInfo[0];
$wmHeight = $wmInfo[1];
$wmResource = $this->createFromSource($watermarkPath);
for ($y = 0; $y < $this->height; $y += $wmHeight + $spacing) {
for ($x = 0; $x < $this->width; $x += $wmWidth + $spacing) {
imagecopymerge($this->srcImage, $wmResource, $x, $y, 0, 0, $wmWidth, $wmHeight, $opacity);
}
}
imagedestroy($wmResource);
return $this;
}
/** 保存/输出 */
public function save(string $destPath, int $quality = 90): bool
{
$dir = dirname($destPath);
if (!is_dir($dir)) mkdir($dir, 0755, true);
switch ($this->mime) {
case 'image/jpeg':
return imagejpeg($this->srcImage, $destPath, $quality);
case 'image/png':
return imagepng($this->srcImage, $destPath, 9); // 压缩级别 0-9
case 'image/gif':
return imagegif($this->srcImage, $destPath);
case 'image/webp':
return imagewebp($this->srcImage, $destPath, $quality);
default:
throw new \RuntimeException("不支持的输出格式: {$this->mime}");
}
}
public function output(int $quality = 90): void
{
header('Content-Type: ' . $this->mime);
switch ($this->mime) {
case 'image/jpeg': imagejpeg($this->srcImage, null, $quality); break;
case 'image/png': imagepng($this->srcImage, null, 9); break;
case 'image/gif': imagegif($this->srcImage); break;
case 'image/webp': imagewebp($this->srcImage, null, $quality); break;
}
}
public function __destruct()
{
if ($this->srcImage) imagedestroy($this->srcImage);
}
// ===== 私有方法 =====
private function createFromSource(string $path)
{
switch ($this->mime ?? mime_content_type($path)) {
case 'image/jpeg': return imagecreatefromjpeg($path);
case 'image/png': return imagecreatefrompng($path);
case 'image/gif': return imagecreatefromgif($path);
case 'image/webp': return imagecreatefromwebp($path);
case 'image/bmp': return imagecreatefrombmp($path);
default: return false;
}
}
private function calcPosition(int $wmW, int $wmH, int $pos, int $offX, int $offY): array
{
// 九宫格: 1左上 2上中 3右上 4左中 5中心 6右中 7左下 8下中 9右下
$x = $offX;
$y = $offY;
switch ($pos) {
case 1: $x = $offX; $y = $offY; break;
case 2: $x = ($this->width - $wmW) / 2; $y = $offY; break;
case 3: $x = $this->width - $wmW - $offX; $y = $offY; break;
case 4: $x = $offX; $y = ($this->height - $wmH) / 2; break;
case 5: $x = ($this->width - $wmW) / 2; $y = ($this->height - $wmH) / 2; break;
case 6: $x = $this->width - $wmW - $offX; $y = ($this->height - $wmH) / 2; break;
case 7: $x = $offX; $y = $this->height - $wmH - $offY; break;
case 8: $x = ($this->width - $wmW) / 2; $y = $this->height - $wmH - $offY; break;
case 9: $x = $this->width - $wmW - $offX; $y = $this->height - $wmH - $offY; break;
}
return [max(0, (int)$x), max(0, (int)$y)];
}
}
<?php
require_once 'ImageWatermark.php';
// 1. 单图片水印 (右下角,50% 透明度)
$watermark = new ImageWatermark('uploads/photo.jpg');
$watermark->addImageWatermark('assets/logo.png', 9, 50, 20, 20)
->save('uploads/photo_wm.jpg');
// 2. 文字水印 (中心,白色,80% 透明度,旋转 -30°)
$watermark2 = new ImageWatermark('uploads/photo.jpg');
$watermark2->addTextWatermark(
'© 2024 MySite.com',
'fonts/msyh.ttc',
36,
[255, 255, 255],
5, // 中心
80,
0, 0,
-30 // 旋转角度
)->output(); // 直接浏览器输出
// 3. 平铺水印 (全覆盖,30% 透明度)
$watermark3 = new ImageWatermark('uploads/photo.jpg');
$watermark3->addTiledWatermark('assets/logo_small.png', 30, 100)
->save('uploads/photo_tiled.jpg');
| 场景 | 函数选择 | 注意事项 |
|---|---|---|
| 水印图片本身透明 (PNG) | imagecopy() | 保留 Alpha 通道,opacity 参数无效 |
| 水印图片不透明 (JPG) 需整体半透明 | imagecopymerge() | opacity 0-100,100 为不透明 |
| 文字水印带透明度 | imagecolorallocatealpha() | alpha 0=不透明, 127=全透明,需换算 |
| 大图片内存溢出 | 分块处理 / 升级 PHP / 用 ImageMagick | GD 全内存解码,5000px 以上慎用 |
GD 水印核心是 imagecopymerge (不透明源+透明度) 与 imagecopy (透明源) 的选择。封装为类后,链式调用支持图片/文字/平铺多种模式组合。生产环境大图/高并发建议迁移到 ImageMagick (imagick 扩展),支持流式处理、更低内存、更丰富特效。