Fix Hebrew visual-order extraction for per-glyph OCR text layers (fixes #826) - #828
Fix Hebrew visual-order extraction for per-glyph OCR text layers (fixes #826)#828palmoni5 wants to merge 3 commits into
Conversation
…doseev#826) Scanned-Hebrew PDFs whose OCR text layer emits one show op per glyph came out character-reversed: the per-glyph spans carry no storage-order information, the reading-order sort arranges them x-ascending (visual order), and no downstream pass reversed the result — the Pass 0 gate requires Arabic presentation forms, which Hebrew never has. Two changes: 1. extractors/text.rs — merge_adjacent_spans now tracks whether the accumulated span was built entirely from >=2 single-glyph RTL pieces. Such a concatenation is the visual left-to-right sequence by construction, so finalize_visual_rtl_glyph_span reverses it to logical order, keeping digit runs forward (UAX yfedoseev#9 L2) and combining marks attached to their base. Build-time-corrected multi-char show strings and Latin/CJK spans are untouched. 2. document.rs — the short-span run merge in reverse_rtl_visual_order_runs moved BEFORE the Pass 0.5 word-order reversal (as "Pass 0.75") and now emits logical characters directly (atomic groups: base+marks, 2-char build-time pieces, digit runs). Previously Pass 0.5 reversed the run's span order and the old Pass 1 then merged in reverse span order — two reversals that cancelled out and leaked visual-order text (letter-spaced Hebrew running heads were the visible symptom). The old Pass 1 is deleted; Pass 0.75 subsumes it. Verification on a 78-page scanned Talmud volume (Tractate Beitza, tesseract-style one-glyph-per-Tj layer): per-page unique-token containment vs PDFium went from 4.8% to 91%, zero reversed words remain (previously ~95% of all Hebrew was reversed), and the full library test suite (5,714 tests) passes unchanged. Fixes yfedoseev#826. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Thank you — and a regression found: real Hebrew/Arabic PDFs now come out reversed via
|
|
Thank you — this is a fair catch, and the 419-doc sweep gating is exactly the safety net this kind of change needs. I dug into the fix direction you suggested and hit two empirical dead-ends I want to share, because they narrow the problem in a useful way and I think I now need one thing from your corpus to finish it. Render mode (
|
Pass 0.75 (merge-and-reverse for per-glyph scanned-OCR Hebrew/Arabic text) fired on any run of 4+ short (<=2-char) same-line RTL spans, not just genuine one-glyph-per-show-op runs. Ordinary two-letter spans from routine font kerning-pair/subsetting-run boundaries in digitally-authored PDFs satisfy the same "<=2 chars" test and got merged and reversed as if they were scanned OCR text. Tightened the run-membership gate to require exactly one base RTL letter per span (mirrors extractors/text.rs's already-correct is_rtl_glyph_piece), matching the fix's own stated intent. New tests reproduce both the original regression (fixed) and a second, deeper problem the fix doesn't solve, with a precise description of what's still wrong — see the test file's module doc.
|
Thanks for the careful root-causing and the honest write-up above — the render-mode and draw-direction dead-ends you shared saved me from repeating them. Pushed a commit onto this branch: one real fix, plus a fixture test file documenting a second, deeper problem I found but haven't solved. Fixed:
|
yfedoseev
left a comment
There was a problem hiding this comment.
🔴 This can't merge yet — the branch leaves a failing test
@palmoni5 thanks again for the deep root-causing. The document.rs Pass-0.75 gate tightening is a strict improvement. But the branch currently has a committed, non-#[ignore]d failing test — reproducible with:
cargo test --test rtl_short_span_glyph_granularity
# ordinary_two_char_spans_are_not_reversed FAILS:
# got "חז\nוה\nדג\nבא" expected "אבגדהוזח"So cargo test is red on this branch (the reversal comes from a pre-existing flush_tj_buffer + Pass-0.5 path, not Pass 0.75).
Separately, the net-new finalize_visual_rtl_glyph_span (src/extractors/text.rs) reverses any span merged from ≥2 single-glyph RTL pieces without a text-render-mode (Tr) gate — unlike main's sibling per-word fix — so it can reverse already-correct non-OCR Hebrew/Arabic (the 419-doc sweep note in rtl_tj_array_word_buffer.rs documents this).
Suggested path to green: split the sound document.rs gate from the risky text.rs finalize; #[ignore] the known-failing test with a tracking issue, or Tr-gate finalize_visual_rtl_glyph_span.
|
Status check on this one, re-verified today against current
Nothing needed from you beyond the rebase right now — the outstanding design question is on our side, and I've asked @yfedoseev whether that documented test should be resolved or marked |
Fixes #826.
Problem
Scanned-Hebrew PDFs whose OCR text layer emits one show op per glyph (tesseract-style layers — an enormous corpus of scanned Hebrew religious texts) come out character-reversed from
extract_text. Root cause chain:detect_visual_order_run, theaccumulated_widthheuristic) never fire — each span is a single character.merge_adjacent_spanswalks the reading-order-sorted (x-ascending) spans and concatenates them — producing the visual left-to-right sequence for RTL text.reverse_rtl_visual_order_runsis gated on Arabic Presentation Forms (Hebrew has none), and the short-span runs that Pass 1 targets no longer exist — the merge already collapsed them.Fix
extractors/text.rs—merge_adjacent_spanstracks whether the accumulated span was built entirely from ≥2 single-glyph RTL pieces. Such a concatenation is the visual sequence by construction (regardless of how the producer stored the text), sofinalize_visual_rtl_glyph_spanreverses it to logical order — keeping digit runs forward (UAX fix: width calculation #9 rule L2) and combining marks attached to their base. Build-time-corrected multi-char show strings and Latin/CJK spans are untouched.document.rs— the short-span run merge moved before Pass 0.5 (as "Pass 0.75") and emits logical characters directly, using atomic groups (base+marks, 2-char build-time pieces kept in order, folded digit runs). This removes the Pass 0.5 ↔ Pass 1 cancellation; the old Pass 1 is deleted as subsumed.Verification
On a 78-page scanned Talmud volume (Tractate Beitza, one-glyph-per-Tj OCR layer, ~9,000 spans/page):
הציב ןושאר קרפביצה פרק ראשון✓The full test suite (5,714 tests) passes unchanged. The behavior was consistent across a 17-volume sample (containment 90–92% on every volume).
Happy to add a regression fixture if you can point me at the preferred format for golden corpus additions — the repro PDF from #826 is freely redistributable.
🤖 Generated with Claude Code