M11: measure every use of an image drawn through deeply nested forms (#1811) (#1933)

The effective-resolution walk skipped any form XObject already visited
once when it was nested more than four deep, with one seen-set for the
whole document. A later, larger use of the same form was never measured,
so Reduce File Size and Optimize PDF resampled the image for its smallest
use: a 1200 px image drawn 1 in and 8 in wide through six nested forms
came out 150 px wide, about 19 ppi on the large copy.

- Cycles are detected on the current path of forms, not document-wide.
- A form with its own resources drawn again at the same scale (only
  moved) is skipped, as that can't change any image's resolution; forms
  without resources draw their caller's images and are always walked.
- Repeat walks are capped at 100,000. If the cap or the 12-level depth
  limit leaves a use unmeasured, every image gets INFINITY: recompressed
  as the settings say, never resampled.

Fixes #1811

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
mlkonrad
2026-10-15 02:30:30 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 9caddc6e9b
commit 3f25d55a96
4 changed files with 213 additions and 53 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ let report = optimize_with_progress(&mut doc, &settings, &mut |stage| keep_going
printed this way went from 4.3 MB to 231 KB (Standard) and 161 KB (Mobile). Not merged yet:
side-by-side tiles, rotated strips, masks.
- **Images:** the effective resolution of each image is measured where pages draw it (the CTM
through form XObjects; the smallest over all uses). Colour and grayscale images above a
through form XObjects, up to 12 deep; the smallest over all uses). 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.
An image is replaced only if the result is smaller. Left alone: CMYK and other colour spaces,
+102 -52
View File
@@ -41,73 +41,123 @@ pub(crate) fn content_bytes(doc: &Document, contents: &Object) -> Vec<u8> {
}
}
/// Walk one content stream, recording each image's displayed resolution (pixels per inch, the
/// smaller of the two axes) and descending into form XObjects.
fn walk(doc: &Document, data: &[u8], resources: &Dict, ctm: Matrix, depth: usize, seen: &mut HashSet<ObjRef>, out: &mut HashMap<ObjRef, f64>) {
if depth > 12 {
return;
}
let xobjects = resources.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default();
let mut stack = vec![ctm];
for op in pdfcraft_content::parse(data).ops {
let top = stack.last().copied().unwrap_or(ctm);
match op.op.as_slice() {
b"q" => stack.push(top),
b"Q" if stack.len() > 1 => {
stack.pop();
}
b"cm" => {
if let Some(m) = Matrix::from_operands(&op.operands)
&& let Some(t) = stack.last_mut()
{
*t = m.then(&top);
/// Forms nested deeper than this aren't followed.
const MAX_FORM_DEPTH: usize = 12;
/// Walks of a form already walked once (other scale or caller) before the measurement gives up.
/// Each form's first walk is free: their number is bounded by the file's object count.
const MAX_REWALKS: usize = 100_000;
/// State of one [`effective_resolutions`] pass.
struct Walk<'a> {
doc: &'a Document,
/// The forms being walked right now, outermost first: a form drawing one of these is a cycle.
path: Vec<ObjRef>,
/// Forms with their own resources already walked, by the linear part of their matrix. Drawing
/// one again at the same scale (only moved) can't change any image's resolution.
walked: HashSet<(ObjRef, [u64; 4])>,
/// Forms walked at least once.
seen: HashSet<ObjRef>,
rewalks: usize,
/// False once a use may have been missed (depth or work limit).
complete: bool,
out: HashMap<ObjRef, f64>,
}
impl Walk<'_> {
/// Walk one content stream, recording each image's displayed resolution (pixels per inch, the
/// smaller of the two axes) and descending into form XObjects.
fn content(&mut self, data: &[u8], resources: &Dict, ctm: Matrix) {
let doc = self.doc;
let xobjects = resources.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default();
let mut stack = vec![ctm];
for op in pdfcraft_content::parse(data).ops {
let top = stack.last().copied().unwrap_or(ctm);
match op.op.as_slice() {
b"q" => stack.push(top),
b"Q" if stack.len() > 1 => {
stack.pop();
}
}
b"Do" => {
let Some(name) = op.name(0) else { continue };
let Some(r) = xobjects.get(name).and_then(Object::as_ref) else { continue };
let obj = doc.get(r);
let Object::Stream(s) = &*obj else { continue };
match s.dict.name(b"Subtype") {
Some(b"Image") => {
let (w, h) = (s.dict.int(b"Width").unwrap_or(0) as f64, s.dict.int(b"Height").unwrap_or(0) as f64);
let [a, b, c, d, ..] = top.0;
let (sx, sy) = ((a * a + b * b).sqrt(), (c * c + d * d).sqrt());
if w > 0.0 && h > 0.0 && sx > 1e-6 && sy > 1e-6 {
let ppi = (w / (sx / 72.0)).min(h / (sy / 72.0));
let e = out.entry(r).or_insert(f64::INFINITY);
*e = e.min(ppi);
}
b"cm" => {
if let Some(m) = Matrix::from_operands(&op.operands)
&& let Some(t) = stack.last_mut()
{
*t = m.then(&top);
}
Some(b"Form") => {
if !seen.insert(r) && depth > 4 {
continue;
}
let m = s.dict.get(b"Matrix").map(|m| doc.resolve(m)).and_then(|m| m.as_array().map(|a| Matrix::from_operands(a))).flatten();
let res =
s.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_else(|| resources.clone());
let inner = m.unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])).then(&top);
walk(doc, &s.decoded().unwrap_or_default(), &res, inner, depth + 1, seen, out);
}
_ => {}
}
b"Do" => {
let Some(name) = op.name(0) else { continue };
let Some(r) = xobjects.get(name).and_then(Object::as_ref) else { continue };
let obj = doc.get(r);
let Object::Stream(s) = &*obj else { continue };
match s.dict.name(b"Subtype") {
Some(b"Image") => {
let (w, h) = (s.dict.int(b"Width").unwrap_or(0) as f64, s.dict.int(b"Height").unwrap_or(0) as f64);
let [a, b, c, d, ..] = top.0;
let (sx, sy) = ((a * a + b * b).sqrt(), (c * c + d * d).sqrt());
if w > 0.0 && h > 0.0 && sx > 1e-6 && sy > 1e-6 {
let ppi = (w / (sx / 72.0)).min(h / (sy / 72.0));
let e = self.out.entry(r).or_insert(f64::INFINITY);
*e = e.min(ppi);
}
}
Some(b"Form") => self.form(r, s, resources, top),
_ => {}
}
}
_ => {}
}
_ => {}
}
}
/// Descend into form `r` drawn with `ctm` from a stream whose resources are `resources`.
fn form(&mut self, r: ObjRef, s: &Stream, resources: &Dict, ctm: Matrix) {
if self.path.contains(&r) {
return;
}
if self.path.len() >= MAX_FORM_DEPTH {
self.complete = false;
return;
}
let doc = self.doc;
let m = s.dict.get(b"Matrix").map(|m| doc.resolve(m)).and_then(|m| m.as_array().map(|a| Matrix::from_operands(a))).flatten();
let inner = m.unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])).then(&ctm);
let own = s.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned());
// A form without resources uses its caller's, so the same form can draw other images.
if own.is_some() {
let [a, b, c, d, ..] = inner.0;
if !self.walked.insert((r, [a.to_bits(), b.to_bits(), c.to_bits(), d.to_bits()])) {
return;
}
}
if !self.seen.insert(r) {
if self.rewalks >= MAX_REWALKS {
self.complete = false;
return;
}
self.rewalks += 1;
}
self.path.push(r);
self.content(&s.decoded().unwrap_or_default(), own.as_ref().unwrap_or(resources), inner);
self.path.pop();
}
}
/// The effective resolution (ppi) of every image drawn by `pages`: the smallest over its uses.
///
/// If forms nest too deeply or are drawn too many times to measure every use, every image is
/// given `INFINITY` (never resampled), since its largest use may be among those missed.
pub fn effective_resolutions(doc: &Document, pages: &[ObjRef]) -> HashMap<ObjRef, f64> {
let mut out = HashMap::new();
let mut seen = HashSet::new();
let mut walk = Walk { doc, path: Vec::new(), walked: HashSet::new(), seen: HashSet::new(), rewalks: 0, complete: true, 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 res = page_resources(doc, *p).unwrap_or_default();
walk(doc, &content_bytes(doc, contents), &res, Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]), 0, &mut seen, &mut out);
walk.content(&content_bytes(doc, contents), &res, Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
}
out
if !walk.complete {
walk.out.values_mut().for_each(|v| *v = f64::INFINITY);
}
walk.out
}
/// Colour components of a colour space PdfCraft resamples: gray (1) or RGB (3).
+107
View File
@@ -821,3 +821,110 @@ fn images_that_do_not_tile_a_picture_are_left_alone() {
let (mut off, _) = printed_page(40, |_| false, |_, s| s);
unmerged(&mut off, &Settings { merge_fragments: false, ..Settings::default() });
}
/// `levels` nested forms with their own resources, the innermost drawing `img` over the unit square.
fn nested_forms(doc: &mut Document, img: ObjRef, levels: usize) -> ObjRef {
let mut inner = form(doc, Some(Object::Dict(resources(xobjects(&[("Im", img)])))), "/Im Do");
for _ in 1..levels {
inner = form(doc, Some(Object::Dict(resources(xobjects(&[("F", inner)])))), "/F Do");
}
inner
}
#[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.
let small = "q 72 0 0 72 36 36 cm /F Do Q";
let large = "q 576 0 0 576 18 100 cm /F Do Q";
for levels in [1, 6, 12] {
for (pages, label) in [(vec![format!("{small} {large}")], "one page"), (vec![small.into(), large.into()], "two pages")] {
let mut doc = Document::new_empty();
let img = image(&mut doc, 1200, 1200, 3, false, None);
let outer = nested_forms(&mut doc, img, levels);
for c in &pages {
page(&mut doc, &[("F", outer)], c);
}
let refs = pdfcraft_annot::page_refs(&doc).unwrap();
assert_eq!(effective_resolutions(&doc, &refs)[&img].round(), 150.0, "{levels} levels, {label}");
optimize(&mut doc, &Settings::default()).unwrap();
assert_eq!(stream(&doc, img).dict.int(b"Width"), Some(1200), "{levels} levels, {label}");
}
}
}
#[test]
fn a_form_drawing_itself_is_walked_once_per_use() {
let mut doc = Document::new_empty();
let img = image(&mut doc, 1200, 1200, 3, false, None);
let f = form(&mut doc, None, "/Im Do q 2 0 0 2 0 0 cm /F Do Q");
let mut x = xobjects(&[("Im", img)]);
x.set(b"F".to_vec(), Object::Ref(f));
let mut d = match &*doc.get(f) {
Object::Stream(s) => s.clone(),
_ => unreachable!(),
};
d.dict.set(b"Resources".to_vec(), Object::Dict(resources(x)));
doc.set(f, Object::Stream(d));
page(&mut doc, &[("F", f)], "q 72 0 0 72 36 36 cm /F Do Q");
let refs = pdfcraft_annot::page_refs(&doc).unwrap();
// Only the outer use: the cycle isn't followed, and doesn't count as a missed use.
assert_eq!(effective_resolutions(&doc, &refs)[&img].round(), 1200.0);
}
#[test]
fn a_shared_form_without_resources_draws_each_callers_image() {
// G has no /Resources, so its /Im is whichever image the form drawing it names.
let mut doc = Document::new_empty();
let a = image(&mut doc, 1200, 1200, 3, false, None);
let b = image(&mut doc, 1200, 1200, 3, false, None);
let g = form(&mut doc, None, "/Im Do");
let mut outers = Vec::new();
for img in [a, b] {
let mut inner = form(&mut doc, Some(Object::Dict(resources(xobjects(&[("Im", img), ("G", g)])))), "/G Do");
for _ in 1..6 {
inner = form(&mut doc, Some(Object::Dict(resources(xobjects(&[("F", inner)])))), "/F Do");
}
outers.push(inner);
}
page(&mut doc, &[("A", outers[0]), ("B", outers[1])], "q 72 0 0 72 36 36 cm /A Do Q q 576 0 0 576 18 100 cm /B Do Q");
let refs = pdfcraft_annot::page_refs(&doc).unwrap();
let ppi = effective_resolutions(&doc, &refs);
assert_eq!(ppi[&a].round(), 1200.0);
assert_eq!(ppi.get(&b).map(|p| p.round()), Some(150.0));
}
#[test]
fn images_are_not_resampled_when_some_uses_could_not_be_measured() {
// Too deep: the 13th form isn't followed, so its use of the image is unknown.
let mut doc = Document::new_empty();
let img = image(&mut doc, 1200, 1200, 3, false, None);
let other = image(&mut doc, 1200, 1200, 3, false, None);
let deep = nested_forms(&mut doc, img, 13);
page(
&mut doc,
&[("F", deep), ("Im", img), ("O", other)],
"q 72 0 0 72 36 36 cm /Im Do Q q 72 0 0 72 200 36 cm /O Do Q q 576 0 0 576 18 100 cm /F Do Q",
);
let refs = pdfcraft_annot::page_refs(&doc).unwrap();
let ppi = effective_resolutions(&doc, &refs);
assert_eq!((ppi[&img], ppi[&other]), (f64::INFINITY, f64::INFINITY));
optimize(&mut doc, &Settings::default()).unwrap();
assert_eq!(stream(&doc, img).dict.int(b"Width"), Some(1200));
// Too much work: forms without resources (so nothing can be skipped), each drawing the next
// three times, twelve deep, would be (3^12 - 1) / 2 walks.
let mut doc = Document::new_empty();
let img = image(&mut doc, 1200, 1200, 3, false, None);
let mut names = vec![("Im", img)];
let mut next = "Im";
let labels = ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"];
for (i, label) in labels.iter().enumerate().rev() {
let s = 1.0 + i as f64 / 100.0;
let f = form(&mut doc, None, &format!("q {s} 0 0 {s} 0 0 cm /{next} Do Q /{next} Do q 1 0 0 1 1 0 cm /{next} Do Q"));
names.push((label, f));
next = label;
}
page(&mut doc, &names, "q 72 0 0 72 36 36 cm /F1 Do Q");
let refs = pdfcraft_annot::page_refs(&doc).unwrap();
assert_eq!(effective_resolutions(&doc, &refs)[&img], f64::INFINITY);
}
+2
View File
@@ -7264,6 +7264,8 @@ commands = ["optimize.advanced"]
tools = ["doc_optimize"]
evidence = ["crates/optimize/tests/optimize.rs::images_are_measured_where_drawn_and_downsampled", "crates/optimize/tests/optimize.rs::settings_choose_what_happens", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/optimize/tests/optimize.rs::a_page_picture_printed_as_one_pixel_strips_is_merged_into_one_image", "crates/optimize/tests/optimize.rs::images_that_do_not_tile_a_picture_are_left_alone"]
notes = "colour and grayscale, bicubic, effective resolution measured where drawn; image fragments stacked edge to edge are merged first (side-by-side tiles not yet); monochrome not yet"
evidence = ["crates/optimize/tests/optimize.rs::images_are_measured_where_drawn_and_downsampled", "crates/optimize/tests/optimize.rs::a_deeply_nested_form_is_measured_at_its_largest_use", "crates/optimize/tests/optimize.rs::images_are_not_resampled_when_some_uses_could_not_be_measured", "crates/optimize/tests/optimize.rs::settings_choose_what_happens", "crates/automation/tests/automation.rs::optimizing_through_tools"]
notes = "colour and grayscale, bicubic, effective resolution measured where drawn (every use, through nested forms up to 12 deep; when a use can't be measured, nothing is resampled); monochrome not yet"
[[feature]]
id = "optimize.recompress-images"