圖片上傳後常需產生縮圖或壓縮版本。PHP GD 擴充套件提供 imagecopyresampled() 高品質重採樣。本文提供一個完整的 ImageResizer 類別,支援 等比例縮放、最大寬高限制、JPEG 品質調整、PNG 透明度保留、GIF 動畫首幀處理、輸出到檔案或瀏覽器。
<?php
class ImageResizer {
private $sourcePath;
private $image;
private $width;
private $height;
private $mime;
private $type; // IMAGETYPE_* 常數
public function __construct(string $sourcePath) {
if (!file_exists($sourcePath)) {
throw new InvalidArgumentException("檔案不存在:{$sourcePath}");
}
$this->sourcePath = $sourcePath;
$this->loadImage();
}
private function loadImage(): void {
$info = getimagesize($this->sourcePath);
$this->width = $info[0];
$this->height = $info[1];
$this->type = $info[2];
$this->mime = $info['mime'];
switch ($this->type) {
case IMAGETYPE_JPEG:
$this->image = imagecreatefromjpeg($this->sourcePath);
break;
case IMAGETYPE_PNG:
$this->image = imagecreatefrompng($this->sourcePath);
break;
case IMAGETYPE_GIF:
$this->image = imagecreatefromgif($this->sourcePath);
break;
case IMAGETYPE_WEBP:
$this->image = imagecreatefromwebp($this->sourcePath);
break;
default:
throw new RuntimeException("不支援的圖片類型:{$this->mime}");
}
}
/**
* 等比例縮放至最大寬高範圍內
* @param int $maxWidth 最大寬度
* @param int $maxHeight 最大高度
* @return self 鏈式呼叫
*/
public function resize(int $maxWidth, int $maxHeight): self {
$ratio = min($maxWidth / $this->width, $maxHeight / $this->height);
// 若原圖已小於限制,不放大
if ($ratio >= 1) return $this;
$newWidth = (int)round($this->width * $ratio);
$newHeight = (int)round($this->height * $ratio);
$dst = imagecreatetruecolor($newWidth, $newHeight);
// PNG/GIF 保留透明度
if ($this->type === IMAGETYPE_PNG || $this->type === IMAGETYPE_GIF) {
imagealphablending($dst, false);
imagesavealpha($dst, true);
$transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127);
imagefilledrectangle($dst, 0, 0, $newWidth, $newHeight, $transparent);
}
// 高品質重採樣
imagecopyresampled($dst, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
imagedestroy($this->image);
$this->image = $dst;
$this->width = $newWidth;
$this->height = $newHeight;
return $this;
}
/**
* 裁切為正方形(從中心)
*/
public function cropSquare(): self {
$size = min($this->width, $this->height);
$x = ($this->width - $size) / 2;
$y = ($this->height - $size) / 2;
$dst = imagecreatetruecolor($size, $size);
imagecopyresampled($dst, $this->image, 0, 0, $x, $y, $size, $size, $size, $size);
imagedestroy($this->image);
$this->image = $dst;
$this->width = $this->height = $size;
return $this;
}
/**
* 儲存至檔案
* @param string $destPath 目的路徑
* @param int $quality JPEG/WebP 品質 0-100
* @param int $pngCompression PNG 壓縮等級 0-9
*/
public function save(string $destPath, int $quality = 85, int $pngCompression = 6): void {
$dir = dirname($destPath);
if (!is_dir($dir)) mkdir($dir, 0755, true);
switch ($this->type) {
case IMAGETYPE_JPEG:
imagejpeg($this->image, $destPath, $quality);
break;
case IMAGETYPE_PNG:
imagepng($this->image, $destPath, $pngCompression);
break;
case IMAGETYPE_GIF:
imagegif($this->image, $destPath);
break;
case IMAGETYPE_WEBP:
imagewebp($this->image, $destPath, $quality);
break;
}
}
/**
* 直接輸出到瀏覽器(附檔名下載或內嵌顯示)
*/
public function output(bool $download = false, string $filename = ''): void {
if ($download) {
header('Content-Disposition: attachment; filename="' . ($filename ?: basename($this->sourcePath)) . '"');
}
header('Content-Type: ' . $this->mime);
header('Cache-Control: public, max-age=31536000');
switch ($this->type) {
case IMAGETYPE_JPEG: imagejpeg($this->image, null, 85); break;
case IMAGETYPE_PNG: imagepng($this->image, null, 6); break;
case IMAGETYPE_GIF: imagegif($this->image); break;
case IMAGETYPE_WEBP: imagewebp($this->image, null, 85); break;
}
exit;
}
public function __destruct() {
if ($this->image) imagedestroy($this->image);
}
}
<?php
require 'ImageResizer.php';
// 1. 產生縮圖(最大 300x300)
$resizer = new ImageResizer('uploads/photo.jpg');
$resizer->resize(300, 300)
->save('uploads/thumbs/photo_thumb.jpg', 80);
// 2. 產生正方形頭像(裁切 + 縮放)
$avatar = new ImageResizer('uploads/avatar.png');
$avatar->cropSquare()
->resize(200, 200)
->save('uploads/avatars/user_123.png');
// 3. 直接輸出到瀏覽器(動態縮圖 API)
// 路由:/api/thumb?file=photo.jpg&w=300&h=200
$file = $_GET['file'] ?? '';
$w = (int)($_GET['w'] ?? 300);
$h = (int)($_GET['h'] ?? 300);
$resizer = new ImageResizer("uploads/{$file}");
$resizer->resize($w, $h)->output(); // 內嵌顯示
// $resizer->resize($w, $h)->output(true, "thumb_{$file}"); // 強制下載
| 問題 | 解決方案 |
|---|---|
| 超大圖(>5000px)導致記憶體耗盡 | 1. 增加 memory_limit2. 使用 Imagick 串流處理3. 分塊處理(GD 不支援,需外部工具) |
| GIF 動畫縮放後只剩首幀 | GD 不支援多幀 GIF,需用 Imagick 或 gifsicle CLI |
| PNG 透明背景變黑 | 確呼叫 imagesavealpha($dst, true) 並關閉 alpha blending |
| JPEG 方向標籤(EXIF Orientation)導致旋轉錯誤 | 上傳前用 exif_read_data() 讀取 Orientation 並物理旋轉修正 |
ImageResizer 類別封裝了 GD 圖片處理的最佳實踐:等比例縮放、透明度保留、品質參數化、鏈式 API、資源自動釋放。實務專案中,上傳流程建議整合「上傳 → 暫存 → 異步佇列(產生多種尺寸縮圖/水印/轉 WebP) → 入庫記錄」,避免同步處理阻塞使用者請求。