PharData
| Language | PHP |
|---|---|
| License | PHP License (bundled phar extension) |
| Repository | github.com/php/php-src |
| Documentation | php.net/manual/en/class.phardata.php |
| Depends on | nothing (the phar extension is enabled by default) |
| Read / write | yes / 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
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#
$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:
$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:
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#
- PharData reads all headers on open; for very large CBT files that takes one pass over the headers each request. Cache the page list.
$tar->extractTo($dir)extracts everything; validate names from untrusted uploads first.