mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-22 18:57:14 +00:00
197 lines
6.7 KiB
Markdown
197 lines
6.7 KiB
Markdown
# pdfgutter
|
|
|
|
Finds books scanned two pages to a sheet, and splits them back into pages.
|
|
|
|
Part of [catena](../../README.md).
|
|
|
|
## What it does
|
|
|
|
A book held open under a camera gives one wide image per shot, with two pages
|
|
on it. Everything that reads it afterwards runs the two columns together: the
|
|
lines interleave, the reading order comes out scrambled, and what lands in the
|
|
index looks like prose and is not.
|
|
|
|
`pdfgutter` says whether a PDF is that, and splits it when it is. The split
|
|
changes the page boxes and nothing else — nothing is re-rendered, the text and
|
|
the images are the ones that were there, and a 250-sheet book becomes a
|
|
500-page book at almost no cost in size.
|
|
|
|
## When you would use it
|
|
|
|
- **Before OCR.** Layout models read a spread as one page and produce jumbled
|
|
text, or none.
|
|
- **Before chunking.** A passage that alternates lines between two pages is
|
|
unusable and does not look it.
|
|
- **Sorting a scanned library.** Most books are fine; the answer tells you
|
|
which ones are not.
|
|
|
|
## Install
|
|
|
|
```sh
|
|
cargo install --git https://git.pumpin.online/funkpower/catena pdfgutter
|
|
```
|
|
|
|
Needs `pdftoppm`, from poppler. Nothing is split without first being looked
|
|
at, so it is needed either way.
|
|
|
|
## Example
|
|
|
|
A book photographed two pages at a time:
|
|
|
|
```
|
|
$ pdfgutter scan.pdf
|
|
two pages to a sheet
|
|
12 pages, median ratio 1.40, gutter on 100% of the wide ones
|
|
```
|
|
|
|
An ordinary one:
|
|
|
|
```
|
|
$ pdfgutter book.pdf
|
|
one page to a sheet
|
|
12 pages, median ratio 0.68, gutter on 0% of the wide ones
|
|
$ echo $?
|
|
1
|
|
```
|
|
|
|
Splitting it:
|
|
|
|
```
|
|
$ pdfgutter --into split.pdf scan.pdf
|
|
two pages to a sheet
|
|
12 pages, median ratio 1.40, gutter on 100% of the wide ones
|
|
20 pages written to split.pdf
|
|
```
|
|
|
|
Twelve sheets became twenty pages: eight spreads split in two, and the four
|
|
covers and end leaves, which were single pages already, left alone.
|
|
|
|
What it looked at:
|
|
|
|
```
|
|
$ pdfgutter --json scan.pdf | jq -c '.sampled[:3][]'
|
|
{"number":3,"ratio":1.4,"gutter":true}
|
|
{"number":4,"ratio":1.4,"gutter":true}
|
|
{"number":5,"ratio":1.4,"gutter":true}
|
|
```
|
|
|
|
Over a directory, splitting only what needs it:
|
|
|
|
```sh
|
|
for f in *.pdf; do
|
|
pdfgutter --quiet "$f" && pdfgutter --quiet --into "split/$f" "$f" || cp "$f" "split/$f"
|
|
done
|
|
```
|
|
|
|
## Options
|
|
|
|
| Option | Effect |
|
|
|---|---|
|
|
| `--into <FILE>` | Write the split document here |
|
|
| `--json` | Emit a record — every page looked at, and what it measured |
|
|
| `--quiet`, `-q` | Say nothing; the exit code is the answer |
|
|
| `--min-ratio <RATIO>` | How wide a sheet must be to hold two pages. Default `1.15` |
|
|
| `--gutter-fraction <FRACTION>` | What share of wide sheets must show a gutter. Default `0.6` |
|
|
| `--man` | Write this page as a manual page, in roff |
|
|
|
|
`--into` writes whatever the verdict was, so a loop over a directory does not
|
|
have to branch. A document with no sheet wide enough to cut is not altered at
|
|
all — every page comes through as it was.
|
|
|
|
## Exit codes
|
|
|
|
| Code | Meaning |
|
|
|---|---|
|
|
| `0` | Two pages to a sheet |
|
|
| `1` | One page to a sheet |
|
|
| `2` | It could not be judged — encrypted, no pages, or a page tree that does not add up |
|
|
| `64` | The command line was wrong |
|
|
| `69` | `pdftoppm` is not installed |
|
|
| `74` | The file could not be read or written |
|
|
|
|
## How it decides
|
|
|
|
**Shape is not enough.** A foldout, a plate, a page set in one narrow centred
|
|
column are all genuinely wide, and splitting one down the middle destroys it.
|
|
So the shape of the ink is checked as well.
|
|
|
|
Up to eight pages are sampled, evenly spaced and away from the ends — covers
|
|
and end matter are single pages even in a book otherwise shot two at a time,
|
|
and sampling them would argue against a document that plainly is a spread.
|
|
|
|
Each sampled page that is wide enough to hold two pages gets rendered small
|
|
and read as a column-by-column measure of how much ink is on it. A gutter is:
|
|
|
|
- a **blank column** somewhere in the middle eight per cent of the width,
|
|
- with **real text** in the third of the page to its left,
|
|
- and **real text** in the third to its right.
|
|
|
|
The outer tenth of the sheet is ignored on both sides, because that is where
|
|
a scan carries the shadow of the binding and of the table the book sat on.
|
|
|
|
A document is two-up when the **middle** sampled page is wide enough and
|
|
**at least three in five** of the wide ones showed a gutter. The middle rather
|
|
than the average throughout, so that one plate or one blank leaf does not move
|
|
the answer for a whole book.
|
|
|
|
### The split
|
|
|
|
Each wide sheet becomes two pages pointing at the same content, differing only
|
|
in how much of it they show. A sheet the file asks to be turned on its side is
|
|
cut across rather than down, and the halves come out in the order a reader
|
|
would read them. An annotation is kept on the half it sits on rather than
|
|
copied onto both.
|
|
|
|
Splitting is safe to repeat: the halves are portrait, so a second pass finds
|
|
nothing to do.
|
|
|
|
Splitting replaces the page tree, which means a page this could not find in
|
|
that tree would be a page dropped from the result. So the number of pages
|
|
found is checked against the number the file says it has, and a document where
|
|
those disagree is refused rather than written short.
|
|
|
|
### What it measures
|
|
|
|
The crop box, where a page has one — that is the rectangle a reader shows and
|
|
the one that gets rendered. A wide sheet cropped down to a single page is
|
|
therefore not a spread, which is what stops a second pass cutting a page in
|
|
half. Pages with no crop box, which is most of them, are their media box.
|
|
|
|
## The record
|
|
|
|
`--json` writes one of these, whatever the verdict:
|
|
|
|
| Field | |
|
|
|---|---|
|
|
| `path` | The file it is about |
|
|
| `two_up` | Whether it is two pages to a sheet |
|
|
| `pages` | How many pages it has |
|
|
| `median_ratio` | The middle ratio of the pages looked at |
|
|
| `gutter_fraction` | What share of the wide ones showed a gutter |
|
|
| `sampled` | One entry per page looked at: its `number`, its `ratio`, and whether it had a `gutter` — `null` where the page was too narrow to be worth rendering |
|
|
|
|
It is a shape of its own rather than one of [the records](../../docs/records.md)
|
|
the tools pass between them, because nothing downstream consumes it.
|
|
|
|
## As a library
|
|
|
|
```rust
|
|
let mut pdf = pdfgutter::pdf::Pdf::open(Path::new("scan.pdf"))?;
|
|
for index in pdfgutter::sample_indices(pdf.len()) {
|
|
println!("page {} ratio {:.2}", index + 1, pdf.geometry(index).ratio());
|
|
}
|
|
let pages = pdf.split(pdfgutter::MIN_RATIO);
|
|
pdf.save(Path::new("split.pdf"))?;
|
|
println!("{pages} pages");
|
|
```
|
|
|
|
`pdfgutter::has_gutter` takes a column ink profile and answers on its own, for
|
|
anything that rasterises its own pages. `pdfgutter::ink::profile` turns a
|
|
greymap into one.
|
|
|
|
## See also
|
|
|
|
- [`pdftriage`](../pdftriage/README.md) — whether the text layer is worth
|
|
keeping, which is the next question after this one.
|
|
- [`booksniff`](../booksniff/README.md) — whether the file is a PDF at all.
|