From c67c44bf2156ce09eccc1d9ebd994f1de56f293d Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:55:14 +0800 Subject: [PATCH] M10: applying recognized words refreshes only the page content they add (#6310) (#6316) --- crates/engine/src/history.rs | 6 ++++- crates/engine/src/lib.rs | 5 +++- crates/engine/src/session/editing.rs | 2 ++ crates/engine/src/test_hooks.rs | 14 ++++++++++ crates/engine/src/tests.rs | 39 ++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/history.rs b/crates/engine/src/history.rs index 6b204ec372..04d848eb8d 100644 --- a/crates/engine/src/history.rs +++ b/crates/engine/src/history.rs @@ -95,7 +95,11 @@ pub(crate) fn scope_of(edit: &Edit) -> Scope { | Edit::AddHeaderFooter { .. } | Edit::AddWatermark { .. } | Edit::AddBackground { .. } - | Edit::RemoveMarks { .. } => Scope::Content, + | Edit::RemoveMarks { .. } + // Recognized text is an invisible text layer stamped on its page, with its font in the + // page's resources (#6310): a full re-inspection made applying one word cost seconds on + // a large file. + | Edit::AddOcrText { .. } => Scope::Content, Edit::Batch { edits, .. } => { let mut scopes = edits.iter().map(scope_of); let first = scopes.next().unwrap_or(Scope::Full); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c6272a0abf..ce0bb5c784 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -161,7 +161,10 @@ pub use session::Session; pub use sources::{CombineSource, Combined, combine_source_check, source_page_count}; use sources::{open_source, open_source_with}; #[cfg(test)] -use test_hooks::{check_refresh_panic, panic_on_refresh, record_link_scan, record_marker_scan, reset_secondary_scan_counts, secondary_scan_counts}; +use test_hooks::{ + check_refresh_panic, panic_on_refresh, record_full_refresh, record_link_scan, record_marker_scan, reset_secondary_scan_counts, + secondary_scan_counts, take_full_refreshes, +}; use xfa_form::{ XFA_REMOVED_PREFIX, XFA_SCRIPTS_OFF, decode_data_file, image_content_type, is_xfa_data_file, note_warnings, strip_xfa, sync_xfa_state, undrawn_field_warnings, xfa_after_data_change, xfa_layout, xfa_redraw, xfa_sync_datasets, xfa_values_from_datasets, diff --git a/crates/engine/src/session/editing.rs b/crates/engine/src/session/editing.rs index 05cf718d4d..ddbd6d1014 100644 --- a/crates/engine/src/session/editing.rs +++ b/crates/engine/src/session/editing.rs @@ -515,6 +515,8 @@ impl Session { } fn refresh_with_indexes(doc: &mut Document, invalidate_markers: bool, invalidate_links: bool) -> Result<(), EditError> { + #[cfg(test)] + crate::record_full_refresh(); // A full refresh may follow structural or annotation changes: cached appearances // may be stale, so they are rebuilt below. if let Some(editor) = doc.editor.as_mut() { diff --git a/crates/engine/src/test_hooks.rs b/crates/engine/src/test_hooks.rs index d6e504599e..1f6bd94822 100644 --- a/crates/engine/src/test_hooks.rs +++ b/crates/engine/src/test_hooks.rs @@ -40,3 +40,17 @@ pub(crate) fn record_link_scan() { counts.set((markers, links + 1)); }); } + +thread_local! { + static FULL_REFRESHES: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// A whole-document refresh (re-inspection) ran on this thread. +pub(crate) fn record_full_refresh() { + FULL_REFRESHES.with(|n| n.set(n.get() + 1)); +} + +/// How many whole-document refreshes ran on this thread since the last call, and starts over. +pub(crate) fn take_full_refreshes() -> usize { + FULL_REFRESHES.with(|n| n.replace(0)) +} diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 2d8eab60db..ad17d569fe 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9066,6 +9066,45 @@ fn content_refresh_lists_match_the_storages_an_edit_touches() { assert_eq!(Session::content_refresh_lists(None), (true, true), "undo/redo re-read both"); } +/// Recognized text is an invisible text layer on its page, so applying it refreshes like any +/// other content edit, not by re-inspecting the whole document (#6310): on a 567-page file with +/// 60,102 font resources one word took 2.4 s, the full refresh nearly all of it. The page tree, +/// comments, fields, links and marks stay as they were; the words are on their page, the font +/// list picks up the OCR font on `refresh_fonts`, undo and redo take the words out and back +/// without a full refresh either, and a save keeps them. +#[test] +fn ocr_words_refresh_only_the_page_content_they_add() { + use crate::ocr::{OcrPage, PlacedWord}; + let (mut s, id) = session_with(3); + let words_on = |s: &Session, page: usize| s.get(id).unwrap().text_lines(page).iter().map(|l| l.text.trim().to_string()).collect::>(); + let before = s.get(id).unwrap(); + let view = format!("{:?}", (&before.info.pages, &before.info.annotations, &before.info.fields, &before.links, before.marks())); + let others = (words_on(&s, 0), words_on(&s, 2)); + assert!(!words_on(&s, 1).iter().any(|w| w.contains("HELLO"))); + take_full_refreshes(); + let word = PlacedWord { text: "HELLO".into(), origin: [40.0, 40.0], across: [60.0, 0.0], up: [0.0, 20.0] }; + assert_eq!(s.apply_ocr(id, &[OcrPage { page: 1, words: vec![word], already_on_page: 0, skipped: None }]).unwrap(), 1); + assert_eq!(take_full_refreshes(), 0, "applying OCR words doesn't re-inspect the document"); + let after = s.get(id).unwrap(); + assert_eq!(format!("{:?}", (&after.info.pages, &after.info.annotations, &after.info.fields, &after.links, after.marks())), view); + assert!(words_on(&s, 1).iter().any(|w| w == "HELLO"), "{:?}", words_on(&s, 1)); + assert_eq!((words_on(&s, 0), words_on(&s, 2)), others, "the other pages are untouched"); + s.refresh_fonts(id); + let d = s.get(id).unwrap(); + let full = pdfcraft_render::inspect(d.bytes.clone(), None).unwrap(); + assert_eq!(format!("{:?}", d.info.fonts), format!("{:?}", full.fonts), "the font list is what a full inspection reads"); + assert_eq!(d.info.file_size, full.file_size); + s.undo(id).unwrap(); + assert!(!words_on(&s, 1).iter().any(|w| w.contains("HELLO")), "undo takes the words out"); + s.redo(id).unwrap(); + assert!(words_on(&s, 1).iter().any(|w| w == "HELLO"), "redo puts them back"); + assert_eq!(take_full_refreshes(), 0, "undo and redo refresh the content alone too"); + let saved = s.save_bytes(id).unwrap(); + let mut reopened = Session::new(); + let rid = reopened.open("saved.pdf", None, saved, None).unwrap(); + assert!(reopened.get(rid).unwrap().text_lines(1).iter().any(|l| l.text.trim() == "HELLO"), "the save keeps the words"); +} + /// A content edit can add a font (Add text in Times); the narrow refresh leaves the font list /// as it was, and `refresh_fonts` reads it again before Properties ▸ Fonts or `doc_info` show it. #[test]