moxcms/conversions/
mba3x4.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 3/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::mab::{BCurves3, MCurves3};
30use crate::safe_math::SafeMul;
31use crate::{
32    CmsError, Cube, DataColorSpace, InPlaceStage, InterpolationMethod, LutMultidimensionalType,
33    MalformedSize, Matrix3d, Stage, TransformOptions, Vector3d, Vector4f,
34};
35
36struct ACurves3x4Inverse<'a, const DEPTH: usize> {
37    curve0: Box<[f32; 65536]>,
38    curve1: Box<[f32; 65536]>,
39    curve2: Box<[f32; 65536]>,
40    curve3: Box<[f32; 65536]>,
41    clut: &'a [f32],
42    grid_size: [u8; 3],
43    interpolation_method: InterpolationMethod,
44    pcs: DataColorSpace,
45}
46
47struct ACurves3x4InverseOptimized<'a> {
48    clut: &'a [f32],
49    grid_size: [u8; 3],
50    interpolation_method: InterpolationMethod,
51    pcs: DataColorSpace,
52}
53
54impl<const DEPTH: usize> ACurves3x4Inverse<'_, DEPTH> {
55    fn transform_impl<Fetch: Fn(f32, f32, f32) -> Vector4f>(
56        &self,
57        src: &[f32],
58        dst: &mut [f32],
59        fetch: Fetch,
60    ) -> Result<(), CmsError> {
61        let scale_value = (DEPTH as u32 - 1u32) as f32;
62
63        assert_eq!(src.len() / 3, dst.len() / 4);
64
65        for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(4)) {
66            let interpolated = fetch(src[0], src[1], src[2]);
67            let a0 = (interpolated.v[0] * scale_value).round().min(scale_value) as u16;
68            let a1 = (interpolated.v[1] * scale_value).round().min(scale_value) as u16;
69            let a2 = (interpolated.v[2] * scale_value).round().min(scale_value) as u16;
70            let a3 = (interpolated.v[3] * scale_value).round().min(scale_value) as u16;
71            let b0 = self.curve0[a0 as usize];
72            let b1 = self.curve1[a1 as usize];
73            let b2 = self.curve2[a2 as usize];
74            let b3 = self.curve3[a3 as usize];
75            dst[0] = b0;
76            dst[1] = b1;
77            dst[2] = b2;
78            dst[3] = b3;
79        }
80        Ok(())
81    }
82}
83
84impl ACurves3x4InverseOptimized<'_> {
85    fn transform_impl<Fetch: Fn(f32, f32, f32) -> Vector4f>(
86        &self,
87        src: &[f32],
88        dst: &mut [f32],
89        fetch: Fetch,
90    ) -> Result<(), CmsError> {
91        assert_eq!(src.len() / 3, dst.len() / 4);
92
93        for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(4)) {
94            let interpolated = fetch(src[0], src[1], src[2]);
95            let b0 = interpolated.v[0];
96            let b1 = interpolated.v[1];
97            let b2 = interpolated.v[2];
98            let b3 = interpolated.v[3];
99            dst[0] = b0;
100            dst[1] = b1;
101            dst[2] = b2;
102            dst[3] = b3;
103        }
104        Ok(())
105    }
106}
107
108impl<const DEPTH: usize> Stage for ACurves3x4Inverse<'_, DEPTH> {
109    fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> {
110        let lut = Cube::new_cube(self.clut, self.grid_size);
111
112        // If PCS is LAB then linear interpolation should be used
113        if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz {
114            return self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z));
115        }
116
117        match self.interpolation_method {
118            #[cfg(feature = "options")]
119            InterpolationMethod::Tetrahedral => {
120                self.transform_impl(src, dst, |x, y, z| lut.tetra_vec4(x, y, z))?;
121            }
122            #[cfg(feature = "options")]
123            InterpolationMethod::Pyramid => {
124                self.transform_impl(src, dst, |x, y, z| lut.pyramid_vec4(x, y, z))?;
125            }
126            #[cfg(feature = "options")]
127            InterpolationMethod::Prism => {
128                self.transform_impl(src, dst, |x, y, z| lut.prism_vec4(x, y, z))?;
129            }
130            InterpolationMethod::Linear => {
131                self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z))?;
132            }
133        }
134        Ok(())
135    }
136}
137
138impl Stage for ACurves3x4InverseOptimized<'_> {
139    fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> {
140        let lut = Cube::new_cube(self.clut, self.grid_size);
141
142        // If PCS is LAB then linear interpolation should be used
143        if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz {
144            return self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z));
145        }
146
147        match self.interpolation_method {
148            #[cfg(feature = "options")]
149            InterpolationMethod::Tetrahedral => {
150                self.transform_impl(src, dst, |x, y, z| lut.tetra_vec4(x, y, z))?;
151            }
152            #[cfg(feature = "options")]
153            InterpolationMethod::Pyramid => {
154                self.transform_impl(src, dst, |x, y, z| lut.pyramid_vec4(x, y, z))?;
155            }
156            #[cfg(feature = "options")]
157            InterpolationMethod::Prism => {
158                self.transform_impl(src, dst, |x, y, z| lut.prism_vec4(x, y, z))?;
159            }
160            InterpolationMethod::Linear => {
161                self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z))?;
162            }
163        }
164        Ok(())
165    }
166}
167
168pub(crate) fn prepare_mba_3x4(
169    mab: &LutMultidimensionalType,
170    lut: &mut [f32],
171    options: TransformOptions,
172    pcs: DataColorSpace,
173) -> Result<Vec<f32>, CmsError> {
174    if mab.num_input_channels != 3 && mab.num_output_channels != 4 {
175        return Err(CmsError::UnsupportedProfileConnection);
176    }
177
178    const LERP_DEPTH: usize = 65536;
179    const BP: usize = 13;
180    const DEPTH: usize = 8192;
181
182    if mab.b_curves.len() == 3 {
183        let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear());
184
185        if !all_curves_linear {
186            let curves: Result<Vec<_>, _> = mab
187                .b_curves
188                .iter()
189                .map(|c| {
190                    c.build_linearize_table::<u16, LERP_DEPTH, BP>()
191                        .ok_or(CmsError::InvalidTrcCurve)
192                })
193                .collect();
194
195            let [curve0, curve1, curve2] =
196                curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?;
197            let b_curves = BCurves3::<DEPTH> {
198                curve0,
199                curve1,
200                curve2,
201            };
202            b_curves.transform(lut)?;
203        }
204    } else {
205        return Err(CmsError::InvalidAtoBLut);
206    }
207
208    if mab.m_curves.len() == 3 {
209        let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear());
210        if !all_curves_linear
211            || !mab.matrix.test_equality(Matrix3d::IDENTITY)
212            || mab.bias.ne(&Vector3d::default())
213        {
214            let curves: Result<Vec<_>, _> = mab
215                .m_curves
216                .iter()
217                .map(|c| {
218                    c.build_linearize_table::<u16, LERP_DEPTH, BP>()
219                        .ok_or(CmsError::InvalidTrcCurve)
220                })
221                .collect();
222
223            let [curve0, curve1, curve2] =
224                curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?;
225
226            let matrix = mab.matrix.to_f32();
227            let bias = mab.bias.cast();
228            let m_curves = MCurves3::<DEPTH> {
229                curve0,
230                curve1,
231                curve2,
232                matrix,
233                bias,
234                inverse: true,
235            };
236            m_curves.transform(lut)?;
237        }
238    }
239
240    let mut new_lut = vec![0f32; (lut.len() / 3) * 4];
241
242    if mab.a_curves.len() == 4 && mab.clut.is_some() {
243        let clut = &mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap();
244
245        let lut_grid = (mab.grid_points[0] as usize)
246            .safe_mul(mab.grid_points[1] as usize)?
247            .safe_mul(mab.grid_points[2] as usize)?
248            .safe_mul(mab.num_output_channels as usize)?;
249        if clut.len() != lut_grid {
250            return Err(CmsError::MalformedClut(MalformedSize {
251                size: clut.len(),
252                expected: lut_grid,
253            }));
254        }
255
256        let grid_size = [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]];
257
258        let all_curves_linear = mab.a_curves.iter().all(|curve| curve.is_linear());
259
260        if all_curves_linear {
261            let a_curves = ACurves3x4InverseOptimized {
262                clut,
263                grid_size: [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]],
264                interpolation_method: options.interpolation_method,
265                pcs,
266            };
267            a_curves.transform(lut, &mut new_lut)?;
268        } else {
269            let curves: Result<Vec<_>, _> = mab
270                .a_curves
271                .iter()
272                .map(|c| {
273                    c.build_linearize_table::<u16, LERP_DEPTH, BP>()
274                        .ok_or(CmsError::InvalidTrcCurve)
275                })
276                .collect();
277
278            let [curve0, curve1, curve2, curve3] =
279                curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?;
280
281            let a_curves = ACurves3x4Inverse::<DEPTH> {
282                curve0,
283                curve1,
284                curve2,
285                curve3,
286                clut,
287                grid_size,
288                interpolation_method: options.interpolation_method,
289                pcs,
290            };
291            a_curves.transform(lut, &mut new_lut)?;
292        }
293    } else {
294        return Err(CmsError::UnsupportedProfileConnection);
295    }
296
297    Ok(new_lut)
298}