fix(extract): reverse visual-order RTL text using glyph geometry
Problem
Base-Hebrew/Arabic text stored in visual (screen left-to-right) order extracts character-reversed. The only visual→logical reversal is gated on Arabic presentation forms (U+FB50–FDFF / U+FE70–FEFE), which base-alphabet RTL never triggers.
Example (public filing, FIBI risk report): the page-1 title extracted as םינוכיסה לע חוד instead of דוח על הסיכונים.
Why geometry, not codepoints
PDF paints glyphs sequentially with no BiDi, so two storage conventions coexist in the wild:
- Visual order (shaped, visible text): each show op holds its run in screen order — reversed relative to reading order — and runs paint left-to-right across the line. Must be reversed.
- Logical order (typically OCR text layers): each op holds one reading-order word, ops positioned right-to-left. Must not be reversed — a codepoint-only trigger corrupts these.
Fix
- Multi-character RTL runs without presentation forms become reversal candidates at show-op decode time (
Tj/TJ/', both the page parser and the Form-XObject parser,ExtractedText::append_intokeeps form indexes in sync). - A per-page geometric vote decides the convention: same-baseline emission direction between candidate runs (left-to-right ⇒ visual, right-to-left ⇒ logical). Mirrored text matrices and backward
TJjumps count as logical evidence; rotated matrices stay neutral. Ties default to reverse — visible RTL painted with forward advances can only render correctly from visual storage. - Reversal keeps combining marks (nikud/harakat) attached to their base and mirrors paired brackets, since visual storage records the on-screen mirrored glyph.
- Item-level BiDi for RTL line/cell assembly: embedded LTR phrases (Latin acronyms, page numbers, Roman numerals) keep screen order via
restore_embedded_ltr_runs, applied insort_line_items,merge_text_items(before merging bakes the concatenation in), and the struct/rect/heuristic table-cell joins with baseline partitioning. Edge neutrals join a phrase only when they carry a bracket; sentence periods and commas stay in the RTL flow.
Testing
- Unit tests: candidate classification, vote directions (rightward reverses, leftward preserved, backward-jump
TJ, cross-line neutrality), mark-preserving/bracket-mirroring reversal, embedded-LTR restoration. - New fixtures
hebrew_visual_order.pdf/hebrew_logical_order.pdfencode the same two lines under each storage convention; both must extract to identical logical-order Markdown. - Full snapshot corpus: non-RTL output is byte-identical; the two RTL documents in the corpus improve (un-reversed page numbers,
IIIV→VIII,)(IASB→(IASB),coD 8400→Doc 8400). External TEDS/NID/MHS benchmark unchanged.
🤖 Generated with Claude Code
Summary by cubic
Fixes character-reversed extraction of base Hebrew/Arabic stored in visual screen order by reversing only visual-order RTL runs based on glyph geometry. Logical-order (OCR-style) runs stay intact; Arabic‑Indic digits and numeric separators keep forward order; embedded LTR phrases render in screen order.
- Detect candidates as multi‑char RTL without Arabic presentation forms; vote per page from same‑baseline emission when horizontal advance dominates (|dx| > |dy|). Mirrored text matrices and true backward
TJbacktracks (once per op) add logical votes; rotated matrices are neutral; ties reverse. - Reverse before merge; keep combining marks attached and mirror paired brackets. In
merge_text_items, restore embedded LTR before concatenation bakes order in. - Preserve embedded LTR and rejoin split Arabic‑Indic numbers (bridging ASCII/Arabic decimal/thousands delimiters); edge neutrals join only when bracketed. Apply via
restore_embedded_ltr_runsin line assembly and viasort_rtl_cell_itemsfor RTL table cells. - Tables: use letters‑only
is_rtl_textfrommain; sort RTL cells by baseline bands top‑to‑bottom and right‑to‑left within a band; digit‑only cells keep number order. - Forms: accumulate candidate indexes and logical votes across nested Form XObjects with
ExtractedText.append_into. - Tests/fixtures: add Hebrew visual/logical PDFs and snapshots; unit tests cover candidate classification, rotation neutrality, once‑per‑op backtracks, banded RTL cell sort, mark‑aware reversal, delimiter bridging, bracket mirroring, and Arabic‑Indic digits/separators. Non‑RTL snapshots unchanged; RTL outputs improve.