mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 14:38:13 -06:00
M11: measure optimizer image resolution in physical page units
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user