feat: honor /ActualText replacement text of marked-content spans#300
Open
wittjeff wants to merge 1 commit into
Open
feat: honor /ActualText replacement text of marked-content spans#300wittjeff wants to merge 1 commit into
wittjeff wants to merge 1 commit into
Conversation
Contributor
|
✅ DCO Check Passed Thanks @wittjeff, all your commits are properly signed off. 🎉 |
Contributor
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
/ActualText (PDF 32000-1, 14.9.4) declares the exact Unicode of a glyph run whose glyph->Unicode mapping cannot be recovered from the font encoding alone: composed accents, ligatures, discretionary hyphens, small-caps fonts. BMC/BDC/EMC were no-ops, so the declared text was discarded and the raw glyph codes decoded instead - a zero-advance caron at code 0x4D turned 'Orešič' into 'OresMi' (docling#3783). The stream decoder now tracks a marked-content stack: BDC captures /ActualText from inline property dictionaries, EMC substitutes it into the cells drawn inside the span (1:1 when character and cell counts match, merge-with-union-geometry otherwise, cell removal for empty replacement text). Substitution is anchored to drawn text cells - spans over non-text content are skipped - and implausibly long replacement strings (descriptive misuse of the key) are rejected. The behavior sits behind a new apply_actual_text config flag, default on. Fixes docling-project/docling#3783 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Jeff Witt <152964771+witt3rd@users.noreply.github.com>
wittjeff
force-pushed
the
feat/actualtext-replacement
branch
from
July 14, 2026 19:48
9ed7de6 to
7b26aa8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes docling-project/docling#3783.
Problem
/ActualTextmarked-content spans (PDF 32000-1, §14.9.4) were parsed but ignored:BMC/BDC/EMCwere no-ops inpdf_decoder<STREAM>. When a producer wraps a glyph run in/ActualTextto declare its true Unicode — composed accents, ligatures, discretionary hyphens — docling-parse decoded the raw glyph codes instead.The character-level corruption in docling#3783: a zero-advance caron drawn at code
0x4D(ASCIIM) over ans, wrapped in/Span <</ActualText <FEFF0161>>>(š), extracts assMinstead ofš—OrešičbecomesOresMi. The declaredWinAnsiEncodinglegitimately maps0x4D → M, so only the/ActualTextspan can recover the intended text; pypdfium2, poppler, and Acrobat all honor it and extractOreši.Fix
pdf_decoder<STREAM>now tracks a marked-content stack:BMC/BDCpush an entry recordingpage_cells.size();BDCinspects an inline properties dictionary for/ActualText(qpdf'sgetUTF8Value()handles the UTF-16BE/PDFDoc string decoding).EMCpops and substitutes the replacement text into the cells created inside the span:/ActualText(purely visual content, e.g. a discretionary hyphen) → span cells are removed.The stack is per-decoder, matching the spec requirement that BDC/EMC balance within a single content stream; cells drawn by nested form XObjects land in the shared
page_cellsand are covered by the recorded start index.Guardrails (trust boundaries)
/ActualTextis replacement text by definition, but to keep misuse (e.g./Alt-style descriptions in the wrong key) from injecting text:/ActualTextover pure graphics (the alternate-text-like usage) has no anchor cell and is skipped with a log line;/Properties) are not resolved in this PR (rare for/ActualText, which is content-specific by nature) — logged at INFO;apply_actual_textconfig flag (default true), exposed inDecodePageConfig(pybind) andDecodeConfig(Python), so the raw glyph decoding remains one switch away.Render-layer text instructions are intentionally untouched: rendering shows the glyphs as drawn.
Tests
tests/test_actual_text.pybuilds the PDFs in memory (base-14 Helvetica + a non-embedded zero-advance "caron" TrueType font, no new test assets):Oreši, the docling#3783 case) and theapply_actual_text=Falseescape hatch (OresMi);/ActualTextremoves the span's cells;/ActualTextis rejected, glyphs kept;Verified against the full synthetic reproducer from docling#3783: docling-parse now extracts
Oreši, byte-identical to the pypdfium2 control. Full test suite passes.Groundtruth impact (needs a regression-dataset revision bump)
7 documents in
docling-project/regression-dataset-for-docling-parsecontain/ActualTextspans and change extraction output (verified individually against a pypdfium2 control; all are the document's own declared replacement text):fillable_form.pdfconSignee→consignee,SIGNATURE & STATUS OF AUThORIzED PERSON→Signature & Status of Authorized Personcropbox_versus_mediabox_01.pdf\x08) from icon-font codes replaced by the declared tab/space (matches pdfium)complex_invisible_fonts_04.pdf,complex_invisible_fonts_05.pdftype3_fonts.pdffont_11.pdf(p2),indexed_device_n.pdfRegenerating with
pytest --update-groundtruthon this branch turns the suite green (3 previously-failing reference tests pass, 89 others unchanged). Happy to provide the regenerated fixture files, or they can be regenerated directly from this branch, following the flow used in #296.