mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 00:15:26 +00:00
fix(xfa): index saved-state names once and bound existing-image discovery
Saved-state matching scanned every template sibling for each saved one, quadratic in a wide form. earlier_images rescanned shared page resources and counted only accepted images, not the entries it examined.
This commit is contained in:
@@ -793,6 +793,14 @@ fn apply_saved_state(nodes: &mut [Node], saved: XmlNode, depth: usize) {
|
||||
return;
|
||||
}
|
||||
let mut seen: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||
// The template siblings of each name, in order, indexed once: scanning every sibling for each
|
||||
// saved one is quadratic in a wide form.
|
||||
let mut by_name: std::collections::HashMap<String, Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, n) in nodes.iter().enumerate() {
|
||||
if let Some(name) = n.common().name.as_deref() {
|
||||
by_name.entry(name.to_owned()).or_default().push(i);
|
||||
}
|
||||
}
|
||||
for c in saved.children().filter(|c| c.is_element()) {
|
||||
if !matches!(c.tag_name().name(), "subform" | "subformSet" | "area" | "field" | "draw" | "exclGroup") {
|
||||
continue;
|
||||
@@ -803,9 +811,8 @@ fn apply_saved_state(nodes: &mut [Node], saved: XmlNode, depth: usize) {
|
||||
*k += 1;
|
||||
*k - 1
|
||||
};
|
||||
let same: Vec<usize> = nodes.iter().enumerate().filter(|(_, n)| n.common().name.as_deref() == Some(name)).map(|(i, _)| i).collect();
|
||||
// More saved objects of the name than the template has: further instances.
|
||||
let Some(target) = same.get(nth).copied().and_then(|i| nodes.get_mut(i)) else { continue };
|
||||
let Some(target) = by_name.get(name).and_then(|same| same.get(nth)).copied().and_then(|i| nodes.get_mut(i)) else { continue };
|
||||
let presence = c.attribute("presence").map(|p| Presence::parse(Some(p)));
|
||||
match target {
|
||||
Node::Subform(s) => {
|
||||
|
||||
+21
-3
@@ -62,6 +62,8 @@ const IMAGE_SHA_KEY: &[u8] = b"PCXfaSHA256";
|
||||
const MAX_PAGES_SEARCHED: usize = 10_000;
|
||||
/// Most pictures remembered.
|
||||
const MAX_REMEMBERED_IMAGES: usize = 1000;
|
||||
/// Most XObject entries read while looking for those images, reusable or not.
|
||||
const MAX_IMAGES_EXAMINED: usize = 100_000;
|
||||
|
||||
struct Emitter<'a> {
|
||||
doc: &'a mut Document,
|
||||
@@ -885,12 +887,28 @@ fn has_host(after: &str) -> bool {
|
||||
fn earlier_images(doc: &Document, pages: ObjRef) -> HashMap<[u8; 32], ObjRef> {
|
||||
let mut out = HashMap::new();
|
||||
let kids = doc.get(pages).as_dict().and_then(|d| d.get(b"Kids").map(|k| doc.resolve(k))).and_then(|k| k.as_array().cloned()).unwrap_or_default();
|
||||
// A page, resource or XObject dictionary listed again is read once, and every entry read
|
||||
// counts, whether or not it is a reusable image.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut examined = 0usize;
|
||||
for page in kids.iter().filter_map(Object::as_ref).take(MAX_PAGES_SEARCHED) {
|
||||
if !seen.insert(page) {
|
||||
continue;
|
||||
}
|
||||
let page = doc.get(page);
|
||||
let Some(resources) = page.as_dict().and_then(|p| p.get(b"Resources")).map(|r| doc.resolve(r)) else { continue };
|
||||
let Some(xobjects) = resources.as_dict().and_then(|r| r.get(b"XObject")).map(|x| doc.resolve(x)) else { continue };
|
||||
let Some(res_entry) = page.as_dict().and_then(|p| p.get(b"Resources")) else { continue };
|
||||
if res_entry.as_ref().is_some_and(|r| !seen.insert(r)) {
|
||||
continue;
|
||||
}
|
||||
let resources = doc.resolve(res_entry);
|
||||
let Some(xo_entry) = resources.as_dict().and_then(|r| r.get(b"XObject")) else { continue };
|
||||
if xo_entry.as_ref().is_some_and(|r| !seen.insert(r)) {
|
||||
continue;
|
||||
}
|
||||
let xobjects = doc.resolve(xo_entry);
|
||||
for r in xobjects.as_dict().into_iter().flat_map(|x| x.iter()).filter_map(|(_, v)| v.as_ref()) {
|
||||
if out.len() >= MAX_REMEMBERED_IMAGES {
|
||||
examined += 1;
|
||||
if out.len() >= MAX_REMEMBERED_IMAGES || examined > MAX_IMAGES_EXAMINED {
|
||||
return out;
|
||||
}
|
||||
if let Object::Stream(s) = &*doc.get(r)
|
||||
|
||||
@@ -4531,3 +4531,22 @@ fn a_container_is_as_wide_as_a_turned_draw_stands() {
|
||||
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The saved state of a wide form is matched through one index of the template's names: 20,000
|
||||
/// same-named fields used to be scanned once for each of their 20,000 saved states.
|
||||
#[test]
|
||||
fn saved_state_of_a_wide_form_is_matched_by_index() {
|
||||
const N: usize = 20_000;
|
||||
let fields = "<field name=\"f\"/>".repeat(N);
|
||||
let saved = format!("{}<field name=\"f\" presence=\"hidden\"/>", "<field name=\"f\"/>".repeat(N - 1));
|
||||
let xml = format!(
|
||||
"<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\">{fields}</subform></template><form xmlns=\"http://www.xfa.org/schema/xfa-form/2.8/\"><subform name=\"form1\">{saved}</subform></form></xdp:xdp>"
|
||||
);
|
||||
let start = std::time::Instant::now();
|
||||
let (t, _) = parse(&xml).unwrap();
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed());
|
||||
let presence: Vec<Presence> = t.root.children.iter().map(|n| n.common().presence).collect();
|
||||
assert_eq!(presence.len(), N);
|
||||
assert_eq!(presence.last(), Some(&Presence::Hidden), "the last saved state reaches the last field");
|
||||
assert!(presence.iter().take(N - 1).all(|p| *p == Presence::Visible));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user