diff --git a/crates/print/src/sheets.rs b/crates/print/src/sheets.rs index 0f013478b..fa95d11c4 100644 --- a/crates/print/src/sheets.rs +++ b/crates/print/src/sheets.rs @@ -1136,6 +1136,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 7882a0578..12c480f22 100644 --- a/crates/print/src/tests/windows_native.rs +++ b/crates/print/src/tests/windows_native.rs @@ -555,3 +555,42 @@ 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:?}"); +} + +#[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:?}"); +}