From 8a0e6a7d6f4636447f3245af509082f4e851a1e5 Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Mon, 16 Feb 2026 18:56:24 -0800 Subject: [PATCH] fix(extractor): Fix TJ space detection for Type3 fonts with non-standard scale Type3 fonts with FontMatrix [1,0,0,-1,0,0] (units_scale=1.0) had broken word spacing: intra-word kerns of -4000 exceeded the clamped threshold of 200, producing "Op en-Domain", "comp etition", etc. Two fixes: - Compute space_width fallback after units_scale is known; for non-standard scales, estimate from average glyph width (~45%) instead of the hardcoded 250 (calibrated for 1/1000-unit fonts) - Remove upper clamp on space threshold (200 was too low for fonts where TJ values naturally range into thousands) Co-Authored-By: Claude Opus 4.6 --- src/extractor.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/extractor.rs b/src/extractor.rs index 76e0392..647c857 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -154,11 +154,6 @@ fn parse_simple_font_widths( widths.insert(code, w); } - // If space width wasn't found in the table, use a reasonable default - if space_width == 0 { - space_width = 250; - } - // Determine units_scale: for Type3 fonts, use FontMatrix[0]; for others, use 1/1000 let units_scale = if let Ok(fm) = font_dict.get(b"FontMatrix") { if let Some(arr) = resolve_array(doc, fm) { @@ -178,6 +173,20 @@ fn parse_simple_font_widths( 0.001 // Standard 1000-unit system }; + // If space width wasn't found in the table, estimate from font metrics. + // The default of 250 is calibrated for standard 1000-unit fonts (units_scale=0.001). + // For Type3 fonts with different coordinate systems, use average glyph width instead. + if space_width == 0 { + if !widths.is_empty() && (units_scale - 0.001).abs() > 0.0005 { + // Non-standard scale: estimate space as ~45% of average glyph width + let sum: u32 = widths.values().map(|&w| w as u32).sum(); + let avg = sum as f32 / widths.len() as f32; + space_width = (avg * 0.45).max(1.0) as u16; + } else { + space_width = 250; + } + } + Some(FontWidthInfo { widths, default_width: 0, @@ -1190,7 +1199,7 @@ fn extract_page_text_items( let space_threshold = if let Some(font_info) = font_info { let space_em = font_info.space_width as f32 * font_info.units_scale; let threshold = space_em * 1000.0 * 0.4; - threshold.clamp(80.0, 200.0) + threshold.max(80.0) } else { 120.0 }; @@ -1654,7 +1663,7 @@ fn extract_form_xobject_text( let space_threshold = if let Some(fi) = font_info { let space_em = fi.space_width as f32 * fi.units_scale; let threshold = space_em * 1000.0 * 0.4; - threshold.clamp(80.0, 200.0) + threshold.max(80.0) } else { 120.0 };