<?php/** * PHP图片处理工具类(生产级) * 支持:压缩、水印、裁剪、WebP转换 * 依赖:GD库(PHP>=7.0) * 优化:PNG透明防黑底、路径遍历安全、资源自动释放、全异常防护 */classImageHandler{ // 原图片路径 private $srcPath; // 图片资源 private $image; // 图片类型(jpg/png/gif/webp) private $type; // 图片宽高 private $width; private $height; // 压缩质量 private $quality; // 允许的图片存储根目录(限制路径遍历) private $allowedRootDir; /** * 初始化图片 * @param string $srcPath 图片路径 * @param string $allowedRootDir 允许的根目录(默认当前目录,限制路径遍历) * @throws Exception 图片不存在/不支持/路径非法 */ public function __construct(string $srcPath, string $allowedRootDir = __DIR__) { // 优化:路径规范化,防止路径遍历攻击 $this->allowedRootDir = realpath($allowedRootDir); if($this->allowedRootDir === false) { throw new Exception('允许的根目录不存在:' . $allowedRootDir); } // 规范化源路径,解析真实路径 $realSrcPath = realpath($srcPath); if($realSrcPath === false) { throw new Exception('图片文件不存在或路径非法:' . $srcPath); } // 检查路径是否在允许的根目录内(防止../../etc/passwd等遍历) if(strpos($realSrcPath, $this->allowedRootDir) !== 0) { throw new Exception('非法路径:不允许访问根目录外的文件'); } $this->srcPath = $realSrcPath; // 获取图片信息 $info = getimagesize($this->srcPath); if(!$info || !isset($info[2])) { throw new Exception('无法识别图片格式'); } // 检查image_type_to_extension入参有效性 $imageExt = image_type_to_extension($info[2], false); if(!$imageExt) { throw new Exception('不支持的图片类型:' . $info[2]); } $this->type = strtolower($imageExt); $this->width = $info[0]; $this->height = $info[1]; // 创建图片资源 $this->createImageResource(); } /** * 析构函数:确保图片资源自动释放(优化:防止内存泄漏) */ public function __destruct() { if($this->image && is_resource($this->image)) { imagedestroy($this->image); $this->image = null; // 清空引用 } } /** * 创建图片资源 */ private function createImageResource() { switch($this->type) { case 'jpg': case 'jpeg': $this->image = imagecreatefromjpeg($this->srcPath); break; case 'png': $this->image = imagecreatefrompng($this->srcPath); // 保留PNG透明通道 imagesavealpha($this->image, true); break; case 'gif': $this->image = imagecreatefromgif($this->srcPath); break; case 'webp': $this->image = imagecreatefromwebp($this->srcPath); break; default: throw new Exception('不支持的图片格式:' . $this->type); } if(!$this->image) { throw new Exception('创建图片资源失败'); } } /** * 图片压缩(按质量/尺寸) * @param int $quality 压缩质量(1-100,越低越小) * @param int $maxWidth 最大宽度(0=不限制) * @param int $maxHeight 最大高度(0=不限制) * @return $this */ public function compress(int $quality = 80, int $maxWidth = 0, int $maxHeight = 0): self { // 限制质量范围,避免无效值 $this->quality = max(1, min(100, $quality)); // 等比例缩放 if($maxWidth > 0 || $maxHeight > 0) { $scale = 1; if($maxWidth > 0 && $this->width > $maxWidth) { $scale = $maxWidth / $this->width; } if($maxHeight > 0 && $this->height * $scale > $maxHeight) { $scale = $maxHeight / $this->height; } if($scale < 1) { $newWidth = (int)($this->width * $scale); $newHeight = (int)($this->height * $scale); // 创建新画布 $newImage = imagecreatetruecolor($newWidth, $newHeight); if(!$newImage) { throw new Exception('创建画布失败,可能内存不足'); } // 优化:PNG透明背景增强处理(绝对防止黑底) if($this->type == 'png') { imagealphablending($newImage, false); imagesavealpha($newImage, true); // 填充完全透明的背景色(alpha=127表示完全透明) $transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127); imagefill($newImage, 0, 0, $transparent); } // 图片重采样 $result = imagecopyresampled( $newImage, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height ); if(!$result) { throw new Exception('图片压缩失败'); } // 释放旧资源 imagedestroy($this->image); $this->image = $newImage; $this->width = $newWidth; $this->height = $newHeight; } } return $this; } /** * 图片裁剪(固定尺寸/等比例) * @param int $cropWidth 裁剪宽度 * @param int $cropHeight 裁剪高度 * @param string $mode 裁剪模式:center(居中)、top_left(左上) * @return $this */ public function crop(int $cropWidth, int $cropHeight, string $mode = 'center'): self { // 计算裁剪起始位置 $x = 0; $y = 0; if($mode == 'center') { $x = ($this->width - $cropWidth) / 2; $y = ($this->height - $cropHeight) / 2; // 防止裁剪尺寸超过原图 $x = $x < 0 ? 0 : $x; $y = $y < 0 ? 0 : $y; $cropWidth = $cropWidth > $this->width ? $this->width : $cropWidth; $cropHeight = $cropHeight > $this->height ? $this->height : $cropHeight; } // 创建裁剪后的画布 $newImage = imagecreatetruecolor($cropWidth, $cropHeight); if(!$newImage) { throw new Exception('创建裁剪画布失败,可能内存不足'); } // 优化:PNG透明背景增强处理(绝对防止黑底) if($this->type == 'png') { imagealphablending($newImage, false); imagesavealpha($newImage, true); // 填充完全透明的背景色 $transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127); imagefill($newImage, 0, 0, $transparent); } // 执行裁剪 $result = imagecopyresampled( $newImage, $this->image, 0, 0, (int)$x, (int)$y, $cropWidth, $cropHeight, $cropWidth, $cropHeight ); if(!$result) { throw new Exception('图片裁剪失败'); } // 释放旧资源 imagedestroy($this->image); $this->image = $newImage; $this->width = $cropWidth; $this->height = $cropHeight; return $this; } /** * 添加文字水印 * @param string $text 水印文字 * @param string $font 字体文件路径 * @param array $config 配置 * @return $this */ public function addTextWatermark(string $text, string $font, array $config = []): self { // 检查字体文件是否存在(路径安全校验) $realFontPath = realpath($font); if($realFontPath === false || strpos($realFontPath, $this->allowedRootDir) !== 0) { throw new Exception('字体文件不存在或路径非法:' . $font); } $default = [ 'size' => 12, 'color' => [180, 180, 180], 'position' => 'bottom_right', 'alpha' => 80 ]; $config = array_merge($default, $config); // 创建水印颜色 $color = imagecolorallocatealpha($this->image, $config['color'][0], $config['color'][1], $config['color'][2], $config['alpha']); if($color === false) { throw new Exception('创建水印颜色失败'); } // 校验文字尺寸 $textBox = imagettfbbox($config['size'], 0, $realFontPath, $text); if($textBox === false) { throw new Exception('获取文字尺寸失败(字体文件错误或文字为空)'); } $textWidth = $textBox[2] - $textBox[0]; $textHeight = $textBox[7] - $textBox[1]; // 计算位置 switch($config['position']) { case 'center': $x = ($this->width - $textWidth) / 2; $y = ($this->height + $textHeight) / 2; break; case 'bottom_right': default: $x = $this->width - $textWidth - 10; $y = $this->height - 10; break; } // 添加文字水印 $result = imagettftext($this->image, $config['size'], 0, $x, $y, $color, $realFontPath, $text); if($result === false) { throw new Exception('添加文字水印失败'); } return $this; } /** * 添加图片水印 * @param string $waterPath 水印图片路径 * @param array $config 配置 * @return $this */ public function addImageWatermark(string $waterPath, array $config = []): self { // 路径安全校验 $realWaterPath = realpath($waterPath); if($realWaterPath === false || strpos($realWaterPath, $this->allowedRootDir) !== 0) { throw new Exception('水印图片不存在或路径非法:' . $waterPath); } $default = [ 'position' => 'bottom_right', 'alpha' => 50, 'scale' => 0.2 ]; $config = array_merge($default, $config); // 获取水印图片信息 $waterInfo = getimagesize($realWaterPath); if(!$waterInfo) { throw new Exception('无法识别水印图片格式'); } $waterType = strtolower(image_type_to_extension($waterInfo[2], false)); if(!$waterType) { throw new Exception('不支持的水印格式:' . $waterInfo[2]); } // 创建水印资源 $waterImage = null; switch($waterType) { case 'jpg': case 'jpeg': $waterImage = imagecreatefromjpeg($realWaterPath); break; case 'png': $waterImage = imagecreatefrompng($realWaterPath); break; case 'gif': $waterImage = imagecreatefromgif($realWaterPath); break; default: throw new Exception('不支持的水印格式:' . $waterType); } if(!$waterImage) { throw new Exception('创建水印图片资源失败'); } $waterWidth = $waterInfo[0] * $config['scale']; $waterHeight = $waterInfo[1] * $config['scale']; // 计算水印位置 switch($config['position']) { case 'center': $x = ($this->width - $waterWidth) / 2; $y = ($this->height - $waterHeight) / 2; break; case 'bottom_right': default: $x = $this->width - $waterWidth - 10; $y = $this->height - $waterHeight - 10; break; } // 类型安全:强制转int $waterWidthInt = (int)$waterWidth; $waterHeightInt = (int)$waterHeight; // 添加图片水印 $result = imagecopymerge( $this->image, $waterImage, (int)$x, (int)$y, 0, 0, $waterWidthInt, $waterHeightInt, $config['alpha'] ); if($result === false) { throw new Exception('添加图片水印失败'); } imagedestroy($waterImage); return $this; } /** * 转换为WebP格式 * @return $this */ public function toWebP(): self { if(!function_exists('imagewebp')) { throw new Exception('当前PHP环境不支持WebP格式(GD库未编译WebP支持)'); } $this->type = 'webp'; return $this; } /** * 保存处理后的图片 * @param string $savePath 保存路径 * @return bool */ public function save(string $savePath): bool { // 路径安全校验:确保保存路径在允许的根目录内 $realSaveDir = realpath(dirname($savePath)) ?: dirname($savePath); if(strpos($realSaveDir, $this->allowedRootDir) !== 0) { throw new Exception('非法保存路径:不允许保存到根目录外'); } $dir = dirname($savePath); if(!is_dir($dir)) { mkdir($dir, 0755, true); } // 检查目录写入权限 if(!is_writable($dir)) { throw new Exception('保存目录无写入权限:' . $dir); } // 执行保存 $result = false; switch($this->type) { case 'jpg': case 'jpeg': $result = imagejpeg($this->image, $savePath, $this->quality ?? 80); break; case 'png': $pngLevel = 9 - (int)(($this->quality ?? 80) / 10); $pngLevel = max(0, min(9, $pngLevel)); $result = imagepng($this->image, $savePath, $pngLevel); break; case 'gif': $result = imagegif($this->image, $savePath); break; case 'webp': $result = imagewebp($this->image, $savePath, $this->quality ?? 80); break; default: throw new Exception('不支持的保存格式:' . $this->type); } if(!$result) { throw new Exception('保存图片失败(路径无写入权限或格式错误)'); } return $result; }}