moxcms/conversions/
lut_transforms.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 2/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::lut3x3::{
30    create_lut3x3, katana_input_stage_lut_3x3, katana_output_stage_lut_3x3,
31};
32use crate::conversions::lut3x4::{create_lut3_samples_norm, create_lut3x4};
33use crate::conversions::lut4::{create_lut4, create_lut4_norm_samples, katana_input_stage_lut_4x3};
34use crate::conversions::mab::{prepare_mab_3x3, prepare_mba_3x3};
35use crate::conversions::transform_lut3_to_4::make_transform_3x4;
36use crate::mlaf::mlaf;
37use crate::{
38    CmsError, ColorProfile, DataColorSpace, InPlaceStage, Layout, LutWarehouse, Matrix3f,
39    ProfileVersion, TransformExecutor, TransformOptions,
40};
41use num_traits::AsPrimitive;
42
43pub(crate) struct MatrixStage {
44    pub(crate) matrices: Vec<Matrix3f>,
45}
46
47impl InPlaceStage for MatrixStage {
48    fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> {
49        if !self.matrices.is_empty() {
50            let m = self.matrices[0];
51            for dst in dst.chunks_exact_mut(3) {
52                let x = dst[0];
53                let y = dst[1];
54                let z = dst[2];
55                dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]);
56                dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]);
57                dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]);
58            }
59        }
60
61        for m in self.matrices.iter().skip(1) {
62            for dst in dst.chunks_exact_mut(3) {
63                let x = dst[0];
64                let y = dst[1];
65                let z = dst[2];
66                dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]);
67                dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]);
68                dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]);
69            }
70        }
71
72        Ok(())
73    }
74}
75
76pub(crate) const LUT_SAMPLING: u16 = 255;
77
78pub(crate) trait Lut3x3Factory {
79    fn make_transform_3x3<
80        T: Copy + AsPrimitive<f32> + Default + PointeeSizeExpressible + 'static + Send + Sync,
81        const SRC_LAYOUT: u8,
82        const DST_LAYOUT: u8,
83        const GRID_SIZE: usize,
84        const BIT_DEPTH: usize,
85    >(
86        lut: Vec<f32>,
87        options: TransformOptions,
88        color_space: DataColorSpace,
89        is_linear: bool,
90    ) -> Box<dyn TransformExecutor<T> + Send + Sync>
91    where
92        f32: AsPrimitive<T>,
93        u32: AsPrimitive<T>,
94        (): LutBarycentricReduction<T, u8>,
95        (): LutBarycentricReduction<T, u16>;
96}
97
98pub(crate) trait Lut4x3Factory {
99    fn make_transform_4x3<
100        T: Copy + AsPrimitive<f32> + Default + PointeeSizeExpressible + 'static + Send + Sync,
101        const LAYOUT: u8,
102        const GRID_SIZE: usize,
103        const BIT_DEPTH: usize,
104    >(
105        lut: Vec<f32>,
106        options: TransformOptions,
107        color_space: DataColorSpace,
108        is_linear: bool,
109    ) -> Box<dyn TransformExecutor<T> + Sync + Send>
110    where
111        f32: AsPrimitive<T>,
112        u32: AsPrimitive<T>,
113        (): LutBarycentricReduction<T, u8>,
114        (): LutBarycentricReduction<T, u16>;
115}
116
117fn pcs_lab_v4_to_v2(profile: &ColorProfile, lut: &mut [f32]) {
118    if profile.pcs == DataColorSpace::Lab
119        && profile.version_internal <= ProfileVersion::V4_0
120        && lut.len() % 3 == 0
121    {
122        assert_eq!(
123            lut.len() % 3,
124            0,
125            "Lut {:?} is not a multiple of 3, this should not happen for lab",
126            lut.len()
127        );
128        let v_mat = vec![Matrix3f {
129            v: [
130                [65280.0 / 65535.0, 0f32, 0f32],
131                [0f32, 65280.0 / 65535.0, 0f32],
132                [0f32, 0f32, 65280.0 / 65535.0f32],
133            ],
134        }];
135        let stage = MatrixStage { matrices: v_mat };
136        stage.transform(lut).unwrap();
137    }
138}
139
140fn pcs_lab_v2_to_v4(profile: &ColorProfile, lut: &mut [f32]) {
141    if profile.pcs == DataColorSpace::Lab
142        && profile.version_internal <= ProfileVersion::V4_0
143        && lut.len() % 3 == 0
144    {
145        assert_eq!(
146            lut.len() % 3,
147            0,
148            "Lut {:?} is not a multiple of 3, this should not happen for lab",
149            lut.len()
150        );
151        let v_mat = vec![Matrix3f {
152            v: [
153                [65535.0 / 65280.0f32, 0f32, 0f32],
154                [0f32, 65535.0f32 / 65280.0f32, 0f32],
155                [0f32, 0f32, 65535.0f32 / 65280.0f32],
156            ],
157        }];
158        let stage = MatrixStage { matrices: v_mat };
159        stage.transform(lut).unwrap();
160    }
161}
162
163macro_rules! make_transform_3x3_fn {
164    ($method_name: ident, $exec_impl: ident) => {
165        fn $method_name<
166            T: Copy
167                + Default
168                + AsPrimitive<f32>
169                + Send
170                + Sync
171                + AsPrimitive<usize>
172                + PointeeSizeExpressible,
173            const GRID_SIZE: usize,
174            const BIT_DEPTH: usize,
175        >(
176            src_layout: Layout,
177            dst_layout: Layout,
178            lut: Vec<f32>,
179            options: TransformOptions,
180            color_space: DataColorSpace,
181            is_linear: bool,
182        ) -> Box<dyn TransformExecutor<T> + Send + Sync>
183        where
184            f32: AsPrimitive<T>,
185            u32: AsPrimitive<T>,
186            (): LutBarycentricReduction<T, u8>,
187            (): LutBarycentricReduction<T, u16>,
188        {
189            match src_layout {
190                Layout::Rgb => match dst_layout {
191                    Layout::Rgb => $exec_impl::make_transform_3x3::<
192                        T,
193                        { Layout::Rgb as u8 },
194                        { Layout::Rgb as u8 },
195                        GRID_SIZE,
196                        BIT_DEPTH,
197                    >(lut, options, color_space, is_linear),
198                    Layout::Rgba => $exec_impl::make_transform_3x3::<
199                        T,
200                        { Layout::Rgb as u8 },
201                        { Layout::Rgba as u8 },
202                        GRID_SIZE,
203                        BIT_DEPTH,
204                    >(lut, options, color_space, is_linear),
205                    _ => unimplemented!(),
206                },
207                Layout::Rgba => match dst_layout {
208                    Layout::Rgb => $exec_impl::make_transform_3x3::<
209                        T,
210                        { Layout::Rgba as u8 },
211                        { Layout::Rgb as u8 },
212                        GRID_SIZE,
213                        BIT_DEPTH,
214                    >(lut, options, color_space, is_linear),
215                    Layout::Rgba => $exec_impl::make_transform_3x3::<
216                        T,
217                        { Layout::Rgba as u8 },
218                        { Layout::Rgba as u8 },
219                        GRID_SIZE,
220                        BIT_DEPTH,
221                    >(lut, options, color_space, is_linear),
222                    _ => unimplemented!(),
223                },
224                _ => unimplemented!(),
225            }
226        }
227    };
228}
229
230macro_rules! make_transform_4x3_fn {
231    ($method_name: ident, $exec_name: ident) => {
232        fn $method_name<
233            T: Copy
234                + Default
235                + AsPrimitive<f32>
236                + Send
237                + Sync
238                + AsPrimitive<usize>
239                + PointeeSizeExpressible,
240            const GRID_SIZE: usize,
241            const BIT_DEPTH: usize,
242        >(
243            dst_layout: Layout,
244            lut: Vec<f32>,
245            options: TransformOptions,
246            data_color_space: DataColorSpace,
247            is_linear: bool,
248        ) -> Box<dyn TransformExecutor<T> + Send + Sync>
249        where
250            f32: AsPrimitive<T>,
251            u32: AsPrimitive<T>,
252            (): LutBarycentricReduction<T, u8>,
253            (): LutBarycentricReduction<T, u16>,
254        {
255            match dst_layout {
256                Layout::Rgb => $exec_name::make_transform_4x3::<
257                    T,
258                    { Layout::Rgb as u8 },
259                    GRID_SIZE,
260                    BIT_DEPTH,
261                >(lut, options, data_color_space, is_linear),
262                Layout::Rgba => $exec_name::make_transform_4x3::<
263                    T,
264                    { Layout::Rgba as u8 },
265                    GRID_SIZE,
266                    BIT_DEPTH,
267                >(lut, options, data_color_space, is_linear),
268                _ => unimplemented!(),
269            }
270        }
271    };
272}
273
274#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))]
275use crate::conversions::neon::NeonLut3x3Factory;
276#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))]
277make_transform_3x3_fn!(make_transformer_3x3, NeonLut3x3Factory);
278
279#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))]
280use crate::conversions::transform_lut3_to_3::DefaultLut3x3Factory;
281#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))]
282make_transform_3x3_fn!(make_transformer_3x3, DefaultLut3x3Factory);
283
284#[cfg(all(target_arch = "x86_64", feature = "avx"))]
285use crate::conversions::avx::AvxLut3x3Factory;
286#[cfg(all(target_arch = "x86_64", feature = "avx"))]
287make_transform_3x3_fn!(make_transformer_3x3_avx_fma, AvxLut3x3Factory);
288
289#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))]
290use crate::conversions::sse::SseLut3x3Factory;
291#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))]
292make_transform_3x3_fn!(make_transformer_3x3_sse41, SseLut3x3Factory);
293
294#[cfg(all(target_arch = "x86_64", feature = "avx"))]
295use crate::conversions::avx::AvxLut4x3Factory;
296use crate::conversions::interpolator::LutBarycentricReduction;
297use crate::conversions::katana::{
298    Katana, KatanaDefaultIntermediate, KatanaInitialStage, KatanaPostFinalizationStage,
299    KatanaStageLabToXyz, KatanaStageXyzToLab, katana_create_rgb_lin_lut, katana_pcs_lab_v2_to_v4,
300    katana_pcs_lab_v4_to_v2, katana_prepare_inverse_lut_rgb_xyz, multi_dimensional_3x3_to_device,
301    multi_dimensional_3x3_to_pcs, multi_dimensional_4x3_to_pcs,
302};
303use crate::conversions::mab4x3::prepare_mab_4x3;
304use crate::conversions::mba3x4::prepare_mba_3x4;
305use crate::conversions::md_luts_factory::{do_any_to_any, prepare_alpha_finalizer};
306// use crate::conversions::bpc::compensate_bpc_in_lut;
307
308#[cfg(all(target_arch = "x86_64", feature = "avx"))]
309make_transform_4x3_fn!(make_transformer_4x3_avx_fma, AvxLut4x3Factory);
310
311#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))]
312use crate::conversions::sse::SseLut4x3Factory;
313#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))]
314make_transform_4x3_fn!(make_transformer_4x3_sse41, SseLut4x3Factory);
315
316#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))]
317use crate::conversions::transform_lut4_to_3::DefaultLut4x3Factory;
318
319#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))]
320make_transform_4x3_fn!(make_transformer_4x3, DefaultLut4x3Factory);
321
322#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))]
323use crate::conversions::neon::NeonLut4x3Factory;
324use crate::conversions::prelude_lut_xyz_rgb::{create_rgb_lin_lut, prepare_inverse_lut_rgb_xyz};
325use crate::conversions::xyz_lab::{StageLabToXyz, StageXyzToLab};
326use crate::transform::PointeeSizeExpressible;
327use crate::trc::GammaLutInterpolate;
328
329#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))]
330make_transform_4x3_fn!(make_transformer_4x3, NeonLut4x3Factory);
331
332#[inline(never)]
333#[cold]
334pub(crate) fn make_lut_transform<
335    T: Copy
336        + Default
337        + AsPrimitive<f32>
338        + Send
339        + Sync
340        + AsPrimitive<usize>
341        + PointeeSizeExpressible
342        + GammaLutInterpolate,
343    const BIT_DEPTH: usize,
344    const LINEAR_CAP: usize,
345    const GAMMA_LUT: usize,
346>(
347    src_layout: Layout,
348    source: &ColorProfile,
349    dst_layout: Layout,
350    dest: &ColorProfile,
351    options: TransformOptions,
352) -> Result<Box<dyn TransformExecutor<T> + Send + Sync>, CmsError>
353where
354    f32: AsPrimitive<T>,
355    u32: AsPrimitive<T>,
356    (): LutBarycentricReduction<T, u8>,
357    (): LutBarycentricReduction<T, u16>,
358{
359    if (source.color_space == DataColorSpace::Cmyk || source.color_space == DataColorSpace::Color4)
360        && (dest.color_space == DataColorSpace::Rgb || dest.color_space == DataColorSpace::Lab)
361    {
362        source.color_space.check_layout(src_layout)?;
363        dest.color_space.check_layout(dst_layout)?;
364        if source.pcs != DataColorSpace::Xyz && source.pcs != DataColorSpace::Lab {
365            return Err(CmsError::UnsupportedProfileConnection);
366        }
367        if dest.pcs != DataColorSpace::Lab && dest.pcs != DataColorSpace::Xyz {
368            return Err(CmsError::UnsupportedProfileConnection);
369        }
370
371        const GRID_SIZE: usize = 17;
372
373        let is_katana_required_for_source = source
374            .get_device_to_pcs(options.rendering_intent)
375            .ok_or(CmsError::UnsupportedLutRenderingIntent(
376                source.rendering_intent,
377            ))
378            .map(|x| x.is_katana_required())?;
379
380        let is_katana_required_for_destination =
381            if dest.is_matrix_shaper() || dest.pcs == DataColorSpace::Xyz {
382                false
383            } else if dest.pcs == DataColorSpace::Lab {
384                dest.get_pcs_to_device(options.rendering_intent)
385                    .ok_or(CmsError::UnsupportedProfileConnection)
386                    .map(|x| x.is_katana_required())?
387            } else {
388                return Err(CmsError::UnsupportedProfileConnection);
389            };
390
391        if is_katana_required_for_source || is_katana_required_for_destination {
392            let initial_stage: Box<dyn KatanaInitialStage<f32, T> + Send + Sync> =
393                match source.get_device_to_pcs(options.rendering_intent).ok_or(
394                    CmsError::UnsupportedLutRenderingIntent(source.rendering_intent),
395                )? {
396                    LutWarehouse::Lut(lut) => {
397                        katana_input_stage_lut_4x3::<T>(lut, options, source.pcs, BIT_DEPTH)?
398                    }
399                    LutWarehouse::Multidimensional(mab) => {
400                        multi_dimensional_4x3_to_pcs::<T>(mab, options, source.pcs, BIT_DEPTH)?
401                    }
402                };
403
404            let mut stages = Vec::new();
405
406            stages.push(katana_pcs_lab_v2_to_v4(source));
407            if source.pcs == DataColorSpace::Lab {
408                stages.push(Box::new(KatanaStageLabToXyz::default()));
409            }
410            if dest.pcs == DataColorSpace::Lab {
411                stages.push(Box::new(KatanaStageXyzToLab::default()));
412            }
413            stages.push(katana_pcs_lab_v4_to_v2(dest));
414
415            let final_stage = if dest.has_pcs_to_device_lut() {
416                let pcs_to_device = dest
417                    .get_pcs_to_device(options.rendering_intent)
418                    .ok_or(CmsError::UnsupportedProfileConnection)?;
419                match pcs_to_device {
420                    LutWarehouse::Lut(lut) => {
421                        katana_output_stage_lut_3x3::<T>(lut, options, dest.pcs, BIT_DEPTH)?
422                    }
423                    LutWarehouse::Multidimensional(mab) => {
424                        multi_dimensional_3x3_to_device::<T>(mab, options, dest.pcs, BIT_DEPTH)?
425                    }
426                }
427            } else if dest.is_matrix_shaper() {
428                let state = katana_prepare_inverse_lut_rgb_xyz::<T, BIT_DEPTH, GAMMA_LUT>(
429                    dest, dst_layout, options,
430                )?;
431                stages.extend(state.stages);
432                state.final_stage
433            } else {
434                return Err(CmsError::UnsupportedProfileConnection);
435            };
436
437            let mut post_finalization: Vec<Box<dyn KatanaPostFinalizationStage<T> + Send + Sync>> =
438                Vec::new();
439            if let Some(stage) =
440                prepare_alpha_finalizer::<T>(src_layout, source, dst_layout, dest, BIT_DEPTH)
441            {
442                post_finalization.push(stage);
443            }
444
445            return Ok(Box::new(Katana::<f32, T> {
446                initial_stage,
447                final_stage,
448                stages,
449                post_finalization,
450            }));
451        }
452
453        let mut lut = match source.get_device_to_pcs(options.rendering_intent).ok_or(
454            CmsError::UnsupportedLutRenderingIntent(source.rendering_intent),
455        )? {
456            LutWarehouse::Lut(lut) => create_lut4::<GRID_SIZE>(lut, options, source.pcs)?,
457            LutWarehouse::Multidimensional(m_curves) => {
458                let mut samples = create_lut4_norm_samples::<GRID_SIZE>();
459                prepare_mab_4x3(m_curves, &mut samples, options, source.pcs)?
460            }
461        };
462
463        pcs_lab_v2_to_v4(source, &mut lut);
464
465        if source.pcs == DataColorSpace::Lab {
466            let lab_to_xyz_stage = StageLabToXyz::default();
467            lab_to_xyz_stage.transform(&mut lut)?;
468        }
469
470        // if source.color_space == DataColorSpace::Cmyk
471        //     && (options.rendering_intent == RenderingIntent::Perceptual
472        //         || options.rendering_intent == RenderingIntent::RelativeColorimetric)
473        //     && options.black_point_compensation
474        // {
475        //     if let (Some(src_bp), Some(dst_bp)) = (
476        //         source.detect_black_point::<GRID_SIZE>(&lut),
477        //         dest.detect_black_point::<GRID_SIZE>(&lut),
478        //     ) {
479        //         compensate_bpc_in_lut(&mut lut, src_bp, dst_bp);
480        //     }
481        // }
482
483        if dest.pcs == DataColorSpace::Lab {
484            let lab_to_xyz_stage = StageXyzToLab::default();
485            lab_to_xyz_stage.transform(&mut lut)?;
486        }
487
488        pcs_lab_v4_to_v2(dest, &mut lut);
489
490        if dest.pcs == DataColorSpace::Xyz {
491            if dest.is_matrix_shaper() {
492                prepare_inverse_lut_rgb_xyz::<T, BIT_DEPTH, GAMMA_LUT>(dest, &mut lut, options)?;
493            } else {
494                return Err(CmsError::UnsupportedProfileConnection);
495            }
496        } else if dest.pcs == DataColorSpace::Lab {
497            let pcs_to_device = dest
498                .get_pcs_to_device(options.rendering_intent)
499                .ok_or(CmsError::UnsupportedProfileConnection)?;
500            match pcs_to_device {
501                LutWarehouse::Lut(lut_data_type) => {
502                    lut = create_lut3x3(lut_data_type, &lut, options, dest.pcs)?
503                }
504                LutWarehouse::Multidimensional(mab) => {
505                    prepare_mba_3x3(mab, &mut lut, options, dest.pcs)?
506                }
507            }
508        }
509
510        let is_dest_linear_profile = dest.color_space == DataColorSpace::Rgb
511            && dest.is_matrix_shaper()
512            && dest.is_linear_matrix_shaper();
513
514        #[cfg(all(target_arch = "x86_64", feature = "avx"))]
515        if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma")
516        {
517            return Ok(make_transformer_4x3_avx_fma::<T, GRID_SIZE, BIT_DEPTH>(
518                dst_layout,
519                lut,
520                options,
521                dest.color_space,
522                is_dest_linear_profile,
523            ));
524        }
525        #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))]
526        if std::arch::is_x86_feature_detected!("sse4.1") {
527            return Ok(make_transformer_4x3_sse41::<T, GRID_SIZE, BIT_DEPTH>(
528                dst_layout,
529                lut,
530                options,
531                dest.color_space,
532                is_dest_linear_profile,
533            ));
534        }
535
536        Ok(make_transformer_4x3::<T, GRID_SIZE, BIT_DEPTH>(
537            dst_layout,
538            lut,
539            options,
540            dest.color_space,
541            is_dest_linear_profile,
542        ))
543    } else if (source.color_space == DataColorSpace::Rgb
544        || source.color_space == DataColorSpace::Lab)
545        && (dest.color_space == DataColorSpace::Cmyk || dest.color_space == DataColorSpace::Color4)
546    {
547        source.color_space.check_layout(src_layout)?;
548        dest.color_space.check_layout(dst_layout)?;
549
550        if source.pcs != DataColorSpace::Xyz && source.pcs != DataColorSpace::Lab {
551            return Err(CmsError::UnsupportedProfileConnection);
552        }
553
554        const GRID_SIZE: usize = 33;
555
556        let mut lut: Vec<f32>;
557
558        if source.has_device_to_pcs_lut() {
559            let device_to_pcs = source
560                .get_device_to_pcs(options.rendering_intent)
561                .ok_or(CmsError::UnsupportedProfileConnection)?;
562            lut = create_lut3_samples_norm::<GRID_SIZE>();
563
564            match device_to_pcs {
565                LutWarehouse::Lut(lut_data_type) => {
566                    lut = create_lut3x3(lut_data_type, &lut, options, source.pcs)?;
567                }
568                LutWarehouse::Multidimensional(mab) => {
569                    prepare_mab_3x3(mab, &mut lut, options, source.pcs)?
570                }
571            }
572        } else if source.is_matrix_shaper() {
573            lut = create_rgb_lin_lut::<T, BIT_DEPTH, LINEAR_CAP, GRID_SIZE>(source, options)?;
574        } else {
575            return Err(CmsError::UnsupportedProfileConnection);
576        }
577
578        pcs_lab_v2_to_v4(source, &mut lut);
579
580        if source.pcs == DataColorSpace::Xyz && dest.pcs == DataColorSpace::Lab {
581            let xyz_to_lab = StageXyzToLab::default();
582            xyz_to_lab.transform(&mut lut)?;
583        } else if source.pcs == DataColorSpace::Lab && dest.pcs == DataColorSpace::Xyz {
584            let lab_to_xyz_stage = StageLabToXyz::default();
585            lab_to_xyz_stage.transform(&mut lut)?;
586        }
587
588        pcs_lab_v4_to_v2(dest, &mut lut);
589
590        let lut = match dest
591            .get_pcs_to_device(options.rendering_intent)
592            .ok_or(CmsError::UnsupportedProfileConnection)?
593        {
594            LutWarehouse::Lut(lut_type) => create_lut3x4(lut_type, &lut, options, dest.pcs)?,
595            LutWarehouse::Multidimensional(m_curves) => {
596                prepare_mba_3x4(m_curves, &mut lut, options, dest.pcs)?
597            }
598        };
599
600        let is_dest_linear_profile = dest.color_space == DataColorSpace::Rgb
601            && dest.is_matrix_shaper()
602            && dest.is_linear_matrix_shaper();
603
604        Ok(make_transform_3x4::<T, GRID_SIZE, BIT_DEPTH>(
605            src_layout,
606            lut,
607            options,
608            dest.color_space,
609            is_dest_linear_profile,
610        ))
611    } else if (source.color_space.is_three_channels()) && (dest.color_space.is_three_channels()) {
612        source.color_space.check_layout(src_layout)?;
613        dest.color_space.check_layout(dst_layout)?;
614
615        const GRID_SIZE: usize = 33;
616
617        let is_katana_required_for_source = if source.is_matrix_shaper() {
618            false
619        } else {
620            source
621                .get_device_to_pcs(options.rendering_intent)
622                .ok_or(CmsError::UnsupportedLutRenderingIntent(
623                    source.rendering_intent,
624                ))
625                .map(|x| x.is_katana_required())?
626        };
627
628        let is_katana_required_for_destination =
629            if source.is_matrix_shaper() || dest.pcs == DataColorSpace::Xyz {
630                false
631            } else if dest.pcs == DataColorSpace::Lab {
632                dest.get_pcs_to_device(options.rendering_intent)
633                    .ok_or(CmsError::UnsupportedProfileConnection)
634                    .map(|x| x.is_katana_required())?
635            } else {
636                return Err(CmsError::UnsupportedProfileConnection);
637            };
638
639        let mut stages: Vec<Box<KatanaDefaultIntermediate>> = Vec::new();
640
641        // Slow and accurate fallback if anything not acceptable is detected by curve analysis
642        if is_katana_required_for_source || is_katana_required_for_destination {
643            let source_stage: Box<dyn KatanaInitialStage<f32, T> + Send + Sync> =
644                if source.is_matrix_shaper() {
645                    let state = katana_create_rgb_lin_lut::<T, BIT_DEPTH, LINEAR_CAP>(
646                        src_layout, source, options,
647                    )?;
648                    stages.extend(state.stages);
649                    state.initial_stage
650                } else {
651                    match source.get_device_to_pcs(options.rendering_intent).ok_or(
652                        CmsError::UnsupportedLutRenderingIntent(source.rendering_intent),
653                    )? {
654                        LutWarehouse::Lut(lut) => {
655                            katana_input_stage_lut_3x3::<T>(lut, options, source.pcs, BIT_DEPTH)?
656                        }
657                        LutWarehouse::Multidimensional(mab) => {
658                            multi_dimensional_3x3_to_pcs::<T>(mab, options, source.pcs, BIT_DEPTH)?
659                        }
660                    }
661                };
662
663            stages.push(katana_pcs_lab_v2_to_v4(source));
664            if source.pcs == DataColorSpace::Lab {
665                stages.push(Box::new(KatanaStageLabToXyz::default()));
666            }
667            if dest.pcs == DataColorSpace::Lab {
668                stages.push(Box::new(KatanaStageXyzToLab::default()));
669            }
670            stages.push(katana_pcs_lab_v4_to_v2(dest));
671
672            let final_stage = if dest.has_pcs_to_device_lut() {
673                let pcs_to_device = dest
674                    .get_pcs_to_device(options.rendering_intent)
675                    .ok_or(CmsError::UnsupportedProfileConnection)?;
676                match pcs_to_device {
677                    LutWarehouse::Lut(lut) => {
678                        katana_output_stage_lut_3x3::<T>(lut, options, dest.pcs, BIT_DEPTH)?
679                    }
680                    LutWarehouse::Multidimensional(mab) => {
681                        multi_dimensional_3x3_to_device::<T>(mab, options, dest.pcs, BIT_DEPTH)?
682                    }
683                }
684            } else if dest.is_matrix_shaper() {
685                let state = katana_prepare_inverse_lut_rgb_xyz::<T, BIT_DEPTH, GAMMA_LUT>(
686                    dest, dst_layout, options,
687                )?;
688                stages.extend(state.stages);
689                state.final_stage
690            } else {
691                return Err(CmsError::UnsupportedProfileConnection);
692            };
693
694            let mut post_finalization: Vec<Box<dyn KatanaPostFinalizationStage<T> + Send + Sync>> =
695                Vec::new();
696            if let Some(stage) =
697                prepare_alpha_finalizer::<T>(src_layout, source, dst_layout, dest, BIT_DEPTH)
698            {
699                post_finalization.push(stage);
700            }
701
702            return Ok(Box::new(Katana::<f32, T> {
703                initial_stage: source_stage,
704                final_stage,
705                stages,
706                post_finalization,
707            }));
708        }
709
710        let mut lut: Vec<f32>;
711
712        if source.has_device_to_pcs_lut() {
713            let device_to_pcs = source
714                .get_device_to_pcs(options.rendering_intent)
715                .ok_or(CmsError::UnsupportedProfileConnection)?;
716            lut = create_lut3_samples_norm::<GRID_SIZE>();
717
718            match device_to_pcs {
719                LutWarehouse::Lut(lut_data_type) => {
720                    lut = create_lut3x3(lut_data_type, &lut, options, source.pcs)?;
721                }
722                LutWarehouse::Multidimensional(mab) => {
723                    prepare_mab_3x3(mab, &mut lut, options, source.pcs)?
724                }
725            }
726        } else if source.is_matrix_shaper() {
727            lut = create_rgb_lin_lut::<T, BIT_DEPTH, LINEAR_CAP, GRID_SIZE>(source, options)?;
728        } else {
729            return Err(CmsError::UnsupportedProfileConnection);
730        }
731
732        pcs_lab_v2_to_v4(source, &mut lut);
733
734        if source.pcs == DataColorSpace::Xyz && dest.pcs == DataColorSpace::Lab {
735            let xyz_to_lab = StageXyzToLab::default();
736            xyz_to_lab.transform(&mut lut)?;
737        } else if source.pcs == DataColorSpace::Lab && dest.pcs == DataColorSpace::Xyz {
738            let lab_to_xyz_stage = StageLabToXyz::default();
739            lab_to_xyz_stage.transform(&mut lut)?;
740        }
741
742        pcs_lab_v4_to_v2(dest, &mut lut);
743
744        if dest.has_pcs_to_device_lut() {
745            let pcs_to_device = dest
746                .get_pcs_to_device(options.rendering_intent)
747                .ok_or(CmsError::UnsupportedProfileConnection)?;
748            match pcs_to_device {
749                LutWarehouse::Lut(lut_data_type) => {
750                    lut = create_lut3x3(lut_data_type, &lut, options, dest.pcs)?;
751                }
752                LutWarehouse::Multidimensional(mab) => {
753                    prepare_mba_3x3(mab, &mut lut, options, dest.pcs)?
754                }
755            }
756        } else if dest.is_matrix_shaper() {
757            prepare_inverse_lut_rgb_xyz::<T, BIT_DEPTH, GAMMA_LUT>(dest, &mut lut, options)?;
758        } else {
759            return Err(CmsError::UnsupportedProfileConnection);
760        }
761
762        let is_dest_linear_profile = dest.color_space == DataColorSpace::Rgb
763            && dest.is_matrix_shaper()
764            && dest.is_linear_matrix_shaper();
765
766        #[cfg(all(feature = "avx", target_arch = "x86_64"))]
767        if std::arch::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
768            return Ok(make_transformer_3x3_avx_fma::<T, GRID_SIZE, BIT_DEPTH>(
769                src_layout,
770                dst_layout,
771                lut,
772                options,
773                dest.color_space,
774                is_dest_linear_profile,
775            ));
776        }
777        #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))]
778        if std::arch::is_x86_feature_detected!("sse4.1") {
779            return Ok(make_transformer_3x3_sse41::<T, GRID_SIZE, BIT_DEPTH>(
780                src_layout,
781                dst_layout,
782                lut,
783                options,
784                dest.color_space,
785                is_dest_linear_profile,
786            ));
787        }
788
789        Ok(make_transformer_3x3::<T, GRID_SIZE, BIT_DEPTH>(
790            src_layout,
791            dst_layout,
792            lut,
793            options,
794            dest.color_space,
795            is_dest_linear_profile,
796        ))
797    } else {
798        do_any_to_any::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_LUT>(
799            src_layout, source, dst_layout, dest, options,
800        )
801    }
802}