ZipArchive
| Language | PHP |
|---|---|
| License | PHP License (extension), BSD-3-Clause (bundled libzip) |
| Repository | github.com/pierrejoye/php_zip |
| Documentation | php.net/manual/en/class.ziparchive.php |
| Depends on | libzip |
| Read / write | yes / yes (ZIP, ZIP64, AES encryption) |
ZipArchive is PHP’s interface to libzip, in the zip extension. It opens an archive, lists entries by index, and reads one entry with getFromName or getStream without extracting the others. It’s the natural choice for a PHP comic site that serves pages or builds thumbnails from CBZ files.
PHP’s strnatcasecmp is a built-in natural, case-insensitive compare, so sorting pages is one line.
Installation#
The extension is bundled with PHP but often packaged separately:
# Debian / Ubuntu
sudo apt install php-zip
# Fedora
sudo dnf install php-pecl-zip
# Docker official image
docker-php-ext-install zip
Check with php -m | grep zip. On Windows, enable extension=zip in php.ini.
Basic usage: list pages in natural order#
<?php
const IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif', 'jxl'];
function isPage(string $name): bool
{
$name = str_replace('\\', '/', $name);
if (str_contains($name, '__MACOSX/') || str_ends_with($name, '/')) {
return false;
}
$base = basename($name);
return $base[0] !== '.' && in_array(strtolower(pathinfo($base, PATHINFO_EXTENSION)), IMAGE_EXTS, true);
}
function listPages(ZipArchive $zip): array
{
$pages = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
if ($name !== false && isPage($name)) {
$pages[] = $name;
}
}
usort($pages, 'strnatcasecmp'); // "2.jpg" before "10.jpg"
return $pages;
}
$zip = new ZipArchive();
if ($zip->open($argv[1], ZipArchive::RDONLY) !== true) {
exit("can't open $argv[1]\n");
}
foreach (listPages($zip) as $i => $name) {
printf("%3d %s\n", $i + 1, $name);
}
$zip->close();
How to extract the cover#
$zip = new ZipArchive();
$zip->open('comic.cbz', ZipArchive::RDONLY);
$pages = listPages($zip);
if ($pages) {
$ext = strtolower(pathinfo($pages[0], PATHINFO_EXTENSION));
file_put_contents("cover.$ext", $zip->getFromName($pages[0]));
}
$zip->close();
How to serve a page over HTTP#
getStream returns a read stream for one entry, so large pages are not loaded into memory:
$zip = new ZipArchive();
$zip->open($cbzPath, ZipArchive::RDONLY);
$pages = listPages($zip);
$n = (int)($_GET['page'] ?? 0);
if (!isset($pages[$n])) {
http_response_code(404);
exit;
}
$types = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
'gif' => 'image/gif', 'webp' => 'image/webp', 'avif' => 'image/avif'];
$ext = strtolower(pathinfo($pages[$n], PATHINFO_EXTENSION));
header('Content-Type: ' . ($types[$ext] ?? 'application/octet-stream'));
header('Content-Length: ' . $zip->statName($pages[$n])['size']);
fpassthru($zip->getStream($pages[$n]));
How to read ComicInfo.xml#
locateName with FL_NOCASE finds the file whatever its case:
$zip = new ZipArchive();
$zip->open('comic.cbz', ZipArchive::RDONLY);
$idx = $zip->locateName('ComicInfo.xml', ZipArchive::FL_NOCASE);
if ($idx !== false) {
$ci = simplexml_load_string($zip->getFromIndex($idx));
echo "{$ci->Series} #{$ci->Number}: {$ci->Title}\n";
echo "Writer: {$ci->Writer}, pages: {$ci->PageCount}\n";
echo 'Right to left: ' . ((string)$ci->Manga === 'YesAndRightToLeft' ? 'yes' : 'no') . "\n";
// Image is the 0-based index into the sorted page list
foreach ($ci->Pages->Page ?? [] as $p) {
if (str_contains((string)$p['Type'], 'FrontCover')) {
echo 'cover: ' . listPages($zip)[(int)$p['Image']] . "\n";
}
}
}
$zip->close();
How to create a CBZ from a folder of images#
Store pages without compression with setCompressionName:
function makeCbz(string $dir, string $out): void
{
$files = array_values(array_filter(scandir($dir), fn($f) => is_file("$dir/$f") && isPage($f)));
usort($files, 'strnatcasecmp');
$zip = new ZipArchive();
if ($zip->open($out, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new RuntimeException("can't create $out");
}
foreach ($files as $i => $f) {
$name = sprintf('%03d.%s', $i + 1, strtolower(pathinfo($f, PATHINFO_EXTENSION)));
$zip->addFile("$dir/$f", $name);
$zip->setCompressionName($name, ZipArchive::CM_STORE);
}
if (is_file("$dir/ComicInfo.xml")) {
$zip->addFile("$dir/ComicInfo.xml", 'ComicInfo.xml');
}
$zip->close(); // the archive is written here
}
To replace ComicInfo.xml in an existing CBZ, open it without RDONLY and call $zip->addFromString('ComicInfo.xml', $xml): an entry with the same name is overwritten.
Tips#
addFileonly records the path; files are read whenclose()runs, so don’t delete them before that.close()returnsfalseon failure; check it when writing.- For untrusted uploads, check
statIndex($i)['size']before reading, and never extract withextractTowithout validating names.