Skip to main content

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