Skip to content

feat(ocr): replace broken PDF text layers with OCR - #4654

Open
yashikam19 wants to merge 15 commits into
docling-project:mainfrom
yashikam19:feat/ocr-replace-junk-text-layer
Open

yashikam19 wants to merge 15 commits into
docling-project:mainfrom
yashikam19:feat/ocr-replace-junk-text-layer

Conversation

@yashikam19

@yashikam19 yashikam19 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Some PDFs have a text layer that does not match the text drawn on the page. A font's glyphs are not mapped to real characters, so the page looks correct but the extracted text is garbage, for example 8VceZWZTReV CW 9VdZXBReZCBd for "Certificate of Designations". Docling keeps that text. The per-cell checks in PagePreprocessingModel.rate_text_quality() can lower parse_score, but nothing replaces the text.

This PR adds OcrOptions.replace_broken_text_layer and the CLI flag --replace-broken-text-layer. Both are off by default. When the option is on, Docling OCRs each page whose text layer is broken, and uses the OCR text for that page only.

PdfPipelineOptions(do_ocr=True, ocr_options=RapidOcrOptions(replace_broken_text_layer=True))
docling report.pdf --replace-broken-text-layer

The change:

  • docling/utils/text_quality.py adds is_broken_text_layer(). Page preprocessing calls it on each page's text layer, only when the option is on. It returns True only for patterns that a broken font mapping produces, and False when it is not sure:
    • Control characters or U+FFFD: ≥3% of the page, or U+FFFD in runs, in several cells, in ≥10% of the lines, or on a short page. A lone U+FFFD at the start of a line, as an unmapped bullet, is not counted.
    • Private-use characters: ≥50% of the page, or one cell with a run of ≥4 different private-use characters. Icon fonts repeat a few code points, so a star rating or an icon row does not trigger this.
    • A token with ≥2 control characters in one cell.
    • A non-Latin font decoded as accented Latin (Îò÷¸ò ïîêàçûâàåò). IPA symbols are not counted, because no 8-bit code page decodes to them.
    • Letters shifted by a broken ToUnicode map (Wkh txduwhuo| uhsruw). The letter-frequency profile keeps the shape of a language, but the letters are in the wrong places. Only tokens with a lowercase letter and ≥3 letters count, so acronyms, part numbers and math variables are skipped. GLYPH<…> placeholders for unmapped glyphs are skipped too. The check needs ≥200 letters and a low vowel share.
  • A broken page gets parse_score 0.0.
  • In BaseOcrModel, a page that the new check flags is OCR'd in full and its PDF cells are dropped, as in OcrMode.FULL_PAGE, but for that page only. Other pages keep their PDF text. A low per-cell score alone does not replace a page, because unmapped bullet glyphs also cause it on pages with good text. parse_score still describes the PDF text layer.
  • The option is in the base OcrOptions, so every engine supports it, including plugins. OcrAutoModel passes it to the engine it selects.
  • In OcrMode.FULL_PAGE the option has no effect, because Docling does not read the PDF text layer.
  • With the option off, preprocessing, parse_score and the output do not change.
  • docs/concepts/OCR.md has a new section, "Replace a broken PDF text layer". The CLI and SDK references of the usage skill describe the option.
  • The check reads visible text only. Invisible text (render modes 3 and 7), such as an OCR layer or accessibility text, does not change the grade of a page.
  • A replaced page keeps its invisible PDF cells where no OCR cell overlaps them, as the OCR merge already does.

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

