mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 16:31:45 +00:00
craft-cms: return errors instead of panicking on caller input
The crate denies panics, but asserts and slice indexing were reachable from arguments: pixel strides, buffers that are not whole pixels or a destination shorter than the source, single-colour slices shorter than the channel count, CLUT and pipeline dimensions, and LUT sizes. Every such check now returns CmsError::InvalidArgument. Buffer conversions validate once per call, before any pixel is written, so the per-pixel loops carry no extra checks; internal callers that already guarantee lengths use crate-private unchecked evaluation. Pipeline, Clut and Lut3d keep their fields private (read through accessors) so a validated value cannot be changed into one that panics. Pipeline::new checks matrix shapes and channel counts; Clut::new and Clut::sample check grid, outputs and the table size limit. API changes (unreleased crate): - Transform::eval, eval_fast, convert_u8/u16/f32, apply, convert_bytes_many and from_pipeline return Result. - Pipeline::new, extend and eval, Clut::new, sample, eval, eval_trilinear and eval_multilinear return Result. - GamutCheck::delta_e and out_of_gamut, Lut3d::from_transform, LutFile::identity and from_fn, synth::cmyk_profile and coated_cmyk return Result. - Pipeline, Clut and Lut3d fields are read through methods.
This commit is contained in:
@@ -18,14 +18,19 @@ use craft_cms::{Builtin, Intent, Profile, Transform};
|
||||
let src = Profile::parse(&icc_bytes)?; // v2/v4: matrix/TRC, gray TRC, mft1/mft2/mAB/mBA
|
||||
let dst = Builtin::CoatedCmyk.profile();
|
||||
let t = Transform::new(&src, dst, Intent::RelativeColorimetric, /*bpc*/ true)?;
|
||||
t.convert_u8(&rgba8, 4, &mut cmyka8, 5, /*copy alpha*/ true); // also convert_u16 / convert_f32 / apply (in place)
|
||||
t.convert_u8(&rgba8, 4, &mut cmyka8, 5, /*copy alpha*/ true)?; // also convert_u16 / convert_f32 / apply (in place)
|
||||
let mut out = [0.0; 4];
|
||||
t.eval(&[1.0, 0.0, 0.0], &mut out); // exact single colour
|
||||
t.eval(&[1.0, 0.0, 0.0], &mut out)?; // exact single colour
|
||||
```
|
||||
|
||||
Calls never panic on caller input: strides below the channel count, buffers that are not
|
||||
whole pixels, a destination with fewer pixels than the source, slices shorter than the
|
||||
channel count and out-of-range table or LUT dimensions return `CmsError::InvalidArgument`.
|
||||
Buffers are checked once per call, before any pixel is written.
|
||||
|
||||
* `Transform::proof(src, proof, display, intent, bpc, simulate_paper)`: soft-proof chain.
|
||||
* `GamutCheck::new(src, proof, ΔE)`: out-of-gamut test (gamut warning).
|
||||
* `Lut3d::from_transform(&t, 33).to_rgba16f_bytes()`: display transform as a 3D texture.
|
||||
* `Lut3d::from_transform(&t, 33)?.to_rgba16f_bytes()`: display transform as a 3D texture.
|
||||
* `transform::cached(src, dst, opts)`: process-wide transform cache.
|
||||
* `lutfile::parse(name, bytes)` / `lutfile::write_cube(&lut)`: `.cube`, `.3dl` and `.look` LUT
|
||||
files; `lutfile::builtin(id)` returns generated looks (`lutfile::BUILTIN`).
|
||||
|
||||
@@ -323,12 +323,35 @@ fn build(b: Builtin) -> Profile {
|
||||
Builtin::GrayGamma22 => gray(b.description(), Curve::Gamma(2.2)),
|
||||
Builtin::SGray => gray(b.description(), srgb_trc()),
|
||||
Builtin::LabD50 => lab(),
|
||||
// The shipped bytes are `synth::coated_cmyk()` (checked by the `regen` test); rebuild
|
||||
// them rather than crash if they ever fail to parse.
|
||||
Builtin::CoatedCmyk => Profile::parse(COATED_CMYK_ICC).unwrap_or_else(|_| crate::synth::coated_cmyk()),
|
||||
// The shipped bytes are `synth::coated_cmyk()` (checked by the `regen` test).
|
||||
Builtin::CoatedCmyk => shipped_cmyk(COATED_CMYK_ICC, b),
|
||||
// The fit needs the measurement data, which only the tests have; the `fogra39` tests
|
||||
// check that the shipped bytes parse, so the synthetic coated profile is a fallback
|
||||
// that does not happen.
|
||||
Builtin::CoatedFogra39 => Profile::parse(COATED_FOGRA39_ICC).unwrap_or_else(|_| crate::synth::coated_cmyk()),
|
||||
// check that the shipped bytes parse.
|
||||
Builtin::CoatedFogra39 => shipped_cmyk(COATED_FOGRA39_ICC, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// A shipped CMYK profile. Should its bytes ever fail to parse, the synthetic coated profile
|
||||
/// is generated instead, and failing that a CMYK profile without tables stands in (transforms
|
||||
/// refuse it with an error) rather than crash.
|
||||
fn shipped_cmyk(bytes: &[u8], b: Builtin) -> Profile {
|
||||
Profile::parse(bytes).or_else(|_| crate::synth::coated_cmyk()).unwrap_or_else(|_| Profile {
|
||||
version: (4, 0x30),
|
||||
class: ProfileClass::Output,
|
||||
color_space: ColorSpace::Cmyk,
|
||||
pcs: Pcs::Lab,
|
||||
rendering_intent: Intent::Perceptual,
|
||||
description: b.description().into(),
|
||||
copyright: COPYRIGHT.into(),
|
||||
white_point: math::d50_quantized(),
|
||||
black_point: None,
|
||||
chad: None,
|
||||
matrix: None,
|
||||
trc: None,
|
||||
gray_trc: None,
|
||||
a2b: [None, None, None],
|
||||
b2a: [None, None, None],
|
||||
bytes: None,
|
||||
hash: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
+99
-35
@@ -5,48 +5,90 @@
|
||||
//! Kasson et al. 1995); 4-input tables interpolate linearly along the first input between
|
||||
//! two tetrahedral evaluations; other input counts use multilinear interpolation.
|
||||
|
||||
use crate::{CmsError, check_len, invalid_arg};
|
||||
|
||||
/// Maximum number of input or output channels of a CLUT.
|
||||
pub const MAX_CHANNELS: usize = 15;
|
||||
|
||||
/// Largest number of stored values (nodes × outputs) a table may hold.
|
||||
pub const MAX_VALUES: usize = 16 * 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Clut {
|
||||
pub inputs: usize,
|
||||
pub outputs: usize,
|
||||
/// Grid points per input dimension (each ≥ 2, or 1 for a degenerate dimension).
|
||||
pub grid: Vec<usize>,
|
||||
/// Normalised node values, `outputs` per node.
|
||||
pub data: Vec<f32>,
|
||||
pub(crate) inputs: usize,
|
||||
pub(crate) outputs: usize,
|
||||
pub(crate) grid: Vec<usize>,
|
||||
pub(crate) data: Vec<f32>,
|
||||
strides: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Clut {
|
||||
/// Builds a table from node data. Panics if `data` has the wrong length.
|
||||
pub fn new(grid: Vec<usize>, outputs: usize, data: Vec<f32>) -> Self {
|
||||
/// Builds a table from node data: `grid` holds the points per input dimension (1 to
|
||||
/// [`MAX_CHANNELS`] dimensions, each ≥ 2, or 1 for a degenerate dimension), `data` holds
|
||||
/// `outputs` values per node. Refuses inconsistent dimensions with
|
||||
/// [`CmsError::InvalidArgument`].
|
||||
pub fn new(grid: Vec<usize>, outputs: usize, data: Vec<f32>) -> Result<Self, CmsError> {
|
||||
let total = Self::check_dims(&grid, outputs)?;
|
||||
if data.len() != total {
|
||||
return Err(invalid_arg(format!("CLUT data has {} values, the grid needs {total}", data.len())));
|
||||
}
|
||||
Ok(Self::from_checked(grid, outputs, data))
|
||||
}
|
||||
|
||||
/// Number of values a `grid` × `outputs` table stores, or why it is refused.
|
||||
fn check_dims(grid: &[usize], outputs: usize) -> Result<usize, CmsError> {
|
||||
if !(1..=MAX_CHANNELS).contains(&grid.len()) || !(1..=MAX_CHANNELS).contains(&outputs) {
|
||||
return Err(invalid_arg(format!("CLUT with {} inputs and {outputs} outputs (1 to {MAX_CHANNELS} each)", grid.len())));
|
||||
}
|
||||
if grid.contains(&0) {
|
||||
return Err(invalid_arg("CLUT grid with zero points in a dimension"));
|
||||
}
|
||||
grid.iter()
|
||||
.try_fold(outputs, |n, g| n.checked_mul(*g))
|
||||
.filter(|n| *n <= MAX_VALUES)
|
||||
.ok_or_else(|| invalid_arg(format!("CLUT grid {grid:?} × {outputs} exceeds {MAX_VALUES} values")))
|
||||
}
|
||||
|
||||
fn from_checked(grid: Vec<usize>, outputs: usize, data: Vec<f32>) -> Self {
|
||||
let inputs = grid.len();
|
||||
assert!((1..=MAX_CHANNELS).contains(&inputs) && (1..=MAX_CHANNELS).contains(&outputs), "bad CLUT dimensions");
|
||||
let mut strides = vec![0usize; inputs];
|
||||
let mut s = outputs;
|
||||
for i in (0..inputs).rev() {
|
||||
strides[i] = s;
|
||||
s *= grid[i].max(1);
|
||||
s *= grid[i];
|
||||
}
|
||||
assert_eq!(data.len(), s, "CLUT data length");
|
||||
Clut { inputs, outputs, grid, data, strides }
|
||||
}
|
||||
|
||||
pub fn inputs(&self) -> usize {
|
||||
self.inputs
|
||||
}
|
||||
pub fn outputs(&self) -> usize {
|
||||
self.outputs
|
||||
}
|
||||
/// Grid points per input dimension.
|
||||
pub fn grid(&self) -> &[usize] {
|
||||
&self.grid
|
||||
}
|
||||
/// Normalised node values, `outputs` per node, first input varying slowest.
|
||||
pub fn data(&self) -> &[f32] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Number of nodes.
|
||||
pub fn nodes(&self) -> usize {
|
||||
self.data.len() / self.outputs
|
||||
}
|
||||
|
||||
/// Builds a table by sampling `f` at every node (inputs normalised to `[0, 1]`).
|
||||
pub fn sample(grid: Vec<usize>, outputs: usize, mut f: impl FnMut(&[f32], &mut [f32])) -> Self {
|
||||
/// Builds a table by sampling `f` at every node (inputs normalised to `[0, 1]`); the
|
||||
/// dimensions are checked as in [`Clut::new`].
|
||||
pub fn sample(grid: Vec<usize>, outputs: usize, mut f: impl FnMut(&[f32], &mut [f32])) -> Result<Self, CmsError> {
|
||||
let total = Self::check_dims(&grid, outputs)?;
|
||||
let inputs = grid.len();
|
||||
let total: usize = grid.iter().product();
|
||||
let mut data = vec![0.0f32; total * outputs];
|
||||
let mut data = vec![0.0f32; total];
|
||||
let mut idx = vec![0usize; inputs];
|
||||
let mut inp = vec![0.0f32; inputs];
|
||||
for node in 0..total {
|
||||
for (node, out) in data.chunks_exact_mut(outputs).enumerate() {
|
||||
let mut rem = node;
|
||||
for d in (0..inputs).rev() {
|
||||
idx[d] = rem % grid[d];
|
||||
@@ -55,9 +97,9 @@ impl Clut {
|
||||
for d in 0..inputs {
|
||||
inp[d] = if grid[d] > 1 { idx[d] as f32 / (grid[d] - 1) as f32 } else { 0.0 };
|
||||
}
|
||||
f(&inp, &mut data[node * outputs..(node + 1) * outputs]);
|
||||
f(&inp, out);
|
||||
}
|
||||
Clut::new(grid, outputs, data)
|
||||
Ok(Self::from_checked(grid, outputs, data))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -71,9 +113,22 @@ impl Clut {
|
||||
(i * self.strides[d], p - i as f32, self.strides[d])
|
||||
}
|
||||
|
||||
/// Interpolates at `input` (`inputs` values) into `out` (`outputs` values).
|
||||
/// Interpolates at `input` (at least `inputs` values) into `out` (at least `outputs`
|
||||
/// values).
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) -> Result<(), CmsError> {
|
||||
self.check_io(input, out)?;
|
||||
self.interp(input, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_io(&self, input: &[f32], out: &[f32]) -> Result<(), CmsError> {
|
||||
check_len("CLUT input", input.len(), self.inputs)?;
|
||||
check_len("CLUT output", out.len(), self.outputs)
|
||||
}
|
||||
|
||||
/// [`Clut::eval`] for callers that guarantee the slice lengths.
|
||||
#[inline]
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) {
|
||||
pub(crate) fn interp(&self, input: &[f32], out: &mut [f32]) {
|
||||
match self.inputs {
|
||||
1 => self.eval1(input[0], out),
|
||||
3 => {
|
||||
@@ -100,7 +155,7 @@ impl Clut {
|
||||
out[..self.outputs].copy_from_slice(&lo[..self.outputs]);
|
||||
}
|
||||
}
|
||||
_ => self.eval_multilinear(input, out),
|
||||
_ => self.multilinear(input, out),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,13 +208,21 @@ impl Clut {
|
||||
}
|
||||
|
||||
/// Trilinear interpolation (3 inputs), kept as a reference for accuracy comparisons.
|
||||
pub fn eval_trilinear(&self, input: &[f32], out: &mut [f32]) {
|
||||
assert_eq!(self.inputs, 3);
|
||||
self.eval_multilinear(input, out);
|
||||
pub fn eval_trilinear(&self, input: &[f32], out: &mut [f32]) -> Result<(), CmsError> {
|
||||
if self.inputs != 3 {
|
||||
return Err(invalid_arg(format!("trilinear interpolation of a {}-input CLUT", self.inputs)));
|
||||
}
|
||||
self.eval_multilinear(input, out)
|
||||
}
|
||||
|
||||
/// Multilinear interpolation over all 2^n cube corners.
|
||||
pub fn eval_multilinear(&self, input: &[f32], out: &mut [f32]) {
|
||||
pub fn eval_multilinear(&self, input: &[f32], out: &mut [f32]) -> Result<(), CmsError> {
|
||||
self.check_io(input, out)?;
|
||||
self.multilinear(input, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn multilinear(&self, input: &[f32], out: &mut [f32]) {
|
||||
let n = self.inputs;
|
||||
let mut base = 0usize;
|
||||
let mut r = [0.0f32; MAX_CHANNELS];
|
||||
@@ -205,28 +268,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn exact_on_nodes_and_linear_functions() {
|
||||
let c = Clut::sample(vec![5, 5, 5], 3, f3);
|
||||
let c = Clut::sample(vec![5, 5, 5], 3, f3).unwrap();
|
||||
let mut o = [0.0; 3];
|
||||
c.eval(&[0.25, 0.5, 0.75], &mut o);
|
||||
c.eval(&[0.25, 0.5, 0.75], &mut o).unwrap();
|
||||
let mut w = [0.0; 3];
|
||||
f3(&[0.25, 0.5, 0.75], &mut w);
|
||||
for i in 0..3 {
|
||||
assert!((o[i] - w[i]).abs() < 1e-6);
|
||||
}
|
||||
// Linear function is reproduced exactly anywhere.
|
||||
c.eval(&[0.13, 0.77, 0.41], &mut o);
|
||||
c.eval(&[0.13, 0.77, 0.41], &mut o).unwrap();
|
||||
assert!((o[0] - (0.13 * 0.5 + 0.77 * 0.3 + 0.41 * 0.2)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_d_and_multilinear_agree_on_linear() {
|
||||
let f = |p: &[f32], o: &mut [f32]| o[0] = 0.1 * p[0] + 0.2 * p[1] + 0.3 * p[2] + 0.4 * p[3];
|
||||
let c = Clut::sample(vec![3, 4, 5, 6], 1, f);
|
||||
let c = Clut::sample(vec![3, 4, 5, 6], 1, f).unwrap();
|
||||
let mut a = [0.0];
|
||||
let mut b = [0.0];
|
||||
for p in [[0.1f32, 0.9, 0.3, 0.7], [1.0, 0.0, 0.5, 0.5], [0.33, 0.66, 0.99, 0.01]] {
|
||||
c.eval(&p, &mut a);
|
||||
c.eval_multilinear(&p, &mut b);
|
||||
c.eval(&p, &mut a).unwrap();
|
||||
c.eval_multilinear(&p, &mut b).unwrap();
|
||||
let want = 0.1 * p[0] + 0.2 * p[1] + 0.3 * p[2] + 0.4 * p[3];
|
||||
assert!((a[0] - want).abs() < 1e-5 && (b[0] - want).abs() < 1e-5);
|
||||
}
|
||||
@@ -237,12 +300,13 @@ mod tests {
|
||||
let c = Clut::sample(vec![3], 2, |p, o| {
|
||||
o[0] = p[0];
|
||||
o[1] = 1.0 - p[0];
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
let mut o = [0.0; 2];
|
||||
c.eval(&[0.25], &mut o);
|
||||
c.eval(&[0.25], &mut o).unwrap();
|
||||
assert!((o[0] - 0.25).abs() < 1e-6 && (o[1] - 0.75).abs() < 1e-6);
|
||||
let c = Clut::sample(vec![2, 2], 1, |p, o| o[0] = p[0] + p[1]);
|
||||
c.eval(&[0.5, 0.25], &mut o);
|
||||
let c = Clut::sample(vec![2, 2], 1, |p, o| o[0] = p[0] + p[1]).unwrap();
|
||||
c.eval(&[0.5, 0.25], &mut o).unwrap();
|
||||
assert!((o[0] - 0.75).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +94,10 @@ impl Curve {
|
||||
}
|
||||
}
|
||||
|
||||
/// Samples `n` evenly spaced points over `[0, 1]`.
|
||||
/// Samples `n` evenly spaced points over `[0, 1]` (one sample is the value at 0).
|
||||
pub fn sample(&self, n: usize) -> Vec<f32> {
|
||||
(0..n).map(|i| self.eval64(i as f64 / (n - 1) as f64) as f32).collect()
|
||||
let step = n.saturating_sub(1).max(1) as f64;
|
||||
(0..n).map(|i| self.eval64(i as f64 / step) as f32).collect()
|
||||
}
|
||||
|
||||
/// `true` when the curve never decreases over `[0, 1]`.
|
||||
|
||||
+17
-16
@@ -4,7 +4,7 @@ use crate::clut::Clut;
|
||||
use crate::pipeline::{Pipeline, Stage};
|
||||
use crate::profile::Profile;
|
||||
use crate::transform::link_stages;
|
||||
use crate::{CmsError, Intent};
|
||||
use crate::{CmsError, Intent, check_len};
|
||||
|
||||
/// Default ΔE76 threshold above which a colour counts as out of gamut.
|
||||
pub const DEFAULT_THRESHOLD: f32 = 4.0;
|
||||
@@ -25,23 +25,23 @@ pub struct GamutCheck {
|
||||
impl GamutCheck {
|
||||
pub fn new(src: &Profile, proof: &Profile, threshold: f32) -> Result<GamutCheck, CmsError> {
|
||||
let lab = crate::Builtin::LabD50.profile();
|
||||
let direct = Pipeline::new(src.channels(), link_stages(src, lab, Intent::RelativeColorimetric, false)?);
|
||||
let direct = Pipeline::new(src.channels(), link_stages(src, lab, Intent::RelativeColorimetric, false)?)?;
|
||||
let mut via = link_stages(src, proof, Intent::RelativeColorimetric, false)?;
|
||||
via.push(Stage::Clamp01);
|
||||
via.extend(link_stages(proof, lab, Intent::RelativeColorimetric, false)?);
|
||||
let via = Pipeline::new(src.channels(), via);
|
||||
let via = Pipeline::new(src.channels(), via)?;
|
||||
let de = |p: &[f32], o: &mut [f32]| {
|
||||
let (mut a, mut b) = ([0.0f32; 16], [0.0f32; 16]);
|
||||
direct.eval(p, &mut a);
|
||||
via.eval(p, &mut b);
|
||||
direct.run(p, &mut a);
|
||||
via.run(p, &mut b);
|
||||
let d = |i: usize, s: f32| (a[i] - b[i]) * s;
|
||||
o[0] = (d(0, 100.0).powi(2) + d(1, 255.0).powi(2) + d(2, 255.0).powi(2)).sqrt();
|
||||
};
|
||||
let n = src.channels();
|
||||
let grid = match n {
|
||||
1 => Some(Clut::sample(vec![256], 1, de)),
|
||||
3 => Some(Clut::sample(vec![33; 3], 1, de)),
|
||||
4 => Some(Clut::sample(vec![17; 4], 1, de)),
|
||||
1 => Some(Clut::sample(vec![256], 1, de)?),
|
||||
3 => Some(Clut::sample(vec![33; 3], 1, de)?),
|
||||
4 => Some(Clut::sample(vec![17; 4], 1, de)?),
|
||||
_ => None,
|
||||
};
|
||||
Ok(GamutCheck { inputs: n, grid, exact: (direct, via), threshold })
|
||||
@@ -51,8 +51,9 @@ impl GamutCheck {
|
||||
self.inputs
|
||||
}
|
||||
|
||||
/// ΔE76 between the colour and its proof round trip.
|
||||
pub fn delta_e(&self, px: &[f32]) -> f32 {
|
||||
/// ΔE76 between the colour (at least `inputs` normalised values) and its proof round trip.
|
||||
pub fn delta_e(&self, px: &[f32]) -> Result<f32, CmsError> {
|
||||
check_len("gamut check input", px.len(), self.inputs)?;
|
||||
let mut o = [0.0f32; 1];
|
||||
match &self.grid {
|
||||
Some(g) => {
|
||||
@@ -60,19 +61,19 @@ impl GamutCheck {
|
||||
for (d, s) in c.iter_mut().zip(&px[..self.inputs]) {
|
||||
*d = s.clamp(0.0, 1.0);
|
||||
}
|
||||
g.eval(&c[..self.inputs], &mut o);
|
||||
g.interp(&c[..self.inputs], &mut o);
|
||||
}
|
||||
None => {
|
||||
let (mut a, mut b) = ([0.0f32; 16], [0.0f32; 16]);
|
||||
self.exact.0.eval(px, &mut a);
|
||||
self.exact.1.eval(px, &mut b);
|
||||
self.exact.0.run(px, &mut a);
|
||||
self.exact.1.run(px, &mut b);
|
||||
o[0] = ((a[0] - b[0]) * 100.0).hypot((a[1] - b[1]) * 255.0).hypot((a[2] - b[2]) * 255.0);
|
||||
}
|
||||
}
|
||||
o[0]
|
||||
Ok(o[0])
|
||||
}
|
||||
|
||||
pub fn out_of_gamut(&self, px: &[f32]) -> bool {
|
||||
self.delta_e(px) > self.threshold
|
||||
pub fn out_of_gamut(&self, px: &[f32]) -> Result<bool, CmsError> {
|
||||
Ok(self.delta_e(px)? > self.threshold)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -83,7 +83,7 @@ impl Intent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from parsing profiles or building transforms.
|
||||
/// Errors from parsing profiles, building transforms, or arguments that do not fit an operation.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CmsError {
|
||||
Truncated,
|
||||
@@ -91,6 +91,10 @@ pub enum CmsError {
|
||||
BadSignature,
|
||||
Invalid(String),
|
||||
Unsupported(String),
|
||||
/// A caller argument does not fit the operation: a pixel stride below the channel count, a
|
||||
/// buffer that is not whole pixels or is too short, a slice shorter than the channel count,
|
||||
/// or table dimensions out of range.
|
||||
InvalidArgument(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CmsError {
|
||||
@@ -100,8 +104,18 @@ impl std::fmt::Display for CmsError {
|
||||
CmsError::BadSignature => write!(f, "not an ICC profile (missing 'acsp' signature)"),
|
||||
CmsError::Invalid(m) => write!(f, "invalid ICC profile: {m}"),
|
||||
CmsError::Unsupported(m) => write!(f, "unsupported ICC feature: {m}"),
|
||||
CmsError::InvalidArgument(m) => write!(f, "invalid argument: {m}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CmsError {}
|
||||
|
||||
pub(crate) fn invalid_arg(m: impl Into<String>) -> CmsError {
|
||||
CmsError::InvalidArgument(m.into())
|
||||
}
|
||||
|
||||
/// Checks that `slice` holds at least `n` values (single-colour evaluation).
|
||||
pub(crate) fn check_len(what: &str, len: usize, n: usize) -> Result<(), CmsError> {
|
||||
if len < n { Err(invalid_arg(format!("{what} has {len} values, {n} needed"))) } else { Ok(()) }
|
||||
}
|
||||
|
||||
+25
-9
@@ -1,34 +1,50 @@
|
||||
//! 3D lookup tables for applying a display transform on the GPU.
|
||||
|
||||
use crate::Transform;
|
||||
use crate::lutfile::MAX_SIZE;
|
||||
use crate::{CmsError, Transform, invalid_arg};
|
||||
|
||||
/// An `size³` RGBA lookup table: entry `r + g·size + b·size²` holds the output for input
|
||||
/// `(r, g, b) / (size − 1)`, alpha 1. This is the memory order of a 3D texture whose x axis is
|
||||
/// red, so it uploads directly (sample at `(c·(size−1) + 0.5) / size` with linear filtering).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Lut3d {
|
||||
pub size: usize,
|
||||
pub data: Vec<[f32; 4]>,
|
||||
size: usize,
|
||||
data: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl Lut3d {
|
||||
/// Samples a 3-input transform (1-output transforms are replicated to gray).
|
||||
pub fn from_transform(t: &Transform, size: usize) -> Lut3d {
|
||||
assert!(t.inputs() == 3 && (t.outputs() == 3 || t.outputs() == 1), "display LUTs need a 3→3 or 3→1 transform");
|
||||
assert!(size >= 2, "LUT size");
|
||||
/// Samples a 3-input transform (1-output transforms are replicated to gray) on a `size`³
|
||||
/// grid, `size` from 2 to [`MAX_SIZE`].
|
||||
pub fn from_transform(t: &Transform, size: usize) -> Result<Lut3d, CmsError> {
|
||||
if t.inputs() != 3 || !(t.outputs() == 3 || t.outputs() == 1) {
|
||||
return Err(invalid_arg(format!("display LUTs need a 3→3 or 3→1 transform, not {}→{}", t.inputs(), t.outputs())));
|
||||
}
|
||||
if !(2..=MAX_SIZE).contains(&size) {
|
||||
return Err(invalid_arg(format!("3D LUT size {size} (2 to {MAX_SIZE})")));
|
||||
}
|
||||
let mut data = vec![[0.0f32, 0.0, 0.0, 1.0]; size * size * size];
|
||||
let s = (size - 1) as f32;
|
||||
let mut out = [0.0f32; 16];
|
||||
for b in 0..size {
|
||||
for g in 0..size {
|
||||
for r in 0..size {
|
||||
t.eval(&[r as f32 / s, g as f32 / s, b as f32 / s], &mut out);
|
||||
t.eval(&[r as f32 / s, g as f32 / s, b as f32 / s], &mut out)?;
|
||||
let v = if t.outputs() == 1 { [out[0]; 3] } else { [out[0], out[1], out[2]] };
|
||||
data[r + g * size + b * size * size] = [v[0], v[1], v[2], 1.0];
|
||||
}
|
||||
}
|
||||
}
|
||||
Lut3d { size, data }
|
||||
Ok(Lut3d { size, data })
|
||||
}
|
||||
|
||||
/// Edge length.
|
||||
pub fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// `size`³ RGBA entries, red fastest.
|
||||
pub fn data(&self) -> &[[f32; 4]] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Trilinear lookup (what GPU filtering computes).
|
||||
|
||||
+13
-12
@@ -50,15 +50,16 @@ fn checked_data_len(size: usize) -> Result<(usize, usize), LutError> {
|
||||
}
|
||||
|
||||
impl LutFile {
|
||||
/// The identity table of edge `size`.
|
||||
pub fn identity(size: usize) -> Self {
|
||||
/// The identity table of edge `size` (2 to [`MAX_SIZE`]).
|
||||
pub fn identity(size: usize) -> Result<Self, LutError> {
|
||||
Self::from_fn("Identity", size, |c| c)
|
||||
}
|
||||
|
||||
/// Samples `f` on an `size`³ grid.
|
||||
pub fn from_fn(title: &str, size: usize, f: impl Fn([f32; 3]) -> [f32; 3]) -> Self {
|
||||
let m = (size.max(2) - 1) as f32;
|
||||
let mut data = Vec::with_capacity(size * size * size * 3);
|
||||
/// Samples `f` on an `size`³ grid, `size` from 2 to [`MAX_SIZE`].
|
||||
pub fn from_fn(title: &str, size: usize, f: impl Fn([f32; 3]) -> [f32; 3]) -> Result<Self, LutError> {
|
||||
let (_, data_len) = checked_data_len(size)?;
|
||||
let m = (size - 1) as f32;
|
||||
let mut data = Vec::with_capacity(data_len);
|
||||
for b in 0..size {
|
||||
for g in 0..size {
|
||||
for r in 0..size {
|
||||
@@ -67,7 +68,7 @@ impl LutFile {
|
||||
}
|
||||
}
|
||||
}
|
||||
LutFile { title: title.to_string(), size, data, domain_min: [0.0; 3], domain_max: [1.0; 3] }
|
||||
Ok(LutFile { title: title.to_string(), size, data, domain_min: [0.0; 3], domain_max: [1.0; 3] })
|
||||
}
|
||||
|
||||
/// Whether the table expects plain 0..1 inputs (the common case; Photoshop's default).
|
||||
@@ -164,7 +165,7 @@ pub fn parse_cube(text: &str) -> Result<LutFile, LutError> {
|
||||
let f = x - i as f32;
|
||||
rows[i * 3 + ch] * (1.0 - f) + rows[(i + 1) * 3 + ch] * f
|
||||
};
|
||||
let mut l = LutFile::from_fn(&title, 33, |c| [curve(0, c[0]), curve(1, c[1]), curve(2, c[2])]);
|
||||
let mut l = LutFile::from_fn(&title, 33, |c| [curve(0, c[0]), curve(1, c[1]), curve(2, c[2])])?;
|
||||
l.title = title;
|
||||
return l.check();
|
||||
}
|
||||
@@ -294,7 +295,7 @@ pub fn builtin(id: &str) -> Option<LutFile> {
|
||||
"monoContrast" => Box::new(|c: [f32; 3]| [smooth(smooth(luma(c))); 3]),
|
||||
_ /* crossProcess */ => Box::new(|c: [f32; 3]| [smooth(c[0]) * 1.05, c[1] * 1.1 - 0.03, c[2] * 0.7 + 0.15]),
|
||||
};
|
||||
Some(LutFile::from_fn(label, 33, f))
|
||||
LutFile::from_fn(label, 33, f).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -362,7 +363,7 @@ DOMAIN_MAX 0.5 1 1
|
||||
|
||||
#[test]
|
||||
fn cube_roundtrip_and_identity() {
|
||||
let id = LutFile::identity(5);
|
||||
let id = LutFile::identity(5).unwrap();
|
||||
let text = write_cube(&LutFile { title: "Id".into(), ..id.clone() });
|
||||
let back = parse_cube(&text).unwrap();
|
||||
assert_eq!(back.size, 5);
|
||||
@@ -408,12 +409,12 @@ DOMAIN_MAX 0.5 1 1
|
||||
}
|
||||
let l = parse_3dl(&text).unwrap();
|
||||
assert_eq!(l.size, 2);
|
||||
assert_eq!(l.data, LutFile::identity(2).data);
|
||||
assert_eq!(l.data, LutFile::identity(2).unwrap().data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn look_hex_floats() {
|
||||
let id = LutFile::identity(2);
|
||||
let id = LutFile::identity(2).unwrap();
|
||||
let hex: String = id.data.iter().flat_map(|v| v.to_le_bytes()).map(|b| format!("{b:02X}")).collect();
|
||||
let xml = format!("<?xml version=\"1.0\"?><look><LUT><size>\"2\"</size><data>\"{hex}\"</data></LUT></look>");
|
||||
assert_eq!(parse("a.look", xml.as_bytes()).unwrap().data, id.data);
|
||||
|
||||
+67
-17
@@ -4,7 +4,7 @@
|
||||
|
||||
use crate::clut::Clut;
|
||||
use crate::curve::Curve;
|
||||
use crate::math;
|
||||
use crate::{CmsError, check_len, invalid_arg, math};
|
||||
|
||||
/// Largest channel count flowing through a pipeline.
|
||||
pub const MAX: usize = 16;
|
||||
@@ -54,6 +54,25 @@ impl Stage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the stage can run on `n` channels: matrix shapes match their data and
|
||||
/// channel counts stay within [`MAX`].
|
||||
fn check(&self, n: usize) -> Result<(), CmsError> {
|
||||
if let Stage::Matrix { rows, cols, m, offset } = self
|
||||
&& (!(1..=MAX).contains(rows) || !(1..=MAX).contains(cols) || m.len() != rows * cols || offset.len() != *rows)
|
||||
{
|
||||
return Err(invalid_arg(format!(
|
||||
"{rows}×{cols} matrix stage with {} coefficients and {} offsets (1 to {MAX} rows and columns)",
|
||||
m.len(),
|
||||
offset.len()
|
||||
)));
|
||||
}
|
||||
let out = self.outputs(n);
|
||||
if !(1..=MAX).contains(&out) {
|
||||
return Err(invalid_arg(format!("pipeline stage with {out} outputs (1 to {MAX})")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn matrix3(m: &math::Mat3) -> Stage {
|
||||
Stage::Matrix { rows: 3, cols: 3, m: m.iter().flatten().copied().collect(), offset: vec![0.0; 3] }
|
||||
}
|
||||
@@ -89,7 +108,7 @@ impl Stage {
|
||||
let mut o = [0.0f32; MAX];
|
||||
let mut inp = [0.0f32; MAX];
|
||||
inp[..n].copy_from_slice(&v[..n]);
|
||||
c.eval(&inp[..c.inputs], &mut o[..c.outputs]);
|
||||
c.interp(&inp[..c.inputs], &mut o[..c.outputs]);
|
||||
v[..c.outputs].copy_from_slice(&o[..c.outputs]);
|
||||
c.outputs
|
||||
}
|
||||
@@ -152,28 +171,59 @@ impl Stage {
|
||||
/// A sequence of stages with fixed input and output channel counts.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pipeline {
|
||||
pub inputs: usize,
|
||||
pub outputs: usize,
|
||||
pub stages: Vec<Stage>,
|
||||
inputs: usize,
|
||||
outputs: usize,
|
||||
stages: Vec<Stage>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new(inputs: usize, stages: Vec<Stage>) -> Self {
|
||||
let outputs = stages.iter().fold(inputs, |n, s| s.outputs(n));
|
||||
Pipeline { inputs, outputs, stages }
|
||||
}
|
||||
|
||||
/// Appends stages.
|
||||
pub fn extend(&mut self, stages: impl IntoIterator<Item = Stage>) {
|
||||
for s in stages {
|
||||
self.outputs = s.outputs(self.outputs);
|
||||
self.stages.push(s);
|
||||
/// A pipeline over `inputs` channels (1 to [`MAX`]). Refuses stages that cannot run on the
|
||||
/// channels reaching them with [`CmsError::InvalidArgument`].
|
||||
pub fn new(inputs: usize, stages: Vec<Stage>) -> Result<Self, CmsError> {
|
||||
if !(1..=MAX).contains(&inputs) {
|
||||
return Err(invalid_arg(format!("pipeline with {inputs} inputs (1 to {MAX})")));
|
||||
}
|
||||
let mut p = Pipeline { inputs, outputs: inputs, stages: Vec::with_capacity(stages.len()) };
|
||||
p.extend(stages)?;
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// Evaluates one pixel: `input` has `inputs` values, `out` receives `outputs` values.
|
||||
pub fn inputs(&self) -> usize {
|
||||
self.inputs
|
||||
}
|
||||
pub fn outputs(&self) -> usize {
|
||||
self.outputs
|
||||
}
|
||||
pub fn stages(&self) -> &[Stage] {
|
||||
&self.stages
|
||||
}
|
||||
|
||||
/// Appends stages, checked as in [`Pipeline::new`]; on error the pipeline is unchanged.
|
||||
pub fn extend(&mut self, stages: impl IntoIterator<Item = Stage>) -> Result<(), CmsError> {
|
||||
let mut n = self.outputs;
|
||||
let mut added = Vec::new();
|
||||
for s in stages {
|
||||
s.check(n)?;
|
||||
n = s.outputs(n);
|
||||
added.push(s);
|
||||
}
|
||||
self.stages.extend(added);
|
||||
self.outputs = n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Evaluates one pixel: `input` has at least `inputs` values, `out` receives `outputs`
|
||||
/// values.
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) -> Result<(), CmsError> {
|
||||
check_len("pipeline input", input.len(), self.inputs)?;
|
||||
check_len("pipeline output", out.len(), self.outputs)?;
|
||||
self.run(input, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`Pipeline::eval`] for callers that guarantee the slice lengths.
|
||||
#[inline]
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) {
|
||||
pub(crate) fn run(&self, input: &[f32], out: &mut [f32]) {
|
||||
let mut v = [0.0f32; MAX];
|
||||
v[..self.inputs].copy_from_slice(&input[..self.inputs]);
|
||||
let mut n = self.inputs;
|
||||
|
||||
@@ -252,9 +252,9 @@ impl Profile {
|
||||
k /= steps;
|
||||
}
|
||||
let (mut la, mut lb) = ([0f32; 3], [0f32; 3]);
|
||||
a.eval(&input[..n], &mut la);
|
||||
b.eval(&input[..n], &mut lb);
|
||||
math::delta_e76(decode(la), decode(lb)) <= SAME_COLORS_MAX_DELTA_E
|
||||
a.eval(&input[..n], &mut la).is_ok()
|
||||
&& b.eval(&input[..n], &mut lb).is_ok()
|
||||
&& math::delta_e76(decode(la), decode(lb)) <= SAME_COLORS_MAX_DELTA_E
|
||||
})
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ fn sig(s: &[u8; 4]) -> u32 {
|
||||
}
|
||||
|
||||
/// Upper bound on CLUT nodes × outputs we accept, to reject hostile files.
|
||||
const MAX_CLUT_VALUES: usize = 16 * 1024 * 1024;
|
||||
const MAX_CLUT_VALUES: usize = crate::clut::MAX_VALUES;
|
||||
|
||||
fn parse_profile(bytes: &[u8]) -> Result<Profile, CmsError> {
|
||||
let r = Reader { b: bytes };
|
||||
@@ -674,7 +674,7 @@ fn parse_lut(d: &[u8], a2b: bool) -> Result<Lut, CmsError> {
|
||||
stages.push(Stage::matrix3(&m));
|
||||
}
|
||||
stages.push(Stage::Curves(in_curves));
|
||||
stages.push(Stage::Clut(Clut::new(vec![g; i], o, data)));
|
||||
stages.push(Stage::Clut(Clut::new(vec![g; i], o, data)?));
|
||||
stages.push(Stage::Curves(out_curves));
|
||||
Ok(Lut { kind: if wide { LutKind::Lut16 } else { LutKind::Lut8 }, inputs: i, outputs: o, stages })
|
||||
}
|
||||
@@ -712,7 +712,7 @@ fn parse_lut(d: &[u8], a2b: bool) -> Result<Lut, CmsError> {
|
||||
2 => r.slice(off + 20, total * 2)?.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]]) as f32 / 65535.0).collect(),
|
||||
_ => return Err(CmsError::Invalid(format!("CLUT precision {prec}"))),
|
||||
};
|
||||
Ok(Stage::Clut(Clut::new(grid, cout, data)))
|
||||
Ok(Stage::Clut(Clut::new(grid, cout, data)?))
|
||||
};
|
||||
let mut stages = Vec::new();
|
||||
if ty == b"mAB " {
|
||||
|
||||
+31
-21
@@ -31,7 +31,7 @@ use crate::curve::Curve;
|
||||
use crate::math;
|
||||
use crate::pipeline::Stage;
|
||||
use crate::profile::{ColorSpace, Lut, LutKind, Pcs, Profile, ProfileClass};
|
||||
use crate::{CmsError, Intent};
|
||||
use crate::{CmsError, Intent, invalid_arg};
|
||||
|
||||
pub const DESCRIPTION: &str = "Photocraft Coated CMYK (synthetic, 300% TAC, medium GCR)";
|
||||
|
||||
@@ -168,7 +168,7 @@ struct TableForward(Clut);
|
||||
impl Forward for TableForward {
|
||||
fn lab(&self, cmyk: [f64; 4]) -> [f64; 3] {
|
||||
let mut o = [0.0f32; 3];
|
||||
self.0.eval(&cmyk.map(|v| v.clamp(0.0, 1.0) as f32), &mut o);
|
||||
self.0.interp(&cmyk.map(|v| v.clamp(0.0, 1.0) as f32), &mut o);
|
||||
decode_v2(o.map(f64::from))
|
||||
}
|
||||
}
|
||||
@@ -404,7 +404,7 @@ fn par_map<T: Send>(n: usize, f: impl Fn(usize) -> T + Sync + Send) -> Vec<T> {
|
||||
(0..n).map(f).collect()
|
||||
}
|
||||
|
||||
fn b2a_table<F: Forward + ?Sized>(model: &F, p: &CmykParams, gbd: Option<&Gbd>) -> Clut {
|
||||
fn b2a_table<F: Forward + ?Sized>(model: &F, p: &CmykParams, gbd: Option<&Gbd>) -> Result<Clut, CmsError> {
|
||||
let n = p.grid_b2a.max(2);
|
||||
let nodes = n * n * n;
|
||||
let vals = par_map(n * n, |row| {
|
||||
@@ -429,6 +429,17 @@ fn b2a_table<F: Forward + ?Sized>(model: &F, p: &CmykParams, gbd: Option<&Gbd>)
|
||||
Clut::new(vec![n; 3], 4, data)
|
||||
}
|
||||
|
||||
/// Refuses grids below 2 nodes or above what a [`Clut`] holds, before anything is sampled.
|
||||
fn check_grids(p: &CmykParams) -> Result<(), CmsError> {
|
||||
let fits = |grid: usize, dims: u32, outputs: usize| {
|
||||
grid >= 2 && grid.checked_pow(dims).and_then(|n| n.checked_mul(outputs)).is_some_and(|n| n <= crate::clut::MAX_VALUES)
|
||||
};
|
||||
if !fits(p.grid_a2b, 4, 3) || !fits(p.grid_b2a, 3, 4) {
|
||||
return Err(invalid_arg(format!("CMYK profile grids {}⁴/{}³ need at least 2 nodes and must fit a CLUT", p.grid_a2b, p.grid_b2a)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lut16(clut: Clut) -> Lut {
|
||||
let (i, o) = (clut.inputs, clut.outputs);
|
||||
Lut {
|
||||
@@ -439,16 +450,17 @@ fn lut16(clut: Clut) -> Lut {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a synthetic CMYK profile.
|
||||
pub fn cmyk_profile(p: &CmykParams) -> Profile {
|
||||
/// Generates a synthetic CMYK profile. Grids are refused as in [`cmyk_profile_from_model`].
|
||||
pub fn cmyk_profile(p: &CmykParams) -> Result<Profile, CmsError> {
|
||||
check_grids(p)?;
|
||||
let model = CmykModel::coated(p.tvi);
|
||||
let a2b = sample_a2b(&model, p.grid_a2b);
|
||||
let a2b = sample_a2b(&model, p.grid_a2b)?;
|
||||
lut_profile(&model, a2b, model.paper, p)
|
||||
}
|
||||
|
||||
/// Samples a forward model into an AToB table with `grid`⁴ nodes (at least 2⁴) and legacy Lab
|
||||
/// encoding.
|
||||
fn sample_a2b<F: Forward + ?Sized>(model: &F, grid: usize) -> Clut {
|
||||
fn sample_a2b<F: Forward + ?Sized>(model: &F, grid: usize) -> Result<Clut, CmsError> {
|
||||
Clut::sample(vec![grid.max(2); 4], 3, |inp, out| {
|
||||
let lab = model.lab([inp[0] as f64, inp[1] as f64, inp[2] as f64, inp[3] as f64]);
|
||||
out.copy_from_slice(&encode_v2(lab));
|
||||
@@ -458,27 +470,25 @@ fn sample_a2b<F: Forward + ?Sized>(model: &F, grid: usize) -> Clut {
|
||||
/// A CMYK profile of any forward model on paper with absolute XYZ `paper`: the AToB table
|
||||
/// samples `model` on `p.grid_a2b`⁴ nodes, and the BToA tables invert that table rather than the
|
||||
/// model, so Lab → CMYK → Lab round trips through the profile are as close as the separation
|
||||
/// allows. Grids below 2 nodes and a zero, negative or non-finite `paper` are refused with
|
||||
/// [`CmsError::Invalid`].
|
||||
/// allows. Grids below 2 nodes or too large for a [`Clut`], and a zero, negative or
|
||||
/// non-finite `paper`, are refused with [`CmsError::InvalidArgument`].
|
||||
pub fn cmyk_profile_from_model<F: Forward + ?Sized>(model: &F, paper: [f64; 3], p: &CmykParams) -> Result<Profile, CmsError> {
|
||||
if p.grid_a2b < 2 || p.grid_b2a < 2 {
|
||||
return Err(CmsError::Invalid(format!("CMYK profile grids {}⁴/{}³ need at least 2 nodes", p.grid_a2b, p.grid_b2a)));
|
||||
}
|
||||
check_grids(p)?;
|
||||
if !paper.iter().all(|v| v.is_finite() && *v > 0.0) {
|
||||
return Err(CmsError::Invalid(format!("paper XYZ {paper:?}")));
|
||||
return Err(invalid_arg(format!("paper XYZ {paper:?}")));
|
||||
}
|
||||
let a2b = sample_a2b(model, p.grid_a2b);
|
||||
let a2b = sample_a2b(model, p.grid_a2b)?;
|
||||
let forward = TableForward(a2b.clone());
|
||||
Ok(lut_profile(&forward, a2b, paper, p))
|
||||
lut_profile(&forward, a2b, paper, p)
|
||||
}
|
||||
|
||||
fn lut_profile<F: Forward + ?Sized>(model: &F, a2b: Clut, paper: [f64; 3], p: &CmykParams) -> Profile {
|
||||
fn lut_profile<F: Forward + ?Sized>(model: &F, a2b: Clut, paper: [f64; 3], p: &CmykParams) -> Result<Profile, CmsError> {
|
||||
let gbd = Gbd::new(model, p);
|
||||
let colorimetric = b2a_table(model, p, None);
|
||||
let perceptual = b2a_table(model, p, Some(&gbd));
|
||||
let colorimetric = b2a_table(model, p, None)?;
|
||||
let perceptual = b2a_table(model, p, Some(&gbd))?;
|
||||
let a2b = lut16(a2b);
|
||||
let (b0, b1) = (lut16(perceptual), lut16(colorimetric));
|
||||
Profile {
|
||||
Ok(Profile {
|
||||
version: (4, 0x30),
|
||||
class: ProfileClass::Output,
|
||||
color_space: ColorSpace::Cmyk,
|
||||
@@ -497,11 +507,11 @@ fn lut_profile<F: Forward + ?Sized>(model: &F, a2b: Clut, paper: [f64; 3], p: &C
|
||||
bytes: None,
|
||||
hash: Default::default(),
|
||||
}
|
||||
.with_encoded_bytes()
|
||||
.with_encoded_bytes())
|
||||
}
|
||||
|
||||
/// The default coated profile (what `profiles/photocraft-coated-cmyk.icc` contains).
|
||||
pub fn coated_cmyk() -> Profile {
|
||||
pub fn coated_cmyk() -> Result<Profile, CmsError> {
|
||||
cmyk_profile(&CmykParams::default())
|
||||
}
|
||||
|
||||
|
||||
+112
-51
@@ -9,7 +9,7 @@ use crate::curve::Curve;
|
||||
use crate::math;
|
||||
use crate::pipeline::{Pipeline, Stage};
|
||||
use crate::profile::{ColorSpace, Pcs, Profile, ProfileClass};
|
||||
use crate::{CmsError, Intent};
|
||||
use crate::{CmsError, Intent, check_len, invalid_arg};
|
||||
|
||||
/// Sample storage of a byte buffer for [`Transform::convert_bytes_many`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -101,7 +101,7 @@ impl Transform {
|
||||
|
||||
pub fn with_options(src: &Profile, dst: &Profile, opts: TransformOptions) -> Result<Transform, CmsError> {
|
||||
let stages = link_stages(src, dst, opts.intent, opts.bpc)?;
|
||||
let mut t = Self::from_pipeline(Pipeline::new(src.channels(), stages), opts);
|
||||
let mut t = Self::from_pipeline(Pipeline::new(src.channels(), stages)?, opts)?;
|
||||
if opts.intent != Intent::AbsoluteColorimetric {
|
||||
t.fix_white(src.color_space, dst.color_space);
|
||||
}
|
||||
@@ -117,15 +117,15 @@ impl Transform {
|
||||
stages.push(Stage::Clamp01);
|
||||
let (i2, b2) = if simulate_paper { (Intent::AbsoluteColorimetric, false) } else { (Intent::RelativeColorimetric, true) };
|
||||
stages.extend(link_stages(proof, dst, i2, b2)?);
|
||||
Ok(Self::from_pipeline(Pipeline::new(src.channels(), stages), TransformOptions { intent, bpc, precise_float: true }))
|
||||
Self::from_pipeline(Pipeline::new(src.channels(), stages)?, TransformOptions { intent, bpc, precise_float: true })
|
||||
}
|
||||
|
||||
/// Builds a transform from an arbitrary pipeline over normalised values.
|
||||
pub fn from_pipeline(mut pipeline: Pipeline, opts: TransformOptions) -> Transform {
|
||||
pub fn from_pipeline(mut pipeline: Pipeline, opts: TransformOptions) -> Result<Transform, CmsError> {
|
||||
pipeline.optimize();
|
||||
let (inputs, outputs) = (pipeline.inputs, pipeline.outputs);
|
||||
let (inputs, outputs) = (pipeline.inputs(), pipeline.outputs());
|
||||
// Split trailing inverse curves into accurate output tables.
|
||||
let mut head = pipeline.stages.clone();
|
||||
let mut head = pipeline.stages().to_vec();
|
||||
let mut post = None;
|
||||
if let Some(Stage::InvCurves(cs)) = head.last()
|
||||
&& cs.len() == outputs
|
||||
@@ -147,8 +147,13 @@ impl Transform {
|
||||
head.pop();
|
||||
}
|
||||
}
|
||||
let head_pipe = Pipeline::new(inputs, head.clone());
|
||||
let mid = head_pipe.outputs;
|
||||
let head_pipe = Pipeline::new(inputs, head.clone())?;
|
||||
let mid = head_pipe.outputs();
|
||||
let sampled = |grid: Vec<usize>| match Clut::sample(grid, mid, |i, o| head_pipe.run(i, o)) {
|
||||
Ok(c) => Core::Grid(c),
|
||||
// More outputs than a table holds: evaluate exactly.
|
||||
Err(_) => Core::Exact,
|
||||
};
|
||||
let core = match head.as_slice() {
|
||||
[] => Core::Shaper { curves8: Vec::new(), curves: Vec::new(), matrix: None },
|
||||
[Stage::Curves(cs)] if cs.len() == inputs => shaper(cs, None),
|
||||
@@ -158,10 +163,10 @@ impl Transform {
|
||||
// a grid would amplify interpolation error through steep output curves.
|
||||
_ if !head.iter().any(|s| matches!(s, Stage::Clut(_))) => Core::Exact,
|
||||
_ => match inputs {
|
||||
1 => Core::Grid(Clut::sample(vec![TABLE_1D], mid, |i, o| head_pipe.eval(i, o))),
|
||||
3 => Core::Grid(Clut::sample(vec![GRID_3D; 3], mid, |i, o| head_pipe.eval(i, o))),
|
||||
4 => Core::Grid(Clut::sample(vec![GRID_4D; 4], mid, |i, o| head_pipe.eval(i, o))),
|
||||
2 => Core::Grid(Clut::sample(vec![GRID_3D; 2], mid, |i, o| head_pipe.eval(i, o))),
|
||||
1 => sampled(vec![TABLE_1D]),
|
||||
3 => sampled(vec![GRID_3D; 3]),
|
||||
4 => sampled(vec![GRID_4D; 4]),
|
||||
2 => sampled(vec![GRID_3D; 2]),
|
||||
_ => Core::Exact,
|
||||
},
|
||||
};
|
||||
@@ -179,7 +184,7 @@ impl Transform {
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
Transform { inputs, outputs, opts, pipeline, core, post, grid8 }
|
||||
Ok(Transform { inputs, outputs, opts, pipeline, core, post, grid8 })
|
||||
}
|
||||
|
||||
/// White preservation: the device-link node at the source's device white gets exactly the
|
||||
@@ -226,24 +231,38 @@ impl Transform {
|
||||
&self.pipeline
|
||||
}
|
||||
|
||||
/// Exact evaluation of one colour (`inputs` → `outputs` normalised values).
|
||||
#[inline]
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) {
|
||||
self.pipeline.eval(input, out);
|
||||
/// Exact evaluation of one colour: `input` holds at least `inputs` normalised values, `out`
|
||||
/// receives `outputs` values.
|
||||
pub fn eval(&self, input: &[f32], out: &mut [f32]) -> Result<(), CmsError> {
|
||||
self.check_io(input, out)?;
|
||||
self.pipeline.run(input, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lookup-table evaluation of one colour (what integer buffers use); slices as in
|
||||
/// [`Transform::eval`].
|
||||
pub fn eval_fast(&self, input: &[f32], out: &mut [f32]) -> Result<(), CmsError> {
|
||||
self.check_io(input, out)?;
|
||||
self.fast(input, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_io(&self, input: &[f32], out: &[f32]) -> Result<(), CmsError> {
|
||||
check_len("transform input", input.len(), self.inputs)?;
|
||||
check_len("transform output", out.len(), self.outputs)
|
||||
}
|
||||
|
||||
/// Lookup-table evaluation of one colour (what integer buffers use).
|
||||
#[inline]
|
||||
pub fn eval_fast(&self, input: &[f32], out: &mut [f32]) {
|
||||
fn fast(&self, input: &[f32], out: &mut [f32]) {
|
||||
let mut mid = [0.0f32; 16];
|
||||
match &self.core {
|
||||
Core::Exact => return self.pipeline.eval(input, out),
|
||||
Core::Exact => return self.pipeline.run(input, out),
|
||||
Core::Grid(c) => {
|
||||
let mut inp = [0.0f32; 16];
|
||||
for (d, s) in inp.iter_mut().zip(&input[..self.inputs]) {
|
||||
*d = s.clamp(0.0, 1.0);
|
||||
}
|
||||
c.eval(&inp[..self.inputs], &mut mid[..c.outputs]);
|
||||
c.interp(&inp[..self.inputs], &mut mid[..c.outputs]);
|
||||
}
|
||||
Core::Shaper { curves, matrix, .. } => {
|
||||
let mut v = [0.0f32; 16];
|
||||
@@ -269,20 +288,24 @@ impl Transform {
|
||||
}
|
||||
|
||||
/// Converts float pixels. `src`/`dst` hold whole pixels of `src_stride`/`dst_stride`
|
||||
/// values; colour channels come first. Trailing channels are copied when `copy_extra`.
|
||||
pub fn convert_f32(&self, src: &[f32], src_stride: usize, dst: &mut [f32], dst_stride: usize, copy_extra: bool) {
|
||||
self.check(src_stride, dst_stride);
|
||||
/// values (strides at least `inputs`/`outputs`); colour channels come first. `dst` holds at
|
||||
/// least as many pixels as `src`; pixels past that are left alone. Trailing channels are
|
||||
/// copied when `copy_extra`. Buffers that do not fit are refused with
|
||||
/// [`CmsError::InvalidArgument`] before any pixel is written.
|
||||
pub fn convert_f32(&self, src: &[f32], src_stride: usize, dst: &mut [f32], dst_stride: usize, copy_extra: bool) -> Result<(), CmsError> {
|
||||
self.check_buffers(src.len(), src_stride, dst.len(), dst_stride)?;
|
||||
let extra = extra_channels(self, src_stride, dst_stride, copy_extra);
|
||||
par_rows(src, src_stride, dst, dst_stride, |s, d| self.run_f32(s, src_stride, d, dst_stride, extra));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_f32(&self, s: &[f32], ss: usize, d: &mut [f32], ds: usize, extra: usize) {
|
||||
let precise = self.opts.precise_float || matches!(self.core, Core::Exact);
|
||||
for (sp, dp) in s.chunks_exact(ss).zip(d.chunks_exact_mut(ds)) {
|
||||
if precise {
|
||||
self.pipeline.eval(sp, dp);
|
||||
self.pipeline.run(sp, dp);
|
||||
} else {
|
||||
self.eval_fast(sp, dp);
|
||||
self.fast(sp, dp);
|
||||
}
|
||||
for e in 0..extra {
|
||||
dp[self.outputs + e] = sp[self.inputs + e];
|
||||
@@ -290,29 +313,37 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
|
||||
/// In-place float conversion; `stride ≥ max(inputs, outputs)`.
|
||||
pub fn apply(&self, buf: &mut [f32], stride: usize) {
|
||||
assert!(stride >= self.inputs.max(self.outputs), "stride too small");
|
||||
/// In-place float conversion of whole pixels of `stride ≥ max(inputs, outputs)` values.
|
||||
pub fn apply(&self, buf: &mut [f32], stride: usize) -> Result<(), CmsError> {
|
||||
let need = self.inputs.max(self.outputs);
|
||||
if stride < need {
|
||||
return Err(invalid_arg(format!("pixel stride {stride} is below the channel count {need}")));
|
||||
}
|
||||
if !buf.len().is_multiple_of(stride) {
|
||||
return Err(invalid_arg(format!("buffer of {} values is not whole pixels of {stride}", buf.len())));
|
||||
}
|
||||
let precise = self.opts.precise_float || matches!(self.core, Core::Exact);
|
||||
let body = |chunk: &mut [f32]| {
|
||||
let mut out = [0.0f32; 16];
|
||||
for p in chunk.chunks_exact_mut(stride) {
|
||||
if precise {
|
||||
self.pipeline.eval(p, &mut out);
|
||||
self.pipeline.run(p, &mut out);
|
||||
} else {
|
||||
self.eval_fast(p, &mut out);
|
||||
self.fast(p, &mut out);
|
||||
}
|
||||
p[..self.outputs].copy_from_slice(&out[..self.outputs]);
|
||||
}
|
||||
};
|
||||
par_inplace(buf, stride, body);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts 8-bit pixels (see [`Transform::convert_f32`] for the layout).
|
||||
pub fn convert_u8(&self, src: &[u8], src_stride: usize, dst: &mut [u8], dst_stride: usize, copy_extra: bool) {
|
||||
self.check(src_stride, dst_stride);
|
||||
pub fn convert_u8(&self, src: &[u8], src_stride: usize, dst: &mut [u8], dst_stride: usize, copy_extra: bool) -> Result<(), CmsError> {
|
||||
self.check_buffers(src.len(), src_stride, dst.len(), dst_stride)?;
|
||||
let extra = extra_channels(self, src_stride, dst_stride, copy_extra);
|
||||
par_rows(src, src_stride, dst, dst_stride, |s, d| self.run_u8(s, src_stride, d, dst_stride, extra));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_u8(&self, s: &[u8], ss: usize, d: &mut [u8], ds: usize, extra: usize) {
|
||||
@@ -384,7 +415,7 @@ impl Transform {
|
||||
for k in 0..self.inputs {
|
||||
inp[k] = sp[k] as f32 / 255.0;
|
||||
}
|
||||
self.eval_fast(&inp, &mut out);
|
||||
self.fast(&inp, &mut out);
|
||||
for k in 0..self.outputs {
|
||||
dp[k] = q(out[k]);
|
||||
}
|
||||
@@ -397,10 +428,11 @@ impl Transform {
|
||||
}
|
||||
|
||||
/// Converts 16-bit pixels (see [`Transform::convert_f32`] for the layout).
|
||||
pub fn convert_u16(&self, src: &[u16], src_stride: usize, dst: &mut [u16], dst_stride: usize, copy_extra: bool) {
|
||||
self.check(src_stride, dst_stride);
|
||||
pub fn convert_u16(&self, src: &[u16], src_stride: usize, dst: &mut [u16], dst_stride: usize, copy_extra: bool) -> Result<(), CmsError> {
|
||||
self.check_buffers(src.len(), src_stride, dst.len(), dst_stride)?;
|
||||
let extra = extra_channels(self, src_stride, dst_stride, copy_extra);
|
||||
par_rows(src, src_stride, dst, dst_stride, |s, d| self.run_u16(s, src_stride, d, dst_stride, extra));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_u16(&self, s: &[u16], ss: usize, d: &mut [u16], ds: usize, extra: usize) {
|
||||
@@ -410,7 +442,7 @@ impl Transform {
|
||||
for k in 0..self.inputs {
|
||||
inp[k] = sp[k] as f32 / 65535.0;
|
||||
}
|
||||
self.eval_fast(&inp, &mut out);
|
||||
self.fast(&inp, &mut out);
|
||||
for k in 0..self.outputs {
|
||||
dp[k] = (out[k].clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
|
||||
}
|
||||
@@ -422,9 +454,27 @@ impl Transform {
|
||||
|
||||
/// Converts many independent buffers of native-endian samples in one parallel pass (e.g.
|
||||
/// all tiles of a surface): far cheaper than one parallel call per small buffer. Each job
|
||||
/// is `(src bytes, dst bytes)` holding whole pixels of `src_stride`/`dst_stride` samples.
|
||||
pub fn convert_bytes_many(&self, sample: SampleKind, jobs: Vec<(&[u8], &mut [u8])>, src_stride: usize, dst_stride: usize, copy_extra: bool) {
|
||||
self.check(src_stride, dst_stride);
|
||||
/// is `(src bytes, dst bytes)` holding whole pixels of `src_stride`/`dst_stride` samples,
|
||||
/// as in [`Transform::convert_f32`]. Every job is checked before any is converted.
|
||||
pub fn convert_bytes_many(
|
||||
&self,
|
||||
sample: SampleKind,
|
||||
jobs: Vec<(&[u8], &mut [u8])>,
|
||||
src_stride: usize,
|
||||
dst_stride: usize,
|
||||
copy_extra: bool,
|
||||
) -> Result<(), CmsError> {
|
||||
let size = match sample {
|
||||
SampleKind::U8 => 1,
|
||||
SampleKind::U16 => 2,
|
||||
SampleKind::F32 => 4,
|
||||
};
|
||||
for (s, d) in &jobs {
|
||||
if !s.len().is_multiple_of(size) || !d.len().is_multiple_of(size) {
|
||||
return Err(invalid_arg(format!("byte buffers of {}/{} bytes are not whole {size}-byte samples", s.len(), d.len())));
|
||||
}
|
||||
self.check_buffers(s.len() / size, src_stride, d.len() / size, dst_stride)?;
|
||||
}
|
||||
let extra = extra_channels(self, src_stride, dst_stride, copy_extra);
|
||||
let run = |(s, d): (&[u8], &mut [u8])| match sample {
|
||||
SampleKind::U8 => self.run_u8(s, src_stride, d, dst_stride, extra),
|
||||
@@ -452,10 +502,21 @@ impl Transform {
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
jobs.into_iter().for_each(run);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check(&self, ss: usize, ds: usize) {
|
||||
assert!(ss >= self.inputs && ds >= self.outputs, "pixel stride smaller than the channel count");
|
||||
/// Checks a conversion's buffers once per call, so the per-pixel loops need no checks.
|
||||
fn check_buffers(&self, src_len: usize, ss: usize, dst_len: usize, ds: usize) -> Result<(), CmsError> {
|
||||
if ss < self.inputs || ds < self.outputs {
|
||||
return Err(invalid_arg(format!("pixel strides {ss}/{ds} are below the channel counts {}/{}", self.inputs, self.outputs)));
|
||||
}
|
||||
if !src_len.is_multiple_of(ss) || !dst_len.is_multiple_of(ds) {
|
||||
return Err(invalid_arg(format!("buffers of {src_len}/{dst_len} samples are not whole pixels of {ss}/{ds} samples")));
|
||||
}
|
||||
if dst_len / ds < src_len / ss {
|
||||
return Err(invalid_arg(format!("destination holds {} pixels, source {}", dst_len / ds, src_len / ss)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,21 +705,21 @@ pub fn black_point(p: &Profile, intent: Intent, _input: bool) -> Result<f64, Cms
|
||||
if lut_based && p.version.0 >= 4 && matches!(intent, Intent::Perceptual | Intent::Saturation) {
|
||||
return Ok(PERCEPTUAL_BLACK[1]);
|
||||
}
|
||||
let pcs_y = |st: Vec<Stage>, pcs: Pcs, dev: &[f32]| -> f64 {
|
||||
let pipe = Pipeline::new(p.channels(), st);
|
||||
let pcs_y = |st: Vec<Stage>, pcs: Pcs, dev: &[f32]| -> Result<f64, CmsError> {
|
||||
let pipe = Pipeline::new(p.channels(), st)?;
|
||||
let mut o = [0.0f32; 16];
|
||||
pipe.eval(dev, &mut o);
|
||||
pipe.eval(dev, &mut o)?;
|
||||
let y = match pcs {
|
||||
Pcs::Xyz => o[1] as f64,
|
||||
Pcs::Lab => math::lab_to_xyz([o[0] as f64, 0.0, 0.0], math::D50)[1],
|
||||
};
|
||||
y.clamp(0.0, 0.5)
|
||||
Ok(y.clamp(0.0, 0.5))
|
||||
};
|
||||
let (to_pcs, pcs) = p.device_to_pcs(intent)?;
|
||||
let additive = matches!(p.color_space, ColorSpace::Rgb | ColorSpace::Gray);
|
||||
if additive || !lut_based {
|
||||
let zero = vec![0.0f32; p.channels()];
|
||||
return Ok(pcs_y(to_pcs, pcs, &zero));
|
||||
return pcs_y(to_pcs, pcs, &zero);
|
||||
}
|
||||
// Subtractive LUT profile: darkest reachable colour.
|
||||
let colorimetric = if intent == Intent::Saturation { Intent::Saturation } else { Intent::RelativeColorimetric };
|
||||
@@ -669,14 +730,14 @@ pub fn black_point(p: &Profile, intent: Intent, _input: bool) -> Result<f64, Cms
|
||||
st.push(Stage::LabToXyz);
|
||||
}
|
||||
st.extend(from_pcs);
|
||||
let pipe = Pipeline::new(3, st);
|
||||
let pipe = Pipeline::new(3, st)?;
|
||||
let mut dev = [0.0f32; 16];
|
||||
pipe.eval(&[0.0, 0.0, 0.0], &mut dev);
|
||||
Ok(pcs_y(to_pcs, pcs, &dev[..p.channels()]))
|
||||
pipe.eval(&[0.0, 0.0, 0.0], &mut dev)?;
|
||||
pcs_y(to_pcs, pcs, dev.get(..p.channels()).unwrap_or(&dev))
|
||||
}
|
||||
Err(_) => {
|
||||
let ones = vec![1.0f32; p.channels()];
|
||||
Ok(pcs_y(to_pcs, pcs, &ones))
|
||||
pcs_y(to_pcs, pcs, &ones)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ fn bench_srgb8_to_cmyk8() {
|
||||
}
|
||||
let mut dst = vec![0u8; n * 5];
|
||||
let t1 = std::time::Instant::now();
|
||||
t.convert_u8(&src, 4, &mut dst, 5, true);
|
||||
t.convert_u8(&src, 4, &mut dst, 5, true).unwrap();
|
||||
eprintln!("convert 6016² RGBA8 → CMYKA8 (one buffer): {:?}", t1.elapsed());
|
||||
let t2 = std::time::Instant::now();
|
||||
for (s, d) in src.chunks(65536 * 4).zip(dst.chunks_mut(65536 * 5)) {
|
||||
t.convert_u8(s, 4, d, 5, true);
|
||||
t.convert_u8(s, 4, d, 5, true).unwrap();
|
||||
}
|
||||
eprintln!("convert per 256² tile: {:?}", t2.elapsed());
|
||||
}
|
||||
|
||||
+118
-49
@@ -26,7 +26,7 @@ fn enc(l: [f64; 3]) -> [f32; 3] {
|
||||
fn to_lab(p: &Profile, dev: &[f32], intent: Intent) -> [f64; 3] {
|
||||
let t = Transform::new(p, lab(), intent, false).unwrap();
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(dev, &mut o);
|
||||
t.eval(dev, &mut o).unwrap();
|
||||
dec(&o)
|
||||
}
|
||||
|
||||
@@ -83,16 +83,16 @@ fn rgb_lab_rgb_roundtrip_8bit_and_float() {
|
||||
}
|
||||
let f: Vec<f32> = src8.iter().map(|v| *v as f32 / 255.0).collect();
|
||||
let mut labf = vec![0.0f32; f.len()];
|
||||
to.convert_f32(&f, 3, &mut labf, 3, false);
|
||||
to.convert_f32(&f, 3, &mut labf, 3, false).unwrap();
|
||||
let mut rgbf = vec![0.0f32; f.len()];
|
||||
back.convert_f32(&labf, 3, &mut rgbf, 3, false);
|
||||
back.convert_f32(&labf, 3, &mut rgbf, 3, false).unwrap();
|
||||
for (a, b) in src8.iter().zip(&rgbf) {
|
||||
assert!((*a as f32 - b * 255.0).abs() <= 1.0, "{a} vs {}", b * 255.0);
|
||||
}
|
||||
// 8-bit RGB stored as 16-bit Lab then back to 8-bit through the lookup tables.
|
||||
let lab16: Vec<u16> = labf.iter().map(|v| (v.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16).collect();
|
||||
let mut rgb16 = vec![0u16; lab16.len()];
|
||||
back.convert_u16(&lab16, 3, &mut rgb16, 3, false);
|
||||
back.convert_u16(&lab16, 3, &mut rgb16, 3, false).unwrap();
|
||||
for (a, b) in src8.iter().zip(&rgb16) {
|
||||
assert!((*a as f32 - *b as f32 / 257.0).abs() <= 1.0, "{a} vs {}", *b as f32 / 257.0);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ fn rgb_lab_rgb_roundtrip_8bit_and_float() {
|
||||
}
|
||||
// Fast 8-bit path: RGB8 → Lab8-grid → compare with exact within 1 level of Lab encoding.
|
||||
let mut lab8 = vec![0u8; src8.len()];
|
||||
to.convert_u8(&src8, 3, &mut lab8, 3, false);
|
||||
to.convert_u8(&src8, 3, &mut lab8, 3, false).unwrap();
|
||||
for (q, e) in lab8.iter().zip(&labf) {
|
||||
assert!((*q as f32 - e * 255.0).abs() <= 1.0);
|
||||
}
|
||||
@@ -115,10 +115,10 @@ fn rgb_to_rgb_matrix_shaper_paths_agree() {
|
||||
let t = Transform::new(srgb(), dst.profile(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let src: Vec<u8> = (0..=255u8).flat_map(|v| [v, v / 2, 255 - v]).collect();
|
||||
let mut out = vec![0u8; src.len()];
|
||||
t.convert_u8(&src, 3, &mut out, 3, false);
|
||||
t.convert_u8(&src, 3, &mut out, 3, false).unwrap();
|
||||
for (px, o) in src.chunks(3).zip(out.chunks(3)) {
|
||||
let mut e = [0.0f32; 3];
|
||||
t.eval(&[px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0], &mut e);
|
||||
t.eval(&[px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0], &mut e).unwrap();
|
||||
for k in 0..3 {
|
||||
assert!((o[k] as f32 - e[k].clamp(0.0, 1.0) * 255.0).abs() <= 0.6, "{dst:?} {px:?}: {o:?} vs {e:?}");
|
||||
}
|
||||
@@ -131,7 +131,7 @@ fn identity_transform_is_identity() {
|
||||
let t = Transform::new(srgb(), srgb(), Intent::RelativeColorimetric, true).unwrap();
|
||||
let src: Vec<u8> = (0..=255u8).flat_map(|v| [v, 255 - v, v / 3, 77]).collect();
|
||||
let mut out = vec![0u8; src.len()];
|
||||
t.convert_u8(&src, 4, &mut out, 4, true);
|
||||
t.convert_u8(&src, 4, &mut out, 4, true).unwrap();
|
||||
assert_eq!(src, out);
|
||||
}
|
||||
|
||||
@@ -162,13 +162,13 @@ fn cmyk_profile_is_monotonic_and_in_range() {
|
||||
for v in (0..=255).rev().step_by(5) {
|
||||
let g = v as f32 / 255.0;
|
||||
let mut o = [0.0f32; 4];
|
||||
t.eval(&[g, g, g], &mut o);
|
||||
t.eval(&[g, g, g], &mut o).unwrap();
|
||||
assert!(o.iter().all(|c| (-1e-6..=1.0 + 1e-6).contains(c)), "{o:?}");
|
||||
assert!(o.iter().sum::<f32>() <= 3.0 + 0.02, "TAC {o:?}");
|
||||
assert!(o[3] >= prev_k - 0.01, "{intent:?} K not monotonic at {v}: {o:?}");
|
||||
prev_k = o[3];
|
||||
let mut l = [0.0f32; 3];
|
||||
back.eval(&o, &mut l);
|
||||
back.eval(&o, &mut l).unwrap();
|
||||
let lab = dec(&l);
|
||||
assert!(lab[0] <= prev_l + 0.3, "{intent:?} L* not monotonic at {v}: {lab:?}");
|
||||
assert!(lab[1].abs() < 3.0 && lab[2].abs() < 3.0, "{intent:?} gray {v} not neutral: {lab:?}");
|
||||
@@ -179,7 +179,7 @@ fn cmyk_profile_is_monotonic_and_in_range() {
|
||||
let t = Transform::new(srgb(), p, Intent::RelativeColorimetric, true).unwrap();
|
||||
let src: Vec<u8> = (0..4096u32).flat_map(|i| [(i * 37 % 256) as u8, (i * 91 % 256) as u8, (i * 13 % 256) as u8]).collect();
|
||||
let mut out = vec![0u8; 4096 * 4];
|
||||
t.convert_u8(&src, 3, &mut out, 4, false);
|
||||
t.convert_u8(&src, 3, &mut out, 4, false).unwrap();
|
||||
for px in out.chunks(4) {
|
||||
assert!(px.iter().map(|v| *v as u32).sum::<u32>() <= 3 * 255 + 8, "{px:?}");
|
||||
}
|
||||
@@ -191,9 +191,9 @@ fn in_gamut_colours_survive_cmyk_roundtrip() {
|
||||
let back = Transform::new(cmyk(), lab(), Intent::RelativeColorimetric, false).unwrap();
|
||||
for target in [[50.0, 0.0, 0.0], [70.0, 10.0, 10.0], [60.0, -20.0, -20.0], [80.0, 0.0, 40.0], [40.0, 30.0, -10.0]] {
|
||||
let mut c = [0.0f32; 4];
|
||||
to.eval(&enc(target), &mut c);
|
||||
to.eval(&enc(target), &mut c).unwrap();
|
||||
let mut l = [0.0f32; 3];
|
||||
back.eval(&c, &mut l);
|
||||
back.eval(&c, &mut l).unwrap();
|
||||
assert!(delta_e76(dec(&l), target) < 2.0, "{target:?} -> {c:?} -> {:?}", dec(&l));
|
||||
}
|
||||
}
|
||||
@@ -205,9 +205,9 @@ fn intents_differ_as_expected() {
|
||||
let sat = Transform::new(srgb(), cmyk(), Intent::Saturation, false).unwrap();
|
||||
let (mut a, mut b, mut s) = ([0.0f32; 4], [0.0f32; 4], [0.0f32; 4]);
|
||||
// Out-of-gamut sRGB blue: perceptual compresses, relative clips: different separations.
|
||||
per.eval(&[0.0, 0.2, 1.0], &mut a);
|
||||
rel.eval(&[0.0, 0.2, 1.0], &mut b);
|
||||
sat.eval(&[0.0, 0.2, 1.0], &mut s);
|
||||
per.eval(&[0.0, 0.2, 1.0], &mut a).unwrap();
|
||||
rel.eval(&[0.0, 0.2, 1.0], &mut b).unwrap();
|
||||
sat.eval(&[0.0, 0.2, 1.0], &mut s).unwrap();
|
||||
let d: f32 = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).sum();
|
||||
assert!(d > 0.03, "perceptual {a:?} vs relative {b:?}");
|
||||
assert_eq!(a, s, "saturation shares the perceptual table");
|
||||
@@ -216,8 +216,8 @@ fn intents_differ_as_expected() {
|
||||
let rel = Transform::new(cmyk(), srgb(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let abs = Transform::new(cmyk(), srgb(), Intent::AbsoluteColorimetric, false).unwrap();
|
||||
let (mut w1, mut w2) = ([0.0f32; 3], [0.0f32; 3]);
|
||||
rel.eval(&[0.0; 4], &mut w1);
|
||||
abs.eval(&[0.0; 4], &mut w2);
|
||||
rel.eval(&[0.0; 4], &mut w1).unwrap();
|
||||
abs.eval(&[0.0; 4], &mut w2).unwrap();
|
||||
assert!(w1.iter().all(|v| *v > 0.995), "{w1:?}");
|
||||
assert!(w2[0] < 0.97 && w2[2] > w2[0], "{w2:?}");
|
||||
let l = to_lab(cmyk(), &[0.0; 4], Intent::AbsoluteColorimetric);
|
||||
@@ -259,7 +259,7 @@ fn absolute_colorimetric_refuses_a_degenerate_media_white() {
|
||||
Err(e) => panic!("{name}: wrong error {e}"),
|
||||
Ok(t) => {
|
||||
let mut out = [0.0f32; 3];
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out);
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out).unwrap();
|
||||
panic!("{name}: built a transform that gives {out:?}");
|
||||
}
|
||||
}
|
||||
@@ -270,17 +270,17 @@ fn absolute_colorimetric_refuses_a_degenerate_media_white() {
|
||||
// Without paper simulation the proof leg is relative colorimetric and stays finite.
|
||||
let t = Transform::proof(srgb(), bad, srgb(), Intent::RelativeColorimetric, true, false).unwrap();
|
||||
let mut out = [0.0f32; 3];
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out);
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out).unwrap();
|
||||
assert!(out.iter().all(|v| v.is_finite()), "{name}: {out:?}");
|
||||
let t = Transform::new(srgb(), bad, Intent::RelativeColorimetric, false).unwrap();
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out);
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out).unwrap();
|
||||
assert!(out.iter().all(|v| v.is_finite()), "{name}: {out:?}");
|
||||
}
|
||||
// A sane patched white still builds and scales.
|
||||
let d50 = srgb_with_wtpt([0xF6D6, 0x10000, 0xD32D]);
|
||||
let t = Transform::new(srgb(), &d50, Intent::AbsoluteColorimetric, false).unwrap();
|
||||
let mut out = [0.0f32; 3];
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out);
|
||||
t.eval(&[0.2, 0.4, 0.6], &mut out).unwrap();
|
||||
assert!(out.iter().all(|v| v.is_finite()), "{out:?}");
|
||||
}
|
||||
|
||||
@@ -297,9 +297,9 @@ fn black_point_compensation() {
|
||||
let back = Transform::new(cmyk(), lab(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let l_of = |t: &Transform, v: f32| {
|
||||
let mut c = [0.0f32; 4];
|
||||
t.eval(&[v, v, v], &mut c);
|
||||
t.eval(&[v, v, v], &mut c).unwrap();
|
||||
let mut l = [0.0f32; 3];
|
||||
back.eval(&c, &mut l);
|
||||
back.eval(&c, &mut l).unwrap();
|
||||
dec(&l)[0]
|
||||
};
|
||||
let (w0, w1) = (l_of(&with, 0.0), l_of(&with, 0.08));
|
||||
@@ -309,14 +309,14 @@ fn black_point_compensation() {
|
||||
// CMYK → sRGB: with BPC the darkest CMYK maps to (near) sRGB black.
|
||||
let dark = {
|
||||
let mut c = [0.0f32; 4];
|
||||
with.eval(&[0.0, 0.0, 0.0], &mut c);
|
||||
with.eval(&[0.0, 0.0, 0.0], &mut c).unwrap();
|
||||
c
|
||||
};
|
||||
let rgb_with = Transform::new(cmyk(), srgb(), Intent::RelativeColorimetric, true).unwrap();
|
||||
let rgb_without = Transform::new(cmyk(), srgb(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let (mut a, mut b) = ([0.0f32; 3], [0.0f32; 3]);
|
||||
rgb_with.eval(&dark, &mut a);
|
||||
rgb_without.eval(&dark, &mut b);
|
||||
rgb_with.eval(&dark, &mut a).unwrap();
|
||||
rgb_without.eval(&dark, &mut b).unwrap();
|
||||
assert!(a[1] < 0.03 && b[1] > a[1] + 0.03, "with {a:?} without {b:?}");
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ fn black_point_compensation() {
|
||||
fn tetrahedral_beats_trilinear() {
|
||||
// Sample sRGB → Lab on a coarse grid and compare interpolation against the exact pipeline.
|
||||
let exact = Transform::new(srgb(), lab(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let clut = Clut::sample(vec![9, 9, 9], 3, |i, o| exact.eval(i, o));
|
||||
let clut = Clut::sample(vec![9, 9, 9], 3, |i, o| exact.eval(i, o).unwrap()).unwrap();
|
||||
let (mut tet_max, mut tri_max, mut tet_sum, mut tri_sum, mut tet_neutral, mut tri_neutral) = (0.0f64, 0.0f64, 0.0, 0.0, 0.0f64, 0.0f64);
|
||||
let mut n = 0.0;
|
||||
let mut seed = 12345u32;
|
||||
@@ -340,9 +340,9 @@ fn tetrahedral_beats_trilinear() {
|
||||
[rnd(), rnd(), rnd()]
|
||||
};
|
||||
let (mut e, mut a, mut b) = ([0.0f32; 3], [0.0f32; 3], [0.0f32; 3]);
|
||||
exact.eval(&p, &mut e);
|
||||
clut.eval(&p, &mut a);
|
||||
clut.eval_trilinear(&p, &mut b);
|
||||
exact.eval(&p, &mut e).unwrap();
|
||||
clut.eval(&p, &mut a).unwrap();
|
||||
clut.eval_trilinear(&p, &mut b).unwrap();
|
||||
let (da, db) = (delta_e76(dec(&a), dec(&e)), delta_e76(dec(&b), dec(&e)));
|
||||
if i < 200 {
|
||||
tet_neutral = tet_neutral.max(dec(&a)[1].abs().max(dec(&a)[2].abs()));
|
||||
@@ -366,24 +366,24 @@ fn proof_and_display_lut() {
|
||||
let p = Transform::proof(srgb(), cmyk(), srgb(), Intent::RelativeColorimetric, true, false).unwrap();
|
||||
let mut o = [0.0f32; 3];
|
||||
// Neutral gray stays (nearly) neutral through the proof.
|
||||
p.eval(&[0.5, 0.5, 0.5], &mut o);
|
||||
p.eval(&[0.5, 0.5, 0.5], &mut o).unwrap();
|
||||
assert!((o[0] - o[1]).abs() < 0.02 && (o[1] - o[2]).abs() < 0.03, "{o:?}");
|
||||
// Saturated blue is desaturated by the CMYK gamut.
|
||||
p.eval(&[0.0, 0.0, 1.0], &mut o);
|
||||
p.eval(&[0.0, 0.0, 1.0], &mut o).unwrap();
|
||||
assert!(o[0] > 0.1 || o[1] > 0.1, "{o:?}");
|
||||
// Paper simulation darkens white.
|
||||
let sim = Transform::proof(srgb(), cmyk(), srgb(), Intent::RelativeColorimetric, true, true).unwrap();
|
||||
sim.eval(&[1.0, 1.0, 1.0], &mut o);
|
||||
sim.eval(&[1.0, 1.0, 1.0], &mut o).unwrap();
|
||||
assert!(o[0] < 0.97, "{o:?}");
|
||||
|
||||
let t = Transform::new(srgb(), Builtin::DisplayP3.profile(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let lut = Lut3d::from_transform(&t, 33);
|
||||
let lut = Lut3d::from_transform(&t, 33).unwrap();
|
||||
assert_eq!(lut.to_rgba16f_bytes().len(), 33 * 33 * 33 * 8);
|
||||
let mut worst = 0.0f32;
|
||||
for i in 0..500u32 {
|
||||
let c = [(i * 7 % 101) as f32 / 100.0, (i * 13 % 101) as f32 / 100.0, (i * 29 % 101) as f32 / 100.0];
|
||||
let mut e = [0.0f32; 3];
|
||||
t.eval(&c, &mut e);
|
||||
t.eval(&c, &mut e).unwrap();
|
||||
let s = lut.sample(c);
|
||||
for k in 0..3 {
|
||||
worst = worst.max((s[k] - e[k]).abs());
|
||||
@@ -397,16 +397,16 @@ fn gray_profiles() {
|
||||
let t = Transform::new(Builtin::SGray.profile(), srgb(), Intent::RelativeColorimetric, false).unwrap();
|
||||
for v in [0.0f32, 0.2, 0.5, 0.9, 1.0] {
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(&[v], &mut o);
|
||||
t.eval(&[v], &mut o).unwrap();
|
||||
for c in o {
|
||||
assert!((c - v).abs() < 1e-3, "sGray {v} -> {o:?}");
|
||||
}
|
||||
}
|
||||
let t = Transform::new(srgb(), Builtin::GrayGamma22.profile(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let mut o = [0.0f32; 1];
|
||||
t.eval(&[1.0, 1.0, 1.0], &mut o);
|
||||
t.eval(&[1.0, 1.0, 1.0], &mut o).unwrap();
|
||||
assert!((o[0] - 1.0).abs() < 1e-3);
|
||||
t.eval(&[0.0, 0.0, 0.0], &mut o);
|
||||
t.eval(&[0.0, 0.0, 0.0], &mut o).unwrap();
|
||||
assert!(o[0].abs() < 1e-3);
|
||||
}
|
||||
|
||||
@@ -423,8 +423,8 @@ fn hdr_floats_survive_matrix_transforms() {
|
||||
let src = [4.0f32, 2.0, 0.5];
|
||||
let mut mid = [0.0f32; 3];
|
||||
let mut out = [0.0f32; 3];
|
||||
t.eval(&src, &mut mid);
|
||||
back.eval(&mid, &mut out);
|
||||
t.eval(&src, &mut mid).unwrap();
|
||||
back.eval(&mid, &mut out).unwrap();
|
||||
for k in 0..3 {
|
||||
assert!((out[k] - src[k]).abs() < 1e-3 * src[k].max(1.0), "{src:?} -> {mid:?} -> {out:?}");
|
||||
}
|
||||
@@ -482,9 +482,12 @@ fn a_light_black_cmyk_lut_has_the_known_relative_ramp() {
|
||||
outputs: 3,
|
||||
stages: vec![
|
||||
Stage::Curves(vec![Curve::Identity; 4]),
|
||||
Stage::Clut(Clut::sample(vec![2; 4], 3, |ink, out| {
|
||||
out.copy_from_slice(&enc([100.0 - 52.0 * f64::from(ink[3]), 0.0, 0.0]));
|
||||
})),
|
||||
Stage::Clut(
|
||||
Clut::sample(vec![2; 4], 3, |ink, out| {
|
||||
out.copy_from_slice(&enc([100.0 - 52.0 * f64::from(ink[3]), 0.0, 0.0]));
|
||||
})
|
||||
.unwrap(),
|
||||
),
|
||||
Stage::Curves(vec![Curve::Identity; 3]),
|
||||
],
|
||||
});
|
||||
@@ -557,7 +560,7 @@ fn system_profiles() {
|
||||
let t = Transform::new(&p, lab(), intent, true).unwrap_or_else(|e| panic!("{} {intent:?}: {e}", path.display()));
|
||||
let n = p.channels();
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(&vec![0.5; n], &mut o);
|
||||
t.eval(&vec![0.5; n], &mut o).unwrap();
|
||||
assert!(o.iter().all(|v| v.is_finite()), "{}", path.display());
|
||||
}
|
||||
// Additive white is (near) L* 100 for display profiles in relative colorimetric.
|
||||
@@ -576,9 +579,9 @@ fn system_profiles() {
|
||||
let to = Transform::new(lab(), &p, Intent::RelativeColorimetric, false).unwrap();
|
||||
let back = Transform::new(&p, lab(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let mut c = [0.0f32; 4];
|
||||
to.eval(&enc([60.0, 10.0, -10.0]), &mut c);
|
||||
to.eval(&enc([60.0, 10.0, -10.0]), &mut c).unwrap();
|
||||
let mut l = [0.0f32; 3];
|
||||
back.eval(&c, &mut l);
|
||||
back.eval(&c, &mut l).unwrap();
|
||||
assert!(delta_e76(dec(&l), [60.0, 10.0, -10.0]) < 3.0, "{}: {:?}", path.display(), dec(&l));
|
||||
}
|
||||
}
|
||||
@@ -601,7 +604,7 @@ fn oracle_moxcms_rgb() {
|
||||
mt.transform(&src, &mut theirs).unwrap();
|
||||
let ours_t = Transform::new(srgb(), dst.profile(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let mut ours = vec![0u8; src.len()];
|
||||
ours_t.convert_u8(&src, 3, &mut ours, 3, false);
|
||||
ours_t.convert_u8(&src, 3, &mut ours, 3, false).unwrap();
|
||||
let worst = ours.iter().zip(&theirs).map(|(a, b)| (*a as i32 - *b as i32).abs()).max().unwrap();
|
||||
assert!(worst <= 2, "{dst:?}: max diff {worst}");
|
||||
}
|
||||
@@ -622,7 +625,7 @@ fn oracle_moxcms_system_cmyk() {
|
||||
mt.transform(&src, &mut theirs).unwrap();
|
||||
let t = Transform::new(&ours_p, srgb(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let mut ours = vec![0u8; 2048 * 3];
|
||||
t.convert_u8(&src, 4, &mut ours, 3, false);
|
||||
t.convert_u8(&src, 4, &mut ours, 3, false).unwrap();
|
||||
let diffs: Vec<i32> = ours.iter().zip(&theirs).map(|(a, b)| (*a as i32 - *b as i32).abs()).collect();
|
||||
let worst = *diffs.iter().max().unwrap();
|
||||
let mean = diffs.iter().sum::<i32>() as f64 / diffs.len() as f64;
|
||||
@@ -824,3 +827,69 @@ fn device_links_parse_and_are_reported_unsupported() {
|
||||
// The output space still bounds the tag: a CMYK→CMYK link with a 4→3 table is malformed.
|
||||
assert!(matches!(Profile::parse(&device_link(b"CMYK", b"CMYK", 4, 3)), Err(craft_cms::CmsError::Invalid(_))));
|
||||
}
|
||||
|
||||
fn is_arg_err<T: std::fmt::Debug>(r: Result<T, craft_cms::CmsError>) -> bool {
|
||||
matches!(r, Err(craft_cms::CmsError::InvalidArgument(_)))
|
||||
}
|
||||
|
||||
/// Strides, buffer lengths and slices that do not fit are refused before anything is written.
|
||||
#[test]
|
||||
fn transform_rejects_bad_buffers() {
|
||||
let t = Transform::new(srgb(), cmyk(), Intent::RelativeColorimetric, true).unwrap();
|
||||
let src = [255u8; 12];
|
||||
let mut dst = [7u8; 16];
|
||||
assert!(is_arg_err(t.convert_u8(&src, 2, &mut dst, 4, false)), "source stride below 3 inputs");
|
||||
assert!(is_arg_err(t.convert_u8(&src, 3, &mut dst, 3, false)), "destination stride below 4 outputs");
|
||||
assert!(is_arg_err(t.convert_u8(&src[..11], 3, &mut dst, 4, false)), "partial source pixel");
|
||||
assert!(is_arg_err(t.convert_u8(&src, 3, &mut dst[..15], 4, false)), "partial destination pixel");
|
||||
assert!(is_arg_err(t.convert_u8(&src, 3, &mut dst[..12], 4, false)), "destination too short");
|
||||
assert_eq!(dst, [7u8; 16], "refused calls write nothing");
|
||||
t.convert_u8(&src, 3, &mut dst, 4, false).unwrap();
|
||||
assert!(is_arg_err(t.convert_u16(&[0u16; 6], 3, &mut [0u16; 4], 4, false)));
|
||||
assert!(is_arg_err(t.convert_f32(&[0.0; 7], 3, &mut [0.0; 12], 4, false)));
|
||||
assert!(is_arg_err(t.apply(&mut [0.0; 12], 3)), "in-place stride below 4 outputs");
|
||||
assert!(is_arg_err(t.apply(&mut [0.0; 10], 4)), "partial in-place pixel");
|
||||
let (s, mut d) = ([0u8; 6], [0u8; 7]);
|
||||
assert!(is_arg_err(t.convert_bytes_many(craft_cms::SampleKind::U16, vec![(&s[..], &mut d[..])], 3, 4, false)));
|
||||
let mut out = [0.0f32; 4];
|
||||
assert!(is_arg_err(t.eval(&[1.0, 1.0], &mut out)));
|
||||
assert!(is_arg_err(t.eval(&[1.0; 3], &mut out[..3])));
|
||||
assert!(is_arg_err(t.eval_fast(&[1.0, 1.0], &mut out)));
|
||||
let g = craft_cms::GamutCheck::new(srgb(), cmyk(), 4.0).unwrap();
|
||||
assert!(is_arg_err(g.delta_e(&[0.5, 0.5])));
|
||||
assert!(is_arg_err(Lut3d::from_transform(&t, 33)), "3→4 transform");
|
||||
let d = Transform::new(srgb(), Builtin::DisplayP3.profile(), Intent::RelativeColorimetric, true).unwrap();
|
||||
assert!(is_arg_err(Lut3d::from_transform(&d, 1)));
|
||||
assert!(is_arg_err(Lut3d::from_transform(&d, 100_000)));
|
||||
}
|
||||
|
||||
/// Table and pipeline dimensions that do not fit are refused.
|
||||
#[test]
|
||||
fn tables_and_pipelines_reject_bad_dimensions() {
|
||||
use craft_cms::pipeline::{Pipeline, Stage};
|
||||
assert!(is_arg_err(Clut::new(vec![2, 2], 3, vec![0.0; 11])), "data length");
|
||||
assert!(is_arg_err(Clut::new(vec![2, 0], 3, Vec::new())), "zero grid points");
|
||||
assert!(is_arg_err(Clut::new(vec![2; 16], 1, vec![0.0; 1 << 16])), "16 inputs");
|
||||
assert!(is_arg_err(Clut::new(vec![2], 0, Vec::new())), "no outputs");
|
||||
assert!(is_arg_err(Clut::sample(vec![usize::MAX; 3], 3, |_, _| {})), "overflowing grid");
|
||||
assert!(is_arg_err(Clut::sample(vec![1000; 3], 3, |_, _| {})), "grid over the size limit");
|
||||
let c = Clut::sample(vec![2; 4], 3, |_, _| {}).unwrap();
|
||||
assert!(is_arg_err(c.eval(&[0.0; 3], &mut [0.0; 3])));
|
||||
assert!(is_arg_err(c.eval(&[0.0; 4], &mut [0.0; 2])));
|
||||
assert!(is_arg_err(c.eval_trilinear(&[0.0; 4], &mut [0.0; 3])), "trilinear needs 3 inputs");
|
||||
assert!(is_arg_err(Pipeline::new(0, Vec::new())));
|
||||
assert!(is_arg_err(Pipeline::new(17, Vec::new())));
|
||||
let bad = Stage::Matrix { rows: 3, cols: 3, m: vec![1.0; 8], offset: vec![0.0; 3] };
|
||||
assert!(is_arg_err(Pipeline::new(3, vec![bad.clone()])), "matrix data shorter than its shape");
|
||||
let wide = Stage::Matrix { rows: 40, cols: 1, m: vec![1.0; 40], offset: vec![0.0; 40] };
|
||||
assert!(is_arg_err(Pipeline::new(1, vec![wide])), "too many rows");
|
||||
let mut p = Pipeline::new(3, vec![Stage::Clamp01]).unwrap();
|
||||
assert!(is_arg_err(p.extend([bad])));
|
||||
assert_eq!(p.stages().len(), 1, "a refused extend leaves the pipeline unchanged");
|
||||
assert!(is_arg_err(p.eval(&[0.0; 2], &mut [0.0; 3])));
|
||||
assert!(craft_cms::lutfile::LutFile::from_fn("x", 1, |c| c).is_err());
|
||||
assert!(craft_cms::lutfile::LutFile::identity(1 << 20).is_err());
|
||||
let params = |a2b, b2a| craft_cms::synth::CmykParams { grid_a2b: a2b, grid_b2a: b2a, ..Default::default() };
|
||||
assert!(is_arg_err(craft_cms::synth::cmyk_profile(¶ms(1, 21))));
|
||||
assert!(is_arg_err(craft_cms::synth::cmyk_profile(¶ms(11, 100_000))));
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ fn profile_lab(profile: &Profile) -> impl Fn([f64; 4]) -> [f64; 3] {
|
||||
let t = Transform::new(profile, Builtin::LabD50.profile(), Intent::RelativeColorimetric, false).unwrap();
|
||||
move |c| {
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(&c.map(|v| v as f32), &mut o);
|
||||
t.eval(&c.map(|v| v as f32), &mut o).unwrap();
|
||||
[o[0] as f64 * 100.0, o[1] as f64 * 255.0 - 128.0, o[2] as f64 * 255.0 - 128.0]
|
||||
}
|
||||
}
|
||||
@@ -444,7 +444,7 @@ fn in_gamut_lab_round_trips() {
|
||||
let target = to_lab(p.cmyk);
|
||||
let enc = [(target[0] / 100.0) as f32, ((target[1] + 128.0) / 255.0) as f32, ((target[2] + 128.0) / 255.0) as f32];
|
||||
let mut c = [0.0f32; 4];
|
||||
to_cmyk.eval(&enc, &mut c);
|
||||
to_cmyk.eval(&enc, &mut c).unwrap();
|
||||
assert!(c.iter().map(|v| *v as f64).sum::<f64>() <= 3.3 + 0.02, "{target:?} -> {c:?}");
|
||||
delta_e2000(to_lab(c.map(f64::from)), target)
|
||||
})
|
||||
@@ -473,7 +473,7 @@ fn to_srgb(profile: &Profile) -> impl Fn([f64; 4]) -> [u8; 3] {
|
||||
let t = Transform::new(profile, Builtin::Srgb.profile(), Intent::RelativeColorimetric, true).unwrap();
|
||||
move |c| {
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(&c.map(|v| v as f32), &mut o);
|
||||
t.eval(&c.map(|v| v as f32), &mut o).unwrap();
|
||||
o.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
|
||||
}
|
||||
}
|
||||
@@ -481,7 +481,7 @@ fn to_srgb(profile: &Profile) -> impl Fn([f64; 4]) -> [u8; 3] {
|
||||
fn srgb_lab(rgb: [u8; 3]) -> [f64; 3] {
|
||||
let t = Transform::new(Builtin::Srgb.profile(), Builtin::LabD50.profile(), Intent::RelativeColorimetric, false).unwrap();
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(&rgb.map(|v| v as f32 / 255.0), &mut o);
|
||||
t.eval(&rgb.map(|v| v as f32 / 255.0), &mut o).unwrap();
|
||||
[o[0] as f64 * 100.0, o[1] as f64 * 255.0 - 128.0, o[2] as f64 * 255.0 - 128.0]
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ fn regen_or_check_coated_cmyk() {
|
||||
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/profiles/photocraft-coated-cmyk.icc");
|
||||
if std::env::var_os("CRAFT_CMS_REGEN_PROFILES").is_some() {
|
||||
let t = std::time::Instant::now();
|
||||
let p = craft_cms::synth::coated_cmyk();
|
||||
let p = craft_cms::synth::coated_cmyk().unwrap();
|
||||
let bytes = p.to_bytes();
|
||||
std::fs::write(path, &*bytes).unwrap();
|
||||
eprintln!("wrote {} bytes in {:?}", bytes.len(), t.elapsed());
|
||||
@@ -32,7 +32,7 @@ fn regen_or_check_coated_cmyk() {
|
||||
for y in [0.0, 0.55, 1.0] {
|
||||
for k in [0.0, 0.5, 1.0] {
|
||||
let mut o = [0.0f32; 3];
|
||||
t.eval(&[c as f32, m as f32, y as f32, k as f32], &mut o);
|
||||
t.eval(&[c as f32, m as f32, y as f32, k as f32], &mut o).unwrap();
|
||||
let got = [o[0] as f64 * 100.0, o[1] as f64 * 255.0 - 128.0, o[2] as f64 * 255.0 - 128.0];
|
||||
let want = model.lab([c, m, y, k]);
|
||||
worst = worst.max(craft_cms::math::delta_e76(got, want));
|
||||
|
||||
Reference in New Issue
Block a user