moxcms/conversions/katana/
md4x3.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::mlaf::mlaf;
32use crate::safe_math::SafeMul;
33use crate::trc::lut_interp_linear_float;
34use crate::{
35    CmsError, DataColorSpace, Hypercube, InterpolationMethod, LutMultidimensionalType,
36    MalformedSize, Matrix3d, Matrix3f, PointeeSizeExpressible, TransformOptions, Vector3d,
37    Vector3f,
38};
39use num_traits::AsPrimitive;
40use std::marker::PhantomData;
41
42pub(crate) fn execute_simple_curves3(dst: &mut [f32], curves: &[Vec<f32>; 3]) {
43    let curve0 = &curves[0];
44    let curve1 = &curves[1];
45    let curve2 = &curves[2];
46
47    for dst in dst.chunks_exact_mut(3) {
48        let a0 = dst[0];
49        let a1 = dst[1];
50        let a2 = dst[2];
51        let b0 = lut_interp_linear_float(a0, curve0);
52        let b1 = lut_interp_linear_float(a1, curve1);
53        let b2 = lut_interp_linear_float(a2, curve2);
54        dst[0] = b0;
55        dst[1] = b1;
56        dst[2] = b2;
57    }
58}
59
60pub(crate) fn execute_matrix_stage3(matrix: Matrix3f, bias: Vector3f, dst: &mut [f32]) {
61    let m = matrix;
62    let b = bias;
63
64    if !m.test_equality(Matrix3f::IDENTITY) || !b.eq(&Vector3f::default()) {
65        for dst in dst.chunks_exact_mut(3) {
66            let x = dst[0];
67            let y = dst[1];
68            let z = dst[2];
69            dst[0] = mlaf(mlaf(mlaf(b.v[0], x, m.v[0][0]), y, m.v[0][1]), z, m.v[0][2]);
70            dst[1] = mlaf(mlaf(mlaf(b.v[1], x, m.v[1][0]), y, m.v[1][1]), z, m.v[1][2]);
71            dst[2] = mlaf(mlaf(mlaf(b.v[2], x, m.v[2][0]), y, m.v[2][1]), z, m.v[2][2]);
72        }
73    }
74}
75
76struct Multidimensional4x3<
77    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
78> {
79    a_curves: Option<Box<[Vec<f32>; 4]>>,
80    m_curves: Option<Box<[Vec<f32>; 3]>>,
81    b_curves: Option<Box<[Vec<f32>; 3]>>,
82    clut: Option<Vec<f32>>,
83    matrix: Matrix3f,
84    bias: Vector3f,
85    direction: MultidimensionalDirection,
86    options: TransformOptions,
87    pcs: DataColorSpace,
88    grid_size: [u8; 4],
89    _phantom: PhantomData<T>,
90    bit_depth: usize,
91}
92
93impl<T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync>
94    Multidimensional4x3<T>
95{
96    fn to_pcs_impl<Fetch: Fn(f32, f32, f32, f32) -> Vector3f>(
97        &self,
98        input: &[T],
99        dst: &mut [f32],
100        fetch: Fetch,
101    ) -> Result<(), CmsError> {
102        let norm_value = if T::FINITE {
103            1.0 / ((1u32 << self.bit_depth) - 1) as f32
104        } else {
105            1.0
106        };
107        assert_eq!(
108            self.direction,
109            MultidimensionalDirection::DeviceToPcs,
110            "PCS to device cannot be used on `to pcs` stage"
111        );
112
113        // A -> B
114        // OR B - A A - curves stage
115
116        if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) {
117            if !clut.is_empty() {
118                let curve0 = &a_curves[0];
119                let curve1 = &a_curves[1];
120                let curve2 = &a_curves[2];
121                let curve3 = &a_curves[3];
122                for (src, dst) in input.chunks_exact(4).zip(dst.chunks_exact_mut(3)) {
123                    let b0 = lut_interp_linear_float(src[0].as_() * norm_value, curve0);
124                    let b1 = lut_interp_linear_float(src[1].as_() * norm_value, curve1);
125                    let b2 = lut_interp_linear_float(src[2].as_() * norm_value, curve2);
126                    let b3 = lut_interp_linear_float(src[3].as_() * norm_value, curve3);
127                    let interpolated = fetch(b0, b1, b2, b3);
128                    dst[0] = interpolated.v[0];
129                    dst[1] = interpolated.v[1];
130                    dst[2] = interpolated.v[2];
131                }
132            }
133        } else {
134            return Err(CmsError::InvalidAtoBLut);
135        }
136
137        // Matrix stage
138
139        if let Some(m_curves) = self.m_curves.as_ref() {
140            execute_simple_curves3(dst, m_curves);
141            execute_matrix_stage3(self.matrix, self.bias, dst);
142        }
143
144        // B-curves is mandatory
145        if let Some(b_curves) = &self.b_curves.as_ref() {
146            execute_simple_curves3(dst, b_curves);
147        }
148
149        Ok(())
150    }
151}
152
153impl<T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync>
154    KatanaInitialStage<f32, T> for Multidimensional4x3<T>
155{
156    fn to_pcs(&self, input: &[T]) -> Result<Vec<f32>, CmsError> {
157        if input.len() % 4 != 0 {
158            return Err(CmsError::LaneMultipleOfChannels);
159        }
160        let fixed_new_clut = Vec::new();
161        let new_clut = self.clut.as_ref().unwrap_or(&fixed_new_clut);
162        let lut = Hypercube::new_hypercube(new_clut, self.grid_size);
163
164        let mut new_dst = vec![0f32; (input.len() / 4) * 3];
165
166        // If PCS is LAB then linear interpolation should be used
167        if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz {
168            self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| {
169                lut.quadlinear_vec3(x, y, z, w)
170            })?;
171            return Ok(new_dst);
172        }
173
174        match self.options.interpolation_method {
175            #[cfg(feature = "options")]
176            InterpolationMethod::Tetrahedral => {
177                self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| lut.tetra_vec3(x, y, z, w))?;
178            }
179            #[cfg(feature = "options")]
180            InterpolationMethod::Pyramid => {
181                self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| {
182                    lut.pyramid_vec3(x, y, z, w)
183                })?;
184            }
185            #[cfg(feature = "options")]
186            InterpolationMethod::Prism => {
187                self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| lut.prism_vec3(x, y, z, w))?;
188            }
189            InterpolationMethod::Linear => {
190                self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| {
191                    lut.quadlinear_vec3(x, y, z, w)
192                })?;
193            }
194        }
195        Ok(new_dst)
196    }
197}
198
199fn make_multidimensional_4x3<
200    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
201>(
202    mab: &LutMultidimensionalType,
203    options: TransformOptions,
204    pcs: DataColorSpace,
205    direction: MultidimensionalDirection,
206    bit_depth: usize,
207) -> Result<Multidimensional4x3<T>, CmsError> {
208    if mab.num_input_channels != 4 && mab.num_output_channels != 3 {
209        return Err(CmsError::UnsupportedProfileConnection);
210    }
211    if mab.b_curves.is_empty() || mab.b_curves.len() != 3 {
212        return Err(CmsError::InvalidAtoBLut);
213    }
214
215    let grid_size = [
216        mab.grid_points[0],
217        mab.grid_points[1],
218        mab.grid_points[2],
219        mab.grid_points[3],
220    ];
221
222    let clut: Option<Vec<f32>> = if mab.a_curves.len() == 4 && mab.clut.is_some() {
223        let clut = mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap();
224        let lut_grid = (mab.grid_points[0] as usize)
225            .safe_mul(mab.grid_points[1] as usize)?
226            .safe_mul(mab.grid_points[2] as usize)?
227            .safe_mul(mab.grid_points[3] as usize)?
228            .safe_mul(mab.num_output_channels as usize)?;
229        if clut.len() != lut_grid {
230            return Err(CmsError::MalformedCurveLutTable(MalformedSize {
231                size: clut.len(),
232                expected: lut_grid,
233            }));
234        }
235        Some(clut)
236    } else {
237        return Err(CmsError::InvalidAtoBLut);
238    };
239
240    let a_curves: Option<Box<[Vec<f32>; 4]>> = if mab.a_curves.len() == 4 && mab.clut.is_some() {
241        let mut arr = Box::<[Vec<f32>; 4]>::default();
242        for (a_curve, dst) in mab.a_curves.iter().zip(arr.iter_mut()) {
243            *dst = a_curve.to_clut()?;
244        }
245        Some(arr)
246    } else {
247        None
248    };
249
250    let b_curves: Option<Box<[Vec<f32>; 3]>> = if mab.b_curves.len() == 3 {
251        let mut arr = Box::<[Vec<f32>; 3]>::default();
252        let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear());
253        if all_curves_linear {
254            None
255        } else {
256            for (c_curve, dst) in mab.b_curves.iter().zip(arr.iter_mut()) {
257                *dst = c_curve.to_clut()?;
258            }
259            Some(arr)
260        }
261    } else {
262        return Err(CmsError::InvalidAtoBLut);
263    };
264
265    let matrix = mab.matrix.to_f32();
266
267    let m_curves: Option<Box<[Vec<f32>; 3]>> = if mab.m_curves.len() == 3 {
268        let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear());
269        if !all_curves_linear
270            || !mab.matrix.test_equality(Matrix3d::IDENTITY)
271            || mab.bias.ne(&Vector3d::default())
272        {
273            let mut arr = Box::<[Vec<f32>; 3]>::default();
274            for (curve, dst) in mab.m_curves.iter().zip(arr.iter_mut()) {
275                *dst = curve.to_clut()?;
276            }
277            Some(arr)
278        } else {
279            None
280        }
281    } else {
282        None
283    };
284
285    let bias = mab.bias.cast();
286
287    let transform = Multidimensional4x3::<T> {
288        a_curves,
289        b_curves,
290        m_curves,
291        matrix,
292        direction,
293        options,
294        clut,
295        pcs,
296        grid_size,
297        bias,
298        _phantom: PhantomData,
299        bit_depth,
300    };
301
302    Ok(transform)
303}
304
305pub(crate) fn multi_dimensional_4x3_to_pcs<
306    T: Copy + Default + AsPrimitive<f32> + PointeeSizeExpressible + Send + Sync,
307>(
308    mab: &LutMultidimensionalType,
309    options: TransformOptions,
310    pcs: DataColorSpace,
311    bit_depth: usize,
312) -> Result<Box<dyn KatanaInitialStage<f32, T> + Send + Sync>, CmsError> {
313    let transform = make_multidimensional_4x3::<T>(
314        mab,
315        options,
316        pcs,
317        MultidimensionalDirection::DeviceToPcs,
318        bit_depth,
319    )?;
320    Ok(Box::new(transform))
321}