All formats

tarfile

LanguagePython
LicensePSF License (Python standard library)
Repositorygithub.com/python/cpython
Documentationdocs.python.org/3/library/tarfile.html
Depends onnothing (standard library)
Read / writeyes / yes (ustar, GNU, pax; gzip/bz2/xz/zstd compressed tar)

tarfile is Python’s built-in TAR module. tarfile.open(path) reads the archive; getmembers() scans all headers once and returns TarInfo objects with names, sizes and data offsets, and extractfile(member) then seeks straight to a member’s data. For an uncompressed CBT that gives random access after one quick scan. Writing with mode "w" produces a plain, uncompressed tar.

Installation#

Nothing to install: import tarfile.

Basic usage: list pages in natural order#

Python
import re
import sys
import tarfile
from pathlib import PurePosixPath

IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif", ".jxl"}


def is_page(name: str) -> bool:
    name = name.replace("\\", "/")
    if "__MACOSX/" in name:
        return False
    p = PurePosixPath(name)
    return not p.name.startswith(".") and p.suffix.lower() in IMAGE_EXTS  # also skips ._001.jpg


def natural_key(name: str):
    return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", name)]


def list_pages(tf: tarfile.TarFile) -> list[tarfile.TarInfo]:
    pages = [m for m in tf.getmembers() if m.isfile() and is_page(m.name)]
    return sorted(pages, key=lambda m: natural_key(m.name))


with tarfile.open(sys.argv[1]) as tf:
    for n, m in enumerate(list_pages(tf), 1):
        print(f"{n:3} {m.name} ({m.size} bytes)")

tarfile.open without a mode detects compression too; use mode="r:" to accept only plain tar, which is what a CBT should be.

How to extract the cover#

Python
with tarfile.open("comic.cbt") as tf:
    pages = list_pages(tf)
    if pages:
        data = tf.extractfile(pages[0]).read()  # seeks to the member's data
        with open("cover" + PurePosixPath(pages[0].name).suffix.lower(), "wb") as f:
            f.write(data)

How to read ComicInfo.xml#

Python
import xml.etree.ElementTree as ET

with tarfile.open("comic.cbt") as tf:
    info = next((m for m in tf.getmembers() if m.isfile() and m.name.lower() == "comicinfo.xml"), None)
    if info:
        root = ET.fromstring(tf.extractfile(info).read())
        print(root.findtext("Series"), "#" + (root.findtext("Number") or ""), root.findtext("Title"))
        print("right to left:", root.findtext("Manga") == "YesAndRightToLeft")
        for page in root.iterfind("Pages/Page"):
            if "FrontCover" in page.get("Type", ""):
                print("cover:", list_pages(tf)[int(page.get("Image"))].name)  # index in sorted order

How to create a CBT from a folder of images#

Python
import tarfile
from pathlib import Path


def make_cbt(folder: str, out: str) -> None:
    files = sorted((p for p in Path(folder).iterdir() if p.is_file() and is_page(p.name)),
                   key=lambda p: natural_key(p.name))
    with tarfile.open(out, "w", format=tarfile.PAX_FORMAT) as tf:  # "w" = no compression
        for i, p in enumerate(files, 1):
            ti = tf.gettarinfo(p, arcname=f"{i:03}{p.suffix.lower()}")
            ti.uname = ti.gname = ""   # don't store local user names
            with open(p, "rb") as f:
                tf.addfile(ti, f)
        info = Path(folder) / "ComicInfo.xml"
        if info.exists():
            tf.add(info, arcname="ComicInfo.xml")


make_cbt("Bone 01", "Bone 01.cbt")

Tips#