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, BIT_DEPTH>(
526                            src_layout, dst_layout, p,
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, BIT_DEPTH>(
552                        src_layout, dst_layout, p,
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                    make_gray_to_unfused::<T, LINEAR_CAP>(
739                        src_layout,
740                        dst_layout,
741                        gray_linear,
742                        red_gamma,
743                        green_gamma,
744                        blue_gamma,
745                        BIT_DEPTH,
746                        GAMMA_CAP,
747                    )
748                }
749            }
750        } else if self.color_space == DataColorSpace::Rgb
751            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
752            && dst_pr.pcs == DataColorSpace::Xyz
753            && self.pcs == DataColorSpace::Xyz
754        {
755            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
756                return Err(CmsError::InvalidLayout);
757            }
758            if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha {
759                return Err(CmsError::InvalidLayout);
760            }
761
762            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
763                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
764                    src_layout, self, dst_layout, dst_pr, options,
765                );
766            }
767
768            let transform = self.transform_matrix(dst_pr).to_f32();
769
770            let vector = Vector3f {
771                v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]],
772            };
773
774            if !T::FINITE && options.allow_extended_range_rgb_xyz {
775                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
776                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
777                        use crate::conversions::make_rgb_to_gray_extended;
778                        return Ok(make_rgb_to_gray_extended::<T>(
779                            src_layout,
780                            dst_layout,
781                            linear_evaluator,
782                            gamma_evaluator,
783                            vector,
784                            BIT_DEPTH,
785                        ));
786                    }
787                }
788            }
789
790            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
791                options.allow_use_cicp_transfer,
792            )?;
793            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
794                options.allow_use_cicp_transfer,
795            )?;
796            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
797                options.allow_use_cicp_transfer,
798            )?;
799            let gray_linear = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
800                &dst_pr.gray_trc,
801                options.allow_use_cicp_transfer,
802            )?;
803
804            let trc_box = ToneReproductionRgbToGray::<T, LINEAR_CAP> {
805                r_linear: lin_r,
806                g_linear: lin_g,
807                b_linear: lin_b,
808                gray_gamma: gray_linear,
809            };
810
811            Ok(make_rgb_to_gray::<T, LINEAR_CAP, BIT_DEPTH, GAMMA_CAP>(
812                src_layout, dst_layout, trc_box, vector,
813            ))
814        } else if (self.color_space.is_three_channels()
815            || self.color_space == DataColorSpace::Cmyk
816            || self.color_space == DataColorSpace::Color4)
817            && (dst_pr.color_space.is_three_channels()
818                || dst_pr.color_space == DataColorSpace::Cmyk
819                || dst_pr.color_space == DataColorSpace::Color4)
820            && (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab)
821            && (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab)
822        {
823            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
824                return Err(CmsError::InvalidLayout);
825            }
826            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
827                return Err(CmsError::InvalidLayout);
828            }
829            make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
830                src_layout, self, dst_layout, dst_pr, options,
831            )
832        } else {
833            make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
834                src_layout, self, dst_layout, dst_pr, options,
835            )
836        }
837    }
838
839    /// Creates transform between source and destination profile
840    /// Only 8 bit is supported.
841    pub fn create_transform_8bit(
842        &self,
843        src_layout: Layout,
844        dst_pr: &ColorProfile,
845        dst_layout: Layout,
846        options: TransformOptions,
847    ) -> Result<Box<Transform8BitExecutor>, CmsError> {
848        self.create_transform_nbit::<u8, 8, 256, 4096>(src_layout, dst_pr, dst_layout, options)
849    }
850
851    pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
852        match intent {
853            RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
854            RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(),
855            RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
856            RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(),
857        }
858    }
859
860    pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
861        match intent {
862            RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
863            RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(),
864            RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
865            RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(),
866        }
867    }
868}
869
870#[cfg(test)]
871mod tests {
872    use crate::{ColorProfile, DataColorSpace, Layout, RenderingIntent, TransformOptions};
873    use rand::Rng;
874
875    #[test]
876    fn test_transform_rgb8() {
877        let mut srgb_profile = ColorProfile::new_srgb();
878        let bt2020_profile = ColorProfile::new_bt2020();
879        let random_point_x = rand::rng().random_range(0..255);
880        let transform = bt2020_profile
881            .create_transform_8bit(
882                Layout::Rgb,
883                &srgb_profile,
884                Layout::Rgb,
885                TransformOptions::default(),
886            )
887            .unwrap();
888        let src = vec![random_point_x; 256 * 256 * 3];
889        let mut dst = vec![random_point_x; 256 * 256 * 3];
890        transform.transform(&src, &mut dst).unwrap();
891
892        let transform = bt2020_profile
893            .create_transform_8bit(
894                Layout::Rgb,
895                &srgb_profile,
896                Layout::Rgb,
897                TransformOptions {
898                    ..TransformOptions::default()
899                },
900            )
901            .unwrap();
902        transform.transform(&src, &mut dst).unwrap();
903        srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric;
904        let transform = bt2020_profile
905            .create_transform_8bit(
906                Layout::Rgb,
907                &srgb_profile,
908                Layout::Rgb,
909                TransformOptions {
910                    ..TransformOptions::default()
911                },
912            )
913            .unwrap();
914        transform.transform(&src, &mut dst).unwrap();
915        srgb_profile.rendering_intent = RenderingIntent::Saturation;
916        let transform = bt2020_profile
917            .create_transform_8bit(
918                Layout::Rgb,
919                &srgb_profile,
920                Layout::Rgb,
921                TransformOptions {
922                    ..TransformOptions::default()
923                },
924            )
925            .unwrap();
926        transform.transform(&src, &mut dst).unwrap();
927    }
928
929    #[test]
930    fn test_transform_rgba8() {
931        let srgb_profile = ColorProfile::new_srgb();
932        let bt2020_profile = ColorProfile::new_bt2020();
933        let random_point_x = rand::rng().random_range(0..255);
934        let transform = bt2020_profile
935            .create_transform_8bit(
936                Layout::Rgba,
937                &srgb_profile,
938                Layout::Rgba,
939                TransformOptions::default(),
940            )
941            .unwrap();
942        let src = vec![random_point_x; 256 * 256 * 4];
943        let mut dst = vec![random_point_x; 256 * 256 * 4];
944        transform.transform(&src, &mut dst).unwrap();
945    }
946
947    #[test]
948    fn test_transform_gray_to_rgb8() {
949        let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32);
950        let bt2020_profile = ColorProfile::new_bt2020();
951        let random_point_x = rand::rng().random_range(0..255);
952        let transform = gray_profile
953            .create_transform_8bit(
954                Layout::Gray,
955                &bt2020_profile,
956                Layout::Rgb,
957                TransformOptions::default(),
958            )
959            .unwrap();
960        let src = vec![random_point_x; 256 * 256];
961        let mut dst = vec![random_point_x; 256 * 256 * 3];
962        transform.transform(&src, &mut dst).unwrap();
963    }
964
965    #[test]
966    fn test_transform_gray_to_rgba8() {
967        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
968        let bt2020_profile = ColorProfile::new_bt2020();
969        let random_point_x = rand::rng().random_range(0..255);
970        let transform = srgb_profile
971            .create_transform_8bit(
972                Layout::Gray,
973                &bt2020_profile,
974                Layout::Rgba,
975                TransformOptions::default(),
976            )
977            .unwrap();
978        let src = vec![random_point_x; 256 * 256];
979        let mut dst = vec![random_point_x; 256 * 256 * 4];
980        transform.transform(&src, &mut dst).unwrap();
981    }
982
983    #[test]
984    fn test_transform_gray_to_gray_alpha8() {
985        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
986        let bt2020_profile = ColorProfile::new_bt2020();
987        let random_point_x = rand::rng().random_range(0..255);
988        let transform = srgb_profile
989            .create_transform_8bit(
990                Layout::Gray,
991                &bt2020_profile,
992                Layout::GrayAlpha,
993                TransformOptions::default(),
994            )
995            .unwrap();
996        let src = vec![random_point_x; 256 * 256];
997        let mut dst = vec![random_point_x; 256 * 256 * 2];
998        transform.transform(&src, &mut dst).unwrap();
999    }
1000
1001    #[test]
1002    fn test_transform_rgb10() {
1003        let srgb_profile = ColorProfile::new_srgb();
1004        let bt2020_profile = ColorProfile::new_bt2020();
1005        let random_point_x = rand::rng().random_range(0..((1 << 10) - 1));
1006        let transform = bt2020_profile
1007            .create_transform_10bit(
1008                Layout::Rgb,
1009                &srgb_profile,
1010                Layout::Rgb,
1011                TransformOptions::default(),
1012            )
1013            .unwrap();
1014        let src = vec![random_point_x; 256 * 256 * 3];
1015        let mut dst = vec![random_point_x; 256 * 256 * 3];
1016        transform.transform(&src, &mut dst).unwrap();
1017    }
1018
1019    #[test]
1020    fn test_transform_rgb12() {
1021        let srgb_profile = ColorProfile::new_srgb();
1022        let bt2020_profile = ColorProfile::new_bt2020();
1023        let random_point_x = rand::rng().random_range(0..((1 << 12) - 1));
1024        let transform = bt2020_profile
1025            .create_transform_12bit(
1026                Layout::Rgb,
1027                &srgb_profile,
1028                Layout::Rgb,
1029                TransformOptions::default(),
1030            )
1031            .unwrap();
1032        let src = vec![random_point_x; 256 * 256 * 3];
1033        let mut dst = vec![random_point_x; 256 * 256 * 3];
1034        transform.transform(&src, &mut dst).unwrap();
1035    }
1036
1037    #[test]
1038    fn test_transform_rgb16() {
1039        let srgb_profile = ColorProfile::new_srgb();
1040        let bt2020_profile = ColorProfile::new_bt2020();
1041        let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16;
1042        let transform = bt2020_profile
1043            .create_transform_16bit(
1044                Layout::Rgb,
1045                &srgb_profile,
1046                Layout::Rgb,
1047                TransformOptions::default(),
1048            )
1049            .unwrap();
1050        let src = vec![random_point_x; 256 * 256 * 3];
1051        let mut dst = vec![random_point_x; 256 * 256 * 3];
1052        transform.transform(&src, &mut dst).unwrap();
1053    }
1054
1055    #[test]
1056    fn test_transform_round_trip_rgb8() {
1057        let srgb_profile = ColorProfile::new_srgb();
1058        let bt2020_profile = ColorProfile::new_bt2020();
1059        let transform = srgb_profile
1060            .create_transform_8bit(
1061                Layout::Rgb,
1062                &bt2020_profile,
1063                Layout::Rgb,
1064                TransformOptions::default(),
1065            )
1066            .unwrap();
1067        let mut src = vec![0u8; 256 * 256 * 3];
1068        for dst in src.chunks_exact_mut(3) {
1069            dst[0] = 175;
1070            dst[1] = 75;
1071            dst[2] = 13;
1072        }
1073        let mut dst = vec![0u8; 256 * 256 * 3];
1074        transform.transform(&src, &mut dst).unwrap();
1075
1076        let transform_inverse = bt2020_profile
1077            .create_transform_8bit(
1078                Layout::Rgb,
1079                &srgb_profile,
1080                Layout::Rgb,
1081                TransformOptions::default(),
1082            )
1083            .unwrap();
1084
1085        transform_inverse.transform(&dst, &mut src).unwrap();
1086
1087        for src in src.chunks_exact_mut(3) {
1088            let diff0 = (src[0] as i32 - 175).abs();
1089            let diff1 = (src[1] as i32 - 75).abs();
1090            let diff2 = (src[2] as i32 - 13).abs();
1091            assert!(
1092                diff0 < 3,
1093                "On channel 0 difference should be less than 3, but it was {diff0}"
1094            );
1095            assert!(
1096                diff1 < 3,
1097                "On channel 1 difference should be less than 3, but it was {diff1}"
1098            );
1099            assert!(
1100                diff2 < 3,
1101                "On channel 2 difference should be less than 3, but it was {diff2}"
1102            );
1103        }
1104    }
1105
1106    #[test]
1107    fn test_transform_round_trip_rgb10() {
1108        let srgb_profile = ColorProfile::new_srgb();
1109        let bt2020_profile = ColorProfile::new_bt2020();
1110        let transform = srgb_profile
1111            .create_transform_10bit(
1112                Layout::Rgb,
1113                &bt2020_profile,
1114                Layout::Rgb,
1115                TransformOptions::default(),
1116            )
1117            .unwrap();
1118        let mut src = vec![0u16; 256 * 256 * 3];
1119        for dst in src.chunks_exact_mut(3) {
1120            dst[0] = 175;
1121            dst[1] = 256;
1122            dst[2] = 512;
1123        }
1124        let mut dst = vec![0u16; 256 * 256 * 3];
1125        transform.transform(&src, &mut dst).unwrap();
1126
1127        let transform_inverse = bt2020_profile
1128            .create_transform_10bit(
1129                Layout::Rgb,
1130                &srgb_profile,
1131                Layout::Rgb,
1132                TransformOptions::default(),
1133            )
1134            .unwrap();
1135
1136        transform_inverse.transform(&dst, &mut src).unwrap();
1137
1138        for src in src.chunks_exact_mut(3) {
1139            let diff0 = (src[0] as i32 - 175).abs();
1140            let diff1 = (src[1] as i32 - 256).abs();
1141            let diff2 = (src[2] as i32 - 512).abs();
1142            assert!(
1143                diff0 < 15,
1144                "On channel 0 difference should be less than 15, but it was {diff0}"
1145            );
1146            assert!(
1147                diff1 < 15,
1148                "On channel 1 difference should be less than 15, but it was {diff1}"
1149            );
1150            assert!(
1151                diff2 < 15,
1152                "On channel 2 difference should be less than 15, but it was {diff2}"
1153            );
1154        }
1155    }
1156
1157    #[test]
1158    fn test_transform_round_trip_rgb12() {
1159        let srgb_profile = ColorProfile::new_srgb();
1160        let bt2020_profile = ColorProfile::new_bt2020();
1161        let transform = srgb_profile
1162            .create_transform_12bit(
1163                Layout::Rgb,
1164                &bt2020_profile,
1165                Layout::Rgb,
1166                TransformOptions::default(),
1167            )
1168            .unwrap();
1169        let mut src = vec![0u16; 256 * 256 * 3];
1170        for dst in src.chunks_exact_mut(3) {
1171            dst[0] = 1750;
1172            dst[1] = 2560;
1173            dst[2] = 3143;
1174        }
1175        let mut dst = vec![0u16; 256 * 256 * 3];
1176        transform.transform(&src, &mut dst).unwrap();
1177
1178        let transform_inverse = bt2020_profile
1179            .create_transform_12bit(
1180                Layout::Rgb,
1181                &srgb_profile,
1182                Layout::Rgb,
1183                TransformOptions::default(),
1184            )
1185            .unwrap();
1186
1187        transform_inverse.transform(&dst, &mut src).unwrap();
1188
1189        for src in src.chunks_exact_mut(3) {
1190            let diff0 = (src[0] as i32 - 1750).abs();
1191            let diff1 = (src[1] as i32 - 2560).abs();
1192            let diff2 = (src[2] as i32 - 3143).abs();
1193            assert!(
1194                diff0 < 25,
1195                "On channel 0 difference should be less than 25, but it was {diff0}"
1196            );
1197            assert!(
1198                diff1 < 25,
1199                "On channel 1 difference should be less than 25, but it was {diff1}"
1200            );
1201            assert!(
1202                diff2 < 25,
1203                "On channel 2 difference should be less than 25, but it was {diff2}"
1204            );
1205        }
1206    }
1207
1208    #[test]
1209    fn test_transform_round_trip_rgb16() {
1210        let srgb_profile = ColorProfile::new_srgb();
1211        let bt2020_profile = ColorProfile::new_bt2020();
1212        let transform = srgb_profile
1213            .create_transform_16bit(
1214                Layout::Rgb,
1215                &bt2020_profile,
1216                Layout::Rgb,
1217                TransformOptions::default(),
1218            )
1219            .unwrap();
1220        let mut src = vec![0u16; 256 * 256 * 3];
1221        for dst in src.chunks_exact_mut(3) {
1222            dst[0] = 1760;
1223            dst[1] = 2560;
1224            dst[2] = 5120;
1225        }
1226        let mut dst = vec![0u16; 256 * 256 * 3];
1227        transform.transform(&src, &mut dst).unwrap();
1228
1229        let transform_inverse = bt2020_profile
1230            .create_transform_16bit(
1231                Layout::Rgb,
1232                &srgb_profile,
1233                Layout::Rgb,
1234                TransformOptions::default(),
1235            )
1236            .unwrap();
1237
1238        transform_inverse.transform(&dst, &mut src).unwrap();
1239
1240        for src in src.chunks_exact_mut(3) {
1241            let diff0 = (src[0] as i32 - 1760).abs();
1242            let diff1 = (src[1] as i32 - 2560).abs();
1243            let diff2 = (src[2] as i32 - 5120).abs();
1244            assert!(
1245                diff0 < 35,
1246                "On channel 0 difference should be less than 35, but it was {diff0}"
1247            );
1248            assert!(
1249                diff1 < 35,
1250                "On channel 1 difference should be less than 35, but it was {diff1}"
1251            );
1252            assert!(
1253                diff2 < 35,
1254                "On channel 2 difference should be less than 35, but it was {diff2}"
1255            );
1256        }
1257    }
1258
1259    #[test]
1260    fn test_transform_rgb_to_gray_extended() {
1261        let srgb = ColorProfile::new_srgb();
1262        let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0);
1263        gray_profile.color_space = DataColorSpace::Gray;
1264        gray_profile.gray_trc = srgb.red_trc.clone();
1265        let mut test_profile = vec![0.; 4];
1266        test_profile[2] = 1.;
1267        let mut dst = vec![0.; 1];
1268
1269        let mut inverse = vec![0.; 4];
1270
1271        let cvt0 = srgb
1272            .create_transform_f32(
1273                Layout::Rgba,
1274                &gray_profile,
1275                Layout::Gray,
1276                TransformOptions {
1277                    allow_extended_range_rgb_xyz: true,
1278                    ..Default::default()
1279                },
1280            )
1281            .unwrap();
1282        cvt0.transform(&test_profile, &mut dst).unwrap();
1283        assert!((dst[0] - 0.273046) < 1e-4);
1284
1285        let cvt_inverse = gray_profile
1286            .create_transform_f32(
1287                Layout::Gray,
1288                &srgb,
1289                Layout::Rgba,
1290                TransformOptions {
1291                    allow_extended_range_rgb_xyz: false,
1292                    ..Default::default()
1293                },
1294            )
1295            .unwrap();
1296        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1297        assert!((inverse[0] - 0.273002833) < 1e-4);
1298
1299        let cvt1 = srgb
1300            .create_transform_f32(
1301                Layout::Rgba,
1302                &gray_profile,
1303                Layout::Gray,
1304                TransformOptions {
1305                    allow_extended_range_rgb_xyz: false,
1306                    ..Default::default()
1307                },
1308            )
1309            .unwrap();
1310        cvt1.transform(&test_profile, &mut dst).unwrap();
1311        assert!((dst[0] - 0.27307168) < 1e-5);
1312
1313        inverse.fill(0.);
1314
1315        let cvt_inverse = gray_profile
1316            .create_transform_f32(
1317                Layout::Gray,
1318                &srgb,
1319                Layout::Rgba,
1320                TransformOptions {
1321                    allow_extended_range_rgb_xyz: true,
1322                    ..Default::default()
1323                },
1324            )
1325            .unwrap();
1326        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1327        assert!((inverse[0] - 0.273002833) < 1e-4);
1328    }
1329}