moxcms/conversions/katana/
md_nx3.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved.
3 * //
4 * // Redistribution and use in source and binary forms, with or without modification,
5 * // are permitted provided that the following conditions are met:
6 * //
7 * // 1.  Redistributions of source code must retain the above copyright notice, this
8 * // list of conditions and the following disclaimer.
9 * //
10 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * // this list of conditions and the following disclaimer in the documentation
12 * // and/or other materials provided with the distribution.
13 * //
14 * // 3.  Neither the name of the copyright holder nor the names of its
15 * // contributors may be used to endorse or promote products derived from
16 * // this software without specific prior written permission.
17 * //
18 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29use crate::conversions::katana::KatanaInitialStage;
30use crate::conversions::katana::md3x3::MultidimensionalDirection;
31use crate::conversions::katana::md4x3::{execute_matrix_stage3, execute_simple_curves3};
32use crate::conversions::md_lut::{
33    MultidimensionalLut, NVector, linear_1i_vec3f, linear_2i_vec3f_direct, linear_3i_vec3f_direct,
34    linear_4i_vec3f, linear_5i_vec3f, linear_6i_vec3f, linear_7i_vec3f, linear_8i_vec3f,
35    linear_9i_vec3f, linear_10i_vec3f, linear_11i_vec3f, linear_12i_vec3f, linear_13i_vec3f,
36    linear_14i_vec3f, linear_15i_vec3f,
37};
38use crate::safe_math::SafeMul;
39use crate::trc::lut_interp_linear_float;
40use crate::{
41    CmsError, DataColorSpace, Layout, LutMultidimensionalType, MalformedSize, Matrix3d, Matrix3f,
42    PointeeSizeExpressible, TransformOptions, Vector3d, Vector3f,
43};
44use num_traits::AsPrimitive;
45use std::marker::PhantomData;
46
47struct MultidimensionalNx3<
48    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
49    const BIT_DEPTH: usize,
50> {
51    a_curves: Option<Vec<Vec<f32>>>,
52    m_curves: Option<Box<[Vec<f32>; 3]>>,
53    b_curves: Option<Box<[Vec<f32>; 3]>>,
54    clut: Option<Vec<f32>>,
55    matrix: Matrix3f,
56    bias: Vector3f,
57    direction: MultidimensionalDirection,
58    grid_size: [u8; 16],
59    input_inks: usize,
60    _phantom: PhantomData<T>,
61}
62
63#[inline(never)]
64pub(crate) fn interpolate_out_function(
65    layout: Layout,
66) -> fn(lut: &MultidimensionalLut, arr: &[f32], inputs: &[f32]) -> NVector<f32, 3> {
67    const OUT: usize = 3;
68    match layout {
69        Layout::Rgb => linear_3i_vec3f_direct::<OUT>,
70        Layout::Rgba => linear_4i_vec3f::<OUT>,
71        Layout::Gray => linear_1i_vec3f::<OUT>,
72        Layout::GrayAlpha => linear_2i_vec3f_direct::<OUT>,
73        Layout::Inks5 => linear_5i_vec3f::<OUT>,
74        Layout::Inks6 => linear_6i_vec3f::<OUT>,
75        Layout::Inks7 => linear_7i_vec3f::<OUT>,
76        Layout::Inks8 => linear_8i_vec3f::<OUT>,
77        Layout::Inks9 => linear_9i_vec3f::<OUT>,
78        Layout::Inks10 => linear_10i_vec3f::<OUT>,
79        Layout::Inks11 => linear_11i_vec3f::<OUT>,
80        Layout::Inks12 => linear_12i_vec3f::<OUT>,
81        Layout::Inks13 => linear_13i_vec3f::<OUT>,
82        Layout::Inks14 => linear_14i_vec3f::<OUT>,
83        Layout::Inks15 => linear_15i_vec3f::<OUT>,
84    }
85}
86
87impl<
88    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
89    const BIT_DEPTH: usize,
90> MultidimensionalNx3<T, BIT_DEPTH>
91{
92    fn to_pcs_impl(&self, input: &[T], dst: &mut [f32]) -> Result<(), CmsError> {
93        let norm_value = if T::FINITE {
94            1.0 / ((1u32 << BIT_DEPTH) - 1) as f32
95        } else {
96            1.0
97        };
98        assert_eq!(
99            self.direction,
100            MultidimensionalDirection::DeviceToPcs,
101            "PCS to device cannot be used on `to pcs` stage"
102        );
103
104        // A -> B
105        // OR B - A A - curves stage
106
107        if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) {
108            let layout = Layout::from_inks(self.input_inks);
109
110            let mut inks = vec![0.; self.input_inks];
111
112            if clut.is_empty() {
113                return Err(CmsError::InvalidAtoBLut);
114            }
115
116            let fetcher = interpolate_out_function(layout);
117
118            let md_lut = MultidimensionalLut::new(self.grid_size, self.input_inks, 3);
119
120            for (src, dst) in input
121                .chunks_exact(layout.channels())
122                .zip(dst.chunks_exact_mut(3))
123            {
124                for ((ink, src_ink), curve) in inks.iter_mut().zip(src).zip(a_curves.iter()) {
125                    *ink = lut_interp_linear_float(src_ink.as_() * norm_value, curve);
126                }
127
128                let interpolated = fetcher(&md_lut, clut, &inks);
129
130                dst[0] = interpolated.v[0];
131                dst[1] = interpolated.v[1];
132                dst[2] = interpolated.v[2];
133            }
134        } else {
135            return Err(CmsError::InvalidAtoBLut);
136        }
137
138        // Matrix stage
139
140        if let Some(m_curves) = self.m_curves.as_ref() {
141            execute_simple_curves3(dst, m_curves);
142            execute_matrix_stage3(self.matrix, self.bias, dst);
143        }
144
145        // B-curves is mandatory
146        if let Some(b_curves) = &self.b_curves.as_ref() {
147            execute_simple_curves3(dst, b_curves);
148        }
149
150        Ok(())
151    }
152}
153
154impl<
155    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
156    const BIT_DEPTH: usize,
157> KatanaInitialStage<f32, T> for MultidimensionalNx3<T, BIT_DEPTH>
158{
159    fn to_pcs(&self, input: &[T]) -> Result<Vec<f32>, CmsError> {
160        if input.len() % self.input_inks != 0 {
161            return Err(CmsError::LaneMultipleOfChannels);
162        }
163
164        let mut new_dst = vec![0f32; (input.len() / self.input_inks) * 3];
165
166        self.to_pcs_impl(input, &mut new_dst)?;
167        Ok(new_dst)
168    }
169}
170
171fn make_multidimensional_nx3<
172    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
173    const BIT_DEPTH: usize,
174>(
175    mab: &LutMultidimensionalType,
176    _: TransformOptions,
177    _: DataColorSpace,
178    direction: MultidimensionalDirection,
179) -> Result<MultidimensionalNx3<T, BIT_DEPTH>, CmsError> {
180    if mab.num_output_channels != 3 {
181        return Err(CmsError::UnsupportedProfileConnection);
182    }
183    if mab.b_curves.is_empty() || mab.b_curves.len() != 3 {
184        return Err(CmsError::InvalidAtoBLut);
185    }
186
187    let clut: Option<Vec<f32>> =
188        if mab.a_curves.len() == mab.num_input_channels as usize && mab.clut.is_some() {
189            let clut = mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap();
190            let mut lut_grid = 1usize;
191            for grid in mab.grid_points.iter().take(mab.num_input_channels as usize) {
192                lut_grid = lut_grid.safe_mul(*grid as usize)?;
193            }
194            let lut_grid = lut_grid.safe_mul(mab.num_output_channels as usize)?;
195            if clut.len() != lut_grid {
196                return Err(CmsError::MalformedCurveLutTable(MalformedSize {
197                    size: clut.len(),
198                    expected: lut_grid,
199                }));
200            }
201            Some(clut)
202        } else {
203            return Err(CmsError::InvalidAtoBLut);
204        };
205
206    let a_curves: Option<Vec<Vec<f32>>> =
207        if mab.a_curves.len() == mab.num_input_channels as usize && mab.clut.is_some() {
208            let mut arr = Vec::new();
209            for a_curve in mab.a_curves.iter() {
210                arr.push(a_curve.to_clut()?);
211            }
212            Some(arr)
213        } else {
214            None
215        };
216
217    let b_curves: Option<Box<[Vec<f32>; 3]>> = if mab.b_curves.len() == 3 {
218        let mut arr = Box::<[Vec<f32>; 3]>::default();
219        let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear());
220        if all_curves_linear {
221            None
222        } else {
223            for (c_curve, dst) in mab.b_curves.iter().zip(arr.iter_mut()) {
224                *dst = c_curve.to_clut()?;
225            }
226            Some(arr)
227        }
228    } else {
229        return Err(CmsError::InvalidAtoBLut);
230    };
231
232    let matrix = mab.matrix.to_f32();
233
234    let m_curves: Option<Box<[Vec<f32>; 3]>> = if mab.m_curves.len() == 3 {
235        let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear());
236        if !all_curves_linear
237            || !mab.matrix.test_equality(Matrix3d::IDENTITY)
238            || mab.bias.ne(&Vector3d::default())
239        {
240            let mut arr = Box::<[Vec<f32>; 3]>::default();
241            for (curve, dst) in mab.m_curves.iter().zip(arr.iter_mut()) {
242                *dst = curve.to_clut()?;
243            }
244            Some(arr)
245        } else {
246            None
247        }
248    } else {
249        None
250    };
251
252    let bias = mab.bias.cast();
253
254    let transform = MultidimensionalNx3::<T, BIT_DEPTH> {
255        a_curves,
256        b_curves,
257        m_curves,
258        matrix,
259        direction,
260        clut,
261        grid_size: mab.grid_points,
262        bias,
263        input_inks: mab.num_input_channels as usize,
264        _phantom: PhantomData,
265    };
266
267    Ok(transform)
268}
269
270pub(crate) fn katana_multi_dimensional_nx3_to_pcs<
271    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
272    const BIT_DEPTH: usize,
273>(
274    src_layout: Layout,
275    mab: &LutMultidimensionalType,
276    options: TransformOptions,
277    pcs: DataColorSpace,
278) -> Result<Box<dyn KatanaInitialStage<f32, T> + Send + Sync>, CmsError> {
279    if pcs == DataColorSpace::Rgb {
280        if mab.num_input_channels != 3 {
281            return Err(CmsError::InvalidAtoBLut);
282        }
283        if src_layout != Layout::Rgba && src_layout != Layout::Rgb {
284            return Err(CmsError::InvalidInksCountForProfile);
285        }
286    } else if mab.num_input_channels != src_layout.channels() as u8 {
287        return Err(CmsError::InvalidInksCountForProfile);
288    }
289    let transform = make_multidimensional_nx3::<T, BIT_DEPTH>(
290        mab,
291        options,
292        pcs,
293        MultidimensionalDirection::DeviceToPcs,
294    )?;
295    Ok(Box::new(transform))
296}