diff --git a/crates/redact/src/sanitize.rs b/crates/redact/src/sanitize.rs index 35eac975f8..5a4ea3bfcd 100644 --- a/crates/redact/src/sanitize.rs +++ b/crates/redact/src/sanitize.rs @@ -587,9 +587,12 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re // The original image and form XObjects the rewrite drops or replaces, on any page. let mut doomed = Vec::new(); let mut retired_properties = Vec::new(); - let n = pdfcraft_model::pages(doc).len(); + // The page tree is read once, and again only after a page's rewrite (pinning its inherited + // resources can change its ancestors), not once per page: a scan of many pages stays linear. + let mut pages = pdfcraft_model::pages(doc); + let n = pages.len(); for pi in 0..n { - let page = pdfcraft_model::pages(doc).swap_remove(pi); + let Some(page) = pages.get(pi).cloned() else { break }; let mut budget = pdfcraft_cos::DecodeBudget::page(); let Ok((list, data)) = page_streams_within(doc, &page.dict, pi, &mut budget) else { if write { @@ -672,6 +675,7 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re pdfcraft_content::Pieces::join(&page_content).data(), ); crate::tags::clean(doc, page.obj, None, &changed_ids, &empty_ids)?; + pages = pdfcraft_model::pages(doc); } for (old, new, changed, empty) in out.tag_changes { crate::tags::clean(doc, page.obj, Some((old, new)), &changed, &empty)?; diff --git a/crates/redact/src/tests/limits.rs b/crates/redact/src/tests/limits.rs index 8519e08608..b241dc4cdc 100644 --- a/crates/redact/src/tests/limits.rs +++ b/crates/redact/src/tests/limits.rs @@ -53,3 +53,23 @@ fn a_change_inside_nested_marked_content_touches_every_enclosing_mcid() { assert_eq!(changed, HashSet::from([0, 1])); assert_eq!(empty, HashSet::from([1])); } + +/// The hidden-information scan read the whole page tree again for every page: 20 000 pages cost +/// 400 million page visits. It reads the tree once per pass now. +#[test] +fn a_scan_of_many_pages_reads_the_page_tree_once() { + use crate::sanitize::{Hidden, scan}; + const N: usize = 20_000; + let kids: String = (0..N).map(|i| format!("{} 0 R ", 4 + i)).collect(); + let mut objs = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + format!("<< /Type /Pages /Kids [{kids}] /Count {N} /MediaBox [0 0 300 300] >>").into_bytes(), + stream("", b"BT 3 Tr (x) Tj ET"), + ]; + objs.extend((0..N).map(|_| b"<< /Type /Page /Parent 2 0 R /Contents 3 0 R >>".to_vec())); + let doc = pdf(objs); + let started = std::time::Instant::now(); + let counts: HashMap = scan(&doc).into_iter().collect(); + assert_eq!(counts[&Hidden::HiddenText], N, "one invisible glyph on every page"); + assert!(started.elapsed() < std::time::Duration::from_secs(60), "{:?}", started.elapsed()); +}