From 7906dfffdd735b3ae5c42ae7169fb76e6edf30f1 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:54:54 +0300 Subject: [PATCH 1/4] print: shrink-to-fit refuses a sheet the written matrix cannot represent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan's scale had no lower bound: for a page far wider than the paper (a runaway-zoom export or hostile MediaBox, which sheets::read accepts on finite positive boxes) the scale falls below what the four-decimal, no-exponent number formatter can write, the '0' it produces is a singular CTM, and the job spooled a blank page and reported success — the same outcome the crate already refuses for a bad UserUnit 'rather than silently printing the wrong physical dimensions'. Refuse what the output cannot represent: parse back what number() would write and error when it is zero or more than one percent off. It only fires for sheets wider than ~4,300 in, so no real job is refused. Regression test: shrink_to_fit_refuses_a_sheet_the_output_cannot_represent — plan returned Ok today; with the bound it is a Spool error and nothing is sent. --- crates/print/src/sheets.rs | 15 +++++++++++++++ crates/print/src/tests/windows_native.rs | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/crates/print/src/sheets.rs b/crates/print/src/sheets.rs index d3c989cd8..b94dca774 100644 --- a/crates/print/src/sheets.rs +++ b/crates/print/src/sheets.rs @@ -1038,6 +1038,21 @@ pub(crate) fn plan(sheets: &[Sheet], paper: (f64, f64), fit: Fit) -> Result= 1.0 && close((w, h), paper) { None } else { + // The matrix is written with at most four decimals and a PDF real has no + // exponent, so a scale that rounds away prints nothing at all and one that + // rounds up prints the wrong size. Refuse both, the way a nondefault + // UserUnit is refused: an error, never a job that spools a blank page. + let written = number(scale).parse::().unwrap_or(0.0); + if written <= 0.0 || (written - scale).abs() > scale * 0.01 { + return Err(Error::Spool(format!( + "shrink to fit cannot place sheet {} ({:.2} x {:.2} in) on the {:.2} x {:.2} in paper; nothing was sent", + i + 1, + w / 72.0, + h / 72.0, + pw / 72.0, + ph / 72.0 + ))); + } let [x0, y0, x1, y1] = sheet.bounds; let (cw, ch) = (x1 - x0, y1 - y0); // The paper in the page's unrotated space. diff --git a/crates/print/src/tests/windows_native.rs b/crates/print/src/tests/windows_native.rs index 157fe68b9..3f5014dcd 100644 --- a/crates/print/src/tests/windows_native.rs +++ b/crates/print/src/tests/windows_native.rs @@ -462,3 +462,15 @@ fn windows_jobs_reach_the_spooler() { let missing = spool::submit(&mixed, &Job { printer: Some(name.into()), ..Job::default() }); assert!(matches!(missing, Err(Error::Spool(ref m)) if m.contains(name)), "{missing:?}"); } + +#[test] +fn shrink_to_fit_refuses_a_sheet_the_output_cannot_represent() { + use crate::sheets::{Fit, plan, read}; + // A runaway-zoom export: 16 000 000 pt wide. Written with four decimals the scale would be + // "0" -- a singular matrix, a blank page, reported back as a spooled job. + let huge = TestPage { media: Some([0.0, 0.0, 16_000_000.0, 792.0]), ..LETTER }; + let pdf = test_pdf(&[huge], false); + let sheets = read(&pdf).unwrap(); + let refused = plan(&sheets.sheets, (612.0, 792.0), Fit::Shrink { margin: 18.0 }); + assert!(matches!(refused, Err(Error::Spool(ref m)) if m.contains("shrink")), "{refused:?}"); +} From 88271c006c375551f58cce8ed773eb31e83eddfb Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:43:32 +0300 Subject: [PATCH 2/4] print: cover the 1% scale tolerance boundary in shrink-to-fit The bound had only the zero-scale side tested. Add the other edge: a scale whose four-decimal serialization is more than 1% off. A 118 750 pt wide sheet on Letter with an 18 pt margin scales by 576 / 118 750 = 0.00485053, written 0.0049 -- 1.02% off -- and the plan refuses as an error before anything is sent. Just inside the bound (118 700 pt: 0.98% off) still prints, its matrix carrying the written 0.0049. --- crates/print/src/tests/windows_native.rs | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/print/src/tests/windows_native.rs b/crates/print/src/tests/windows_native.rs index 3f5014dcd..bf4335093 100644 --- a/crates/print/src/tests/windows_native.rs +++ b/crates/print/src/tests/windows_native.rs @@ -474,3 +474,30 @@ fn shrink_to_fit_refuses_a_sheet_the_output_cannot_represent() { let refused = plan(&sheets.sheets, (612.0, 792.0), Fit::Shrink { margin: 18.0 }); assert!(matches!(refused, Err(Error::Spool(ref m)) if m.contains("shrink")), "{refused:?}"); } + +#[test] +fn shrink_to_fit_holds_the_one_percent_scale_boundary() { + use crate::sheets::{Fit, plan, read}; + // The matrix carries the scale through the four-decimal formatter, so the plan refuses when + // what would be written is more than 1% off the real scale. Letter with an 18 pt margin gives + // 576 pt of printable width: a 118 700 pt wide, 120 000 pt tall sheet (no quarter turn) scales + // by 576 / 118 700 = 0.00485257, written 0.0049 -- 0.98% short, inside the bound, so it + // prints. + let inside = TestPage { media: Some([0.0, 0.0, 118_700.0, 120_000.0]), ..LETTER }; + let pdf = test_pdf(&[inside], false); + let sheets = read(&pdf).unwrap(); + let edits = plan(&sheets.sheets, (612.0, 792.0), Fit::Shrink { margin: 18.0 }).unwrap(); + let updated = sheets.update(&edits).unwrap(); + let frame = frame_of(&updated, pdf.len()); + let written: f64 = frame.split_whitespace().nth(1).unwrap().parse().unwrap(); + let real = edits[0].frame.as_ref().unwrap().matrix[0]; + assert!(written != real && (written - real).abs() < real * 0.01, "just inside the 1% bound the written scale is accepted: {frame}"); + + // A 118 750 pt wide sheet: 576 / 118 750 = 0.00485053, written 0.0049, 1.02% off -- refused as + // an error before anything is sent, rather than printed a percent too small. + let outside = TestPage { media: Some([0.0, 0.0, 118_750.0, 120_000.0]), ..LETTER }; + let pdf = test_pdf(&[outside], false); + let sheets = read(&pdf).unwrap(); + let refused = plan(&sheets.sheets, (612.0, 792.0), Fit::Shrink { margin: 18.0 }); + assert!(matches!(refused, Err(Error::Spool(ref m)) if m.contains("shrink") && m.contains("nothing was sent")), "{refused:?}"); +} From 714b1f8b3fa9d7d3b4aea5a361a40f8ddc0f9811 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:16:46 +0300 Subject: [PATCH 3/4] cms: index the primaries with an iterator in the 16-way sum clippy's needless_range_loop (stable) flags the loop variable indexing self.prim_n; the offset is still the bit mask. --- crates/cms/src/synth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cms/src/synth.rs b/crates/cms/src/synth.rs index e52deb6aa..a9799195c 100644 --- a/crates/cms/src/synth.rs +++ b/crates/cms/src/synth.rs @@ -133,7 +133,7 @@ impl CmykModel { (v + 4.0 * self.tvi[i] * v * (1.0 - v)).clamp(0.0, 1.0) }); let mut s = [0.0; 3]; - for m in 0..16 { + for (m, prim) in self.prim_n.iter().enumerate().take(16) { let mut w = 1.0; for (i, ai) in a.iter().enumerate() { w *= if m & (1 << i) != 0 { *ai } else { 1.0 - ai }; @@ -141,7 +141,7 @@ impl CmykModel { if w == 0.0 { continue; } - for (acc, p) in s.iter_mut().zip(&self.prim_n[m]) { + for (acc, p) in s.iter_mut().zip(prim) { *acc += w * p; } } From 72b6005033e09b8ea2f48943493510cbcdbd8cb6 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:23:49 +0300 Subject: [PATCH 4/4] Revert "cms: index the primaries with an iterator in the 16-way sum" This reverts commit 714b1f8b3fa9d7d3b4aea5a361a40f8ddc0f9811. --- crates/cms/src/synth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cms/src/synth.rs b/crates/cms/src/synth.rs index a9799195c..e52deb6aa 100644 --- a/crates/cms/src/synth.rs +++ b/crates/cms/src/synth.rs @@ -133,7 +133,7 @@ impl CmykModel { (v + 4.0 * self.tvi[i] * v * (1.0 - v)).clamp(0.0, 1.0) }); let mut s = [0.0; 3]; - for (m, prim) in self.prim_n.iter().enumerate().take(16) { + for m in 0..16 { let mut w = 1.0; for (i, ai) in a.iter().enumerate() { w *= if m & (1 << i) != 0 { *ai } else { 1.0 - ai }; @@ -141,7 +141,7 @@ impl CmykModel { if w == 0.0 { continue; } - for (acc, p) in s.iter_mut().zip(prim) { + for (acc, p) in s.iter_mut().zip(&self.prim_n[m]) { *acc += w * p; } }