mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(organize): analyse each form once when pruning unused XObjects
The pruner decoded and walked a form XObject again on every Do that drew it, so a form drawing itself a few times per level was analysed exponentially often within the depth limit. Forms are now remembered with the resources owner they were drawn against, and a repeat records nothing new.
This commit is contained in:
@@ -29,7 +29,7 @@ enum XKey {
|
||||
}
|
||||
|
||||
/// Who owns the resources currently being walked, for the key drawn names are recorded under.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum Owner {
|
||||
/// An indirect resources object (a page's, or a form XObject's own).
|
||||
Resources(ObjRef),
|
||||
@@ -50,6 +50,9 @@ struct Pruner<'a> {
|
||||
glyph_seen: HashSet<ObjRef>,
|
||||
/// An indirect font's explicit resources have one owner regardless of which page uses it.
|
||||
glyph_fonts: HashSet<ObjRef>,
|
||||
/// Forms already analysed, with the owner of the resources they drew against: drawing one
|
||||
/// again records nothing new, and re-reading it on every `Do` is exponential in the nesting.
|
||||
forms_seen: HashSet<(ObjRef, Owner)>,
|
||||
}
|
||||
|
||||
impl Pruner<'_> {
|
||||
@@ -211,6 +214,11 @@ impl Pruner<'_> {
|
||||
// A form without its own resources draws against the ones it was invoked with.
|
||||
None => (resources.clone(), owner),
|
||||
};
|
||||
if !self.forms_seen.insert((r, inner_owner)) {
|
||||
continue;
|
||||
}
|
||||
#[cfg(test)]
|
||||
crate::work(1);
|
||||
match s.decoded() {
|
||||
Ok(data) => self.content(&data, &inner, inner_owner, depth + 1),
|
||||
// What the form draws is unknown: keep everything it could reach.
|
||||
@@ -314,6 +322,7 @@ pub(crate) fn prune_unused_xobjects(doc: &mut Document) -> Result<usize, Organiz
|
||||
glyph_work_left: MAX_GLYPH_RESOURCE_WORK,
|
||||
glyph_seen: HashSet::new(),
|
||||
glyph_fonts: HashSet::new(),
|
||||
forms_seen: HashSet::new(),
|
||||
};
|
||||
for (page, attrs) in &pages {
|
||||
let Some(res) = attrs.get(b"Resources") else { continue };
|
||||
|
||||
@@ -3221,3 +3221,28 @@ fn name_trees_with_shared_kids_are_walked_once_per_node() {
|
||||
let tree = out.resolve(names.get(b"EmbeddedFiles").unwrap()).as_dict().cloned().unwrap();
|
||||
assert_eq!(tree.get(b"Names").and_then(|n| n.as_array()).map(Vec::len), Some(2), "one attachment, listed once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_form_drawing_itself_repeatedly_is_analysed_once() {
|
||||
let form = "/F Do /F Do /F Do /F Do";
|
||||
let b: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /XObject << /F 5 0 R /Unused 6 0 R >> >> >>".into(),
|
||||
"<< /Length 5 >>\nstream\n/F Do\nendstream".into(),
|
||||
format!(
|
||||
"<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources << /XObject << /F 5 0 R >> >> /Length {} >>\nstream\n{form}\nendstream",
|
||||
form.len()
|
||||
),
|
||||
"<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Length 0 >>\nstream\n\nendstream".into(),
|
||||
];
|
||||
let src = open(build(&b, "/Root 1 0 R"));
|
||||
take_work();
|
||||
let out = extract_pages(&src, &[0]).unwrap();
|
||||
let work = take_work();
|
||||
assert!(work <= 4, "{work} form analyses");
|
||||
let page = out.get(pages(&out).unwrap()[0].obj).as_dict().cloned().unwrap();
|
||||
let res = out.resolve(page.get(b"Resources").unwrap()).as_dict().cloned().unwrap();
|
||||
let x = out.resolve(res.get(b"XObject").unwrap()).as_dict().cloned().unwrap();
|
||||
assert!(x.contains(b"F") && !x.contains(b"Unused"), "pruning still keeps what is drawn and drops the rest");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user