fix(extract): reverse visual-order RTL text using glyph geometry

Merged
AB
2mo ago
+988-57

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_into keeps 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 TJ jumps 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 in sort_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.pdf encode 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 TJ backtracks (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_runs in line assembly and via sort_rtl_cell_items for RTL table cells.
  • Tables: use letters‑only is_rtl_text from main; 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.