From 3908a03aa62c6ccbb01e11017a91bb1f2013bd62 Mon Sep 17 00:00:00 2001 From: storybored379 Date: Sun, 4 Oct 2026 10:24:06 -0700 Subject: [PATCH] Optimizer: Audit space usage counts images PrintCraft added as images (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Content added with Edit ▸ Add content records its image in the content stream's dictionary (/PCAdded /Image). The audit claimed a page's content streams before its resources and followed that reference, so an added image's bytes were reported as Content Streams and Images showed 0 % (a 1.9 MB picture read as 99.9 % content). Resources are now claimed first, so whatever a content stream's dictionary points at keeps its own category. Co-authored-by: storybored379 Co-authored-by: Claude Opus 5.5 --- crates/optimize/src/audit.rs | 6 ++++-- crates/optimize/tests/optimize.rs | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/optimize/src/audit.rs b/crates/optimize/src/audit.rs index f5105d6e0..077097737 100644 --- a/crates/optimize/src/audit.rs +++ b/crates/optimize/src/audit.rs @@ -188,11 +188,13 @@ pub fn audit_space(doc: &Document, file_len: u64) -> Vec { a.claim(&x, if widget { SpaceCategory::Forms } else { SpaceCategory::Comments }); } } + // Resources before the content: a content stream's dictionary may point at what it + // draws (PrintCraft's /PCAdded records an added image), which stays an image or font. + let res = a.resolve_dict(p.dict.get(b"Resources")); + a.resources(&res); if let Some(c) = p.dict.get(b"Contents") { a.claim(c, SpaceCategory::ContentStreams); } - let res = a.resolve_dict(p.dict.get(b"Resources")); - a.resources(&res); if let Some(t) = p.dict.get(b"Thumb") { a.claim(t, SpaceCategory::Thumbnails); } diff --git a/crates/optimize/tests/optimize.rs b/crates/optimize/tests/optimize.rs index f39004f0a..3fcb593ed 100644 --- a/crates/optimize/tests/optimize.rs +++ b/crates/optimize/tests/optimize.rs @@ -193,6 +193,25 @@ fn space_audit_shares_the_file_out_by_kind() { assert!((audit.iter().map(|u| u.percent).sum::() - 100.0).abs() < 1e-6); } +#[test] +fn space_audit_counts_images_a_content_stream_dictionary_points_at_as_images() { + // Content added with Edit ▸ Add content records its image in the content stream's + // dictionary (/PCAdded); the image is still an image, not part of the content stream. + let mut doc = Document::new_empty(); + let img = image(&mut doc, 300, 300, 3, false, None); + let p = page(&mut doc, &[("Im0", img)], "q 300 0 0 300 0 0 cm /Im0 Do Q"); + let c = doc.get(p).as_dict().and_then(|d| d.reference(b"Contents")).unwrap(); + let mut added = Dict::new(); + added.set(b"Kind".to_vec(), Object::name("Image")); + added.set(b"Image".to_vec(), Object::Ref(img)); + doc.update_dict(c, |d| d.set(b"PCAdded".to_vec(), Object::Dict(added))).unwrap(); + let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap(); + let doc = Document::open(Arc::new(bytes.clone())).unwrap(); + let audit = audit_space(&doc, bytes.len() as u64); + let images = audit.iter().find(|u| u.category == SpaceCategory::Images).unwrap(); + assert!(images.percent > 80.0, "{audit:?}"); +} + #[test] fn invalid_links_and_unreferenced_destinations_go() { let mut doc = Document::new_empty();