PDF/UA tagging

PDF/UA tagging

htmlbag emits PDF logical-structure elements when the underlying document is set to document.FormatPDFUA (PDF/UA-1, ISO 14289-1, on PDF 1.7) or document.FormatPDFUA2 (PDF/UA-2, ISO 14289-2, on PDF 2.0). Tagging is automatic — the tree of structure elements is built as content is laid out, and each VList carries a back-reference to the structure node it should attach to.

Activation

Set the format on the frontend.Document before constructing the CSSBuilder:

// PDF/UA-1 (ISO 14289-1)
fd.Doc.Format = document.FormatPDFUA

// or PDF/UA-2 (ISO 14289-2, PDF 2.0)
fd.Doc.Format = document.FormatPDFUA2

cb, _ := htmlbag.New(fd, css)

After New(), cb.enableTagging is true, cb.structureRoot points to a root structure element (created on fd.Doc if not already present), and cb.structureCurrent tracks the current parent during walk.

For UA-2, New() also declares the PDF 2.0 Standard Structure Namespace and an HTML5 namespace with a complete RoleMapNS on fd.Doc, so every emitted structure element is properly namespace-qualified (required by ISO 14289-2 §8.2.4).

Element-to-role mapping

Each HTML tag maps to a canonical PDF Standard Structure role; the final emission depends on the active format.

HTML PDF SSN (UA-1) HTML5 NS (UA-2)
<h1> … <h6> H1 … H6 h1 … h6
<p>, <pre> P p
<div> Div div
<span> Span span
<a> Link a
<img> Figure figure
<figure> Figure figure
<figure> with blocks inside Sect section
<figcaption>, <caption> Caption caption
<table> Table table
<thead>/<tbody>/<tfoot> THead/TBody/TFoot thead/tbody/tfoot
<tr>, <th>, <td> TR, TH, TD tr, th, td
<ul>/<ol> L L
<li> LI li
<blockquote> BlockQuote blockquote
<code> Code code
<section> Sect section
<article> Art article
<math> Formula Formula

Roles that have no HTML5 equivalent (Document, LBody, Lbl, L, Part, Formula) stay in the PDF 2.0 Standard Structure Namespace under UA-2.

A <figure> with blocks inside, such as an image and a <figcaption>, is a Sect that holds a Figure with the image’s /Alt and a Caption. It is a Sect rather than a Div because ISO 32005 sees through a Div and would count the Caption as a child of the element around the figure. A table’s <caption> is set as a block above the table and tagged as the first child of its Table.

Internally, canonicalRoleForTag resolves the HTML tag to the canonical PDF SSN name, then newSE(canonical, format) builds the StructureElement with the right Role and NS. The two helpers live in htmlbag/tagging.go.

Footnotes attach as Note structure elements under UA-1 and as FENote under UA-2: PDF 2.0 replaced Note with FENote and has no Note in its Standard Structure Namespace.

Repeated table headers

When a table breaks across pages, the repeated header rows on continuation pages are tagged as artifacts (not as TH again) to avoid duplication in the structure tree.

Form XObjects (imported PDFs)

A <img src="…pdf"> (an imported PDF page) is wrapped as a Form XObject with /StructParent and an OBJR-entry in the parent Figure’s /K array (PDF 1.7 §14.7.4.4 + PDF/UA-1 §7.1 Note 1). This stops Acrobat’s tag inspector from exploding the imported content stream’s path operators into a “Pfad / Path” list under the Figure tag.

Mathematics (Formula)

An inline <math> element (see Mathematics (MathML)) is tagged as a Formula structure element with two accessibility payloads:

  • /Alt — a plain-text fallback. It comes from the MathML alttext attribute when the author supplies one, otherwise from a plain-text rendering of the token content (<mi>/<mn>/<mo> concatenated in document order).
  • /AF — under PDF/UA-2 only — the MathML source embedded as an associated file (application/mathml+xml, /AFRelationship /Supplement, PDF 2.0 §14.13). The xmlns="http://www.w3.org/1998/Math/MathML" namespace is re-attached to the root <math> so the file is self-describing XML. A MathML-aware reader consumes this for full semantics; PDF/UA-1 (PDF 1.7) emits the Formula element and /Alt but no associated file, since associated files are a PDF 2.0 feature.

Because a formula usually sits inside a sentence, the renderer splits the surrounding paragraph’s marked content so the reading order is text · formula · text, with the Formula element nested between the two text runs of its paragraph:

<P>
 ├─ MCID 0   "The relation "          (text before)
 ├─ <Formula>            /Alt "a 2"   /AF → formula.mml
 │    └─ MCID 1          (formula glyphs)
 └─ MCID 2   " matters."              (text after)

The split relies on a /K array serialized in document reading order: each marked-content and object reference carries a document-wide reading-order stamp, and a child element inherits the smallest stamp in its subtree, so the three kinds of /K entry (marked content, object references, child elements) interleave correctly. The paragraph’s own ActualText is suppressed when it contains a formula — ActualText would otherwise replace the element’s entire content and hide the formula’s /Alt from assistive technology.

Destinations under UA-2

PDF/UA-2 §8.8 requires intra-document destinations to be structure destinations, not page destinations. When the document is FormatPDFUA2, the PDF outline that OutputPagesFromText writes (see PDF bookmarks) uses /Dest [ <SE-objref> /Fit ] for each heading instead of a page destination. The link from each HeadingEntry to its backing StructureElement is set in vlistbuilder.go at the time the heading SE is constructed.

Internal links get structure destinations as well. A tagged block with an id, a container, a table and the inline ids of a paragraph register their structure element under the id in fd.Doc.StructureDestinations; when two elements claim an id, the one closer to the start of the document keeps it. Under UA-2 the GoTo action of a link to #id then carries /SD with that structure element next to /D for viewers that do not read it. Finish writes these actions at the end of the document, as a target may come after its link.

Custom roles

The mapping in canonicalToHTML5 (htmlbag/tagging.go) is the single source of truth for the PDF SSN ↔ HTML5 correspondence. To add a custom mapping, extend that map; html5RoleMap() builds the UA-2 RoleMapNS from it automatically.

Verification

Both profiles are validated against veraPDF as part of the showcase examples:

verapdf --flavour ua1 boxesandglue-examples/glu/xslfo/10-pdfua/result.pdf
verapdf --flavour ua2 boxesandglue-examples/glu/xslfo/11-pdfua2/result.pdf

Both report isCompliant=true.

The MathML showcase boxesandglue-examples/glu/html/mathml renders as PDF/UA-2 and passes pdfa11y, including the dedicated MathML checks (Formula has a math representation; the associated file declares Subtype = application/mathml+xml, AFRelationship = /Supplement, and is well-formed XML rooted at a namespaced <math>):

pdfa11y boxesandglue-examples/glu/html/mathml/result.pdf
# Verdict: PASS