門戶類網站常面臨同一張圖需在多處以不同尺寸、比例展示的需求:列表頁 200×150、詳情頁 800×600、手機端 320×240……若僅靠 CSS 強制縮放會導致變形、流量浪費;預生成所有規格則維護成本極高。本文提供一套按需生成、自動緩存、支援裁切/縮放模式的完整解決方案。
/data/attachment/forum/abc_200_100.jpg 表示原圖 abc.jpg 裁切為 200×100<?php
/**
* 生成裁切後圖片 URL
* @param string $img 原圖相對/絕對路徑
* @param int $width 目標寬度
* @param int $height 目標高度
* @return string 處理後 URL(若外鏈直接返回原 URL)
*/
function crop_img($img, $width = 200, $height = 200) {
$info = parse_url($img);
// 外部圖片不處理
if (!empty($info['host']) && $info['host'] !== $_SERVER['HTTP_HOST']) {
return $img;
}
$pos = strrpos($img, '.');
if ($pos === false) return $img; // 無副檔名
return substr($img, 0, $pos) . "_{$width}_{$height}" . substr($img, $pos);
}
/**
* 直接輸出 <img> 標籤
*/
function img_tag($img, $width, $height, $attrs = '') {
$src = crop_img($img, $width, $height);
return "<img src=\"{$src}\" width=\"{$width}\" height=\"{$height}\" {$attrs} />";
}
// Discuz 模板中調用:{eval echo img_tag($thread['image'], 200, 100);}
// 輸出:<img src="/data/attachment/forum/abc_200_100.jpg" width="200" height="100" />
?>
<IfModule mod_rewrite.c>
RewriteEngine On
# 僅處理附件目錄下的不存在文件
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^data/attachment/(.*)$ images.php?url=$1 [L,QSA]
</IfModule>
關鍵點:!-f 確保已生成的緩存文件直接由 Apache 服務,不經 PHP。
<?php
// images.php
$url = $_GET['url'] ?? '';
if (!$url) exit('Missing url param');
// 還原原圖路徑:移除 _寬_高 後綴
$srcPath = './data/attachment/' . preg_replace('/_\d+_\d+/', '', $url);
$cachePath = './data/attachment/' . $url;
// 緩存命中直接輸出
if (file_exists($cachePath)) {
outputImage($cachePath);
exit;
}
// 解析尺寸
if (!preg_match('/_(\d+)_(\d+)/', $url, $m)) {
outputDefault();
exit;
}
$width = (int)$m[1];
$height = (int)$m[2];
// 原圖存在性檢查
if (!file_exists($srcPath)) {
outputDefault();
exit;
}
// 執行裁切/縮放
thumb($srcPath, $width, $height, $cachePath, 'crop', 85);
// 輸出剛生成的文件
outputImage($cachePath);
/**
* 核心縮放裁切函數
* @param string $src 源文件路徑
* @param int $width 目標寬
* @param int $height 目標高
* @param string $dest 保存路徑
* @param string $mode 'crop' 裁切填滿 | 'scale' 等比縮放留白
* @param int $quality JPEG 品質 1-100
*/
function thumb($src, $width, $height, $dest, $mode = 'crop', $quality = 85) {
$info = getimagesize($src);
if (!$info) return false;
$srcW = $info[0];
$srcH = $info[1];
$mime = $info['mime'];
// 建立源圖資源
$createFunc = match($mime) {
'image/jpeg' => 'imagecreatefromjpeg',
'image/png' => 'imagecreatefrompng',
'image/gif' => 'imagecreatefromgif',
'image/webp' => 'imagecreatefromwebp',
default => null,
};
if (!$createFunc) return false;
$source = $createFunc($src);
// 計算裁切/縮放參數
$dstX = $dstY = 0;
$srcX = $srcY = 0;
$dstW = $width;
$dstH = $height;
$srcCropW = $srcW;
$srcCropH = $srcH;
if ($mode === 'crop') {
// 裁切模式:目標比例 ≠ 源比例時,保留長邊、居中裁切短邊
$srcRatio = $srcW / $srcH;
$dstRatio = $width / $height;
if ($srcRatio > $dstRatio) {
// 源較寬:全高、居中裁寬
$srcCropH = $srcH;
$srcCropW = $srcH * $dstRatio;
$srcX = ($srcW - $srcCropW) / 2;
} else {
// 源較高:全寬、居中裁高
$srcCropW = $srcW;
$srcCropH = $srcW / $dstRatio;
$srcY = ($srcH - $srcCropH) / 2;
}
} else {
// 縮放模式:等比縮放至目標框內,居中留白
if ($srcW <= $width && $srcH <= $height) {
// 原圖更小:居中放置
$dstX = ($width - $srcW) / 2;
$dstY = ($height - $srcH) / 2;
$dstW = $srcW;
$dstH = $srcH;
} else {
// 等比縮放
$scale = min($width / $srcW, $height / $srcH);
$dstW = $srcW * $scale;
$dstH = $srcH * $scale;
$dstX = ($width - $dstW) / 2;
$dstY = ($height - $dstH) / 2;
}
}
// 建立真彩色畫布
$thumb = imagecreatetruecolor($width, $height);
// 白色背景(縮放模式留白區域)
$white = imagecolorallocate($thumb, 255, 255, 255);
imagefill($thumb, 0, 0, $white);
// 處理透明度(PNG/GIF/WEBP)
if (in_array($mime, ['image/png', 'image/gif', 'image/webp'])) {
imagecolortransparent($thumb, $white);
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
}
// 重採樣繪製
imagecopyresampled($thumb, $source, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcCropW, $srcCropH);
// 保存並輸出
imagejpeg($thumb, $dest, $quality);
imagedestroy($thumb);
imagedestroy($source);
return true;
}
function outputImage($path) {
$mime = mime_content_type($path);
header("Content-Type: {$mime}");
header("Content-Length: " . filesize($path));
header("Cache-Control: public, max-age=31536000"); // 1 年緩存
readfile($path);
}
function outputDefault() {
$default = __DIR__ . '/media/images/nopic.jpg';
if (file_exists($default)) outputImage($default);
else http_response_code(404);
}
?>
| 特性 | crop 模式 | scale 模式 |
|---|---|---|
| 圖片比例 | 強制目標比例 | 保持原始比例 |
| 內容完整性 | 居中裁切,可能丟失邊緣 | 完整保留,四周留白 |
| 適用場景 | 頭像、縮略圖、Banner 固定框 | 產品圖、藝術作品、文檔截圖 |
Accept: image/webp,優先輸出 WebP 體積減 30%images.php 校驗 Referer 或簽名 Tokendata/attachment 刪除 30 天未訪問的衍生文件location ~ ^/data/attachment/ {
try_files $uri @image_processor;
}
location @image_processor {
rewrite ^/data/attachment/(.*)$ /images.php?url=$1 last;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
此方案已在千萬級 PV 門戶站穩定運行 5 年以上,核心優勢在於零侵入業務代碼、按需生成、靜態化緩存、支援任意尺寸,極大降低了運維與存儲成本。