PHP 縮略圖生成看似簡單,實則涉及多種業務模式(等比縮放、填滿裁剪、固定畫布居中、添加水印等)。本文系統梳理四大核心方法,從底層 GD 函數原理到封裝類設計,提供可直接落地的代碼與選型指南。
核心邏輯:min($max_w/$src_w, $max_h/$src_h) 計算縮放比,確保圖片完整放入目標框內。
<?php
function resize_fit($src, $dst, $max_w, $max_h, $quality = 90) {
$info = getimagesize($src);
if (!$info) return false;
[$src_w, $src_h, $type] = $info;
$ratio = min($max_w / $src_w, $max_h / $src_h);
$dst_w = (int)round($src_w * $ratio);
$dst_h = (int)round($src_h * $ratio);
$src_img = load_image($src, $type);
$dst_img = imagecreatetruecolor($dst_w, $dst_h);
copy_transparency($src_img, $dst_img, $type); // PNG/GIF 透明處理
imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $dst_w, $dst_h, $src_w, $src_h);
save_image($dst_img, $dst, $type, $quality);
imagedestroy($src_img);
imagedestroy($dst_img);
return true;
}
適用:商品主圖統一尺寸、相冊網格、需要固定長寬比的場景。
function resize_canvas($src, $dst, $canvas_w, $canvas_h, $bg_color = '#FFFFFF', $quality = 90) {
$info = getimagesize($src);
if (!$info) return false;
[$src_w, $src_h, $type] = $info;
$src_ratio = $src_w / $src_h;
$dst_ratio = $canvas_w / $canvas_h;
if ($dst_ratio > $src_ratio) {
// 目標更寬:以高為準
$new_h = $canvas_h;
$new_w = (int)round($canvas_h * $src_ratio);
$x = (int)(($canvas_w - $new_w) / 2);
$y = 0;
} else {
// 目標更高:以寬為準
$new_w = $canvas_w;
$new_h = (int)round($canvas_w / $src_ratio);
$x = 0;
$y = (int)(($canvas_h - $new_h) / 2);
}
$src_img = load_image($src, $type);
$dst_img = imagecreatetruecolor($canvas_w, $canvas_h);
fill_bg($dst_img, $bg_color);
copy_transparency($src_img, $dst_img, $type);
imagecopyresampled($dst_img, $src_img, $x, $y, 0, 0, $new_w, $new_h, $src_w, $src_h);
save_image($dst_img, $dst, $type, $quality);
imagedestroy($src_img);
imagedestroy($dst_img);
return true;
}
適用:Banner、封面、頭像方框、背景圖。
function resize_cover($src, $dst, $target_w, $target_h, $quality = 90) {
$info = getimagesize($src);
if (!$info) return false;
[$src_w, $src_h, $type] = $info;
$src_ratio = $src_w / $src_h;
$dst_ratio = $target_w / $target_h;
if ($dst_ratio > $src_ratio) {
// 目標更寬:源圖以高為準縮放,寬度超出 → 裁剪左右
$scale = $target_h / $src_h;
$scaled_w = (int)round($src_w * $scale);
$scaled_h = $target_h;
$src_x = (int)(($scaled_w - $target_w) / 2);
$src_y = 0;
} else {
// 目標更高:源圖以寬為準縮放,高度超出 → 裁剪上下
$scale = $target_w / $src_w;
$scaled_w = $target_w;
$scaled_h = (int)round($src_h * $scale);
$src_x = 0;
$src_y = (int)(($scaled_h - $target_h) / 2);
}
$src_img = load_image($src, $type);
// 中間畫布:縮放後的完整圖
$mid_img = imagecreatetruecolor($scaled_w, $scaled_h);
copy_transparency($src_img, $mid_img, $type);
imagecopyresampled($mid_img, $src_img, 0, 0, 0, 0, $scaled_w, $scaled_h, $src_w, $src_h);
imagedestroy($src_img);
// 目標畫布:裁剪目標區域
$dst_img = imagecreatetruecolor($target_w, $target_h);
imagecopy($dst_img, $mid_img, 0, 0, $src_x, $src_y, $target_w, $target_h);
imagedestroy($mid_img);
save_image($dst_img, $dst, $type, $quality);
imagedestroy($dst_img);
return true;
}
生產環境推薦面向對象封裝,代碼復用性高、擴展性強。
<?php
class ThumbnailGenerator {
private $src_img;
private $src_w;
private $src_h;
private $type;
private $mime;
// 鏈式調用配置
private $mode = 'fit'; // fit | canvas | cover
private $target_w = 200;
private $target_h = 200;
private $bg_color = '#FFFFFF';
private $quality = 90;
private $watermark = null; // ['text'=>'...', 'font'=>'...', 'color'=>..., 'pos'=>...]
public static function fromFile(string $path): self {
$instance = new self();
$info = getimagesize($path);
if (!$info) throw new Exception("Invalid image: $path");
[$instance->src_w, $instance->src_h, $instance->type] = $info;
$instance->mime = $info['mime'] ?? '';
$instance->src_img = $instance->loadImage($path, $instance->type);
return $instance;
}
// 模式設置
public function fit(int $w, int $h): self { $this->mode='fit'; $this->target_w=$w; $this->target_h=$h; return $this; }
public function canvas(int $w, int $h, string $bg='#FFFFFF'): self { $this->mode='canvas'; $this->target_w=$w; $this->target_h=$h; $this->bg_color=$bg; return $this; }
public function cover(int $w, int $h): self { $this->mode='cover'; $this->target_w=$w; $this->target_h=$h; return $this; }
public function quality(int $q): self { $this->quality = $q; return $this; }
// 文字水印
public function textWatermark(string $text, string $font, array $color, string $pos='bottom-right', int $size=16, int $offset=10): self {
$this->watermark = compact('text','font','color','pos','size','offset');
return $this;
}
// 圖片水印
public function imageWatermark(string $wm_path, string $pos='bottom-right', int $offset=10, float $opacity=1.0): self {
$this->watermark = ['image'=>$wm_path, 'pos'=>$pos, 'offset'=>$offset, 'opacity'=>$opacity] + $this->watermark;
return $this;
}
// 執行生成
public function save(string $dst_path): bool {
$dst_img = $this->process();
$this->applyWatermark($dst_img);
$this->saveImage($dst_img, $dst_path);
imagedestroy($dst_img);
return true;
}
// 核心處理邏輯
private function process() {
switch ($this->mode) {
case 'fit': return $this->processFit();
case 'canvas': return $this->processCanvas();
case 'cover': return $this->processCover();
}
}
private function processFit() {
$ratio = min($this->target_w / $this->src_w, $this->target_h / $this->src_h);
$w = (int)round($this->src_w * $ratio);
$h = (int)round($this->src_h * $ratio);
$dst = imagecreatetruecolor($w, $h);
$this->copyWithTransparency($dst, 0, 0, $w, $h);
return $dst;
}
private function processCanvas() {
$dst = imagecreatetruecolor($this->target_w, $this->target_h);
$this->fillBackground($dst);
$src_ratio = $this->src_w / $this->src_h;
$dst_ratio = $this->target_w / $this->target_h;
if ($dst_ratio > $src_ratio) {
$h = $this->target_h;
$w = (int)round($h * $src_ratio);
$x = (int)(($this->target_w - $w) / 2);
$y = 0;
} else {
$w = $this->target_w;
$h = (int)round($w / $src_ratio);
$x = 0;
$y = (int)(($this->target_h - $h) / 2);
}
$this->copyWithTransparency($dst, $x, $y, $w, $h);
return $dst;
}
private function processCover() {
$src_ratio = $this->src_w / $this->src_h;
$dst_ratio = $this->target_w / $this->target_h;
if ($dst_ratio > $src_ratio) {
$scale = $this->target_h / $this->src_h;
$mid_w = (int)round($this->src_w * $scale);
$mid_h = $this->target_h;
$src_x = (int)(($mid_w - $this->target_w) / 2);
$src_y = 0;
} else {
$scale = $this->target_w / $this->src_w;
$mid_w = $this->target_w;
$mid_h = (int)round($this->src_h * $scale);
$src_x = 0;
$src_y = (int)(($mid_h - $this->target_h) / 2);
}
$mid = imagecreatetruecolor($mid_w, $mid_h);
$this->copyWithTransparency($mid, 0, 0, $mid_w, $mid_h);
$dst = imagecreatetruecolor($this->target_w, $this->target_h);
imagecopy($dst, $mid, 0, 0, $src_x, $src_y, $this->target_w, $this->target_h);
imagedestroy($mid);
return $dst;
}
// 輔助方法
private function loadImage($path, $type) {
return match($type) {
IMAGETYPE_JPEG => imagecreatefromjpeg($path),
IMAGETYPE_PNG => imagecreatefrompng($path),
IMAGETYPE_GIF => imagecreatefromgif($path),
IMAGETYPE_WEBP => imagecreatefromwebp($path),
default => throw new Exception("Unsupported type: $type"),
};
}
private function copyWithTransparency($dst, $dx, $dy, $dw, $dh) {
if (in_array($this->type, [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
imagealphablending($dst, false);
imagesavealpha($dst, true);
$tp = imagecolorallocatealpha($dst, 255,255,255,127);
imagefill($dst, 0, 0, $tp);
}
imagecopyresampled($dst, $this->src_img, $dx, $dy, 0, 0, $dw, $dh, $this->src_w, $this->src_h);
}
private function fillBackground($img) {
$hex = ltrim($this->bg_color, '#');
if (strlen($hex) === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
$r = hexdec(substr($hex,0,2));
$g = hexdec(substr($hex,2,2));
$b = hexdec(substr($hex,4,2));
$c = imagecolorallocate($img, $r, $g, $b);
imagefill($img, 0, 0, $c);
}
private function applyWatermark($img) {
if (!$this->watermark) return;
// 文字水印實現略...
// 圖片水印實現略...
}
private function saveImage($img, $path) {
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$func = match($ext) {
'jpg','jpeg' => fn() => imagejpeg($img, $path, $this->quality),
'png' => fn() => imagepng($img, $path, (int)(9 - $this->quality/10)),
'gif' => fn() => imagegif($img, $path),
'webp' => fn() => imagewebp($img, $path, $this->quality),
default => throw new Exception("Unsupported output format: $ext"),
};
$func();
}
public function __destruct() {
if ($this->src_img) imagedestroy($this->src_img);
}
}
// ===== 使用示例 =====
ThumbnailGenerator::fromFile('photo.jpg')
->cover(400, 300)
->quality(90)
->textWatermark('© MySite', __DIR__.'/fonts/Roboto.ttf', [255,255,255,100], 'bottom-right', 14)
->save('thumbs/photo_cover.jpg');
| 方法 | 關鍵字 | 特點 | 適用場景 | 輸出尺寸 |
|---|---|---|---|---|
| 等比縮放 | fit / contain | 完整顯示、不變形、尺寸不固定 | 列表圖、文章配圖、原圖預覽 | ≤ 目標框 |
| 固定畫布居中 | canvas / pad | 尺寸固定、不變形、留白填充背景色 | 商品主圖、相冊網格、輪播圖統一尺寸 | = 目標框 |
| 填滿裁剪 | cover / crop | 尺寸固定、不變形、無留白、居中裁剪 | Banner、封面、頭像方框、Hero 圖 | = 目標框 |
| 強制拉伸 | stretch | 尺寸固定、可能變形 | 極少用,僅特殊佔位圖 | = 目標框 |
function save_image($img, $path, $type, $quality) {
$func = match($type) {
IMAGETYPE_JPEG => fn() => imagejpeg($img, $path, $quality),
IMAGETYPE_PNG => fn() => imagepng($img, $path, (int)(9 - $quality/10)),
IMAGETYPE_GIF => fn() => imagegif($img, $path),
IMAGETYPE_WEBP => fn() => imagewebp($img, $path, $quality),
default => throw new Exception("Unsupported save type"),
};
$func();
}
function copy_transparency($src, $dst, $type) {
if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
imagealphablending($dst, false);
imagesavealpha($dst, true);
$tp = imagecolorallocatealpha($dst, 255,255,255,127);
imagefill($dst, 0, 0, $tp);
}
}
dst, src, dst_x, dst_y, src_x, src_y, dst_w, dst_h, src_w, src_h —— 目標在前、源在後,寬高對應。imagepng($img, $path, 6)。imagecreate* 必須配對 imagedestroy(),類模式用 __destruct 保底。memory_limit 或用 Imagick。PHP 縮略圖生成的核心在于「幾何計算(比例/座標)」+「GD 繪圖管線(載入→畫布→重採樣→輸出)」。掌握 **fit/canvas/cover** 三種模式的數學邏輯,配合 **透明度保持**、**水印疊加**、**多格式輸出** 等工程化細節,即可構建滿足各類業務需求的圖片處理體系。推薦採用 ThumbnailGenerator 類封裝,鏈式調用、單一職責、易測試、易擴展。