GD 是 PHP 內建圖形庫,支援 JPEG/PNG/GIF/WebP/BMP,適合中等負載的圖片處理。本文涵蓋驗證碼生成、動態水印、智慧縮圖 (保持比例/裁剪/填充)、多行文字自動換行、字體子集嵌入、WebP 輸出優化,並解決中文亂碼、記憶體洩漏、座標計算陷阱。
# Ubuntu/Debian apt-get install -y php-gd fonts-noto-cjk fonts-dejavu-core # 檢查 php -r 'var_dump(gd_info());' # 確認 FreeType Support => true, TTF Support => true
專案目錄結構:
/fonts/ NotoSansTC-Regular.otf # 繁體中文 DejaVuSans.ttf # 英文/符號 fallback NotoEmoji-Regular.ttf # Emoji 支援 (可選)
<?php
declare(strict_types=1);
namespace App\Image;
class CaptchaGenerator {
public function __construct(
private int $width = 160,
private int $height = 50,
private int $length = 5,
private string $fontFile = __DIR__.'/../fonts/DejaVuSans.ttf',
private int $fontSize = 24
) {}
public function generate(): array {
$code = $this->randomCode();
$img = imagecreatetruecolor($this->width, $this->height);
// 背景
$bg = imagecolorallocate($img, 245, 245, 245);
imagefill($img, 0, 0, $bg);
// 干擾線
for ($i = 0; $i < 5; $i++) {
$color = imagecolorallocatealpha($img, rand(100,200), rand(100,200), rand(100,200), 80);
imageline($img, rand(0,$this->width), rand(0,$this->height), rand(0,$this->width), rand(0,$this->height), $color);
}
// 噪點
for ($i = 0; $i < 200; $i++) {
$color = imagecolorallocatealpha($img, rand(0,255), rand(0,255), rand(0,255), 60);
imagesetpixel($img, rand(0,$this->width), rand(0,$this->height), $color);
}
// 文字 (逐字隨機旋轉/位移)
$x = 15;
for ($i = 0; $i < $this->length; $i++) {
$char = $code[$i];
$angle = rand(-15, 15);
$color = imagecolorallocate($img, rand(30,100), rand(30,100), rand(30,100));
$bbox = imagettfbbox($this->fontSize, $angle, $this->fontFile, $char);
$charW = $bbox[2] - $bbox[0];
$y = $this->height/2 + ($bbox[1] - $bbox[7])/2 + rand(-3,3);
imagettftext($img, $this->fontSize, $angle, $x, $y, $color, $this->fontFile, $char);
$x += $charW + rand(5, 12);
}
ob_start();
imagepng($img);
$pngData = ob_get_clean();
imagedestroy($img);
return ['code' => strtolower($code), 'image' => $pngData];
}
private function randomCode(): string {
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
return substr(str_shuffle($chars), 0, $this->length);
}
}
?>
<?php
class Thumbnailer {
public enum Mode: string {
case FIT = 'fit'; // 等比縮放,完整顯示,留白
case COVER = 'cover'; // 等比縮放,填滿畫布,裁剪溢出
case STRETCH = 'stretch'; // 強制拉伸填滿
}
public function resize(string $srcPath, string $dstPath, int $w, int $h, Mode $mode = Mode::FIT, int $quality = 85): void {
[$srcW, $srcH, $type] = getimagesize($srcPath);
$src = $this->createFromType($srcPath, $type);
switch ($mode) {
case Mode::FIT:
$ratio = min($w/$srcW, $h/$srcH);
$dstW = (int)round($srcW * $ratio);
$dstH = (int)round($srcH * $ratio);
$dstX = ($w - $dstW) / 2;
$dstY = ($h - $dstH) / 2;
break;
case Mode::COVER:
$ratio = max($w/$srcW, $h/$srcH);
$dstW = (int)round($srcW * $ratio);
$dstH = (int)round($srcH * $ratio);
$srcX = ($dstW - $w) / 2 / $ratio;
$srcY = ($dstH - $h) / 2 / $ratio;
$srcW = $w / $ratio;
$srcH = $h / $ratio;
$dstX = 0; $dstY = 0;
break;
case Mode::STRETCH:
$dstW = $w; $dstH = $h;
$dstX = 0; $dstY = 0;
break;
}
$dst = imagecreatetruecolor($w, $h);
// 透明背景支援
if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
imagealphablending($dst, false);
imagesavealpha($dst, true);
$transparent = imagecolorallocatealpha($dst, 0,0,0,127);
imagefill($dst, 0, 0, $transparent);
} else {
$white = imagecolorallocate($dst, 255,255,255);
imagefill($dst, 0, 0, $white);
}
if ($mode === Mode::COVER) {
imagecopyresampled($dst, $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
} else {
imagecopyresampled($dst, $src, $dstX, $dstY, 0, 0, $dstW, $dstH, $srcW, $srcH);
}
$this->saveByType($dst, $dstPath, $type, $quality);
imagedestroy($src); imagedestroy($dst);
}
private function createFromType(string $path, int $type): \GdImage {
return match($type) {
IMAGETYPE_JPEG => imagecreatefromjpeg($path),
IMAGETYPE_PNG => imagecreatefrompng($path),
IMAGETYPE_GIF => imagecreatefromgif($path),
IMAGETYPE_WEBP => imagecreatefromwebp($path),
default => throw new \InvalidArgumentException('不支援的圖片類型'),
};
}
private function saveByType(\GdImage $img, string $path, int $type, int $q): void {
match($type) {
IMAGETYPE_JPEG => imagejpeg($img, $path, $q),
IMAGETYPE_PNG => imagepng($img, $path, (int)round($q/10)), // PNG 壓縮 0-9
IMAGETYPE_WEBP => imagewebp($img, $path, $q),
default => imagepng($img, $path),
};
}
}
?>
<?php
function drawTextBox(\GdImage $img, string $text, int $x, int $y, int $boxW, int $boxH, string $font, int $size, array $color, int $align = 0, int $valign = 0, float $lineHeight = 1.5): void {
// $align: 0=left, 1=center, 2=right; $valign: 0=top, 1=middle, 2=bottom
$lines = mb_wordwrap($text, $boxW, $font, $size); // 自訂函數,按像素寬度換行
$totalH = count($lines) * $size * $lineHeight;
$startY = match($valign) {
0 => $y,
1 => $y + ($boxH - $totalH)/2,
2 => $y + $boxH - $totalH,
};
$textColor = imagecolorallocatealpha($img, $color[0], $color[1], $color[2], $color[3] ?? 0);
foreach ($lines as $i => $line) {
$bbox = imagettfbbox($size, 0, $font, $line);
$lineW = $bbox[2] - $bbox[0];
$lineX = match($align) {
0 => $x,
1 => $x + ($boxW - $lineW)/2,
2 => $x + $boxW - $lineW,
};
$lineY = $startY + $i * $size * $lineHeight + $size;
imagettftext($img, $size, 0, $lineX, $lineY, $textColor, $font, $line);
}
}
function mb_wordwrap(string $text, int $maxWidth, string $font, int $size): array {
$words = preg_split('/(\s+)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$lines = [''];
foreach ($words as $word) {
$test = $lines[count($lines)-1] . $word;
$bbox = imagettfbbox($size, 0, $font, $test);
if ($bbox[2] - $bbox[0] <= $maxWidth) {
$lines[count($lines)-1] = $test;
} else {
$lines[] = $word;
}
}
return $lines;
}
?>
<?php
function outputWebP(\GdImage $img, int $quality = 80): void {
header('Content-Type: image/webp');
header('Cache-Control: public, max-age=31536000');
imagewebp($img, null, $quality); // null 直接輸出到瀏覽器
}
// 標籤優雅降級
echo '<picture>';
echo '<source type="image/webp" srcset="image.webp">';
echo '<img src="image.jpg" alt="描述">';
echo '</picture>';
?>
ini_set('memory_limit', '512M') 並及時 imagedestroy()。Imagick (多執行緒、串流、格式支援更全),GD 單執行緒且全載入記憶體。imagettfbbox 重複呼叫同字體同大小可快取 bounding box。ob_start() 避免標頭已送出錯誤。