diff --git a/crates/pdf/src/import_lines.rs b/crates/pdf/src/import_lines.rs index d29b354e7..b880c74ab 100644 --- a/crates/pdf/src/import_lines.rs +++ b/crates/pdf/src/import_lines.rs @@ -153,6 +153,10 @@ fn block(nodes: &[Arc], lines: &HashMap) -> (usize, Opt /// the file's widths are rounded, or it moved a glyph a hair (`TJ`). const EVEN_TRACKING: f64 = 10.0; +/// How far the tracking of a wrapped line's spaces may stray (thousandths of an em, a fiftieth of +/// an em) and still be one value: each space the file draws beside a wider gap is tracked apart. +const SPACE_TRACKING_TOLERANCE: f64 = 20.0; + /// A line's spacing: the tracking after its letters, and how much more its spaces take (word /// spacing, `Tw`; `None` when it has none). `None` when its letters are tracked apart (set by /// hand), or its spaces more than `slack` apart. @@ -194,7 +198,7 @@ fn area_type(nodes: &[Arc], lines: &HashMap, b: &Block) // type's own. // A wrapped line's spaces may each be tracked apart, within a fiftieth of an em of each other. let spacings: Vec<(f64, Option)> = - texts.iter().map(|(t, f)| line_spacing(t, if f.wraps { 20.0 } else { EVEN_TRACKING })).collect::>()?; + texts.iter().map(|(t, f)| line_spacing(t, if f.wraps { SPACE_TRACKING_TOLERANCE } else { EVEN_TRACKING })).collect::>()?; let trackings: Vec = spacings.iter().map(|(t, _)| *t).collect(); let words: Vec> = spacings.iter().map(|(_, w)| *w).collect(); let wrapped: Vec = texts.iter().filter(|(_, f)| f.wraps).map(|(_, f)| f.length).collect(); diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index 4eca5d11a..dddb0075e 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -276,7 +276,7 @@ impl TextLine { self.column = true; // Letter-spaced Japanese is common: a gap reads as a space only when a whole em is left // out (the tracking set in `finish` keeps narrower gaps). - if step > size * 0.9 && !run.ends_with(' ') && !text.starts_with(' ') { + if step > size * 0.9 && !run.ends_with(is_space) && !text.starts_with(is_space) { run.push(' '); } if last.takes(look) { @@ -334,15 +334,16 @@ impl TextLine { if !on_line && !on_curve { return false; } - if !run.ends_with(' ') && !text.starts_with(' ') { + if !run.ends_with(is_space) && !text.starts_with(is_space) { self.gaps.push(Gap { at: bytes, width: gap, em: self.last_size.min(at.size), on_line }); } // A space the file draws between two glyphs spans the gaps on both sides of it. let drawn = self.drawn_space.take(); if on_line { - if text == " " && !run.ends_with(' ') { + let mut chars = text.chars(); + if chars.next().is_some_and(is_space) && chars.next().is_none() && !run.ends_with(is_space) { self.drawn_space = Some((bytes, gap + advance)); - } else if let Some((b, before)) = drawn.filter(|_| !text.starts_with(' ')) { + } else if let Some((b, before)) = drawn.filter(|_| !text.starts_with(is_space)) { self.drawn_spaces.push((b, before + gap)); } } @@ -904,12 +905,13 @@ fn tracked(runs: Vec, tracking: f64, spaces: &[(usize, f64)]) -> Vec, tracking: f64, spaces: &[(usize, f64)]) -> Vec bool { + matches!(c, ' ' | '\u{a0}' | '\u{2000}'..='\u{200a}' | '\u{202f}') +} + fn round(v: f64) -> f64 { (v * 1000.0).round() / 1000.0 } diff --git a/crates/pdf/src/tests_import_lines.rs b/crates/pdf/src/tests_import_lines.rs index 4e81666c0..ebb128857 100644 --- a/crates/pdf/src/tests_import_lines.rs +++ b/crates/pdf/src/tests_import_lines.rs @@ -226,6 +226,21 @@ fn words_set_ems_apart_beside_the_spaces_the_file_draws_keep_their_place() { } } +#[test] +fn words_set_ems_apart_beside_a_drawn_no_break_or_thin_space_keep_their_place() { + // As above, the file drawing a no-break space and a thin space alone, with a gap on each side. + let (x1, x2) = (48.0, 84.0); + let lines: &[(f32, f32, f32, &str)] = + &[(20.0, 50.0, 10.0, "ab"), (36.0, 50.0, 10.0, "\u{a0}"), (x1, 50.0, 10.0, "cd"), (70.0, 50.0, 10.0, "\u{2009}"), (x2, 50.0, 10.0, "ef")]; + let t = texts(&import(&source_sans(lines)).unwrap()); + assert_eq!(t.len(), 1); + assert_eq!(t[0].plain_text(), "ab\u{a0}cd\u{2009}ef"); + let laid = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t[0]); + let at = |byte: usize| (t[0].xf * laid.glyphs.iter().find(|g| g.byte == byte).unwrap().origin).x; + assert!((at(4) - f64::from(x1)).abs() < 0.05, "the second word starts where the file has it: {} vs {x1}", at(4)); + assert!((at(9) - f64::from(x2)).abs() < 0.05, "the third word starts where the file has it: {} vs {x2}", at(9)); +} + /// A document of area type `text` (Source Sans 3, 10 pt) `width` wide at (20, 30). fn area_doc(text: &str, justify: Justify, width: f64, space_before: f64) -> Document { let style = CharStyle { size: 10.0, fill: Paint::solid(Color::BLACK), ..Default::default() };