Skip to main content

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's PdfDocumentBuilder: 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 /Artifact writer, extended to the Layout, Page and Background types a generator needs;
  • tagging the library's own non-HTML output: image pages (M07) as Figure with the caller's alternative text, barcodes (M10) as Figure with their AlternativeText, 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, flex order, 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 (Link with OBJR and /Contents), notes (Note with 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-div template 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's PdfConformancePolicy;
  • the tool's html2pdf gains --pdf-ua and --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, the Artifact element), 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 — Form elements, 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 a Formula only 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.Conformance satellite (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​

PartWhereWhy
Structure types, attributes, artifacts, the forward-only writerCore, 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 checksAdCodicem.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 testsCore (internal), extending M06'sTests 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:

GroupTypes M13 emitsTypes it never emits by default
GroupingDocument, Part, Art, Sect, Div, BlockQuote, Caption, TOC, TOCIIndex, NonStruct, Private (reachable by override)
BlockP, H1–H6, L, LI, Lbl, LBody, Table, TR, TH, TD, THead, TBody, TFootH (mixing H and Hn is a PDF/UA-1 failure; H only by override, and then alone)
InlineSpan, Quote, Note, Reference, Code, LinkBibEntry (override); Annot (M11's path); Ruby, RB, RT, RP (M30); Warichu, WT, WP refused
IllustrationFigure, FormulaForm (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. Begin records the element under its parent. Content marked with BeginTagged on a page becomes a marked-content reference in the element's kids and an entry in that page's parent-tree array. End closes 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. At Finish: 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, and DisplayDocTitle true 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 /K is 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 /Pg is written as an MCR dictionary with its own /Pg; a paragraph broken across two pages is one P with 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): TR only in Table, THead, TBody or TFoot; TH and TD only in TR; LI only in L; Lbl and LBody only in LI; TOCI only in TOC; Caption first or last in a Table, first in an L; 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 Do in 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 second Do of one.
  • Annotations. Reference writes the OBJR, 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; /Namespaces is M28's.

Tagging the library's own output​

  • Image pages (M07): when the output is tagged, each image page is a Figure under a Part with the caller's alternative text and a Layout BBox; 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 Figure with PdfBarcode.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 BeginTagged and EndMarkedContent goes to the element they opened; text written outside any marked content in a tagged document is refused with InvalidOperationException at Finish of 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:

  1. the author's CSS: -adc-pdf-tag and its companions (below);
  2. the element's WAI-ARIA role, as the ARIA table below maps it;
  3. the element's HTML semantics, as the HTML table maps it;
  4. 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):

HTMLStructureNotes
html / bodyDocumentOne per document; lang on html becomes the catalog's /Lang
articleArt
section, main, navSectnav with -adc-pdf-tag: TOC on its list becomes a table of contents
header, footer, aside, address, div, hgroupDivIn the flow they are content; in a margin box or running element they are artifacts
pP
h1–h6H1–H6
blockquoteBlockQuotecite ignored
qQuoteIts generated quotation marks are content of the Quote
preP, or Code when it holds only a codeLine breaks are content
code, kbd, sampCode
abbr title, acronym titleSpan with /EThe expansion from title
span lang, any inline element whose lang differs from its parent'sSpan with /LangOtherwise inline elements are transparent
em, strong, b, i, mark, small, sub, sup, time, cite, dfn, vartransparentPDF 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 hrefLinkIts text, and one OBJR per annotation M12.6 writes for it
imgFigure with /Altalt="", role=presentation or role=none: a Layout artifact
svg (inline or as an image)Figure with /AltFrom aria-label, aria-labelledby, or its <title>; its text drawn inside the figure's one sequence
figureFigureHolding the image's content directly, and Caption for figcaption; two or more images make Figures under a Div
figcaptionCaption
ul, ol, menuL with ListNumberingFrom list-style-type (below)
liLI: Lbl for ::marker, LBody for the rest
dlLEach dt group is an LI, dt its Lbl, dd its LBody
tableTable with a Layout BBox when on one pagerole=presentation or role=none: transparent, rows and cells too
captionCaption
thead, tbody, tfootTHead, TBody, TFootAn implied tbody is written
trTR
thTH with /ScopeAlways 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
tdTDrowspan, colspan, and headers as /Headers over the header cells' IDs
hrLayout artifact
br, wbrnothing
input, select, textarea, buttonpainted as content in M13; Form is M17's
mathnot rendered by M12 (M30)
iframe, object, embed, video, audio, canvasas 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:

roleStructure
heading with aria-level nHn; above 6, H6, reported
paragraph, blockquote, code, caption, figureP, BlockQuote, Code, Caption, Figure
list, listitemL, LI
table, grid, row, rowgroupTable, Table, TR, TBody
cell, gridcell, columnheader, rowheaderTD, TD, TH Column, TH Row
imgFigure, its descendants presentational: one sequence, no element inside
linkLink when the element has a target M12.6 can link; Span otherwise
mathFormula with /Alt from aria-label
doc-toc, doc-footnote, doc-endnote, doc-noteref (DPUB-ARIA)TOC, Note, Note, Reference
presentation, nonetransparent; 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, cm and q that 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:

PaintedArtifact
Margin boxes, running elements, position: fixed repetitionsPagination, 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 imagesBackground
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 subtotalsPagination — the tagged THead is the first occurrence and the tagged TFoot the last
Decorative images and generated content whose alternative text is emptyLayout

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.

PropertyValuesInitialWhat it does
-adc-pdf-tagauto | none | artifact | a standard type | a custom nameautoThe 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-artifactauto | layout | pagination | page | backgroundautoThe artifact type, when the element is one
-adc-pdf-artifact-subtypenone | header | footer | watermark | page-number | batesnoneThe subtype; the last two are written as Header or Footer in 1.7 output, by position (M09)
-adc-pdf-altnone | <string> | attr(<name>)none/Alt
-adc-pdf-actual-textnone | <string> | attr(<name>)none/ActualText
-adc-pdf-expansionnone | <string> | attr(<name>)none/E
-adc-pdf-scopeauto | row | column | bothautoA TH's /Scope
-adc-pdf-list-numberingauto | none | disc | circle | square | decimal | upper-roman | lower-roman | upper-alpha | lower-alphaautoAn 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 to auto.
  • Overrides are checked against containment when the tree is built: an override that would put a TD outside a TR, or an LI outside an L, falls back to auto for that element and is reported (tagging.override-containment). Types whose content model the engine cannot honor — Form, Ruby, Warichu and 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:

RequirementWhat the engine does
§5: the identification schema, pdfuaid:part 1Written 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 otherBy 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 /SuspectsBy construction and by the role-map checks
§7.2: the natural language determinable for all text, alternative text includedlang on html, or the caller's Language option; none: a conflict
§7.3: a Figure has alternative textEach 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 mixedtagging.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 associationsAlways written
§7.6: lists L, LI, Lbl, LBodyBy construction
§7.8: running heads and feet as pagination artifactsBy construction
§7.9: a Note has a unique /IDGenerated
§7.10: optional-content configurations named, without /ASM12 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 /ContentsBy construction; an empty accessible name is a conflict
§7.18.3: /Tabs /S on every page with annotationsWritten
§7.20: no reference XObject; a form XObject with MCIDs drawn onceBy construction
§7.21: fonts embedded, every glyph mapped to Unicode, no .notdefM08 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):

