mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 03:27:21 -06:00
Merge PR #3608: Page-tree walks in annot, forms and organize skip repeated children
This commit is contained in:
+45
-16
@@ -651,25 +651,54 @@ impl ReviewState {
|
||||
pub fn page_refs(doc: &Document) -> Result<Vec<ObjRef>, AnnotError> {
|
||||
let root = doc.root().ok_or(AnnotError::NoPageTree)?;
|
||||
let pages = doc.get(root).as_dict().and_then(|d| d.reference(b"Pages")).ok_or(AnnotError::NoPageTree)?;
|
||||
let mut out = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut stack = vec![pages];
|
||||
while let Some(node) = stack.pop() {
|
||||
if !seen.insert(node) || seen.len() > 1_000_000 {
|
||||
continue;
|
||||
}
|
||||
let obj = doc.get(node);
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
match d.get(b"Kids").map(|k| doc.resolve(k)) {
|
||||
Some(kids) if d.name(b"Type") != Some(b"Page") => {
|
||||
if let Some(a) = kids.as_array() {
|
||||
stack.extend(a.iter().rev().filter_map(|k| k.as_ref()));
|
||||
}
|
||||
Ok(PageWalk::new(doc, pages).collect())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
struct PageWalk<'a> {
|
||||
doc: &'a Document,
|
||||
seen: std::collections::HashSet<ObjRef>,
|
||||
pending: Vec<(std::sync::Arc<Object>, 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)] }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for PageWalk<'_> {
|
||||
type Item = ObjRef;
|
||||
|
||||
fn next(&mut self) -> Option<ObjRef> {
|
||||
while let Some((kids, next)) = self.pending.last_mut() {
|
||||
let Some(kid) = kids.as_array().and_then(|a| a.get(*next)) else {
|
||||
self.pending.pop();
|
||||
continue;
|
||||
};
|
||||
let node = kid.as_ref();
|
||||
*next += 1;
|
||||
let Some(node) = node else { continue };
|
||||
if self.seen.len() >= 1_000_000 {
|
||||
self.pending.clear();
|
||||
return None;
|
||||
}
|
||||
if !self.seen.insert(node) {
|
||||
continue;
|
||||
}
|
||||
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)),
|
||||
_ => return Some(node),
|
||||
}
|
||||
_ => out.push(node),
|
||||
}
|
||||
None
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn page_ref(doc: &Document, page: usize) -> Result<ObjRef, AnnotError> {
|
||||
|
||||
@@ -1382,3 +1382,20 @@ fn free_text_properties_show_legacy_or_absent_border_width() {
|
||||
.unwrap();
|
||||
assert_eq!(props(&doc, 0, index).unwrap().width, Some(4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_walk_skips_repeated_and_cyclic_kids_without_queueing_them() {
|
||||
let mut doc = fixture();
|
||||
let root = ObjRef { num: 2, generation: 0 };
|
||||
let (first, second) = (ObjRef { num: 3, generation: 0 }, ObjRef { num: 4, generation: 0 });
|
||||
// A cycle back to the root, then both pages 128 times over.
|
||||
let mut kids = vec![Object::Ref(root)];
|
||||
kids.extend([Object::Ref(first), Object::Ref(second)].into_iter().cycle().take(256));
|
||||
doc.update_dict(root, |d| d.set(b"Kids".to_vec(), Object::Array(kids))).unwrap();
|
||||
let mut walk = PageWalk::new(&doc, root);
|
||||
assert_eq!(walk.next(), Some(first));
|
||||
assert!(walk.pending.len() <= 2, "repeated kids retained {} pending entries", walk.pending.len());
|
||||
assert_eq!(walk.next(), Some(second));
|
||||
assert_eq!(walk.next(), None);
|
||||
assert_eq!(page_refs(&doc).unwrap(), [first, second]);
|
||||
}
|
||||
|
||||
+45
-16
@@ -454,25 +454,54 @@ fn nums(doc: &Document, o: Option<&Object>) -> Option<Vec<f64>> {
|
||||
pub(crate) fn page_refs(doc: &Document) -> Vec<ObjRef> {
|
||||
let Some(root) = doc.root() else { return Vec::new() };
|
||||
let Some(pages) = doc.get(root).as_dict().and_then(|d| d.reference(b"Pages")) else { return Vec::new() };
|
||||
let mut out = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut stack = vec![pages];
|
||||
while let Some(node) = stack.pop() {
|
||||
if !seen.insert(node) || seen.len() > 1_000_000 {
|
||||
continue;
|
||||
}
|
||||
let obj = doc.get(node);
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
match d.get(b"Kids").map(|k| doc.resolve(k)) {
|
||||
Some(kids) if d.name(b"Type") != Some(b"Page") => {
|
||||
if let Some(a) = kids.as_array() {
|
||||
stack.extend(a.iter().rev().filter_map(|k| k.as_ref()));
|
||||
}
|
||||
PageWalk::new(doc, pages).collect()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
struct PageWalk<'a> {
|
||||
doc: &'a Document,
|
||||
seen: std::collections::HashSet<ObjRef>,
|
||||
pending: Vec<(std::sync::Arc<Object>, 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)] }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for PageWalk<'_> {
|
||||
type Item = ObjRef;
|
||||
|
||||
fn next(&mut self) -> Option<ObjRef> {
|
||||
while let Some((kids, next)) = self.pending.last_mut() {
|
||||
let Some(kid) = kids.as_array().and_then(|a| a.get(*next)) else {
|
||||
self.pending.pop();
|
||||
continue;
|
||||
};
|
||||
let node = kid.as_ref();
|
||||
*next += 1;
|
||||
let Some(node) = node else { continue };
|
||||
if self.seen.len() >= 1_000_000 {
|
||||
self.pending.clear();
|
||||
return None;
|
||||
}
|
||||
if !self.seen.insert(node) {
|
||||
continue;
|
||||
}
|
||||
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)),
|
||||
_ => return Some(node),
|
||||
}
|
||||
_ => out.push(node),
|
||||
}
|
||||
None
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The AcroForm dictionary.
|
||||
|
||||
@@ -2508,3 +2508,19 @@ fn document_scripts_read_each_name_tree_node_once() {
|
||||
]);
|
||||
assert_eq!(document_scripts_named(&doc), [("init".to_string(), "var ready = true;".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_walk_skips_repeated_and_cyclic_kids_without_queueing_them() {
|
||||
let mut doc = fixture();
|
||||
let root = ObjRef { num: 2, generation: 0 };
|
||||
let page = ObjRef { num: 3, generation: 0 };
|
||||
// A cycle back to the root, then the page 128 times over.
|
||||
let mut kids = vec![Object::Ref(root)];
|
||||
kids.extend(std::iter::repeat_n(Object::Ref(page), 128));
|
||||
doc.update_dict(root, |d| d.set(b"Kids".to_vec(), Object::Array(kids))).unwrap();
|
||||
let mut walk = PageWalk::new(&doc, root);
|
||||
assert_eq!(walk.next(), Some(page));
|
||||
assert!(walk.pending.len() <= 2, "repeated kids retained {} pending entries", walk.pending.len());
|
||||
assert_eq!(walk.next(), None);
|
||||
assert_eq!(page_refs(&doc), [page]);
|
||||
}
|
||||
|
||||
+63
-25
@@ -72,36 +72,74 @@ fn pages_root(doc: &Document) -> Result<ObjRef, OrganizeError> {
|
||||
|
||||
/// All leaf pages in order, with their effective inherited attributes.
|
||||
fn walk(doc: &Document) -> Result<Vec<(ObjRef, Dict)>, OrganizeError> {
|
||||
let root = pages_root(doc)?;
|
||||
let mut out = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
// (node, inherited attributes from ancestors)
|
||||
let mut stack: Vec<(ObjRef, Dict)> = vec![(root, Dict::new())];
|
||||
while let Some((node, inherited)) = stack.pop() {
|
||||
if !seen.insert(node) || seen.len() > 1_000_000 {
|
||||
continue; // cycles in broken trees
|
||||
}
|
||||
let obj = doc.get(node);
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
let mut attrs = inherited.clone();
|
||||
for k in INHERITABLE {
|
||||
if let Some(v) = d.get(k) {
|
||||
attrs.set(k.to_vec(), v.clone());
|
||||
Ok(PageWalk::new(doc, pages_root(doc)?).collect())
|
||||
}
|
||||
|
||||
/// For each of `INHERITABLE`, the nearest node that sets it, shared rather than copied.
|
||||
type Owners = [Option<std::sync::Arc<Object>>; 4];
|
||||
|
||||
/// Depth-first walk over the leaf pages under `top`. It keeps one cursor per open `/Kids` array,
|
||||
/// never one entry per child, and carries inherited attributes as the nodes that set them, so a
|
||||
/// child listed many times or shared between nodes is skipped by the seen check before anything
|
||||
/// is queued or copied (`pdfcraft_model::pages` walks the same way). Cycles in broken trees end
|
||||
/// at the seen check and at most a million nodes are visited.
|
||||
struct PageWalk<'a> {
|
||||
doc: &'a Document,
|
||||
seen: std::collections::HashSet<ObjRef>,
|
||||
pending: Vec<(std::sync::Arc<Object>, usize, Owners)>,
|
||||
}
|
||||
|
||||
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, Default::default())] }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for PageWalk<'_> {
|
||||
type Item = (ObjRef, Dict);
|
||||
|
||||
fn next(&mut self) -> Option<(ObjRef, Dict)> {
|
||||
while let Some((kids, next, inherited)) = self.pending.last_mut() {
|
||||
let Some(kid) = kids.as_array().and_then(|a| a.get(*next)) else {
|
||||
self.pending.pop();
|
||||
continue;
|
||||
};
|
||||
let node = kid.as_ref();
|
||||
*next += 1;
|
||||
let Some(node) = node else { continue };
|
||||
if self.seen.len() >= 1_000_000 {
|
||||
self.pending.clear();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let is_pages = d.name(b"Type") == Some(b"Pages") || (d.contains(b"Kids") && d.name(b"Type") != Some(b"Page"));
|
||||
if is_pages {
|
||||
let kids = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()).unwrap_or_default();
|
||||
for k in kids.iter().rev() {
|
||||
if let Some(r) = k.as_ref() {
|
||||
stack.push((r, attrs.clone()));
|
||||
if !self.seen.insert(node) {
|
||||
continue;
|
||||
}
|
||||
let obj = self.doc.get(node);
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
let mut owners = inherited.clone();
|
||||
for (k, owner) in INHERITABLE.iter().zip(&mut owners) {
|
||||
if d.contains(k) {
|
||||
*owner = Some(obj.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push((node, attrs));
|
||||
let is_pages = d.name(b"Type") == Some(b"Pages") || (d.contains(b"Kids") && d.name(b"Type") != Some(b"Page"));
|
||||
if is_pages {
|
||||
if let Some(kids) = d.get(b"Kids") {
|
||||
self.pending.push((self.doc.resolve(kids), 0, owners));
|
||||
}
|
||||
} else {
|
||||
let mut attrs = Dict::new();
|
||||
for (k, owner) in INHERITABLE.iter().zip(&owners) {
|
||||
if let Some(v) = owner.as_ref().and_then(|o| o.as_dict()).and_then(|o| o.get(k)) {
|
||||
attrs.set(k.to_vec(), v.clone());
|
||||
}
|
||||
}
|
||||
return Some((node, attrs));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Leaf pages in document order.
|
||||
|
||||
@@ -2848,3 +2848,27 @@ fn rotate_repeated_indices_accumulate_from_the_current_page_value() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_walk_skips_repeated_and_cyclic_kids_without_copying_attributes() {
|
||||
let mut doc = open(fixture());
|
||||
let (root, branch) = (ObjRef { num: 2, generation: 0 }, ObjRef { num: 3, generation: 0 });
|
||||
let (first, second) = (ObjRef { num: 4, generation: 0 }, ObjRef { num: 5, generation: 0 });
|
||||
// The branch node carries Resources for its pages: a cycle back to it, then both pages
|
||||
// 128 times over.
|
||||
let mut kids = vec![Object::Ref(branch)];
|
||||
kids.extend([Object::Ref(first), Object::Ref(second)].into_iter().cycle().take(256));
|
||||
doc.update_dict(branch, |d| d.set(b"Kids".to_vec(), Object::Array(kids))).unwrap();
|
||||
let mut walk = PageWalk::new(&doc, root);
|
||||
let (page, attrs) = walk.next().unwrap();
|
||||
assert_eq!(page, first);
|
||||
assert!(walk.pending.len() <= 3, "repeated kids retained {} pending entries", walk.pending.len());
|
||||
assert!(attrs.get(b"Resources").and_then(|r| r.as_dict()).is_some_and(|r| r.contains(b"Font")), "Resources from the branch");
|
||||
assert_eq!(attrs.get(b"Rotate").and_then(Object::as_int), Some(90), "Rotate from the root");
|
||||
assert_eq!(walk.next().map(|(p, _)| p), Some(second));
|
||||
let (_, last) = walk.next().unwrap();
|
||||
let media_box = last.get(b"MediaBox").and_then(Object::as_array).map(|m| m.iter().filter_map(Object::as_int).collect::<Vec<_>>());
|
||||
assert_eq!(media_box, Some(vec![0, 0, 500, 500]), "own MediaBox kept over the root's");
|
||||
assert!(walk.next().is_none());
|
||||
assert_eq!(labels(&doc), ["Page 1", "Page 2", "Page 3"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user