|
| 1 | +# PDF conformance roadmap |
| 2 | + |
| 3 | +How close the Rust PDF pipeline gets to docling's **default** Markdown, measured |
| 4 | +byte-for-byte against the committed groundtruth (`tests/data/pdf/groundtruth/*.md`), |
| 5 | +and what it would take to close the remaining gap. |
| 6 | + |
| 7 | +> Measure locally with `scripts/pdf_groundtruth.sh` (no docling install needed — |
| 8 | +> it diffs against the checked-in reference). The numbers below are the current |
| 9 | +> state. |
| 10 | +
|
| 11 | +## Current state |
| 12 | + |
| 13 | +**1 / 14 groundtruth PDFs are byte-for-byte exact** (`picture_classification`); |
| 14 | +the rest are blocked on one of the categories below. Diff = changed lines vs the |
| 15 | +groundtruth (one changed line counts as 2). |
| 16 | + |
| 17 | +| PDF | diff | dominant blocker | |
| 18 | +|---|---:|---| |
| 19 | +| picture_classification | **exact** | — | |
| 20 | +| right_to_left_01 | 4 | RTL/bidi | |
| 21 | +| code_and_formula | 6 | inter-run spacing + code fencing | |
| 22 | +| right_to_left_02 | 8 | RTL/bidi | |
| 23 | +| amt_handbook_sample | 12 | double-spaces, duplicate glyphs, fractions | |
| 24 | +| 2305.03393v1-pg9 | 25 | table structure | |
| 25 | +| right_to_left_03 | 74 | RTL/bidi | |
| 26 | +| multi_page | 76 | inter-run spacing + line-wrap hyphens | |
| 27 | +| normal_4pages | 108 | reading order (CJK) | |
| 28 | +| 2305.03393v1 | 152 | table structure | |
| 29 | +| table_mislabeled_as_picture | 151 | table structure | |
| 30 | +| 2203.01017v2 | 346 | table structure (+ inter-run spacing) | |
| 31 | +| 2206.01062 | 321 | table structure | |
| 32 | +| redp5110_sampled | 342 | table structure | |
| 33 | + |
| 34 | +Shipped in this PR (no regressions; `pdf_conformance` stays 76/76): |
| 35 | +de-hyphenation + typography normalization, `<!-- formula-not-decoded -->`, |
| 36 | +caption-before-image pairing, and (strict-mode only) punctuation tightening. |
| 37 | + |
| 38 | +Reaching ~50% exact requires the two big items below: **text-stream extraction** |
| 39 | +(unlocks the spacing-bound PDFs) and **TableFormer** (unlocks the six |
| 40 | +table-bound PDFs). |
| 41 | + |
| 42 | +--- |
| 43 | + |
| 44 | +## Blocker 1 — inter-run text spacing (a.k.a. "text-stream extraction") |
| 45 | + |
| 46 | +**Symptom.** pdfium splits a visual line into multiple style *segments* (a |
| 47 | +citation's superscripts, a code line's tokens, mixed fonts). We emit one cell |
| 48 | +per segment and join them with single spaces, so the real inter-run spacing is |
| 49 | +lost: `[ 37 , 36 ]` instead of `[37, 36]`, `function add ( a , b )` instead of |
| 50 | +`function add(a, b)`. docling reads text via pypdfium2's `get_text_range` |
| 51 | +(`FPDFText_GetText`), which inserts spaces from each glyph's *advance* and so |
| 52 | +reproduces the PDF's real spacing. |
| 53 | + |
| 54 | +**What was tried in this PR and why each failed** (all reverted): |
| 55 | + |
| 56 | +1. **Raw char API** (`PdfPageText::chars()` → `unicode_char()` + `loose_bounds()`, |
| 57 | + concatenated per line). pdfium's per-char list is *unreliable*: some lines |
| 58 | + come back with no space characters at all (`Thiscontentisextremelyvaluablefor`) |
| 59 | + and the char order is occasionally scrambled. Net regression. |
| 60 | +2. **`inside_rect()`** (`FPDFText_GetBoundedText`) over a whole line's bounding |
| 61 | + box. `GetBoundedText` ≠ `GetText`: it *drops* inter-run spaces on |
| 62 | + multi-segment lines (`{ahn,nli,mly,taa}@zurich` vs docling's |
| 63 | + `{ ahn,nli,mly,taa } @zurich`) and *bleeds* glyphs from vertically adjacent |
| 64 | + lines (`nevertheless exLines of different…`). Net regression. |
| 65 | +3. **Hybrid** (segment text for single-segment lines, `inside_rect` only for |
| 66 | + multi-segment lines). Same `GetBoundedText` divergence on exactly the lines |
| 67 | + that need fixing. |
| 68 | + |
| 69 | +**Root cause / the real fix.** `segment.text()` is itself `inside_rect(segment. |
| 70 | +bounds())` — i.e. the *only* reliable text unit pdfium-render exposes is a single |
| 71 | +style run. What docling uses, `FPDFText_GetText(textpage, start_index, count, …)` |
| 72 | +for an arbitrary **character range**, is *not* wrapped by `pdfium-render` |
| 73 | +0.8.37. The path forward is to get that call: |
| 74 | + |
| 75 | +- add a thin binding for `FPDFText_GetText` over a char range (upstream PR to |
| 76 | + `pdfium-render`, or call it through the crate's `PdfiumLibraryBindings` handle |
| 77 | + directly), then |
| 78 | +- group segments into lines (by vertical band, splitting at column gutters — the |
| 79 | + clustering already prototyped in this PR), map each line to its `[start, count]` |
| 80 | + char range, and read the whole line with `GetText`. |
| 81 | + |
| 82 | +This is the single highest-leverage change for default-mode conformance: it |
| 83 | +fixes citations, inline code, fractions, and the justified-text double spaces, |
| 84 | +and unblocks `multi_page` and `code_and_formula` (the latter also needs code |
| 85 | +regions rendered as fenced blocks). **Stopgap shipped:** `--strict` tightens the |
| 86 | +citation/parenthetical spacing at serialization time, so strict Markdown already |
| 87 | +reads cleanly even though default mode still mirrors the segment spacing. |
| 88 | + |
| 89 | +Also needed alongside it: |
| 90 | +- **Line-wrap de-hyphenation for real hyphens.** We already drop the U+0002 soft |
| 91 | + hyphen; `multi_page` wraps words with a real `-` (`professi-`/`onal`), which |
| 92 | + needs line-end-hyphen detection during the line join. |
| 93 | +- **Double-space preservation.** docling keeps the PDF's wide justified spacing |
| 94 | + (`the stainless steel nuts`); `clean_text` currently collapses runs of |
| 95 | + whitespace. With `GetText` per line, stop collapsing intra-line spacing. |
| 96 | + |
| 97 | +## Blocker 2 — table structure (TableFormer) |
| 98 | + |
| 99 | +**Symptom.** Six PDFs (`2206.01062`, `2305.03393v1[-pg9]`, `redp5110_sampled`, |
| 100 | +`table_mislabeled_as_picture`, and the table on `2203.01017v2`) are dominated by |
| 101 | +table differences. We reconstruct grids *geometrically* (cluster cells into |
| 102 | +rows/columns); docling runs **TableFormer**, an autoregressive transformer that |
| 103 | +predicts the table structure as an OTSL/HTML tag sequence plus per-cell bounding |
| 104 | +boxes, which recovers spanning headers and merged cells we cannot. |
| 105 | + |
| 106 | +**Scope of a port** (large — own PR, likely staged over several): |
| 107 | + |
| 108 | +1. **Weights.** TableFormer ships in `docling-ibm-models` (`TableModel04_rs`, |
| 109 | + "accurate"/"fast" variants). Export the encoder + the two decoders to ONNX |
| 110 | + from the published checkpoint; confirm the license permits redistribution of |
| 111 | + a converted model. |
| 112 | +2. **Inference loop.** Unlike the layout/OCR models (single `Session::run`), |
| 113 | + TableFormer is **autoregressive**: encode the table-crop image once, then step |
| 114 | + the structure decoder to emit OTSL tokens until `<end>`, feeding each token |
| 115 | + back in. The cell-bbox decoder runs per predicted cell. This is a real |
| 116 | + decoding loop in `fleischwolf-pdf`, not a one-shot call — budget for KV-cache |
| 117 | + handling and a token vocabulary/OTSL grammar. |
| 118 | +3. **Cell content.** Map predicted cell bounding boxes back onto the PDF text |
| 119 | + cells (we have these) to fill cell text — the same matching docling does for |
| 120 | + "PDF" tables (it does not OCR programmatic tables). |
| 121 | +4. **Serialization.** Convert the predicted OTSL grid (with row/col spans) to the |
| 122 | + `Table` node; the Markdown table serializer already exists but assumes a plain |
| 123 | + grid, so spans need representing. |
| 124 | + |
| 125 | +A cheaper interim improvement (not docling-exact, but closes some diff): better |
| 126 | +geometric reconstruction — detect header rows, merge obvious spanning cells, and |
| 127 | +handle the multi-line header cells that currently shatter into many columns. |
| 128 | + |
| 129 | +## Blocker 3 — RTL / bidi (Arabic) |
| 130 | + |
| 131 | +`right_to_left_01/02/03`. Two compounding issues: (a) reading order — Latin runs |
| 132 | +embedded in RTL text and the overall right-to-left flow are emitted left-to-right |
| 133 | +(`Python و ة R` vs `R و Python`); we'd need Unicode bidi reordering of each line. |
| 134 | +(b) Arabic shaping — pdfium returns presentation-form / decomposed sequences that |
| 135 | +differ from docling's (`اإل` vs `الإ`), needing NFC-ish normalization of the |
| 136 | +Arabic block. Both are self-contained but specialized; lower priority than 1–2. |
| 137 | + |
| 138 | +## Smaller items |
| 139 | + |
| 140 | +- **Duplicate glyphs** (`amt_handbook`: `T he`, `F Figure 7-26 6`). pdfium emits |
| 141 | + doubled glyphs for some bold/overlapping text; needs de-duplication of |
| 142 | + overlapping cells. |
| 143 | +- **Code regions** → fenced ```` ``` ```` blocks with the caption *after* (code |
| 144 | + captions trail; figure captions lead). Pairs with Blocker 1 for the code text. |
0 commit comments