Read and write CBZ comic archives with Anansi-schema
ComicInfo.xml metadata. A longbox is the archival box comic collectors
store issues in; this library is the archival format layer for digital
comics.
Zero non-stdlib dependencies — just archive/zip and encoding/xml.
Requires Go 1.21+.
go get github.com/ophymx/longbox
Pages are added in reading order; that order is authoritative. Entries are
named with zero-padded numerals (0001.jpg) so readers that sort by
filename agree with the intended order — numeric input names like 1.jpg
are normalized, not preserved. Images are stored uncompressed (JPEG/PNG
don't deflate; recompressing just burns CPU), and ComicInfo.xml is
written at the archive root with exactly that casing. AddPage rejects
names without a recognized image extension — readers identify pages by
extension, so such an entry would silently read back as junk instead of a
page.
w := longbox.NewWriter(out)
w.SetComicInfo(&comicinfo.ComicInfo{
Series: "Uchuu no Hako",
Number: "1",
Manga: comicinfo.MangaYesAndRightToLeft, // RTL reading direction
})
for _, path := range pagePaths { // already in reading order
f, _ := os.Open(path)
if err := w.AddPage(filepath.Base(path), f); err != nil {
return err
}
f.Close()
}
if err := w.Close(); err != nil {
return err
}The reader tolerates archives from the wild: missing ComicInfo, junk
entries (.DS_Store, __MACOSX/, Thumbs.db), pages nested in
subdirectories, and unpadded numeric names (a natural sort puts 2.jpg
before 10.jpg). Page access is random-access via the zip central
directory — serving page N reads only page N, no unpacking.
a, err := longbox.Open("issue-001.cbz")
if err != nil {
return err
}
defer a.Close()
if ci := a.ComicInfo(); ci != nil { // nil when the archive has none
fmt.Println(ci.Series, ci.Number)
}
for i := 0; i < a.NumPages(); i++ {
rc, err := a.Page(i) // reading order, random access
...
}The comicinfo package covers the full Anansi schema — v2.0 plus the
v2.1 draft fields (Translator, Tags, StoryArcNumber, GTIN) —
including the <Pages> table, creator roles, AgeRating, and
Manga: YesAndRightToLeft for right-to-left reading direction. It can be
used on its own:
ci, err := comicinfo.Read(r) // parse ComicInfo.xml
err = ci.Validate() // enum membership, field ranges
err = ci.Write(w) // UTF-8, no namespace on the root elementMarshaling follows the details strict consumers (Komga, Kavita, KOReader)
require: no namespace on the root <ComicInfo> element, UTF-8 with an XML
declaration, and enum values serialized with exact casing.
CBR/RAR, image processing, and other metadata formats (ComicBookInfo, CoMet, ACBF).
go test ./... runs round-trip tests (write → read yields identical page
order, bytes, and metadata) and validates golden ComicInfo.xml fixtures
against the vendored Anansi XSDs when xmllint is installed (skipped
otherwise — XSD validation is test-only, never a runtime dependency).
Regenerate goldens with go test ./comicinfo -update.