無需系統 `zip` 指令、無需 `ZipArchive` 擴充,純 PHP 實現的 `zipfile` 類可將多張圖片打包為 ZIP 並直接串流輸出下載,避免磁碟 I/O 佔用與臨時檔清理麻煩。本文提供完整可用類別與實戰範例。
<?php
class zipfile {
private $datasec = []; // 檔案資料區
private $ctrl_dir = []; // 中央目錄區
private $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00";
private $old_offset = 0;
// Unix 時間轉 DOS 時間格式
private function unix2_dostime($unixtime = 0) {
$t = ($unixtime == 0) ? getdate() : getdate($unixtime);
if ($t['year'] < 1980) { $t = array('year'=>1980,'mon'=>1,'mday'=>1,'hours'=>0,'minutes'=>0,'seconds'=>0); }
return (($t['year']-1980)<<25) | ($t['mon']<<21) | ($t['mday']<<16) | ($t['hours']<<11) | ($t['minutes']<<5) | ($t['seconds']>>1);
}
// 新增檔案內容
public function add_file($data, $name, $time = 0) {
$name = str_replace('\\', '/', $name);
$dtime = dechex($this->unix2_dostime($time));
$hexdtime = "\x{$dtime[6]}{$dtime[7]}\x{$dtime[4]}{$dtime[5]}\x{$dtime[2]}{$dtime[3]}\x{$dtime[0]}{$dtime[1]}";
eval('$hexdtime = "' . $hexdtime . '";'); // 產生二進位時間字串
$fr = "\x50\x4b\x03\x04"; // Local file header signature
$fr .= "\x14\x00"; // Version needed
$fr .= "\x00\x00"; // General purpose bit flag
$fr .= "\x08\x00"; // Compression method (deflate)
$fr .= $hexdtime; // Mod time/date
$unc_len = strlen($data);
$crc = crc32($data);
$zdata = substr(substr(gzcompress($data), 0, -4), 2); // 移除 zlib header(2bytes) + Adler32(4bytes)
$c_len = strlen($zdata);
$fr .= pack('V', $crc);
$fr .= pack('V', $c_len);
$fr .= pack('V', $unc_len);
$fr .= pack('v', strlen($name));
$fr .= pack('v', 0); // Extra field length
$fr .= $name;
$fr .= $zdata;
// Data descriptor (延續 local header)
$fr .= pack('V', $crc);
$fr .= pack('V', $c_len);
$fr .= pack('V', $unc_len);
$this->datasec[] = $fr;
// Central directory record
$cdrec = "\x50\x4b\x01\x02";
$cdrec .= "\x00\x00"; // Version made by
$cdrec .= "\x14\x00"; // Version needed
$cdrec .= "\x00\x00"; // General purpose
$cdrec .= "\x08\x00"; // Compression
$cdrec .= $hexdtime;
$cdrec .= pack('V', $crc);
$cdrec .= pack('V', $c_len);
$cdrec .= pack('V', $unc_len);
$cdrec .= pack('v', strlen($name));
$cdrec .= pack('v', 0); // Extra field
$cdrec .= pack('v', 0); // File comment
$cdrec .= pack('v', 0); // Disk number
$cdrec .= pack('v', 0); // Internal attr
$cdrec .= pack('V', 32); // External attr (archive)
$cdrec .= pack('V', $this->old_offset);
$this->old_offset += strlen($fr);
$cdrec .= $name;
$this->ctrl_dir[] = $cdrec;
}
// 遞迴新增目錄 (可選)
public function add_path($path, $strip = 0) {
$strip = $strip ?: strlen($path) + 1;
$dh = opendir($path);
while (($file = readdir($dh)) !== false) {
if ($file == '.' || $file == '..') continue;
$full = $path . '/' . $file;
if (is_dir($full)) $this->add_path($full, $strip);
else $this->add_file(file_get_contents($full), substr($full, $strip));
}
closedir($dh);
}
// 組裝完整 ZIP 二進位字串
public function file() {
$data = implode('', $this->datasec);
$ctrldir = implode('', $this->ctrl_dir);
return $data . $ctrldir . $this->eof_ctrl_dir
. pack('v', count($this->ctrl_dir)) // Total entries (this disk)
. pack('v', count($this->ctrl_dir)) // Total entries (total)
. pack('V', strlen($ctrldir)) // Central dir size
. pack('V', strlen($data)) // Local headers size
. "\x00\x00"; // Comment length
}
// 直接輸出下載 (無臨時檔)
public function download($filename = 'download.zip') {
$zipData = $this->file();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . strlen($zipData));
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
echo $zipData;
exit;
}
}
<?php
require_once 'zipfile.class.php';
$zip = new zipfile();
// 要打包的圖片陣列:['src' => 伺服器絕對路徑/URL, 'name' => 壓縮包內顯示名稱(可含子目錄)]
$images = [
['src' => '/var/www/images/photo1.jpg', 'name' => '相簿/photo1.jpg'],
['src' => '/var/www/images/photo2.jpg', 'name' => '相簿/photo2.jpg'],
// 遠端圖片需先 file_get_contents 或 curl 取得二進位
['src' => 'https://cdn.example.com/banner.png', 'name' => 'banner.png'],
];
foreach ($images as $img) {
$content = @file_get_contents($img['src']);
if ($content !== false) {
$zip->add_file($content, $img['name']);
} else {
error_log("圖片讀取失敗: {$img['src']}");
}
}
// 直接串流輸出下載,不寫入磁碟
$zip->download('我的相簿_' . date('Ymd_His') . '.zip');
| 方案 | 依賴 | 記憶體 | 中文檔名 | 適用場景 |
|---|---|---|---|---|
| 純 PHP zipfile 類 | 無 | 全量載入 | 需手動轉碼 | 中小打包、無擴充權限環境 |
| ZipArchive 擴充 | php-zip | 流式寫入磁碟 | 原生支援 UTF-8 (PHP 7.2+) | 生產環境標準選擇 |
| 系統指令 `zip -r` | OS 安裝 zip | 極低 | 原生支援 | 超大打包、備份腳本 |
| Box/Spout | Composer | 串流 | 支援 | 現代專案、大數據匯出 |
根據專案環境與打包規模選擇適當方案;純 PHP `zipfile` 類在「無擴充權限、中小檔案、快速原型」場景極具價值。