diff --git a/crates/edit/src/text_regions/ocr_words.rs b/crates/edit/src/text_regions/ocr_words.rs index e3e32691ac..d511ccdf47 100644 --- a/crates/edit/src/text_regions/ocr_words.rs +++ b/crates/edit/src/text_regions/ocr_words.rs @@ -183,7 +183,8 @@ fn drawn_right(before: &[TextLine], indices: &[usize], after: &[TextLine]) -> Re } /// Rewrite each change of `plan` in `doc`, last first: a rewrite goes where its words were or at -/// the end of the page, so the words before it keep their indexes. Gives how far right each drew. +/// the end of the page, so the words before it keep their indexes (an error if they don't). Gives +/// how far right each drew. fn apply( doc: &mut Document, page: usize, @@ -198,9 +199,17 @@ fn apply( let mut style = BlockStyle { align: Some(crate::added::Align::Left), changed_words_only: false, ..style.clone() }; let mut rights = vec![None; plan.len()]; let mut substituted = None; + let original = word_lines(doc, page)?; for (at, change) in plan.iter().enumerate().rev() { let indices = members.get(change.runs.clone()).ok_or_else(|| EditError::Invalid("the phrase has no such words".into()))?; let before = word_lines(doc, page)?; + // The words up to the next that stays must still be where they were. A rewrite also takes + // away copies of its words, and a copy earlier on the page moves these up the list. + let up_to = change.runs.end.saturating_add(1).min(members.len()); + let same = |index: &usize| before.get(*index).map(key) == original.get(*index).map(key); + if !members.get(..up_to).unwrap_or_default().iter().all(same) { + return Err(EditError::Invalid("rewriting these words alone would lose track of the words before them".into())); + } let part = stretch(phrase, &before, indices)?; // Relative to the scale the words have, which a layer may have narrowed already. let own_scale = indices.first().and_then(|i| before.get(*i)).map_or(1.0, |line| line.origin.state.scale); diff --git a/crates/engine/tests/it/ocr_edit.rs b/crates/engine/tests/it/ocr_edit.rs index e636088bf5..078815e172 100644 --- a/crates/engine/tests/it/ocr_edit.rs +++ b/crates/engine/tests/it/ocr_edit.rs @@ -527,6 +527,25 @@ fn spacing_that_has_to_be_drawn_redraws_the_phrase() { assert!(drawn.join("").contains("Jon Smy"), "the typed spaces are there: {drawn:?}"); } +/// A word drawn twice, its copy earlier in the layer: rewriting the word takes the copy with it, and +/// the words after the copy move up the list. The next change must not land on the wrong word. +#[test] +fn a_change_lands_on_its_own_word_when_a_rewrite_removes_an_earlier_copy() { + let bytes = under_scan( + "BT /F1 12 Tf 3 Tr 1 0 0 1 84 44 Tm (Jones) Tj 1 0 0 1 20 44 Tm (John) Tj 1 0 0 1 50 44 Tm (Smith) Tj 1 0 0 1 84 44 Tm (Jones) Tj ET", + ); + let mut s = Session::new(); + let id = s.open("copy.pdf", None, bytes, None).unwrap(); + let block = texts(&s, id).iter().position(|t| t == "John Smith Jones").expect("the phrase, after its copy"); + // John goes. A deletion draws nothing, so nothing else would notice it landing on Smith. + edit_block(&mut s, id, block, "Smith Joness"); + let text = s.get(id).unwrap().text_lines(0).iter().map(|l| l.text.as_str()).collect::>().join(" "); + let words: Vec<&str> = text.split_whitespace().collect(); + assert!(words.contains(&"Smith"), "Smith wasn't changed: {words:?}"); + assert!(!words.contains(&"John"), "John is the word that goes: {words:?}"); + assert!(words.contains(&"Joness"), "{words:?}"); +} + /// A layer whose words are already narrowed (50 Tz): squeezing a replacement narrows it further, /// rather than setting a scale wider than the one it had and running it into the next word. #[test]