Help us shape the future of Go! Take the 2026 Go Developer Survey

corpus

module
v0.0.0-...-e47e22b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: BSD-3-Clause

README

corpus — html2pdf validated against real public pages

A repeatable harness that fetches each URL in urls.txt, renders it with html2pdf, and records whether it survived plus a few robustness signals — page count, extracted-text length, timing. Same spirit as engine-webengine/bench: real pages surface real shapes a hand-written test never would.

Why a separate module

corpus/ is its own Go module with a local replace github.com/go-pdfkit/html2pdf => .., so go build ./... / go test ./... from the repo root never see it and the library's own dependency graph stays small.

Run

cd corpus
go run ./cmd/corpus -urls urls.txt

Requires pdfinfo and pdftotext (poppler) and pdftoppm on PATH.

Outputs (relative to the working directory):

  • results.json — per-URL {url, ok, pages, pdf_bytes, text_chars, fetch_ms, render_ms, error}
  • CORPUS.md — the run header + table. The hand-written analysis below the BEGIN ANALYSIS marker is preserved across re-runs; only the table above it is overwritten.
  • out/<slug>.pdf and out/<slug>-p1-*.png — the rendered PDF and a PNG of its first page, for a quick visual check without opening every PDF by hand.

Adding a URL

Append it to urls.txt with a one-line comment saying what real-world shape it's meant to stress (a nested layout table, a sticky sidebar, a huge data table, ...) — the same convention bench/urls.txt uses. Prefer genuinely document-shaped public pages (articles, specs, references) over app shells; this tool has no JavaScript, so a JS-heavy SPA will only ever show its static skeleton — which is a legitimate thing to keep one fixture around to prove, not a reason to avoid the corpus entirely.

Timing against a reference: cmd/bench

cmd/bench times html2pdf against headless Chrome's own print-to-PDF on the same inputs — the corpus URLs plus self-contained local fixtures — and writes BENCH.md (same preserved-analysis convention as CORPUS.md, via internal/mdreport) and bench.json.

go build -o /tmp/html2pdf-bin ../cmd/html2pdf     # time the binary, not the compiler
go run ./cmd/bench -html2pdf /tmp/html2pdf-bin -n 5

Both tools run as child processes under /usr/bin/time -l, so the numbers are symmetric: wall-clock from "give me a PDF of this input" to the file on disk — process start-up and, for a URL, the fetch included — plus peak resident memory. Runs are interleaved A/B/A/B… and reported as medians, so a background load hits both sides alike; the load average at start is written into the report header because it cannot be removed. Chrome gets --virtual-time-budget (default 8 s) so a page's resources can settle before it prints; html2pdf fetches the page's images inside its own run. Read the two columns as "time to a usable PDF", not as a CPU micro-benchmark.

fixtures/longdoc.html is a deterministic ~660 KB document — 180 sections of prose, tables, code blocks and decorated inline runs, inline CSS only, no external resource — regenerated by go run ./cmd/longdoc, so both tools can be timed with nothing on the network. CHROME_BIN overrides the Chrome path.

Page breaks where a browser puts them: cmd/breakcheck

fixtures/breaks.html asks for every kind of break CSS Fragmentation gives a page medium — sections with break-before: page, a table and a figure with break-inside: avoid placed to straddle a page end, an h2 with break-after: avoid, an eight-line paragraph with orphans/widows at 3 — on an @page { size: A5; margin: 15mm }. Chrome's headless print-to-PDF of it is the answer key, frozen once as fixtures/breaks.expected.tsv (marker → page). cmd/breakcheck renders the fixture with html2pdf and reports, marker by marker, whether it landed on the same page:

go run ./cmd/breakcheck -html2pdf /path/to/html2pdf-bin

The bench includes the same fixture, so its pages / text columns show the count against Chrome's on every run.

Every reader, not one: judges.sh

