All formats

PharData

LanguagePHP
LicensePHP License (bundled phar extension)
Repositorygithub.com/php/php-src
Documentationphp.net/manual/en/class.phardata.php
Depends onnothing (the phar extension is enabled by default)
Read / writeyes / yes (TAR and ZIP; gzip/bzip2 compressed tar)

PharData is the archive class of PHP’s phar extension, which is compiled in by default. It opens a TAR (or ZIP) file, reads all headers once, and then gives access to each file like an array element: $tar['001.jpg']->getContent(). Every file is also reachable as a phar:// stream URL, so file_get_contents, fopen and readfile work on pages inside a CBT.

The phar.readonly setting only blocks writing executable .phar archives; PharData can create and modify tar files with the default settings.

Installation#

Nothing to install; check with php -m | grep -i phar.

Basic usage: list pages in natural order#

RecursiveIteratorIterator walks the archive, including subfolders. strnatcasecmp is PHP’s built-in natural, case-insensitive compare:

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/')) {
        return false;
    }
    $base = basename($name);
    return $base[0] !== '.' && in_array(strtolower(pathinfo($base, PATHINFO_EXTENSION)), IMAGE_EXTS, true);
}

function listPages(string $path): array
{
    $tar = new PharData($path);
    $prefix = 'phar://' . str_replace('\\', '/', realpath($path)) . '/';
    $pages = [];
    foreach (new RecursiveIteratorIterator($tar) as $file) {
        $name = substr(str_replace('\\', '/', $file->getPathname()), strlen($prefix));
        if (isPage($name)) {
            $pages[] = $name;
        }
    }
    usort($pages, 'strnatcasecmp');  // "2.jpg" before "10.jpg"
    return $pages;
}

foreach (listPages($argv[1]) as $i => $name) {
    printf("%3d %s\n", $i + 1, $name);
}

How to extract the cover#

PHP
$pages = listPages('comic.cbt');
if ($pages) {
    $tar = new PharData('comic.cbt');
    $ext = strtolower(pathinfo($pages[0], PATHINFO_EXTENSION));
    file_put_contents("cover.$ext", $tar[$pages[0]]->getContent());
}

To send a page to the browser without loading it into memory, use the stream URL: readfile('phar://' . realpath('comic.cbt') . '/' . $pages[0]).

How to read ComicInfo.xml#

Array access is case-sensitive, so look up the name in the listing first:

PHP
$tar = new PharData('comic.cbt');
foreach ($tar as $file) {  // top-level entries
    if (strcasecmp($file->getFilename(), 'ComicInfo.xml') !== 0) {
        continue;
    }
    $ci = simplexml_load_string($file->getContent());
    echo "{$ci->Series} #{$ci->Number}: {$ci->Title}\n";
    foreach ($ci->Pages->Page ?? [] as $p) {
        if (str_contains((string)$p['Type'], 'FrontCover')) {
            echo 'cover: ' . listPages('comic.cbt')[(int)$p['Image']] . "\n";  // index in sorted order
        }
    }
}

How to create a CBT from a folder of images#

Create the archive with Phar::TAR as the format and add files under zero-padded names:

PHP
function makeCbt(string $dir, string $out): void
{
    $files = array_values(array_filter(scandir($dir), fn($f) => is_file("$dir/$f") && isPage($f)));
    usort($files, 'strnatcasecmp');

    @unlink($out);
    $tar = new PharData($out, 0, null, Phar::TAR);
    foreach ($files as $i => $f) {
        $tar->addFile("$dir/$f", sprintf('%03d.%s', $i + 1, strtolower(pathinfo($f, PATHINFO_EXTENSION))));
    }
    if (is_file("$dir/ComicInfo.xml")) {
        $tar->addFile("$dir/ComicInfo.xml", 'ComicInfo.xml');
    }
}

$tar->addFromString('ComicInfo.xml', $xml) adds or replaces the metadata in an existing CBT.

Tips#