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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-11-02 11:09:09 +01:00
1 parent ada2a1ccb7
commit c7c018ac6d
2 files changed
+21 -6

No files matched your search

+6 -6
View File
@@ -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<Hidden> = 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 {
+15
View File
@@ -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]