Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
6e5463b
feat: integrate native AcroForm widgets as typed fields
cau-git Sep 2, 2026
77dd6f9
docs: add AcroForm widget/layout-text collision handoff
cau-git Sep 3, 2026
d4802e5
feat: nest AcroForm checkbox state as inline field-value child
cau-git Sep 3, 2026
28dd6c7
feat: match AcroForm checkbox widget to layout cluster and lift its l…
cau-git Sep 3, 2026
06790b2
feat(acroform): lift matched checkbox cluster text verbatim and use i…
cau-git Sep 3, 2026
ac110b9
feat(acroform): group widgets inlined in a paragraph into one keyed f…
cau-git Sep 3, 2026
279174c
fix(acroform): materialize inlined field_item in place, not a new fie…
cau-git Sep 3, 2026
505a96b
feat(acroform): skip zero-size widgets and push buttons
cau-git Sep 3, 2026
2ee25cc
feat(acroform): add order-preserving widget->label binding core
cau-git Sep 4, 2026
fae17b7
feat(acroform): bind detached labels to keyless form/unmatched widgets
cau-git Sep 4, 2026
b503231
fix(acroform): make label binding actually fire, and drop the left-or…
cau-git Sep 4, 2026
d4ba8a0
docs(acroform): record Phase-1 validation outcome and row-only guard
cau-git Sep 4, 2026
e363ae4
docs(acroform): consolidate Phase-1 impl surface + fixture observations
cau-git Sep 4, 2026
1b27f92
docs(acroform): add cold-start section for the next agent
cau-git Sep 4, 2026
1cdbcc7
docs(acroform): record constant sweep — 2.0/1.5 validated
cau-git Sep 4, 2026
f6cdc8c
feat(acroform): forbid label binding across a table boundary (Phase 4)
cau-git Sep 4, 2026
db0564f
Merge branch 'main' of github.com:DS4SD/docling into feat/acroform-la…
cau-git Sep 8, 2026
306945d
test(acroform): snapshot visual references and replay evidence
cau-git Sep 10, 2026
2cf40e5
feat(acroform): snapshot offline optimizer and widget coverage
cau-git Sep 10, 2026
8836277
experiment(acroform): snapshot rejected conditional grouping
cau-git Sep 10, 2026
f1efc63
experiment(acroform): snapshot caption alternatives and nested groups
cau-git Sep 10, 2026
c990ae5
Merge branch 'main' of github.com:DS4SD/docling into codex/acroform-k…
cau-git Sep 16, 2026
73588cd
chore: remove output/ dir from git tree
cau-git Sep 16, 2026
36d87a3
Update gitignore
cau-git Sep 16, 2026
58ce1bf
erge branch 'main' of github.com:DS4SD/docling into feat/acroform-nat…
cau-git Sep 16, 2026
e86c2b6
Merge remote-tracking branch 'origin/feat/acroform-native-fields' int…
cau-git Sep 16, 2026
a068798
feat(acroform): key form fields with a geometric solver in the form_f…
DanieldeAlcala Sep 24, 2026
b7dd464
refactor(acroform): key from live page objects and remove experiment …
cau-git Oct 1, 2026
a8981e6
Merge remote-tracking branch 'origin/main' into pr-4364
cau-git Oct 1, 2026
8363408
Remove widget-offset correction from docling branch
cau-git Oct 2, 2026
5819734
test(cli): disable rich styling in form-fields CLI test
cau-git Oct 2, 2026
b066380
DCO Remediation Commit for github-actions[bot] <github-actions[bot]@u…
cau-git Oct 2, 2026
a4be685
DCO Remediation Commit for Christoph Auer <cau@zurich.ibm.com>
cau-git Oct 2, 2026
18b3f49
fix(acroform): drop key captions from partly used text clusters
DanieldeAlcala Oct 2, 2026
9355e52
refactor(acroform): retain painted source cells for cleanup
cau-git Oct 7, 2026
c4d1681
Merge origin/main into pr-4364
cau-git Oct 7, 2026
4b4e1fe
refactor(acroform): use field key and hint terminology
cau-git Oct 8, 2026
34ac611
fix(acroform): address the review of the form-field extraction
DanieldeAlcala Oct 13, 2026
e67854b
feat(acroform): place fields among the text printed around them
DanieldeAlcala Oct 13, 2026
1a3527e
fix(acroform): keep heading and formula keys as plain field keys
cau-git Oct 13, 2026
73b1b49
fix(acroform): place fields outside a form block without a placeholde…
cau-git Oct 13, 2026
fc5190f
test(acroform): check penalized_pairs against scoring every pair
cau-git Oct 13, 2026
4bf6b5f
docs: add --extract-form-fields to the CLI reference
cau-git Oct 13, 2026
157f5fb
fix(layout): keep the drawn box of checkbox clusters
cau-git Oct 14, 2026
025a5ea
fix(acroform): key a checkbox by the checkbox block around it
cau-git Oct 14, 2026
d8af343
fix(acroform): let a checkbox block compete in the key search
DanieldeAlcala Oct 14, 2026
84841fd
fix(acroform): keep a checkbox block's key out of other groups
cau-git Oct 16, 2026
08dfd92
fix(acroform): keep the hints of checkboxes keyed by their block
cau-git Oct 16, 2026
9e9ef32
Merge remote-tracking branch 'origin/main' into pr-4364
cau-git Oct 19, 2026
3875d9a
chore: allow the reading-order model and form tests over the line limit
cau-git Oct 19, 2026
892638f
refactor(form): hold form field predictions like the other stages
cau-git Oct 19, 2026
b8de2c1
fix(cli): refuse form fields with DocTags before reading inputs
cau-git Oct 19, 2026
9d26297
test(form): put each form test where its code is owned
cau-git Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/max-lines-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ docling/backend/opendocument_backend.py
docling/datamodel/pipeline_options.py
docling/datamodel/service/options.py
docling/datamodel/stage_model_specs.py
docling/models/stages/reading_order/readingorder_model.py
docling/pipeline/standard_pdf_pipeline.py
docling/service_client/_async_client.py
docling/service_client/client.py
Expand All @@ -29,4 +30,5 @@ tests/test_service_client_sdk_unit.py
tests/test_backend_opendocument.py
tests/test_backend_html.py
tests/test_backend_msword.py
tests/test_cli.py
tests/test_cli.py
tests/test_form_extraction.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
model_artifacts/
scratch/
ds_convert_models/
output/