CodeSeverityMeaning
tagging.alt-missingWarningA figure has no alternative text
tagging.title-missing, tagging.language-missingWarningThe document has no title, or no language
tagging.language-invalidWarningA lang value is not a BCP 47 tag; not written
tagging.heading-level-skipped, tagging.first-heading-not-h1WarningThe heading sequence PDF/UA-1 §7.4 requires is broken
tagging.link-name-missingWarningA link has no accessible name
tagging.table-irregularWarningRows of one table have different numbers of cells after spans
tagging.role-map-rejected, tagging.override-containmentWarningA role-map entry or an override was refused; the element fell back to auto
tagging.id-renamedInformationA caller's or template's id collided; the structure ID was suffixed
tagging.list-numbering-approximated, tagging.visual-order-differs, tagging.aria-hidden-artifactInformationAs above
tagging.untagged-contentWarningContent 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-removedConformanceLossThe 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.

  1. The structure writer. Delivers PdfStructureBuilder, PdfStructureElement, the standard types, BeginTagged on 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 of Begin, End and 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's pdfinfo -struct-text prints it with its text, qpdf --check is silent, and veraPDF's PDF/UA-1 profile finds no failure in the structure clauses. Leaves artifacts, attributes and annotations.
  2. Artifacts, attributes, alternative descriptions and annotations. Delivers PdfArtifact public with the Layout, Page and Background types beside M09's Pagination, the attribute owners, Alt, ActualText, E and Lang on elements and on marked content, Reference for annotations with /Tabs, the pdfuaid identification and DisplayDocTitle, 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 with pdfinfo -struct-text on the three committed PDF/UA-1 documents and on every committed document with a structure tree (below). Leaves the library's own outputs.
  3. The library's own outputs. Delivers Figure tagging 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-text shows each Figure with its Alt. Leaves the HTML engine.
  4. 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 order pdfinfo -struct-text prints 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.
  5. Tables, lists, figures and links. Delivers Table and its parts with scope, spans, Headers and IDs, captions and BBox; repeated headers and footers and carried-forward subtotals as artifacts; L with ListNumbering, Lbl, LBody, and dl; Figure for img, svg, figure and figcaption; Link with its OBJRs and /Contents. Proved by unit tests per element and per edge (a table spanning three pages, a rowspan across a page break, a link broken across lines, an image with alt=""); integration: the rendered invoice's line-item table and the report's measures table pass veraPDF's table rules, and pdfinfo -struct-text shows each header row once. Leaves generated content and notes.
  6. Generated content, tables of contents and notes. Delivers ::marker, ::before and ::after as content, CSS alternative text for generated content, leaders as artifacts, TOC and TOCI for M12.6's generated tables of contents, and Note, Reference and IDs for M12.7's footnotes. Proved by unit tests; integration: the report's contents (its nav list, and the same contents generated by target-counter()) and the accessibility reference's footnotes pass veraPDF, each Note found once after its call in pdfinfo -struct-text. Leaves language.
  7. Language, abbreviations and bidirectional text. Delivers /Lang on elements and on spans (the catalog's is M12.6's), BCP 47 checking, E from abbr, the title check, per-run sequences for bidirectional lines, and the decision on right-to-left ActualText. Proved by unit tests; integration: the accessibility reference (French with English, German and Arabic passages) passes veraPDF's language rules; pdfinfo -struct-text gives 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.
  8. ARIA and CSS overrides. Delivers the ARIA mapping, accessible names, aria-hidden, the -adc-pdf-* properties, @-adc-pdf-role-map and its checks, and the containment fall-back. Proved by unit tests of every property, every precedence and every refusal (a cycle, a remapped P, a TD outside a row); FsCheck over arbitrary role-map declarations (the checker is total and bounded); integration: the layout-div invoice, tagged only by a stylesheet, produces the same structure — types, order and text — as the semantic invoice, and passes veraPDF. Leaves the target.
  9. The PDF/UA-1 target. Delivers PdfConformanceTarget.PdfUA1, the pre-write checks, the collected conflicts under Refuse and RemoveClaim, the 2.0 refusal, and html2pdf --pdf-ua; the manifest's claimsUa and uaConformanceValid — the PDF/UA part claimed and veraPDF's verdict on it, beside the PDF/A claim's two fields —, written by build_corpus.py, the calibration the referee needs before it judges ours. Proved by unit tests with a set of faulty templates — an image without alt, h1 then h3, an empty link, no <title>, no lang, 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 under RemoveClaim carries no pdfuaid, and veraPDF's failures on it are the conflicts we reported. Leaves scale.
  10. 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 in status.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 and fr-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 Do wrapped; 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; a div with 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 (one THead), a tfoot repeated (one TFoot, 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-labelledby cycles and self-references, an alt of 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 under RemoveClaim, exactly the failures we reported.
  • poppler, pdfinfo -struct and -struct-text: the tree, its types, its Alt and Lang, 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, /StructParents and /StructParent keys, /Tabs, and that every image XObject drawn is inside a Figure with /Alt or inside an artifact.
  • qpdf --check on 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.

DocumentsBehaviorVerified by
The reference documents, tagged under the PDF/UA-1 targetveraPDF's PDF/UA-1 profile reports no failure; qpdf is silentTaggingRefereeTests.Reference_documents_pass_pdf_ua_1
The reference documentsThe 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 breakCorpusTaggingTests.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 pageThe 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 onceTaggingRefereeTests.Reading_order_matches_the_visual_order_across_columns_and_pages
Every document this milestone generates, faulty templates includedEvery 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 alikeCorpusTaggingTests.Every_image_has_alternative_text_or_is_an_artifact
Every document this milestone generatesNo painting operator outside a tagged sequence or an artifact; no artifact inside tagged contentCorpusTaggingTests.All_content_is_tagged_or_an_artifact
The rendered invoice and the reportEach 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 /SCorpusTaggingTests.Tables_and_links_are_tagged_as_pdf_ua_1_asks
The layout-div invoice, tagged by a stylesheet aloneIts structure — types, order and text — equals that of the semantic invoice; veraPDF's PDF/UA-1 profile reports no failureCorpusTaggingTests.Css_overrides_tag_a_layout_template_like_semantic_html
The accessibility referenceEach 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 /ETaggingRefereeTests.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 emptyThe 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 differCorpusTaggingTests.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 runCorpusTaggingTests.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 codesveraPDF's PDF/UA-1 profile reports no failure; each image page and code is a Figure with its textTaggingRefereeTests.Image_pages_and_barcodes_are_figures
Our tagged report merged by M06 with each of the three PDF/UA-1 documentsveraPDF's PDF/UA-1 verdict is kept: the claim stays and no failure appearsTaggingRefereeTests.Merging_tagged_outputs_keeps_pdf_ua_1
The faulty templates under Refuse and RemoveClaimExactly the conflicts each carries, collected in one exception; under RemoveClaim, no pdfuaid in the file and veraPDF's failures are the conflicts we namedCorpusTaggingTests.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 sourceMemory flat as pages grow, within the budget recorded in status.md; the structure's size per page measuredCorpusTaggingTests.Tagging_a_thousand_pages_holds_its_budget, TaggingBenchmarks
Every row aboveTwo renders give identical bytes, under the invariant culture and under fr-FRCorpusTaggingTests.Tagging_is_deterministic
The reference documents through the toolhtml2pdf --pdf-ua produces the API's bytesCorpusToolTests.Html2pdf_pdf_ua_matches_the_api

Corpus​

What the corpus holds​

  • Sources: sources/invoice-fr.html (two tables — the line items and the totals —, h1 and h2, lang="fr", a title), sources/report-fr.html (a nav contents list of eight links, nine h2 sections, a 13-row table with thead, an ordered list, a two-column annex), sources/contract-fr.html (eight articles under h2). None has an image, a figure, a footnote, a nested list, a table with rowspan, colspan or headers, 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 thead table, 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 /Lang is 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, StructParents without 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​

NeedWhyPriorityLikely source
Our engine's renderings of the three reference sources, documents/*/adcodicem-*-fr.pdf, committedEvery acceptance row is written against them; M12 commits them untagged, and M13 regenerates them tagged in the commit that turns tagging on1Generated 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 barcodeThe existing sources exercise a fraction of the mapping; the roadmap's "every image carries alternative text" cannot be tested on sources that have no image1Generated 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 itThe CSS overrides' acceptance compares it with the semantic invoice1Generated 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 typeThe PDF/UA-1 conflicts must be shown exactly, and on documents a template author would write1Generated 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 manifest2Generated 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 optionTwo 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 disagree2Generated 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, committedThe reader's calibration and M06's merge rest on two committed producers3Generated 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 scriptThe bidirectional decision of slice 7 is checked on our output only3A 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.
  • Alt on a grouping element hides its content from assistive technology that honors it. aria-label goes on figures, formulas and links only.
  • Templates skip heading levels for styling (h1 then h3 because h2 is 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: table is not a table. Layout divs styled as tables must not become Tables; a real <table> used for layout needs role=presentation, which the guide recommends.
  • A TH without 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 /Nums of a hundred thousand entries is valid and slow for every reader; leaves of fixed size with /Limits are 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 alt of 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: the tagging.* codes.
  • docs/website/docs/reference/tool/: html2pdf --pdf-ua and --untagged.
  • docs/website/docs/introduction.md and docs/features/features.json: the tagged-pdf entry brought to its state.
  • docs/architecture.md: Tagging/ in the HTML engine, the structure writer in Structure/.
  • docs/corpus.md: the sources this milestone adds, claimsUa and uaConformanceValid, 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​

  • PdfStructureBuilder writes 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 corpus run recorded in status.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.
  • TaggingBenchmarks measures the cost of tagging on the reference documents and on M12's long report, with MemoryDiagnoser; status.md records 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).