mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
fix(organize): register imported form fields in linear time
Each imported top-level field was checked against every field already in /AcroForm /Fields before it was appended, so a page with many distinct widget fields cost time quadratic in their number. Membership is now kept in a set beside the list. Adds a test-only work counter to the crate, so tests can bound the work a hostile document causes without timing it.
This commit is contained in:
@@ -826,8 +826,11 @@ fn register_fields(dst: &mut Document, fields: &[ObjRef]) -> Result<(), Organize
|
||||
_ => (None, Dict::new()),
|
||||
};
|
||||
let mut list = form.get(b"Fields").map(|f| dst.resolve(f)).and_then(|f| f.as_array().cloned()).unwrap_or_default();
|
||||
let mut listed: HashSet<ObjRef> = list.iter().filter_map(Object::as_ref).collect();
|
||||
for f in fields {
|
||||
if !list.contains(&Object::Ref(*f)) {
|
||||
#[cfg(test)]
|
||||
crate::work(1);
|
||||
if listed.insert(*f) {
|
||||
list.push(Object::Ref(*f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,5 +739,24 @@ pub fn remove_standard_id(doc: &mut Document, standard: Standard) -> Result<(),
|
||||
if removed { Ok(()) } else { Err(OrganizeError::Invalid(format!("the document doesn't declare {}", standard.label()))) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Steps taken on this thread by loops whose cost follows the input's shape, so a test can
|
||||
/// bound the work a hostile document causes without timing it.
|
||||
static WORK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Count `n` steps of input-shaped work (see [`WORK`]).
|
||||
#[cfg(test)]
|
||||
fn work(n: usize) {
|
||||
WORK.with(|w| w.set(w.get().saturating_add(n)));
|
||||
}
|
||||
|
||||
/// The steps counted on this thread since the last call.
|
||||
#[cfg(test)]
|
||||
fn take_work() -> usize {
|
||||
WORK.with(|w| w.replace(0))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -3132,3 +3132,31 @@ fn page_walk_skips_repeated_and_cyclic_kids_without_copying_attributes() {
|
||||
assert!(walk.next().is_none());
|
||||
assert_eq!(labels(&doc), ["Page 1", "Page 2", "Page 3"]);
|
||||
}
|
||||
|
||||
/// A one-page document whose page carries `annots` (object bodies 5…), with `extra` bodies after them.
|
||||
fn page_with_annots(annots: &[String], extra: &[String], catalog_extra: &str) -> Document {
|
||||
let n = annots.len();
|
||||
let refs: String = (0..n).map(|i| format!("{} 0 R ", i + 5)).collect();
|
||||
let mut b: Vec<String> = vec![
|
||||
format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >>".into(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Annots [{refs}] >>"),
|
||||
body("P1"),
|
||||
];
|
||||
b.extend(annots.iter().cloned());
|
||||
b.extend(extra.iter().cloned());
|
||||
open(build(&b, "/Root 1 0 R"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registering_many_imported_fields_is_linear() {
|
||||
let n = 2_000;
|
||||
let widgets: Vec<String> = (0..n).map(|i| format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T (f{i}) /Rect [0 0 10 10] >>")).collect();
|
||||
let src = page_with_annots(&widgets, &[], "");
|
||||
take_work();
|
||||
let out = extract_pages(&src, &[0]).unwrap();
|
||||
let work = take_work();
|
||||
assert!(work <= 4 * n, "{work} steps registering {n} fields");
|
||||
let form = out.resolve(catalog(&out).get(b"AcroForm").unwrap()).as_dict().cloned().unwrap();
|
||||
assert_eq!(form.get(b"Fields").and_then(|f| f.as_array()).map(Vec::len), Some(n));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user