mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 03:27:21 -06:00
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:
+16
-5
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user