M11: measure optimizer image resolution in physical page units

This commit is contained in:
Kyle Kassen
2026-10-23 16:37:10 -05:00
parent a2ec27b477
commit bf86689fd7
7 changed files with 95 additions and 8 deletions
+3 -1
View File
@@ -21,7 +21,9 @@ let report = optimize_with_progress(&mut doc, &settings, &mut |stage| keep_going
side-by-side tiles, rotated strips, masks. Content arrays use the shared space boundary
policy, so trailing comments, split operands and graphics state keep their meaning after merging.
- **Images:** the effective resolution of each image is measured where pages draw it (the CTM
through form XObjects, up to 12 deep; the smallest over all uses). If a use can't be
through form XObjects, up to 12 deep; the smallest over all uses).
Page-local `/UserUnit` scaling is included, so large-format pages are measured in physical inches.
If a use can't be
measured (forms nested deeper, or drawn too many times), no image is resampled. Colour and grayscale images above a
threshold are resampled with a bicubic (Catmull-Rom) filter to a target resolution and
stored as JPEG (Acrobat's quality steps) or ZIP; soft masks are resampled with their image.
+6 -3
View File
@@ -158,15 +158,18 @@ pub fn effective_resolutions(doc: &Document, pages: &[ObjRef]) -> HashMap<ObjRef
out: HashMap::new(),
};
for p in pages {
let Some(d) = doc.get(*p).as_dict().cloned() else { continue };
let Some(contents) = d.get(b"Contents") else { continue };
let Some(dict) = doc.get(*p).as_dict().cloned() else { continue };
let page = pdfcraft_model::Page { obj: *p, dict };
let Some(contents) = page.dict.get(b"Contents") else { continue };
let res = page_resources(doc, *p).unwrap_or_default();
walk.budget = DecodeBudget::page();
let Some(data) = content_bytes(doc, contents, &mut walk.budget) else {
walk.complete = false;
continue;
};
walk.content(&data, &res, Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
// Content coordinates are in page units; image resolution is in physical inches.
let unit = page.user_unit(doc);
walk.content(&data, &res, Matrix([unit, 0.0, 0.0, unit, 0.0, 0.0]));
}
if !walk.complete {
walk.out.values_mut().for_each(|v| *v = f64::INFINITY);
+68
View File
@@ -74,6 +74,57 @@ fn stream(doc: &Document, r: ObjRef) -> Stream {
}
}
#[test]
fn image_resolution_uses_the_pages_physical_unit() {
for (unit, scale) in [
(None, 1.0),
(Some(Object::Int(10)), 10.0),
(Some(Object::Real(2.5)), 2.5),
(Some(Object::Int(0)), 1.0),
(Some(Object::Int(-10)), 1.0),
(Some(Object::Real(0.5)), 1.0),
(Some(Object::name("Ten")), 1.0),
(Some(Object::Int(100_000)), pdfcraft_model::MAX_USER_UNIT),
] {
for indirect in [false, true] {
let mut doc = Document::new_empty();
let img = image(&mut doc, 720, 720, 1, false, None);
let p = page(&mut doc, &[("Im", img)], "q 72 0 0 72 0 0 cm /Im Do Q");
let parent = doc.get(p).as_dict().unwrap().reference(b"Parent").unwrap();
// UserUnit is page-local, unlike Resources and MediaBox.
doc.update_dict(parent, |d| d.set(b"UserUnit".to_vec(), Object::Int(10))).unwrap();
if let Some(unit) = unit.clone() {
let unit = if indirect { Object::Ref(doc.add(unit)) } else { unit };
doc.update_dict(p, |d| d.set(b"UserUnit".to_vec(), unit)).unwrap();
}
let ppi = effective_resolutions(&doc, &[p])[&img];
assert!((ppi - 720.0 / scale).abs() < 1e-9, "unit={unit:?}, indirect={indirect}: {ppi}");
}
}
}
#[test]
fn optimization_retains_an_image_below_threshold_on_a_scaled_page() {
let mut doc = Document::new_empty();
let img = image(&mut doc, 720, 720, 3, false, None);
let p = page(&mut doc, &[("Im", img)], "q 72 0 0 72 0 0 cm /Im Do Q");
doc.update_dict(p, |d| d.set(b"UserUnit".to_vec(), Object::Int(10))).unwrap();
// 720 pixels across ten inches is 72 ppi, already below the 225 ppi threshold.
let report = optimize(&mut doc, &Settings::default()).unwrap();
assert_eq!(report.images_resampled, 0, "{report:?}");
let image = stream(&doc, img);
assert_eq!((image.dict.int(b"Width"), image.dict.int(b"Height")), (Some(720), Some(720)));
let reopened = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
let dimensions: Vec<_> = reopened
.scan_objects()
.filter_map(|(_, object)| match &*object {
Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Image") => Some((s.dict.int(b"Width"), s.dict.int(b"Height"))),
_ => None,
})
.collect();
assert_eq!(dimensions, [(Some(720), Some(720))]);
}
#[test]
fn images_are_measured_where_drawn_and_downsampled() {
let mut doc = Document::new_empty();
@@ -925,6 +976,23 @@ fn nested_forms(doc: &mut Document, img: ObjRef, levels: usize) -> ObjRef {
inner
}
#[test]
fn shared_forms_are_measured_in_each_pages_physical_units() {
for reverse in [false, true] {
let mut doc = Document::new_empty();
let img = image(&mut doc, 720, 720, 3, false, None);
let outer = nested_forms(&mut doc, img, 3);
let normal = page(&mut doc, &[("F", outer)], "q 72 0 0 72 0 0 cm /F Do Q");
let scaled = page(&mut doc, &[("F", outer)], "q 72 0 0 72 0 0 cm /F Do Q");
doc.update_dict(scaled, |d| d.set(b"UserUnit".to_vec(), Object::Int(10))).unwrap();
let pages = if reverse { [scaled, normal] } else { [normal, scaled] };
assert_eq!(effective_resolutions(&doc, &pages)[&img], 72.0, "reverse={reverse}");
let report = optimize(&mut doc, &Settings::default()).unwrap();
assert_eq!(report.images_resampled, 0, "{report:?}");
assert_eq!(stream(&doc, img).dict.int(b"Width"), Some(720));
}
}
#[test]
fn a_deeply_nested_form_is_measured_at_its_largest_use() {
// 1200 px drawn 1 in wide (1200 ppi) and then 8 in wide (150 ppi): 150 ppi, so it isn't resampled.