From bdace448d9005a6e107184631cbcf4ea680b002a Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 02:42:36 +0000 Subject: [PATCH] prepress: non_exhaustive public enums, and checked plate sizes for hand-made Plates --- crates/prepress/src/cms.rs | 1 + crates/prepress/src/composite.rs | 18 +++++++++++++++--- crates/prepress/src/coverage.rs | 26 +++++++++++++++++++------- crates/prepress/src/ink.rs | 1 + crates/prepress/src/plates.rs | 8 ++++---- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/crates/prepress/src/cms.rs b/crates/prepress/src/cms.rs index 103d861b..97c5a74d 100644 --- a/crates/prepress/src/cms.rs +++ b/crates/prepress/src/cms.rs @@ -19,6 +19,7 @@ pub struct CmsConversion { /// Why a profile can't be a printing condition. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] +#[non_exhaustive] pub enum ProfileError { #[error("the profile could not be read: {0}")] Unreadable(String), diff --git a/crates/prepress/src/composite.rs b/crates/prepress/src/composite.rs index bc8416a4..fd5d48ec 100644 --- a/crates/prepress/src/composite.rs +++ b/crates/prepress/src/composite.rs @@ -7,6 +7,7 @@ use craft_pdf::interpret::color::{CmykConversion, cmyk_to_srgb8}; use crate::cms::lab_to_srgb8; +use crate::coverage::filled; use crate::{Alternate, InkId, Plates, Process}; /// Pixels converted per call into the CMYK profile, to bound the scratch buffers. @@ -60,10 +61,11 @@ impl Plates { /// colorimetric with black point compensation, or through the bundled CMYK profile when /// `cms` can't. Each spot plate is then multiplied over them by the spot's appearance at /// its tint, from its alternate. Spots whose Lab alternate craft-cms can't convert are - /// left out. + /// left out. Empty when `width × height × 3` overflows or can't be allocated. pub fn composite(&self, enabled: &[bool], cms: &dyn CmykConversion) -> Vec { - let n = (self.width as usize).saturating_mul(self.height as usize); - let mut rgb = vec![255_u8; n.saturating_mul(3)]; + let Some(mut rgb) = self.pixels().and_then(|n| n.checked_mul(3)).and_then(|len| filled(len, 255_u8)) else { + return Vec::new(); + }; let process: Vec> = Process::ALL .iter() .map(|p| { @@ -140,4 +142,14 @@ mod tests { let no_gold = p.composite(&[true, true, true, true, false], &cms); assert_eq!(&no_gold[6..9], black); } + + #[test] + fn plates_too_large_to_hold_give_empty_results() { + let p = crate::Plates { width: u32::MAX, height: u32::MAX, ..synthetic(0, 0, [50.0, 0.0, 0.0], Default::default()) }; + assert!(p.composite(&[], &CmsConversion::fogra39()).is_empty()); + assert!(p.tac(&[]).is_empty()); + assert_eq!(p.stats(&[]).max_total_at, None); + assert!(p.above(10.0, &[]).regions().is_empty()); + assert_eq!(p.tint(crate::InkId::process(crate::Process::Black), u32::MAX - 1, u32::MAX - 1), None); + } } diff --git a/crates/prepress/src/coverage.rs b/crates/prepress/src/coverage.rs index d88cad9a..74563b50 100644 --- a/crates/prepress/src/coverage.rs +++ b/crates/prepress/src/coverage.rs @@ -66,6 +66,14 @@ pub struct Region { pub pixels: u32, } +/// `len` copies of `value`; `None` when the memory can't be had. +pub(crate) fn filled(len: usize, value: T) -> Option> { + let mut v = Vec::new(); + v.try_reserve_exact(len).ok()?; + v.resize(len, value); + Some(v) +} + fn on(enabled: &[bool], index: usize) -> bool { enabled.get(index).copied().unwrap_or(true) } @@ -75,12 +83,16 @@ fn percent(tint: u64) -> f32 { } impl Plates { - fn pixels(&self) -> usize { - (self.width as usize).saturating_mul(self.height as usize) + /// `width × height`; `None` when it overflows `usize`, as it can for hand-made plates. + pub(crate) fn pixels(&self) -> Option { + usize::try_from(self.width).ok()?.checked_mul(usize::try_from(self.height).ok()?) } - fn index_of(&self, x: u32, y: u32) -> Option { - (x < self.width && y < self.height).then(|| y as usize * self.width as usize + x as usize) + pub(crate) fn index_of(&self, x: u32, y: u32) -> Option { + if x >= self.width || y >= self.height { + return None; + } + usize::try_from(y).ok()?.checked_mul(usize::try_from(self.width).ok()?)?.checked_add(usize::try_from(x).ok()?) } fn enabled_planes<'a>(&'a self, enabled: &'a [bool]) -> impl Iterator> + 'a { @@ -112,9 +124,9 @@ impl Plates { } /// The TAC of the enabled inks at every pixel, row-major, as a sum of tint bytes (255 per - /// ink at 100%). + /// ink at 100%). Empty when `width × height` overflows or can't be allocated. fn totals(&self, enabled: &[bool]) -> Vec { - let mut totals = vec![0_u32; self.pixels()]; + let Some(mut totals) = self.pixels().and_then(|n| filled(n, 0_u32)) else { return Vec::new() }; for plane in self.enabled_planes(enabled) { for (sum, tint) in totals.iter_mut().zip(plane) { *sum = sum.saturating_add(u32::from(*tint)); @@ -130,7 +142,7 @@ impl Plates { /// Each ink's figures over the page, and the TAC of the enabled inks. pub fn stats(&self, enabled: &[bool]) -> Stats { - let n = self.pixels(); + let n = self.pixels().unwrap_or(0); let mean = |sum: u64| if n == 0 { 0.0 } else { percent(sum) / n as f32 }; let inks = self .planes diff --git a/crates/prepress/src/ink.rs b/crates/prepress/src/ink.rs index 929d47be..22dfa12f 100644 --- a/crates/prepress/src/ink.rs +++ b/crates/prepress/src/ink.rs @@ -359,6 +359,7 @@ impl InkSet { /// A color as ink tints. #[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] pub enum InkColor { /// Tints `0.0..=1.0` of the listed inks; other inks are unspecified (they keep what is /// under the paint when it overprints, and are knocked out otherwise). One entry is a diff --git a/crates/prepress/src/plates.rs b/crates/prepress/src/plates.rs index 4e2ce33f..0f976a1b 100644 --- a/crates/prepress/src/plates.rs +++ b/crates/prepress/src/plates.rs @@ -53,6 +53,7 @@ const MAX_CONDITION: usize = 160; /// The printing condition process color converts to when the page has no usable output /// intent. #[derive(Clone, Debug, Default)] +#[non_exhaustive] pub enum Fallback { /// craft-cms's Coated FOGRA39 profile. #[default] @@ -82,6 +83,7 @@ impl Default for Options { /// How process color that isn't CMYK already became CMYK. #[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] pub enum Conversion { /// Through the CMYK profile of the page's or document's output intent (ISO 32000-2 /// §14.11.5). @@ -123,14 +125,12 @@ impl Plates { /// Ink `id`'s tint at pixel (`x`, `y`), 0–255. pub fn tint(&self, id: InkId, x: u32, y: u32) -> Option { - if x >= self.width || y >= self.height { - return None; - } - self.plane(id)?.get(y as usize * self.width as usize + x as usize).copied() + self.plane(id)?.get(self.index_of(x, y)?).copied() } } #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum Error { #[error(transparent)] Open(#[from] OpenError),