Files
catena/crates/booksniff
funkpower 48478fe84e booksniff: a zip of pictures is not a comic until something says so
A zip whose members are all images was called cbz from their names alone,
so a folder of holiday photographs came back as a comic. It is now cbz
only when it carries a ComicInfo.xml or is itself named .cbz or .cbr;
otherwise it is unknown, with a note that it is a zip of N images and
would be a comic book if its name said .cbz. Only the archive's directory
is read, so members are still judged by name, and the README says so,
for a wrapped FictionBook too.

{\rtf has to be the whole control word: the byte after it must be the
version digit, a space or the next control word, so {\rtfoo is not RTF.

A FictionBook saved as UTF-16 was unknown. A byte-order mark in either
order now has the front decoded, at most 2048 code units, before the same
walk to the root element.
2026-11-13 15:49:17 -05:00
..

booksniff

What a file actually is, whatever its name claims.

Part of catena.

What it does

Reads the first few bytes of a file, and an archive's directory at the back, and says what format it really is. It never reads the whole file, so a directory of nine-hundred-megabyte scans costs the same as a directory of anything else.

Writes one doc record per file, with a note for anything worth remarking on: a name that disagrees with the bytes, a file too small to be a book, a name that says "sample".

When you would use it

  • Before a conversion step, so a converter is never handed a format it cannot read. Most conversion failures are this, and the error a converter gives for it is rarely the error you want.
  • Auditing a library you did not build, where files have been renamed, re-saved, and moved between machines that disagreed about extensions.
  • Finding the files that are not books — the samples, the readmes, the cover images that ended up in a folder of PDFs.

A library accumulates files that lie: a DjVu named .pdf, a Word document named .epub, an RTF saved as .doc, an EPUB saved as .zip, a single PDF that somebody zipped and renamed back to .pdf.

Install

cargo install --git https://git.pumpin.online/funkpower/catena booksniff

Example

atlas.pdf is a DjVu that somebody renamed:

$ booksniff --plain book.pdf atlas.pdf chapter.md
pdf	book.pdf
djvu	atlas.pdf
	named .pdf, but the bytes say DjVu
txt	chapter.md
	925 bytes, which is too small to be a book

The record form, which is what a pipeline wants:

$ booksniff book.pdf
{"v":1,"path":"book.pdf","kind":"pdf","declared_ext":"pdf","bytes":17595,"pages":null,"language":null,"text_layer":null,"needs_ocr":null,"notes":[]}

Finding everything in a folder that is not really a PDF:

booksniff library/*.pdf | jq -r 'select(.kind != "pdf") | "\(.kind)\t\(.path)"'

Options

Option Effect
--plain Print a line a person can read instead of the record
--man Write this page as a manual page, in roff

Exit codes

Code Meaning
0 Every file was identified
1 At least one was not
64 The command line was wrong
74 A file could not be read

A file that cannot be read is reported on stderr and the rest still run, so one bad file does not stop a sweep.

What it recognises

Kind How
pdf %PDF-
djvu The IFF container tag
ole2 The compound-file header every binary Word document starts with
epub A zip holding mimetype and META-INF/container.xml
zip-wrapped-pdf A zip whose only real member is one PDF
zip-wrapped-fb2 A zip whose only real member is named .fb2 — the .fb2.zip most FictionBooks are passed around as
cbz A zip whose every real member is a page image (JPEG, PNG, GIF or WebP), and which carries a ComicInfo.xml or is itself named .cbz or .cbr
azw3, mobi A Palm database tagged BOOKMOBI, told apart by the format version inside the first record
lit ITOLITLS, Microsoft Reader's signature
rtf {\rtf at the front, after any byte-order mark and blank space, followed by the version digit, a space or the next control word
fb2 XML whose root element is FictionBook, found by stepping over the declaration, comments and doctype in the first four kilobytes; UTF-16 with a byte-order mark is decoded first
html Markup at the front, after any byte-order mark
txt Text that is none of the above
unknown Anything else, with a note saying what was seen

An archive holding several PDFs is a collection, not a wrapped book: choosing among them would be a guess, and naming one chapter as the whole work is worse than refusing.

A zipped FictionBook is reported as wrapped rather than as fb2, the same way a zipped PDF is: a converter handed it as FictionBook is handed a zip, and the kind is there to stop exactly that.

A comic is the pages and nothing else. Directories, the ComicInfo.xml a comic reader writes into the archive, and the desktop clutter every archive picks up are passed over; anything more — one PDF among the pages, a readme — and the zip is not a comic, and says what it holds instead.

A zip of pictures is as often a holiday as a comic, so images alone are not enough. It is a comic when it carries a ComicInfo.xml, or when its own name ends .cbz or .cbr. Otherwise it is unknown, with a note saying it is a zip of so many images and would be a comic book if its name said .cbz.

Members are judged by name. Only the archive's directory is read, so no member is ever unpacked and a page called 001.jpg is taken to be an image without its bytes being looked at. The same goes for a wrapped FictionBook: the one member is taken to be a FictionBook because its name ends .fb2.

FictionBook is read as bytes rather than as text. A great many are written in windows-1251, which spells its markup in ASCII but is not UTF-8, and would otherwise be called binary.

As a library

use booksniff::bytes::File;

let file = File::open(std::path::Path::new("atlas.pdf"))?;
let doc = booksniff::sniff(&file, "atlas.pdf");
assert_eq!(doc.kind, corpus_core::Kind::Djvu);

sniff works over anything implementing booksniff::bytes::Bytes, which is implemented for &[u8] as well as for a file — so a caller that already has the bytes need not write them to disk first.

See also

  • pdftriage — once you know it is a PDF, whether its text is worth reading.
  • The records — the shape this writes.