Target: the next printpdf point release. A single static binary that makes the crate's whole pipeline scriptable: PDF ⇄ JSON ⇄ jq ⇄ PDF, plus rendering, text/box extraction, and (later) smart OCR.
| tool | model | strength | gap ppdf fills |
|---|---|---|---|
qpdf |
object-level QDF | lossless structural surgery | no semantic model - you edit raw dicts/streams |
mutool |
render + clean + extract | best-in-class rendering | JSON output is per-purpose, not round-trippable |
pdftk |
page-level | merge/split ergonomics | nothing below page granularity |
pdfcpu |
CLI + Go API | validation, permissions | same: no editable content model |
None of them can do pdf → json | jq '.pages[0].ops |= …' | json → pdf.
printpdf already has the round-trippable serde model (STRUCTS.md is its
TypeScript mirror) - the CLI is mostly plumbing plus flag design.
- New binary crate
ppdf/in this repo (ownCargo.toml, path-dep on printpdf, excluded from the printpdf package - the 10 MiB crates.io cap is at 88%). Published separately:cargo install ppdf. clapderive, subcommand per verb. Everything reads stdin/writes stdout when no file is given, so it composes withjq/pipes.- Exit codes: 0 ok, 1 error, 2 ok-with-error-severity-warnings (printpdf's
PdfWarnMsgstream goes to stderr as JSON-lines with--warnings=json). - Feature-gated heavy deps:
render(resvg rasterization),ocr(tesseract-static). Default build = pure printpdf.
ppdf json in.pdf [-o out.json] [--pages 1-3,7] [--strip-binaries DIR] [--pretty]
ppdf pdf in.json [-o out.pdf] [--subset-fonts] [--no-optimize] [--binaries DIR]
ppdf text in.pdf [--pages …] # plain text (extract_text)
ppdf boxes in.pdf [--pages …] [--glyphs] # hOCR-style JSON (extract_text_boxes)
ppdf svg in.pdf [--pages …] [-o DIR|-] # one SVG per page (to_svg)
ppdf info in.pdf # pages, boxes, fonts, encryption, warnings
ppdf fonts in.pdf [-o DIR] # list / extract embedded font programs
ppdf images in.pdf [-o DIR] # list / extract images
ppdf html in.html [-o out.pdf] [--css …] [--width mm] [--height mm] # from_html
v1.x candidates, in rough priority order: pages (select/reorder/merge -
trivially: json + jq + pdf, but a native verb is friendlier), render (PNG via
resvg, needed for ocr anyway), optimize/compress, validate, ocr.
Non-goals (use qpdf): encryption, linearization, signatures, object-level
repair.
# change every 24pt font to 12pt
ppdf json in.pdf | jq '(.pages[].ops[] | select(.type=="set-font").data.size) |= 12' | ppdf pdf - -o out.pdf
# drop page 3, swap 1 and 2
ppdf json in.pdf | jq '.pages |= [.[1], .[0]] + .[3:]' | ppdf pdf - -o out.pdfTwo things make or break this:
- Binary payload ergonomics. Fonts/images serialize as base64 data-URIs -
a 4 MB font turns
jqinteractive use into molasses.--strip-binaries DIRwrites each payload toDIR/<id>and replaces it with{"$file": "<id>"};ppdf pdf --binaries DIRre-inlines them. jq then operates on a small, readable document. (The wasm demo's tree editor already does exactly this elision in JS -stripResources; this moves the idea into the format.) - Format stability. The serde model is now a public wire format. Add
"$schema": "printpdf-json/1"at the top level, version it, and keep the existing "old array format stays readable forever" discipline. STRUCTS.md becomes the normative doc for it.
Goal: one command that produces complete text boxes for any PDF - born-digital text deterministically, scanned/rasterized text via tesseract - and optionally writes a searchable PDF back out.
ppdf ocr in.pdf [-o out.json] # merged hOCR-style JSON (like `boxes`)
ppdf ocr in.pdf --searchable out.pdf # + invisible text layer (Tr 3) over images
[--lang eng+deu] [--dpi 300] [--tessdata DIR]
Pipeline per page:
- Deterministic pass -
extract_text_boxes→ word/glyph boxes in pt (top-left origin; conveniently already the raster's coordinate orientation). - Rasterize -
to_svg→ resvg → RGBA at--dpi(default 300; scale = dpi/72, so pt→px is one multiply). - Mask - paint every deterministic word bbox (plus ~1px bleed) with the page background before handing the image to tesseract. Sampling the median border color of each box beats plain white on tinted backgrounds; start with white, it covers 99%. Why: tesseract's page segmentation is the slow part and rendered vector text is exactly what it would waste time re-recognizing (imperfectly - producing duplicate, slightly-off text that then has to be deduplicated against the deterministic layer). Blanking solves speed and merge ambiguity in one move.
- OCR - tesseract-static
get_hocr_text(page)→ParsedHocr(has rect bounds). Traineddata: embedded eng/deu from the crate,--tessdata/TESSDATA_PREFIXfor others; write to a temp dir like the crate's example. - Merge - map hOCR px → pt (divide by scale), then interleave OCR words
into the deterministic line structure by baseline proximity; tag every word
"source": "text" | "ocr"(and carry tesseract'sx_wconfasconfidencefor OCR words; deterministic words get 100). Overlap conflicts should be rare because of step 3 - when they happen, deterministic wins. - Searchable PDF (optional) - for each OCR word, emit an invisible-text
run (
SetTextRenderingMode(Invisible), builtin Helvetica, size fitted to the box height,Tzfitted to the box width) at the word's position, then re-save. This is the standard OCRmyPDF-style text layer, built from ops we already have.
In place: extract_text_boxes (pt, top-left, per-word/per-glyph), to_svg,
the JSON model, CID/CMap-correct decoding, tesseract-static with hOCR parsing.
Open (tracked from the parser audit - these bound fidelity, not feasibility):
per-page resource scoping (multi-page /F1 collisions), Form-XObject content
recursion (text inside forms is invisible to both extraction and masking),
page /Rotate, /Widths-as-authoritative advances, MediaBox-origin/CropBox
handling in to_svg. The first two matter most for OCR correctness: unmasked
form text would get double-recognized.
ppdf/crate with the v1 verbs above (json/pdf/text/boxes/svg/info/fonts/ images/html) - pure printpdf, no new deps beyond clap. CI: build + a roundtrip smoke test (json | jq . | pdfon the corpus PDFs).- Parser-audit Tier-1 fixes land in printpdf (they benefit the library regardless of the CLI).
render+ocrfeatures; tesseract-static gets a maintenance pass (it predates the current printpdf; check build on current toolchains).- azul-layout bump rides whichever printpdf release is next, independently.