Skip to main content

moxcms/
transform.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::*;
30use crate::err::CmsError;
31use crate::interceptors::{FromCmykaInterceptor, ToCmykaInterceptor};
32use crate::trc::GammaLutInterpolate;
33use crate::{ColorProfile, DataColorSpace, LutWarehouse, RenderingIntent, Vector3f, Xyzd};
34use num_traits::AsPrimitive;
35use std::sync::Arc;
36
37/// Transformation executor itself
38pub trait TransformExecutor<V: Copy + Default> {
39    /// Count of samples always must match.
40    /// If there is N samples of *Cmyk* source then N samples of *Rgb* is expected as an output.
41    fn transform(&self, src: &[V], dst: &mut [V]) -> Result<(), CmsError>;
42}
43
44/// Helper for intermediate transformation stages
45pub trait Stage {
46    fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError>;
47}
48
49/// Helper for intermediate transformation stages
50pub trait InPlaceStage {
51    fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError>;
52}
53
54pub trait InPlaceTransformExecutor<V: Copy + Default> {
55    fn transform(&self, in_out: &mut [V]) -> Result<(), CmsError>;
56}
57
58/// Barycentric interpolation weights size.
59///
60/// Bigger weights increases precision.
61#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
62pub enum BarycentricWeightScale {
63    #[default]
64    /// Low scale weights is enough for common case.
65    ///
66    /// However, it might crush dark zones and gradients.
67    /// Weights increasing costs 5% performance.
68    Low,
69    #[cfg(feature = "options")]
70    High,
71}
72
73/// Declares additional transformation options
74#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
75pub struct TransformOptions {
76    pub rendering_intent: RenderingIntent,
77    /// If set it will try to use Transfer Characteristics from CICP
78    /// on transform. This might be more precise and faster.
79    pub allow_use_cicp_transfer: bool,
80    /// Prefers fixed point where implemented as default.
81    /// Most of the applications actually do not need floating point.
82    ///
83    /// Do not change it if you're not sure that extreme precision is required,
84    /// in most cases it is a simple way to spend energy to warming up environment
85    /// a little.
86    ///
87    /// Q2.13 for RGB->XYZ->RGB is used.
88    /// LUT interpolation use Q0.15.
89    pub prefer_fixed_point: bool,
90    /// Interpolation method for 3D LUT
91    ///
92    /// This parameter has no effect on LAB/XYZ interpolation and scene linear RGB.
93    ///
94    /// Technically, it should be assumed to perform cube dividing interpolation:
95    /// - Source colorspace is gamma-encoded (discards scene linear RGB and XYZ).
96    /// - Colorspace is uniform.
97    /// - Colorspace has linear scaling (discards LAB).
98    /// - Interpolation doesn't shift hues (discards LAB).
99    ///
100    /// For LAB, XYZ and scene linear RGB `trilinear/quadlinear` always in force.
101    pub interpolation_method: InterpolationMethod,
102    /// Barycentric weights scale.
103    ///
104    /// This value controls LUT weights precision.
105    pub barycentric_weight_scale: BarycentricWeightScale,
106    /// For floating points transform, it will try to detect gamma function on *Matrix Shaper* profiles.
107    /// If gamma function is found, then it will be used instead of LUT table.
108    /// This allows to work with excellent precision with extended range,
109    /// at a cost of execution time.
110    #[cfg(feature = "extended_range")]
111    pub allow_extended_range_rgb_xyz: bool,
112    // pub black_point_compensation: bool,
113}
114
115#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
116/// Defines the interpolation method.
117///
118/// All methods produce very close results that almost not possible to separate without
119/// some automation tools.
120///
121/// This implementation chooses the fastest method as default.
122pub enum InterpolationMethod {
123    /// General Tetrahedron interpolation.
124    /// This is used in lcms2 and others CMS.
125    #[cfg(feature = "options")]
126    Tetrahedral,
127    /// Divides cube into a pyramids and interpolate then in the pyramid.
128    #[cfg(feature = "options")]
129    Pyramid,
130    /// Interpolation by dividing cube into prisms.
131    #[cfg(feature = "options")]
132    Prism,
133    /// Trilinear/Quadlinear interpolation
134    #[default]
135    Linear,
136}
137
138impl Default for TransformOptions {
139    fn default() -> Self {
140        Self {
141            rendering_intent: RenderingIntent::default(),
142            allow_use_cicp_transfer: true,
143            prefer_fixed_point: true,
144            interpolation_method: InterpolationMethod::default(),
145            barycentric_weight_scale: BarycentricWeightScale::default(),
146            #[cfg(feature = "extended_range")]
147            allow_extended_range_rgb_xyz: false,
148            // black_point_compensation: false,
149        }
150    }
151}
152
153pub type Transform8BitExecutor = dyn TransformExecutor<u8> + Send + Sync;
154pub type Transform16BitExecutor = dyn TransformExecutor<u16> + Send + Sync;
155pub type TransformF32Executor = dyn TransformExecutor<f32> + Send + Sync;
156pub type TransformF64Executor = dyn TransformExecutor<f64> + Send + Sync;
157
158/// Layout declares a data layout.
159/// For RGB it shows also the channel order.
160/// To handle different data bit-depth appropriate executor must be used.
161/// Cmyk8 uses the same layout as Rgba8.
162#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
163pub enum Layout {
164    Rgb = 0,
165    Rgba = 1,
166    Cmyka = 16,
167    Gray = 2,
168    GrayAlpha = 3,
169    Inks5 = 4,
170    Inks6 = 5,
171    Inks7 = 6,
172    Inks8 = 7,
173    Inks9 = 8,
174    Inks10 = 9,
175    Inks11 = 10,
176    Inks12 = 11,
177    Inks13 = 12,
178    Inks14 = 13,
179    Inks15 = 14,
180}
181
182impl Layout {
183    /// Returns Red channel index
184    #[inline(always)]
185    pub const fn r_i(self) -> usize {
186        match self {
187            Layout::Rgb => 0,
188            Layout::Rgba => 0,
189            Layout::Gray => unimplemented!(),
190            Layout::GrayAlpha => unimplemented!(),
191            _ => unimplemented!(),
192        }
193    }
194
195    /// Returns Green channel index
196    #[inline(always)]
197    pub const fn g_i(self) -> usize {
198        match self {
199            Layout::Rgb => 1,
200            Layout::Rgba => 1,
201            Layout::Gray => unimplemented!(),
202            Layout::GrayAlpha => unimplemented!(),
203            _ => unimplemented!(),
204        }
205    }
206
207    /// Returns Blue channel index
208    #[inline(always)]
209    pub const fn b_i(self) -> usize {
210        match self {
211            Layout::Rgb => 2,
212            Layout::Rgba => 2,
213            Layout::Gray => unimplemented!(),
214            Layout::GrayAlpha => unimplemented!(),
215            _ => unimplemented!(),
216        }
217    }
218
219    #[inline(always)]
220    pub const fn a_i(self) -> usize {
221        match self {
222            Layout::Rgb => unimplemented!(),
223            Layout::Rgba => 3,
224            Layout::Gray => unimplemented!(),
225            Layout::GrayAlpha => 1,
226            _ => unimplemented!(),
227        }
228    }
229
230    #[inline(always)]
231    pub const fn has_alpha(self) -> bool {
232        match self {
233            Layout::Rgb => false,
234            Layout::Rgba => true,
235            Layout::Gray => false,
236            Layout::GrayAlpha => true,
237            _ => false,
238        }
239    }
240
241    #[inline]
242    pub const fn channels(self) -> usize {
243        match self {
244            Layout::Rgb => 3,
245            Layout::Rgba => 4,
246            Layout::Gray => 1,
247            Layout::GrayAlpha => 2,
248            Layout::Cmyka => 5,
249            Layout::Inks5 => 5,
250            Layout::Inks6 => 6,
251            Layout::Inks7 => 7,
252            Layout::Inks8 => 8,
253            Layout::Inks9 => 9,
254            Layout::Inks10 => 10,
255            Layout::Inks11 => 11,
256            Layout::Inks12 => 12,
257            Layout::Inks13 => 13,
258            Layout::Inks14 => 14,
259            Layout::Inks15 => 15,
260        }
261    }
262
263    #[cfg(feature = "any_to_any")]
264    pub(crate) fn from_inks(inks: usize) -> Self {
265        match inks {
266            1 => Layout::Gray,
267            2 => Layout::GrayAlpha,
268            3 => Layout::Rgb,
269            4 => Layout::Rgba,
270            5 => Layout::Inks5,
271            6 => Layout::Inks6,
272            7 => Layout::Inks7,
273            8 => Layout::Inks8,
274            9 => Layout::Inks9,
275            10 => Layout::Inks10,
276            11 => Layout::Inks11,
277            12 => Layout::Inks12,
278            13 => Layout::Inks13,
279            14 => Layout::Inks14,
280            15 => Layout::Inks15,
281            _ => unreachable!("Impossible amount of inks"),
282        }
283    }
284}
285
286impl From<u8> for Layout {
287    fn from(value: u8) -> Self {
288        match value {
289            0 => Layout::Rgb,
290            1 => Layout::Rgba,
291            2 => Layout::Gray,
292            3 => Layout::GrayAlpha,
293            _ => unimplemented!(),
294        }
295    }
296}
297
298impl Layout {
299    #[inline(always)]
300    pub const fn resolve(value: u8) -> Self {
301        match value {
302            0 => Layout::Rgb,
303            1 => Layout::Rgba,
304            2 => Layout::Gray,
305            3 => Layout::GrayAlpha,
306            4 => Layout::Inks5,
307            5 => Layout::Inks6,
308            6 => Layout::Inks7,
309            7 => Layout::Inks8,
310            8 => Layout::Inks9,
311            9 => Layout::Inks10,
312            10 => Layout::Inks11,
313            11 => Layout::Inks12,
314            12 => Layout::Inks13,
315            13 => Layout::Inks14,
316            14 => Layout::Inks15,
317            _ => unimplemented!(),
318        }
319    }
320}
321
322#[doc(hidden)]
323pub trait PointeeSizeExpressible {
324    fn _as_usize(self) -> usize;
325    const FINITE: bool;
326    const NOT_FINITE_GAMMA_TABLE_SIZE: usize;
327    const NOT_FINITE_LINEAR_TABLE_SIZE: usize;
328    const IS_U8: bool;
329    const IS_U16: bool;
330}
331
332impl PointeeSizeExpressible for u8 {
333    #[inline(always)]
334    fn _as_usize(self) -> usize {
335        self as usize
336    }
337
338    const FINITE: bool = true;
339    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
340    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
341    const IS_U8: bool = true;
342    const IS_U16: bool = false;
343}
344
345impl PointeeSizeExpressible for u16 {
346    #[inline(always)]
347    fn _as_usize(self) -> usize {
348        self as usize
349    }
350
351    const FINITE: bool = true;
352
353    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
354    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
355
356    const IS_U8: bool = false;
357    const IS_U16: bool = true;
358}
359
360impl PointeeSizeExpressible for f32 {
361    #[inline(always)]
362    fn _as_usize(self) -> usize {
363        const MAX_14_BIT: f32 = ((1 << 14u32) - 1) as f32;
364        ((self * MAX_14_BIT).max(0f32).min(MAX_14_BIT) as u16) as usize
365    }
366
367    const FINITE: bool = false;
368
369    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 32768;
370    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 14u32;
371    const IS_U8: bool = false;
372    const IS_U16: bool = false;
373}
374
375impl PointeeSizeExpressible for f64 {
376    #[inline(always)]
377    fn _as_usize(self) -> usize {
378        const MAX_16_BIT: f64 = ((1 << 16u32) - 1) as f64;
379        ((self * MAX_16_BIT).max(0.).min(MAX_16_BIT) as u16) as usize
380    }
381
382    const FINITE: bool = false;
383
384    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 65536;
385    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 16;
386    const IS_U8: bool = false;
387    const IS_U16: bool = false;
388}
389
390impl ColorProfile {
391    /// Checks if profile is valid *Matrix Shaper* profile
392    pub fn is_matrix_shaper(&self) -> bool {
393        self.color_space == DataColorSpace::Rgb
394            && self.red_colorant != Xyzd::default()
395            && self.green_colorant != Xyzd::default()
396            && self.blue_colorant != Xyzd::default()
397            && self.red_trc.is_some()
398            && self.green_trc.is_some()
399            && self.blue_trc.is_some()
400    }
401
402    /// Creates transform between source and destination profile
403    /// Use for 16 bit-depth data bit-depth only.
404    pub fn create_transform_16bit(
405        &self,
406        src_layout: Layout,
407        dst_pr: &ColorProfile,
408        dst_layout: Layout,
409        options: TransformOptions,
410    ) -> Result<Arc<Transform16BitExecutor>, CmsError> {
411        let mut core_src_layout = src_layout;
412        if src_layout == Layout::Cmyka {
413            core_src_layout = Layout::Rgba;
414        }
415        let mut core_dst_layout = dst_layout;
416        if dst_layout == Layout::Cmyka {
417            core_dst_layout = Layout::Rgba;
418        }
419        let handle = self.create_transform_nbit::<u16, 16, 65536, 65536>(
420            core_src_layout,
421            dst_pr,
422            core_dst_layout,
423            options,
424        )?;
425        if core_src_layout == Layout::Cmyka {
426            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
427        } else if core_dst_layout == Layout::Cmyka {
428            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
429        }
430        Ok(handle)
431    }
432
433    /// Creates transform between source and destination profile
434    /// Use for 16 bit-depth data bit-depth only.
435    ///
436    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
437    #[cfg(feature = "in_place")]
438    pub fn create_in_place_transform_16bit(
439        &self,
440        layout: Layout,
441        dst_pr: &ColorProfile,
442        options: TransformOptions,
443    ) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
444        self.create_transform_in_place_nbit::<u16, 16, 65536, 65536>(layout, dst_pr, options)
445    }
446
447    /// Creates transform between source and destination profile
448    /// Use for 12 bit-depth data bit-depth only.
449    pub fn create_transform_12bit(
450        &self,
451        src_layout: Layout,
452        dst_pr: &ColorProfile,
453        dst_layout: Layout,
454        options: TransformOptions,
455    ) -> Result<Arc<Transform16BitExecutor>, CmsError> {
456        let mut core_src_layout = src_layout;
457        if src_layout == Layout::Cmyka {
458            core_src_layout = Layout::Rgba;
459        }
460        let mut core_dst_layout = dst_layout;
461        if dst_layout == Layout::Cmyka {
462            core_dst_layout = Layout::Rgba;
463        }
464        let handle = self.create_transform_nbit::<u16, 12, 65536, 16384>(
465            core_src_layout,
466            dst_pr,
467            core_dst_layout,
468            options,
469        )?;
470        if core_src_layout == Layout::Cmyka {
471            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
472        } else if core_dst_layout == Layout::Cmyka {
473            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
474        }
475        Ok(handle)
476    }
477
478    /// Creates transform between source and destination profile
479    /// Use for 12 bit-depth data bit-depth only.
480    ///
481    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
482    #[cfg(feature = "in_place")]
483    pub fn create_in_place_transform_12bit(
484        &self,
485        layout: Layout,
486        dst_pr: &ColorProfile,
487        options: TransformOptions,
488    ) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
489        self.create_transform_in_place_nbit::<u16, 12, 65536, 16384>(layout, dst_pr, options)
490    }
491
492    /// Creates transform between source and destination profile
493    /// Use for 10 bit-depth data bit-depth only.
494    pub fn create_transform_10bit(
495        &self,
496        src_layout: Layout,
497        dst_pr: &ColorProfile,
498        dst_layout: Layout,
499        options: TransformOptions,
500    ) -> Result<Arc<Transform16BitExecutor>, CmsError> {
501        let mut core_src_layout = src_layout;
502        if src_layout == Layout::Cmyka {
503            core_src_layout = Layout::Rgba;
504        }
505        let mut core_dst_layout = dst_layout;
506        if dst_layout == Layout::Cmyka {
507            core_dst_layout = Layout::Rgba;
508        }
509        let handle = self.create_transform_nbit::<u16, 10, 65536, 8192>(
510            core_src_layout,
511            dst_pr,
512            core_dst_layout,
513            options,
514        )?;
515        if core_src_layout == Layout::Cmyka {
516            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
517        } else if core_dst_layout == Layout::Cmyka {
518            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
519        }
520        Ok(handle)
521    }
522
523    /// Creates transform between source and destination profile
524    /// Use for 10 bit-depth data bit-depth only.
525    ///
526    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
527    #[cfg(feature = "in_place")]
528    pub fn create_in_place_transform_10bit(
529        &self,
530        layout: Layout,
531        dst_pr: &ColorProfile,
532        options: TransformOptions,
533    ) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
534        self.create_transform_in_place_nbit::<u16, 10, 65536, 8192>(layout, dst_pr, options)
535    }
536
537    /// Creates transform between source and destination profile
538    /// Data has to be normalized into [0, 1] range.
539    /// ICC profiles and LUT tables do not exist in infinite precision.
540    /// Thus, this implementation considers `f32` as 14-bit values.
541    /// Floating point transformer works in extended mode, that means returned data might be negative
542    /// or more than 1.
543    pub fn create_transform_f32(
544        &self,
545        src_layout: Layout,
546        dst_pr: &ColorProfile,
547        dst_layout: Layout,
548        options: TransformOptions,
549    ) -> Result<Arc<TransformF32Executor>, CmsError> {
550        let mut core_src_layout = src_layout;
551        if src_layout == Layout::Cmyka {
552            core_src_layout = Layout::Rgba;
553        }
554        let mut core_dst_layout = dst_layout;
555        if dst_layout == Layout::Cmyka {
556            core_dst_layout = Layout::Rgba;
557        }
558        let handle = self.create_transform_nbit::<f32, 1, 65536, 32768>(
559            core_src_layout,
560            dst_pr,
561            core_dst_layout,
562            options,
563        )?;
564        if core_src_layout == Layout::Cmyka {
565            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
566        } else if core_dst_layout == Layout::Cmyka {
567            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
568        }
569        Ok(handle)
570    }
571
572    /// Creates transform between source and destination profile.
573    /// Data has to be normalized into [0, 1] range.
574    /// ICC profiles and LUT tables do not exist in infinite precision.
575    /// Thus, this implementation considers `f32` as 14-bit values.
576    /// Floating point transformer works in extended mode, that means returned data might be negative
577    /// or more than 1.
578    ///
579    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
580    #[cfg(feature = "in_place")]
581    pub fn create_in_place_transform_f32(
582        &self,
583        layout: Layout,
584        dst_pr: &ColorProfile,
585        options: TransformOptions,
586    ) -> Result<Arc<dyn InPlaceTransformExecutor<f32> + Send + Sync>, CmsError> {
587        self.create_transform_in_place_nbit::<f32, 1, 65536, 32768>(layout, dst_pr, options)
588    }
589
590    /// Creates transform between source and destination profile
591    /// Data has to be normalized into [0, 1] range.
592    /// ICC profiles and LUT tables do not exist in infinite precision.
593    /// Thus, this implementation considers `f64` as 16-bit values.
594    /// Floating point transformer works in extended mode, that means returned data might be negative
595    /// or more than 1.
596    pub fn create_transform_f64(
597        &self,
598        src_layout: Layout,
599        dst_pr: &ColorProfile,
600        dst_layout: Layout,
601        options: TransformOptions,
602    ) -> Result<Arc<TransformF64Executor>, CmsError> {
603        let mut core_src_layout = src_layout;
604        if src_layout == Layout::Cmyka {
605            core_src_layout = Layout::Rgba;
606        }
607        let mut core_dst_layout = dst_layout;
608        if dst_layout == Layout::Cmyka {
609            core_dst_layout = Layout::Rgba;
610        }
611        let handle = self.create_transform_nbit::<f64, 1, 65536, 65536>(
612            core_src_layout,
613            dst_pr,
614            core_dst_layout,
615            options,
616        )?;
617        if core_src_layout == Layout::Cmyka {
618            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
619        } else if core_dst_layout == Layout::Cmyka {
620            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
621        }
622        Ok(handle)
623    }
624
625    /// Creates transform between source and destination profile
626    /// Data has to be normalized into [0, 1] range.
627    /// ICC profiles and LUT tables do not exist in infinite precision.
628    /// Thus, this implementation considers `f64` as 16-bit values.
629    /// Floating point transformer works in extended mode, that means returned data might be negative
630    /// or more than 1.
631    ///
632    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
633    #[cfg(feature = "in_place")]
634    pub fn create_in_place_transform_f64(
635        &self,
636        layout: Layout,
637        dst_pr: &ColorProfile,
638        options: TransformOptions,
639    ) -> Result<Arc<dyn InPlaceTransformExecutor<f64> + Send + Sync>, CmsError> {
640        self.create_transform_in_place_nbit::<f64, 1, 65536, 65536>(layout, dst_pr, options)
641    }
642
643    #[cfg(feature = "in_place")]
644    fn create_transform_in_place_nbit<
645        T: Copy
646            + Default
647            + AsPrimitive<usize>
648            + PointeeSizeExpressible
649            + Send
650            + Sync
651            + AsPrimitive<f32>
652            + RgbXyzFactory<T>
653            + RgbXyzFactoryOpt<T>
654            + GammaLutInterpolate,
655        const BIT_DEPTH: usize,
656        const LINEAR_CAP: usize,
657        const GAMMA_CAP: usize,
658    >(
659        &self,
660        layout: Layout,
661        dst_pr: &ColorProfile,
662        options: TransformOptions,
663    ) -> Result<Arc<dyn InPlaceTransformExecutor<T> + Send + Sync>, CmsError>
664    where
665        f32: AsPrimitive<T>,
666        u32: AsPrimitive<T>,
667    {
668        // In-place transforms supports only matrix shaper transforms
669        let is_rgb_transform = self.color_space == DataColorSpace::Rgb
670            && dst_pr.pcs == DataColorSpace::Xyz
671            && dst_pr.color_space == DataColorSpace::Rgb
672            && self.pcs == DataColorSpace::Xyz
673            && self.is_matrix_shaper()
674            && dst_pr.is_matrix_shaper();
675        let is_gray_transform = (self.color_space == DataColorSpace::Gray
676            && self.gray_trc.is_some())
677            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
678            && self.pcs == DataColorSpace::Xyz
679            && dst_pr.pcs == DataColorSpace::Xyz;
680
681        if is_rgb_transform {
682            let transform = self.transform_matrix(dst_pr);
683
684            if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
685                let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
686                    options.allow_use_cicp_transfer,
687                )?;
688
689                let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
690                    &dst_pr.red_trc,
691                    options.allow_use_cicp_transfer,
692                )?;
693
694                let profile_transform = TransformMatrixShaperOptimized {
695                    linear,
696                    gamma,
697                    adaptation_matrix: transform.to_f32(),
698                };
699
700                return T::make_in_place_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
701                    layout,
702                    profile_transform,
703                    options,
704                );
705            }
706
707            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
708                options.allow_use_cicp_transfer,
709            )?;
710            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
711                options.allow_use_cicp_transfer,
712            )?;
713            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
714                options.allow_use_cicp_transfer,
715            )?;
716
717            let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
718                &dst_pr.red_trc,
719                options.allow_use_cicp_transfer,
720            )?;
721            let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
722                &dst_pr.green_trc,
723                options.allow_use_cicp_transfer,
724            )?;
725            let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
726                &dst_pr.blue_trc,
727                options.allow_use_cicp_transfer,
728            )?;
729
730            let profile_transform = TransformMatrixShaper {
731                r_linear: lin_r,
732                g_linear: lin_g,
733                b_linear: lin_b,
734                r_gamma: gamma_r,
735                g_gamma: gamma_g,
736                b_gamma: gamma_b,
737                adaptation_matrix: transform.to_f32(),
738            };
739
740            return T::make_in_place_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
741                layout,
742                profile_transform,
743                options,
744            );
745        }
746
747        if is_gray_transform {
748            // Gray -> Gray case
749            let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
750
751            let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
752                &dst_pr.gray_trc,
753                options.allow_use_cicp_transfer,
754            )?;
755
756            use crate::conversions::make_gray_to_gray_in_place;
757            return make_gray_to_gray_in_place::<T, LINEAR_CAP>(
758                layout,
759                &gray_linear,
760                &gray_gamma,
761                BIT_DEPTH,
762                GAMMA_CAP,
763            );
764        }
765
766        Err(CmsError::UnsupportedProfileConnection)
767    }
768
769    fn create_transform_nbit<
770        T: Copy
771            + Default
772            + AsPrimitive<usize>
773            + PointeeSizeExpressible
774            + Send
775            + Sync
776            + AsPrimitive<f32>
777            + RgbXyzFactory<T>
778            + RgbXyzFactoryOpt<T>
779            + GammaLutInterpolate,
780        const BIT_DEPTH: usize,
781        const LINEAR_CAP: usize,
782        const GAMMA_CAP: usize,
783    >(
784        &self,
785        src_layout: Layout,
786        dst_pr: &ColorProfile,
787        dst_layout: Layout,
788        options: TransformOptions,
789    ) -> Result<Arc<dyn TransformExecutor<T> + Send + Sync>, CmsError>
790    where
791        f32: AsPrimitive<T>,
792        u32: AsPrimitive<T>,
793        (): LutBarycentricReduction<T, u8>,
794        (): LutBarycentricReduction<T, u16>,
795    {
796        if self.color_space == DataColorSpace::Rgb
797            && dst_pr.pcs == DataColorSpace::Xyz
798            && dst_pr.color_space == DataColorSpace::Rgb
799            && self.pcs == DataColorSpace::Xyz
800            && self.is_matrix_shaper()
801            && dst_pr.is_matrix_shaper()
802        {
803            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
804                return Err(CmsError::InvalidLayout);
805            }
806            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
807                return Err(CmsError::InvalidLayout);
808            }
809
810            #[cfg(feature = "lut")]
811            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
812                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
813                    src_layout, self, dst_layout, dst_pr, options,
814                );
815            }
816
817            let transform = self.transform_matrix(dst_pr);
818
819            #[cfg(feature = "extended_range")]
820            if !T::FINITE && options.allow_extended_range_rgb_xyz {
821                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
822                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
823                        use crate::conversions::{
824                            TransformShaperFloatInOut, make_rgb_xyz_rgb_transform_float_in_out,
825                        };
826                        use std::marker::PhantomData;
827                        let p = TransformShaperFloatInOut {
828                            linear_evaluator,
829                            gamma_evaluator,
830                            adaptation_matrix: transform.to_f32(),
831                            phantom_data: PhantomData,
832                        };
833                        return make_rgb_xyz_rgb_transform_float_in_out::<T>(
834                            src_layout, dst_layout, p, BIT_DEPTH,
835                        );
836                    }
837
838                    let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
839                        options.allow_use_cicp_transfer,
840                    )?;
841                    let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
842                        options.allow_use_cicp_transfer,
843                    )?;
844                    let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
845                        options.allow_use_cicp_transfer,
846                    )?;
847
848                    use crate::conversions::{
849                        TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float,
850                    };
851                    use std::marker::PhantomData;
852                    let p = TransformShaperRgbFloat {
853                        r_linear: lin_r,
854                        g_linear: lin_g,
855                        b_linear: lin_b,
856                        gamma_evaluator,
857                        adaptation_matrix: transform.to_f32(),
858                        phantom_data: PhantomData,
859                    };
860                    return make_rgb_xyz_rgb_transform_float::<T, LINEAR_CAP>(
861                        src_layout, dst_layout, p, BIT_DEPTH,
862                    );
863                }
864            }
865
866            if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
867                let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
868                    options.allow_use_cicp_transfer,
869                )?;
870
871                let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
872                    &dst_pr.red_trc,
873                    options.allow_use_cicp_transfer,
874                )?;
875
876                let profile_transform = TransformMatrixShaperOptimized {
877                    linear,
878                    gamma,
879                    adaptation_matrix: transform.to_f32(),
880                };
881
882                return T::make_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
883                    src_layout,
884                    dst_layout,
885                    profile_transform,
886                    options,
887                );
888            }
889
890            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
891                options.allow_use_cicp_transfer,
892            )?;
893            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
894                options.allow_use_cicp_transfer,
895            )?;
896            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
897                options.allow_use_cicp_transfer,
898            )?;
899
900            let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
901                &dst_pr.red_trc,
902                options.allow_use_cicp_transfer,
903            )?;
904            let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
905                &dst_pr.green_trc,
906                options.allow_use_cicp_transfer,
907            )?;
908            let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
909                &dst_pr.blue_trc,
910                options.allow_use_cicp_transfer,
911            )?;
912
913            let profile_transform = TransformMatrixShaper {
914                r_linear: lin_r,
915                g_linear: lin_g,
916                b_linear: lin_b,
917                r_gamma: gamma_r,
918                g_gamma: gamma_g,
919                b_gamma: gamma_b,
920                adaptation_matrix: transform.to_f32(),
921            };
922
923            T::make_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
924                src_layout,
925                dst_layout,
926                profile_transform,
927                options,
928            )
929        } else if (self.color_space == DataColorSpace::Gray && self.gray_trc.is_some())
930            && (dst_pr.color_space == DataColorSpace::Rgb
931                || (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some()))
932            && self.pcs == DataColorSpace::Xyz
933            && dst_pr.pcs == DataColorSpace::Xyz
934        {
935            if src_layout != Layout::GrayAlpha && src_layout != Layout::Gray {
936                return Err(CmsError::InvalidLayout);
937            }
938
939            #[cfg(feature = "lut")]
940            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
941                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
942                    src_layout, self, dst_layout, dst_pr, options,
943                );
944            }
945
946            let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
947
948            if dst_pr.color_space == DataColorSpace::Gray {
949                #[cfg(feature = "extended_range")]
950                if !T::FINITE && options.allow_extended_range_rgb_xyz {
951                    if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
952                        if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
953                            // Gray -> Gray case extended range
954                            use crate::conversions::make_gray_to_one_trc_extended;
955                            return make_gray_to_one_trc_extended::<T>(
956                                src_layout,
957                                dst_layout,
958                                linear_evaluator,
959                                gamma_evaluator,
960                                BIT_DEPTH,
961                            );
962                        }
963                    }
964                }
965
966                // Gray -> Gray case
967                let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
968                    &dst_pr.gray_trc,
969                    options.allow_use_cicp_transfer,
970                )?;
971
972                make_gray_to_x::<T, LINEAR_CAP>(
973                    src_layout,
974                    dst_layout,
975                    &gray_linear,
976                    &gray_gamma,
977                    BIT_DEPTH,
978                    GAMMA_CAP,
979                )
980            } else {
981                #[allow(clippy::collapsible_if)]
982                if dst_pr.are_all_trc_the_same() {
983                    #[cfg(feature = "extended_range")]
984                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
985                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
986                            if let Some(linear_evaluator) =
987                                self.try_extended_linearizing_evaluator()
988                            {
989                                // Gray -> RGB where all TRC is the same with extended range
990                                use crate::conversions::make_gray_to_one_trc_extended;
991                                return make_gray_to_one_trc_extended::<T>(
992                                    src_layout,
993                                    dst_layout,
994                                    linear_evaluator,
995                                    gamma_evaluator,
996                                    BIT_DEPTH,
997                                );
998                            }
999                        }
1000                    }
1001
1002                    // Gray -> RGB where all TRC is the same
1003                    let rgb_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1004                        &dst_pr.red_trc,
1005                        options.allow_use_cicp_transfer,
1006                    )?;
1007
1008                    make_gray_to_x::<T, LINEAR_CAP>(
1009                        src_layout,
1010                        dst_layout,
1011                        &gray_linear,
1012                        &rgb_gamma,
1013                        BIT_DEPTH,
1014                        GAMMA_CAP,
1015                    )
1016                } else {
1017                    // Gray -> RGB where all TRC is NOT the same
1018                    #[cfg(feature = "extended_range")]
1019                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
1020                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
1021                            if let Some(linear_evaluator) =
1022                                self.try_extended_linearizing_evaluator()
1023                            {
1024                                // Gray -> RGB where all TRC is NOT the same with extended range
1025
1026                                use crate::conversions::make_gray_to_rgb_extended;
1027                                return make_gray_to_rgb_extended::<T>(
1028                                    src_layout,
1029                                    dst_layout,
1030                                    linear_evaluator,
1031                                    gamma_evaluator,
1032                                    BIT_DEPTH,
1033                                );
1034                            }
1035                        }
1036                    }
1037
1038                    let red_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1039                        &dst_pr.red_trc,
1040                        options.allow_use_cicp_transfer,
1041                    )?;
1042                    let green_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1043                        &dst_pr.green_trc,
1044                        options.allow_use_cicp_transfer,
1045                    )?;
1046                    let blue_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1047                        &dst_pr.blue_trc,
1048                        options.allow_use_cicp_transfer,
1049                    )?;
1050
1051                    let mut gray_linear2 = Box::new([0f32; 65536]);
1052                    for (dst, src) in gray_linear2.iter_mut().zip(gray_linear.iter()) {
1053                        *dst = *src;
1054                    }
1055
1056                    make_gray_to_unfused::<T, LINEAR_CAP>(
1057                        src_layout,
1058                        dst_layout,
1059                        gray_linear2,
1060                        red_gamma,
1061                        green_gamma,
1062                        blue_gamma,
1063                        BIT_DEPTH,
1064                        GAMMA_CAP,
1065                    )
1066                }
1067            }
1068        } else if self.color_space == DataColorSpace::Rgb
1069            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
1070            && dst_pr.pcs == DataColorSpace::Xyz
1071            && self.pcs == DataColorSpace::Xyz
1072        {
1073            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
1074                return Err(CmsError::InvalidLayout);
1075            }
1076            if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha {
1077                return Err(CmsError::InvalidLayout);
1078            }
1079
1080            #[cfg(feature = "lut")]
1081            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
1082                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
1083                    src_layout, self, dst_layout, dst_pr, options,
1084                );
1085            }
1086
1087            let transform = self.transform_matrix(dst_pr).to_f32();
1088
1089            let vector = Vector3f {
1090                v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]],
1091            };
1092
1093            #[cfg(feature = "extended_range")]
1094            if !T::FINITE && options.allow_extended_range_rgb_xyz {
1095                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
1096                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
1097                        use crate::conversions::make_rgb_to_gray_extended;
1098                        return make_rgb_to_gray_extended::<T>(
1099                            src_layout,
1100                            dst_layout,
1101                            linear_evaluator,
1102                            gamma_evaluator,
1103                            vector,
1104                            BIT_DEPTH,
1105                        );
1106                    }
1107                }
1108            }
1109
1110            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
1111                options.allow_use_cicp_transfer,
1112            )?;
1113            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
1114                options.allow_use_cicp_transfer,
1115            )?;
1116            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
1117                options.allow_use_cicp_transfer,
1118            )?;
1119            let gray_linear = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1120                &dst_pr.gray_trc,
1121                options.allow_use_cicp_transfer,
1122            )?;
1123
1124            let trc_box = ToneReproductionRgbToGray::<T, LINEAR_CAP> {
1125                r_linear: lin_r,
1126                g_linear: lin_g,
1127                b_linear: lin_b,
1128                gray_gamma: gray_linear,
1129            };
1130
1131            make_rgb_to_gray::<T, LINEAR_CAP>(
1132                src_layout, dst_layout, trc_box, vector, GAMMA_CAP, BIT_DEPTH,
1133            )
1134        } else if (self.color_space.is_three_channels()
1135            || self.color_space == DataColorSpace::Cmyk
1136            || self.color_space == DataColorSpace::Color4)
1137            && (dst_pr.color_space.is_three_channels()
1138                || dst_pr.color_space == DataColorSpace::Cmyk
1139                || dst_pr.color_space == DataColorSpace::Color4)
1140            && (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab)
1141            && (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab)
1142        {
1143            #[cfg(feature = "lut")]
1144            {
1145                if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
1146                    return Err(CmsError::InvalidLayout);
1147                }
1148                if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
1149                    return Err(CmsError::InvalidLayout);
1150                }
1151                make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
1152                    src_layout, self, dst_layout, dst_pr, options,
1153                )
1154            }
1155            #[cfg(not(feature = "lut"))]
1156            {
1157                Err(CmsError::UnsupportedProfileConnection)
1158            }
1159        } else {
1160            #[cfg(feature = "lut")]
1161            {
1162                make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
1163                    src_layout, self, dst_layout, dst_pr, options,
1164                )
1165            }
1166            #[cfg(not(feature = "lut"))]
1167            {
1168                Err(CmsError::UnsupportedProfileConnection)
1169            }
1170        }
1171    }
1172
1173    /// Creates transform between source and destination profile
1174    /// Only 8 bit is supported.
1175    pub fn create_transform_8bit(
1176        &self,
1177        src_layout: Layout,
1178        dst_pr: &ColorProfile,
1179        dst_layout: Layout,
1180        options: TransformOptions,
1181    ) -> Result<Arc<Transform8BitExecutor>, CmsError> {
1182        self.create_transform_nbit::<u8, 8, 256, 4096>(src_layout, dst_pr, dst_layout, options)
1183    }
1184
1185    /// Creates transform between source and destination profile
1186    ///
1187    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
1188    #[cfg(feature = "in_place")]
1189    pub fn create_in_place_transform_8bit(
1190        &self,
1191        layout: Layout,
1192        dst_pr: &ColorProfile,
1193        options: TransformOptions,
1194    ) -> Result<Arc<dyn InPlaceTransformExecutor<u8> + Send + Sync>, CmsError> {
1195        self.create_transform_in_place_nbit::<u8, 8, 256, 4096>(layout, dst_pr, options)
1196    }
1197
1198    #[allow(unused)]
1199    pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
1200        match intent {
1201            RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
1202            RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(),
1203            RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
1204            RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(),
1205        }
1206    }
1207
1208    #[allow(unused)]
1209    pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
1210        match intent {
1211            RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
1212            RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(),
1213            RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
1214            RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(),
1215        }
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use crate::*;
1222    use rand::RngExt;
1223
1224    #[test]
1225    fn test_transform_rgb8() {
1226        let mut srgb_profile = ColorProfile::new_srgb();
1227        let bt2020_profile = ColorProfile::new_bt2020();
1228        let random_point_x = rand::rng().random_range(0..255);
1229        let transform = bt2020_profile
1230            .create_transform_8bit(
1231                Layout::Rgb,
1232                &srgb_profile,
1233                Layout::Rgb,
1234                TransformOptions::default(),
1235            )
1236            .unwrap();
1237        let src = vec![random_point_x; 256 * 256 * 3];
1238        let mut dst = vec![random_point_x; 256 * 256 * 3];
1239        transform.transform(&src, &mut dst).unwrap();
1240
1241        let transform = bt2020_profile
1242            .create_transform_8bit(
1243                Layout::Rgb,
1244                &srgb_profile,
1245                Layout::Rgb,
1246                TransformOptions {
1247                    ..TransformOptions::default()
1248                },
1249            )
1250            .unwrap();
1251        transform.transform(&src, &mut dst).unwrap();
1252        srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric;
1253        let transform = bt2020_profile
1254            .create_transform_8bit(
1255                Layout::Rgb,
1256                &srgb_profile,
1257                Layout::Rgb,
1258                TransformOptions {
1259                    ..TransformOptions::default()
1260                },
1261            )
1262            .unwrap();
1263        transform.transform(&src, &mut dst).unwrap();
1264        srgb_profile.rendering_intent = RenderingIntent::Saturation;
1265        let transform = bt2020_profile
1266            .create_transform_8bit(
1267                Layout::Rgb,
1268                &srgb_profile,
1269                Layout::Rgb,
1270                TransformOptions {
1271                    ..TransformOptions::default()
1272                },
1273            )
1274            .unwrap();
1275        transform.transform(&src, &mut dst).unwrap();
1276    }
1277
1278    #[test]
1279    fn test_transform_rgba8() {
1280        let srgb_profile = ColorProfile::new_srgb();
1281        let bt2020_profile = ColorProfile::new_bt2020();
1282        let random_point_x = rand::rng().random_range(0..255);
1283        let transform = bt2020_profile
1284            .create_transform_8bit(
1285                Layout::Rgba,
1286                &srgb_profile,
1287                Layout::Rgba,
1288                TransformOptions::default(),
1289            )
1290            .unwrap();
1291        let src = vec![random_point_x; 256 * 256 * 4];
1292        let mut dst = vec![random_point_x; 256 * 256 * 4];
1293        transform.transform(&src, &mut dst).unwrap();
1294    }
1295
1296    #[test]
1297    fn test_transform_gray_to_rgb8() {
1298        let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32);
1299        let bt2020_profile = ColorProfile::new_bt2020();
1300        let random_point_x = rand::rng().random_range(0..255);
1301        let transform = gray_profile
1302            .create_transform_8bit(
1303                Layout::Gray,
1304                &bt2020_profile,
1305                Layout::Rgb,
1306                TransformOptions::default(),
1307            )
1308            .unwrap();
1309        let src = vec![random_point_x; 256 * 256];
1310        let mut dst = vec![random_point_x; 256 * 256 * 3];
1311        transform.transform(&src, &mut dst).unwrap();
1312    }
1313
1314    #[test]
1315    fn test_transform_gray_to_rgba8() {
1316        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
1317        let bt2020_profile = ColorProfile::new_bt2020();
1318        let random_point_x = rand::rng().random_range(0..255);
1319        let transform = srgb_profile
1320            .create_transform_8bit(
1321                Layout::Gray,
1322                &bt2020_profile,
1323                Layout::Rgba,
1324                TransformOptions::default(),
1325            )
1326            .unwrap();
1327        let src = vec![random_point_x; 256 * 256];
1328        let mut dst = vec![random_point_x; 256 * 256 * 4];
1329        transform.transform(&src, &mut dst).unwrap();
1330    }
1331
1332    #[test]
1333    fn test_transform_gray_to_gray_alpha8() {
1334        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
1335        let bt2020_profile = ColorProfile::new_bt2020();
1336        let random_point_x = rand::rng().random_range(0..255);
1337        let transform = srgb_profile
1338            .create_transform_8bit(
1339                Layout::Gray,
1340                &bt2020_profile,
1341                Layout::GrayAlpha,
1342                TransformOptions::default(),
1343            )
1344            .unwrap();
1345        let src = vec![random_point_x; 256 * 256];
1346        let mut dst = vec![random_point_x; 256 * 256 * 2];
1347        transform.transform(&src, &mut dst).unwrap();
1348    }
1349
1350    #[test]
1351    fn test_transform_rgb10() {
1352        let srgb_profile = ColorProfile::new_srgb();
1353        let bt2020_profile = ColorProfile::new_bt2020();
1354        let random_point_x = rand::rng().random_range(0..((1 << 10) - 1));
1355        let transform = bt2020_profile
1356            .create_transform_10bit(
1357                Layout::Rgb,
1358                &srgb_profile,
1359                Layout::Rgb,
1360                TransformOptions::default(),
1361            )
1362            .unwrap();
1363        let src = vec![random_point_x; 256 * 256 * 3];
1364        let mut dst = vec![random_point_x; 256 * 256 * 3];
1365        transform.transform(&src, &mut dst).unwrap();
1366    }
1367
1368    #[test]
1369    fn test_transform_rgb12() {
1370        let srgb_profile = ColorProfile::new_srgb();
1371        let bt2020_profile = ColorProfile::new_bt2020();
1372        let random_point_x = rand::rng().random_range(0..((1 << 12) - 1));
1373        let transform = bt2020_profile
1374            .create_transform_12bit(
1375                Layout::Rgb,
1376                &srgb_profile,
1377                Layout::Rgb,
1378                TransformOptions::default(),
1379            )
1380            .unwrap();
1381        let src = vec![random_point_x; 256 * 256 * 3];
1382        let mut dst = vec![random_point_x; 256 * 256 * 3];
1383        transform.transform(&src, &mut dst).unwrap();
1384    }
1385
1386    #[test]
1387    fn test_transform_rgb16() {
1388        let srgb_profile = ColorProfile::new_srgb();
1389        let bt2020_profile = ColorProfile::new_bt2020();
1390        let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16;
1391        let transform = bt2020_profile
1392            .create_transform_16bit(
1393                Layout::Rgb,
1394                &srgb_profile,
1395                Layout::Rgb,
1396                TransformOptions::default(),
1397            )
1398            .unwrap();
1399        let src = vec![random_point_x; 256 * 256 * 3];
1400        let mut dst = vec![random_point_x; 256 * 256 * 3];
1401        transform.transform(&src, &mut dst).unwrap();
1402    }
1403
1404    #[test]
1405    fn test_transform_round_trip_rgb8() {
1406        let srgb_profile = ColorProfile::new_srgb();
1407        let bt2020_profile = ColorProfile::new_bt2020();
1408        let transform = srgb_profile
1409            .create_transform_8bit(
1410                Layout::Rgb,
1411                &bt2020_profile,
1412                Layout::Rgb,
1413                TransformOptions::default(),
1414            )
1415            .unwrap();
1416        let mut src = vec![0u8; 256 * 256 * 3];
1417        for dst in src.chunks_exact_mut(3) {
1418            dst[0] = 175;
1419            dst[1] = 75;
1420            dst[2] = 13;
1421        }
1422        let mut dst = vec![0u8; 256 * 256 * 3];
1423        transform.transform(&src, &mut dst).unwrap();
1424
1425        let transform_inverse = bt2020_profile
1426            .create_transform_8bit(
1427                Layout::Rgb,
1428                &srgb_profile,
1429                Layout::Rgb,
1430                TransformOptions::default(),
1431            )
1432            .unwrap();
1433
1434        transform_inverse.transform(&dst, &mut src).unwrap();
1435
1436        for src in src.chunks_exact_mut(3) {
1437            let diff0 = (src[0] as i32 - 175).abs();
1438            let diff1 = (src[1] as i32 - 75).abs();
1439            let diff2 = (src[2] as i32 - 13).abs();
1440            assert!(
1441                diff0 < 3,
1442                "On channel 0 difference should be less than 3, but it was {diff0}"
1443            );
1444            assert!(
1445                diff1 < 3,
1446                "On channel 1 difference should be less than 3, but it was {diff1}"
1447            );
1448            assert!(
1449                diff2 < 3,
1450                "On channel 2 difference should be less than 3, but it was {diff2}"
1451            );
1452        }
1453    }
1454
1455    #[test]
1456    fn test_transform_round_trip_rgb10() {
1457        let srgb_profile = ColorProfile::new_srgb();
1458        let bt2020_profile = ColorProfile::new_bt2020();
1459        let transform = srgb_profile
1460            .create_transform_10bit(
1461                Layout::Rgb,
1462                &bt2020_profile,
1463                Layout::Rgb,
1464                TransformOptions::default(),
1465            )
1466            .unwrap();
1467        let mut src = vec![0u16; 256 * 256 * 3];
1468        for dst in src.chunks_exact_mut(3) {
1469            dst[0] = 175;
1470            dst[1] = 256;
1471            dst[2] = 512;
1472        }
1473        let mut dst = vec![0u16; 256 * 256 * 3];
1474        transform.transform(&src, &mut dst).unwrap();
1475
1476        let transform_inverse = bt2020_profile
1477            .create_transform_10bit(
1478                Layout::Rgb,
1479                &srgb_profile,
1480                Layout::Rgb,
1481                TransformOptions::default(),
1482            )
1483            .unwrap();
1484
1485        transform_inverse.transform(&dst, &mut src).unwrap();
1486
1487        for src in src.chunks_exact_mut(3) {
1488            let diff0 = (src[0] as i32 - 175).abs();
1489            let diff1 = (src[1] as i32 - 256).abs();
1490            let diff2 = (src[2] as i32 - 512).abs();
1491            assert!(
1492                diff0 < 15,
1493                "On channel 0 difference should be less than 15, but it was {diff0}"
1494            );
1495            assert!(
1496                diff1 < 15,
1497                "On channel 1 difference should be less than 15, but it was {diff1}"
1498            );
1499            assert!(
1500                diff2 < 15,
1501                "On channel 2 difference should be less than 15, but it was {diff2}"
1502            );
1503        }
1504    }
1505
1506    #[test]
1507    fn test_transform_round_trip_rgb12() {
1508        let srgb_profile = ColorProfile::new_srgb();
1509        let bt2020_profile = ColorProfile::new_bt2020();
1510        let transform = srgb_profile
1511            .create_transform_12bit(
1512                Layout::Rgb,
1513                &bt2020_profile,
1514                Layout::Rgb,
1515                TransformOptions::default(),
1516            )
1517            .unwrap();
1518        let mut src = vec![0u16; 256 * 256 * 3];
1519        for dst in src.chunks_exact_mut(3) {
1520            dst[0] = 1750;
1521            dst[1] = 2560;
1522            dst[2] = 3143;
1523        }
1524        let mut dst = vec![0u16; 256 * 256 * 3];
1525        transform.transform(&src, &mut dst).unwrap();
1526
1527        let transform_inverse = bt2020_profile
1528            .create_transform_12bit(
1529                Layout::Rgb,
1530                &srgb_profile,
1531                Layout::Rgb,
1532                TransformOptions::default(),
1533            )
1534            .unwrap();
1535
1536        transform_inverse.transform(&dst, &mut src).unwrap();
1537
1538        for src in src.chunks_exact_mut(3) {
1539            let diff0 = (src[0] as i32 - 1750).abs();
1540            let diff1 = (src[1] as i32 - 2560).abs();
1541            let diff2 = (src[2] as i32 - 3143).abs();
1542            assert!(
1543                diff0 < 25,
1544                "On channel 0 difference should be less than 25, but it was {diff0}"
1545            );
1546            assert!(
1547                diff1 < 25,
1548                "On channel 1 difference should be less than 25, but it was {diff1}"
1549            );
1550            assert!(
1551                diff2 < 25,
1552                "On channel 2 difference should be less than 25, but it was {diff2}"
1553            );
1554        }
1555    }
1556
1557    #[test]
1558    fn test_transform_round_trip_rgb16() {
1559        let srgb_profile = ColorProfile::new_srgb();
1560        let bt2020_profile = ColorProfile::new_bt2020();
1561        let transform = srgb_profile
1562            .create_transform_16bit(
1563                Layout::Rgb,
1564                &bt2020_profile,
1565                Layout::Rgb,
1566                TransformOptions::default(),
1567            )
1568            .unwrap();
1569        let mut src = vec![0u16; 256 * 256 * 3];
1570        for dst in src.chunks_exact_mut(3) {
1571            dst[0] = 1760;
1572            dst[1] = 2560;
1573            dst[2] = 5120;
1574        }
1575        let mut dst = vec![0u16; 256 * 256 * 3];
1576        transform.transform(&src, &mut dst).unwrap();
1577
1578        let transform_inverse = bt2020_profile
1579            .create_transform_16bit(
1580                Layout::Rgb,
1581                &srgb_profile,
1582                Layout::Rgb,
1583                TransformOptions::default(),
1584            )
1585            .unwrap();
1586
1587        transform_inverse.transform(&dst, &mut src).unwrap();
1588
1589        for src in src.chunks_exact_mut(3) {
1590            let diff0 = (src[0] as i32 - 1760).abs();
1591            let diff1 = (src[1] as i32 - 2560).abs();
1592            let diff2 = (src[2] as i32 - 5120).abs();
1593            assert!(
1594                diff0 < 35,
1595                "On channel 0 difference should be less than 35, but it was {diff0}"
1596            );
1597            assert!(
1598                diff1 < 35,
1599                "On channel 1 difference should be less than 35, but it was {diff1}"
1600            );
1601            assert!(
1602                diff2 < 35,
1603                "On channel 2 difference should be less than 35, but it was {diff2}"
1604            );
1605        }
1606    }
1607
1608    #[test]
1609    #[cfg(feature = "extended_range")]
1610    fn test_transform_rgb_to_gray_extended() {
1611        let srgb = ColorProfile::new_srgb();
1612        let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0);
1613        gray_profile.color_space = DataColorSpace::Gray;
1614        gray_profile.gray_trc = srgb.red_trc.clone();
1615        let mut test_profile = vec![0.; 4];
1616        test_profile[2] = 1.;
1617        let mut dst = vec![0.; 1];
1618
1619        let mut inverse = vec![0.; 4];
1620
1621        let cvt0 = srgb
1622            .create_transform_f32(
1623                Layout::Rgba,
1624                &gray_profile,
1625                Layout::Gray,
1626                TransformOptions {
1627                    allow_extended_range_rgb_xyz: true,
1628                    ..Default::default()
1629                },
1630            )
1631            .unwrap();
1632        cvt0.transform(&test_profile, &mut dst).unwrap();
1633        assert!((dst[0] - 0.273046) < 1e-4);
1634
1635        let cvt_inverse = gray_profile
1636            .create_transform_f32(
1637                Layout::Gray,
1638                &srgb,
1639                Layout::Rgba,
1640                TransformOptions {
1641                    allow_extended_range_rgb_xyz: false,
1642                    ..Default::default()
1643                },
1644            )
1645            .unwrap();
1646        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1647        assert!((inverse[0] - 0.273002833) < 1e-4);
1648
1649        let cvt1 = srgb
1650            .create_transform_f32(
1651                Layout::Rgba,
1652                &gray_profile,
1653                Layout::Gray,
1654                TransformOptions {
1655                    allow_extended_range_rgb_xyz: false,
1656                    ..Default::default()
1657                },
1658            )
1659            .unwrap();
1660        cvt1.transform(&test_profile, &mut dst).unwrap();
1661        assert!((dst[0] - 0.27307168) < 1e-5);
1662
1663        inverse.fill(0.);
1664
1665        let cvt_inverse = gray_profile
1666            .create_transform_f32(
1667                Layout::Gray,
1668                &srgb,
1669                Layout::Rgba,
1670                TransformOptions {
1671                    allow_extended_range_rgb_xyz: true,
1672                    ..Default::default()
1673                },
1674            )
1675            .unwrap();
1676        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1677        assert!((inverse[0] - 0.273002833) < 1e-4);
1678    }
1679
1680    /// Test that multi-pixel RGB transforms produce consistent results for each pixel.
1681    /// This catches bugs where SIMD implementations incorrectly share data between pixels.
1682    /// Specifically tests the case where even/odd pixels should have independent blue channels.
1683    #[test]
1684    fn test_transform_rgb8_pixel_independence() {
1685        let srgb_profile = ColorProfile::new_srgb();
1686        let bt2020_profile = ColorProfile::new_bt2020();
1687
1688        let transform = bt2020_profile
1689            .create_transform_8bit(
1690                Layout::Rgb,
1691                &srgb_profile,
1692                Layout::Rgb,
1693                TransformOptions {
1694                    prefer_fixed_point: true,
1695                    ..Default::default()
1696                },
1697            )
1698            .unwrap();
1699
1700        // Create 4 pixels with distinct colors - specifically testing that
1701        // even/odd pixels don't share blue channel values
1702        // Pixel 0: Red (255, 0, 0)
1703        // Pixel 1: Green (0, 255, 0)
1704        // Pixel 2: Blue (0, 0, 255)
1705        // Pixel 3: Yellow (255, 255, 0)
1706        let src: Vec<u8> = vec![
1707            255, 0, 0, // Pixel 0: Red
1708            0, 255, 0, // Pixel 1: Green
1709            0, 0, 255, // Pixel 2: Blue
1710            255, 255, 0, // Pixel 3: Yellow
1711        ];
1712        let mut dst = vec![0u8; 12];
1713        transform.transform(&src, &mut dst).unwrap();
1714
1715        // Process same pixels individually for comparison
1716        let mut single_pixel_results = Vec::new();
1717        for pixel in src.chunks(3) {
1718            let mut single_dst = vec![0u8; 3];
1719            transform.transform(pixel, &mut single_dst).unwrap();
1720            single_pixel_results.extend(single_dst);
1721        }
1722
1723        // Each pixel in batch processing should match single-pixel processing
1724        // This catches the vr0/vr1 bug where pixel 1's blue channel would get pixel 0's value
1725        for (i, (batch, single)) in dst.iter().zip(single_pixel_results.iter()).enumerate() {
1726            assert_eq!(
1727                batch,
1728                single,
1729                "Mismatch at byte {} (pixel {}, channel {}): batch={}, single={}",
1730                i,
1731                i / 3,
1732                i % 3,
1733                batch,
1734                single
1735            );
1736        }
1737    }
1738}