fix(annot): read a shared page tree Kids array once

The page walk pushed a node's /Kids array for every new node, so many page
tree nodes sharing one indirect /Kids array re-read it for each node:
quadratic work while the seen check skipped every entry. A shared array
object is read under the first node that lists it, and the walk stops
after four million /Kids entries.
This commit is contained in:
aiongg
2026-10-22 18:02:33 +00:00
parent 240d051b4e
commit 05d09de1e3
2 changed files with 38 additions and 5 deletions
+16 -5
View File
@@ -665,17 +665,20 @@ pub fn page_refs(doc: &Document) -> Result<Vec<ObjRef>, AnnotError> {
/// Depth-first walk over the leaf pages under `top`. It keeps one cursor per open `/Kids` array,
/// never one entry per child, so a child listed many times or shared between nodes is skipped by
/// the seen check without being queued first (`pdfcraft_model::pages` walks the same way).
/// Cycles end at the seen check and at most a million nodes are visited.
/// Cycles end at the seen check, a `/Kids` array object shared by several nodes is read once,
/// and at most a million nodes and four million `/Kids` entries are read.
struct PageWalk<'a> {
doc: &'a Document,
seen: std::collections::HashSet<ObjRef>,
pending: Vec<(std::sync::Arc<Object>, usize)>,
/// `/Kids` entries read so far.
examined: usize,
}
impl<'a> PageWalk<'a> {
fn new(doc: &'a Document, top: ObjRef) -> Self {
let top = std::sync::Arc::new(Object::Array(vec![Object::Ref(top)]));
Self { doc, seen: Default::default(), pending: vec![(top, 0)] }
Self { doc, seen: Default::default(), pending: vec![(top, 0)], examined: 0 }
}
}
@@ -690,8 +693,9 @@ impl Iterator for PageWalk<'_> {
};
let node = kid.as_ref();
*next += 1;
self.examined += 1;
let Some(node) = node else { continue };
if self.seen.len() >= 1_000_000 {
if self.seen.len() >= 1_000_000 || self.examined > 4_000_000 {
self.pending.clear();
return None;
}
@@ -700,8 +704,15 @@ impl Iterator for PageWalk<'_> {
}
let obj = self.doc.get(node);
let Some(d) = obj.as_dict() else { continue };
match d.get(b"Kids").map(|k| self.doc.resolve(k)) {
Some(kids) if d.name(b"Type") != Some(b"Page") => self.pending.push((kids, 0)),
match d.get(b"Kids") {
// A `/Kids` array object shared by several nodes is read under the first: under
// the others it would only list nodes already seen.
Some(Object::Ref(array)) if d.name(b"Type") != Some(b"Page") => {
if self.seen.insert(*array) {
self.pending.push((self.doc.get(*array), 0));
}
}
Some(kids) if d.name(b"Type") != Some(b"Page") => self.pending.push((self.doc.resolve(kids), 0)),
_ => return Some(node),
}
}
+22
View File
@@ -1504,3 +1504,25 @@ fn typed_signature_uses_nonzero_winding_fill() {
assert!(ap.ends_with("h\nf\n"), "expected non-zero winding fill (`f`), got: {ap}");
assert!(!ap.contains("f*"), "must not use even-odd fill (`f*`): {ap}");
}
/// Many page tree nodes sharing one `/Kids` array read it once, not once per node.
#[test]
fn page_walk_reads_a_shared_kids_array_once() {
let mut doc = fixture();
let root = ObjRef { num: 2, generation: 0 };
let (first, second) = (ObjRef { num: 3, generation: 0 }, ObjRef { num: 4, generation: 0 });
let shared = doc.add(Object::Array([Object::Ref(first), Object::Ref(second)].into_iter().cycle().take(2_000).collect()));
let nodes: Vec<Object> = (0..1_000)
.map(|_| {
let mut d = Dict::new();
d.set(b"Type".to_vec(), Object::name("Pages"));
d.set(b"Kids".to_vec(), Object::Ref(shared));
Object::Ref(doc.add(d))
})
.collect();
doc.update_dict(root, |d| d.set(b"Kids".to_vec(), Object::Array(nodes))).unwrap();
let mut walk = PageWalk::new(&doc, root);
let pages: Vec<ObjRef> = walk.by_ref().collect();
assert_eq!(pages, [first, second]);
assert!(walk.examined <= 10_000, "{} kids entries read", walk.examined);
}