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 MathMLalttextattribute 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). Thexmlns="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 theFormulaelement and/Altbut 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.pdfBoth 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