Skip to content

Commit 36e38ef

Browse files
artizclaude
andcommitted
feat(pdf): docling-parse migration phase 1 — char-extraction parity + tooling
Groundwork for porting docling-parse's text reconstruction (the path to higher PDF conformance). Two pieces: - Lam-alef ligature order: pdfium splits the Arabic lam-alef ligature into two chars at the *same* x in visual order (`alef-variant, lam`); docling-parse and logical order are `lam, alef-variant`. Reorder same-x ligature pairs at the glyph level. The shared-x test reliably distinguishes a real ligature from a genuine `alef + lam` sequence (article `ال`, `فعالة`) whose glyphs sit at different x — unlike the earlier ambiguous mid-word heuristic. - Debug tooling: `pdfium_backend::debug_glyphs` + `examples/dump_chars` dump the raw pdfium char stream (codepoint + x) to diff against docling-parse's char cells; `pdfium_backend` is now `pub` for inspection. Verified char-extraction parity against docling-parse: both emit chars in content-stream order (visual for RTL), and with the ligature fix the lam-alef order matches. The EXACT 3 stay exact; Arabic-only, no other regressions. Next (phase 2): port `cells.h`'s 3-pass line contraction (corner-distance adjacency + merge_with space insertion, LTR/RTL/LTR-reverse) to replace the ad-hoc reconstruction — targets multi_page (run-boundary colons) and the RTL PDFs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7bf2caa commit 36e38ef

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Dump pdfium's raw char stream (codepoint + x) for a page, to compare char
2+
//! extraction against docling-parse. Usage: `... --example dump_chars -- file.pdf`
3+
fn main() {
4+
let path = std::env::args().nth(1).expect("usage: dump_chars <pdf>");
5+
let bytes = std::fs::read(&path).expect("read");
6+
let glyphs = fleischwolf_pdf::pdfium_backend::debug_glyphs(&bytes, 0);
7+
println!("pdfium CHAR order (first 16):");
8+
for (ch, l, _b, _r, _t) in glyphs.iter().take(16) {
9+
println!(" {:?} U+{:04X} xl={:.1}", ch, *ch as u32, l);
10+
}
11+
}

crates/fleischwolf-pdf/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod assemble;
1313
pub mod layout;
1414
mod mets;
1515
mod ocr;
16-
mod pdfium_backend;
16+
pub mod pdfium_backend;
1717
pub mod resample;
1818
pub mod tableformer;
1919

crates/fleischwolf-pdf/src/pdfium_backend.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,34 @@ impl Drop for FfiText<'_> {
237237
/// pdfium emits these on most lines and they pin word splits exactly. Hard line
238238
/// breaks are dropped (line structure comes from geometry); the gap heuristic in
239239
/// [`lines_from_glyphs`] is the fallback for the lines pdfium leaves space-less.
240+
/// Debug helper: the raw pdfium glyph stream (codepoint + native bottom-left
241+
/// box) for a page, in pdfium's character order. For comparing against
242+
/// docling-parse's char cells.
243+
pub fn debug_glyphs(bytes: &[u8], index: i32) -> Vec<(char, f32, f32, f32, f32)> {
244+
let Ok(pdfium) = bind() else {
245+
return Vec::new();
246+
};
247+
let ffi = FfiText::load(pdfium.bindings(), bytes, None);
248+
if ffi.doc.is_null() {
249+
return Vec::new();
250+
}
251+
let b = ffi.bindings;
252+
let page = b.FPDF_LoadPage(ffi.doc, index);
253+
if page.is_null() {
254+
return Vec::new();
255+
}
256+
let tp = b.FPDFText_LoadPage(page);
257+
let mut out = Vec::new();
258+
if !tp.is_null() {
259+
for g in glyphs(b, tp) {
260+
out.push((g.ch, g.l, g.b, g.r, g.t));
261+
}
262+
b.FPDFText_ClosePage(tp);
263+
}
264+
b.FPDF_ClosePage(page);
265+
out
266+
}
267+
240268
fn glyphs(b: &dyn PdfiumLibraryBindings, tp: FPDF_TEXTPAGE) -> Vec<Glyph> {
241269
let n = b.FPDFText_CountChars(tp);
242270
let mut out = Vec::with_capacity(n.max(0) as usize);
@@ -270,6 +298,23 @@ fn glyphs(b: &dyn PdfiumLibraryBindings, tp: FPDF_TEXTPAGE) -> Vec<Glyph> {
270298
t: top as f32,
271299
});
272300
}
301+
// pdfium splits the Arabic lam-alef ligature into two chars at the *same* x
302+
// (it's one glyph) in visual order — `alef-variant, lam`. docling-parse and
303+
// logical order are `lam, alef-variant`. Detect the ligature by the shared x
304+
// and swap. The shared-x test reliably distinguishes a true ligature from a
305+
// genuine `alef + lam` sequence (the article `ال`, or `فعالة`), whose two
306+
// glyphs sit at different x and must NOT be reordered.
307+
for i in 0..out.len().saturating_sub(1) {
308+
let same_x = out[i].l.is_finite()
309+
&& out[i + 1].l.is_finite()
310+
&& (out[i].l - out[i + 1].l).abs() < 1.0;
311+
if same_x
312+
&& matches!(out[i].ch, '\u{0622}' | '\u{0623}' | '\u{0625}' | '\u{0627}')
313+
&& out[i + 1].ch == '\u{0644}'
314+
{
315+
out.swap(i, i + 1);
316+
}
317+
}
273318
out
274319
}
275320

0 commit comments

Comments
 (0)