M10: applying recognized words refreshes only the page content they add (#6310) (#6316)

This commit is contained in:
mtlcl410
2026-11-09 08:55:14 +08:00
committed by GitHub
parent 616f245b04
commit c67c44bf21
5 changed files with 64 additions and 2 deletions
+5 -1
View File
@@ -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);
+4 -1
View File
@@ -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,
+2
View File
@@ -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() {
+14
View File
@@ -40,3 +40,17 @@ pub(crate) fn record_link_scan() {
counts.set((markers, links + 1));
});
}
thread_local! {
static FULL_REFRESHES: std::cell::Cell<usize> = 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))
}
+39
View File
@@ -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::<Vec<_>>();
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]