# Created by https://www.toptal.com/developers/gitignore/api/python,macos,virtualenv,pycharm,visualstudiocode,emacs,vim,jupyternotebooks
# Edit at https://www.toptal.com/developers/gitignore?templates=python,macos,virtualenv,pycharm,visualstudiocode,emacs,vim,jupyternotebooks
Expand Down
1 change: 1 addition & 0 deletions docling/.agents/skills/docling/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ docling report.pdf --no-tables --output /tmp/ # skip table structure
docling report.pdf --table-mode accurate --output /tmp/ # vs. fast
docling report.pdf --layout-engine docling_layout_default --output /tmp/ # choose layout engine
docling report.pdf --table-structure-engine docling_tableformer_v2 --output /tmp/ # choose table engine
docling form.pdf --extract-form-fields --output /tmp/ # AcroForm widgets as fields with keys from printed text (not with --to doctags)
docling report.pdf --enrich-code --output /tmp/ # code understanding
docling report.pdf --enrich-formula --output /tmp/ # formula understanding
docling report.pdf --enrich-picture-classes --output /tmp/
Expand Down
15 changes: 15 additions & 0 deletions docling/.agents/skills/docling/references/python-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Useful `PdfPipelineOptions` / base fields:
|---|---|
| `do_ocr` | Run OCR (default engine EasyOCR) |
| `do_table_structure` | Detect table structure |
| `extract_form_fields` | Convert docling-parse PDF widgets (AcroForm) into fillable fields with keys from printed text |
| `do_code_enrichment` / `do_formula_enrichment` | Enrich code / formulas |
| `code_formula_options` | Code/formula model, crop margin (`expansion_factor`), and stopping repeated output (`stop_on_repetition`, on by default) |
| `ocr_options` | Choose/parametrize the OCR engine (see below) |
Expand All @@ -72,6 +73,20 @@ Useful `PdfPipelineOptions` / base fields:
| `artifacts_path` | Use pre-downloaded model artifacts (offline) |
| `enable_remote_services` | Gate all outbound HTTP (required for any remote model) |

`extract_form_fields=True` associates each widget with a field key from printed
text, using page geometry. The key can be text in or beside its cell, or a
paragraph that contains the widget. Checkbox and radio option text becomes an
option key; a shared prompt stays as ordinary text. Additional context becomes
a field hint. A section header or formula used as a key stays inside the key.
Fields keep their widget order and sit among the surrounding text by page
geometry. A widget in a detected table goes into its table cell, with a key
only from text in that same cell. PDF field
names are not used as keys, and PDF-specific widget metadata is not stored in
the `DoclingDocument`. Use `generate_parsed_pages=True` to retain raw widgets
on parsed pages when needed. Backends without page-local widgets produce no
fields; table cells need `do_table_structure=True`. Not available in the
service client, and not supported by the DocTags export.

