From 475f375ff418ef3b0565b57dda01828a21865b4c Mon Sep 17 00:00:00 2001 From: komo1iddin <45395275+komo1iddin@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:41:17 +0800 Subject: [PATCH] M7: keep a word on its line when its spacing differs by less than a thousandth of an em Joining runs into one line compared their character and word spacing to 1e-6. Word documents exported through macOS Quartz write 0.0001 Tc on some words, so a line like "for company AEROSOL" split into "for company A" and "EROSOL", two paragraphs that edit separately. Spacing below a thousandth of an em, the unit glyph widths are given in, can't be seen. Compare character and word spacing to that; scale and rise still have to match exactly, and visible spacing still keeps runs apart. Co-Authored-By: Claude Opus 5.5 --- crates/edit/src/tests.rs | 24 ++++++++++++++++++++++++ crates/edit/src/text.rs | 9 +++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 879ebc6c6..20fc7b23e 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -5218,3 +5218,27 @@ fn a_clipping_copy_on_the_same_baseline_is_refused() { assert!(matches!(&block, Err(EditError::Invalid(m)) if m.contains("clipping path")), "{block:?}"); assert_eq!(text::text_lines(&doc, 0).unwrap().len(), 2, "refused means untouched"); } + +/// A producer wrote `0.0001 Tc` on one word of a line (Word through macOS Quartz). That spacing +/// can't be seen, so the word stays on its line; spacing that can be seen still keeps runs apart. +#[test] +fn a_word_with_invisible_extra_spacing_stays_on_its_line() { + let page = |spacing: &str| { + text_page(&format!( + "BT /F1 12 Tf 1 0 0 1 72 700 Tm (for company ) Tj ET \ + BT /F1 12 Tf 1 0 0 1 141.36 700 Tm (A) Tj ET \ + BT {spacing} Tc /F1 12 Tf 1 0 0 1 149.364 700 Tm (EROSOL) Tj ET" + )) + }; + let blocks = text::text_blocks(&page("0.0001"), 0).unwrap(); + assert_eq!(blocks.iter().map(|b| b.text.as_str()).collect::>(), ["for company AEROSOL"]); + + let mut doc = page("0.0001"); + text::rewrite_block(&mut doc, 0, 0, Some("for company AEROSOL LLC"), &BlockStyle::default()).unwrap(); + let lines = text::text_lines(&reopen(&doc), 0).unwrap(); + assert_eq!(lines.iter().map(|l| l.text.as_str()).collect::>(), ["for company AEROSOL LLC"]); + assert!((lines[0].rect[0] - 72.0).abs() < 0.01, "{:?}", lines[0].rect); + + let blocks = text::text_blocks(&page("0.5"), 0).unwrap(); + assert_eq!(blocks.iter().map(|b| b.text.as_str()).collect::>(), ["for company A", "EROSOL"]); +} diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 267259217..49d27e31e 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -1203,12 +1203,17 @@ pub fn text_lines(doc: &Document, page: usize) -> Result, EditErro /// Joining independent text objects is safe only when they use the same text and graphics /// transforms and spacing. In particular, a nearby rotated label isn't part of this line. fn compatible_word(line: &TextLine, shown: &Shown) -> bool { - let same = |a: &[f64], b: &[f64]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-6); + let within = |a: &[f64], b: &[f64], tolerance: f64| a.iter().zip(b).all(|(a, b)| (a - b).abs() < tolerance); + let same = |a: &[f64], b: &[f64]| within(a, b, 1e-6); let (a, b) = (&line.origin.state, TextState::of(&shown.state)); + // Spacing that differs by less than a thousandth of an em, the unit glyph widths are given in, + // can't be seen. Some producers still write it (`0.0001 Tc` on one word of a line). + let em = shown.state.size.abs() / 1000.0; line.scope == shown.scope && same(&line.origin.tm[..4], &shown.tm.0[..4]) && same(&line.origin.ctm, &shown.state.ctm.0) - && same(&[a.char_spacing, a.word_spacing, a.scale, a.rise], &[b.char_spacing, b.word_spacing, b.scale, b.rise]) + && within(&[a.char_spacing, a.word_spacing], &[b.char_spacing, b.word_spacing], em.max(1e-6)) + && same(&[a.scale, a.rise], &[b.scale, b.rise]) && a.fill.len() == b.fill.len() && a.fill.iter().zip(&b.fill).all(|(a, b)| a.op == b.op && a.operands == b.operands) }