Here is a real one: a privacy notice published by a Hungarian village council, five pages, exported from Word 2019 — one of 111 born-digital public-sector PDFs in the sample behind our earlier piece on where Word-exported PDFs actually fail. We picked it because it carries almost everything a tagged PDF can contain in a small space: a title, headings, a table, a list, a figure, a couple of links.
What's actually inside
Every tagged PDF has a structure tree — a hierarchy stored alongside the pages that a screen reader, Acrobat, or a plain PDF library can walk independently of what's drawn on the page. Ours starts like this:
uv run python scripts/dump_struct_tree.py privacy-notice.pdf
Document
Title MCIDs=[0, 1]
Figure MCID=30
Title MCIDs=[2, 3]
P MCID=4
P MCID=5
P MCID=6
Table
THead …
TBody … (7 rows total — the contact-details table below)
H3 MCID=24
P MCID=25
P MCID=26
P MCID=27
P MCID=28
H3 MCID=29
P MCID=0
L [ListNumbering=/Disc]
LI
Lbl MCID=1
LBody MCIDs=[5, 6]
LI
Lbl MCID=7
LBody MCID=10
H3 MCID=11
Document is the root. The second Title element holds the notice's actual
headline, "Weboldal Adatkezelési Tájékoztató" ("Website Privacy Notice") —
Word tags a headline this way and maps it to a top-level heading behind the
scenes, so a screen reader announces it the way it would announce any other
H1. The first Title, MCIDs 0 and 1, turns out to be blank: two marked
regions with no characters in them at all, sitting above the badge that
comes next. Tagged, styled, empty. Figure marks that badge — a small blue
circle with the EU's ring of stars and the word "GDPR" inside it, the kind of
clip art every second privacy notice opens with: a picture with a bounding
box on the page and, we'll come back to this, nothing else. Table splits into header and body rows, the same shape as an
HTML table and for the same reason — it lets a screen reader say "header:
postal address" before reading the value next to it, instead of a bare grid.
H3 marks the section headings further down the page, the same tags that
let a screen reader user
jump straight between headings
rather than reading everything in between, the way a sighted reader skims a
page for the right subheading. L is the list: each LI splits into a
Lbl (the bullet itself) and an LBody (what the bullet introduces) — two
separate tags for what looks, on the page, like one line of text.
The numbers written on the page
None of that is decoration. Every MCID — Marked Content ID — is a number
written directly into the page's own drawing instructions, the same stream of
"draw this glyph here" commands that produces the visible page. A tag
doesn't contain text; it points at a numbered region of the page and states
what that region is. Pull the text bound to a set of MCIDs and you get
exactly what a screen reader would read out for that tag, which is what
scripts/debug_dump_struct.py does for every table cell:
uv run python scripts/debug_dump_struct.py privacy-notice.pdf
TABLE #1
Row 0: n_cells=2
Cell 0: role=/TH text=A weboldal üzemeltetője, … (a továbbiakban: Adatkezelő):
Cell 1: role=/TH text=[the council's name]
Row 1: n_cells=1
Cell 0: role=/TH text=Elérhetőségeink:
Row 2: n_cells=2
Cell 0: role=/TH text=Postai címünk:
Cell 1: role=/TD text=[postal address]
(Row 0 names the data controller — the council that publishes the file. We have replaced the council's name and address here: this article is about the tag tree, not about which village got it wrong, and with 39 clean sites out of 1,215 in our wider crawl this is a systemic pattern rather than one office's failing. Row 1, "Elérhetőségeink" — "our contact details" — is a sub-heading welded into the table as a row with a single cell, where every other row has two.)
That single-cell row is where the tree stops matching the standard, which is the whole point of this article.
Tags say what something is, not whether it's right
A structure tree tells a screen reader what kind of thing it's looking at. It says nothing about whether that classification is complete or correctly formed — and the same file that produced the tree above fails PDF/UA-1 on eight separate rules when checked with veraPDF 1.30.2 (build 2026-06-03):
~/.local/share/verapdf/verapdf -f ua1 --format json privacy-notice.pdf
| Clause | What it means | What veraPDF found here |
|---|---|---|
| 7.2-43 | table rows must share the same column count | "Table rows 1 and 2 span different number of columns (2 and 1 respectively)" — the exact row quoted above |
| 7.4.2-1 | a heading sequence can't skip a level | the document's first heading is tagged H3; H1 and H2 never appear anywhere in the file |
| 7.3-1 | figures need alternative text or a replacement | "Figure structure element neither has an alternate description nor a replacement text" — the GDPR badge at the top of page one |
That gap between "tagged" and "correct" is exactly what WCAG's own technique
for adding alt text inside a tagged PDF
is written against: when an image carries a word that matters — "GDPR," in
this case, sitting right there in the badge's own pixels — that word belongs
in the /Alt entry, not just on the page. Without it, a screen reader
reaching this Figure has no filename to fall back on and no caption to
read; just a picture tag with nothing behind it.
Eight rules, 36 individual failed checks in total, against 17,049 checks the same file does pass. It has a title, a marked-content flag, a declared language, a table, a list, a heading structure — genuinely tagged, by any definition a checklist would accept — and it still isn't a conforming file. "Has tags" and "conforms to PDF/UA-1" are two different claims, and only one of them is what the standard actually requires. What each clause means in general, and which ones repair automatically, is in our PDF/UA error reference; what the standard requires end to end, and how it relates to WCAG, is in the PDF/UA guide.
The alternative is worse
Set that file next to one with no tree at all. A benefit-application form from the same sample — "KÉRELEM az ápolási díj megállapítására," a request for a nursing-care allowance — was produced through Windows' own Print to PDF driver rather than Word's PDF export. Run the same command against it:
uv run python scripts/dump_struct_tree.py apolasi-dij-kerelem.pdf
### apolasi-dij-kerelem.pdf: no StructTreeRoot ###
There is no tree to walk. Every character on the page carries mcid: None —
pdfplumber can still pull the text out, because the glyphs are real text and
not a scanned image, but nothing marks which characters form the numbered
heading "1.1.1. Neve:" ("1.1.1. Name:") versus the blank line meant for an
answer. Visually, the numbering makes the structure obvious. To a screen
reader, every one of those lines is the same kind of nothing.
What a tag tree can't tell you
Look again at row 0 of that contact table. Both cells are tagged /TH — a
header cell — even though the second one holds the council's actual name,
not a label. Nothing in the standard requires a /TH to genuinely introduce
something; the row/column check that flagged row 1 fires on a mismatched
count, not on whether a header cell is really a header. Whether it is stays
a judgement call no validator makes.
That's the trap in trusting a tree view on its own. A tags panel — Acrobat's or PAC's — renders exactly the tree we've been reading here: correctly nested, properly labelled, easy to mistake for "done." It doesn't run the row-count check, the heading-sequence check, or the alt-text check against what it's showing you; those are a separate validation pass, over the same file, against the same tree. A tree that looks complete and a file that conforms are two different claims, and a tree view only ever answers the first one.
Seeing the tree on your own file
How to open a tag tree without a terminal
Adobe Acrobat Pro (Windows, macOS)
View → Show/Hide → Navigation Panes → Tags opens the same kind of tree shown above. Click any tag and Acrobat highlights the matching region on the page — the fastest way to see whether a heading, table or figure actually points at what you think it does.
PAC (PDF Accessibility Checker, free, Windows)
PAC's "Logical Structure" view shows the tree alongside every PDF/UA-1 failure it finds, which is closer to what this article did than Acrobat's tree view on its own.
No installation
Our own PDF accessibility checker reports the same clause-level failures veraPDF does, from a browser, and the check is free.
If a document has come back from a validator with a list of clause numbers, that's the second half of this article, not the first — the error reference linked above explains what each one means. If a document has never been checked at all, that's worth doing before assuming the tags, if there are any, are enough.