### Choosing an OCR engine

```python
Expand Down
27 changes: 26 additions & 1 deletion docling/backend/docling_parse_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union

from docling_core.types.doc import BoundingBox, Size
from docling_core.types.doc import BoundingBox, CoordOrigin, Size
from docling_core.types.doc.page import (
PdfCellRenderingMode,
PdfTextCell,
Expand Down Expand Up @@ -243,6 +243,31 @@ def get_connected_shape_bounding_boxes(
)
]

def get_thin_shape_boxes(
self, *, max_thickness: float
) -> Optional[list[BoundingBox]]:
if not self.is_valid():
return []

page_height = self.get_size().height
boxes = []
# docling-parse exposes the individual painted paths only through its
# page decoder; get_connected_shape_bounding_boxes merges them first.
for shape in self._result._require_page_decoder().get_page_shapes():
Comment thread
cau-git marked this conversation as resolved.
xs, ys = list(shape.get_x()), list(shape.get_y())
if not xs or not ys:
continue
box = BoundingBox(
l=min(xs),
b=min(ys),
r=max(xs),
t=max(ys),
coord_origin=CoordOrigin.BOTTOMLEFT,
)
if min(box.width, box.height) <= max_thickness:
boxes.append(box.to_top_left_origin(page_height))
return boxes

def get_page_image(
self, scale: float = 1, cropbox: Optional[BoundingBox] = None
) -> Image.Image:
Expand Down
11 changes: 11 additions & 0 deletions docling/backend/pdf_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ def get_connected_shape_bounding_boxes(
"""
return None

def get_thin_shape_boxes(
self, *, max_thickness: float
) -> Optional[list[BoundingBox]]:
"""Return the boxes of painted shapes no thicker than `max_thickness`.

Rules drawn as thin filled rectangles, which `get_shape_lines` (strokes
only) misses. Boxes use top-left origin. `None` means this backend cannot
answer the query at all.
"""
return None

@abstractmethod
def get_page_image(
self, scale: float = 1, cropbox: Optional[BoundingBox] = None
Expand Down
27 changes: 24 additions & 3 deletions docling/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,17 @@ def convert( # noqa: C901
help="If enabled, the table structure model will be used to extract table information.",
),
] = True,
extract_form_fields: Annotated[
bool,
typer.Option(
...,
help=(
"If enabled, native PDF form widgets (AcroForm) become fillable field "
"values with field keys from printed text. Standard and legacy pipelines "
"with the docling-parse backend only."
),
),
] = False,
reading_order_separators: Annotated[
bool,
typer.Option(
Expand Down Expand Up @@ -1409,6 +1420,18 @@ def _resolve_pdf_backend() -> tuple[type[PdfDocumentBackend], PdfBackendOptions]
)
raise typer.Abort()

if to_formats is None:
to_formats = [OutputFormat.MARKDOWN]
if (
extract_form_fields
and pipeline in {ProcessingPipeline.STANDARD, ProcessingPipeline.LEGACY}
and OutputFormat.DOCTAGS in to_formats
):
# DocTags has no tokens for form fields; the export raises on them.
raise typer.BadParameter(
"--extract-form-fields cannot be combined with `--to doctags`."
)

if profiling or save_profiling:
settings.debug.profile_pipeline_timings = True

Expand Down Expand Up @@ -1474,9 +1497,6 @@ def _resolve_pdf_backend() -> tuple[type[PdfDocumentBackend], PdfBackendOptions]
_log.info(err) # will print more details if verbose is activated
raise typer.Abort()

if to_formats is None:
to_formats = [OutputFormat.MARKDOWN]

if output_file is not None:
if len(input_doc_paths) != 1:
err_console.print(
Expand Down Expand Up @@ -1565,6 +1585,7 @@ def _resolve_pdf_backend() -> tuple[type[PdfDocumentBackend], PdfBackendOptions]
do_ocr=ocr,
ocr_options=ocr_options,
do_table_structure=tables,
extract_form_fields=extract_form_fields,
use_reading_order_separators=reading_order_separators,
layout_options=layout_options,
table_structure_options=table_structure_options,
Expand Down
64 changes: 62 additions & 2 deletions docling/datamodel/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any, Optional, Type, Union
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Type, Union

import numpy as np
from docling_core.types.doc import (
Expand Down Expand Up @@ -429,18 +429,69 @@ class ApiImageStreamingRequestResult:
stop_reason: VlmStopReason = VlmStopReason.UNSPECIFIED


class FieldValuePrediction(BaseModel):
text: str
orig: str
bbox: BoundingBox
# Selection state for a checkbox/radio widget; None for text-like fields.
# When set, the value is materialized as an empty field value that nests a
# CHECKBOX_SELECTED/UNSELECTED child (state lives on the child, not the text).
checkbox: (
Literal[DocItemLabel.CHECKBOX_SELECTED, DocItemLabel.CHECKBOX_UNSELECTED] | None
) = None


class FieldItemPrediction(BaseModel):
# A keyed item groups one or more values under one field key chosen from
# printed text beside a widget, in its table cell, or in a paragraph that
# contains it. key_bbox is the provenance of the key. Keyless items carry
# a single value. hint_text is extra context emitted as a field hint:
# outside detected tables, an aligned header along the other axis of a
# grid of like values. Items in a table cell never carry a hint.
key_text: str = ""
key_bbox: BoundingBox | None = None
values: list[FieldValuePrediction] = []
hint_text: str = ""


class FormFieldPrediction(BaseModel):
# One entry per placement route of the form field stage. Items in table
# cells are on Table.field_cells instead.
inline_items: dict[int, FieldItemPrediction] = {} # host paragraph id
form_items: dict[int, list[FieldItemPrediction]] = {} # FORM cluster id
loose_items: list[FieldItemPrediction] = [] # placed by geometry


class TableFieldPrediction(BaseModel):
# Field items that live inside one cell of a structured table. The table's
# own row and column headers carry the association, so the items only hold
# a key printed in that same cell. Offsets follow TableCell's convention.
start_row_offset_idx: int
end_row_offset_idx: int
start_col_offset_idx: int
end_col_offset_idx: int
items: list[FieldItemPrediction] = []


class ContainerElement(
BasePageElement
): # Used for Form and Key-Value-Regions, only for typing.
pass


class FieldRegionElement(ContainerElement):
items: list[FieldItemPrediction] = []


class Table(BasePageElement):
otsl_seq: list[str]
num_rows: int = 0
num_cols: int = 0
orientation: Orientation = Orientation.ROT_0
table_cells: list[TableCell]
# AcroForm field items placed in this table's cells (see TableFieldPrediction);
# written by the form field stage, empty unless form fields are extracted.
field_cells: list[TableFieldPrediction] = []


class TableStructurePrediction(BaseModel):
Expand All @@ -450,6 +501,12 @@ class TableStructurePrediction(BaseModel):
class TextElement(BasePageElement):
text: str
hyperlink: Optional[Union[AnyUrl, Path]] = None
# Set when this paragraph inlines AcroForm widgets (e.g. a sentence with an
# inline checkbox and a fillable amount). The paragraph then materializes as
# a field_item -- its text becomes the key, the widgets its values -- in the
# paragraph's own place in the reading order (its list, its container), rather
# than being pulled out into a separate field_region.
field_item: Optional["FieldItemPrediction"] = None


class FigureElement(BasePageElement):
Expand Down Expand Up @@ -481,13 +538,16 @@ class EquationPrediction(BaseModel):

class PagePredictions(BaseModel):
layout: LayoutPrediction | None = None
form_fields: FormFieldPrediction | None = None
tablestructure: TableStructurePrediction | None = None
figures_classification: FigureClassificationPrediction | None = None
equations_prediction: EquationPrediction | None = None
vlm_response: VlmPrediction | None = None


PageElement = Union[TextElement, Table, FigureElement, ContainerElement]
PageElement = Union[
TextElement, Table, FigureElement, ContainerElement, FieldRegionElement
]


class AssembledUnit(BaseModel):
Expand Down
15 changes: 15 additions & 0 deletions docling/datamodel/pipeline_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,21 @@ class PdfPipelineOptions(PaginatedPipelineOptions):
)
),
] = True
extract_form_fields: Annotated[
bool,
Field(
description=(
"Extract native interactive PDF widgets (AcroForm) as format-neutral fillable field values, "
"each with a field key from printed text associated by page geometry: text in or beside "
"its cell, or a whole paragraph that inlines the widget. A widget in a detected table goes "
"into its table cell, keyed only by text printed in that same cell; row and column "
"headers are not used as keys or hints. Requires the docling-parse backend, which supplies "
"page widgets; table cells need table structure. Raw widget metadata remains available only "
"on retained parsed pages. Scanned or flattened forms and backends without page widgets are "
"unaffected."
)
),
] = False
do_ocr: Annotated[
bool,
Field(
Expand Down
2 changes: 2 additions & 0 deletions docling/models/stages/form_field/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: The Docling Contributors
# SPDX-License-Identifier: MIT
Loading
Loading