fix(annot): document-wide annotation scans copy a shared Annots array once

This commit is contained in:
Dylan M. Taylor
2026-11-04 04:16:38 +00:00
parent 9ce6d89f46
commit fdd4e50e80
3 changed files with 39 additions and 3 deletions
+2 -2
View File
@@ -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};
@@ -163,7 +163,7 @@ pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCac
// reads `doc`, which never gets the appearance, so every repeat used to add another stream to
// the copy, and the display bytes grew with the number of listings.
let mut seen = std::collections::HashSet::new();
for page in page_refs(doc).ok()? {
for page in 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;
+9 -1
View File
@@ -73,6 +73,14 @@ pub(crate) fn page_ref(doc: &Document, page: usize) -> Result<ObjRef, AnnotError
page_refs(doc)?.get(page).copied().ok_or(AnnotError::NoSuchPage(page))
}
/// `pages` without those whose `/Annots` is an indirect array an earlier page already lists. For a
/// consumer that visits each annotation once however often it is listed: a shared array is then
/// copied and scanned once, not once per page that names it.
pub(crate) fn with_distinct_annots(doc: &Document, pages: Vec<ObjRef>) -> Vec<ObjRef> {
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<Object> {
let obj = doc.get(page);
@@ -172,7 +180,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 };
+28
View File
@@ -2274,3 +2274,31 @@ fn indirect_typewriter_names_keep_outer_whitespace_in_summaries() {
check(&doc);
check(&reopen(&doc));
}
/// 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<Object> = (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");
}