The smallest PDF is worth nothing if a reader in the field disagrees with the others about it. The harness that asks them is the org's, not this repository's: it began here and moved to go-pdfkit/conformance (cmd/judges, conformance #27) so that render, ops and gotex judge their output with the same tool. judges.sh runs it over the corpus PDFs — html2pdf's outputs and, as the control, Chrome's PDFs of the same pages — and writes JUDGES.md + judges.json:

judge what it is gives
qpdf --check structural validator clean / warnings / errors
poppler (pdfinfo/pdftoppm/pdftotext) the reference the others are compared to pages, text, page-1 render
MuPDF (mutool) independent parser + rasteriser pages, text, render
Ghostscript (gs) PostScript-lineage interpreter text, render
pdfium (pdfium_test) Chrome's engine — PDFIUM_TEST=/path or -pdfium pages, text, render
pdf.js (conformance's judges/pdfjs-*.mjs under node) Firefox's engine pages, text, render
Quartz (sips) macOS ImageIO — Preview's engine page-1 render

A judge whose binary is absent is skipped with a note, never faked. Each cell reads pages · text ratio · Δworst (page): pages reported, extracted-text length as a ratio of poppler's (non-whitespace characters — Ghostscript pads lines with layout spaces, pdfium separates runs), and the largest distance of its renders from poppler's over a sample of pages — the first, the middle and the last, at 96 dpi, share of pixels whose grey level moves by more than 48/255 after both are downsampled to the same width — with the page it happened on. Text and page counts cover every page. A consensus column gives the mean pairwise distance between all judges' renders of a page (worst page), so no reader is privileged as the truth. A judge that disagrees on Chrome's PDFs too is judge noise; one that disagrees only on ours is a defect.

./judges.sh                       # PDFIUM_TEST=/path/to/pdfium_test to override the default

The script pins conformance through tools.go + go.mod, and on first run copies conformance's judges/ into .judges-node/ and runs npm ci there (pdfjs-dist + @napi-rs/canvas): the Go module cache is read-only, and pdf.js's scripts cannot travel through go run.

Directories

Path Synopsis
cmd
bench command
Command bench times html2pdf against headless Chrome's own print-to-PDF on the same inputs — the corpus URLs plus self-contained local fixtures — and writes BENCH.md, in the spirit of engine-webengine/bench.
Command bench times html2pdf against headless Chrome's own print-to-PDF on the same inputs — the corpus URLs plus self-contained local fixtures — and writes BENCH.md, in the spirit of engine-webengine/bench.
breakcheck command
Command breakcheck renders the fragmentation fixture (fixtures/breaks.html) with html2pdf and reports, marker by marker, the page each lands on against Chrome's answer key (fixtures/breaks.expected.tsv, recorded once with `chrome --headless --print-to-pdf`): a page break that CSS asked for either happened where a browser put it or it did not.
Command breakcheck renders the fragmentation fixture (fixtures/breaks.html) with html2pdf and reports, marker by marker, the page each lands on against Chrome's answer key (fixtures/breaks.expected.tsv, recorded once with `chrome --headless --print-to-pdf`): a page break that CSS asked for either happened where a browser put it or it did not.
corpus command
Command corpus fetches each URL in urls.txt, renders it with html2pdf, and records whether it survived, how big the result is, and how much text came out the other end — a repeatable signal for real-world robustness, in the spirit of engine-webengine's own bench/ harness.
Command corpus fetches each URL in urls.txt, renders it with html2pdf, and records whether it survived, how big the result is, and how much text came out the other end — a repeatable signal for real-world robustness, in the spirit of engine-webengine's own bench/ harness.
longdoc command
Command longdoc writes a deterministic, self-contained long HTML document — headings, prose, tables, code blocks, styled inline runs, inline CSS only, no external resource — so a renderer can be timed on pure layout+paint with nothing on the network for either side of a comparison.
Command longdoc writes a deterministic, self-contained long HTML document — headings, prose, tables, code blocks, styled inline runs, inline CSS only, no external resource — so a renderer can be timed on pure layout+paint with nothing on the network for either side of a comparison.
internal
mdreport
Package mdreport writes a generated Markdown report while preserving the hand-written analysis a previous run left below a marker line — the convention engine-webengine/bench established for its REPORT.md, shared here by the corpus and bench harnesses so a re-run never clobbers notes.
Package mdreport writes a generated Markdown report while preserving the hand-written analysis a previous run left below a marker line — the convention engine-webengine/bench established for its REPORT.md, shared here by the corpus and bench harnesses so a re-run never clobbers notes.
pdfstat
Package pdfstat counts things in a PDF — link annotations above all — through go-pdfkit/reader, the org's own parser: cross-reference streams, object streams and filters are its job, so a document written with PDF 1.5 object streams (where the annotation dictionaries live packed and compressed) counts the same as a classic one, and a count is a count of objects on pages rather than of a byte pattern in the file.
Package pdfstat counts things in a PDF — link annotations above all — through go-pdfkit/reader, the org's own parser: cross-reference streams, object streams and filters are its job, so a document written with PDF 1.5 object streams (where the annotation dictionaries live packed and compressed) counts the same as a classic one, and a count is a count of objects on pages rather than of a byte pattern in the file.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL