M28 — PDF 2.0 conformance: PDF/UA-2, WTPDF, PDF/A-4
State: to do — Depends on: M03, M13, M17, M20 — On the PDF 2.0 writer by ADR 40, which kept PDF/UA-2, Well-Tagged PDF and PDF/A-4 for this milestone
Goal
Generate accessible and archival documents on PDF 2.0 — PDF/UA-2 and Well-Tagged PDF on the PDF 2.0 structure namespace, PDF/A-4, 4f and 4e — with their identifications, their PDF Declarations and structure destinations for every internal link; read and merge structure trees that mix the PDF 1.7 and 2.0 namespaces as ISO/TS 32005 says; and judge a third party's PDF/UA-2 and Well-Tagged PDF claims on M20's engine.
ADR 40 put PDF/UA-1 first because Factur-X needs PDF/A-3, which is a PDF 1.7 format, and an accessible invoice
must carry both. Everything else that asks for accessible or archival PDF is moving to PDF 2.0: ISO 14289-2
(PDF/UA-2, 2024) and the PDF Association's Well-Tagged PDF 1.0 (2024) are what accessibility checkers now
implement, and ISO 19005-4 (PDF/A-4, 2020) is the archival part built on PDF 2.0, with 4f carrying attachments
as part 3 did. The structure M13 writes is correct PDF 1.7 and, in 2.0 output, sits in the default namespace
without claiming anything; this milestone writes it in PDF 2.0's own namespace, with the types PDF 2.0 added —
Em, Strong, Aside, Title, FENote, headings past H6 — and says so in the file. The corpus holds PDF/A-4
and 4f documents from veraPDF and WeasyPrint and one hand-written tagged PDF 2.0 file, and no PDF/UA-2 or
Well-Tagged PDF document at all: what the library writes here is judged first by veraPDF, then by documents from
other producers as the corpus gains them.
Scope
In:
- the PDF 2.0 structure namespace in the core's structure writer (M13): namespace dictionaries,
/NSon elements,/Namespaceson the structure tree root,RoleMapNS, the PDF 2.0 standard types and attributes —NoteType,Ref,ContinuedList, theNSOowner —, theArtifactstructure element, and pronunciation hints (PhoneticAlphabet,Phoneme); - ISO/TS 32005: the PDF 1.7 and PDF 2.0 namespaces in one tree — the containment rules enforced by the builder in 2.0 output, the mapping between the two namespaces as data, used by the reader (M15), by the merge (M06) and by validation;
- PDF/UA-2 output from the builder and from the HTML engine: M13's emitter writing the 2.0 namespace, PDF 2.0's
types where HTML has an equivalent, the 1.7 types PDF 2.0 left out kept in the 1.7 namespace, the pre-write checks
under
PdfConformancePolicy, andpdfuaidpart 2 with its revision; - structure destinations for every internal link, bookmark, contents entry and note call, beside a page destination for readers that predate them;
- forms and annotations under PDF/UA-2: M17's fields as
Formin the 2.0 namespace, M11's annotations asAnnot, links asLink; - PDF Declarations: read and written through M14's XMP model, their claim data from the caller, their extension schema under PDF/A-1 to 3;
- Well-Tagged PDF 1.0, reuse and accessibility, identified by declarations;
- PDF/A-4, 4f and 4e generation on the 2.0 writer: the identification,
/Inforeduced, output intents, embedded files by level, 4e's 3D and RichMedia preserved from a received document and never authored (ADR 37), and PDF/A-4 as a merge target under M20's preservation; - validation profiles for PDF/UA-2 and Well-Tagged PDF in
AdCodicem.Pdf.Conformance, on M20's public API, with review items for the conditions only a person can judge, as the roadmap's line says, since M20 leaves these profiles to this milestone; - the tool:
html2pdf --pdf-ua 2,--wtpdf reuse|accessibility,--pdf-a 4|4f|4e;validate --profile pdf-ua-2|wtpdf-reuse|wtpdf-accessibility;metadata declarations.
Out, explicitly:
- MathML — layout, the MathML namespace's elements in our output, formulae as associated files — M30. The MathML namespace is recognized here when reading and role-mapping;
- ruby and vertical writing — M30; warichu — not planned: no CSS specification lays it out (M30);
- Factur-X on PDF/A-4f — read and reported (M14), never written: Factur-X requires PDF/A-3, and ADR 40 names its acceptance of 4f as what would reopen the choice;
- PDF/UA-2, WTPDF or PDF/A-4 together with PDF/A-1 to 3 or PDF/UA-1 — refused when the options are built: different base versions of ISO 32000;
- converting received documents to PDF/A-4 — not planned (M21 converts to parts 2 and 3);
- authoring 3D, RichMedia or multimedia for 4e — never (ADR 37); 4e keeps what a received document carried;
- heuristic tagging of untagged documents and editing a received document's structure — neither in the roadmap;
- PDF/X-6, the PDF 2.0 print part — M29, if a print shop asks for it;
- ISO/TS 32001 to 32004 (hashes, ECC, AES-GCM, the integrity MAC) — M16 reads them, M27 verifies with them;
- declaring a person's review: a declaration's claim data says who claimed and when, as the caller supplies it; the library never claims on a person's behalf that the human conditions were judged.
Dependencies. The roadmap gives M03 (the 2.0 writer and its version policy), M13 (the structure writer and the HTML emitter) and M20 (the public rule engine, claims, verdicts, review items, merge preservation and the PDF/A-4 profile). Through M13 come M12's layout, links, bookmarks and contents (M12.6) and footnotes (M12.7). Also used: M14's XMP model and associated files, M15's structure reader, M06's merge, M09's pagination artifacts in 2.0 output, M11's annotations — all earlier in the chain — and M17's form fields, which M17 itself defers to this milestone under PDF/UA-2, hence M17 in the roadmap's table.
Design
Where it lives
| Part | Where | Why |
|---|---|---|
Namespaces, the 2.0 types and attributes, RoleMapNS, containment, the ISO/TS 32005 table | Core, Structure/ | The structure writer is the core's (M13); the table serves the reader and the merge as well |
| Namespace resolution when reading; mapping a 1.7 part into a 2.0 tree | Core, Structure/ (M15's reader) and Documents/ (M06's merge) | |
| Structure destinations | Core (the actions and destinations writer), fed by M12.6 | Any caller of the builder links to an element |
| PDF Declarations | Core, Metadata/ (M14's XMP model) | Declarations are metadata; the core writes those of its own targets |
| PDF/A-4 targets and part 4's constraint set | Core, beside M14's and M20's | The writer, fonts and builder enforce them |
| The 2.0 mapping of HTML, the PDF/UA-2 and WTPDF pre-write checks | AdCodicem.Pdf.Html, Tagging/ | It reads computed style and the box tree |
| PDF/UA-2 and WTPDF profiles, their review conditions | AdCodicem.Pdf.Conformance | M20's satellite |
No new package, no new dependency.
The PDF 2.0 structure namespace
ISO 32000-2 §14.8.6 defines the PDF 2.0 standard structure namespace beside PDF 1.7's; an element without /NS
is in the default namespace, which is PDF 1.7's — so M13's tree in 2.0 output claims nothing, and a 2.0 tree
names its namespace on every element.
PdfStructureNamespace Pdf17 (http://iso.org/pdf/ssn), Pdf20 (http://iso.org/pdf2/ssn),
MathML (http://www.w3.org/1998/Math/MathML), or a caller's URI with its RoleMapNS
PdfStructureType gains PDF 2.0's types: DocumentFragment, Aside, Title, FENote, Sub, Em, Strong,
Artifact, and headings H7 and beyond; each constant knows its namespaces
PdfStructureOptions gains Namespace, Ref (related elements), PhoneticAlphabet and Phoneme
PdfStructureAttributes gains NoteType (Footnote, Endnote, None), ContinuedList and ContinuedFrom, and
the NSO owner for attributes of another namespace
- Writing. Under a PDF 2.0 tagged target the builder writes one namespace dictionary per namespace used, lists
them in
/Namespaces, and gives every element its/NS— a reference, a few bytes per element. The containment checks M13 enforces for 1.7 grow into the table ISO 32000-2's Annex L gives and ISO/TS 32005 makes normative across namespaces: a violation is the caller's mistake (InvalidOperationException), as in M13. - The types PDF 2.0 left out of its namespace —
Art,BlockQuote,Quote,Note,Reference,BibEntry,Code,TOC,TOCI,Index,Private, as §14.8.6 lists them when the list is encoded — are never retyped into something poorer: an element of one of them is written in the 1.7 namespace, explicitly, and ISO/TS 32005 says how a reader resolves it. The semantics a template gave survive the change of version. - A caller's namespace carries its
RoleMapNS, mapping each of its types to a type of the 2.0, 1.7 or MathML namespace;PdfStructureBuilderrefuses a mapping that does not end in one of them, and a chain that loops. - Memory stays M13's: open elements and a few bytes per page; the namespace dictionaries are written once.
ISO/TS 32005: two namespaces in one tree
- The table. The mapping between PDF 1.7 and PDF 2.0 types, and the containment of one namespace's elements in the other's, are data transcribed from ISO/TS 32005, each row checked against the text when encoded and pinned by a test that fails when the table and the constants disagree.
- Reading. M15's structure reader resolves each element to a standard type: through
/RoleMapfor the default namespace, through each namespace'sRoleMapNS, across namespaces, until a type of 1.7, 2.0 or MathML. A chain is followed iteratively and stops when it revisits a (namespace, type) pair — a cycle, which no valid file has, so no constant bounds it — reported asstructure.role-map-cycleand resolved to nothing. An unknown namespace without a role map is reported (structure.namespace-unknown), its elements treated asNonStructfor extraction. - Merging (M06). A 1.7-tagged part in a 2.0 output keeps its elements in the 1.7 namespace, now named explicitly,
its role map moved into the 1.7 namespace dictionary's
RoleMapNS; M06's union of namespaces, role maps and ID trees applies. Under a PDF/UA-2 target, the part is wrapped in aDocumentFragment— ISO 32000-2's element for content taken from another document — and the claim is kept only when M20's checker, running this milestone's profile, passes the merged tree. - Extraction (M15) and redaction (M19) read a 2.0 tree through the same resolution, so that
Emis inline,FENotea note andAsidea block, whatever namespace said so.
PDF/UA-2 from HTML
M13's emitter, under a PDF 2.0 tagged target, maps as follows; what is not listed maps as in M13:
| HTML | M13 (1.7 namespace) | M28 (2.0 namespace) |
|---|---|---|
| The root | Document | Document, the tree's single root, as PDF/UA-2 asks |
h1–h6, role=heading with aria-level above 6 | H1–H6, levels past 6 capped and reported | H1–Hn, uncapped |
em, strong | transparent | Em, Strong |
b, i, mark, small, sub, sup, cite, dfn, var, time | transparent | transparent — sub and sup are not PDF 2.0's Sub, which is a sub-division of a block such as one line of an address |
aside in the flow, role=complementary | Div | Aside |
Footnotes (float: footnote, M12.7) and DPUB doc-footnote, doc-endnote | Note with /ID | FENote with NoteType, related to its call through Ref both ways, the call a Lbl or Link |
blockquote, q, code, doc-toc and its entries, doc-biblioentry | BlockQuote, Quote, Code, TOC, TOCI, BibEntry | The same, in the 1.7 namespace, explicitly |
| A visible document title | P or a heading | Title, by -adc-pdf-tag: Title only: HTML has no element for it |
| A PDF page placed as a letterhead or an image (M12.6) | an artifact or a Figure | the same |
| Merged parts (M06), in 2.0 output | Part | DocumentFragment |
| Links | Link with OBJR, /Contents | the same, and a structure destination |
| Figures | Figure with /Alt; Caption beside | Figure with /Alt; Caption inside it where the containment table allows it |
| Lists | L, LI, Lbl, LBody, ListNumbering | the same, with ContinuedList and ContinuedFrom for a list resumed after an interruption — an ol whose start continues the previous list's numbering |
| Pronunciation | — | -adc-pdf-phoneme and -adc-pdf-phonetic-alphabet on an element: Phoneme and PhoneticAlphabet |
Pre-write checks. M13's checks, each against ISO 14289-2's clause rather than 14289-1's, and those PDF/UA-2
adds: every element resolves to a type of the 2.0, MathML or — through ISO/TS 32005 — 1.7 namespace; the containment
table; one Document root; every FENote related to its call; every internal link with a structure destination;
pdfuaid:part 2 with its rev; dc:title, DisplayDocTitle, Lang as for UA-1. Where ISO 14289-2 relaxes a
UA-1 requirement — the heading sequence among them — the check follows 14289-2's text, read again at encoding. A
conflict follows PdfConformancePolicy, as in M13: Refuse collects them all and throws, RemoveClaim writes the
document tagged without the claim and reports tagging.conformance-claim-removed.
Structure destinations
- A structure destination (ISO 32000-2 §12.3.2.3) names a structure element rather than a page:
/SD [element /XYZ left top zoom]in aGoToaction. Under a 2.0 tagged target every internal link, bookmark, contents entry and note call gets one, and keeps its page destination/Dbeside it, so that a reader that predates structure destinations still navigates. - From HTML:
href="#id"resolves to the target element's structure element; the page and position are M12.6's, from the same layout — the two destinations cannot disagree, and a test checks that they never do. - Through merges and splits: M06 renumbers elements and remaps
/SDwith them; M07's rewriting ofGoToRintoGoTokeeps/SDwhen the target part is tagged; a destination whose element is removed (a page excluded) keeps its/Dand is reported (link.structure-destination-dropped).
Forms and annotations under PDF/UA-2
M17's fields are written as Form elements of the 2.0 namespace with their OBJR, /TU as their accessible name,
and PrintField for static controls; M11's annotations as Annot with /Contents; links as Link. /Tabs /S on
every page with annotations, as for UA-1.
PDF Declarations
PdfDeclaration ConformsTo (an absolute URI); ClaimData: By, Date, Credentials, Report — each optional,
supplied by the caller
PdfDocumentMetadata gains Declarations, read and written through M14's XMP model
PdfDeclarationUris the URIs the PDF Association's registry defines that the library writes or reads —
Well-Tagged PDF's reuse and accessibility levels among them — as data
- The
pdfdnamespace and its properties —pdfd:declarations, a bag of structures ofpdfd:conformsToandpdfd:claimData(claimBy,claimDate,claimCredentials,claimReport) — as the specification defines them, checked against it when encoded. - The library writes a declaration only for a level its own checks enforced (the WTPDF targets below). A
declaration the caller supplies — an organization's own profile, a review by a named person — is written as given
and reported as not verified (
declaration.unverified): the file says who claimed it. - Under a PDF/A-1, 2 or 3 claim,
pdfdneeds an extension schema, which M14's generator writes; under PDF/A-4 none: veraPDF's part-4 profile checks the identification schema and no extension schema (ISO 19005-4's text read again at the slice). - The claim date comes from the caller, never from the clock (invariant 6).
- Reading: M20's claim reader lists declarations;
PdfConformanceValidatorchecks a WTPDF declaration against its profile, and the verdict says when a declared level does not hold.
Well-Tagged PDF
WTPDF 1.0 defines two levels: reuse — a structure fit for extraction and reflow, with semantic types, reading
order, Unicode and language — and accessibility — reuse plus what assistive technology needs, aligned with
PDF/UA-2. Its only identification is a declaration. PdfConformanceTarget gains WtpdfReuse and
WtpdfAccessibility, on 2.0 output, combinable with PdfUA2 (the accessibility level with UA-2 is the common case)
and with part 4.
PDF/A-4, 4f and 4e
PdfConformanceTarget gains PdfA4, PdfA4F and PdfA4E, on 2.0 output only. M20's constraint data gets part 4's
set; each row is read again against ISO 19005-4 and veraPDF's part-4 profile before it is encoded:
| Area | Requirement | Where enforced | When it cannot be met |
|---|---|---|---|
| File | A %PDF-2.n header with a binary comment, trailer /ID, no encryption, no LZW | M03's 2.0 writer | Encryption with the target: ArgumentException when the options are built |
| Identification | pdfaid:part 4, pdfaid:rev the edition's year, pdfaid:conformance F or E for 4f and 4e, absent for 4 | M14's model | — |
| Metadata | XMP authoritative (ADR 40); no document information dictionary unless the catalog has /PieceInfo, and then /ModDate alone (veraPDF's part-4 profile, 6.1.3 tests 4 and 5) — M03's 2.0 output already moves its entries to XMP | M03, M14 | — |
| Output intents | A GTS_PDFA1 intent with an embedded profile; page-level intents (PDF 2.0) kept consistent with it when a merge brings them | The builder, M06 | A page intent that contradicts the document's: a conflict |
| Fonts and Unicode | Embedded; widths consistent; no .notdef; ToUnicode values valid where present | M08 | M08's fallback, reported |
| Color, transparency, graphics state, XObjects | As part 2, on PDF 2.0's definitions | M08, M12 | A conflict |
| Annotations and actions | As parts 2 and 3; 3D and RichMedia only under 4e | M11, M12.6 | A conflict |
| Embedded files | 4: only PDF/A-1, PDF/A-2 or PDF/A-4 documents — not PDF/A-3 (veraPDF's part-4 profile, 6.9 test 3) —, each with /F, /UF, AFRelationship and a MIME type; 4f: any file as an associated file, with the same entries; 4e: as 4f, and the files 3D and RichMedia annotations use | M06's and M14's associated files | An attachment the level forbids: a conflict |
| Logical structure | None required: part 4 has no level a, and accessibility is PDF/UA-2's | — | — |
- 4e is claimed only for what the library can stand behind: our generator never authors 3D or RichMedia (ADR 37). A 4e document built from the builder is 4f's rules under the E identification; the allowances of 4e matter when a merge (M06) brings in a received engineering document's 3D annotations and RichMedia, kept byte for byte and checked against part 4's annex for 4e, never rewritten.
- PDF/A-4 as a merge target (M20's preservation): parts claiming PDF/A-1, 2 or 3 are rewritten to 2.0 — M03 moves
their
/Infointo XMP — and checked against part 4 before the claim is written; a part whose attachments part 4's level forbids is a conflict, named. - With PDF/UA-2 and WTPDF: one document may claim PDF/A-4f, PDF/UA-2 and WTPDF accessibility together — three identifications, one structure, one set of fonts.
Validation profiles: PDF/UA-2 and Well-Tagged PDF
pdf-ua-2,wtpdf-reuse-1.0andwtpdf-accessibility-1.0join M20's profiles. A requirement PDF/UA-1 and 2 share keeps its identifier —pdfua-font.not-embeddedruns in both, with a reference to each clause —, as the PDF/A rules share theirs across parts; what only PDF/UA-2 asks gets a rule of its own in the same families (pdfua-structure.namespace-unresolved,pdfua-structure.containment-violated,pdfua-navigation.structure-destination-missing), and what only WTPDF asks takeswtpdf-*families (wtpdf-declaration.missing).- Human conditions: PDF/UA-2 has no protocol of its own comparable to Matterhorn as far as this milestone knows
when written; its conditions are transcribed from ISO 14289-2's text as data, with the same review questions as
M20's where the requirement is the same, and replaced by a published protocol if one appears. The verdict of a
UA-2 or WTPDF-accessibility claim is never
Conformsfrom a machine, as for UA-1. - veraPDF's
ua2,wt1randwt1aflavors are the referees; the checklist test of M20 covers their clauses and tests too.
Diagnostics
In PdfDiagnosticCodes, disjoint from rule identifiers (ADR 36):
| Code | Severity | Meaning |
|---|---|---|
structure.role-map-cycle | Warning | A role-map chain loops, across namespaces or within one; its elements resolve to no type |
structure.namespace-unknown | Information | An element's namespace is none the reader knows and has no role map |
tagging.namespace-unresolved | Warning | Under a 2.0 tagged target, an element that resolves to no standard type: a conflict |
tagging.phoneme-invalid | Warning | A pronunciation override the alphabet does not allow; not written |
link.structure-destination-dropped | Information | A structure destination's element was removed; the page destination kept |
declaration.unverified | Information | A caller's declaration written as given |
declaration.malformed | Warning | A declaration read that the specification does not allow: a relative URI, a claim date that is not a date |
write.structure-namespace-declared | Information | A merged part's 1.7 elements given an explicit namespace in a 2.0 output |
Referees
All in containers (ADR 27), pinned by digest:
| Referee | Confirms |
|---|---|
veraPDF (ua2, wt1r, wt1a, 4, 4f, 4e) | Every output at the claimed level; every claim in the corpus, against our profiles |
| pikepdf | Namespaces, /NS, RoleMapNS, containment walked independently; each /SD resolving to the element whose first page is /D's; declarations and pdfaid in open_metadata() |
poppler (pdfinfo -struct-text, pdftotext) | The structure's text order equals the 1.7 tree's for the same source; extraction unchanged by the namespace |
| pdf.js and MuPDF | Every internal link navigates, through its page destination |
qpdf (--check) | Every output sound in 2.0 |
| ExifTool | Declarations, pdfaid:rev, pdfuaid:rev as an independent XMP parser reads them |
The command-line tool
html2pdf --pdf-ua 2 [--wtpdf reuse|accessibility] [--pdf-a 4|4f|4e] [--declare URI]; validate --profile pdf-ua-2|wtpdf-reuse|wtpdf-accessibility; metadata declarations FILE [--json] and metadata declare FILE --uri URI [--by …] [--date …]; merge --conformance pdf-a-4.
Slices
Each slice ends on a green commit, with its codes documented and its measurements recorded in docs/status.md.
- The 2.0 namespace in the structure writer. Delivers
PdfStructureNamespace, the 2.0 types and attributes, namespace dictionaries,/NSand/Namespaces,RoleMapNSfor callers' namespaces, the containment table and its refusals, theArtifactelement, pronunciation hints. Proved by unit tests per type, attribute and refusal; an FsCheck property — any tree the builder accepts in 2.0 output resolves every element to a standard type in a finite chain and satisfies the containment table —; integration: pikepdf walks a builder-made three-page document and finds the namespaces and tree built,qpdf --checkis silent, M02's object-shape rules generated from the Arlington model (ADR 44) report nothing on its 2.0 dictionaries, and veraPDF'sua2profile finds no failure in its structure clauses. Leaves ISO/TS 32005. - ISO/TS 32005: reading and merging two namespaces. Delivers the 1.7–2.0 table, resolution across namespaces in
M15's reader with cycles reported, the 1.7 types kept in their namespace, M06's merge of a 1.7 part into a 2.0 tree
in a
DocumentFragment, extraction and redaction through the resolution. Proved by unit tests over the table and hostile role maps (a 100,000-link cycle, a namespace chain through every type); integration: the remote hand-written PDF 2.0 file's tree resolves as pikepdf reads it; the three committed PDF/UA-1 documents merged into a 2.0 volume keep every element resolvable, their text in order inpdfinfo -struct-text. Leaves the HTML engine. - PDF/UA-2 from HTML. Delivers the 2.0 mapping table in the emitter,
FENotewithNoteTypeandRef, headings past six,Titleby override, the pre-write checks against ISO 14289-2,pdfuaidpart 2,html2pdf --pdf-ua 2. Proved by unit tests over box trees per mapping row and per conflict; integration: veraPDF'sua2profile passes the reference documents; their structure's text order equals that of M13's 1.7 rendering of the same sources. Leaves navigation. - Structure destinations. Delivers
/SDbeside/Dfor links, bookmarks, contents entries and note calls, their remapping by M06 and M07, the dropped-destination report. Proved by unit tests, and a property — for every internal link written,/SD's element's first page is/D's page —; integration: pikepdf resolves every/SDin the reference documents; pdf.js and MuPDF navigate every link; veraPDF'sua2passes. Leaves forms. - Forms and annotations under PDF/UA-2. Delivers M17's fields as
Formin the 2.0 namespace,Annotfor M11's annotations,Link,/Tabs /S. Proved by unit tests; integration: M17's onboarding pack under PDF/UA-2 passes veraPDF'sua2, and qpdf and pikepdf list its fields with their names, types and values. Leaves declarations. - PDF Declarations. Delivers
PdfDeclaration,PdfDeclarationUris, reading and writing through M14's model, the extension schema under parts 1 to 3, claim data from the caller,metadata declarationsanddeclare. Proved by unit tests (a relative URI, a missing claim date, several declarations, a declaration in a superseded packet); integration: ExifTool reads every declaration we write;libreoffice-report-pdfa2b.pdfgiven a declaration keeps the PDF/A-2b claim veraPDF upheld. Leaves Well-Tagged PDF. - Well-Tagged PDF. Delivers the
WtpdfReuseandWtpdfAccessibilitytargets and their pre-write checks, their declarations, and--wtpdf. Proved by unit tests per check; integration: veraPDF'swt1randwt1apass the reference documents under each level. Leaves PDF/A-4. - PDF/A-4, 4f and 4e. Delivers the targets, part 4's constraint set, the identification, output intents
including page-level ones kept by merges, attachments by level, 4e for preserved engineering content, the triple
claim with PDF/UA-2 and WTPDF,
--pdf-a 4|4f|4e, PDF/A-4 as a merge target. Proved by unit tests per row and per refusal; integration: veraPDF's4,4fand4epass the reference documents, the invoice with its source HTML and a CSV of its lines attached passes4f, and the triple claim passes4f,ua2andwt1atogether. Leaves validation. - Profiles for PDF/UA-2 and Well-Tagged PDF. Delivers the three profiles, the shared and new rules, the UA-2
human conditions as review data, the checklist test extended to veraPDF's
ua2,wt1randwt1aclauses. Proved by unit tests per rule (M20's triad); integration: on every output of this milestone and every document of the corpus that claims PDF/UA-2 or declares WTPDF — none today (below) —, our verdicts and failed clauses equal veraPDF's. Leaves the corpus closed. - The corpus closed. Delivers the acceptance conditions below, the outputs committed to the corpus,
Pdf20TaggingBenchmarks, the documentation. Proved by the acceptance conditions.
Tests required
Unit —
- The writer: each 2.0 type and attribute;
/NSon every element and the namespace dictionaries written once; a caller's namespace and itsRoleMapNS; every containment refusal; the 1.7 types kept in their namespace; theArtifactelement; pronunciation hints and their alphabets. - ISO/TS 32005: every row of the table; resolution through
RoleMapandRoleMapNSin both directions; a cycle; an unknown namespace; a merge's 1.7 part given its namespace explicitly and its role map moved. - The emitter: every row of the 2.0 mapping;
subandsupstaying transparent;FENoterelated to its call both ways; headingsH7and beyond;Titleonly by override; each PDF/UA-2 conflict underRefuseandRemoveClaim. - Structure destinations:
/SDand/Dagreeing; remapped by a merge; dropped with an excluded page; kept by M07'sGoToRrewriting. - Declarations: every property; the extension schema under parts 1 to 3 and none under part 4; caller dates only; a declaration never written by the library for a level it did not check.
- PDF/A-4: every row of the requirement table; the identification per level; a PDF/A-3 part in a 4f merge; a 4e document's preserved 3D annotations byte for byte; the refusal of 1.7-based claims beside a 2.0 target.
- Profiles: each rule, a triggering, a passing and a legal-but-unusual document; the shared identifiers with both references; the review conditions' applicability.
- Hostile: a tree of 1,000,000 elements each in a namespace of its own; a
RoleMapNSchain through 100,000 namespaces ending in a cycle;/Namespacesholding a million entries; an/SDpointing at a page object, at a deleted element, at itself; a declaration packet with 100,000 claim data entries — each bounded in time and memory, never an exception. - Properties (FsCheck): resolution is total and terminates on any role-map graph; any accepted tree satisfies the containment table; two runs give identical bytes.
Integration — in containers (ADR 27): veraPDF (ua2, wt1r, wt1a, 4, 4f, 4e) on every output and every
claim in the corpus; pikepdf, poppler, qpdf and ExifTool on every output; pdf.js and MuPDF on every link.
Acceptance conditions
"The reference documents" are M12's renderings of sources/invoice-fr.html, report-fr.html and
contract-fr.html, and M13's accessibility reference, here rendered on the PDF 2.0 writer under the targets named.
Rows naming remote documents close only on a green Remote corpus run.
| Documents | Behavior | Verified by |
|---|---|---|
| The reference documents under PDF/UA-2 | veraPDF's ua2 reports no error; our pdf-ua-2 profile finds no machine failure and returns ConformsPendingReview; pdfuaid part 2 with its revision | Pdf20TaggingRefereeTests.Reference_documents_pass_pdf_ua_2 (new) |
| The same, beside M13's PDF/UA-1 renderings of the same sources | pdfinfo -struct-text gives the same text in the same order; em and strong are Em and Strong, aside is Aside, the accessibility reference's footnotes are FENotes each related to its call, and nothing is lost of the 1.7 types PDF 2.0 dropped | CorpusPdf20StructureTests.The_2_0_tree_says_what_the_1_7_tree_said_and_more (new) |
| The reference documents under WTPDF reuse, and under WTPDF accessibility with PDF/UA-2 | veraPDF's wt1r, and wt1a with ua2, report no error; each carries the declaration of its level, as ExifTool reads it | Pdf20TaggingRefereeTests.Reference_documents_pass_wtpdf_at_each_level (new) |
The reference documents under PDF/A-4 and 4f — the invoice with its source HTML (Source) and a CSV of its lines (Data) attached —, and under 4e | veraPDF's 4, 4f and 4e report no error; pdfaid part 4 with its rev and level; /Info as part 4 allows | PdfA4RefereeTests.Reference_documents_pass_pdf_a_4_at_every_level (new) |
| The invoice under PDF/A-4f, PDF/UA-2 and WTPDF accessibility together | veraPDF passes 4f, ua2 and wt1a in one run; our three verdicts agree | PdfA4RefereeTests.The_triple_claim_passes_every_profile (new) |
| Every internal link, bookmark, contents entry and note call in the reference documents | /SD resolves, by pikepdf, to the element whose first page is /D's page; pdf.js and MuPDF navigate each through /D | Pdf20TaggingRefereeTests.Every_internal_link_has_a_structure_destination_that_agrees (new) |
| M17's onboarding pack under PDF/UA-2 | veraPDF's ua2 passes; qpdf and pikepdf list every field with its name, type and value; each widget in a Form of the 2.0 namespace | Pdf20TaggingRefereeTests.Forms_pass_pdf_ua_2 (new) |
vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf, indesign13-pdfua1-german-book-chapter.pdf and indesign15-pdfua1-form.pdf (PDF/UA-1, the 1.7 namespace) merged with the reference report under PDF/UA-2 | Each part in a DocumentFragment, its elements in the 1.7 namespace named explicitly; every element resolves (pikepdf agrees); the UA-2 claim kept when our profile and veraPDF's ua2 both find no machine failure, and otherwise removed with the rule named | CorpusPdf20StructureTests.Merging_1_7_tagged_parts_into_a_2_0_document (new) |
vendor/eu-publications/pdflib-oj-exchange-rates-greek.pdf (PDF/A-2a), documents/archival/libreoffice-report-pdfa2b.pdf (PDF/A-2b), vendor/verapdf/pdfa3b-embedded-pass.pdf (PDF/A-3b) merged with the reference invoice under PDF/A-4f | Each part rewritten to 2.0 and checked against part 4 first; veraPDF's 4f passes the volume; the part-3 attachment kept as an associated file | CorpusPdf20StructureTests.Pdf_a_parts_merge_into_a_pdf_a_4f_volume (new) |
Remote remote/opf-format-corpus/acrobat9-portfolio-signed-3d.pdf: its members with U3D and PRC models, unpacked by M07 and assembled under PDF/A-4e | The 3D annotations and their streams byte for byte as they came, nothing authored; veraPDF's 4e passes, or — since one model carries a script — the plan names what part 4's annex forbids, the claim is not written, and veraPDF fails exactly those clauses | CorpusPdf20StructureTests.Engineering_content_is_preserved_under_pdf_a_4e (new) |
vendor/verapdf/pdfa4-metadata-pass.pdf, vendor/verapdf/pdf20-version-mismatch.pdf; remote remote/mustang/dwc-weasyprint-facturx-extended-pdfa4f.pdf and remote/pdf20examples/handwritten-pdf20-utf8-strings.pdf | Read: claims, declarations and namespaces as pikepdf and ExifTool see them; the DWC invoice's Factur-X on PDF/A-4f reported, never written; a stamp (M09) and a merge keep each PDF/A-4 claim veraPDF upheld | CorpusPdf20StructureTests.Pdf_2_0_documents_read_and_keep_their_claims (new) |
| Every document of the corpus that claims PDF/UA-2 or declares WTPDF, once the corpus holds them (below), and every output above | Our pdf-ua-2, wtpdf-reuse and wtpdf-accessibility verdicts and failed clauses equal veraPDF's; each disagreement fixed or recorded in the manifest with its reason | Ua2DifferentialTests.Our_ua_2_and_wtpdf_verdicts_are_verapdfs (new) |
| The reference invoice asked for PDF/A-3b with PDF/UA-2, and for Factur-X on PDF/A-4f | Refused when the options are built, naming the base versions and ADR 40 | Pdf20TaggingTests.Claims_on_different_base_versions_are_refused (new) |
| M12's long report at 1,000 pages under PDF/UA-2 and PDF/A-4 | Memory flat as pages grow, the overhead over M13's 1.7 tree measured (the /NS references) and recorded | CorpusPdf20StructureTests.Tagging_in_2_0_holds_its_budget (new), Pdf20TaggingBenchmarks (new) |
| Every output above | Two runs give identical bytes | CorpusPdf20StructureTests.Pdf_2_0_output_is_deterministic (new) |
| The same operations through the tool | The verbs produce the API's bytes and reports | CorpusToolTests.Pdf_2_0_conformance_verbs_match_the_api (new) |
Corpus
What the corpus holds
- PDF/A-4: veraPDF's passing metadata fixture and its PDF 2.0 version-mismatch fixture (a part-4 failure whose
claim the manifest does not record), and remote WeasyPrint's PDF/A-4f Factur-X invoice from DWC's generator — two
rdf:RDFblocks in one packet,factur-x.xmltwice in/AF,/Infodown toModDate. - Tagged PDF 2.0: remote, the PDF Association's hand-written UTF-8 example — a tagged page, UTF-8 alternate text, a duplicated MCID.
- PDF/UA-1 in the 1.7 namespace to merge into 2.0 volumes: PDFlib's invoice, InDesign's book chapter and form,
committed; AbleDocs' two documents and InDesign CS6's brochure, remote — role maps, class maps, custom types,
ActualText, notes, tables, lists, links withOBJR. - Engineering content, remote: Acrobat 9's portfolio with U3D and PRC models inside its members.
- PDF/A-1 to 3 claims veraPDF upholds, from part 1 to 3u, for PDF/A-4 merges (M20's list).
What it lacks
| Need | Why | Priority | Likely source |
|---|---|---|---|
PDF/UA-2 documents from other producers, each with veraPDF's ua2 verdict | The pdf-ua-2 profile cannot be accepted on our own outputs alone (invariant 10), and the reader's namespace resolution has one hand-written file to go on | 1 | Generated here: WeasyPrint (a Python producer; the version and whether it writes PDF/UA-2 checked when run) and LibreOffice's PDF/UA export if its pinned version offers part 2; a public source: the veraPDF corpus's PDF/UA-2 test files (CC BY 4.0), where it publishes them |
| Documents declaring Well-Tagged PDF, reuse and accessibility | The WTPDF profiles and the declaration reader have nothing third-party to judge | 1 | A public source: the veraPDF corpus's WTPDF test files, and the PDF Association's own publications that declare WTPDF — their license checked, remote if it is not attribution-only |
Pass and fail fixtures for each clause of veraPDF's ua2, wt1r and wt1a profiles | A profile of dozens of rules needs a pair per rule, as M20's gap says for the other parts | 1 | A public source: the veraPDF corpus (CC BY 4.0), a selection committed or a pinned archive in the remote corpus (ADR 33) |
| Our own PDF/UA-2, WTPDF and PDF/A-4, 4f and 4e renderings of the reference documents, committed | Later milestones — M30 on PDF/UA-2 above all — accept on them | 1 | Generated here, by this milestone, recorded in build_corpus.py |
The PDF/A-4 claim of vendor/verapdf/pdf20-version-mismatch.pdf recorded in the manifest, with veraPDF's verdict | The fixture escapes every claimed-document row today | 1 | Generated here: the list-valued claim field M20 asks for |
| PDF/A-4e documents with 3D or RichMedia that veraPDF upholds, and a committed one | 4e's allowances are checked against one remote portfolio whose members make no part-4 claim | 2 | A public source: the veraPDF corpus's PDF/A-4e files; a contribution |
| A PDF 2.0 document using structure destinations from another producer | The reader's /SD resolution and M07's rewriting are checked on our output only | 2 | A public source: the pdf20examples set (CC BY-SA 4.0, so remote), or a producer's sample |
A PDF 2.0 document whose tree mixes the 1.7 and 2.0 namespaces with RoleMapNS, from another producer | ISO/TS 32005 reading is otherwise proven on trees we built | 2 | Generated here with a producer that writes both, if one in the container does; else a public source |
| A PDF 2.0 document with page-level output intents | Part 4 allows them and the merge keeps them consistent; no document has one | 2 | A public source: the pdf20examples set's page-level output intent example (remote) |
| PDF Declarations other than WTPDF's, from the registry | The reader's handling of declarations it does not know | 3 | Generated here with pikepdf's XMP editing, on a committed document, recorded as a derived variant |
Traps
- An element without
/NSis in PDF 1.7's namespace, even in a PDF 2.0 file. M13's tree in 2.0 output claims nothing for that reason; a 2.0 tree must name its namespace on every element, or it is a 1.7 tree. - PDF 2.0's
Subis not a subscript. It is a sub-division of a block — one line of an address, of a poem — and mapping HTML'ssuborsupto it would say something false about the content. M13's mapping table's note that M28 "addsSub" must not be read that way. - Types PDF 2.0 dropped are not wrong: a
BlockQuotestays aBlockQuotein the 1.7 namespace. Retyping it as aDivto "be 2.0" loses what the author said. NotebecameFENote, withNoteTypeand a relation to its call: a footnote that is only anFENotewithout itsRefis a note nobody can reach from the text.- Role maps cross namespaces now. A chain can go through
RoleMapNSof three namespaces and loop; it is followed iteratively and stopped at the first revisited pair, never by recursion. - Structure destinations need their page destination too: most readers deployed today navigate by
/D, and a link with/SDalone goes nowhere in them. /SDand/Dmust agree. They come from one layout; a merge that renumbers elements and not destinations, or pages and not elements, makes them disagree — which no viewer reports.- A declaration is a claim someone makes. The library writes one only for what its own checks enforced; a caller's declaration is written as given and reported as unverified, never presented as ours.
- PDF/A-4 dropped levels b, u and a: a
pdfaid:conformanceofBunder part 4 is wrong, and so is inferring accessibility from a part-4 claim — that is PDF/UA-2's. pdfaid:revandpdfuaid:revare required by their parts and absent from earlier ones; a validator that looks forconformanceunder part 4 or forrevunder part 3 is reading the wrong edition.- 4f is not Factur-X's base. DWC's invoice is a valid PDF/A-4f and not a Factur-X a French or German platform must accept; writing one would mislead the invoice's recipient (ADR 40).
- 4e allows engineering content; it does not create it. A 4e claim on a document with no 3D is legal and says nothing; one on a merged 3D model promises that model is kept exactly, which is all the library does with it.
- Page-level output intents can contradict the document's: a merge that brings in a page with its own intent must keep it consistent, or the page's color means something else than it did.
- Moving
/Infointo XMP for a PDF/A-4 merge changes a PDF/A-1 to 3 part's metadata model; its old claim is gone once the part is in a 2.0 file, and only the part-4 check may put a claim back. - veraPDF's PDF 2.0 profiles are younger than its part 1 to 3 ones, and the standards' own errata still move; a disagreement is recorded with the clause's text, and the veraPDF version pinned, never absorbed.
Documentation
docs/website/docs/concepts/pdf-2-0-tagging.md(new): the two namespaces, what PDF 2.0 added and dropped, ISO/TS 32005, structure destinations, pronunciation hints.docs/website/docs/guides/accessible-documents.md(M13's): PDF/UA-2 and WTPDF targets, choosing between UA-1 and UA-2 (Factur-X keeps UA-1), what veraPDF checks and what a person must.docs/website/docs/concepts/pdf-a.md(M14's): PDF/A-4, 4f and 4e, 4e's preservation-only rule, PDF/A-4 as a merge target, and why an invoice stays on part 3.docs/website/docs/guides/declarations.md(new): reading and writing PDF Declarations, who claims what.docs/website/docs/reference/html-to-structure.md(M13's): the 2.0 column of the mapping.docs/website/docs/reference/css-tagging-properties.md(M13's): the pronunciation properties andTitle.docs/website/docs/reference/conformance-rules.md(M20's, generated): the PDF/UA-2 and WTPDF rules.docs/website/docs/reference/diagnostics.md: thestructure.*,tagging.*,link.*,declaration.*andwrite.*codes added.docs/website/docs/reference/tool/:html2pdf --pdf-ua 2,--wtpdf,--pdf-a 4*,validate --profile pdf-ua-2|wtpdf-*,metadata declarationsanddeclare,merge --conformance pdf-a-4.docs/website/docs/introduction.mdanddocs/features/features.json: thepdf2-conformanceentry brought to its state.docs/architecture.md: namespaces inStructure/, declarations inMetadata/.docs/corpus.md: veraPDF's PDF 2.0 flavors as referees; the claim and declaration fields.docs/status.md: the measurements, the tagging overhead in 2.0, and the agreement with veraPDF's PDF 2.0 profiles.
Exit criteria
- The structure writer writes the PDF 2.0 namespace, its types and attributes,
RoleMapNSand the containment table; the ISO/TS 32005 table is data, checked against the text and held to the constants. - The reader resolves elements across namespaces, cycles reported; M06 merges 1.7 parts into 2.0 trees.
- PDF/UA-2 is generated from the builder and from HTML, with structure destinations on every internal link.
- M17's forms and M11's annotations are tagged in the 2.0 namespace.
- PDF Declarations are read and written; the library declares only what its checks enforced.
- WTPDF reuse and accessibility, and PDF/A-4, 4f and 4e, are generated; 4e preserves and never authors.
- The
pdf-ua-2and WTPDF profiles ship inAdCodicem.Pdf.Conformance, every clause and test of veraPDF'sua2,wt1randwt1amapped to a rule or a recorded reason. - The priority-1 gaps above are filled; each remaining gap is recorded in
docs/corpus-contributions.md. - The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on a
green
Remote corpusrun recorded instatus.md. -
Pdf20TaggingBenchmarksmeasures the 1,000-page report under PDF/UA-2 and PDF/A-4 withMemoryDiagnoser; the budget and the overhead over 1.7 are recorded. - Unit tests cover each behavior, its degenerate and hostile cases; the FsCheck properties hold.
- Integration tests run veraPDF, pikepdf, poppler, pdf.js, MuPDF, qpdf and ExifTool in containers.
- The documentation site publishes the 2.0 tagging concepts, the targets, declarations and the rule reference.
- Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).