Files
catena/crates/pdftriage/README.md
T

138 lines
4.5 KiB
Markdown
Raw Normal View History

# pdftriage
Whether a PDF's text layer is worth reading, or whether it has to be OCRed.
Part of [catena](../../README.md).
## What it does
Extracts a sample of text from the middle of a PDF and judges it: healthy,
garbled, sparse, reflected-font, or absent — and therefore whether OCR is
worth its cost.
It never runs OCR, never writes to the file, and reads two page ranges rather
than the whole document.
## When you would use it
- **Deciding what to OCR.** OCR is the expensive step in any ingest: minutes
of GPU time a page, and a result that is worse than a text layer the file
already had. This is the question worth answering carefully before spending
that.
- **Finding books that are already broken in an index.** A garbled layer
extracts as characters, not words, so it indexes silently and returns
nothing anybody searched for.
- **Recovering a layer that only looks dead.** Some PDFs store every character
code reflected about a fixed point, so the text extracts as punctuation
soup — `ײ¬®±¼«½¬·±²` is "Introduction". That is recoverable in full, which
is strictly better than an OCR pass that could only approximate it.
## Install
```sh
cargo install --git https://git.pumpin.online/funkpower/catena pdftriage
```
Needs `pdftotext` and `pdfinfo` on `PATH`. Both ship with poppler. Without
either, the tool exits `69` and says so rather than guessing.
## Example
A book whose text is fine:
```
$ pdftriage book.pdf
healthy, no OCR needed
1805 characters on the opening pages
1805 characters over 5 sample pages, ascii text score 1.00
letters 0.98, latin-1 0.00
361 characters a page
english function words 0.44
$ echo $?
1
```
A scan with no text at all:
```
$ pdftriage scan.pdf
absent, needs OCR
0 characters on the opening pages
$ echo $?
0
```
The exit code is the answer, so a shell can act on it directly:
```sh
for f in *.pdf; do pdftriage --quiet "$f" && ocrmypdf "$f" "ocr/$f"; done
```
The record form:
```
$ pdftriage --json book.pdf
{"v":1,"path":"book.pdf","kind":"pdf","declared_ext":"pdf","bytes":17595,"pages":20,"language":null,"text_layer":"healthy","needs_ocr":false,"notes":["1805 characters on the opening pages","1805 characters over 5 sample pages, ascii text score 1.00","letters 0.98, latin-1 0.00","361 characters a page","english function words 0.44"]}
```
## Options
| Option | Effect |
|---|---|
| `--json` | Emit a [`doc` record](../../docs/records.md#doc--what-a-file-is) instead of a readable verdict |
| `-q`, `--quiet` | Write no verdict; answer with the exit code alone. Diagnostics still go to stderr, because a silent failure is worse than a quiet one |
| `--man` | Write this page as a manual page, in roff |
## Exit codes
| Code | Meaning |
|---|---|
| `0` | The layer needs OCR |
| `1` | It does not |
| `2` | Could not tell — the extractor could not read the file |
| `64` | The command line was wrong |
| `69` | `pdftotext` or `pdfinfo` is not installed |
| `74` | The file could not be read |
## The verdicts
| Verdict | What it means | OCR? |
|---|---|---|
| `healthy` | Extractable and readable as it stands | no |
| `reflected-font` | Permuted by a non-standard font encoding, and recoverable without OCR once the permutation is undone | no |
| `garbled` | Present but mojibake — extraction yields characters, not words | yes |
| `sparse` | Present but too thin to be the whole page: a scan whose layer holds only the page numbers and the running heads | yes |
| `absent` | No text at all | yes |
The sample comes from the middle of the document, not the front. Front matter
is often a bare scan in a book whose body carries good text, and often cleanly
typeset boilerplate in a book whose body is damaged — judging a book by its
title page gets both wrong.
The thresholds are measurements rather than guesses. Each one sits in the
middle of a gap observed across a few thousand real PDFs, and the reason is
recorded beside it in the source.
## As a library
The judging is a pure function over already-extracted text, so it can be
tested and used without a PDF anywhere in sight:
```rust
use pdftriage::{Limits, Sample};
let found = pdftriage::triage(
&Sample { opening, middle, middle_pages: 5, pages: Some(412) },
&Limits::default(),
);
println!("{:?} needs_ocr={}", found.layer, found.needs_ocr);
```
Running `pdftotext` is the command's job, not the library's.
## See also
- [`booksniff`](../booksniff/README.md) — whether it is a PDF at all.
- [`mdscrub`](../mdscrub/README.md) — cleaning up the text once it has been extracted.
- [The records](../../docs/records.md) — the shape `--json` writes.