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