docling-project / docling-project/docling

Proposal: AcroForm form fields aligned with PDF/UA-2 (follow-on to #4140)

Open
#4,143 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
66.4k
Forks
4.8k
Avg merge
2d 21h
Merged PRs (30d)
84

Description

Follow-on proposal to #4140 (which supersedes #4055), written up so the docling-parse, docling-core, and docling pieces can be discussed and sequenced in one place. Feedback welcome on the open questions in section 8 in particular.

**Status:** draft proposal. **Builds on:** docling #4140, docling-parse #334, docling-core #733. **Author:** Jeff Witt. **Date:** 2026-09-02.

## 1. Goal

Bring docling's PDF form extraction to parity with the form semantics defined by Well-Tagged PDF 1.0 (WTPDF), the tagged-PDF profile that ISO 14289-2 (PDF/UA-2) is built on. The target is a testable statement:

> For a PDF that conforms to WTPDF, the `FieldRegionItem` / `FieldItem` tree in the `DoclingDocument` is isomorphic to the Form and Lbl elements of the structure tree. For a PDF that does not, docling degrades to the current geometry path and records that it did so.

A second parity target is internal: the tree docling produces for a tagged PDF form should have the same shape the HTML backend already produces for the equivalent HTML form.

## 2. Standards basis

All clause numbers refer to WTPDF 1.0. PDF/UA-2 adopts these clauses; the veraPDF PDF/UA-2 profile encodes the machine-checkable ones with the same numbering.

| Clause | Requirement | Consequence for docling |
|---|---|---|
| 8.10.1 | Each widget annotation is enclosed by a Form structure element; a Form element encloses at most one widget. A Form element does not equate to a form field. | One `FieldItem` per widget. A field with several widgets (radio group, multi-widget checkbox) is a group of `FieldItem`s. |
| 8.10.2.1 | Context comes from surrounding content, grouping, the field's label, the field's `TU`, the widget's label, and the widget's `Contents`. The field's name (`T`) does not contribute. | Label precedence is fixed by the spec. `T` never appears as text. |
| 8.10.2.2 | A widget's label is real content in `Lbl` elements that are direct descendants of its Form element. A group label is `Lbl` inside the parent that contains every Form element of the group. | Widget `Lbl` becomes `FieldKeyItem`. Group `Lbl` becomes `FieldHeadingItem`. |
| 8.10.2.3 | If the label is absent or insufficient, `Contents` supplies description and context and should not duplicate the label. With an `AA` entry, `Contents` describes the action. | `Contents` becomes `FieldHint` when it is not consumed as the name. |
| 8.10.2.4 | `TU` complements the label. With several widgets per field, `TU` alone is insufficient. | `TU` is the key fallback when no `Lbl` exists, and a hint otherwise. |
| 8.10.3.2 | For button fields, `Contents` reflects the `CA` caption in the appearance characteristics dictionary. | Push buttons are emitted with role `button` and `CA` as text, never as empty values. |
| 8.10.3.2.3 | Checkbox and radio fields contain several widgets. Export values are not descriptive. | Value text is the state. Export values live in `orig` and in the source record only. |
| 8.10.3.4 | `Opt` holds both export value and display text; the display text conveys intent. | Choice values use display text. |
| 8.10.3.5 | Signature widgets are real content when location carries legal meaning, otherwise artifacts. | Signature widgets are emitted with role `signature`; zero-size ones are skipped. |
| 8.10.4 | Non-interactive form content is enclosed in a Form element with PrintField attributes `Role`, `Checked`, `Desc`. Labels follow the same `Lbl` rules. | Second ingestion path producing `kind="read_only"` values. |
| 8.9.2.4.13 | A widget of zero height and width is an artifact. | Skipped. |
| 8.9.3.3 | `Tabs` must be A, W, or S, but an annotation's logical position is defined by the structure tree regardless. | Ordering follows structure order, not `Tabs`. |
| 8.2.6.4 | The ARIA-1.1 attribute owner may carry ARIA roles on any structure element. | The role vocabulary in docling-core uses ARIA names. |

## 3. Model

The spec separates three things that #4055 and #4140 each collapsed differently: the widget, the field, and the label.

| Level | PDF construct | docling-core item | Notes |
|---|---|---|---|
| Region | The structure element grouping one or more Form elements, or a layout `FORM` cluster | `FieldRegionItem` | Unchanged from #4140. |
| Group label | `Lbl` in the grouping parent | `FieldHeadingItem` | New in this proposal. Only emitted when a group label exists. |
| Field item | One Form structure element, one widget | `FieldItem` | One per widget, per 8.10.1. |
| Key | Widget `Lbl`, else `TU`, else `Contents`, else heuristic | `FieldKeyItem` | Carries `name_from`. |
| Value | `V` / `AS` / `Opt` display text / `CA`, or the real content of a non-interactive Form element | `FieldValueItem` | `kind="fillable"` for widgets, `read_only` for non-interactive. |
| Hint | `Contents` or `TU` not consumed as key; PrintField `Desc` | `FieldHint` text item | Existing label `FIELD_HINT`. |
| Marker | Required indicator in page text | `MARKER` text item | Same as the HTML backend. |

### 3.1 Name computation

Modeled on the W3C accessible-name algorithm: an explicit precedence order, and the winning source is recorded rather than hidden.

1. `structure`: `Lbl` child of the widget's own Form element.
2. `tooltip`: the field's `TU`.
3. `contents`: the widget's `Contents`.
4. `heuristic`: nearest text cluster to the left or above the widget rectangle within a margin, colon stripped. Off by default.
5. None: the field item is keyless, as in #4140 today.

A group label (`Lbl` in the parent) never becomes a widget's key. It becomes the region's `FieldHeadingItem`.

### 3.2 Description

The hint is the first of `Contents`, then `TU`, that was not consumed as the name and is not textually identical to it. For non-interactive fields the hint is PrintField `Desc`.

### 3.3 Role and state vocabulary

Roles use ARIA names so the schema stays format neutral. Both PDF and HTML map onto them one to one.

| Role | Interactive source | Non-interactive source (PrintField `Role`) | HTML |
|---|---|---|---|
| `textbox` | `/FT /Tx` | `tv` | `input type=text`, `textarea` |
| `checkbox` | `/FT /Btn`, no radio or pushbutton flag | `cb` | `input type=checkbox` |
| `radio` | `/FT /Btn`, radio flag (bit 16) | `rb` | `input type=radio` |
| `combobox` | `/FT /Ch`, combo flag (bit 18) | none | `select` |
| `listbox` | `/FT /Ch`, no combo flag | `lb` | `select multiple` |
| `button` | `/FT /Btn`, pushbutton flag (bit 17) | `pb` | `button` |
| `signature` | `/FT /Sig` | none | none |

State: `checked` with values `on`, `off`, `mixed` (PrintField `neutral`). Flags: `required` (bit 2), `read_only` (bit 1). Options: list of `(export_value, display_text)` for choice fields.

## 4. Ingestion paths

**A. Interactive, tagged.** Role, state, value, and options come from the field dictionary. Label comes from the structure tree. This is the PDF/UA-2 path and the one the parity test targets.

**B. Non-interactive, tagged.** A Form structure element without a widget. Role and state from PrintField attributes, hint from `Desc`, value from the real content inside the Form element, key from `Lbl`. Produces `kind="read_only"`.

**C. Untagged.** Today's #4140 path: widgets bound to layout `FORM` clusters by coverage, keyless unless `TU` or `Contents` exists, heuristic binding only when enabled. Every key carries `name_from`.

The three paths converge on the same tree. Nothing PDF specific enters `DoclingDocument` text; PDF identity lives in a typed `source` entry.

**Tolerance.** Paths A and B assume conformance only for the parity test. Real tagged forms (Acrobat autotag, Word export, most UA-1 files in the wild) routinely violate 8.10.1 and 8.10.2.2: one Form element wrapping several widgets, widgets with no Form element at all, `Lbl` as a sibling rather than a child. Each violation falls back per widget to path C's name computation, with `name_from` recording what happened. Fallback is never per document. The path is selected by `MarkInfo` `Marked` and the presence of Form elements, not by `pdfuaid:part`, because UA-1 files outnumber UA-2 files by a wide margin (see #4146 section 1.1 for the adoption picture).

## 5. Changes by repository

### 5.1 docling-parse

Per widget, in addition to what #334 exposes:

- `/MK /CA` caption.
- `/Opt` as a list of `(export, display)` pairs, and array `/V` for multi-select fields.
- `/Contents`.
- Structure linkage: for each widget, the label text of `Lbl` children of its Form element, the group label text of the nearest ancestor `Lbl`, the structure order index, PrintField attributes when present, and the ARIA-1.1 role attribute when present.

Non-interactive: a per-page list of Form structure elements without widgets, each with PrintField attributes, `Lbl` text, content bounding box, and structure order index.

This requires a structure-tree walker in docling-parse: `StructTreeRoot` to `Form` elements, `OBJR` to the annotation, `MCID` marked content to text. It is the largest single item and the only route to true PDF/UA-2 equivalence.

### 5.2 docling-core

Additive schema changes, no migration:

- `FieldKeyItem.name_from: Literal["structure", "tooltip", "contents", "heuristic"]`.
- `FieldItem.meta: FieldMeta | None` with `role`, `checked`, `required`, `read_only`, `options`, following the `PictureMeta` pattern.
- `PdfWidgetSource(kind="pdf_widget", page_no, widget_index, field_name, struct_elem_id)` added to the `SourceType` union. This is where `T` and export values go.
- HTML serializer: `FieldRegionItem` to `fieldset`, `FieldHeadingItem` to `legend`, `FieldItem` to `label` plus a control chosen by role, with `for` and `id` pairing, `aria-required`, `checked`, `readonly`, and `aria-describedby` pointing at the hint. Markdown serializer: `Label: value` lines with `[x]` and `[ ]` for checkable roles. DocLang already has tokens for every part.

### 5.3 docling

- `PdfFormFieldModel` applies the name precedence, sets `name_from`, skips zero-size widgets, emits push buttons with `CA`, and groups widgets that share a fully qualified field name under one region with the group label as heading.
- Reading order sorts field items by structure order index when present, then native widget order, then bbox.
- Page assembly unchanged.
- Options: `extract_form_fields` unchanged. New `form_label_heuristics: bool = False` gating step 4 of name computation.
- The non-interactive path is a second producer of `FieldRegionPrediction` from the structure list, sharing the assembler and reading order code.

## 6. Testing and oracle

- Fixtures in pairs. `acroform_sample.pdf` stays as the untagged fixture. A new tagged fixture validated with veraPDF against the PDF/UA-2 profile holds a text field with `Lbl`, a radio group with a group `Lbl`, a checkbox with `Contents` only, a push button with `CA`, a zero-size widget, and a choice field with `Opt`. A third fixture is a non-interactive PrintField form. A fourth is the HTML form equivalent of the tagged fixture.
- A UA-1 tagged form produced by Acrobat or Word, the majority case in the wild, sits beside the UA-2 fixture and is validated with the veraPDF UA-1 profile. A non-conformant tagged form (one Form element over a radio group, a widget outside any Form element) exercises the tolerance rules in section 4.
- Test names carry clause ids, for example `test_8_10_2_2_widget_label_from_lbl`, so the suite is tied to the standard rather than to one implementation.
- Parity test: the tagged PDF fixture and the HTML fixture produce the same tree shape and the same key and value texts.
- veraPDF validation reports are stored next to the fixtures. Running veraPDF in CI is optional because it needs a JVM.

## 7. Phases

1. **In #4140 review.** Push-button and zero-size handling, `Opt` display text, checkbox representation consistent with the HTML backend.
2. **docling, stacked on #4140.** `TU` as key with `name_from` recorded on the prediction, artifact skipping, tests. Ships before the schema change by keeping `name_from` on the page prediction only.
3. **docling-core.** `name_from`, `FieldMeta`, `PdfWidgetSource`, HTML and Markdown serializers.
4. **docling-parse.** `CA`, `Opt`, `Contents`, array `V`; then the structure-tree walker.
5. **docling.** Structure path, non-interactive path, structure ordering, heuristic binding behind the option, parity test.
6. **Docs.** `docs/usage/advanced_options.md`, the packaged usage skill, and an example.

## 8. Open questions

- Whether `name_from` belongs on `FieldKeyItem` or on `FieldItem`.
- Whether a multi-widget field is a nested group inside a region or a region of its own.
- Checkbox representation: #4140 emits a `FieldValueItem` with text `checked`; the HTML backend emits `CHECKBOX_SELECTED` items. One of the two should change.
- Whether calculation-only widgets can be detected without JavaScript inspection. Probably not; they are skipped only when zero-size.

## 9. Out of scope

XFA (prohibited by the spec), JavaScript actions, field detection on scanned forms, the VLM pipeline, and writing values back into forms.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing #4140, docling-parse #334, and docling-core #733, then trace PdfFormFieldModel and FieldRegionPrediction in the docling repository. Inspect the named AcroForm fixtures and clause-specific tests; done means the staged tagged, non-interactive, and fallback paths produce the specified common tree and pass the parity and tolerance cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
html, python
Domain
backend, data
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.