fix(redact): read the page tree once per sanitize pass, not per page (#5983)

This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-09 18:37:46 +08:00
1 parent 8f29ed66f9
commit 4cdc3d15dd
2 files changed
+26 -2

No files matched your search

+6 -2
View File
@@ -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)?;
+20
View File
@@ -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<Hidden, usize> = 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());
}