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