Skip to content

fix(sanitizer): keep repeated letters of very small text in remove_duplicate_cells - #372

Open
galer1us wants to merge 1 commit into
docling-project:mainfrom
galer1us:fix/duplicate-cells-glyph-scale
Open

galer1us wants to merge 1 commit into
docling-project:mainfrom
galer1us:fix/duplicate-cells-glyph-scale

Conversation

@galer1us

@galer1us galer1us commented Oct 1, 2026

Copy link
Copy Markdown

Resolves #371

Problem

pdf_decoder<PAGE> always calls remove_duplicate_cells(page_cells, 0.5, true) (src/parse/pdf_decoders/page.h). It drops a later cell with the same text and font if all four corners are within an absolute 0.5pt of an earlier one. On text smaller than about 1pt, the next copy of the same letter in a word is itself closer than 0.5pt, so distinct glyphs get dropped. Committee decodes as Commit e at 1pt and Com it e at 0.5pt. No decode option avoids this, because the call does not check do_sanitization.

Change

The corners must now also be within half of the earlier cell's width, i.e. tol = min(eps, 0.5 * width). An exact repeat (distance 0) still counts, so zero-width cells behave as before.

  • If a glyph is at least 1pt wide, tol is still 0.5pt, so ordinary text behaves exactly as before.
  • A redraw in the same place (stroke-then-fill fake bold, which is what this filter is for; see duplicate_bold_text_01.pdf) overlaps its original almost completely, so it is still removed at any size.
  • The next letter of a word overlaps the previous one by at most a small kerning amount, so it is no longer mistaken for a redraw.

What I deliberately did not change:

  • I did not gate the call on do_sanitization. That would bring back duplicate fake-bold text for anyone who turns sanitisation off.
  • I did not touch remove_adjacent_cells, which has nothing calling it.

This composes with #367 / #369 (for #366, the opposite failure in the same function). They add an exact-match pass before this loop and leave the tolerant scan alone. Both diffs apply cleanly on top of this branch (git apply --check).

Tests

New file tests/test_unit_duplicate_cells.py. It builds its own PDFs with tests/pdf_builder.py and checks the result through DoclingPdfParser → textline_cells:

  • test_repeated_letters_survive_at_any_text_size[12|2|1|0.8|0.5]: before this change the 1, 0.8 and 0.5 cases fail.
  • test_run_drawn_twice_in_place_is_read_once[12|1]: a run drawn twice (1 Tr then 0 Tr) at the same origin decodes once. The 1pt case also failed before, because it lost the same letters.

Results:

  • uv run pytest tests/test_unit_*.py: 211 passed, 1 skipped.
  • pre-commit run --all-files (ruff format, ruff check, mypy, uv-lock): all pass.

Groundtruth changes (parser regression)

I ran test_regression_parse.py, test_regression_threaded_parse.py, test_regression_cell_contraction.py, test_regression_embedded_fonts.py and test_regression_locale_safety.py: 81 passed, 2 failed. The two failures are the parse and threaded-parse groundtruth comparisons. Both fail on the same 5 pages and nothing else.

On every changed page the patch only adds cells. Each added cell is 0.31–0.50pt from its nearest same-text neighbour, so none of them is an exact overprint.

Page Change pdfium count of that glyph (stock → patched)
ligatures_01.pdf p1 +22 l, +53 narrow spaces, +1 f: interpupil ary → interpupillary, il uminator → illuminator (21 lines) l: 334 (312 → 334)
1265837786161076401-1.pdf p1 +1 -: R → -R -: 127 (126 → 127)
b6855cce7fbfd31b_0004.pdf p1 +2 space char cells; words and lines unchanged –
right_to_left_04.pdf p1 +3 ة: the elongated runs no longer split mid-word (ةةةةةةةة ةل → ةةةةةةةةةةل) 80 (105 → 108)
153a16136ed8f5ac_0011.pdf p1 +30 ي: one more stretch glyph in 30 elongated Arabic words 520 (855 → 885)

The two Arabic pages need a maintainer's view. Their stretched glyphs repeat at about 0.43–0.50pt spacing, and stock already decodes more of them than pdfium does, so I can't say whether the extra copy is ink or a redraw.

