App 發布需提供多種尺寸的 Logo(iOS/Android 各平台規範不同)。美工通常只給 1024x1024 主圖,開發者需手動裁切。本文提供一個 Image 類別,支援縮放、裁切、浮水印、多格式輸出,一鍵產生所有規格並打包下載。
<?php
class Image {
private $source = [];
private $image; // GD 資源
private $ext; // 副檔名
private $error = ''; // 錯誤訊息
public function __construct($source = null) {
if ($source !== null) $this->source($source);
}
// 設定來源(支援檔案路徑或 $_FILES 陣列)
public function source($source) {
if (!is_array($source)) {
$this->source['name'] = $source;
$this->source['tmp_name'] = $source;
$ext = strtolower(pathinfo($source, PATHINFO_EXTENSION));
$types = ['jpg'=>'image/jpeg','jpeg'=>'image/jpeg','gif'=>'image/gif','png'=>'image/png'];
$this->source['type'] = $types[$ext] ?? null;
} else {
$this->source = $source;
}
$this->destination = $this->source['name'];
}
// 縮放(保持比例,僅給寬或高即可)
public function resize($width = null, $height = null) {
if (empty($this->source['tmp_name']) || !file_exists($this->source['tmp_name'])) return;
list($sw, $sh) = getimagesize($this->source['tmp_name']);
if ($width === null && $height !== null) $width = ($sw * $height) / $sh;
if ($height === null && $width !== null) $height = ($sh * $width) / $sw;
if ($width === null && $height === null) { $width = $sw; $height = $sh; }
$create = ['image/jpeg'=>'imagecreatefromjpeg','image/gif'=>'imagecreatefromgif','image/png'=>'imagecreatefrompng'];
$func = $create[$this->source['type']] ?? null;
if (!$func) return;
$src = $func($this->source['tmp_name']);
$this->image = imagecreatetruecolor($width, $height);
// 保持透明度(PNG/GIF)
if (in_array($this->source['type'], ['image/gif','image/png'])) {
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
}
imagecopyresampled($this->image, $src, 0,0,0,0, $width,$height, $sw,$sh);
imagedestroy($src);
}
// 裁切(指定左上座標與寬高)
public function crop($x, $y, $width, $height) {
if (empty($this->source['tmp_name']) || !file_exists($this->source['tmp_name'])) return;
$create = ['image/jpeg'=>'imagecreatefromjpeg','image/gif'=>'imagecreatefromgif','image/png'=>'imagecreatefrompng'];
$func = $create[$this->source['type']] ?? null;
if (!$func) return;
$src = $func($this->source['tmp_name']);
$this->image = imagecreatetruecolor($width, $height);
imagecopy($this->image, $src, 0,0, $x,$y, $width,$height);
imagedestroy($src);
}
// 浮水印
public function watermark($markFile, $opacity = 50, $posX = 'right', $posY = 'bottom') {
if (empty($this->image) || !file_exists($markFile)) return;
$markExt = strtolower(pathinfo($markFile, PATHINFO_EXTENSION));
$mFunc = ['jpg'=>'imagecreatefromjpeg','jpeg'=>'imagecreatefromjpeg','gif'=>'imagecreatefromgif','png'=>'imagecreatefrompng'];
$mark = $mFunc[$markExt]($markFile);
list($mw, $mh) = getimagesize($markFile);
$sw = imagesx($this->image); $sh = imagesy($this->image);
$x = ($posX === 'left') ? 0 : $sw - $mw;
$y = ($posY === 'top') ? 0 : $sh - $mh;
imagecopymerge($this->image, $mark, $x, $y, 0, 0, $mw, $mh, $opacity);
imagedestroy($mark);
}
// 輸出檔案
public function create($dest, $quality = 90) {
if (empty($this->image)) return false;
$ext = strtolower(pathinfo($dest, PATHINFO_EXTENSION));
switch ($ext) {
case 'gif': imagegif($this->image, $dest); break;
case 'png': imagepng($this->image, $dest, round($quality/10)-1); break;
default: imagejpeg($this->image, $dest, $quality); break;
}
return true;
}
// 驗證副檔名
public function validateExtension() {
$allowed = ['image/jpeg','image/pjpeg','image/png','image/gif'];
if (!in_array($this->source['type'] ?? '', $allowed)) {
$this->error .= 'extension;';
}
}
// 驗證檔案大小(KB)
public function validateSize($maxKb) {
if (($this->source['size'] ?? 0) > $maxKb * 1024) {
$this->error .= 'size;';
}
}
// 取得錯誤
public function getErrors() {
$msgs = [];
if (strpos($this->error, 'extension') !== false) $msgs[] = '不支援的檔案格式';
if (strpos($this->error, 'size') !== false) $msgs[] = '檔案過大';
return $msgs;
}
}
?>
<?php
require 'Image.php';
$src = 'logo_1024.png'; // 美工提供的主圖
$specs = [
// iOS
['name'=>'Icon-20.png', 'w'=>20, 'h'=>20],
['name'=>'Icon-20@2x.png', 'w'=>40, 'h'=>40],
['name'=>'Icon-20@3x.png', 'w'=>60, 'h'=>60],
['name'=>'Icon-29.png', 'w'=>29, 'h'=>29],
['name'=>'Icon-29@2x.png', 'w'=>58, 'h'=>58],
['name'=>'Icon-29@3x.png', 'w'=>87, 'h'=>87],
['name'=>'Icon-40@2x.png', 'w'=>80, 'h'=>80],
['name'=>'Icon-40@3x.png', 'w'=>120, 'h'=>120],
['name'=>'Icon-60@2x.png', 'w'=>120, 'h'=>120],
['name'=>'Icon-60@3x.png', 'w'=>180, 'h'=>180],
['name'=>'Icon-76.png', 'w'=>76, 'h'=>76],
['name'=>'Icon-76@2x.png', 'w'=>152, 'h'=>152],
['name'=>'Icon-83.5@2x.png', 'w'=>167, 'h'=>167],
['name'=>'Icon-1024.png', 'w'=>1024,'h'=>1024],
// Android (mipmap)
['name'=>'mipmap-mdpi/ic_launcher.png', 'w'=>48, 'h'=>48],
['name'=>'mipmap-hdpi/ic_launcher.png', 'w'=>72, 'h'=>72],
['name'=>'mipmap-xhdpi/ic_launcher.png', 'w'=>96, 'h'=>96],
['name'=>'mipmap-xxhdpi/ic_launcher.png', 'w'=>144, 'h'=>144],
['name'=>'mipmap-xxxhdpi/ic_launcher.png', 'w'=>192, 'h'=>192],
];
$outDir = __DIR__ . '/output_logos/';
if (!is_dir($outDir)) mkdir($outDir, 0755, true);
foreach ($specs as $spec) {
$img = new Image($src);
$img->resize($spec['w'], $spec['h']);
$dest = $outDir . $spec['name'];
// 確保子目錄存在
@mkdir(dirname($dest), 0755, true);
$img->create($dest);
echo "Generated: {$spec['name']} ({$spec['w']}x{$spec['h']})<br>";
}
// 打包下載
$zip = new ZipArchive();
$zipFile = $outDir . 'app_logos.zip';
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outDir));
foreach ($files as $file) {
if ($file->isFile() && $file->getFilename() !== 'app_logos.zip') {
$zip->addFile($file->getRealPath(), str_replace($outDir, '', $file->getRealPath()));
}
}
$zip->close();
echo "<a href='app_logos.zip'>下載打包 ZIP</a>";
}
?>