Skip to content

Commit b60a5c5

Browse files
artizclaude
andcommitted
feat(pdf): docling-parse migration phase 3.2 — recompose decomposed ligatures
pdfium decomposes a single ligature glyph (Latin fi/ffi, Arabic lam-alef) into several chars at the *same* loose box. The contraction's gap is a Euclidean distance (always positive), so the zero-width overlap read as a real gap and inserted spurious intra-word spaces (`conf iguration`, `di f f i cult`). Recompose them: consecutive glyphs sharing a loose box are appended into one cell, exactly like docling-parse keeps the ligature whole. This recovers 2305-pg9 to EXACT (was 0→4) and RTL_01 to 2, making the dp path net-positive with no regression vs legacy: 2203 309→285, 2206 200→168, multi_page 54→22, normal_4pages 108→82, redp5110 300→252, amt 16→14, table_mislabeled 113→111; the 3 EXACT PDFs stay exact. Still behind the flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dd95c83 commit b60a5c5

3 files changed

Lines changed: 63 additions & 45 deletions

File tree

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
//! Dump pdfium's raw char stream (codepoint + x + font hash) for a page.
2-
//! Usage: `... --example dump_chars -- file.pdf`
1+
//! Dump pdfium's raw char stream (codepoint + loose left/right) for a page,
2+
//! optionally filtered to a substring window. Usage:
3+
//! ... --example dump_chars -- file.pdf [needle]
34
fn main() {
4-
let path = std::env::args().nth(1).expect("usage: dump_chars <pdf>");
5+
let path = std::env::args()
6+
.nth(1)
7+
.expect("usage: dump_chars <pdf> [needle]");
8+
let needle = std::env::args().nth(2);
59
let bytes = std::fs::read(&path).expect("read");
610
let glyphs = fleischwolf_pdf::pdfium_backend::debug_glyphs(&bytes, 0);
7-
println!("pdfium CHAR order (ch / x / font-hash):");
8-
for (ch, l, font) in glyphs.iter().take(40) {
9-
println!(
10-
" {:?} U+{:04X} xl={:.1} font={}",
11-
ch, *ch as u32, l, font
12-
);
11+
let text: String = glyphs.iter().map(|(c, _, _)| *c).collect();
12+
let start = needle
13+
.as_deref()
14+
.and_then(|n| text.find(n))
15+
.map(|b| text[..b].chars().count())
16+
.unwrap_or(0);
17+
println!("pdfium chars (ch / loose-left / loose-right / gap-to-prev):");
18+
let mut prev_r = f32::NAN;
19+
for (ch, l, r) in glyphs.iter().skip(start).take(20) {
20+
let gap = l - prev_r;
21+
println!(" {:?} l={:7.2} r={:7.2} gap={:+.2}", ch, l, r, gap);
22+
prev_r = *r;
1323
}
1424
}

crates/fleischwolf-pdf/src/dp_lines.rs

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -186,41 +186,49 @@ fn contract(cells: &mut Vec<Cell>) {
186186

187187
/// Build line cells from a page's glyph stream via the docling-parse contraction.
188188
pub(crate) fn line_cells(glyphs: &[Glyph], page_h: f32) -> Vec<TextCell> {
189-
let mut cells: Vec<Cell> = glyphs
190-
.iter()
191-
.filter_map(|g| {
192-
// Use the loose box (uniform font ascent/descent + advance) so adjacent
193-
// glyphs share a top edge, matching docling-parse's `compute_rect`.
194-
if !g.ll.is_finite() {
195-
return None;
196-
}
197-
// Drop *degenerate* space glyphs (zero-width loose box): pdfium's
198-
// generated spaces get a zero-width box at the wrong baseline that
199-
// breaks corner-distance adjacency. Without them the inter-word gap
200-
// drives `merge_with`'s space insertion. Spaces with a real width are
201-
// kept (they carry justified double-space information).
202-
if g.ch == ' ' && (g.lr - g.ll).abs() < 0.5 {
203-
return None;
189+
let mut cells: Vec<Cell> = Vec::new();
190+
for g in glyphs {
191+
// Use the loose box (uniform font ascent/descent + advance) so adjacent
192+
// glyphs share a top edge, matching docling-parse's `compute_rect`.
193+
if !g.ll.is_finite() {
194+
continue;
195+
}
196+
// Drop *degenerate* space glyphs (zero-width loose box): pdfium's generated
197+
// spaces get a zero-width box at the wrong baseline that breaks the
198+
// corner-distance adjacency. Without them the inter-word gap drives
199+
// `merge_with`'s space insertion. Spaces with a real width are kept (they
200+
// carry justified double-space information).
201+
if g.ch == ' ' && (g.lr - g.ll).abs() < 0.5 {
202+
continue;
203+
}
204+
// Recompose a ligature: pdfium decomposes one font glyph (Latin fi/ffi,
205+
// Arabic lam-alef) into several chars at the *same* loose box. Append them
206+
// into one cell so the contraction never inserts a space inside it.
207+
if let Some(last) = cells.last_mut() {
208+
if (last.rx0 - g.ll as f64).abs() < 0.5 && (last.rx1 - g.lr as f64).abs() < 0.5 {
209+
last.text.push(g.ch);
210+
last.ltr = !is_right_to_left(&last.text);
211+
continue;
204212
}
205-
let text = g.ch.to_string();
206-
let ltr = !is_right_to_left(&text);
207-
Some(Cell {
208-
text,
209-
rx0: g.ll as f64,
210-
ry0: g.lb as f64,
211-
rx1: g.lr as f64,
212-
ry1: g.lb as f64,
213-
rx2: g.lr as f64,
214-
ry2: g.lt as f64,
215-
rx3: g.ll as f64,
216-
ry3: g.lt as f64,
217-
ltr,
218-
active: true,
219-
lig_carry: false,
220-
font: g.font,
221-
})
222-
})
223-
.collect();
213+
}
214+
let text = g.ch.to_string();
215+
let ltr = !is_right_to_left(&text);
216+
cells.push(Cell {
217+
text,
218+
rx0: g.ll as f64,
219+
ry0: g.lb as f64,
220+
rx1: g.lr as f64,
221+
ry1: g.lb as f64,
222+
rx2: g.lr as f64,
223+
ry2: g.lt as f64,
224+
rx3: g.ll as f64,
225+
ry3: g.lt as f64,
226+
ltr,
227+
active: true,
228+
lig_carry: false,
229+
font: g.font,
230+
});
231+
}
224232
contract(&mut cells);
225233
cells
226234
.into_iter()

crates/fleischwolf-pdf/src/pdfium_backend.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ impl Drop for FfiText<'_> {
259259
/// Debug helper: the raw pdfium glyph stream (codepoint + native bottom-left
260260
/// box) for a page, in pdfium's character order. For comparing against
261261
/// docling-parse's char cells.
262-
pub fn debug_glyphs(bytes: &[u8], index: i32) -> Vec<(char, f32, u64)> {
262+
pub fn debug_glyphs(bytes: &[u8], index: i32) -> Vec<(char, f32, f32)> {
263263
let Ok(pdfium) = bind() else {
264264
return Vec::new();
265265
};
@@ -276,7 +276,7 @@ pub fn debug_glyphs(bytes: &[u8], index: i32) -> Vec<(char, f32, u64)> {
276276
let mut out = Vec::new();
277277
if !tp.is_null() {
278278
for g in glyphs(b, tp, true) {
279-
out.push((g.ch, g.l, g.font));
279+
out.push((g.ch, g.ll, g.lr));
280280
}
281281
b.FPDFText_ClosePage(tp);
282282
}

0 commit comments

Comments
 (0)