The groundtruth for those 5 pages lives in the HF dataset, which I can't update from a fork. I can supply the files --update-groundtruth regenerates: for each page, <doc>.page_no_1.py.json.gz plus its .char.txt, .word.txt and .line.txt; for b6855cce…, only the .json.gz and .char.txt change.

Before / after on real documents

These are SEC filings from FinanceBench. I converted them with docling 2.129.0 (TableFormer accurate, OCR off) and compared against the pdfium text of each page.

WALMART_2019_10K: the 28 pages whose glyphs are all under 1pt tall (p91–p145, odd pages)

stock 7.22.1 patched
letters/digits missing vs pdfium 3208 0
extra letters/digits 0 0
fused words 8 0

The rest of the "missing" count (212) is curly quotes folded to ASCII, the same in both builds.

p139, before → after: In any mat er elating sole y to a Com it e member's indiv dual rights or ben fits → In any matter relating solely to a Committee member's individual rights or benefits.

36 control pages (dense, table and TOC pages from 10-K/10-Q/8-K filings, plus 3 table fixtures)

  • Tables: 86 = 86 tables, all identical.
  • Text: identical on 32 of 36 pages.
  • The 4 pages that differ are AMCOR_2020_10K p148–p150 and p153. These are also sub-1pt microtext: 2257 → 0, 1974 → 1, 1399 → 0 and 91 → 24 characters missing, with no extra letters/digits.

Disclosure: Found, reproduced and fix drafted with an AI coding agent (Claude Code); reviewed before filing.

…plicate_cells

remove_duplicate_cells drops a later cell with the same text and font when
all four corners lie within an absolute 0.5pt of an earlier one. On text
smaller than about a point the next occurrence of the same letter in a word
is itself closer than that, so "Committee" decoded as "Com it e".

Also require the corners to lie within half of the earlier cell's width.
For any glyph at least 1pt wide the bound stays 0.5pt, so nothing changes;
an exact redraw (stroke-then-fill bold) is still removed at any size.

Resolves docling-project#371

Signed-off-by: galer1us <66819755+galer1us@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @galer1us, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@PeterStaar-IBM
PeterStaar-IBM self-requested a review October 5, 2026 03:58
@galer1us

Copy link
Copy Markdown
Author

The regression-tests failure is this change working as intended: the five documents it flags now come out differently, and their ground truth records the old output. main passes the same suite, and nothing merged since this branch's base touches cells.h or these documents.

I built this branch and its base (8d1144b) side by side and compared page 1 of each flagged document. The base build reproduces the stored ground truth exactly (3399 cells on 1265837786161076401-1.pdf).

Document What changes Assessment
ligatures_01.pdf Doubled letters are no longer dropped: "interpupil ary" → "interpupillary", "il uminator" → "illuminator", "fol ow the fol owing" → "follow the following", "al ows" → "allows", "On/Of" → "On/Off". 21 lines change, all in this direction. The bug in #371
b6855cce7fbfd31b_0004.pdf 2 extra space cells; words and lines unchanged. No visible effect
1265837786161076401-1.pdf One extra - (0.47 pt wide, 0.37 pt from the other), so a word "R" becomes "-R". Unclear from the cells whether the hyphen is drawn twice or redrawn
153a16136ed8f5ac_0011.pdf 30 more ي, one in each word where the base already repeats it, e.g. "فضيييييلا" (5×) → 6×. See below
right_to_left_04.pdf 3 more ة in runs the base already repeats 8×. See below

One side effect is worth stating plainly. In both Arabic documents, copies of the same glyph are drawn about half a glyph width apart (offset ≈ 0.46–0.50 pt on glyphs ≈ 0.82–0.86 pt wide). That spacing now counts as a separate letter rather than a redraw: the same rule that keeps "ll" in ligatures_01. Those runs were already wrong in the base output; this adds one glyph to each rather than breaking text that was right. If you'd rather the threshold sit somewhere other than half a glyph width, I'm happy to change it.

Could you regenerate the parser ground truth for these five documents when you review, as was done for the cells.h change in #361? The ground truth lives in the Hugging Face dataset, which I can't update from a fork.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repeated letters are dropped from text set below ~1pt: remove_duplicate_cells uses a fixed 0.5pt tolerance

1 participant