docling-project / docling-project/docling
Proposal: tagged-PDF structure ingestion for PDF/UA-2 beyond forms (companion to #4143)
- Dominant language
- Python
- Stars
- 66.4k
- Forks
- 4.8k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 84
Description
Companion to #4143 (forms). This outlines the remaining PDF/UA-2 areas in the same tagged-first pattern: read the structure tree when a file has one, run the models for the untagged tail, and record on every item which path produced it. Phase 1, the structure-tree reader in docling-parse, is open as docling-project/docling-parse#337. Section 2.1 carries the benchmark numbers from #3988 that motivate the split.
**Status:** proposal for discussion. **Assumes:** docling #4140 and #4144 land and the forms follow-on in #4143 proceeds. **Basis:** Well-Tagged PDF 1.0 (WTPDF), which ISO 14289-2 (PDF/UA-2) adopts. **Author:** Jeff Witt. **Date:** 2026-09-02.
## 1. Goal
Forms were the first area where Docling learned to treat authored PDF semantics as authoritative and fall back to models only when they are absent. This outline applies the same pattern to everything else WTPDF specifies. The testable statement generalizes the one in #4143:
> For a PDF that conforms to WTPDF, the `DoclingDocument` body tree, content layers, heading levels, list and table structure, captions, alternate text, links, footnotes, and language declarations are derived from the structure tree, and each item records that origin. For a PDF that does not, Docling runs its models as today and records that instead.
The parity target also generalizes: a WTPDF-conformant PDF and its HTML twin should produce the same tree, because HTML is the natural expression of the same semantics (heading levels, list markup, `th` with scope, `alt`, `lang`, `a href`, footnote references).
### 1.1 Why PDF/UA-2 is the model, not the gate
Adoption check, 2026-09-02. Regulators still cite PDF/UA-1: the Section 508 refresh incorporates ISO 14289-1 by reference, and Title II and EN 301 549 point at WCAG, not at PDF 2.0. PAC fully supports PDF 1.7 and treats PDF 2.0 files as "best try", so auditors cannot confirm UA-2. Acrobat, Word, InDesign, and LibreOffice 25.8 export UA-1; production UA-2 comes from LaTeX (default under TeX Live 2026) and commercial libraries (iText, BFO, PDFlib). Viewer support for the PDF 2.0 namespace elements (Title, FENote, Aside, DocumentFragment) is still arriving, so screen readers see fewer of those tags today than the UA-1 equivalents. veraPDF validates UA-1, UA-2, and WTPDF.
Consequences for this proposal:
- The reader is modeled on PDF 2.0 and WTPDF because that vocabulary is the superset: namespaces and role maps resolve UA-1 and legacy tagging into the same types. It is not a conformance gate. The stage keys on `MarkInfo` `Marked`, never on `pdfuaid:part`.
- The tagged corpus in the wild is overwhelmingly untagged, legacy-tagged (non-conformant), or UA-1. Fixtures and thresholds follow that distribution; UA-2 files are the PDF 2.0 edge of the test matrix, not its center.
- WTPDF clauses are heuristics for non-conformant files, not contracts. Every "shall" a real file violates (`H` instead of `Hn`, Figure without Alt, `L` without `ListNumbering`, irregular table headers) degrades that element to the model path. It never fails the page.
## 2. What Docling already has
Not starting from zero. These pieces exist and shape the design.
| Capability | Where | Gap relative to WTPDF |
|---|---|---|
| `/ActualText` substitution into text cells | docling-parse marked-content tracking | Works per span; the substitution is not attributed as such on the item. |
| Document `/Lang` detection | docling-parse annotation reader | Detected and logged; not surfaced into the document model. |
| Document outline | `get_document_outline`, consumed by the heading-hierarchy model | Outline is navigation (8.12.2), not structure; heading levels should come from `Hn` elements (8.2.5.12). |
| Hyperlinks | Page-level rectangles matched geometrically in page assembly | Link and Reference structure elements define the exact span (8.2.5.20); geometry is secondary (8.9.3.2). |
| Annotations | `get_annotations` in docling-parse; `add_comment` with targets and the `comment_section` group in docling-core | Not wired into the pipeline. |
| Content layers | `ContentLayer` body, furniture, background, invisible, notes | Assigned by the layout model, not by Artifact tagging (8.3). |
| Alt text slots | `PictureItem.annotations` with `DescriptionAnnotation`; `PictureMeta.description` | Populated by picture-description models only. |
| Captions, footnotes, references | `captions`, `footnotes`, `references` on pictures and tables | Filled by geometry heuristics; Caption, FENote, and `Ref` give exact pairing. |
| Per-item language | `BaseMeta.language` | Never set from PDF. |
| Widgets | #4140 and #4144 | Forms path; the template for everything below. |
The missing foundation is one thing: docling-parse does not read the structure tree. Every area below depends on that.
### 2.1 What our measurements add
YRA's benchmark over OmniDocBench and DocLayNet structure slices (about 40 engines, element-level scoring; methodology in docling discussion #3988) gives the untagged baseline the tagged path is measured against.
| Finding | Number | Consequence |
|---|---|---|
| Heading levels on the PDF path | every heading defaults to level 1 | Explicit `Hn` levels (8.2.5.12) fix this outright for tagged files. |
| Heading versus body | role accuracy 0.96 | The model side is strong; the gap is levels, not detection. |
| Captions, footnotes, formulas, code | downgrade to `P` in our PDF/UA writer | Detection is not reliable enough to assert the tag; structure elements are. |
| Element pass rate | 0.71 on OmniDocBench, 0.77 on DocLayNet (n=50 each) | Docling is the strongest open structure scaffold we measure. |
| Reading order | about 6 points of headroom against specialized engines | Structure order (8.2.3) removes the question for tagged files. |
These numbers are the argument for the split in section 3: tags where they exist, models for the rest, and the remediation-corrections corpus from #3988 as training signal for the rest.
## 3. Foundation: structure-tree ingestion
**Clauses:** 8.2.1 to 8.2.4 (logical structure, real content, logical content order, structure types), 8.2.6 (attributes), 8.3 (artifacts).
**docling-parse.** A structure-tree reader (docling-project/docling-parse#337) producing, per document, a tree of elements with: standard type after role mapping and namespace resolution (PDF 1.7 and PDF 2.0 namespaces, 4.4 and 4.5), attributes (`Alt`, `ActualText`, `E`, `Lang`, `ID`, `Ref`, layout, list, table, PrintField, Artifact, ARIA-1.1), structure order index, and kids resolved to either marked-content ids on a page, object references to annotations, or child elements. Per page, a map from marked-content id to the text cells and graphics drawn inside it, plus artifact marked-content sequences with their `Type` and `Subtype`. Exposed as `PdfDocument.structure` and `SegmentedPdfPage.marked_content`.
**docling.** A `TaggedStructureModel` stage placed where #4140 placed the form stage: after OCR, before layout postprocessing. When the document is tagged (`MarkInfo` `Marked` true) and the option allows, it converts structure elements with page content into pre-labeled clusters with confidence 1.0 and the cells assigned from marked-content ids, exactly as the form stage injects widget rectangles. Layout postprocessing then treats them as authoritative. Reading order comes from structure order (8.2.3), not from the reading-order model. Options: `tagged_structure: Literal["off", "prefer", "require"]`, default `prefer` once stable. `prefer` uses tags where present and models elsewhere on the same page, which matters for partially tagged files.
**docling-core.** A `PdfStructureSource(kind="pdf_struct", element_id, element_type, page_no, mcids)` source kind, so every item can say which element produced it. A document-level record of the declared conformance (Appendix C `pdfuaid:part`, WTPDF PDF Declarations from 6.1) and of whether the structure path was used, so consumers know how much to trust the tree.
**Fallback.** Untagged or `MarkInfo` absent: today's pipeline unchanged. Partially tagged: per-page mix, with `source` distinguishing tagged from predicted items.
## 4. Areas, in the forms pattern
Each row names the clause, the docling-core target, and the specific gap. The docling-parse need is the foundation in section 3 unless noted.
### 4.1 Block structure and headings
| Clause | Structure | docling-core target | Gap |
|---|---|---|---|
| 8.2.5.2 to 8.2.5.6 | Document, Part, Art, Sect, Div | Groups with `chapter` and `section` labels | Grouping is inferred by heading hierarchy today; tags give it directly. |
| 8.2.5.12 | H1 to Hn, H prohibited | `SectionHeaderItem.level` | Levels are explicit in tags; the heading-hierarchy model should defer to them. |
| 8.2.5.13 | Title | `TitleItem` | Title must not be a heading; today it is a layout label. |
| 8.2.5.11 | P | `TextItem` | Direct. |
| 8.2.5.7, 8.2.5.9 | BlockQuote, Aside | none | New labels or group labels needed. Aside maps naturally to the `notes` layer or a group. |
| 8.2.5.8, 8.2.5.30, 8.2.5.31 | TOC and TOCI, Index, BibEntry | `document_index`, `reference` labels | TOC entries carry structure destinations (8.8) that should become intra-document references. |
| 8.2.5.32 | Code | `CodeItem` | Direct. |
| 8.2.5.15, 8.2.5.16 | Sub, Lbl | `MARKER` text items | Direct for list and note labels. |
| 8.2.5.17 to 8.2.5.19 | Span, Quote, Em, Strong | `Formatting` on text items | Em and Strong map to formatting; Quote needs a slot. |
| 8.2.5.23, 8.2.5.24 | Ruby, Warichu | `inline` group | Low priority; needs a representation decision for CJK glosses. |
### 4.2 Lists
**Clause:** 8.2.5.25. `L`, `LI`, `Lbl`, `LBody`, with `ListNumbering` (required when `Lbl` present, never `None`), `ContinuedList`, `ContinuedFrom`, and `Ref` between segments.
**Target:** `list` and `ordered_list` groups, `ListItem` with marker. **Gap:** Docling infers numbering from marker text; `ListNumbering` states it. Lists split across pages are merged heuristically today; `ContinuedList` plus `Ref` makes the merge exact.
### 4.3 Tables
**Clause:** 8.2.5.26 and B.4. Tables shall be regular. `TH` with `Scope`, `Headers` attribute for irregular header association, `RowSpan` and `ColSpan`, `THead`, `TBody`, `TFoot`, Caption as first or last child (8.2.5.27).
**Target:** `TableData` cells with `column_header` and `row_header` flags and spans; `captions`. **Gap:** TableFormer predicts all of this. For tagged tables the structure is ground truth, so the table-structure model should be skipped for tables that come from `Table` elements, which is both more accurate and cheaper. The `Headers` attribute has no slot in `TableCell` today.
### 4.4 Figures, formulas, and non-text content
**Clauses:** 8.2.5.28 (Figure encloses all content that generates its appearance; Alt or ActualText required at the accessibility level), 8.2.5.29 (Formula with Alt or ActualText, MathML via associated files), 8.5 (real content without textual semantics, ActualText for vector-drawn text), 8.2.5.27 (Caption).
**Target:** `PictureItem` with `DescriptionAnnotation` for Alt, `captions` from the Caption child, `FormulaItem` with text from ActualText and MathML from an associated file. **Gap:** Picture description is model-generated today; authored Alt should win and be attributed. Vector-drawn text with ActualText should become a text item, not a picture. Formula enrichment produces LaTeX; a MathML associated file (8.14.2) is a better source when present.
### 4.5 Artifacts and content layers
**Clauses:** 8.3, 8.2.3 (artifact order), 8.9.2.2 (annotations as artifacts).
**Target:** `ContentLayer`. Pagination artifacts (headers, footers, page numbers) to `furniture`, `Background` and `Layout` artifacts to `background`, invisible content to `invisible`. **Gap:** The layout model predicts page headers and footers today; artifact tagging is authoritative and also covers things the model cannot see, such as line numbers inside `Sub` (8.3.2).
### 4.6 Text representation and language
**Clauses:** 8.4.3 (ActualText, PUA), 8.4.4 (Lang on catalog, elements, and marked content), 8.4.5 (fonts, Unicode maps, `.notdef`), 8.5.2.
**Target:** `BaseMeta.language` per item, a document-level language, `orig` versus `text` to preserve the substituted span. **Gap:** docling-parse already substitutes ActualText but nothing records that it did. Language is detected at the catalog and dropped. Unmappable glyphs and PUA code points are a text-quality signal that belongs in Docling's confidence scores.
### 4.7 Links, references, and footnotes
**Clauses:** 8.2.5.20 (Link for external, Reference for intra-document, one element per target, Alt on the element), 8.8 (structure destinations, `Ref` entries), 8.2.5.14 (FENote with `Ref` in both directions), 8.9.2.4.2 (Contents on link annotations, contiguous content).
**Target:** `TextItem.hyperlink` for external links; intra-document references as `RefItem` targets on `references`; footnotes paired through `footnotes`. **Gap:** Hyperlinks are matched by rectangle overlap today, which mis-assigns split or adjacent links; the Link element defines the span. Intra-document targets need a representation, since `hyperlink` is a URL or path. FENote with `Ref` replaces the geometric footnote heuristic. Link `Contents` and Alt have no slot on text items.
### 4.8 Annotations
**Clauses:** 8.9.2.3 (markup annotations inside Annot elements; `Contents` and `RC` equivalence), 8.9.2.4 (per-type rules), 8.9.3 (placement near annotated content, `Ref` association), 8.14 (embedded files with `Desc`).
**Target:** `add_comment` with `targets` for text markup, sticky notes, free text, ink, and stamps; `comment_section` group; picture placeholders with Alt for screen, sound, and movie annotations; file attachments with their `Desc`. **Gap:** Nothing in the pipeline consumes `get_annotations`. Redaction annotations that have been applied are artifacts and should be skipped. Placement follows structure position, not rectangle.
### 4.9 Metadata and navigation
**Clauses:** 8.11 (dc:title required, `DisplayDocTitle` true), 8.12.2 (outlines with actions), 8.12.3 (page labels must match perceived numbering), 8.7 (optional content), 8.13 (actions), 8.14 (embedded and associated files).
**Target:** `DoclingDocument.name` from dc:title; page labels on `PageItem`; outline entries resolved to structure destinations and linked to the headings they point at. **Gap:** `PageItem` has no label field. Optional content, ECMAScript, and article threads stay out of scope.
## 5. docling-core additions, consolidated
The table below is the complete scope of the first docling-core change proposed here. Anything else found while building the `TaggedStructureModel` comes as a separate proposal. All additions are optional and defaulted, so existing documents load unchanged. Structure order needs no field: it is carried by body order.
**A. Fields on existing types**
| Field | Type | Default | Consumer |
|---|---|---|---|
| `DocItem.source`: two new kinds in the existing discriminated union beside `track` | `PdfStructureSource(kind="pdf_struct", element_id, element_type, page_no, mcids)`; `PdfWidgetSource(kind="pdf_widget", page_no, widget_index, field_name, struct_elem_id)` from #4143 | empty list, unchanged | writers, audit and review tooling, the parity test |
| `DoclingDocument.conformance` | `PdfConformanceRecord(pdfua_part, wtpdf_declarations, structure_path_used)` | `None` | consumers deciding how far to trust the tree; gap reports |
| `DocItem.alt`: alternate description on any item, not only pictures | `str \| None`; pictures keep `PictureMeta.description`, with provenance `authored` when it comes from `Alt` | `None` | HTML `alt` and `aria-label`; PDF `Alt`; link `Contents` |
| `TextItem.text_provenance`: attribution of `ActualText` substitution | `Literal["glyphs", "actual_text", "ocr"] \| None`; `orig` and `text` unchanged | `None` | text-quality signals; writers choosing between `orig` and `text` |
| `TextItem.references`: intra-document targets | `list[RefItem]`; `hyperlink` stays URL or path only | empty | footnote and TOC pairing; PDF `Ref`; HTML `href="#id"` |
| `ListGroup.numbering` | `Literal["none", "unordered", "description", "disc", "circle", "square", "decimal", "upper_roman", "lower_roman", "upper_alpha", "lower_alpha"] \| None` | `None` | HTML `type`; PDF `/ListNumbering`; Markdown marker choice |
| `TableCell.headers`: explicit header association for irregular tables | `list[CellRef]` by row and column offset; `column_header` and `row_header` unchanged | empty | HTML `headers`; PDF `/Headers` |
| `TableCell.scope` | `Literal["row", "column", "both"] \| None` | `None` | HTML `scope`; PDF `/Scope` |
| `PageItem.label` | `str \| None` | `None` | page labels in serializers; PDF `/PageLabels` |
| `DocItem.artifact_type` on furniture and background items | `Literal["pagination", "layout", "page", "background"] \| None` | `None` | round trip of `Artifact` typing; content-layer mapping |
| `DocItem.role`: authored ARIA role | `str \| None` | `None` | HTML `role`; PDF ARIA-1.1 attribute |
**B. New labels**
| Label | Kind | Serializes as |
|---|---|---|
| `DocItemLabel.BLOCK_QUOTE` | text item | `blockquote` |
| `GroupLabel.ASIDE` | group | `aside` |
| `Formatting.quote` | inline formatting flag beside bold, italic, underline | `q` |
**C. Already present, needs populating only**
`BaseMeta.language` (`LanguageMetaField`, from `/Lang` on catalog, element, and marked content); `TextItem.orig` and `text`; `PictureMeta.description` with its provenance string; `captions`, `footnotes`, `references` on tables and pictures; `ContentLayer` values `furniture`, `background`, `invisible`, `notes`; `SectionHeaderItem.level`.
**D. Serializer behaviour that follows, no decision needed**
HTML emits `lang`, `alt`, `headers` and `scope` on cells, `blockquote`, `aside`, `q`, footnote references, and `role` where one was authored (8.2.6.4). Markdown uses `numbering` to choose list markers. The set is chosen so that a downstream writer can round-trip the tree in either the PDF 1.7 or the PDF 2.0 vocabulary; Docling itself does not write PDF.
## 6. Testing and oracle
Same approach as forms: tests named by clause, an HTML twin for each conformant fixture, and a parity assertion on the resulting tree. The veraPDF corpus already contains clause-targeted pass and fail files and should be mined before authoring new ones.
The fixture mix mirrors what section 1.1 found in the wild, in this order of weight:
1. **UA-1 files from Word, Acrobat, and InDesign.** The majority case. Validated with the veraPDF UA-1 profile; a PAC report is stored beside the file where one can be produced, because PAC is what auditors run.
2. **Legacy tagged, non-conformant files.** Acrobat autotag output and print-driver PDFs with `MarkInfo` set but broken trees (`H` everywhere, Figures without Alt, tables without `TH`). No validation claim; the test is that the element degrades to the model path and the page still converts.
3. **UA-2 and WTPDF files.** LaTeX output (TeX Live 2026) and a pikepdf-built fixture that veraPDF confirms as PDF/UA-2 conformant. Validated with the veraPDF UA-2 and WTPDF profiles, reports stored beside them. These exercise namespaces, `Title`, `FENote`, `ListNumbering`, and MathML associated files.
4. **HTML twins** for the conformant fixtures in 1 and 3.
A small hand-built set is enough per area: one conformant file per generation, one file that fails the specific clause, and the HTML twin.
## 7. Phases
1. **Structure reader in docling-parse.** Tree, attributes, marked-content mapping, artifact sequences. Everything else waits on this. Draft: docling-project/docling-parse#337.
2. **docling-core additions** from section 5, all additive.
3. **Tagged-structure stage in docling** behind `tagged_structure`, producing pre-labeled clusters and structure reading order. Headings, paragraphs, lists, artifacts, and captions land here because they need no new model behavior, only deference.
4. **Tables and figures.** Skip TableFormer for tagged tables; authored Alt and ActualText win over generated descriptions; Caption placement.
5. **Links, references, footnotes, annotations.** Exact link spans, FENote pairing, comments from markup annotations.
6. **Language, text quality, metadata.** `Lang` at every level, ActualText attribution, PUA and `.notdef` signals into confidence scores, dc:title and page labels.
7. **Docs and skills.** Advanced options page, packaged usage skill, an example on tagged PDFs, and a statement of what "tagged PDF support" means in Docling.
## 8. Open questions
- Whether `prefer` mode should mix tagged and predicted items on one page, or fall back to models for the whole page when tagging is partial.
- How to represent intra-document references and structure destinations without overloading `hyperlink`.
- Whether authored Alt should suppress picture-description models or be stored alongside their output with distinct provenance.
- Whether table-structure skipping for tagged tables needs a validation step, since tagged tables are sometimes wrong in ways veraPDF cannot detect.
- Ruby and Warichu representation.
## 9. Out of scope
XFA, ECMAScript behavior, optional-content group switching, article threads, font embedding and rendering conformance (8.4.5 beyond Unicode mapping), and validation of PDF/UA-2 conformance itself, which stays with veraPDF.
Contributor guide
Research direction
Start by reading docling-parse#337, then review the forms-stage work referenced in #4140, #4144, and #4143. Trace the proposed TaggedStructureModel, PdfDocument.structure, SegmentedPdfPage.marked_content, and PdfStructureSource targets. Done would require an agreed, tested structure-ingestion design covering the listed areas and fallback behavior, not just one isolated edit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100