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::{
30    LutBarycentricReduction, RgbXyzFactory, RgbXyzFactoryOpt, ToneReproductionRgbToGray,
31    TransformMatrixShaper, make_gray_to_unfused, make_gray_to_x, make_lut_transform,
32    make_rgb_to_gray,
33};
34use crate::err::CmsError;
35use crate::trc::GammaLutInterpolate;
36use crate::{ColorProfile, DataColorSpace, LutWarehouse, RenderingIntent, Vector3f, Xyzd};
37use num_traits::AsPrimitive;
38use std::marker::PhantomData;
39
40/// Transformation executor itself
41pub trait TransformExecutor<V: Copy + Default> {
42    /// Count of samples always must match.
43    /// If there is N samples of *Cmyk* source then N samples of *Rgb* is expected as an output.
44    fn transform(&self, src: &[V], dst: &mut [V]) -> Result<(), CmsError>;
45}
46
47/// Helper for intermediate transformation stages
48pub trait Stage {
49    fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError>;
50}
51
52/// Helper for intermediate transformation stages
53pub trait InPlaceStage {
54    fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError>;
55}
56
57/// Barycentric interpolation weights size.
58///
59/// Bigger weights increases precision.
60#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
61pub enum BarycentricWeightScale {
62    #[default]
63    /// Low scale weights is enough for common case.
64    ///
65    /// However, it might crush dark zones and gradients.
66    /// Weights increasing costs 5% performance.
67    Low,
68    #[cfg(feature = "options")]
69    High,
70}
71
72/// Declares additional transformation options
73#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
74pub struct TransformOptions {
75    pub rendering_intent: RenderingIntent,
76    /// If set it will try to use Transfer Characteristics from CICP
77    /// on transform. This might be more precise and faster.
78    pub allow_use_cicp_transfer: bool,
79    /// Prefers fixed point where implemented as default.
80    /// Most of the applications actually do not need floating point.
81    ///
82    /// Do not change it if you're not sure that extreme precision is required,
83    /// in most cases it is a simple way to spend energy to warming up environment
84    /// a little.
85    ///
86    /// Q2.13 for RGB->XYZ->RGB is used.
87    /// LUT interpolation use Q0.15.
88    pub prefer_fixed_point: bool,
89    /// Interpolation method for 3D LUT
90    ///
91    /// This parameter has no effect on LAB/XYZ interpolation and scene linear RGB.
92    ///
93    /// Technically, it should be assumed to perform cube dividing interpolation:
94    /// - Source colorspace is gamma-encoded (discards scene linear RGB and XYZ).
95    /// - Colorspace is uniform.
96    /// - Colorspace has linear scaling (discards LAB).
97    /// - Interpolation doesn't shift hues (discards LAB).
98    ///
99    /// For LAB, XYZ and scene linear RGB `trilinear/quadlinear` always in force.
100    pub interpolation_method: InterpolationMethod,
101    /// Barycentric weights scale.
102    ///
103    /// This value controls LUT weights precision.
104    pub barycentric_weight_scale: BarycentricWeightScale,
105    /// For floating points transform, it will try to detect gamma function on *Matrix Shaper* profiles.
106    /// If gamma function is found, then it will be used instead of LUT table.
107    /// This allows to work with excellent precision with extended range,
108    /// at a cost of execution time.
109    pub allow_extended_range_rgb_xyz: bool,
110    // pub black_point_compensation: bool,
111}
112
113#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
114/// Defines the interpolation method.
115///
116/// All methods produce very close results that almost not possible to separate without
117/// some automation tools.
118///
119/// This implementation chooses the fastest method as default.
120pub enum InterpolationMethod {
121    /// General Tetrahedron interpolation.
122    /// This is used in lcms2 and others CMS.
123    #[cfg(feature = "options")]
124    Tetrahedral,
125    /// Divides cube into a pyramids and interpolate then in the pyramid.
126    #[cfg(feature = "options")]
127    Pyramid,
128    /// Interpolation by dividing cube into prisms.
129    #[cfg(feature = "options")]
130    Prism,
131    /// Trilinear/Quadlinear interpolation
132    #[default]
133    Linear,
134}
135
136impl Default for TransformOptions {
137    fn default() -> Self {
138        Self {
139            rendering_intent: RenderingIntent::default(),
140            allow_use_cicp_transfer: true,
141            prefer_fixed_point: true,
142            interpolation_method: InterpolationMethod::default(),
143            barycentric_weight_scale: BarycentricWeightScale::default(),
144            allow_extended_range_rgb_xyz: false,
145            // black_point_compensation: false,
146        }
147    }
148}
149
150pub type Transform8BitExecutor = dyn TransformExecutor<u8> + Send + Sync;
151pub type Transform16BitExecutor = dyn TransformExecutor<u16> + Send + Sync;
152pub type TransformF32BitExecutor = dyn TransformExecutor<f32> + Send + Sync;
153pub type TransformF64BitExecutor = dyn TransformExecutor<f64> + Send + Sync;
154
155/// Layout declares a data layout.
156/// For RGB it shows also the channel order.
157/// To handle different data bit-depth appropriate executor must be used.
158/// Cmyk8 uses the same layout as Rgba8.
159#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
160pub enum Layout {
161    Rgb = 0,
162    Rgba = 1,
163    Gray = 2,
164    GrayAlpha = 3,
165    Inks5 = 4,
166    Inks6 = 5,
167    Inks7 = 6,
168    Inks8 = 7,
169    Inks9 = 8,
170    Inks10 = 9,
171    Inks11 = 10,
172    Inks12 = 11,
173    Inks13 = 12,
174    Inks14 = 13,
175    Inks15 = 14,
176}
177
178impl Layout {
179    /// Returns Red channel index
180    #[inline(always)]
181    pub const fn r_i(self) -> usize {
182        match self {
183            Layout::Rgb => 0,
184            Layout::Rgba => 0,
185            Layout::Gray => unimplemented!(),
186            Layout::GrayAlpha => unimplemented!(),
187            _ => unimplemented!(),
188        }
189    }
190
191    /// Returns Green channel index
192    #[inline(always)]
193    pub const fn g_i(self) -> usize {
194        match self {
195            Layout::Rgb => 1,
196            Layout::Rgba => 1,
197            Layout::Gray => unimplemented!(),
198            Layout::GrayAlpha => unimplemented!(),
199            _ => unimplemented!(),
200        }
201    }
202
203    /// Returns Blue channel index
204    #[inline(always)]
205    pub const fn b_i(self) -> usize {
206        match self {
207            Layout::Rgb => 2,
208            Layout::Rgba => 2,
209            Layout::Gray => unimplemented!(),
210            Layout::GrayAlpha => unimplemented!(),
211            _ => unimplemented!(),
212        }
213    }
214
215    #[inline(always)]
216    pub const fn a_i(self) -> usize {
217        match self {
218            Layout::Rgb => unimplemented!(),
219            Layout::Rgba => 3,
220            Layout::Gray => unimplemented!(),
221            Layout::GrayAlpha => 1,
222            _ => unimplemented!(),
223        }
224    }
225
226    #[inline(always)]
227    pub const fn has_alpha(self) -> bool {
228        match self {
229            Layout::Rgb => false,
230            Layout::Rgba => true,
231            Layout::Gray => false,
232            Layout::GrayAlpha => true,
233            _ => false,
234        }
235    }
236
237    #[inline]
238    pub const fn channels(self) -> usize {
239        match self {
240            Layout::Rgb => 3,
241            Layout::Rgba => 4,
242            Layout::Gray => 1,
243            Layout::GrayAlpha => 2,
244            Layout::Inks5 => 5,
245            Layout::Inks6 => 6,
246            Layout::Inks7 => 7,
247            Layout::Inks8 => 8,
248            Layout::Inks9 => 9,
249            Layout::Inks10 => 10,
250            Layout::Inks11 => 11,
251            Layout::Inks12 => 12,
252            Layout::Inks13 => 13,
253            Layout::Inks14 => 14,
254            Layout::Inks15 => 15,
255        }
256    }
257
258    pub(crate) fn from_inks(inks: usize) -> Self {
259        match inks {
260            1 => Layout::Gray,
261            2 => Layout::GrayAlpha,
262            3 => Layout::Rgb,
263            4 => Layout::Rgba,
264            5 => Layout::Inks5,
265            6 => Layout::Inks6,
266            7 => Layout::Inks7,
267            8 => Layout::Inks8,
268            9 => Layout::Inks9,
269            10 => Layout::Inks10,
270            11 => Layout::Inks11,
271            12 => Layout::Inks12,
272            13 => Layout::Inks13,
273            14 => Layout::Inks14,
274            15 => Layout::Inks15,
275            _ => unreachable!("Impossible amount of inks"),
276        }
277    }
278}
279
280impl From<u8> for Layout {
281    fn from(value: u8) -> Self {
282        match value {
283            0 => Layout::Rgb,
284            1 => Layout::Rgba,
285            2 => Layout::Gray,
286            3 => Layout::GrayAlpha,
287            _ => unimplemented!(),
288        }
289    }
290}
291
292impl Layout {
293    #[inline(always)]
294    pub const fn resolve(value: u8) -> Self {
295        match value {
296            0 => Layout::Rgb,
297            1 => Layout::Rgba,
298            2 => Layout::Gray,
299            3 => Layout::GrayAlpha,
300            4 => Layout::Inks5,
301            5 => Layout::Inks6,
302            6 => Layout::Inks7,
303            7 => Layout::Inks8,
304            8 => Layout::Inks9,
305            9 => Layout::Inks10,
306            10 => Layout::Inks11,
307            11 => Layout::Inks12,
308            12 => Layout::Inks13,
309            13 => Layout::Inks14,
310            14 => Layout::Inks15,
311            _ => unimplemented!(),
312        }
313    }
314}
315
316#[doc(hidden)]
317pub trait PointeeSizeExpressible {
318    fn _as_usize(self) -> usize;
319    const FINITE: bool;
320    const NOT_FINITE_GAMMA_TABLE_SIZE: usize;
321    const NOT_FINITE_LINEAR_TABLE_SIZE: usize;
322    const IS_U8: bool;
323    const IS_U16: bool;
324}
325
326impl PointeeSizeExpressible for u8 {
327    #[inline(always)]
328    fn _as_usize(self) -> usize {
329        self as usize
330    }
331
332    const FINITE: bool = true;
333    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
334    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
335    const IS_U8: bool = true;
336    const IS_U16: bool = false;
337}
338
339impl PointeeSizeExpressible for u16 {
340    #[inline(always)]
341    fn _as_usize(self) -> usize {
342        self as usize
343    }
344
345    const FINITE: bool = true;
346
347    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
348    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
349
350    const IS_U8: bool = false;
351    const IS_U16: bool = true;
352}
353
354impl PointeeSizeExpressible for f32 {
355    #[inline(always)]
356    fn _as_usize(self) -> usize {
357        const MAX_14_BIT: f32 = ((1 << 14u32) - 1) as f32;
358        ((self * MAX_14_BIT).max(0f32).min(MAX_14_BIT) as u16) as usize
359    }
360
361    const FINITE: bool = false;
362
363    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 32768;
364    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 14u32;
365    const IS_U8: bool = false;
366    const IS_U16: bool = false;
367}
368
369impl PointeeSizeExpressible for f64 {
370    #[inline(always)]
371    fn _as_usize(self) -> usize {
372        const MAX_16_BIT: f64 = ((1 << 16u32) - 1) as f64;
373        ((self * MAX_16_BIT).max(0.).min(MAX_16_BIT) as u16) as usize
374    }
375
376    const FINITE: bool = false;
377
378    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 65536;
379    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 16;
380    const IS_U8: bool = false;
381    const IS_U16: bool = false;
382}
383
384impl ColorProfile {
385    /// Checks if profile is valid *Matrix Shaper* profile
386    pub fn is_matrix_shaper(&self) -> bool {
387        self.color_space == DataColorSpace::Rgb
388            && self.red_colorant != Xyzd::default()
389            && self.green_colorant != Xyzd::default()
390            && self.blue_colorant != Xyzd::default()
391            && self.red_trc.is_some()
392            && self.green_trc.is_some()
393            && self.blue_trc.is_some()
394    }
395
396    /// Creates transform between source and destination profile
397    /// Use for 16 bit-depth data bit-depth only.
398    pub fn create_transform_16bit(
399        &self,
400        src_layout: Layout,
401        dst_pr: &ColorProfile,
402        dst_layout: Layout,
403        options: TransformOptions,
404    ) -> Result<Box<Transform16BitExecutor>, CmsError> {
405        self.create_transform_nbit::<u16, 16, 65536, 65536>(src_layout, dst_pr, dst_layout, options)
406    }
407
408    /// Creates transform between source and destination profile
409    /// Use for 12 bit-depth data bit-depth only.
410    pub fn create_transform_12bit(
411        &self,
412        src_layout: Layout,
413        dst_pr: &ColorProfile,
414        dst_layout: Layout,
415        options: TransformOptions,
416    ) -> Result<Box<Transform16BitExecutor>, CmsError> {
417        self.create_transform_nbit::<u16, 12, 65536, 16384>(src_layout, dst_pr, dst_layout, options)
418    }
419
420    /// Creates transform between source and destination profile
421    /// Use for 10 bit-depth data bit-depth only.
422    pub fn create_transform_10bit(
423        &self,
424        src_layout: Layout,
425        dst_pr: &ColorProfile,
426        dst_layout: Layout,
427        options: TransformOptions,
428    ) -> Result<Box<Transform16BitExecutor>, CmsError> {
429        self.create_transform_nbit::<u16, 10, 65536, 8192>(src_layout, dst_pr, dst_layout, options)
430    }
431
432    /// Creates transform between source and destination profile
433    /// Data has to be normalized into [0, 1] range.
434    /// ICC profiles and LUT tables do not exist in infinite precision.
435    /// Thus, this implementation considers `f32` as 14-bit values.
436    /// Floating point transformer works in extended mode, that means returned data might be negative
437    /// or more than 1.
438    pub fn create_transform_f32(
439        &self,
440        src_layout: Layout,
441        dst_pr: &ColorProfile,
442        dst_layout: Layout,
443        options: TransformOptions,
444    ) -> Result<Box<TransformF32BitExecutor>, CmsError> {
445        self.create_transform_nbit::<f32, 1, 65536, 32768>(src_layout, dst_pr, dst_layout, options)
446    }
447
448    /// Creates transform between source and destination profile
449    /// Data has to be normalized into [0, 1] range.
450    /// ICC profiles and LUT tables do not exist in infinite precision.
451    /// Thus, this implementation considers `f64` as 16-bit values.
452    /// Floating point transformer works in extended mode, that means returned data might be negative
453    /// or more than 1.
454    pub fn create_transform_f64(
455        &self,
456        src_layout: Layout,
457        dst_pr: &ColorProfile,
458        dst_layout: Layout,
459        options: TransformOptions,
460    ) -> Result<Box<TransformF64BitExecutor>, CmsError> {
461        self.create_transform_nbit::<f64, 1, 65536, 65536>(src_layout, dst_pr, dst_layout, options)
462    }
463
464    fn create_transform_nbit<
465        T: Copy
466            + Default
467            + AsPrimitive<usize>
468            + PointeeSizeExpressible
469            + Send
470            + Sync
471            + AsPrimitive<f32>
472            + RgbXyzFactory<T>
473            + RgbXyzFactoryOpt<T>
474            + GammaLutInterpolate,
475        const BIT_DEPTH: usize,
476        const LINEAR_CAP: usize,
477        const GAMMA_CAP: usize,
478    >(
479        &self,
480        src_layout: Layout,
481        dst_pr: &ColorProfile,
482        dst_layout: Layout,
483        options: TransformOptions,
484    ) -> Result<Box<dyn TransformExecutor<T> + Send + Sync>, CmsError>
485    where
486        f32: AsPrimitive<T>,
487        u32: AsPrimitive<T>,
488        (): LutBarycentricReduction<T, u8>,
489        (): LutBarycentricReduction<T, u16>,
490    {
491        if self.color_space == DataColorSpace::Rgb
492            && dst_pr.pcs == DataColorSpace::Xyz
493            && dst_pr.color_space == DataColorSpace::Rgb
494            && self.pcs == DataColorSpace::Xyz
495            && self.is_matrix_shaper()
496            && dst_pr.is_matrix_shaper()
497        {
498            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
499                return Err(CmsError::InvalidLayout);
500            }
501            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
502                return Err(CmsError::InvalidLayout);
503            }
504
505            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
506                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
507                    src_layout, self, dst_layout, dst_pr, options,
508                );
509            }
510
511            let transform = self.transform_matrix(dst_pr);
512
513            if !T::FINITE && options.allow_extended_range_rgb_xyz {
514                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
515                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
516                        use crate::conversions::{
517                            TransformShaperFloatInOut, make_rgb_xyz_rgb_transform_float_in_out,
518                        };
519                        let p = TransformShaperFloatInOut {
520                            linear_evaluator,
521                            gamma_evaluator,
522                            adaptation_matrix: transform.to_f32(),
523                            phantom_data: PhantomData,
524                        };
525                        return make_rgb_xyz_rgb_transform_float_in_out::<T>(
526                            src_layout, dst_layout, p, BIT_DEPTH,
527                        );
528                    }
529
530                    let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
531                        options.allow_use_cicp_transfer,
532                    )?;
533                    let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
534                        options.allow_use_cicp_transfer,
535                    )?;
536                    let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
537                        options.allow_use_cicp_transfer,
538                    )?;
539
540                    use crate::conversions::{
541                        TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float,
542                    };
543                    let p = TransformShaperRgbFloat {
544                        r_linear: lin_r,
545                        g_linear: lin_g,
546                        b_linear: lin_b,
547                        gamma_evaluator,
548                        adaptation_matrix: transform.to_f32(),
549                        phantom_data: PhantomData,
550                    };
551                    return make_rgb_xyz_rgb_transform_float::<T, LINEAR_CAP>(
552                        src_layout, dst_layout, p, BIT_DEPTH,
553                    );
554                }
555            }
556
557            if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
558                let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
559                    options.allow_use_cicp_transfer,
560                )?;
561
562                let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
563                    &dst_pr.red_trc,
564                    options.allow_use_cicp_transfer,
565                )?;
566
567                let profile_transform = crate::conversions::TransformMatrixShaperOptimized {
568                    linear,
569                    gamma,
570                    adaptation_matrix: transform.to_f32(),
571                };
572
573                return T::make_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
574                    src_layout,
575                    dst_layout,
576                    profile_transform,
577                    options,
578                );
579            }
580
581            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
582                options.allow_use_cicp_transfer,
583            )?;
584            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
585                options.allow_use_cicp_transfer,
586            )?;
587            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
588                options.allow_use_cicp_transfer,
589            )?;
590
591            let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
592                &dst_pr.red_trc,
593                options.allow_use_cicp_transfer,
594            )?;
595            let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
596                &dst_pr.green_trc,
597                options.allow_use_cicp_transfer,
598            )?;
599            let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
600                &dst_pr.blue_trc,
601                options.allow_use_cicp_transfer,
602            )?;
603
604            let profile_transform = TransformMatrixShaper {
605                r_linear: lin_r,
606                g_linear: lin_g,
607                b_linear: lin_b,
608                r_gamma: gamma_r,
609                g_gamma: gamma_g,
610                b_gamma: gamma_b,
611                adaptation_matrix: transform.to_f32(),
612            };
613
614            T::make_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
615                src_layout,
616                dst_layout,
617                profile_transform,
618                options,
619            )
620        } else if (self.color_space == DataColorSpace::Gray && self.gray_trc.is_some())
621            && (dst_pr.color_space == DataColorSpace::Rgb
622                || (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some()))
623            && self.pcs == DataColorSpace::Xyz
624            && dst_pr.pcs == DataColorSpace::Xyz
625        {
626            if src_layout != Layout::GrayAlpha && src_layout != Layout::Gray {
627                return Err(CmsError::InvalidLayout);
628            }
629
630            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
631                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
632                    src_layout, self, dst_layout, dst_pr, options,
633                );
634            }
635
636            let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
637
638            if dst_pr.color_space == DataColorSpace::Gray {
639                if !T::FINITE && options.allow_extended_range_rgb_xyz {
640                    if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
641                        if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
642                            // Gray -> Gray case extended range
643                            use crate::conversions::make_gray_to_one_trc_extended;
644                            return make_gray_to_one_trc_extended::<T>(
645                                src_layout,
646                                dst_layout,
647                                linear_evaluator,
648                                gamma_evaluator,
649                                BIT_DEPTH,
650                            );
651                        }
652                    }
653                }
654
655                // Gray -> Gray case
656                let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
657                    &dst_pr.gray_trc,
658                    options.allow_use_cicp_transfer,
659                )?;
660
661                make_gray_to_x::<T, LINEAR_CAP>(
662                    src_layout,
663                    dst_layout,
664                    &gray_linear,
665                    &gray_gamma,
666                    BIT_DEPTH,
667                    GAMMA_CAP,
668                )
669            } else {
670                #[allow(clippy::collapsible_if)]
671                if dst_pr.are_all_trc_the_same() {
672                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
673                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
674                            if let Some(linear_evaluator) =
675                                self.try_extended_linearizing_evaluator()
676                            {
677                                // Gray -> RGB where all TRC is the same with extended range
678                                use crate::conversions::make_gray_to_one_trc_extended;
679                                return make_gray_to_one_trc_extended::<T>(
680                                    src_layout,
681                                    dst_layout,
682                                    linear_evaluator,
683                                    gamma_evaluator,
684                                    BIT_DEPTH,
685                                );
686                            }
687                        }
688                    }
689
690                    // Gray -> RGB where all TRC is the same
691                    let rgb_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
692                        &dst_pr.red_trc,
693                        options.allow_use_cicp_transfer,
694                    )?;
695
696                    make_gray_to_x::<T, LINEAR_CAP>(
697                        src_layout,
698                        dst_layout,
699                        &gray_linear,
700                        &rgb_gamma,
701                        BIT_DEPTH,
702                        GAMMA_CAP,
703                    )
704                } else {
705                    // Gray -> RGB where all TRC is NOT the same
706                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
707                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
708                            if let Some(linear_evaluator) =
709                                self.try_extended_linearizing_evaluator()
710                            {
711                                // Gray -> RGB where all TRC is NOT the same with extended range
712
713                                use crate::conversions::make_gray_to_rgb_extended;
714                                return make_gray_to_rgb_extended::<T>(
715                                    src_layout,
716                                    dst_layout,
717                                    linear_evaluator,
718                                    gamma_evaluator,
719                                    BIT_DEPTH,
720                                );
721                            }
722                        }
723                    }
724
725                    let red_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
726                        &dst_pr.red_trc,
727                        options.allow_use_cicp_transfer,
728                    )?;
729                    let green_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
730                        &dst_pr.green_trc,
731                        options.allow_use_cicp_transfer,
732                    )?;
733                    let blue_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
734                        &dst_pr.blue_trc,
735                        options.allow_use_cicp_transfer,
736                    )?;
737
738                    let mut gray_linear2 = Box::new([0f32; 65536]);
739                    for (dst, src) in gray_linear2.iter_mut().zip(gray_linear.iter()) {
740                        *dst = *src;
741                    }
742
743                    make_gray_to_unfused::<T, LINEAR_CAP>(
744                        src_layout,
745                        dst_layout,
746                        gray_linear2,
747                        red_gamma,
748                        green_gamma,
749                        blue_gamma,
750                        BIT_DEPTH,
751                        GAMMA_CAP,
752                    )
753                }
754            }
755        } else if self.color_space == DataColorSpace::Rgb
756            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
757            && dst_pr.pcs == DataColorSpace::Xyz
758            && self.pcs == DataColorSpace::Xyz
759        {
760            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
761                return Err(CmsError::InvalidLayout);
762            }
763            if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha {
764                return Err(CmsError::InvalidLayout);
765            }
766
767            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
768                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
769                    src_layout, self, dst_layout, dst_pr, options,
770                );
771            }
772
773            let transform = self.transform_matrix(dst_pr).to_f32();
774
775            let vector = Vector3f {
776                v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]],
777            };
778
779            if !T::FINITE && options.allow_extended_range_rgb_xyz {
780                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
781                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
782                        use crate::conversions::make_rgb_to_gray_extended;
783                        return Ok(make_rgb_to_gray_extended::<T>(
784                            src_layout,
785                            dst_layout,
786                            linear_evaluator,
787                            gamma_evaluator,
788                            vector,
789                            BIT_DEPTH,
790                        ));
791                    }
792                }
793            }
794
795            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
796                options.allow_use_cicp_transfer,
797            )?;
798            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
799                options.allow_use_cicp_transfer,
800            )?;
801            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
802                options.allow_use_cicp_transfer,
803            )?;
804            let gray_linear = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
805                &dst_pr.gray_trc,
806                options.allow_use_cicp_transfer,
807            )?;
808
809            let trc_box = ToneReproductionRgbToGray::<T, LINEAR_CAP> {
810                r_linear: lin_r,
811                g_linear: lin_g,
812                b_linear: lin_b,
813                gray_gamma: gray_linear,
814            };
815
816            Ok(make_rgb_to_gray::<T, LINEAR_CAP>(
817                src_layout, dst_layout, trc_box, vector, GAMMA_CAP, BIT_DEPTH,
818            ))
819        } else if (self.color_space.is_three_channels()
820            || self.color_space == DataColorSpace::Cmyk
821            || self.color_space == DataColorSpace::Color4)
822            && (dst_pr.color_space.is_three_channels()
823                || dst_pr.color_space == DataColorSpace::Cmyk
824                || dst_pr.color_space == DataColorSpace::Color4)
825            && (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab)
826            && (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab)
827        {
828            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
829                return Err(CmsError::InvalidLayout);
830            }
831            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
832                return Err(CmsError::InvalidLayout);
833            }
834            make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
835                src_layout, self, dst_layout, dst_pr, options,
836            )
837        } else {
838            make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
839                src_layout, self, dst_layout, dst_pr, options,
840            )
841        }
842    }
843
844    /// Creates transform between source and destination profile
845    /// Only 8 bit is supported.
846    pub fn create_transform_8bit(
847        &self,
848        src_layout: Layout,
849        dst_pr: &ColorProfile,
850        dst_layout: Layout,
851        options: TransformOptions,
852    ) -> Result<Box<Transform8BitExecutor>, CmsError> {
853        self.create_transform_nbit::<u8, 8, 256, 4096>(src_layout, dst_pr, dst_layout, options)
854    }
855
856    pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
857        match intent {
858            RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
859            RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(),
860            RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
861            RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(),
862        }
863    }
864
865    pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
866        match intent {
867            RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
868            RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(),
869            RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
870            RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(),
871        }
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use crate::{ColorProfile, DataColorSpace, Layout, RenderingIntent, TransformOptions};
878    use rand::Rng;
879
880    #[test]
881    fn test_transform_rgb8() {
882        let mut srgb_profile = ColorProfile::new_srgb();
883        let bt2020_profile = ColorProfile::new_bt2020();
884        let random_point_x = rand::rng().random_range(0..255);
885        let transform = bt2020_profile
886            .create_transform_8bit(
887                Layout::Rgb,
888                &srgb_profile,
889                Layout::Rgb,
890                TransformOptions::default(),
891            )
892            .unwrap();
893        let src = vec![random_point_x; 256 * 256 * 3];
894        let mut dst = vec![random_point_x; 256 * 256 * 3];
895        transform.transform(&src, &mut dst).unwrap();
896
897        let transform = bt2020_profile
898            .create_transform_8bit(
899                Layout::Rgb,
900                &srgb_profile,
901                Layout::Rgb,
902                TransformOptions {
903                    ..TransformOptions::default()
904                },
905            )
906            .unwrap();
907        transform.transform(&src, &mut dst).unwrap();
908        srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric;
909        let transform = bt2020_profile
910            .create_transform_8bit(
911                Layout::Rgb,
912                &srgb_profile,
913                Layout::Rgb,
914                TransformOptions {
915                    ..TransformOptions::default()
916                },
917            )
918            .unwrap();
919        transform.transform(&src, &mut dst).unwrap();
920        srgb_profile.rendering_intent = RenderingIntent::Saturation;
921        let transform = bt2020_profile
922            .create_transform_8bit(
923                Layout::Rgb,
924                &srgb_profile,
925                Layout::Rgb,
926                TransformOptions {
927                    ..TransformOptions::default()
928                },
929            )
930            .unwrap();
931        transform.transform(&src, &mut dst).unwrap();
932    }
933
934    #[test]
935    fn test_transform_rgba8() {
936        let srgb_profile = ColorProfile::new_srgb();
937        let bt2020_profile = ColorProfile::new_bt2020();
938        let random_point_x = rand::rng().random_range(0..255);
939        let transform = bt2020_profile
940            .create_transform_8bit(
941                Layout::Rgba,
942                &srgb_profile,
943                Layout::Rgba,
944                TransformOptions::default(),
945            )
946            .unwrap();
947        let src = vec![random_point_x; 256 * 256 * 4];
948        let mut dst = vec![random_point_x; 256 * 256 * 4];
949        transform.transform(&src, &mut dst).unwrap();
950    }
951
952    #[test]
953    fn test_transform_gray_to_rgb8() {
954        let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32);
955        let bt2020_profile = ColorProfile::new_bt2020();
956        let random_point_x = rand::rng().random_range(0..255);
957        let transform = gray_profile
958            .create_transform_8bit(
959                Layout::Gray,
960                &bt2020_profile,
961                Layout::Rgb,
962                TransformOptions::default(),
963            )
964            .unwrap();
965        let src = vec![random_point_x; 256 * 256];
966        let mut dst = vec![random_point_x; 256 * 256 * 3];
967        transform.transform(&src, &mut dst).unwrap();
968    }
969
970    #[test]
971    fn test_transform_gray_to_rgba8() {
972        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
973        let bt2020_profile = ColorProfile::new_bt2020();
974        let random_point_x = rand::rng().random_range(0..255);
975        let transform = srgb_profile
976            .create_transform_8bit(
977                Layout::Gray,
978                &bt2020_profile,
979                Layout::Rgba,
980                TransformOptions::default(),
981            )
982            .unwrap();
983        let src = vec![random_point_x; 256 * 256];
984        let mut dst = vec![random_point_x; 256 * 256 * 4];
985        transform.transform(&src, &mut dst).unwrap();
986    }
987
988    #[test]
989    fn test_transform_gray_to_gray_alpha8() {
990        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
991        let bt2020_profile = ColorProfile::new_bt2020();
992        let random_point_x = rand::rng().random_range(0..255);
993        let transform = srgb_profile
994            .create_transform_8bit(
995                Layout::Gray,
996                &bt2020_profile,
997                Layout::GrayAlpha,
998                TransformOptions::default(),
999            )
1000            .unwrap();
1001        let src = vec![random_point_x; 256 * 256];
1002        let mut dst = vec![random_point_x; 256 * 256 * 2];
1003        transform.transform(&src, &mut dst).unwrap();
1004    }
1005
1006    #[test]
1007    fn test_transform_rgb10() {
1008        let srgb_profile = ColorProfile::new_srgb();
1009        let bt2020_profile = ColorProfile::new_bt2020();
1010        let random_point_x = rand::rng().random_range(0..((1 << 10) - 1));
1011        let transform = bt2020_profile
1012            .create_transform_10bit(
1013                Layout::Rgb,
1014                &srgb_profile,
1015                Layout::Rgb,
1016                TransformOptions::default(),
1017            )
1018            .unwrap();
1019        let src = vec![random_point_x; 256 * 256 * 3];
1020        let mut dst = vec![random_point_x; 256 * 256 * 3];
1021        transform.transform(&src, &mut dst).unwrap();
1022    }
1023
1024    #[test]
1025    fn test_transform_rgb12() {
1026        let srgb_profile = ColorProfile::new_srgb();
1027        let bt2020_profile = ColorProfile::new_bt2020();
1028        let random_point_x = rand::rng().random_range(0..((1 << 12) - 1));
1029        let transform = bt2020_profile
1030            .create_transform_12bit(
1031                Layout::Rgb,
1032                &srgb_profile,
1033                Layout::Rgb,
1034                TransformOptions::default(),
1035            )
1036            .unwrap();
1037        let src = vec![random_point_x; 256 * 256 * 3];
1038        let mut dst = vec![random_point_x; 256 * 256 * 3];
1039        transform.transform(&src, &mut dst).unwrap();
1040    }
1041
1042    #[test]
1043    fn test_transform_rgb16() {
1044        let srgb_profile = ColorProfile::new_srgb();
1045        let bt2020_profile = ColorProfile::new_bt2020();
1046        let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16;
1047        let transform = bt2020_profile
1048            .create_transform_16bit(
1049                Layout::Rgb,
1050                &srgb_profile,
1051                Layout::Rgb,
1052                TransformOptions::default(),
1053            )
1054            .unwrap();
1055        let src = vec![random_point_x; 256 * 256 * 3];
1056        let mut dst = vec![random_point_x; 256 * 256 * 3];
1057        transform.transform(&src, &mut dst).unwrap();
1058    }
1059
1060    #[test]
1061    fn test_transform_round_trip_rgb8() {
1062        let srgb_profile = ColorProfile::new_srgb();
1063        let bt2020_profile = ColorProfile::new_bt2020();
1064        let transform = srgb_profile
1065            .create_transform_8bit(
1066                Layout::Rgb,
1067                &bt2020_profile,
1068                Layout::Rgb,
1069                TransformOptions::default(),
1070            )
1071            .unwrap();
1072        let mut src = vec![0u8; 256 * 256 * 3];
1073        for dst in src.chunks_exact_mut(3) {
1074            dst[0] = 175;
1075            dst[1] = 75;
1076            dst[2] = 13;
1077        }
1078        let mut dst = vec![0u8; 256 * 256 * 3];
1079        transform.transform(&src, &mut dst).unwrap();
1080
1081        let transform_inverse = bt2020_profile
1082            .create_transform_8bit(
1083                Layout::Rgb,
1084                &srgb_profile,
1085                Layout::Rgb,
1086                TransformOptions::default(),
1087            )
1088            .unwrap();
1089
1090        transform_inverse.transform(&dst, &mut src).unwrap();
1091
1092        for src in src.chunks_exact_mut(3) {
1093            let diff0 = (src[0] as i32 - 175).abs();
1094            let diff1 = (src[1] as i32 - 75).abs();
1095            let diff2 = (src[2] as i32 - 13).abs();
1096            assert!(
1097                diff0 < 3,
1098                "On channel 0 difference should be less than 3, but it was {diff0}"
1099            );
1100            assert!(
1101                diff1 < 3,
1102                "On channel 1 difference should be less than 3, but it was {diff1}"
1103            );
1104            assert!(
1105                diff2 < 3,
1106                "On channel 2 difference should be less than 3, but it was {diff2}"
1107            );
1108        }
1109    }
1110
1111    #[test]
1112    fn test_transform_round_trip_rgb10() {
1113        let srgb_profile = ColorProfile::new_srgb();
1114        let bt2020_profile = ColorProfile::new_bt2020();
1115        let transform = srgb_profile
1116            .create_transform_10bit(
1117                Layout::Rgb,
1118                &bt2020_profile,
1119                Layout::Rgb,
1120                TransformOptions::default(),
1121            )
1122            .unwrap();
1123        let mut src = vec![0u16; 256 * 256 * 3];
1124        for dst in src.chunks_exact_mut(3) {
1125            dst[0] = 175;
1126            dst[1] = 256;
1127            dst[2] = 512;
1128        }
1129        let mut dst = vec![0u16; 256 * 256 * 3];
1130        transform.transform(&src, &mut dst).unwrap();
1131
1132        let transform_inverse = bt2020_profile
1133            .create_transform_10bit(
1134                Layout::Rgb,
1135                &srgb_profile,
1136                Layout::Rgb,
1137                TransformOptions::default(),
1138            )
1139            .unwrap();
1140
1141        transform_inverse.transform(&dst, &mut src).unwrap();
1142
1143        for src in src.chunks_exact_mut(3) {
1144            let diff0 = (src[0] as i32 - 175).abs();
1145            let diff1 = (src[1] as i32 - 256).abs();
1146            let diff2 = (src[2] as i32 - 512).abs();
1147            assert!(
1148                diff0 < 15,
1149                "On channel 0 difference should be less than 15, but it was {diff0}"
1150            );
1151            assert!(
1152                diff1 < 15,
1153                "On channel 1 difference should be less than 15, but it was {diff1}"
1154            );
1155            assert!(
1156                diff2 < 15,
1157                "On channel 2 difference should be less than 15, but it was {diff2}"
1158            );
1159        }
1160    }
1161
1162    #[test]
1163    fn test_transform_round_trip_rgb12() {
1164        let srgb_profile = ColorProfile::new_srgb();
1165        let bt2020_profile = ColorProfile::new_bt2020();
1166        let transform = srgb_profile
1167            .create_transform_12bit(
1168                Layout::Rgb,
1169                &bt2020_profile,
1170                Layout::Rgb,
1171                TransformOptions::default(),
1172            )
1173            .unwrap();
1174        let mut src = vec![0u16; 256 * 256 * 3];
1175        for dst in src.chunks_exact_mut(3) {
1176            dst[0] = 1750;
1177            dst[1] = 2560;
1178            dst[2] = 3143;
1179        }
1180        let mut dst = vec![0u16; 256 * 256 * 3];
1181        transform.transform(&src, &mut dst).unwrap();
1182
1183        let transform_inverse = bt2020_profile
1184            .create_transform_12bit(
1185                Layout::Rgb,
1186                &srgb_profile,
1187                Layout::Rgb,
1188                TransformOptions::default(),
1189            )
1190            .unwrap();
1191
1192        transform_inverse.transform(&dst, &mut src).unwrap();
1193
1194        for src in src.chunks_exact_mut(3) {
1195            let diff0 = (src[0] as i32 - 1750).abs();
1196            let diff1 = (src[1] as i32 - 2560).abs();
1197            let diff2 = (src[2] as i32 - 3143).abs();
1198            assert!(
1199                diff0 < 25,
1200                "On channel 0 difference should be less than 25, but it was {diff0}"
1201            );
1202            assert!(
1203                diff1 < 25,
1204                "On channel 1 difference should be less than 25, but it was {diff1}"
1205            );
1206            assert!(
1207                diff2 < 25,
1208                "On channel 2 difference should be less than 25, but it was {diff2}"
1209            );
1210        }
1211    }
1212
1213    #[test]
1214    fn test_transform_round_trip_rgb16() {
1215        let srgb_profile = ColorProfile::new_srgb();
1216        let bt2020_profile = ColorProfile::new_bt2020();
1217        let transform = srgb_profile
1218            .create_transform_16bit(
1219                Layout::Rgb,
1220                &bt2020_profile,
1221                Layout::Rgb,
1222                TransformOptions::default(),
1223            )
1224            .unwrap();
1225        let mut src = vec![0u16; 256 * 256 * 3];
1226        for dst in src.chunks_exact_mut(3) {
1227            dst[0] = 1760;
1228            dst[1] = 2560;
1229            dst[2] = 5120;
1230        }
1231        let mut dst = vec![0u16; 256 * 256 * 3];
1232        transform.transform(&src, &mut dst).unwrap();
1233
1234        let transform_inverse = bt2020_profile
1235            .create_transform_16bit(
1236                Layout::Rgb,
1237                &srgb_profile,
1238                Layout::Rgb,
1239                TransformOptions::default(),
1240            )
1241            .unwrap();
1242
1243        transform_inverse.transform(&dst, &mut src).unwrap();
1244
1245        for src in src.chunks_exact_mut(3) {
1246            let diff0 = (src[0] as i32 - 1760).abs();
1247            let diff1 = (src[1] as i32 - 2560).abs();
1248            let diff2 = (src[2] as i32 - 5120).abs();
1249            assert!(
1250                diff0 < 35,
1251                "On channel 0 difference should be less than 35, but it was {diff0}"
1252            );
1253            assert!(
1254                diff1 < 35,
1255                "On channel 1 difference should be less than 35, but it was {diff1}"
1256            );
1257            assert!(
1258                diff2 < 35,
1259                "On channel 2 difference should be less than 35, but it was {diff2}"
1260            );
1261        }
1262    }
1263
1264    #[test]
1265    fn test_transform_rgb_to_gray_extended() {
1266        let srgb = ColorProfile::new_srgb();
1267        let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0);
1268        gray_profile.color_space = DataColorSpace::Gray;
1269        gray_profile.gray_trc = srgb.red_trc.clone();
1270        let mut test_profile = vec![0.; 4];
1271        test_profile[2] = 1.;
1272        let mut dst = vec![0.; 1];
1273
1274        let mut inverse = vec![0.; 4];
1275
1276        let cvt0 = srgb
1277            .create_transform_f32(
1278                Layout::Rgba,
1279                &gray_profile,
1280                Layout::Gray,
1281                TransformOptions {
1282                    allow_extended_range_rgb_xyz: true,
1283                    ..Default::default()
1284                },
1285            )
1286            .unwrap();
1287        cvt0.transform(&test_profile, &mut dst).unwrap();
1288        assert!((dst[0] - 0.273046) < 1e-4);
1289
1290        let cvt_inverse = gray_profile
1291            .create_transform_f32(
1292                Layout::Gray,
1293                &srgb,
1294                Layout::Rgba,
1295                TransformOptions {
1296                    allow_extended_range_rgb_xyz: false,
1297                    ..Default::default()
1298                },
1299            )
1300            .unwrap();
1301        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1302        assert!((inverse[0] - 0.273002833) < 1e-4);
1303
1304        let cvt1 = srgb
1305            .create_transform_f32(
1306                Layout::Rgba,
1307                &gray_profile,
1308                Layout::Gray,
1309                TransformOptions {
1310                    allow_extended_range_rgb_xyz: false,
1311                    ..Default::default()
1312                },
1313            )
1314            .unwrap();
1315        cvt1.transform(&test_profile, &mut dst).unwrap();
1316        assert!((dst[0] - 0.27307168) < 1e-5);
1317
1318        inverse.fill(0.);
1319
1320        let cvt_inverse = gray_profile
1321            .create_transform_f32(
1322                Layout::Gray,
1323                &srgb,
1324                Layout::Rgba,
1325                TransformOptions {
1326                    allow_extended_range_rgb_xyz: true,
1327                    ..Default::default()
1328                },
1329            )
1330            .unwrap();
1331        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1332        assert!((inverse[0] - 0.273002833) < 1e-4);
1333    }
1334}