All formats

ZipArchive

LanguagePHP
LicensePHP License (extension), BSD-3-Clause (bundled libzip)
Repositorygithub.com/pierrejoye/php_zip
Documentationphp.net/manual/en/class.ziparchive.php
Depends onlibzip
Read / writeyes / 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:

Shell
# 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
<?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#

PHP
$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:

PHP
$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:

PHP
$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:

PHP
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#