M11: Reduce file size shrinks office scans, and never saves a bigger copy (refs #490) (#849)

Office scanners write the default decode array (/Decode [0 1 0 1 0 1]) on their page
images. Reduce skipped every image with a decode array, so a scan stayed the same
size. The default array is now treated as absent; any other array still skips the
image.

Reduce also saved its copy even when it wasn't smaller, reported as "-N% smaller".
It now saves nothing and says the file is already as small as it can be made;
doc_reduce returns "written": false. Optimize PDF still saves (what it discards
may matter more than the size) but says "not smaller".

Co-authored-by: Andrew Taylor <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-10 16:20:50 -04:00
1 parent 74be522c18
commit 4fd2c2b3a3
26 files changed
+178 -53

No files matched your search

+33
View File
@@ -131,6 +131,39 @@ fn images_are_measured_where_drawn_and_downsampled() {
assert_ne!(&out.rgba[i..i + 3], &[255, 255, 255]);
}
#[test]
fn an_identity_decode_array_does_not_stop_reduce() {
// Office scanners write /Decode [0 1 0 1 0 1] (no change) on their page images (#490).
let with_decode = |doc: &mut Document, r: ObjRef, decode: Vec<Object>| {
let mut s = stream(doc, r);
s.dict.set(b"Decode".to_vec(), Object::Array(decode));
doc.set(r, Object::Stream(s));
};
let mut doc = Document::new_empty();
// 900 px drawn 216 pt (3 in) wide → 300 ppi, like a scanned page.
let scan = image(&mut doc, 900, 900, 3, true, None);
with_decode(&mut doc, scan, [0, 1, 0, 1, 0, 1].map(Object::Int).to_vec());
let gray = image(&mut doc, 900, 900, 1, false, None);
with_decode(&mut doc, gray, vec![Object::Real(0.0), Object::Real(1.0)]);
// A decode array that inverts the colours, and one of the wrong length: left alone.
let inverted = image(&mut doc, 900, 900, 3, true, None);
with_decode(&mut doc, inverted, [1, 0, 1, 0, 1, 0].map(Object::Int).to_vec());
let odd = image(&mut doc, 900, 900, 3, true, None);
with_decode(&mut doc, odd, [0, 1].map(Object::Int).to_vec());
page(
&mut doc,
&[("S", scan), ("G", gray), ("I", inverted), ("O", odd)],
"q 216 0 0 216 0 0 cm /S Do Q q 216 0 0 216 216 0 cm /G Do Q q 216 0 0 216 0 300 cm /I Do Q q 216 0 0 216 216 300 cm /O Do Q",
);
let report = optimize(&mut doc, &Settings::default()).unwrap();
assert_eq!(report.images_resampled, 2, "{report:?}");
assert_eq!(stream(&doc, scan).dict.int(b"Width"), Some(450));
assert_eq!(stream(&doc, gray).dict.int(b"Width"), Some(450));
assert!(!stream(&doc, scan).dict.contains(b"Decode"), "the default decode array isn't carried over");
assert_eq!(stream(&doc, inverted).dict.int(b"Width"), Some(900));
assert_eq!(stream(&doc, odd).dict.int(b"Width"), Some(900));
}
#[test]
fn settings_choose_what_happens() {
let mut doc = Document::new_empty();