nigig-org/REVIEWS/adr/0039-pdf-xmp-and-conformance-decisions.md
andodeki ac8f8aa002
Some checks failed
p2p-intel / engine (push) Waiting to run
p2p-intel / notifications (push) Waiting to run
p2p-intel / coverage (push) Waiting to run
p2p-intel / makepad-app (push) Waiting to run
p2p-intel / exchange-tab (push) Waiting to run
Payment domain, storage, platform and UI / isolated-payment-tests (push) Waiting to run
Payment domain, storage, platform and UI / payment-ui-tests (push) Waiting to run
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
chore: sync full working tree to gitdab
Whole-tree sync: cad-core/cad-ui split sources, nigig-build
construction_frame migration, pdf port progress, mpesa/pay/uikit/doc
updates, workspace members/profiles/lock, CI workflows and reviews.
See individual file history for details.
2026-09-12 07:15:24 +03:00

115 lines
No EOL
5.6 KiB
Markdown

# ADR 0039: XMP metadata handling and the conformance/OCR decisions
- **Status:** Accepted
- **Date:** 2026-09-05
- **Review item:** `NIGIG_PDF_FEATURE_PARITY_PLAN.md` Phase 9, "PDF/A
validation helpers", "PDF/UA accessibility checks", "XMP metadata
read/write", "OCR layer"
- **Supersedes:** nothing
- **Related:** ADR 0011 (the PDF/UA structure tree, which the PDF/UA bullet
of this phase is built on), ADR 0021 (the process note that drove the
table format)
## Context
Phase 9 has four bullets. Surveying the existing code before changing
anything:
1. **PDF/UA accessibility checks.** Already done. ADR 0011 covers the
structure tree (`/StructTreeRoot`, `/K`, `/IDTree`, `/ParentTree`,
`/RoleMap`), custom-tag resolution, and typed `AccessibilityIssue`s;
`pdf-document/tests/structure.rs` is the acceptance suite. The module
deliberately carries no `is_pdf_ua` verdict, and a test asserts that
absence. Nothing to build.
2. **XMP metadata read/write.** Genuinely missing on the read side. The
writer (`pdf-cos::writer`) accepts raw XMP bytes (`set_xmp_metadata`) but
nothing parses a `/Metadata` stream back into fields, and there is no
XMP packet generator.
3. **PDF/A validation helpers.** Missing. A PDF/A *check* is a certification
claim, and this project has a standing rule (ADR 0011, ADR 0017) against
mechanical checks wearing a certification hat.
4. **OCR layer.** Missing, and the plan itself flags it "decision required".
dart-pdf binds an external engine; a Rust equivalent is a new heavyweight
dependency.
## Decision
### XMP: read the packet, bridge to `DocumentInfo`, generate a packet
`pdf-document::xmp` (new module, no XML dependency):
- `read_xmp` locates the catalogue `/Metadata` stream (trailer `/Root`
catalog `/Metadata``resolve_stream`, so `/Filter`-decoded bytes arrive
already decompressed) and parses it.
- `parse_packet` scavenges the fields a metadata packet actually carries:
`dc:title` and `dc:description` as `rdf:Alt` containers (the `x-default`
literal wins), `dc:creator` as an ordered `rdf:Seq`, `dc:subject` as a
`rdf:Bag`, and the plain-element forms `pdf:Producer`, `xmp:CreatorTool`,
`xmp:CreateDate`, `xmp:ModifyDate`.
- `to_document_info` merges onto the existing `DocumentInfo` (`/Info`), XMP
winning for the fields `/Info` does not carry (creators list, keyword
list).
- `to_packet` emits a canonical packet (with the `xpacket` PI wrapper and
the `x:xmpmeta` envelope) so writer workflows have a real producer for
`set_xmp_metadata`.
The parser is **a targeted scavenger, not a general XML parser** — it is
tolerant (a foreign or malformed packet yields `None` fields, never an
error opening the document) and it does not validate RDF. That is a
deliberate line: full XMP/RDF parsing is a spec-sized project, and the
fields above are what viewers and workflows actually read.
### PDF/UA: mark done via ADR 0011, nothing new built
The bullet's substance — struct-tree completeness, tag map, alt text — is
ADR 0011's deliverable. The plan now records it as done rather than
re-deriving it.
### PDF/A helpers: defer, do not hand-write
PDF/A conformance is a certification. A hand-rolled "is this PDF/A-1b?"
check risks becoming a boolean downstream trust adopts while checking only
the entries someone remembered. Doing it properly is a spec-grounded pass
over required entries and value ranges, plus a fixture corpus. Not this
pass's work; recorded as deferred with the reason.
### OCR: defer with a recorded decision
nigig will not bind an OCR engine now. Reasons:
- It is a new heavyweight dependency (a Rust engine or a Tesseract binding)
bolted onto a second pipeline, with a different failure model than
extraction.
- The text-first surfaces already delivered (extraction, search, and Phase
8's `document_ai` structure) cover everything a reader can present for a
text PDF; OCR only matters for scanned documents with no text layer.
- Deferring is reversible: the decision is "not yet", not "never". When a
scanned-document workflow becomes a requirement, the seam is clear — an
`OcrLayer` standing between image decode and `PageText`.
## Consequences
- A reader can show canonical metadata from either `/Info` or `/Metadata`,
and a writer can publish the same facts into XMP.
- No XML dependency was added, and the parser's tolerance means a hostile or
foreign packet cannot fail document open.
- The PDF/A and OCR decisions are recorded, so the plan's "decision
required" flag is answered rather than left dangling.
- The Phase 9 exit criterion (full conformance suite) is officially **not
fully met**: PDF/UA and XMP are covered, PDF/A has no suite by decision.
## Merge criteria
Enumerated from the plan bullets first, per ADR 0021.
| Criterion | State |
|---|---|
| XMP packet round-trips through the parser | ✅ `packet_round_trips_through_the_parser` |
| Empty/foreign packets are tolerated, not fatal | ✅ `empty_packet_parses_to_empty_fields`, `garbage_is_tolerated_not_fatal` |
| `x-default` literal wins over other languages | ✅ `x_default_literal_wins_over_others` |
| Plain (non-container) elements parse | ✅ `plain_element_without_container_parses` |
| `XmpMetadata``DocumentInfo` bridge | ✅ `document_info_bridge_uses_first_creator` |
| PDF/UA checks exist and are tested | ✅ ADR 0011 + `tests/structure.rs`; no `is_pdf_ua` boolean |
| PDF/A validation helpers | ❌ **deferred** — certification-level work; reason recorded |
| OCR layer | ❌ **deferred by decision** — not binding an external OCR engine now; reason recorded |
| Conformance test suite over generated + corpus documents | ⚠️ partial — PDF/UA + XMP covered; PDF/A suite not written (deferred) |