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> H1H6 h1h6
<p>, <pre> P p
<div> Div div
<span> Span span
<a> Link a
<img> Figure img
<figure> Figure figure
<table> Table table
<thead>/<tbody>/<tfoot> THead/TBody/TFoot thead/tbody/tfoot
<tr>, <th>, <td> TR, TH, TD tr, th, td
<ul>/<ol> L ul/ol
<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, Note, LBody, Lbl, L, Part, Formula) stay in the PDF 2.0 Standard Structure Namespace under UA-2.

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.

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).
  • /AFunder 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.

Outline 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, glu/markdown’s outline generator emits /Dest [ <SE-objref> /Fit ] for each heading instead of [ <page-objref> /Fit ]. The link from each HeadingEntry to its backing StructureElement is set in vlistbuilder.go at the time the heading SE is constructed.

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