From c7c018ac6d1f7cf1e93ddb9965ce9900d08e2457 Mon Sep 17 00:00:00 2001 From: Grzegorz Martens Date: Mon, 2 Nov 2026 11:09:09 +0100 Subject: [PATCH] M11: Optimize passes remove_hidden only the ticked categories that hold something Catching remove_hidden's refusal kept a copy it may have changed: with bookmarks ticked it drops an empty outline root before counting nothing, and its own documentation says to drop such a copy. The optimizer now scans first and passes only the ticked categories the scan finds something in, and with none doesn't call it, so an empty outline root stays, as Remove Hidden Information leaves it. Corrects 278a588bf's message: the ticks are under Discard Objects and Discard User Data, and hidden text is one of them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY --- crates/engine/src/optimizer.rs | 12 ++++++------ crates/engine/src/tests.rs | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/optimizer.rs b/crates/engine/src/optimizer.rs index 68f5a143da..b403b82373 100644 --- a/crates/engine/src/optimizer.rs +++ b/crates/engine/src/optimizer.rs @@ -95,12 +95,12 @@ impl OptimizeJob { let discarded = if discard.is_empty() { Vec::new() } else { - // `remove_hidden` refuses a run that removes nothing. Ticked categories that hold - // nothing are nothing to discard, not a reason to end the whole optimization. - match pdfcraft_redact::sanitize::remove_hidden(&mut cos, &discard) { - Err(pdfcraft_redact::RedactError::NothingToApply) => Vec::new(), - r => r?, - } + // Only the ticked categories its scan finds something in: `remove_hidden` refuses when + // it counts every one it is given as 0, which would end the whole optimization, and + // may have changed the copy by then (an empty outline root goes with bookmarks). + let counts = pdfcraft_redact::sanitize::scan(&cos); + let which: Vec = discard.iter().copied().filter(|h| counts.iter().any(|(c, n)| c == h && *n > 0)).collect(); + if which.is_empty() { Vec::new() } else { pdfcraft_redact::sanitize::remove_hidden(&mut cos, &which)? } }; let report = optimize::optimize_with_progress(&mut cos, &settings, &mut |s| { step(match s { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 18108c44da..9f5dece295 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3618,6 +3618,21 @@ fn optimize_discarding_an_empty_category_still_runs() { let (bytes, report) = s.optimized_bytes(id, &optimize::Settings::default(), &[crate::Hidden::Bookmarks, crate::Hidden::Comments]).unwrap(); assert!(bytes.starts_with(b"%PDF-")); assert!(report.discarded.is_empty(), "{:?}", report.discarded); + // An outline root with no entries is no bookmarks: it stays, as Remove Hidden Information, + // which refuses then, leaves it. + let outline = pdfcraft_testkit::Pdf::new() + .obj("<< /Type /Catalog /Pages 2 0 R /Outlines 4 0 R /PageMode /UseOutlines >>") + .obj("<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 612 792] >>") + .obj("<< /Type /Page /Parent 2 0 R >>") + .obj("<< /Type /Outlines /Count 0 >>") + .build(); + let mut s = Session::new(); + let id = s.open("outline.pdf", None, Arc::new(outline), None).unwrap(); + let (bytes, report) = s.optimized_bytes(id, &optimize::Settings::default(), &[crate::Hidden::Bookmarks]).unwrap(); + assert!(report.discarded.is_empty(), "{:?}", report.discarded); + let saved = pdfcraft_cos::Document::open(bytes).unwrap(); + let catalog = saved.get(saved.root().unwrap()).as_dict().cloned().unwrap(); + assert!(catalog.contains(b"Outlines") && catalog.contains(b"PageMode"), "{catalog:?}"); } #[test]