Skip to content

Commit 02dd568

Browse files
committed
docs: update PDF conformance to 6/14 strict, 7/14 whitespace-normalized
Refresh all docs for the current PDF state and the pure-Rust text parser: - PDF_CONFORMANCE.md: 4/14 → 6/14 strict / 7/14 ws-normalized; per-PDF table refreshed (rtl_01, rtl_02 now exact; amt ws-ok; updated diff counts); text section rewritten around the pure-Rust parser (default, DOCLING_PDFIUM_TEXT to opt out) — Form XObject recursion, glyph-name fallback, overprint dedup, punctuation normalization, false-picture / page-number layout; blockers updated (RTL double-space resolved; amt font-metrics added). - COMPARING.md: PDF row 4/14 → 6/14 (+7/14 ws-normalized) and the PDF prose. - MIGRATION.md: 4/14 → 6/14, the PDF pipeline row + text bullet, snapshot 76→91. - README.md: PDF pipeline one-liner (text parser + pdfium raster + ONNX); snapshot baseline 76/91. - PDF_PARSER_NOTES.md: parser is the default (was opt-in DOCLING_RUST_PARSER). Docs-only; no code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYCj8stK1TzKcy6crtTfha
1 parent 177a6be commit 02dd568

5 files changed

Lines changed: 126 additions & 82 deletions

File tree

COMPARING.md

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,35 +177,44 @@ case — see the divergence table below.
177177
| **PPTX** | **7 / 7**| 7 / 7 |
178178
| **DOCX** | **25 / 26** | 25 / 26 |
179179
| **HTML** | **28 / 33** | 28 / 33 |
180-
| **PDF** | **4 / 14**| 5 / 14 |
180+
| **PDF** | **6 / 14**| 7 / 14 |
181181

