M13 — Tagged structure and accessibility
State: to do — Depends on: M12 — Targets PDF/UA-1 by ADR 40; traceability designed in by ADR 7
Goal
Generate documents that are genuinely accessible — a complete logical structure in reading order, every image described or marked decorative, every table, list, note and link tagged as what it is, the language declared — so that the reference documents rendered from HTML pass PDF/UA-1 with no error, and so that the core can tag what it generates without HTML.
Tagging is what separates a PDF that a screen reader speaks from one it reads as a stream of glyphs in paint
order. It is also what makes every later promise about content hold: M14's PDF/A-3a needs it, M15's extraction
prefers it (ADR 15), and since 28 June 2025
the European Accessibility Act (Directive (EU) 2019/882) has required the services it covers — consumer banking,
e-commerce, passenger transport and e-books among them — to be accessible, the information they provide included
(its Annex I). The failures this milestone exists to prevent are the ordinary ones: an invoice whose line-item
table reads as one run of numbers, a two-column annex read across the columns, a logo read aloud as "image", a
repeated table header read on every page, a footer page number read in the middle of a sentence, and a template
made of divs whose author cannot fix any of it without rewriting the HTML.
Scope
In:
- a forward-only structure writer in the core,
PdfStructureBuilder, reached from M08'sPdfDocumentBuilder: structure elements, marked-content identifiers (MCIDs), marked-content references across pages, object references (OBJR) for annotations, the parent tree, the role map, the class map, the ID tree, attributes (Layout,List,Table), alternative descriptions (Alt,ActualText,E) and language (Lang), with memory bounded by the elements still open and the number of pages; - artifacts for everything that is not content — M09's
/Artifactwriter, extended to theLayout,PageandBackgroundtypes a generator needs; - tagging the library's own non-HTML output: image pages (M07) as
Figurewith the caller's alternative text, barcodes (M10) asFigurewith theirAlternativeText, and M08's simple-text path inside elements the caller opens; - the HTML engine's structure emission (
AdCodicem.Pdf.Html): every box traced to its source element mapped to a structure type by HTML semantics, WAI-ARIA roles and the author's CSS; reading order from the document order, not the paint order, through floats, positioning, multi-column layout, flexorder, grid placement and page breaks; running elements, margin boxes, backgrounds, borders, leaders, repeated table headers and footers and carried-forward subtotals as artifacts; tagged tables (header scope and associations, spans, captions), lists (labels and numbering), figures (alternative text and bounding box), links (LinkwithOBJRand/Contents), notes (Notewith its identifier), tables of contents, quotations, code, abbreviations, and generated content; - author overrides from CSS: a structure type, an artifact marking, alternative, actual and expansion
text, table-header scope, list numbering, and a role map — so that a layout-
divtemplate can be tagged without rewriting its HTML; - the PDF/UA-1 target (ADR 40): the
identification in the XMP (
pdfuaid:part 1), the title shown,/MarkInfo, the tab order, and the checks the engine can make before a byte is written, under M09'sPdfConformancePolicy; - the tool's
html2pdfgains--pdf-uaand--untagged.
Out, explicitly:
- PDF/UA-2 and Well-Tagged PDF — the PDF 2.0 structure namespace,
RoleMapNS, the PDF 2.0 types (Title,FENote,Aside,Em,Strong,Sub,DocumentFragment, theArtifactelement), structure destinations, pronunciation hints (PhoneticAlphabet), PDF Declarations — M28. In 2.0 output M13 writes the same tree in the default (PDF 1.7) namespace and makes no claim; - forms —
Formelements, widgets in the structure,/TU— M17; - PDF/A levels a — M14 claims PDF/A-3a on the structure written here; M20 adds 2a;
- MathML — M30; until then a
<math>element is not rendered by M12, and an image of a formula is aFormulaonly by override; - ruby and vertical writing — M30; warichu — not planned (no CSS specification lays it out);
- a PDF/UA-1 validation profile for third-party documents — M20, in the
AdCodicem.Pdf.Conformancesatellite (ADR 36); M13 checks only what it writes, and uses veraPDF as the referee; - reading a document's structure for extraction — M15. M13's own reader of structure trees stays internal: it extends the one M06 merges with, and exists here to prove what M13 writes;
- editing the structure of a received document (alternative text on a third party's figure, a language on an element) and heuristic tagging of untagged documents — neither is in the roadmap; the second is one of its open questions;
- tagging on existing documents — M06 (merge), M09 (stamps as artifacts) and M11 (annotations) already keep a tagged input tagged;
- executing JavaScript in a template or reading
aria-*state that a script would change — never (ADR 37).
Dependencies. The roadmap gives M12 alone, and M12 brings the rest with it: M08's content builder, its
marked content with Lang, ActualText, Alt and E, and its glyph-run path; M09's artifact writer and its
PdfConformancePolicy; M11's tagging of annotations; M06's reading and merging of structure trees; M03's internal
XMP writer. M12's traceability from DOM to box to fragment is the precondition ADR 7 set; M13 is the milestone
that cashes it.
Design
Where it lives
| Part | Where | Why |
|---|---|---|
| Structure types, attributes, artifacts, the forward-only writer | Core, Structure/ (namespace AdCodicem.Pdf.Structure) | The writer and its object numbers are the core's; M07, M10 and callers of PdfDocumentBuilder tag without HTML; no dependency is needed |
| Mapping DOM and style to structure, emission during painting, the PDF/UA-1 pre-write checks | AdCodicem.Pdf.Html, Tagging/ beside M12's Paint/ | It reads computed style and the box tree; the core knows neither |
| The internal structure reader used by the acceptance tests | Core (internal), extending M06's | Tests need an instrument; M15 makes a public model of it |
No new package. Nothing here is a satellite's: invariant 1 holds, and the core's structure code is trimmed away from an application that never tags.
Standard structure types
M13 emits the standard structure types of ISO 32000-1 §14.8.4 — the PDF 1.7 namespace, which is also the default namespace of PDF 2.0 (ISO 32000-2 §14.8.6), so that the same tree serves both outputs:
| Group | Types M13 emits | Types it never emits by default |
|---|---|---|
| Grouping | Document, Part, Art, Sect, Div, BlockQuote, Caption, TOC, TOCI | Index, NonStruct, Private (reachable by override) |
| Block | P, H1–H6, L, LI, Lbl, LBody, Table, TR, TH, TD, THead, TBody, TFoot | H (mixing H and Hn is a PDF/UA-1 failure; H only by override, and then alone) |
| Inline | Span, Quote, Note, Reference, Code, Link | BibEntry (override); Annot (M11's path); Ruby, RB, RT, RP (M30); Warichu, WT, WP refused |
| Illustration | Figure, Formula | Form (M17) |
PdfStructureType holds them as constants. A custom type is any other name, valid only with a role-map entry
to one of them.
The structure writer (core)
PdfStructureBuilder PdfDocumentBuilder.Structure, present when the options ask for a tagged document;
Begin(type, options) -> PdfStructureElement; End(element); Finish is the document's
PdfStructureElement a handle: type, alternative descriptions, Lang, ID, attributes; its object number is
reserved when it first receives content or a child, never before
PdfStructureOptions immutable per element: Alt, ActualText, E, Lang, Title (/T), ID, attributes
PdfStructureAttributes Layout (BBox, Placement), List (ListNumbering), Table (RowSpan, ColSpan, Headers,
Scope, Summary) — the owners ISO 32000-1 §14.8.5 defines, validated per type
PdfArtifact Type (Pagination, Layout, Page, Background), Subtype (Header, Footer, Watermark;
PageNum, Bates in 2.0 output), BBox, Attached — M09's writer, made public
PdfContentBuilder BeginTagged(element) -> MCID; BeginArtifact(artifact); EndMarkedContent (M08's builder)
PdfStructureBuilder.Reference(element, annotation) an OBJR, the annotation's /StructParent
- Lifecycle.
Beginrecords the element under its parent. Content marked withBeginTaggedon a page becomes a marked-content reference in the element's kids and an entry in that page's parent-tree array.Endcloses the element; it is written as soon as it is closed and every descendant is written, and forgotten. A page's parent-tree array is written when the page is finished. AtFinish: the structure tree root (its number reserved at the start, since the catalog names it), the parent tree as a number tree of leaves of fixed size with/Limits,/ParentTreeNextKey, the role map, the class map (only when a caller asks for one), the ID tree as a name tree, and in the catalog/MarkInfo << /Marked true >>,/Lang, andDisplayDocTitletrue among M06's typed viewer preferences when the document has a title or the PDF/UA-1 target is set — the HTML engine's M12.6 already sets it from<title>, and the builder only fills the gap. - Kids are ordered by logical position, not by arrival. A child element and a marked-content reference
carry a sort key the caller supplies — the HTML engine's document-order position — and an element's
/Kis written in key order when it closes. A caller of the core who adds in reading order never needs a key. - Pages. MCIDs restart at 0 on each page and are dense. A marked-content reference to a page other than
the element's own
/Pgis written as anMCRdictionary with its own/Pg; a paragraph broken across two pages is onePwith references on both. - Parent tree keys are one space for pages (
/StructParents) and annotations and XObjects (/StructParent), allocated in order, never reused. - Memory follows the elements still open — each holds its kids as packed integers until it closes — plus a
few bytes per page and per annotation for the parent-tree index, plus the ID tree's keys. A 1000-page report
holds its open sections and the current page, never its structure. The bound is measured (slice 10) and
recorded in
status.md. - Containment the builder enforces, because the tree is ours and a violation is the caller's mistake
(
InvalidOperationException):TRonly inTable,THead,TBodyorTFoot;THandTDonly inTR;LIonly inL;LblandLBodyonly inLI;TOCIonly inTOC;Captionfirst or last in aTable, first in anL; attribute owners only on the types they apply to. No other containment is checked in 1.7 output — ISO/TS 32005's rules are PDF 2.0's, and M28's. - Marked content never straddles: a sequence opened inside a text object closes inside it, and a sequence never spans two content streams. M08's state machine refuses both.
- Form XObjects. Real content drawn through a form XObject (an SVG, a barcode, a PDF page used as an image)
is tagged by wrapping the
Doin marked content on the page; the XObject itself carries no MCID, so it may be reused. An XObject with MCIDs of its own may be drawn once only — PDF/UA-1 forbids a form XObject with MCIDs referenced more than once — and the builder refuses a secondDoof one. - Annotations.
Referencewrites theOBJR, sets/StructParent, and marks the page for/Tabs /S, which the page gets when it is finished. - Determinism. Object numbers, MCIDs, parent-tree keys and generated identifiers depend only on the order of
calls; generated IDs are
adc-followed by a counter per kind, and a caller's ID that collides is suffixed and reported (tagging.id-renamed). - Version. Everything above needs no more than PDF 1.5 (
/Tabs), and raises nothing past 1.7 (ADR 40 asks each feature to say so). In 2.0 output the tree is written without/NS, which places every element in the default namespace;/Namespacesis M28's.
Tagging the library's own output
- Image pages (M07): when the output is tagged, each image page is a
Figureunder aPartwith the caller's alternative text and aLayoutBBox; without text, the image is an artifact only if the caller says it is decorative, and otherwise reported (tagging.alt-missing) — a scan of an exhibit is content. M07's "image part reported as untagged" becomes a tagged part. - Barcodes (M10): a code placed by the builder in a tagged document is a
FigurewithPdfBarcode.AlternativeText; a code stamped on a received document stays M09's artifact. - Stamps and separator pages (M09) are unchanged: artifacts.
- Simple text (M08): text a caller writes between
BeginTaggedandEndMarkedContentgoes to the element they opened; text written outside any marked content in a tagged document is refused withInvalidOperationExceptionatFinishof the page — untagged content in a tagged document is a PDF/UA-1 failure the core can see coming.
From HTML to structure
Traceability. Every box keeps its source element (architecture §4). At cascade time each element gets a tag role — a small struct in its computed style: the structure type as an index, a flags word (artifact, transparent, table-header scope, list numbering) and indexes into the element's alternative texts — so that the layout and paint loops read it without allocating (invariant 3).
Role resolution, first match wins:
- the author's CSS:
-adc-pdf-tagand its companions (below); - the element's WAI-ARIA
role, as the ARIA table below maps it; - the element's HTML semantics, as the HTML table maps it;
- transparent: the element makes no structure element, and its content joins its nearest ancestor's.
Anonymous block boxes that CSS creates around loose inline content in a block with block siblings become P:
text never sits directly in a Div or a Sect. A Div or Sect that ends up with a single child and no
content of its own is kept — collapsing it would change what lang and alternative text apply to — but an
element that ends up with no content and no child is never written.
The HTML mapping (the defaults, after the PDF Association's Tagged PDF Best Practice Guide: Syntax and W3C's HTML-AAM where they apply):
| HTML | Structure | Notes |
|---|---|---|
html / body | Document | One per document; lang on html becomes the catalog's /Lang |
article | Art | |
section, main, nav | Sect | nav with -adc-pdf-tag: TOC on its list becomes a table of contents |
header, footer, aside, address, div, hgroup | Div | In the flow they are content; in a margin box or running element they are artifacts |
p | P | |
h1–h6 | H1–H6 | |
blockquote | BlockQuote | cite ignored |
q | Quote | Its generated quotation marks are content of the Quote |
pre | P, or Code when it holds only a code | Line breaks are content |
code, kbd, samp | Code | |
abbr title, acronym title | Span with /E | The expansion from title |
span lang, any inline element whose lang differs from its parent's | Span with /Lang | Otherwise inline elements are transparent |
em, strong, b, i, mark, small, sub, sup, time, cite, dfn, var | transparent | PDF 1.7 has no Em or Strong; M28 maps em and strong to them in 2.0 output and keeps sub and sup transparent — PDF 2.0's Sub is a sub-division of a block, not a subscript |
a href | Link | Its text, and one OBJR per annotation M12.6 writes for it |
img | Figure with /Alt | alt="", role=presentation or role=none: a Layout artifact |
svg (inline or as an image) | Figure with /Alt | From aria-label, aria-labelledby, or its <title>; its text drawn inside the figure's one sequence |
figure | Figure | Holding the image's content directly, and Caption for figcaption; two or more images make Figures under a Div |
figcaption | Caption | |
ul, ol, menu | L with ListNumbering | From list-style-type (below) |
li | LI: Lbl for ::marker, LBody for the rest | |
dl | L | Each dt group is an LI, dt its Lbl, dd its LBody |
table | Table with a Layout BBox when on one page | role=presentation or role=none: transparent, rows and cells too |
caption | Caption | |
thead, tbody, tfoot | THead, TBody, TFoot | An implied tbody is written |
tr | TR | |
th | TH with /Scope | Always a scope: scope if given, Column in a thead, Row for a leading header cell in a body row, Both for the top-left corner |
td | TD | rowspan, colspan, and headers as /Headers over the header cells' IDs |
hr | Layout artifact | |
br, wbr | nothing | |
input, select, textarea, button | painted as content in M13; Form is M17's | |
math | not rendered by M12 (M30) | |
iframe, object, embed, video, audio, canvas | as M12 paints them: a fallback image is a Figure |
List numbering: disc, circle, square → Disc, Circle, Square; decimal and
decimal-leading-zero → Decimal; lower-roman, upper-roman → LowerRoman, UpperRoman; lower-alpha,
lower-latin, upper-alpha, upper-latin → LowerAlpha, UpperAlpha; none → None; any other counter
style → Decimal when numeric and Disc when symbolic, reported once (tagging.list-numbering-approximated,
information).
The ARIA mapping — roles a template can use where the HTML is not semantic:
role | Structure |
|---|---|
heading with aria-level n | Hn; above 6, H6, reported |
paragraph, blockquote, code, caption, figure | P, BlockQuote, Code, Caption, Figure |
list, listitem | L, LI |
table, grid, row, rowgroup | Table, Table, TR, TBody |
cell, gridcell, columnheader, rowheader | TD, TD, TH Column, TH Row |
img | Figure, its descendants presentational: one sequence, no element inside |
link | Link when the element has a target M12.6 can link; Span otherwise |
math | Formula with /Alt from aria-label |
doc-toc, doc-footnote, doc-endnote, doc-noteref (DPUB-ARIA) | TOC, Note, Note, Reference |
presentation, none | transparent; on an image, a Layout artifact |
aria-label and aria-labelledby (resolved without following a reference twice, joined by spaces, at most
the length of a PDF text string) give /Alt on a Figure or Formula and /Contents on a link annotation —
and nothing on other elements, where an Alt would hide the element's own content from assistive technology.
aria-hidden="true" makes what the element paints a Layout artifact, reported
(tagging.aria-hidden-artifact, information), since the content stays visible.
Emission. M12's painter already calls a structure sink — a no-op until this milestone — and already writes
margin boxes, running elements and position: fixed repeats as Pagination artifacts and leaders as Layout
ones. M13 makes the sink the emitter. The painter visits a page's fragments in paint order and asks it to mark
each one:
- the emitter keeps the page's current marked-content sequence and coalesces consecutive fragments of the
same element into one MCID — a paragraph of forty lines is one
BDC … EMC, not forty — and closes it when the painter moves to another element, an artifact, or a text object boundary; - each fragment carries its element's handle and its logical position (M12's document-order position of the fragment, which inline layout already assigns), which becomes the marked-content reference's sort key;
- an element is finished when its source element's last fragment has been painted and its descendants are finished. Content that layout defers — a footnote carried to the next page, a float pushed down — keeps its ancestors open until it is painted; M12's deferral bounds are therefore the structure's too;
- clipping paths,
cmandqthat serve a fragment are written inside its sequence, so that no painting operator is left between sequences.
Reading order is the document order, whatever the paint order: floats and positioned elements in the
flow where their source is; multi-column text column by column as the DOM gives it; flex order and grid
placement ignored, as the HTML-AAM ignores them; a footnote's Note right after the paragraph that calls it;
running elements and margin boxes nowhere, since they are artifacts. A template that relies on order or grid
placement to say something the DOM order does not is reported once
(tagging.visual-order-differs, information) — the structure follows the DOM, as a screen reader following
the HTML would.
Artifacts:
| Painted | Artifact |
|---|---|
Margin boxes, running elements, position: fixed repetitions | Pagination, Header at the top and Footer at the bottom (with /Attached); in 2.0 output a page counter is PageNum (M09's rule) |
| Late-filled counter XObjects (ADR 39) | As the margin box that holds them |
| Page backgrounds, element backgrounds, background images | Background |
Borders, rules, box shadows, outlines, hr, the footnote separator, leaders (leader()) | Layout |
| Table header rows repeated on continuation pages; table footer rows except their last occurrence; carried-forward subtotals | Pagination — the tagged THead is the first occurrence and the tagged TFoot the last |
| Decorative images and generated content whose alternative text is empty | Layout |
Generated content. ::before and ::after text — counters in headings ("7.2"), quotation marks,
"Article 3 —" — is content of its element. CSS Generated Content's alternative text is honored:
content: "→" / "" makes it a Layout artifact; content: url(star.svg) / "Recommended" makes it a Span
with that ActualText (an image: a Figure with that Alt). ::marker is the Lbl. target-counter() text
is content — it is what the reader hears in "see page 12".
Tables of contents. A table of contents M12.6 generates, or a list the author marks -adc-pdf-tag: TOC, is
a TOC whose items are TOCI, each holding a Reference that holds the Link, with the page number as
content and the leader as an artifact. A nested list is a nested TOC.
Notes. A footnote (float: footnote, M12.7) is a Note with a unique /ID — generated when the element
has no id — placed after the element that calls it; the call mark is a Reference (holding the Link when
M12.7 links it), the note's marker its Lbl.
Links. A Link holds the link's text references and an OBJR for each annotation M12.6 writes for it (a
link broken across two lines may be two annotations); each annotation's /Contents is the accessible name —
aria-label, then title, then the link text — and a link whose name is empty is reported
(tagging.link-name-missing). Internal links keep M12.6's named destinations; structure destinations are M28's.
Language. lang (or xml:lang) on html sets the catalog's /Lang, as M12.6 already does; on any
element it sets that element's /Lang when it differs from the language its structure parent resolves to; on
an inline element with no semantics it makes a Span for it. A value that is not a well-formed BCP 47 tag is reported
(tagging.language-invalid) and not written; the empty value is written as the specification's "unknown" and
reported. The language of Alt, ActualText, E and /Contents is the element's.
Bidirectional text. Structure order is logical by construction. Within a line, each bidirectional run
is its own sequence, so that the references of a mixed Arabic and Latin line are in logical order. Glyphs of a
right-to-left run are drawn in visual order (HarfBuzz's output through M08's glyph-run path), and PDF 1.7 offers
only ReversedChars to say so, which few readers honor: slice 7 settles, against poppler, PyMuPDF and
pdfminer, whether an ActualText in logical order on each right-to-left run is needed, and records what each
referee does.
Titles. <title> becomes dc:title and /Title, with DisplayDocTitle set (M12.6). A tagged document
without a title is written and reported (tagging.title-missing); under the PDF/UA-1 target it is a conflict
(below).
Author overrides from CSS
The engine's CSS extensions carry the -adc- prefix M12 fixed and enter its property table. None is inherited;
all cascade normally, so a class in a stylesheet tags every element of a layout template at once.
| Property | Values | Initial | What it does |
|---|---|---|---|
-adc-pdf-tag | auto | none | artifact | a standard type | a custom name | auto | The element's structure type. none makes it transparent; artifact makes everything it paints an artifact. A custom name needs a role-map entry |
-adc-pdf-artifact | auto | layout | pagination | page | background | auto | The artifact type, when the element is one |
-adc-pdf-artifact-subtype | none | header | footer | watermark | page-number | bates | none | The subtype; the last two are written as Header or Footer in 1.7 output, by position (M09) |
-adc-pdf-alt | none | <string> | attr(<name>) | none | /Alt |
-adc-pdf-actual-text | none | <string> | attr(<name>) | none | /ActualText |
-adc-pdf-expansion | none | <string> | attr(<name>) | none | /E |
-adc-pdf-scope | auto | row | column | both | auto | A TH's /Scope |
-adc-pdf-list-numbering | auto | none | disc | circle | square | decimal | upper-roman | lower-roman | upper-alpha | lower-alpha | auto | An L's ListNumbering |
@-adc-pdf-role-map {
InvoiceLine: TR;
Amount: TD;
}
.line { -adc-pdf-tag: InvoiceLine; }
.amount { -adc-pdf-tag: Amount; }
.logo { -adc-pdf-tag: Figure; -adc-pdf-alt: "Acme Ltd"; }
.rule { -adc-pdf-tag: artifact; }
- The role map is checked when the stylesheet is compiled: the target must be a standard type; a standard
type may not be remapped (PDF/UA-1 §7.1); a custom name may not map to another custom name, so no chain and no
cycle; a name mapped twice keeps the last and reports it. A refused entry is a CSS diagnostic
(
tagging.role-map-rejected), and the elements that used it fall back toauto. - Overrides are checked against containment when the tree is built: an override that would put a
TDoutside aTR, or anLIoutside anL, falls back toautofor that element and is reported (tagging.override-containment). Types whose content model the engine cannot honor —Form,Ruby,Warichuand their parts — are refused. - The class map is not written from CSS classes: they would bloat the file and say nothing to a reader.
The PDF/UA-1 target
PdfConformanceTarget.PdfUA1, on PdfGenerationOptions (M08) and on M12's PdfRenderOptions.Conformance,
combines with the PDF/A targets M14 adds. Tagging is on by default in the HTML engine — an accessible
document is what the European Accessibility Act asks of the services it covers, as Chromium's print already tags
by default, and slice 10 measures the cost — and PdfRenderOptions.Tagging set to None turns it off for a
caller who wants the smaller file; the PDF/UA-1 target requires it. Puppeteer's tagged option, which M12
accepts, takes effect here.
What the target checks before a byte of the page is written, by the clauses of ISO 14289-1 and the machine- checkable failure conditions of the Matterhorn Protocol 1.1 that veraPDF implements:
| Requirement | What the engine does |
|---|---|
§5: the identification schema, pdfuaid:part 1 | Written by M03's internal XMP writer, extended by one property (M14's public model replaces it) |
§7.1: a title, shown (dc:title, DisplayDocTitle) | From <title>; none: a conflict |
| §7.1: all content real and tagged, or an artifact; neither inside the other | By construction; the emitter checks each page before it is written, and a violation — a defect of ours, never of the template — is a conflict (tagging.untagged-content) on which every test fails |
§7.1: custom types role-mapped, standard types not remapped; /Marked true; no /Suspects | By construction and by the role-map checks |
| §7.2: the natural language determinable for all text, alternative text included | lang on html, or the caller's Language option; none: a conflict |
§7.3: a Figure has alternative text | Each image without it: tagging.alt-missing, a conflict |
§7.4: headings — the first numbered heading H1, no level skipped going down, H and Hn not mixed | tagging.heading-level-skipped, tagging.first-heading-not-h1: conflicts. Never remapped in silence: the author fixes the template or says -adc-pdf-tag: H3 |
§7.5: a TH has a scope, or header associations | Always written |
§7.6: lists L, LI, Lbl, LBody | By construction |
| §7.8: running heads and feet as pagination artifacts | By construction |
§7.9: a Note has a unique /ID | Generated |
§7.10: optional-content configurations named, without /AS | M12 writes no optional content; a caller's layer (M11) is checked, and a configuration carrying /AS — which M11's layer form of a print-only stamp writes — is a conflict |
§7.18.1, §7.18.5: annotations in the structure; a link in a Link with its OBJR and /Contents | By construction; an empty accessible name is a conflict |
§7.18.3: /Tabs /S on every page with annotations | Written |
| §7.20: no reference XObject; a form XObject with MCIDs drawn once | By construction |
§7.21: fonts embedded, every glyph mapped to Unicode, no .notdef | M08 under the target: the OFL substitute, ToUnicode, a drawn box instead of .notdef |
A conflict follows M09's PdfConformancePolicy: Refuse, the default, throws PdfConformanceException once
the render has collected every conflict — naming each element by its source position, the clause and the
Matterhorn failure condition — so that a template is fixed in one pass, not one error at a time;
RemoveClaim writes the document tagged, without pdfuaid, and reports tagging.conformance-claim-removed at
ConformanceLoss severity. What only a person can judge — whether an alternative text is meaningful, whether
the reading order makes sense, whether color alone carries meaning — is not checked, and the documentation says
so, with the Matterhorn conditions it leaves to a human.
In 2.0 output, the PDF/UA-1 target is refused when the options are built (ArgumentException): PDF/UA-1
is defined on ISO 32000-1, and PDF/UA-2 is M28's. The tree is still written when tagging is on.
Diagnostics
In PdfDiagnosticCodes for the core's, and in the HTML engine's diagnostics for the rest, disjoint from
validation rule identifiers (ADR 36):
| Code | Severity | Meaning |
|---|---|---|
tagging.alt-missing | Warning | A figure has no alternative text |
tagging.title-missing, tagging.language-missing | Warning | The document has no title, or no language |
tagging.language-invalid | Warning | A lang value is not a BCP 47 tag; not written |
tagging.heading-level-skipped, tagging.first-heading-not-h1 | Warning | The heading sequence PDF/UA-1 §7.4 requires is broken |
tagging.link-name-missing | Warning | A link has no accessible name |
tagging.table-irregular | Warning | Rows of one table have different numbers of cells after spans |
tagging.role-map-rejected, tagging.override-containment | Warning | A role-map entry or an override was refused; the element fell back to auto |
tagging.id-renamed | Information | A caller's or template's id collided; the structure ID was suffixed |
tagging.list-numbering-approximated, tagging.visual-order-differs, tagging.aria-hidden-artifact | Information | As above |
tagging.untagged-content | Warning | Content outside every element and artifact: a defect of the library, never of the template; a conflict under the target, and a failure in every test |
tagging.conformance-claim-removed | ConformanceLoss | The PDF/UA-1 claim was not written, and why |
The command-line tool
html2pdf gains --pdf-ua (the target, with --policy refuse|remove-claim) and --untagged; its JSON report
lists the tagging.* diagnostics with each element's source line and column.
Slices
Each slice ends on a green commit, with the diagnostics it introduces documented and its measurements, if any,
recorded in docs/status.md.
- The structure writer. Delivers
PdfStructureBuilder,PdfStructureElement, the standard types,BeginTaggedon M08's content builder, marked-content references across pages, the parent tree written page by page and finished as a number tree,/StructParents,/MarkInfo, the catalog's/Lang, the ID tree, the role map, the containment checks, determinism, and the lazy reservation of numbers. Proved by unit tests of each part and each refusal; an FsCheck property — any well-nested sequence ofBegin,Endand marked content over any number of pages yields a tree whose parent tree is exactly the inverse of its marked-content references, every MCID of every page referenced once, keys unique —; integration: pikepdf walks a hand-built three-page document and finds exactly the tree built, poppler'spdfinfo -struct-textprints it with its text,qpdf --checkis silent, and veraPDF's PDF/UA-1 profile finds no failure in the structure clauses. Leaves artifacts, attributes and annotations. - Artifacts, attributes, alternative descriptions and annotations. Delivers
PdfArtifactpublic with theLayout,PageandBackgroundtypes beside M09'sPagination, the attribute owners,Alt,ActualText,EandLangon elements and on marked content,Referencefor annotations with/Tabs, thepdfuaididentification andDisplayDocTitle, and the internal structure reader extended to resolve MCIDs to their text. Proved by unit tests; integration: veraPDF's PDF/UA-1 profile passes a builder-made document with a figure, a table, a list, a link and a footer; the internal reader agrees withpdfinfo -struct-texton the three committed PDF/UA-1 documents and on every committed document with a structure tree (below). Leaves the library's own outputs. - The library's own outputs. Delivers
Figuretagging of M07's image pages and M10's barcodes, text in elements through M08's simple path, and the refusal of untagged content in a tagged document. Proved by unit tests; integration: a tagged volume built by M06 from corpus images turned into pages by M07, with alternative text, and a page of M10's reference codes pass veraPDF's PDF/UA-1 profile;pdfinfo -struct-textshows eachFigurewith itsAlt. Leaves the HTML engine. - Blocks, sections and reading order in the HTML engine. Delivers the tag role in computed style, role
resolution, the HTML mapping for grouping, heading, paragraph, quotation and code elements, anonymous
Ps, the emitter with coalescing and logical sort keys, element finishing and deferral, reading order through floats, positioning, multi-column layout, flex and grid, and the artifacts of margin boxes, running elements, backgrounds and borders. Proved by unit tests over box trees built by hand, with an FsCheck property — for any DOM, the structure's text order equals the DOM's text order, whatever the paint order —; integration: the structure orderpdfinfo -struct-textprints for the rendered report and contract equals the source's text order; veraPDF finds no content outside the structure. Leaves tables, lists, figures and links. - Tables, lists, figures and links. Delivers
Tableand its parts with scope, spans,Headersand IDs, captions andBBox; repeated headers and footers and carried-forward subtotals as artifacts;LwithListNumbering,Lbl,LBody, anddl;Figureforimg,svg,figureandfigcaption;Linkwith itsOBJRs and/Contents. Proved by unit tests per element and per edge (a table spanning three pages, arowspanacross a page break, a link broken across lines, an image withalt=""); integration: the rendered invoice's line-item table and the report's measures table pass veraPDF's table rules, andpdfinfo -struct-textshows each header row once. Leaves generated content and notes. - Generated content, tables of contents and notes. Delivers
::marker,::beforeand::afteras content, CSS alternative text for generated content, leaders as artifacts,TOCandTOCIfor M12.6's generated tables of contents, andNote,Referenceand IDs for M12.7's footnotes. Proved by unit tests; integration: the report's contents (itsnavlist, and the same contents generated bytarget-counter()) and the accessibility reference's footnotes pass veraPDF, eachNotefound once after its call inpdfinfo -struct-text. Leaves language. - Language, abbreviations and bidirectional text. Delivers
/Langon elements and on spans (the catalog's is M12.6's), BCP 47 checking,Efromabbr, the title check, per-run sequences for bidirectional lines, and the decision on right-to-leftActualText. Proved by unit tests; integration: the accessibility reference (French with English, German and Arabic passages) passes veraPDF's language rules;pdfinfo -struct-textgives the Arabic passage in logical order, and what poppler, PyMuPDF and pdfminer each extract from it is recorded. Leaves ARIA and the author's CSS. - ARIA and CSS overrides. Delivers the ARIA mapping, accessible names,
aria-hidden, the-adc-pdf-*properties,@-adc-pdf-role-mapand its checks, and the containment fall-back. Proved by unit tests of every property, every precedence and every refusal (a cycle, a remappedP, aTDoutside a row); FsCheck over arbitrary role-map declarations (the checker is total and bounded); integration: the layout-divinvoice, tagged only by a stylesheet, produces the same structure — types, order and text — as the semantic invoice, and passes veraPDF. Leaves the target. - The PDF/UA-1 target. Delivers
PdfConformanceTarget.PdfUA1, the pre-write checks, the collected conflicts underRefuseandRemoveClaim, the 2.0 refusal, andhtml2pdf --pdf-ua; the manifest'sclaimsUaanduaConformanceValid— the PDF/UA part claimed and veraPDF's verdict on it, beside the PDF/A claim's two fields —, written bybuild_corpus.py, the calibration the referee needs before it judges ours. Proved by unit tests with a set of faulty templates — an image withoutalt,h1thenh3, an empty link, no<title>, nolang, a remapped standard type — each raising exactly its conflicts; integration: the reference documents pass veraPDF's PDF/UA-1 profile with no failure; each faulty template underRemoveClaimcarries nopdfuaid, and veraPDF's failures on it are the conflicts we reported. Leaves scale. - Scale, determinism and the whole. Delivers the measured memory bound,
TaggingBenchmarks, determinism across runs and cultures, and the documentation. Proved by M12's long report tagged at 10, 100 and 1,000 pages with memory flat; the tagged and untagged reference documents compared for time, allocations and size, recorded instatus.md; M12's memory and throughput budgets measured again with tagging on, the default this milestone sets, and recorded beside the untagged ones; two renders byte-identical under the invariant culture andfr-FR; M06 merging our tagged report with the three committed PDF/UA-1 documents keeps veraPDF's PDF/UA-1 verdict.
Tests required
Unit —
- The writer: nesting, closing out of order refused, an element closed with a child still open refused, a
marked-content reference to another page, a paragraph over three pages, dense MCIDs per page, parent-tree
leaves at the chunk boundary (0, 1, a full leaf, a full leaf plus one),
/ParentTreeNextKey, annotation keys after page keys, ID tree ordering, role-map validation, each containment refusal, attribute owners on the wrong type refused, lazy reservation (an empty element leaves no object and no gap), determinism of numbering. - Marked content: never straddles a text object or a content stream; a reused form XObject with MCIDs refused;
a
Dowrapped; artifacts inside tagged content and tagged content inside artifacts refused. - The HTML mapping: one test per row of each table above, and per ARIA role; precedence of CSS over ARIA over
HTML; anonymous
Ps; transparent inline elements; adivwith a single child kept; empty elements not written. - Order: floats, absolute and fixed positioning, multi-column, flex
order, grid areas, footnotes deferred to the next page, a table header repeated on three pages (oneTHead), atfootrepeated (oneTFoot, the last), carried-forward subtotals, running elements. - Generated content: counters, quotes, markers, alternative text empty and not, images as generated content.
- Language: inheritance, a
span lang, an invalid tag, the empty tag,xml:lang, alternative text in a language other than its element's. - Hostile: a DOM nested 100,000 deep (the emitter and the writer are iterative; the depth costs memory in
proportion, never stack), a list of a million items, a table of 10,000 rows spanning 300 pages,
aria-labelledbycycles and self-references, analtof 1 MB (bounded to the longest text string the target allows, reported), a role map of 10,000 entries, IDs that collide ten thousand times. - Properties (FsCheck): the parent tree inverts the references for any tree; structure text order equals DOM text order for any DOM; the role-map checker is total; output is identical for identical input.
Integration — in containers (ADR 27), on every document this milestone generates:
- veraPDF, PDF/UA-1 profile (
--flavour ua1): no failure on what we claim; on what we write underRemoveClaim, exactly the failures we reported. - poppler,
pdfinfo -structand-struct-text: the tree, its types, itsAltandLang, and its text in structure order — the independent reading of what we wrote. - pdftotext and PyMuPDF: the visual reading order, compared with the structure order where the roadmap asks for it (the two-column annex, page breaks).
- pikepdf: a script, independent of our reader, that checks the parent tree against the marked content of
every page,
/StructParentsand/StructParentkeys,/Tabs, and that every image XObject drawn is inside aFigurewith/Altor inside an artifact. - qpdf
--checkon every output.
Acceptance conditions
"The reference documents" are our engine's renderings of tests/corpus/sources/invoice-fr.html,
report-fr.html (with its contents list, its measures table and its two-column annex, section 8) and
contract-fr.html, which M12 commits as documents/invoice/adcodicem-invoice-fr.pdf,
documents/report/adcodicem-report-fr.pdf and documents/contract/adcodicem-contract-fr.pdf — here rendered
tagged —, and of the accessibility reference and the layout-div invoice this milestone adds (below). "The
three PDF/UA-1 documents" are
vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf,
vendor/pdf-association/indesign13-pdfua1-german-book-chapter.pdf and
vendor/pdf-association/indesign15-pdfua1-form.pdf, whose claims veraPDF upholds.
| Documents | Behavior | Verified by |
|---|---|---|
| The reference documents, tagged under the PDF/UA-1 target | veraPDF's PDF/UA-1 profile reports no failure; qpdf is silent | TaggingRefereeTests.Reference_documents_pass_pdf_ua_1 |
| The reference documents | The text of the structure in order, as pdfinfo -struct-text prints it, equals the source's text in document order, whitespace normalized — across every page break | CorpusTaggingTests.Structure_order_is_the_source_order |
| The rendered report's two-column annex, and its measures table set on a page short enough that it breaks across two, as M12.3's acceptance sets it — rendered as Chromium paginates it, the table fits one page | The structure's order equals the visual order pdftotext and PyMuPDF report for the annex — left column, then right —, and the table's rows appear once each, its header once | TaggingRefereeTests.Reading_order_matches_the_visual_order_across_columns_and_pages |
| Every document this milestone generates, faulty templates included | Every image drawn — XObject, inline image, or form XObject holding one — is inside a Figure with a non-empty /Alt or inside an artifact; no exception, in our reader and in the pikepdf script alike | CorpusTaggingTests.Every_image_has_alternative_text_or_is_an_artifact |
| Every document this milestone generates | No painting operator outside a tagged sequence or an artifact; no artifact inside tagged content | CorpusTaggingTests.All_content_is_tagged_or_an_artifact |
| The rendered invoice and the report | Each TH has a /Scope; the header row of a table repeated on later pages is an artifact there; the Links of the report's contents hold their OBJR and /Contents, and every page with a link has /Tabs /S | CorpusTaggingTests.Tables_and_links_are_tagged_as_pdf_ua_1_asks |
The layout-div invoice, tagged by a stylesheet alone | Its structure — types, order and text — equals that of the semantic invoice; veraPDF's PDF/UA-1 profile reports no failure | CorpusTaggingTests.Css_overrides_tag_a_layout_template_like_semantic_html |
| The accessibility reference | Each passage in English, German and Arabic carries its /Lang; the Arabic passage is in logical order in pdfinfo -struct-text; each footnote is one Note with a unique /ID right after its call; each abbr has its /E | TaggingRefereeTests.Language_notes_and_abbreviations_are_tagged |
The three PDF/UA-1 documents; every committed document with a structure tree — the InDesign IRS publications in Arabic, Russian and Chinese, the Word 2010 and 2019 exports, the PDFMaker 7 to 11 files, the OpenOffice.org PDF/A-1a files, handwritten-utf16le-strings.pdf, the weclapp invoice whose /MarkInfo aliases its page tree and whose structure root is empty | The internal reader the acceptance relies on prints the same types, Alt, ActualText, Lang and text, in the same order, as pdfinfo -struct-text — through Word's style-named role maps, OpenOffice's Document mapped to itself, class maps and UTF-16LE strings — or records why they differ | CorpusTaggingTests.The_structure_reader_agrees_with_poppler |
The remote tagged documents: remote/pdf-association/abledocs-pdfua1-textbook-chapter.pdf, abledocs-pdfua1-tagged-textbook-scan.pdf, indesign-cs6-pdfua1-brochure.pdf (NUL-terminated alternative texts), remote/pdf20examples/handwritten-pdf20-utf8-strings.pdf (a duplicated MCID) | As the row above; closes only on a green Remote corpus run | CorpusTaggingTests.The_structure_reader_agrees_with_poppler |
| A volume of our tagged report, M07's image pages made from the corpus's image files (the JPEG, PNG and CCITT TIFF files M07 adds) with alternative text, and M10's reference codes | veraPDF's PDF/UA-1 profile reports no failure; each image page and code is a Figure with its text | TaggingRefereeTests.Image_pages_and_barcodes_are_figures |
| Our tagged report merged by M06 with each of the three PDF/UA-1 documents | veraPDF's PDF/UA-1 verdict is kept: the claim stays and no failure appears | TaggingRefereeTests.Merging_tagged_outputs_keeps_pdf_ua_1 |
The faulty templates under Refuse and RemoveClaim | Exactly the conflicts each carries, collected in one exception; under RemoveClaim, no pdfuaid in the file and veraPDF's failures are the conflicts we named | CorpusTaggingTests.Pdf_ua_conflicts_are_refused_or_reported_exactly |
| M12's long report — not in the corpus, M12's need — tagged, at 10, 100 and 1,000 pages, from a streamed source | Memory flat as pages grow, within the budget recorded in status.md; the structure's size per page measured | CorpusTaggingTests.Tagging_a_thousand_pages_holds_its_budget, TaggingBenchmarks |
| Every row above | Two renders give identical bytes, under the invariant culture and under fr-FR | CorpusTaggingTests.Tagging_is_deterministic |
| The reference documents through the tool | html2pdf --pdf-ua produces the API's bytes | CorpusToolTests.Html2pdf_pdf_ua_matches_the_api |
Corpus
What the corpus holds
- Sources:
sources/invoice-fr.html(two tables — the line items and the totals —,h1andh2,lang="fr", a title),sources/report-fr.html(anavcontents list of eight links, nineh2sections, a 13-row table withthead, an ordered list, a two-column annex),sources/contract-fr.html(eight articles underh2). None has an image, a figure, a footnote, a nested list, a table withrowspan,colspanorheaders, an abbreviation or a passage in another language. - Other producers' structure, to read: three committed PDF/UA-1 claims (PDFlib's PDF/A-2a invoice with a
captioned figure and a
theadtable, InDesign's German book chapter with tables, lists, a contents, links, class and role maps,ActualText, InDesign's form); some sixty-five committed documents with a structure tree — Word 2010, 2019 and 365 (word-invoice-fr.pdf,word2019-ccs-contract-schedule.pdf), PDFMaker 7 to 11 (rolemap-word-style-names,rolemap-and-classmap), OpenOffice.org 3.2 PDF/A-1a (rolemap-document-to-itself,rolemap-custom-style-to-p), InDesign IRS publications whose/Langis wrong (lang-mismatch), ABBYY FineReader with an artifact that carries an MCID (artifact-with-mcid), the weclapp invoice (markinfo-aliases-pages-node,empty-struct-tree-root), UTF-16LE alternative texts (handwritten-utf16le-strings.pdf); remote, three more PDF/UA-1 documents from AbleDocs and InDesign CS6, UTF-8 strings and a duplicated MCID in a PDF 2.0 file, a structure without/MarkInfo,StructParentswithout a structure tree. - Inputs for tagged image pages and codes: the image files M07 adds to the corpus (M07 records their absence today as its own priority-1 gap) and M10's reference payload set.
- Right-to-left text from other producers, for comparison: the Arabic IRS publication (tagged by InDesign), and remote Hebrew and shaped Arabic (W09).
What it lacks
| Need | Why | Priority | Likely source |
|---|---|---|---|
Our engine's renderings of the three reference sources, documents/*/adcodicem-*-fr.pdf, committed | Every acceptance row is written against them; M12 commits them untagged, and M13 regenerates them tagged in the commit that turns tagging on | 1 | Generated here, by M12 and then this milestone, recorded in build_corpus.py |
An accessibility reference source: images with and without alt, an SVG chart with a <title>, figure and figcaption, nested ul, ol and dl, a table with caption, rowspan, colspan, scope and headers long enough to break across pages, footnotes, a generated contents with target-counter(), links broken across lines, abbr, q, code, blockquote, passages with lang in English, German and Arabic, generated content with and without alternative text, a barcode | The existing sources exercise a fraction of the mapping; the roadmap's "every image carries alternative text" cannot be tested on sources that have no image | 1 | Generated here: sources/accessibility-fr.html with its images, written for the purpose, MIT |
A layout-div version of the invoice with the stylesheet that tags it | The CSS overrides' acceptance compares it with the semantic invoice | 1 | Generated here: sources/invoice-fr-divs.html and its stylesheet, derived from invoice-fr.html |
A set of faulty templates — no alt, a skipped heading level, an empty link, no title, no lang, a remapped standard type | The PDF/UA-1 conflicts must be shown exactly, and on documents a template author would write | 1 | Generated here, derived from the reference sources by recorded edits |
veraPDF's PDF/UA-1 verdict recorded for each document that claims PDF/UA (the manifest's conformanceValid covers the PDF/A claim only) | The referee's container version must be calibrated on third-party claims before it judges ours; three claims are said upheld in prose, none in the manifest | 2 | Generated here: build_corpus.py --committed-only and --remote record it in claimsUa and uaConformanceValid, which slice 9 adds to the schema beside claimsConformance and conformanceValid — a dual claim is two fields, not one — and the features pdfua1-claim and pdfua-1-claim made one |
| The same sources rendered tagged by Chromium and exported by LibreOffice with its PDF/UA option | Two other producers' tagging decisions on the same content — tables, lists, headings, the two-column annex — to compare ours with, and a second opinion when veraPDF and poppler disagree | 2 | Generated here: Chromium's tagged print and LibreOffice 24.2's PDF/UA export, both already in the container |
| A PDF/UA-1 document from a producer other than Adobe, PDFlib and AbleDocs, committed | The reader's calibration and M06's merge rest on two committed producers | 3 | Generated here (LibreOffice's PDF/UA export above), or Word's accessible export on Windows (build_word.ps1) |
| A tagged, PDF/UA-1 document in a right-to-left script | The bidirectional decision of slice 7 is checked on our output only | 3 | A public source (a government publication in Arabic or Hebrew with a PDF/UA claim), or a contribution (W09) |
Traps
- The paint order is not the reading order, and nothing in a PDF says which is which except the structure. An emitter that creates elements as it paints writes the footer before the body and the right column's float before the left column's text. Elements are ordered by document position, always.
- A reused form XObject cannot carry MCIDs: its marked content would belong to two places in the tree.
Wrap the
Do; never tag inside the XObject. - A page of another PDF used as an image or a letterhead (M12.6) brings its own marked content, whose MCIDs mean nothing in our tree. M06's copier drops its structure keys; the MCIDs left inside the XObject's content are what veraPDF flags, so the slice that tags figures proves them neutralized on a tagged source page.
- MCIDs are per page, and a sequence belongs to one content stream. A sequence left open at the end of a
page's stream, or across
ET, is invalid, and some readers drop the page's whole tree. - Artifacts inside tagged content, and content inside artifacts, are both failures. A background painted in the middle of a paragraph's sequence must close the sequence first.
- Repeated table headers read on every page are the commonest defect of generated tables. The first
occurrence is the
THead; the others are artifacts. The same holds for footers, the other way round. Alton a grouping element hides its content from assistive technology that honors it.aria-labelgoes on figures, formulas and links only.- Templates skip heading levels for styling (
h1thenh3becauseh2is too large). PDF/UA-1 fails it. Remapping it in silence would hide the author's structure from the author; the conflict names the element. display: tableis not a table. Layoutdivs styled as tables must not becomeTables; a real<table>used for layout needsrole=presentation, which the guide recommends.- A
THwithout a scope fails veraPDF when the table has no header associations; writing a scope always is cheaper than inferring when it is needed. - Empty elements confuse readers and waste objects; an element is numbered only when it receives something.
- Language tags from templates are often wrong —
fr_FR,French,(English)as the corpus shows. A malformed tag written as is claims a language nobody can resolve. - Right-to-left glyphs are drawn in visual order. Extraction tools reorder them, or not, by their own rules; the structure's order is logical, but what a tool reads inside a sequence is not the structure's to decide.
- The parent tree is a number tree, not an array. A flat
/Numsof a hundred thousand entries is valid and slow for every reader; leaves of fixed size with/Limitsare what readers expect. - Alternative text is a text string: UTF-16BE with a byte order mark when it is not ASCII, or accented alternative text breaks in every reader.
- A text string longer than 32,767 bytes breaks PDF/A-2 and 3's implementation limits (M14). An
altof a megabyte from a template is cut, and reported. - Screen-reader-only text (clipped to one pixel) is tagged content — and so is extracted by every tool. That is its purpose; the documentation says so, so nobody hides text they want kept out of the file.
Documentation
docs/website/docs/concepts/tagged-pdf.md(new): real content and artifacts, the structure tree, reading order, alternative descriptions, language — and what the library tags without being asked.docs/website/docs/guides/accessible-documents.md(new): writing templates that tag well — titles,lang, headings, tables, lists, figures, links, notes —, the PDF/UA-1 target and its policy, what veraPDF checks and the Matterhorn conditions only a person can judge.docs/website/docs/reference/html-to-structure.md(new): the HTML, ARIA and artifact mapping tables.docs/website/docs/reference/css-tagging-properties.md(new): the-adc-pdf-*properties and the role-map at-rule, their grammar and checks.docs/website/docs/guides/generating-a-document.md(M08's): the structure builder, artifacts and annotations for callers who generate without HTML.docs/website/docs/reference/diagnostics.md: thetagging.*codes.docs/website/docs/reference/tool/:html2pdf --pdf-uaand--untagged.docs/website/docs/introduction.mdanddocs/features/features.json: thetagged-pdfentry brought to its state.docs/architecture.md:Tagging/in the HTML engine, the structure writer inStructure/.docs/corpus.md: the sources this milestone adds,claimsUaanduaConformanceValid, and poppler's structure output as a referee.docs/status.md: the memory bound, the tagging overhead, the referees' behavior on right-to-left text.
Exit criteria
-
PdfStructureBuilderwrites structure forward-only, with memory bounded by the elements open and the pages, measured and recorded. - Image pages (M07) and barcodes (M10) are tagged as
Figures in tagged output. - The HTML engine tags every element by the HTML and ARIA mappings, in document order, with artifacts for all that is not content.
- The CSS overrides and the role map exist, checked, and documented with their grammar.
- The PDF/UA-1 target collects its conflicts and applies
PdfConformancePolicy; 2.0 output refuses it. - 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. - Unit tests cover each behavior, its degenerate cases and its hostile ones; the FsCheck properties hold.
- Integration tests run veraPDF, poppler, pdftotext, PyMuPDF, pikepdf and qpdf in containers.
-
TaggingBenchmarksmeasures the cost of tagging on the reference documents and on M12's long report, withMemoryDiagnoser;status.mdrecords time, allocations and size, tagged and untagged. - The documentation site publishes the pages listed above.
- Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).