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 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 |
azw3, mobi |
A Palm database tagged BOOKMOBI, told apart by the format version inside the first record |
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.
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.