Ten PDF feature commits landed without an ADR, covering three whole phases.
Every feature gets one; these are the four that were owed. Written against
the code as it stands and re-verified by running it, not transcribed from
the commit messages.
0020 CCITT, JBIG2 and JPEG 2000 — the codecs ADR 0015 refused by name
0021 Phase 4 completion — stamping, reconciliation, CFF, cmap, and the
audit that corrected a false "complete" in ADR 0019
0022 Editing — content_edit, page_ops, flatten, catalog_edit
0023 Redaction and compaction, and the three reader defects they found
Verified rather than assumed, on the tree at f75c1cc:
ccitt 30, jbig2 29, jpx 44 unit tests; jpx_roundtrip's 7 compare against
OpenJPEG-generated pixels, exactly, because a JPEG 2000 bug produces a
plausible image rather than an error.
Redaction re-checked with a test written from outside the module. The
middle line is the one worth keeping:
after redact, secret present = true <- earlier revision
after compact, secret present = false
public text retained = true
Redaction alone leaves the secret in the bytes. That is why the two
shipped together, and why the report carries
earlier_revisions_retain_content.
Determinism re-checked by generating the same document from four separate
processes: byte-identical. The HashMap key-order defect really is fixed.
This ADR contains a correction to its own first draft. 0022 initially
listed text-run rewriting and annotation sub-types as deferred, on the
strength of a grep for "rewrite" finding nothing. Both are implemented —
the rewriter is ContentEditor::set_text, which preserves the operator kind
so a ' keeps its line advance and a TJ keeps its kerning; and "callout" and
"comment" are dart-pdf's names for a FreeText with a /CL and a Text
annotation, not distinct /Subtype values. Checking the plan's claim against
the code was right; concluding from a failed grep was not.
One criterion across the four is left unticked, and it is real: the ui.rs
interaction tests, blocked on the Makepad headless backend since Phase 1.
0021 also records a process note. ADR 0019's merge criteria were derived
from what had been built, so all of them ticked while three unbuilt items
stayed invisible. Criteria should come from the plan text first, then be
marked done or explicitly deferred — a deferral stated is fine, a deferral
unstated is a false claim.
pdf: 1187 passed. No code changed.
6.2 KiB
ADR 0021: finishing Phase 4 — stamping, reconciliation, CFF, and an audit that corrected a false claim
- Status: Accepted
- Date: 2026-08-17
- Review item:
NIGIG_PDF_FEATURE_PARITY_PLAN.md§1 Phase 4, the items ADR 0019 did not implement - Supersedes: nothing
- Related: ADR 0019 (document creation — this closes what it left and corrects what it overclaimed), ADR 0017 (declared-versus-delivered)
- Written retrospectively. The work landed in
8701f5d,89ca518and1220f89without an ADR.
Context
ADR 0019 declared Phase 4 complete. It was not, and the ADR said so over
three unimplemented items. A later audit (89ca518) checked the tree
rather than the commit message and found:
- Field-value reconciliation — setting a value wrote
/V, marked the field dirty and regenerated/AP, which is right for a field we changed. The missing case is a file that arrives with/Vand/APalready disagreeing. Nothing decided that. - Type1/CFF embedding —
sfnt.rsdetected CFF outlines andfont.rsread an existing/FontFile3; nothing wrote one. Creation was TrueType-only. The plan hedges this with "if feasible", so deferring was legitimate — claiming it was done was not. repair-cmap— no equivalent existed.
Also missing from ADR 0019: image embedding and header/footer stamping, both named in the Phase 4 spec.
This is worth recording plainly because it is the same failure this project keeps finding in its own code, one level up. ADR 0019's merge criteria were all ticked; the box that was not on the list was the one that mattered. A checklist derived from what you built cannot tell you what you did not build — only the spec can, item by item.
Decision
Image embedding and stamping (stamp.rs, 835 lines, 27 tests)
embed_image takes a JPEG or raw RGB/grey source and produces an image
XObject; jpeg_dimensions sniffs geometry from the JPEG header rather than
trusting a caller. attach_image_to_page wires it into page resources.
Banner, header_content, footer_content and image_stamp_content
generate the content streams, with Align for placement and
height_for_width/width_for_height so a caller can scale without
distorting.
combine_content concatenates content fragments. Stamping is deliberately
additive content, not a page rewrite: a header that corrupted the page
beneath it would be a poor trade for convenience.
Reconciliation (reconcile.rs, 825 lines, 20 tests)
The important decision here is a refusal. A field's value lives in /V and
its rendered appearance in /AP, and nothing in the format keeps them in
step. PDF 32000-1 §12.7.3.3 settles exactly one case —
/NeedAppearances true means /V wins — and is silent on the other, where
a conforming viewer renders /AP and never consults /V.
So the module does not pick a winner. It classifies the disagreement and
resolves it against a caller-declared Intent, because the correct answer
genuinely differs by caller: a viewer must show /AP to match other
viewers, an extractor must read /V, an editor must regenerate so the saved
file agrees with itself.
Choosing silently would be ADR 0017's failure in a new place — every answer plausible, none checkable, and the caller unaware a decision was made on its behalf.
Two cases are not judgement calls and are handled outright: a missing or
dangling appearance renders blank, and blank is never what a producer
meant, so even Display regenerates; and an unselected radio member showing
/Off while the group's /V names another member is correct, not a
disagreement.
CFF and cmap
CFF embedding and repair-cmap landed in create.rs and subset.rs, with
a checked-in cff_sample.otf fixture and its generator.
External verification became a CI gate
The audit did the thing ADR 0019 could not: ran generated output through
implementations sharing no code with ours — qpdf --check, pdfinfo,
pdftotext, qpdf --list-attachments — and all passed.
tools/check-pdf-external-readers.sh makes it repeatable and pdf.yml runs
it. It treats a qpdf warning as failure, not just an error, because qpdf
warns where it had to reconstruct — and needing reconstruction is exactly
the defect a generator must not ship.
That closes the half of the Phase 4 exit criterion that ADR 0019 recorded as unverifiable. It is a better answer than the one I gave.
Verification
Re-run against the tree at f75c1cc:
reconcile:: 20 passed
stamp.rs 27 #[test] (within pdf-graphics: 289 passed)
tools/check-pdf-external-readers.sh present, executable, wired into pdf.yml
Merge criteria
- Image embedding for JPEG and raw samples
- JPEG geometry read from the codestream, not from the caller
- Header, footer and image stamping with alignment and margins
- Stamping is additive; page content beneath is unmodified
- Reconciliation classifies
/Vvs/APdisagreement - Resolution is driven by a caller-declared intent, not chosen silently
/NeedAppearanceshonoured where the spec settles the case- A blank or dangling appearance is always regenerated
- An unselected radio member is not treated as a disagreement
- CFF embedding, with a checked-in fixture and generator
repair-cmapequivalent- External readers verified and gated in CI
- The plan's status line corrected to match the tree
Consequences
Positive. Phase 4's exit criterion is now enforced by CI against three external tools rather than asserted. Reconciliation gives the viewer, the extractor and the editor each the answer their job requires.
Negative. Intent is a concept a caller must understand to use the API
correctly. The alternative — choosing for them — is worse, but this is a
real cost and the documentation carries it.
Process consequence, and the reason this ADR exists. ADR 0019's merge criteria were self-derived: they listed what had been built, so everything was ticked and three unbuilt items were invisible. Future phase ADRs should enumerate criteria from the plan text first, then mark each done or explicitly deferred — a deferral stated is fine, a deferral unstated is a false claim. ADR 0020 and 0022 follow that shape.