diff --git a/crates/annot/src/draw.rs b/crates/annot/src/draw.rs index c0e82eb151..d1e70e7f2d 100644 --- a/crates/annot/src/draw.rs +++ b/crates/annot/src/draw.rs @@ -1,5 +1,5 @@ use super::create::{finite, num_array}; -use super::lookup::{annot_dict, annots, page_refs}; +use super::lookup::{annot_dict, annots, page_refs, with_distinct_annots}; use super::modify::refuse_foreign_text; use super::types::NOTE_SIZE; use super::{AnnotError, Dict, Document, ObjRef, Object, Stream, appearance}; @@ -383,7 +383,7 @@ pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCac // Pages' visible regions, read once and only when an annotation drawn through its points is. let mut crops: Option> = None; let mut budget = PASS_BUDGET; - for page in page_refs(doc).ok()? { + for page in crate::lookup::with_distinct_annots(doc, page_refs(doc).ok()?) { for r in annots(doc, page).iter().filter_map(Object::as_ref) { if !seen.insert(r) { continue; diff --git a/crates/annot/src/lookup.rs b/crates/annot/src/lookup.rs index 788774c428..dcdeb42b42 100644 --- a/crates/annot/src/lookup.rs +++ b/crates/annot/src/lookup.rs @@ -73,6 +73,14 @@ pub(crate) fn page_ref(doc: &Document, page: usize) -> Result) -> Vec { + let mut seen = std::collections::HashSet::new(); + pages.into_iter().filter(|p| doc.get(*p).as_dict().and_then(|d| d.reference(b"Annots")).is_none_or(|r| seen.insert(r))).collect() +} + /// The entries of a page's `/Annots` (references or inline dictionaries). pub(crate) fn annots(doc: &Document, page: ObjRef) -> Vec { let obj = doc.get(page); @@ -229,7 +237,7 @@ pub(crate) fn drawn_upright(doc: &Document, d: &Dict) -> bool { /// Whether any page has a Fill & Sign annotation that flatten would bake in (not hidden). pub fn has_visible_fill_sign(doc: &Document) -> bool { let Ok(pages) = page_refs(doc) else { return false }; - for page in pages { + for page in with_distinct_annots(doc, pages) { for entry in annots(doc, page) { let obj = doc.resolve(&entry); let Some(d) = obj.as_dict() else { continue }; diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index ce5a8c1604..341513715a 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -5619,3 +5619,31 @@ endstream", assert!(list(&doc, 0)[0].contains(b"AAPL:AKExtras")); assert!(doc.repair_log().is_empty(), "{:?}", doc.repair_log()); } + +/// Pages sharing one indirect `/Annots` array have it copied once by the consumers that visit each +/// annotation once, not once per page. +#[test] +fn document_wide_scans_copy_a_shared_annots_array_once() { + let mut doc = fixture(); + let root = ObjRef { num: 2, generation: 0 }; + let mut note = Dict::new(); + note.set(b"Type".to_vec(), Object::name("Annot")); + note.set(b"Subtype".to_vec(), Object::name("Text")); + note.set(b"Rect".to_vec(), Object::Array([10, 10, 30, 30].map(Object::Int).to_vec())); + let note = doc.add(note); + let shared = doc.add(Object::Array(std::iter::repeat_n(Object::Ref(note), 100).collect())); + let pages: Vec = (0..500) + .map(|_| { + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("Page")); + d.set(b"Annots".to_vec(), Object::Ref(shared)); + Object::Ref(doc.add(d)) + }) + .collect(); + doc.update_dict(root, |d| d.set(b"Kids".to_vec(), Object::Array(pages))).unwrap(); + ANNOTS_ENTRIES_COPIED.with(|c| c.set(0)); + assert!(!has_visible_fill_sign(&doc)); + assert!(with_missing_appearances(&doc).is_some()); + let copied = ANNOTS_ENTRIES_COPIED.with(std::cell::Cell::get); + assert!(copied <= 200, "{copied} annotation entries copied"); +}