為什麼選 ImageMagick?
- 處理大圖記憶體效率遠優於 GD
- 支援 200+ 格式(含 HEIC、AVIF、TIFF 多頁)
- 指令列工具
convert 功能極豐富(銳化、色彩空間、EXIF 處理)
- 可透過
Imagick PHP 擴充直接呼叫 API,避免 shell_exec 安全風險
環境安裝
# Ubuntu/Debian
apt-get install imagemagick php-imagick
# CentOS/RHEL
yum install ImageMagick ImageMagick-devel
pecl install imagick
echo "extension=imagick.so" > /etc/php.d/imagick.ini
# 驗證
php -r "var_dump(class_exists('Imagick'));"
convert -version
安全上傳與縮圖完整流程
<?php
declare(strict_types=1);
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
const UPLOAD_DIR = __DIR__ . '/uploads/';
const THUMB_DIR = __DIR__ . '/uploads/thumbs/';
const THUMB_WIDTH = 300;
session_start();
// 1. 確保目錄存在且不可直接執行 PHP
foreach ([UPLOAD_DIR, THUMB_DIR] as $dir) {
if (!is_dir($dir)) mkdir($dir, 0755, true);
file_put_contents($dir . '.htaccess', 'php_flag engine off');
}
// 2. 處理上傳
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
$file = $_FILES['image'];
// 基礎驗證
if ($file['error'] !== UPLOAD_ERR_OK) {
die(json_encode(['error' => '上傳失敗:' . $file['error']]));
}
if ($file['size'] > MAX_SIZE) {
die(json_encode(['error' => '檔案過大,限制 5MB']));
}
// MIME 偵測(防止偽裝副檔名)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, ALLOWED_MIME, true)) {
die(json_encode(['error' => '不支援的圖片格式']));
}
// 產生安全檔名
$ext = match ($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
};
$basename = bin2hex(random_bytes(16));
$origPath = UPLOAD_DIR . $basename . '.' . $ext;
$thumbPath = THUMB_DIR . $basename . '_thumb.' . $ext;
// 移動檔案
if (!move_uploaded_file($file['tmp_name'], $origPath)) {
die(json_encode(['error' => '儲存失敗']));
}
// 3. 生成縮圖(使用 Imagick OO 介面)
try {
$img = new Imagick($origPath);
$img->setImageFormat($ext);
$img->thumbnailImage(THUMB_WIDTH, 0, true); // 等比縮放
$img->stripImage(); // 移除 EXIF/GPS 等敏感資訊
$img->writeImage($thumbPath);
$img->destroy();
} catch (ImagickException $e) {
error_log("Thumbnail failed: {$e->getMessage()}");
// 失敗時複製原圖作為備援
copy($origPath, $thumbPath);
}
echo json_encode([
'success' => true,
'original' => '/uploads/' . basename($origPath),
'thumbnail' => '/uploads/thumbs/' . basename($thumbPath),
]);
exit;
}
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>上傳測試</title></head>
<body>
<form id="up" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required>
<button>上傳</button>
</form>
<div id="preview"></div>
<script>
document.getElementById('up').onsubmit = async e => {
e.preventDefault();
const fd = new FormData(e.target);
const res = await fetch('', {method:'POST', body:fd}).then(r=>r.json());
if (res.success) {
document.getElementById('preview').innerHTML =
`<img src="${res.thumbnail}"><br>原圖: <a href="${res.original}" target="_blank">檢視</a>`;
} else {
alert(res.error);
}
};
</script>
</body>
</html>
進階縮圖參數調校
$img->thumbnailImage(300, 300, true); // 限制長邊 300px,等比
$img->cropThumbnailImage(200, 200); // 強制裁切為 200x200 正方形
$img->adaptiveResizeImage(300, 200); // 智慧裁切保留焦點區域
$img->unsharpMask(0.5, 1.0, 1.0, 0.05); // 銳化補償縮放模糊
$img->setImageCompressionQuality(85); // JPEG 品質 85%
效能與安全清單
| 項目 | 建議 |
| 記憶體限制 | php.ini memory_limit 至少 256M;大圖處理可暫時 ini_set('memory_limit', '512M') |
| 執行緒數 | ImageMagick 多執行緒可能佔滿 CPU,設定 MAGICK_THREAD_LIMIT=1 環境變數 |
| 暫存目錄 | policy.xml 限制 <policy domain="resource" name="tmp" value="1GiB"/> |
| 防止殘留進程 | 使用 Imagick 類別而非 shell_exec('convert ...') |
| CDN 整合 | 上傳至 S3/OSS 後觸發 Lambda/Function 非同步生成多規格縮圖 |