prepress: non_exhaustive public enums, and checked plate sizes for hand-made Plates

This commit is contained in:
aiongg committed 2026-10-24 02:42:36 +00:00
1 parent fde8b638c3
commit bdace448d9
5 files changed
+40 -14

No files matched your search

+1
View File
@@ -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),
+15 -3
View File
@@ -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<u8> {
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<Option<&[u8]>> = 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);
}
}
+19 -7
View File
@@ -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<T: Clone>(len: usize, value: T) -> Option<Vec<T>> {
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> {
usize::try_from(self.width).ok()?.checked_mul(usize::try_from(self.height).ok()?)
}
fn index_of(&self, x: u32, y: u32) -> Option<usize> {
(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<usize> {
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<Item = &'a Vec<u8>> + '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<u32> {
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
+1
View File
@@ -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
+4 -4
View File
@@ -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<u8> {
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),