182182
> † The pure-parse backends above are scored against **live** docling. **PDF** is a
183183
> discriminative ML reconstruction pipeline (not a deterministic parse), so it is
184184
> scored against a committed groundtruth corpus (`tests/data/pdf/groundtruth`) that
185185
> is **regenerated from live docling** and therefore matches `scripts/conformance.sh
186-
> pdf` (padded GitHub tables, current docling text). Within-one adds
187-
> `right_to_left_01` (a 2-line diff).
188-
189-
**PDF** (`*.pdf`) ports docling's *standard* (discriminative) PDF pipeline. pdfium
190-
extracts the text layer (glyph cells + bounding boxes) and renders each page to a
191-
bitmap; an ONNX model stack then interprets it — **layout detection** (the
192-
`heron`/RT-DETR region model), **TableFormer** table-structure recognition (a full
193-
port: image encoder + autoregressive OTSL structure decoder + cell-bbox decoder,
194-
exported to ONNX — see `tableformer.rs`, with cv2-exact `INTER_AREA`/`INTER_LINEAR`
186+
> pdf` (padded GitHub tables, current docling text). The PDF score is reported two
187+
> ways: **6 / 14 strict** byte-for-byte, and **7 / 14 whitespace-normalized** — the
188+
> 7th (`amt_handbook_sample`) differs only by docling's spurious double space in a
189+
> `1⁄4` fraction, where the Rust output's single space is the more faithful
190+
> rendering.
191+
192+
**PDF** (`*.pdf`) ports docling's *standard* (discriminative) PDF pipeline. A
193+
**pure-Rust text parser** (`textparse.rs`, on `lopdf`) reconstructs each glyph's
194+
box from the font's own advance widths and the PDF text/graphics matrices — the
195+
same information docling's `docling-parse` C++ parser uses, and a closer match
196+
than pdfium's *rendered* boxes. It is the default text layer (`DOCLING_PDFIUM_TEXT=1`
197+
falls back to pdfium; pages with no text layer fall back automatically). pdfium
198+
still renders each page to a bitmap and supplies word/code cells for tables. An
199+
ONNX model stack interprets the page — **layout detection** (the `heron`/RT-DETR
200+
region model), **TableFormer** table-structure recognition (a full port: image
201+
encoder + autoregressive OTSL structure decoder + cell-bbox decoder, exported to
202+
ONNX — see `tableformer.rs`, with cv2-exact `INTER_AREA`/`INTER_LINEAR`
195203
preprocessing in `resample.rs`), and **PaddleOCR** recognition for scanned /
196204
image-only pages — and regions are assembled in reading order into a
197-
`DoclingDocument`. Text reconstruction ports **docling-parse's line sanitizer**
198-
(`dp_lines.rs`, from `cells.h`): the 3-pass corner-distance contraction with
199-
`merge_with` space insertion, `enforce_same_font`, ligature recomposition, and
200-
loose-box geometry — fed by pdfium glyphs. This closed the text-run-boundary gap
201-
that previously capped conformance (inter-run spacing like `LABEL :`, justified
202-
double-spacing, lam-alef ordering). Byte-exact today: `picture_classification`,
203-
`code_and_formula`, `2305.03393v1-pg9` (**including its TableFormer-reconstructed
204-
table, cell for cell**), and `multi_page`. The rest are structurally correct but
205-
not yet byte-exact; the remaining gaps are model-level — TableFormer multi-row
206-
header/span structure on dense papers, layout classification (a TOC read as a
207-
picture, a survey read as tables), title-page reading order, and justified RTL
208-
double-spaces (`right_to_left_01`, within one line).
205+
`DoclingDocument`. The parser's cells feed the ported **docling-parse line
206+
sanitizer** (`dp_lines.rs`, from `cells.h`): the 3-pass corner-distance
207+
contraction with `merge_with` space insertion, `enforce_same_font`, ligature
208+
recomposition, and loose-box geometry. Together they closed the text-run-boundary
209+
gap that capped conformance (inter-run spacing like `LABEL :`, justified
210+
double-spacing, lam-alef ordering, the RTL sentence period, kashida over-emission).
211+
Byte-exact today: `picture_classification`, `code_and_formula`,
212+
`2305.03393v1-pg9` (**including its TableFormer-reconstructed table, cell for
213+
cell**), `multi_page`, `right_to_left_01`, and `right_to_left_02`. `amt` matches
214+
once whitespace is normalized. The rest are structurally correct but not yet
215+
byte-exact; the remaining gaps are model-level — TableFormer multi-row header/span
216+
structure on dense papers, layout classification (a TOC read as a picture, a
217+
survey read as tables), and title-page reading order.
209218

210219
**DOCX** (`*.docx`) is a core port of `MsWordDocumentBackend` (`roxmltree` over
211220
the `ooxml` helper): paragraphs, headings (by style, incl. Title), **numbered

MIGRATION.md

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ phased plan is kept at the end as history.)
1010
> (legacy + a Rust-only *strict* mode), docling-native **JSON** output, and
1111
> **image extraction**. The declarative formats are pure-Rust and checked
1212
> byte-for-byte against *live* docling; the PDF/image/METS ML path lives in
13-
> `fleischwolf-pdf` (pdfium + ONNX layout/TableFormer/OCR + a port of
14-
> docling-parse's line sanitizer) and is also measured byte-for-byte against live
15-
> docling — **4 / 14 PDF fixtures exact** (see `PDF_CONFORMANCE.md`), with a
16-
> snapshot baseline guarding against regressions. `cargo test` is green (unit
17-
> tests + a 131-source output-regression suite).
13+
> `fleischwolf-pdf` (a pure-Rust PDF text parser + pdfium rasterization + ONNX
14+
> layout/TableFormer/OCR + a port of docling-parse's line sanitizer) and is also
15+
> measured byte-for-byte against live docling — **6 / 14 PDF fixtures exact, 7 / 14
16+
> whitespace-normalized** (see `PDF_CONFORMANCE.md`), with a snapshot baseline
17+
> guarding against regressions. `cargo test` is green (unit tests + a 131-source
18+
> output-regression suite).
1819
1920
---
2021

@@ -86,13 +87,13 @@ content-type resolution, and image extraction — reused by DOCX/PPTX/XLSX/EPUB.
8687

8788
These run docling's *discriminative* PDF pipeline ported to ONNX. They are now
8889
measured **byte-for-byte against live docling** (the committed PDF groundtruth is
89-
regenerated from it): **4 / 14 exact**, the rest close — see `PDF_CONFORMANCE.md`.
90-
A deterministic snapshot baseline (`scripts/pdf_conformance.sh`) still guards
91-
against regressions.
90+
regenerated from it): **6 / 14 exact (7 / 14 whitespace-normalized)**, the rest
91+
close — see `PDF_CONFORMANCE.md`. A deterministic snapshot baseline
92+
(`scripts/pdf_conformance.sh`) still guards against regressions.
9293

9394
| Format | How |
9495
|---|---|
95-
| PDF | pdfium glyph cells + page render → RT-DETR layout (ONNX) → **TableFormer** table structure (ONNX) → PP-OCRv3 OCR for scanned pages → **docling-parse line sanitizer** (`dp_lines.rs`) + reading-order assembly |
96+
| PDF | **pure-Rust text parser** (`textparse.rs`, font-advance glyph boxes) + pdfium page render → RT-DETR layout (ONNX) → **TableFormer** table structure (ONNX) → PP-OCRv3 OCR for scanned pages → **docling-parse line sanitizer** (`dp_lines.rs`) + reading-order assembly |
9697
| Images (tiff/webp/png/jpeg) | the same pipeline, image as a single page |
9798
| METS / Google Books | `.tar.gz` of per-page hOCR + TIFF → cells from hOCR → the same layout+assembly path (no OCR needed) |
9899

@@ -158,15 +159,22 @@ These are deliberate or unavoidable divergences, not bugs.
158159
decoder + cell-bbox decoder, ported to ONNX), on a cv2-exact preprocessed
159160
crop. Reproduces docling's padded GitHub tables — `2305-pg9` is cell-for-cell
160161
exact; multi-row headers / spans on the dense papers still differ.
161-
- **Text assembly** — a port of docling-parse's line sanitizer (`dp_lines.rs`):
162-
3-pass corner-distance contraction with gap-proportional space insertion,
163-
`enforce_same_font`, ligature recomposition, loose-box geometry. Plus
164-
docling's markdown escaping, wrap dehyphenation, paragraph-continuation
165-
merging, and band-aware two-column reading order. Default; set
166-
`DOCLING_LEGACY_LINES` for the old gap-heuristic join.
162+
- **Text** — a **pure-Rust PDF text parser** (`textparse.rs`, on `lopdf`)
163+
reconstructs glyph boxes from font advance widths + the text/graphics matrices
164+
(matching docling-parse's geometry, not pdfium's rendered boxes); handles
165+
Type0/CID + simple fonts, ToUnicode/encodings, Form XObject recursion, a
166+
glyph-name fallback, and overprint dedup. It is the default text layer
167+
(`DOCLING_PDFIUM_TEXT=1` falls back to pdfium). Its cells feed a port of
168+
docling-parse's line sanitizer (`dp_lines.rs`): 3-pass corner-distance
169+
contraction with gap-proportional space insertion, `enforce_same_font`,
170+
ligature recomposition, loose-box geometry. Plus docling's markdown escaping,
171+
typographic-punctuation normalization, wrap dehyphenation,
172+
paragraph-continuation merging, band-aware two-column reading order, and
173+
false-picture / page-number layout fixes.
167174
- Output is measured **byte-for-byte against live docling** (PDF_CONFORMANCE.md):
168-
**4 / 14 exact**, the rest close. The remaining gaps are model-level
169-
(TableFormer structure on complex tables, layout classification, RTL).
175+
**6 / 14 exact, 7 / 14 whitespace-normalized**, the rest close. The remaining
176+
gaps are model-level (TableFormer structure on complex tables, layout
177+
classification) plus `amt`'s fraction spacing (a docling quirk).
170178

171179
6. **Extracted image bytes are real but not byte-identical.** Cropped/embedded
172180
pixels are correct, but the PNG re-encoding differs from docling's, so the
@@ -225,7 +233,7 @@ when the TableFormer graphs aren't present.)
225233
committed fixtures (131 sources × 3). `FLEISCHWOLF_REGEN=1` refreshes them.
226234
The JSON fixtures double as a docling-core load check.
227235
- **Snapshot harness**`scripts/pdf_conformance.sh` regenerates and diffs the
228-
PDF/image/METS baseline (needs pdfium + the ONNX models; **76/76 exact**).
236+
PDF/image/METS baseline (needs pdfium + the ONNX models; **91/91 exact**).
229237
- **Conformance**`scripts/conformance.sh <fmt>` scores a format against the
230238
latest published docling (installed from PyPI; see
231239
[`COMPARING.md`](./COMPARING.md)).

PDF_CONFORMANCE.md

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,36 @@ The groundtruth is regenerated from **live published docling**, so it agrees wit
77

88
> Measure locally with `scripts/pdf_groundtruth.sh` (diffs the checked-in
99
> reference; no docling install needed) or `scripts/conformance.sh pdf` (installs
10-
> docling and diffs against it). Diff = changed lines vs the groundtruth (one
11-
> changed line counts as 2).
10+
> docling and diffs against it). Both report two metrics: **strict** (byte-for-byte)
11+
> and **whitespace-normalized** (spacing-only diffs ignored). Diff = changed lines
12+
> vs the groundtruth (one changed line counts as 2).
1213
1314
## Current state
1415

15-
**4 / 14 groundtruth PDFs are byte-for-byte exact.**
16+
**6 / 14 strict** · **7 / 14 whitespace-normalized.**
1617

1718
| PDF | diff | dominant remaining blocker |
1819
|---|---:|---|
1920
| picture_classification | **exact** ||
2021
| code_and_formula | **exact** ||
2122
| multi_page | **exact** ||
2223
| 2305.03393v1-pg9 | **exact** | — (TableFormer table, cell-for-cell) |
23-
| right_to_left_01 | 2 | RTL justified double-space |
24-
| right_to_left_02 | 8 | RTL bidi spacing |
25-
| amt_handbook_sample | 14 | justified spacing + figure captions |
26-
| 2305.03393v1 | 40 | title-page reading order + author-ID run spacing |
24+
| right_to_left_01 | **exact** | — (RTL period attachment) |
25+
| right_to_left_02 | **exact** | — (kashida dedup + page-number layout) |
26+
| amt_handbook_sample | 2 *(ws-ok)* | docling's spurious fraction double space — ours is more faithful |
27+
| normal_4pages | 54 | reading order (heading numbering, footnote order) |
2728
| right_to_left_03 | 66 | RTL bidi |
28-
| normal_4pages | 74 | reading order |
29-
| table_mislabeled_as_picture | 110 | layout over-detects tables (survey rendered as tables) |
30-
| 2206.01062 | 234 | TableFormer multi-row headers + title-page reading order |
31-
| 2203.01017v2 | 247 | TableFormer structure + reading order |
32-
| redp5110_sampled | 271 | TOC mis-classified as a picture; cover-page ordering |
29+
| 2305.03393v1 | 93 | title-page reading order + author-ID run spacing |
30+
| table_mislabeled_as_picture | 108 | layout over-detects tables (survey rendered as tables) |
31+
| 2206.01062 | 198 | TableFormer multi-row headers + title-page reading order |
32+
| 2203.01017v2 | 209 | TableFormer structure + reading order |
33+
| redp5110_sampled | 226 | TOC mis-classified as a picture; cover-page ordering |
3334

34-
The close ones (`right_to_left_01/02`, `amt_handbook`) are 1–2 fixes from exact —
35-
the realistic path to 50% (7/14).
35+
`amt` is the 7th under the whitespace-normalized metric: its only diff is
36+
docling's spurious double space before the `1⁄4` fraction, where our single-spaced
37+
output is the more faithful rendering. The remaining non-exact PDFs are heavy
38+
multi-column / table docs whose gaps are model-level (TableFormer structure,
39+
layout classification, title-page reading order), not text-layer.
3640

3741
## How the pipeline works
3842

@@ -44,24 +48,43 @@ to ONNX in `tableformer.rs`) on a cv2-exact preprocessed crop (`resample.rs`); t
4448
structure + matched cell text reproduce docling's padded GitHub tables (2305-pg9
4549
is cell-for-cell exact).
4650

47-
### Text reconstruction: docling-parse line sanitizer (ported)
48-
49-
The byte-exact ceiling used to be the **text extractor** — pdfium differs from
50-
docling's own `docling-parse` C++ parser at text-run boundaries. We closed it by
51-
porting docling-parse's line sanitizer (`src/parse/page_item_sanitators/cells.h`
52-
`dp_lines.rs`): a 3-pass corner-distance contraction (LTR → RTL → LTR-reverse)
53-
with `merge_with` space insertion (one space when the gap exceeds
54-
0.33×avg-char-width, plus literal space glyphs), `enforce_same_font`, ligature
55-
recomposition (same-loose-box glyphs become one cell), and loose-box geometry
56-
(uniform font ascent/descent), fed by pdfium glyph cells. It is the **default**
57-
(set `DOCLING_LEGACY_LINES` to fall back to the old gap heuristic). This fixed
58-
justified double-spacing, the space before `:`, lam-alef ordering, and fi/ffi
59-
ligatures — and got `multi_page` to byte-exact.
60-
61-
Other text/serializer fixes matching docling: markdown escaping (`_``\_`, then
62-
HTML-escape `&`/`<`/`>`), U+2212→`-`, `@`-glue (`mAP @0.5`), wrap dehyphenation,
63-
paragraph-continuation merging across column/page breaks, and band-aware
64-
two-column reading order (full-width regions break the columns into bands).
51+
### Text reconstruction: a pure-Rust PDF text parser (default)
52+
53+
The byte-exact ceiling was the **text extractor** — pdfium's *rendered* glyph
54+
boxes diverge from docling's own `docling-parse` C++ parser at exactly the points
55+
that drive conformance (generated spaces, combining marks, ligature/fraction
56+
positioning). The pipeline now ships a **pure-Rust text parser** (`textparse.rs`,
57+
on `lopdf`) that reconstructs each glyph's box from the *font's own advance
58+
widths* and the PDF text/graphics matrices — the same information docling-parse
59+
uses. It is the **default** text layer; set `DOCLING_PDFIUM_TEXT=1` to fall back
60+
to pdfium. Pages without a parseable text layer fall back to pdfium
61+
automatically, so scanned/OCR pages are unaffected. (pdfium still provides page
62+
rasters and word/code cells for TableFormer.)
63+
64+
The parser handles Type0/CID + Identity-H and simple Type1/TrueType fonts,
65+
ToUnicode CMaps (`bfchar`/`bfrange`), WinAnsi/MacRoman + `/Differences`
66+
encodings, **Form XObject recursion** (`Do` — bulk body text in heavy PDFs lives
67+
inside a form; 2206 p1 was dropping ~9000 chars), a **glyph-name fallback**
68+
(docling emits an unmappable subset-font name verbatim, `/g115`), and an
69+
**overprint dedup** (a kashida elongation re-stamped on itself — right_to_left_02).
70+
A char-frequency validator (`scripts/parser_completeness.py`) confirms nothing is
71+
silently skipped.
72+
73+
Its cells feed the ported **docling-parse line sanitizer** (`dp_lines.rs`, from
74+
`src/parse/page_item_sanitators/cells.h`): a 3-pass corner-distance contraction
75+
(LTR → RTL → LTR-reverse) with `merge_with` space insertion (one space when the
76+
gap exceeds 0.33×avg-char-width, plus literal space glyphs), `enforce_same_font`,
77+
ligature recomposition, and loose-box geometry. On the clean parser boxes it uses
78+
the Euclidean corner gap (matching docling); on pdfium's loose boxes it keeps the
79+
signed horizontal gap.
80+
81+
Other text/serializer/layout fixes matching docling: markdown escaping (`_``\_`,
82+
then HTML-escape `&`/`<`/`>`), typographic-punctuation normalization
83+
(```'`, ``/```-`, `“”``"`, or `'` for Hangul fonts), `@`-glue
84+
(`mAP @0.5`), wrap dehyphenation, paragraph-continuation merging across
85+
column/page breaks, band-aware two-column reading order, **false-picture
86+
suppression** (empty low-confidence margin boxes on text pages), and
87+
**page-number-first** ordering.
6588

6689
## Remaining blockers (model-level)
6790

@@ -78,6 +101,10 @@ These yield smaller or uncertain gains than the text-layer work already shipped.
78101
3. **Complex title-page reading order.** Author-block / abstract interleaving on
79102
the academic papers (band reading-order handles the full-width title; the
80103
in-column author/abstract order is still off).
81-
4. **RTL justified double-spaces.** pdfium emits zero-width space boxes, losing
82-
the literal space that, with the inserted one, forms a justified double
83-
(`right_to_left_01`). Needs space-box reconstruction that estimates width.
104+
4. **amt fraction double space (text-layer, strict-only).** docling boxes glyphs
105+
with the embedded font's OS/2 typographic metrics, not the PDF descriptor's;
106+
that ~0.3 pt difference makes its justified line insert a *second* space before
107+
the `1⁄4` numerator. Our single-spaced output is the more faithful rendering
108+
(the whitespace-normalized metric credits it); reproducing docling's exact
109+
spacing needs an embedded-font metrics layer, which globally entangles with RTL
110+
geometry. See `PDF_PARSER_NOTES.md`.

PDF_PARSER_NOTES.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ So the text parser is the lever; 50% is reachable.
2323

2424
## What's built (`crates/fleischwolf-pdf/src/textparse.rs`)
2525

26-
Opt-in via `DOCLING_RUST_PARSER=1` (default pipeline is unchanged). Pdfium still
27-
provides page rasters + word/code cells; the parser only replaces prose line
28-
cells, fed through the existing `dp_lines` sanitizer.
26+
The **default** text layer (opt out with `DOCLING_PDFIUM_TEXT=1`). Pdfium still
27+
provides page rasters + word/code cells; the parser replaces prose line cells,
28+
fed through the existing `dp_lines` sanitizer.
2929

3030
- Content-stream interpreter: `cm/q/Q`, `BT/ET`, `Tf/Td/TD/Tm/T*/Tc/Tw/Tz/TL/Ts`,
3131
`Tj/TJ/'/"` with text + graphics matrices.

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ the full architecture, the Python → Rust mapping, and the phased plan.
1111
The public API works end to end across **Markdown, CSV, HTML, AsciiDoc, DOCX,
1212
PPTX, XLSX, EPUB, ODF, WebVTT, Email, JATS, USPTO, XBRL, LaTeX, JSON, PDF,
1313
images and METS** — plus Markdown / docling-JSON output and image extraction.
14-
The discriminative PDF/image pipeline (pdfium + ONNX layout/OCR) lives in
15-
`fleischwolf-pdf`. Audio/ASR is the main format still on the roadmap (see
16-
`MIGRATION.md`).
14+
The discriminative PDF/image pipeline lives in `fleischwolf-pdf`: a pure-Rust PDF
15+
text parser, pdfium for page rasterization, and an ONNX layout/TableFormer/OCR
16+
stack. Audio/ASR is the main format still on the roadmap (see `MIGRATION.md`).
1717

1818
Output is checked against upstream Python docling — declarative formats
1919
byte-for-byte against live docling, the ML pipeline against a deterministic
@@ -131,7 +131,7 @@ export PDFIUM_DYNAMIC_LIB_PATH="$(pwd)/.pdfium/lib"
131131
export DOCLING_LAYOUT_ONNX="$(pwd)/models/layout_heron.onnx"
132132
export DOCLING_OCR_REC_ONNX="$(pwd)/models/ocr_rec.onnx"
133133
export DOCLING_OCR_DICT="$(pwd)/models/ppocr_keys_v1.txt"
134-
bash scripts/pdf_conformance.sh # regenerate + diff the snapshot baseline (76/76)
134+
bash scripts/pdf_conformance.sh # regenerate + diff the snapshot baseline (91/91)
135135
```
136136

137137
## Try it

0 commit comments

Comments
 (0)