Review evidence:

  • Before and after, on real PDFs with a broken text layer. The first column is main. The second column is this branch with the option on. The engine is the default OcrAutoOptions (ocrmac on macOS). The Marathi page uses TesseractCliOcrOptions(lang=["mar"]), because ocrmac cannot read Marathi.

    document main this branch, option on detected by
    10-K exhibit list 8VceZWZTReV CW 9VdZXBReZCBd hZeY cVdaVTe Certificate of Designations with respect to Series C Preferred Stock control/U+FFFD share, shifted letters
    10-K, two columns PFDKFCF@>KQIV @E>KDB QEB @LJMBQFQFSB significantly change the competitive environment U+FFFD in ≥10% of the lines
    proxy statement table ,75GD;FK (I@7DE:;B A8 Security Ownership of Certain Beneficial Owners and Management control/U+FFFD share
    retirement plan ���' ��(�������� ������$ 2.46 Definitions Related to Employer Contributions control/U+FFFD share
    event program 0P�PSICOK>GS- 5KUPUT9DCTJK Co-organizer: Hitotsubashi University International Seminars control/U+FFFD share
    Marathi report, legacy font H¥$VrH$m`©H«$_mMr {Xem ४. कृतीकार्यक्रमाची दिशा shifted letters

    In the same run, the output of each PDF with a good text layer did not change.

    Screenshots: page, main, this branch (Markdown export as is, first lines) 1_10k_two_columns 3_marathi_legacy_font 2_retirement_plan
  • False positives, with the check run on each page's text cells:

    • DocLayNet v1.1 test split (4,969 pages with text, 196 documents, 6 categories): 18 pages flagged (0.36%), all in one document whose whole text layer is broken (control characters and accented-Latin mojibake). No page with a good text layer was flagged. The dataset gives the PDF cells of an older parser, without visibility, and it has no U+FFFD, so it does not test the U+FFFD rules.
    • The PDFs in tests/data (173 pages with text): 4 pages flagged, all with U+FFFD in place of real characters. No page with a good text layer was flagged.
    • The check takes about 1 ms per page (median), only when the option is on.
  • Document tree: on a replaced page, the text cells come from OCR only, as in OcrMode.FULL_PAGE. Layout, tables and reading order then run on those cells. Pages that are not replaced do not change.

  • Reference data: no change. The option is off by default, and no ground-truth file changes.

  • Tests:

    • tests/test_text_quality.py: text that must not be flagged (code, LaTeX, JSON, markdown, units, Greek, Cyrillic, Hebrew, Arabic, Thai, Devanagari, CJK, French, Czech, Welsh, Vietnamese, part numbers, base64, DNA, icon ratings, a single U+FFFD, a pdfium hyphen marker, math variables, GLYPH<…> placeholders in a figure, a dotted capital I, an IPA vowel chart, unmapped bullets in a list, a slide and an agenda)
      ), and text that must be flagged (control codes, U+FFFD, one U+FFFD in each of many lines, private-use glyphs, accented-Latin mojibake, shifted letters, one broken cell on a good page).
    • tests/test_replace_broken_text_layer.py: end to end on a generated PDF, with Tesseract and with OcrAutoOptions. It covers the option on and off, a good text layer that must stay, and a page with a poor per-cell score alone that keeps its text layer. Without the OcrAutoModel change, the auto-engine test fails.
  • Commands and results:

    • uv run pytest tests/test_text_quality.py tests/test_replace_broken_text_layer.py: 55 passed.
    • make validate: all hooks pass (Ruff, ty, Tach, license header, max lines).
  • Scope:

    • The OCR text depends on the engine and the language, so the user must select an engine that reads the script of the page.
    • The check does not detect text that uses the right alphabet but wrong letters, or right letters in the wrong order. docs/concepts/OCR.md states this.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

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

@mergify

mergify Bot commented Oct 7, 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)(?:\(.+\))?(!)?:

@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem CLI issues related to the command line interface (CLI) enhancement New feature or request ocr labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit bec1a5e84f. The analysis read the diff only. It did not run code from this pull request.

The PR adds the option OcrOptions.replace_broken_text_layer and the CLI flag --replace-broken-text-layer. When the option is on, Docling detects PDF pages with a broken text layer using the new is_broken_text_layer() and replaces their text with full-page OCR. The option is off by default.

CI safety: no concern found. A maintainer must still approve CI.

2 related pull request(s) in the same code
PR State Signal Assessment
#4781 open changes the same code related: Both PRs add an opt-in PDF pipeline option and a CLI flag, so they edit the same option and CLI files. They add different features: PDF document metadata and language versus OCR replacement of a broken text layer.
#4815 open changes the same code related: Both PRs add an opt-in step to the standard PDF pipeline and its options. Superscript and subscript marking is a different feature from OCR replacement of a broken text layer.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@yashikam19

Copy link
Copy Markdown
Contributor Author

Thanks for the triage note. #4643 and this PR solve the same problem, but they differ in design. A short comparison to help the review:

#4643 this PR
Default always on: changes the shared rate_text_quality(), so parse_score and OCR routing change for all users opt-in replace_broken_text_layer; no change when off
Replaced unit layout region (PDF-aware modes) whole page (all modes except FULL_PAGE)
Detects shifted letters (8VceZWZTReV) and accented-Latin mojibake (Îò÷¸ò) no yes

I ran the rate_text_quality() from #4643 on a few inputs. These cells with a good text layer score 0.0: AT&T Inc., R&D expenses, S&P 500, x+y=z, and a bullet from an icon font (\uf002 Jahresumsatz 55 Millionen Euro). On a slide deck with a good text layer, 9 of 15 cells score 0.0 because of the icon bullets.

The region-level routing in #4643 is finer than the page-level replacement here. If maintainers prefer, I am happy to combine the two: region-level routing from #4643 behind the opt-in option and the detector of this PR. @Kshitij3h, let me know what you think.

@Kshitij3h

Copy link
Copy Markdown

Hi @yashikam19, thanks for the ping and the comparison.

I agree with your suggestion to combine the approaches. Region-level OCR routing is essential to avoid discarding valid digital text on mixed pages, and pairing that with refined detection heuristics handles the edge cases well.

I'll refine the quality heuristics in #4643 to ensure they generalize cleanly across broader document formats, symbols, and presentation artifacts. Happy to follow the maintainers' guidance on how best to coordinate or combine the work.

@wittjeff

wittjeff commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

I am concerned that the text layer may in some cases have additional text bits that serve as accessibility enhancements, and that these additional bits (which may or may not be identifiable as being for accessibility) may confound the quality rating. A check-and-purge of garbage content in the text layer seems sensible, but the purge should probably be human-gated.

@yashikam19

Copy link
Copy Markdown
Contributor Author

Thanks @wittjeff, that is a fair concern.

The option is off by default, so a person decides per run whether any page can be replaced, and a replaced page keeps its low parse_score in the confidence report.

But you are right about hidden text: the check reads all text cells of a page, including invisible ones (render modes 3 and 7), where accessibility text or an older OCR layer often sits. On a replaced page, those invisible cells are dropped from the output too.

Docling already treats invisible text separately when it selects OCR regions and merges OCR with PDF text (_segregate_by_visibility). I propose to reuse that here:

  1. Run the check on visible text only, so hidden text cannot make a page look broken or clean.
  2. Keep the invisible cells on a replaced page, as the OCR merge already does, so hidden text is not lost.

Would this address the concern?

@wittjeff

wittjeff commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

yashikam19 I think your check-invisible-text-only idea makes sense as a strong heuristic so I suggest going with that. But I admit I don't feel like I have enough experience to know how well this will fit the broad range of use cases out there, so I'm curious if anyone else has thoughts.

@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem and removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem labels Oct 8, 2026
Some PDFs carry a text layer that does not match the text drawn on the page,
such as an unmapped font encoding or leftover noise from a scanner's OCR.
Docling trusts that layer, so the noise ends up in the output, and the page's
parse_score still reports 1.0 because the existing per-cell checks only look
for glyph-name dumps.

Add docling.utils.text_quality.is_broken_text_layer(), which judges a page's
whole text layer on patterns only broken text produces: control, private-use
and replacement characters, symbols inside words, non-ASCII Latin runs,
punctuation soup, image file names left by scanners, and unusual characters
on scanned pages. Symbols common in ordinary documents never count.

Page preprocessing sets parse_score to 0.0 for a broken page, so the score,
its poor grade and the document-level score reflect it.

Add OcrOptions.replace_broken_text_layer (default False). When enabled, a
page whose parse_score is graded poor (below 0.5) is OCR'd in full and its
OCR text replaces its PDF text, as OcrMode.FULL_PAGE does, for that page
only. The option lives in the base OCR model, so every engine, including
external plugins, supports it.

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
Some PDFs carry a text layer that does not match the text drawn on the page,
because a font's glyphs were not mapped to real characters. Docling keeps that
text, so garbage reaches the output while parse_score still reports 1.0.

Add OcrOptions.replace_broken_text_layer and the CLI flag
--replace-broken-text-layer (default off). When enabled, page preprocessing
checks each page's whole text layer with
docling.utils.text_quality.is_broken_text_layer(), which flags text made
mostly of control or private-use characters, or Latin text that is mostly
non-ASCII letters. It relies on Unicode properties only, with no character
lists. A broken page's parse_score drops to 0.0, and every page graded poor
(below POOR_SCORE_BOUND, shared with QualityGrade) is OCR'd in full, with its
OCR text replacing its PDF text, as OcrMode.FULL_PAGE does, for that page
only. parse_score keeps describing the PDF text layer.

With the option off, preprocessing and parse_score are unchanged. The option
lives in the base OCR model, so every OCR engine, including plugins, supports
it.

Tests: unit tests on ordinary text (code, LaTeX, JSON, markdown, units,
several scripts) and on unmapped-font patterns, plus end-to-end tests with
Tesseract on a generated PDF, with the option on and off. The usage skill
docs cover the new option.

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
is_broken_text_layer() looked only at the whole page, and only for control,
private-use and non-ASCII Latin characters. It missed a broken font that
covers only part of a page, and a broken font that maps glyphs to the wrong
ASCII letters.

Keep the existing checks and add:
- Per-line checks, so one broken cell on a good page is found: a run of 4 or
  more different private-use characters (icons repeat a few code points), or
  a token with 2 or more control characters.
- U+FFFD density rules: runs on short pages, many U+FFFD, U+FFFD in several
  lines, or long runs. One stray U+FFFD does not flag a page.
- A shifted-letter check for broken ToUnicode maps. The letter-frequency
  profile keeps the shape of a language, but the letters are in the wrong
  places. It counts only tokens with a lowercase letter, so acronyms and part
  numbers are skipped, and it needs 200 letters and a low vowel share.

The non-ASCII Latin check now also needs few ASCII letters on the page, to
protect mixed-language and Vietnamese text.

Tests: ordinary text long enough for the letter check (English, Czech, Welsh,
Vietnamese, C code, part numbers, base64, DNA, icon ratings, a single U+FFFD,
a pdfium hyphen marker), and broken text that the old checks missed (shifted
letters, one broken cell, U+FFFD below the old share)

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
OcrAutoModel built the options of the selected engine from mode and lang
only, so with OcrAutoOptions a broken page was graded poor but not OCR'd.

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
…ed page

The broken-text-layer check reads visible text only, so an invisible OCR layer or accessibility text cannot change the grade of a page. A replaced page keeps its invisible PDF cells where no OCR cell overlaps them, as the OCR merge already does.

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
@yashikam19
yashikam19 force-pushed the feat/ocr-replace-junk-text-layer branch from 10d1da5 to 48a1eb8 Compare October 8, 2026 18:02
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem and removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem labels Oct 8, 2026
Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
@github-actions github-actions Bot removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem labels Oct 22, 2026
@yashikam19

Copy link
Copy Markdown
Contributor Author

@geoHeil, thanks for the reproduction. Fixed in afb05c77.

The accented-Latin rule counted IPA symbols (ɪ ʊ ə …) as Latin letters outside ASCII, so a page of mostly IPA looked like a non-Latin font decoded as Latin. No 8-bit code page decodes to the IPA blocks, so the rule now skips U+0250–U+02AF and U+1D00–U+1DBF. Cyrillic mojibake (Îò÷¸ò ïîêàçûâàåò) is still detected.

Your vowel chart is a regression case in tests/test_text_quality.py. It fails without the fix and passes with it. tests/test_text_quality.py and tests/test_replace_broken_text_layer.py: 52 passed.

Could you re-review when you have time?

geoHeil
geoHeil previously approved these changes Oct 22, 2026

@geoHeil geoHeil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed afb05c774f6a2a267df1e9dafcec7251bfb69512. The earlier blocker is fixed. LGTM.

_non_ascii_latin_share() no longer counts IPA symbols (U+0250–U+02AF and U+1D00–U+1DBF) as accented Latin. No 8-bit code page decodes to these symbols, so the change does not hide real mojibake. The vowel chart from my earlier review is now a regression case in tests/test_text_quality.py. This case fails with the rule at 031cff70 and passes at this head. is_broken_text_layer still detects the mojibake case of this PR and Cyrillic cp1251 text that is read as cp1252. The real-PDF reproduction now keeps all three IPA rows, and parse_score stays at 1.0.

Local validation: 52 text-quality and broken-text-layer tests pass with real Tesseract. 360 related OCR, CLI, and options tests pass. CI and Docs CI pass at this head.

Non-blocking follow-ups:

  1. The replace_broken_text_layer field description in docling/datamodel/pipeline_options.py still says that a page in the poor grade "from this check or from the existing glyph-name checks" gets full-page OCR. Now only is_broken_text_layer causes the replacement (docling/models/base_ocr_model.py:155). The docstring at docling/models/base_ocr_model.py:629-630 has the same problem. Please update both texts. The field description is part of the public API.
  2. docs/concepts/OCR.md does not list the accented-Latin rule in the patterns that Docling detects.
  3. page_preprocessing_model.py imports the private function _segregate_by_visibility from base_ocr_model.
  4. A page that lists Latin-1 letters one by one, for example a character table, is still flagged as broken. Such pages are rare, and the option is off by default.

…helper public

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
@github-actions github-actions Bot added the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 23, 2026
@yashikam19

Copy link
Copy Markdown
Contributor Author

Thanks @geoHeil for the approval and the follow-ups. I addressed points 1 to 3 in 364e4c01:

  1. The replace_broken_text_layer field description and the BaseOcrModel docstring now say that only is_broken_text_layer replaces a page, and that a low parse_score alone does not.
  2. docs/concepts/OCR.md now lists accented Latin letters that stand for another script.
  3. The visibility helpers in base_ocr_model are now public (segregate_by_visibility, is_invisible), so page preprocessing no longer imports a private function.

I left point 4 (a page that lists Latin-1 letters one by one) as a known limit, since such pages are rare and the option is off by default.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI first review (advisory): no blocker found

This review covers the changes since 3532a125b8 at 364e4c0120. It read the code and did not run it.

The new U+FFFD rule can route good pages to full-page OCR. It flags a page when 3 or more lines hold one U+FFFD each and they are at least 10% of the lines. Check whether that rate happens on healthy pages, for example with an unmapped bullet glyph.

A maintainer makes the merge decision. This review never approves the pull request.

Comment thread docling/utils/text_quality.py
@github-actions github-actions Bot added the ai:review-lgtm AI first review found no blocker or major issue (advisory) label Oct 23, 2026
Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) and removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:review-lgtm AI first review found no blocker or major issue (advisory) labels Oct 23, 2026
@yashikam19

Copy link
Copy Markdown
Contributor Author

@geoHeil, one more small change in 24889cee, from the AI review: a lone U+FFFD at the start of a line (an unmapped bullet) no longer counts toward the "U+FFFD in many lines" rule. Could you take another look when you have time?

geoHeil
geoHeil previously approved these changes Oct 23, 2026

@geoHeil geoHeil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head 24889ceeb0b455a8bb700015add4413bac03ad11 (merge base d01c9c09). This re-review covers the changes since my approval at afb05c77: 364e4c01 and 24889cee. LGTM, on the condition that Run CI passes at this head. Run CI was still running when I posted this review. Run Docs CI and the fast checks pass.

Earlier follow-ups

  1. The field description of replace_broken_text_layer and the BaseOcrModel docstring: fixed. Both now say that only is_broken_text_layer replaces a page. This agrees with base_ocr_model.py:155. The CLI help, docs/concepts/OCR.md, and the usage-skill references contain no stale wording.
  2. docs/concepts/OCR.md now lists the accented-Latin rule.
  3. The visibility helpers are now public (segregate_by_visibility, is_invisible). No reference to the old private names remains. A merge with current main (36be1d24) has no conflicts and no old names.
  4. A page that lists Latin-1 letters one by one: kept as a known limit. I agree.

The AI-review question about bullets is fixed for normal pages. A page with 3 lone U+FFFD bullets in 25 lines is kept. The new ORDINARY case bullet_replacement_markers fails with the afb05c77 detector and passes at this head.

Suggestion (non-blocking): short pages with unmapped bullets are still flagged

Location: docling/utils/text_quality.py:210 and text_quality.py:144.

24889cee changes only the "U+FFFD in many lines" rule. Two other rules still count a lone bullet U+FFFD: the 3% share of control characters and U+FFFD, and the rule for 3 lines at a 2.5% share. On a short page, a few bullets reach these shares.

  • A slide with a title and 3 bullet items, where each bullet is its own cell (93 non-space characters): flagged.
  • An agenda slide with 5 short items, each U+FFFD + space + text: flagged.

With the option on, these slides get full-page OCR, and the OCR text replaces their correct PDF text. The afb05c77 detector flags them too, so this is not a new regression, and the option is off by default.

One possible fix: remove a lone U+FFFD at the start of each line before all U+FFFD checks run, not only before the line-share rule. I tested this rule in a separate script. Both slides are then kept. All 34 ORDINARY cases and all 11 BROKEN cases give the same result as at this head.

Notes

  • Cost of the bullet rule: when the only damage on a broken page is a lone U+FFFD followed by a space at the start of a line, the line-share rule does not flag the page. This occurs only when a font leaves one-letter words unmapped, so I accept it.
  • Nit: _has_replacement_in_text (text_quality.py:125 and :127) uses a literal U+FFFD character. The rest of the module uses "�".

What I ran

  • The head tests/test_text_quality.py with the afb05c77 detector: 1 failed (bullet_replacement_markers), 45 passed.
  • A probe of is_broken_text_layer with slides, a page with a no-break space after the bullet, broken text with U+FFFD inside words, and the variant above.
  • A read of the diff since afb05c77, a search for stale wording, and a merge check against current main.

What I did not run

  • The test suites at this head. I rely on Run CI for them.

I made no source, test, or reference-data changes.

Signed-off-by: Yashika Malhotra <106444881+yashikam19@users.noreply.github.com>
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) and removed ai:ci-safe AI triage found no CI-safety concern (advisory) labels Oct 23, 2026
@yashikam19

Copy link
Copy Markdown
Contributor Author

Thanks @geoHeil for the approval and the suggestion. Both are in bec1a5e8: the lone-bullet check now runs before all U+FFFD rules, the two slides from your review (3 bullet items in separate cells, and a 5-item agenda) are now tests of text that must be kept (they fail on 24889cee), and the helper uses "\ufffd".

The push dismissed your approval automatically, because the repository dismisses approvals when new commits are pushed. Could you take another look when you have time?

Comment thread docling/utils/text_quality.py

@geoHeil geoHeil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head bec1a5e84f557af8d9e469edd2c88f61db07724e. This re-review covers the change since my approval at 24889cee: bec1a5e8. LGTM.

Earlier suggestion

Short pages with unmapped bullets: fixed. A lone U+FFFD at the start of a line is now removed before all U+FFFD checks run, not only before the line-share rule. The new ORDINARY cases bullet_slide and bullet_agenda fail with the 24889cee detector and pass at this head. The nit about the literal U+FFFD is fixed too.

A probe confirms that real damage is still detected: U+FFFD inside words (also when the line starts with a marker), a no-break space after U+FFFD, and �� at the start of a line. Bullets with a no-break space, indented bullets, and bullet-only cells are kept.

Configurable thresholds

I agree to keep the values in text_quality.py internal. They work together, and the option and OcrMode.FULL_PAGE give users control. A single setting can be added later without a breaking change.

What I ran

  • tests/test_text_quality.py and tests/test_replace_broken_text_layer.py at this head, with real Tesseract: 55 passed.
  • The head tests/test_text_quality.py with the 24889cee detector: 2 failed (bullet_agenda, bullet_slide), 46 passed.
  • A merge with current main (53c11a20): no conflicts. On the merge, test_text_quality.py, test_replace_broken_text_layer.py, test_cli.py, and test_options.py: 178 passed.
  • Run CI and Run Docs CI pass at this head.

What I did not run

  • make check and the full suite. I rely on CI for them.

Known limit, as accepted earlier: a broken page whose only damage is unmapped one-letter words at the start of lines is not flagged. The option is off by default.

I made no source, test, or reference-data changes.

@yashikam19

Copy link
Copy Markdown
Contributor Author

@nikos-livathinos @cau-git , this PR is ready for review. CI passes, there are no conflicts, and @geoHeil reviewed and approved the current head. The summary comment above has the analysis on ParseBench and DocLayNet. Could you take a look when you have time, and merge it if it looks good to you?

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

ai:ci-safe AI triage found no CI-safety concern (advisory) CLI issues related to the command line interface (CLI) enhancement New feature or request ocr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants