Skip to main content

ai_image/metadata/
cicp.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3use core::time::Duration;
4#[cfg(not(feature = "std"))]
5use num_traits::float::FloatCore as _;
6
7/// A `Now` implementation that returns epoch (zero duration).
8/// Used for ICC profile creation timestamps where the exact time is not important.
9struct EpochNow;
10
11impl moxcms::Now for EpochNow {
12    fn now() -> Duration {
13        Duration::from_secs(0)
14    }
15}
16
17/// CICP (coding independent code points) defines the colorimetric interpretation of rgb-ish color
18/// components.
19use crate::{
20    color::FromPrimitive,
21    error::{ParameterError, ParameterErrorKind},
22    math::multiply_accumulate,
23    traits::{
24        private::{LayoutWithColor, SealedPixelWithColorType},
25        PixelWithColorType,
26    },
27    utils::vec_try_with_capacity,
28    DynamicImage, ImageError, Pixel, Primitive,
29};
30
31/// Reference: <https://www.itu.int/rec/T-REC-H.273-202407-I/en> (V4)
32#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
33pub struct Cicp {
34    /// Defines the exact color of red, green, blue primary colors.
35    pub primaries: CicpColorPrimaries,
36    /// The electro-optical transfer function (EOTF) that maps color components to linear values.
37    pub transfer: CicpTransferCharacteristics,
38    /// A matrix between linear values and primary color representation.
39    ///
40    /// For an RGB space this is the identity matrix.
41    pub matrix: CicpMatrixCoefficients,
42    /// Whether the color components use all bits of the encoded values, or have headroom.
43    ///
44    /// For compute purposes, `image` only supports [`CicpVideoFullRangeFlag::FullRange`] and you
45    /// get errors when trying to pass a non-full-range color profile to transform APIs such as
46    /// [`DynamicImage::apply_color_space`] or [`CicpTransform::new`].
47    pub full_range: CicpVideoFullRangeFlag,
48}
49
50/// An internal representation of what our `T: PixelWithColorType` can do, i.e. ImageBuffer.
51#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
52pub(crate) struct CicpRgb {
53    pub(crate) primaries: CicpColorPrimaries,
54    pub(crate) transfer: CicpTransferCharacteristics,
55    pub(crate) luminance: DerivedLuminance,
56}
57
58/// Defines the exact color of red, green, blue primary colors.
59///
60/// Each set defines the CIE 1931 XYZ (2°) color space coordinates of the primary colors and an
61/// illuminant/whitepoint under which those colors are viewed.
62///
63/// Refer to Rec H.273 Table 2.
64#[repr(u8)]
65#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
66#[non_exhaustive]
67pub enum CicpColorPrimaries {
68    /// ITU-R BT.709-6
69    SRgb = 1,
70    /// Explicitly, the color space is not determined.
71    Unspecified = 2,
72    /// ITU-R BT.470-6 System M
73    RgbM = 4,
74    /// ITU-R BT.470-6 System B, G
75    RgbB = 5,
76    /// SMPTE 170M
77    /// functionally equivalent to 7
78    Bt601 = 6,
79    /// SMPTE 240M
80    /// functionally equivalent to 6
81    Rgb240m = 7,
82    /// Generic film (colour filters using Illuminant C)
83    GenericFilm = 8,
84    /// Rec. ITU-R BT.2020-2
85    /// Rec. ITU-R BT.2100-2
86    Rgb2020 = 9,
87    /// SMPTE ST 428-1
88    ///
89    /// (CIE 1931 XYZ as in ISO/CIE 11664-1)
90    Xyz = 10,
91    /// SMPTE RP 431-2 (aka. DCI P3)
92    SmpteRp431 = 11,
93    /// SMPTE EG 432-1, DCI P3 variant with the D65 whitepoint (matching sRGB and BT.2020)
94    SmpteRp432 = 12,
95    /// Corresponds to value 22 but
96    ///
97    /// > No corresponding industry specification identified
98    ///
99    /// But moxcms identifies it as EBU Tech 3213-E: <https://tech.ebu.ch/docs/tech/tech3213.pdf>
100    ///
101    /// However, there are some differences in the second digit of red's CIE 1931 and the precision
102    /// is only 2 digits whereas CICP names three; so unsure if this is fully accurate as the
103    /// actual source material.
104    Industry22 = 22,
105}
106
107impl CicpColorPrimaries {
108    fn to_moxcms(self) -> moxcms::CicpColorPrimaries {
109        use moxcms::CicpColorPrimaries as M;
110
111        match self {
112            CicpColorPrimaries::SRgb => M::Bt709,
113            CicpColorPrimaries::Unspecified => M::Unspecified,
114            CicpColorPrimaries::RgbM => M::Bt470M,
115            CicpColorPrimaries::RgbB => M::Bt470Bg,
116            CicpColorPrimaries::Bt601 => M::Bt601,
117            CicpColorPrimaries::Rgb240m => M::Smpte240,
118            CicpColorPrimaries::GenericFilm => M::GenericFilm,
119            CicpColorPrimaries::Rgb2020 => M::Bt2020,
120            CicpColorPrimaries::Xyz => M::Xyz,
121            CicpColorPrimaries::SmpteRp431 => M::Smpte431,
122            CicpColorPrimaries::SmpteRp432 => M::Smpte432,
123            CicpColorPrimaries::Industry22 => M::Ebu3213,
124        }
125    }
126}
127
128/// The transfer characteristics, expressing relation between encoded values and linear color
129/// values.
130///
131/// Refer to Rec H.273 Table 3.
132#[repr(u8)]
133#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
134#[non_exhaustive]
135pub enum CicpTransferCharacteristics {
136    /// Rec. ITU-R BT.709-6
137    /// Rec. ITU-R BT.1361-0 conventional
138    /// (functionally the same as the values 6, 14 and 15)
139    Bt709 = 1,
140    /// Explicitly, the transfer characteristics are not determined.
141    Unspecified = 2,
142    /// Rec. ITU-R BT.470-6 System M (historical)
143    /// United States National Television System Committee 1953 Recommendation for transmission standards for color television
144    /// United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20)
145    /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
146    ///
147    /// Assumed gamma of 2.2
148    Bt470M = 4,
149    /// Rec. ITU-R BT.470-6 System B, G (historical)
150    Bt470BG = 5,
151    /// Rec. ITU-R BT.601-7 525 or 625
152    /// Rec. ITU-R BT.1358-1 525 or 625 (historical)
153    /// Rec. ITU-R BT.1700-0 NTSC
154    /// SMPTE ST 170 (functionally the same as the values 1, 14 and 15)
155    Bt601 = 6,
156    /// SMPTE ST 240
157    Smpte240m = 7,
158    /// Linear transfer characteristics
159    Linear = 8,
160    /// Logarithmic transfer characteristic (100:1 range)
161    Log100 = 9,
162    /// Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range)
163    LogSqrt = 10,
164    /// IEC 61966-2-4
165    Iec61966_2_4 = 11,
166    /// Rec. ITU-R BT.1361-0 extended colour gamut system (historical)
167    Bt1361 = 12,
168    /// IEC 61966-2-1 sRGB (with MatrixCoefficients equal to 0)
169    /// IEC 61966-2-1 sYCC (with MatrixCoefficients equal to 5)
170    SRgb = 13,
171    /// Rec. ITU-R BT.2020-2 (10-bit system)
172    /// (functionally the same as the values 1, 6 and 15)
173    Bt2020_10bit = 14,
174    /// Rec. ITU-R BT.2020-2 (12-bit system)
175    /// (functionally the same as the values 1, 6 and 14)
176    Bt2020_12bit = 15,
177    /// SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems
178    /// Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system
179    Smpte2084 = 16,
180    /// SMPTE ST 428-1
181    Smpte428 = 17,
182    /// ARIB STD-B67
183    /// Rec. ITU-R BT.2100-2 hybrid log- gamma (HLG) system
184    Bt2100Hlg = 18,
185}
186
187impl CicpTransferCharacteristics {
188    fn to_moxcms(self) -> moxcms::TransferCharacteristics {
189        use moxcms::TransferCharacteristics as T;
190
191        match self {
192            CicpTransferCharacteristics::Bt709 => T::Bt709,
193            CicpTransferCharacteristics::Unspecified => T::Unspecified,
194            CicpTransferCharacteristics::Bt470M => T::Bt470M,
195            CicpTransferCharacteristics::Bt470BG => T::Bt470Bg,
196            CicpTransferCharacteristics::Bt601 => T::Bt601,
197            CicpTransferCharacteristics::Smpte240m => T::Smpte240,
198            CicpTransferCharacteristics::Linear => T::Linear,
199            CicpTransferCharacteristics::Log100 => T::Log100,
200            CicpTransferCharacteristics::LogSqrt => T::Log100sqrt10,
201            CicpTransferCharacteristics::Iec61966_2_4 => T::Iec61966,
202            CicpTransferCharacteristics::Bt1361 => T::Bt1361,
203            CicpTransferCharacteristics::SRgb => T::Srgb,
204            CicpTransferCharacteristics::Bt2020_10bit => T::Bt202010bit,
205            CicpTransferCharacteristics::Bt2020_12bit => T::Bt202012bit,
206            CicpTransferCharacteristics::Smpte2084 => T::Smpte2084,
207            CicpTransferCharacteristics::Smpte428 => T::Smpte428,
208            CicpTransferCharacteristics::Bt2100Hlg => T::Hlg,
209        }
210    }
211}
212
213///
214/// Refer to Rec H.273 Table 4.
215#[repr(u8)]
216#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
217#[non_exhaustive]
218pub enum CicpMatrixCoefficients {
219    /// The identity matrix.
220    /// Typically used for GBR (often referred to as RGB); however, may also be used for YZX (often referred to as XYZ);
221    /// IEC 61966-2-1 sRGB
222    /// SMPTE ST 428-1
223    Identity = 0,
224    /// Rec. ITU-R BT.709-6
225    /// Rec. ITU-R BT.1361-0 conventional colour gamut system and extended colour gamut system (historical)
226    /// IEC 61966-2-4 xvYCC709
227    /// SMPTE RP 177 Annex B
228    Bt709 = 1,
229    /// Explicitly, the matrix coefficients are not determined.
230    Unspecified = 2,
231    /// United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20)
232    UsFCC = 4,
233    ///  Rec. ITU-R BT.470-6 System B, G (historical)
234    /// Rec. ITU-R BT.601-7 625
235    /// Rec. ITU-R BT.1358-0 625 (historical)
236    /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
237    /// IEC 61966-2-1 sYCC
238    /// IEC 61966-2-4 xvYCC601
239    /// (functionally the same as the value 6)
240    Bt470BG = 5,
241    /// (functionally the same as the value 5)
242    Smpte170m = 6,
243    /// SMPTE ST 240
244    Smpte240m = 7,
245    /// YCgCo
246    YCgCo = 8,
247    /// Rec. ITU-R BT.2020-2 (non-constant luminance)
248    /// Rec. ITU-R BT.2100-2 Y′CbCr
249    Bt2020NonConstant = 9,
250    /// Rec. ITU-R BT.2020-2 (constant luminance)
251    Bt2020Constant = 10,
252    /// SMPTE ST 2085
253    Smpte2085 = 11,
254    /// Chromaticity-derived non-constant luminance system
255    ChromaticityDerivedNonConstant = 12,
256    /// Chromaticity-derived constant luminance system
257    ChromaticityDerivedConstant = 13,
258    /// Rec. ITU-R BT.2100-2 ICTCp
259    Bt2100 = 14,
260    /// Colour representation developed in SMPTE as IPT-PQ-C2.
261    IptPqC2 = 15,
262    /// YCgCo with added bit-depth (2-bit).
263    YCgCoRe = 16,
264    /// YCgCo with added bit-depth (1-bit).
265    YCgCoRo = 17,
266}
267
268impl CicpMatrixCoefficients {
269    fn to_moxcms(self) -> Option<moxcms::MatrixCoefficients> {
270        use moxcms::MatrixCoefficients as M;
271
272        Some(match self {
273            CicpMatrixCoefficients::Identity => M::Identity,
274            CicpMatrixCoefficients::Unspecified => M::Unspecified,
275            CicpMatrixCoefficients::Bt709 => M::Bt709,
276            CicpMatrixCoefficients::UsFCC => M::Fcc,
277            CicpMatrixCoefficients::Bt470BG => M::Bt470Bg,
278            CicpMatrixCoefficients::Smpte170m => M::Smpte170m,
279            CicpMatrixCoefficients::Smpte240m => M::Smpte240m,
280            CicpMatrixCoefficients::YCgCo => M::YCgCo,
281            CicpMatrixCoefficients::Bt2020NonConstant => M::Bt2020Ncl,
282            CicpMatrixCoefficients::Bt2020Constant => M::Bt2020Cl,
283            CicpMatrixCoefficients::Smpte2085 => M::Smpte2085,
284            CicpMatrixCoefficients::ChromaticityDerivedNonConstant => M::ChromaticityDerivedNCL,
285            CicpMatrixCoefficients::ChromaticityDerivedConstant => M::ChromaticityDerivedCL,
286            CicpMatrixCoefficients::Bt2100 => M::ICtCp,
287            CicpMatrixCoefficients::IptPqC2
288            | CicpMatrixCoefficients::YCgCoRe
289            | CicpMatrixCoefficients::YCgCoRo => return None,
290        })
291    }
292}
293
294/// The used encoded value range.
295#[repr(u8)]
296#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
297#[non_exhaustive]
298pub enum CicpVideoFullRangeFlag {
299    /// The color components are encoded in a limited range, e.g., 16-235 for 8-bit.
300    ///
301    /// Do note that `image` does not support computing with this setting (yet).
302    NarrowRange = 0,
303    /// The color components are encoded in the full range, e.g., 0-255 for 8-bit.
304    FullRange = 1,
305}
306
307#[repr(u8)]
308#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
309pub(crate) enum DerivedLuminance {
310    /// Luminance is calculated in linear space:
311    ///     Y' = dot(K_rgb, RGB)'
312    #[allow(dead_code)] // We do not support this yet but should prepare call sites for the
313    // eventuality.
314    Constant,
315    /// Luminance is calculated in the transferred space:
316    ///     Y' = dot(K_rgb, RGB')
317    NonConstant,
318}
319
320/// Apply to colors of the input color space to get output color values.
321///
322/// We do not support all possible Cicp color spaces, but when we support one then all builtin
323/// `Pixel` types can be converted with their respective components. This value is used to signify
324/// that some particular combination is supported.
325#[derive(Clone)]
326pub struct CicpTransform {
327    from: Cicp,
328    into: Cicp,
329    u8: RgbTransforms<u8>,
330    u16: RgbTransforms<u16>,
331    f32: RgbTransforms<f32>,
332    // Converting RGB to Y in the output.
333    output_coefs: [f32; 3],
334}
335
336pub(crate) type CicpApplicable<'lt, C> = dyn Fn(&[C], &mut [C]) + Send + Sync + 'lt;
337
338#[derive(Clone)]
339struct RgbTransforms<C> {
340    slices: [Arc<CicpApplicable<'static, C>>; 4],
341    luma_rgb: [Arc<CicpApplicable<'static, C>>; 4],
342    rgb_luma: [Arc<CicpApplicable<'static, C>>; 4],
343    luma_luma: [Arc<CicpApplicable<'static, C>>; 4],
344}
345
346impl CicpTransform {
347    /// Construct a transform between two color spaces.
348    ///
349    /// Returns `Some` if the transform is guaranteed to be supported by `image`. Both color spaces
350    /// are well understood and can be expected to be supported in future versions. However, we do
351    /// not make guarantees about adjusting the rounding modes, accuracy, and exact numeric values
352    /// used in the transform. Also, out-of-gamut colors may be handled differently per API.
353    ///
354    /// Returns `None` if the transformation is not (yet) supported.
355    ///
356    /// This is used with [`ConvertColorOptions`][`crate::ConvertColorOptions`] in
357    /// [`ImageBuffer::copy_from_color_space`][`crate::ImageBuffer::copy_from_color_space`],
358    /// [`DynamicImage::copy_from_color_space`][`DynamicImage::copy_from_color_space`].
359    pub fn new(from: Cicp, into: Cicp) -> Option<Self> {
360        if !from.qualify_stability() || !into.qualify_stability() {
361            // To avoid regressions, we do not support all kinds of transforms from the start.
362            // Instead, a selected list will be gradually enlarged as more in-depth tests are done
363            // and the selected implementation library is checked for suitability in use.
364            return None;
365        }
366
367        // Unused, but introduces symmetry to the supported color space transforms. That said we
368        // calculate the derived luminance coefficients for all color that have a matching moxcms
369        // profile so this really should not block anything.
370        let _input_coefs = from.into_rgb().derived_luminance()?;
371        let output_coefs = into.into_rgb().derived_luminance()?;
372
373        let mox_from = from.to_moxcms_compute_profile()?;
374        let mox_into = into.to_moxcms_compute_profile()?;
375
376        let opt = moxcms::TransformOptions::default();
377
378        let f32_fallback = {
379            let try_f32 = Self::LAYOUTS.map(|(from_layout, into_layout)| {
380                let (from, from_layout) = mox_from.map_layout(from_layout);
381                let (into, into_layout) = mox_into.map_layout(into_layout);
382
383                from.create_transform_f32(from_layout, into, into_layout, opt)
384                    .ok()
385            });
386
387            if try_f32.iter().any(Option::is_none) {
388                return None;
389            }
390
391            try_f32.map(Option::unwrap)
392        };
393
394        // TODO: really these should be lazy, eh?
395        Some(CicpTransform {
396            from,
397            into,
398            u8: Self::build_transforms(
399                Self::LAYOUTS.map(|(from_layout, into_layout)| {
400                    let (from, from_layout) = mox_from.map_layout(from_layout);
401                    let (into, into_layout) = mox_into.map_layout(into_layout);
402
403                    from.create_transform_8bit(from_layout, into, into_layout, opt)
404                        .ok()
405                }),
406                f32_fallback.clone(),
407                output_coefs,
408            )?,
409            u16: Self::build_transforms(
410                Self::LAYOUTS.map(|(from_layout, into_layout)| {
411                    let (from, from_layout) = mox_from.map_layout(from_layout);
412                    let (into, into_layout) = mox_into.map_layout(into_layout);
413
414                    from.create_transform_16bit(from_layout, into, into_layout, opt)
415                        .ok()
416                }),
417                f32_fallback.clone(),
418                output_coefs,
419            )?,
420            f32: Self::build_transforms(
421                f32_fallback.clone().map(Some),
422                f32_fallback.clone(),
423                output_coefs,
424            )?,
425            output_coefs,
426        })
427    }
428
429    /// For a Pixel with known color layout (`ColorType`) get a transform that is accurate.
430    ///
431    /// This returns `None` if we do not support the transform. At writing that is true for
432    /// instance for transforms involved 'Luma` pixels which are interpreted as the `Y` in a
433    /// `YCbCr` color based off the actual whitepoint, with coefficients according to each
434    /// primary's luminance. Only Rgb transforms are supported via `moxcms`.
435    ///
436    /// Maybe provide publicly?
437    pub(crate) fn supported_transform_fn<From: PixelWithColorType, Into: PixelWithColorType>(
438        &self,
439    ) -> &'_ CicpApplicable<'_, From::Subpixel> {
440        use crate::traits::private::double_dispatch_transform_from_sealed;
441        double_dispatch_transform_from_sealed::<From, Into>(self)
442    }
443
444    /// Does this transform realize the conversion `from` to `into`.
445    pub(crate) fn check_applicable(&self, from: Cicp, into: Cicp) -> Result<(), ImageError> {
446        let check_expectation = |expected, found| {
447            if expected == found {
448                Ok(())
449            } else {
450                Err(ParameterError::from_kind(
451                    ParameterErrorKind::CicpMismatch { expected, found },
452                ))
453            }
454        };
455
456        check_expectation(self.from, from).map_err(ImageError::Parameter)?;
457        check_expectation(self.into, into).map_err(ImageError::Parameter)?;
458
459        Ok(())
460    }
461
462    fn build_transforms<P: ColorComponentForCicp + Default + 'static>(
463        trs: [Option<Arc<dyn moxcms::TransformExecutor<P> + Send + Sync>>; 4],
464        f32: [Arc<dyn moxcms::TransformExecutor<f32> + Send + Sync>; 4],
465        output_coef: [f32; 3],
466    ) -> Option<RgbTransforms<P>> {
467        // We would use `[array]::try_map` here, but it is not stable yet.
468        if trs.iter().any(Option::is_none) {
469            return None;
470        }
471
472        let trs = trs.map(Option::unwrap);
473
474        // rgb-rgb transforms are done directly via moxcms.
475        let slices = trs.clone().map(|tr| {
476            Arc::new(move |input: &[P], output: &mut [P]| {
477                tr.transform(input, output).expect("transform failed")
478            }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>
479        });
480
481        const N: usize = 256;
482
483        // luma-rgb transforms expand the Luma to Rgb (and LumaAlpha to Rgba)
484        let luma_rgb = {
485            let [tr33, tr34, tr43, tr44] = f32.clone();
486
487            [
488                Arc::new(move |input: &[P], output: &mut [P]| {
489                    let mut ibuffer = [0.0f32; 3 * N];
490                    let mut obuffer = [0.0f32; 3 * N];
491
492                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(3 * N)) {
493                        let n = luma.len();
494                        let ibuffer = &mut ibuffer[..3 * n];
495                        let obuffer = &mut obuffer[..3 * n];
496                        Self::expand_luma_rgb(luma, ibuffer);
497                        tr33.transform(ibuffer, obuffer).expect("transform failed");
498                        Self::clamp_rgb(obuffer, output);
499                    }
500                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
501                Arc::new(move |input: &[P], output: &mut [P]| {
502                    let mut ibuffer = [0.0f32; 3 * N];
503                    let mut obuffer = [0.0f32; 4 * N];
504
505                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(4 * N)) {
506                        let n = luma.len();
507                        let ibuffer = &mut ibuffer[..3 * n];
508                        let obuffer = &mut obuffer[..4 * n];
509                        Self::expand_luma_rgb(luma, ibuffer);
510                        tr34.transform(ibuffer, obuffer).expect("transform failed");
511                        Self::clamp_rgba(obuffer, output);
512                    }
513                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
514                Arc::new(move |input: &[P], output: &mut [P]| {
515                    let mut ibuffer = [0.0f32; 4 * N];
516                    let mut obuffer = [0.0f32; 3 * N];
517
518                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(3 * N)) {
519                        let n = luma.len() / 2;
520                        let ibuffer = &mut ibuffer[..4 * n];
521                        let obuffer = &mut obuffer[..3 * n];
522                        Self::expand_luma_rgba(luma, ibuffer);
523                        tr43.transform(ibuffer, obuffer).expect("transform failed");
524                        Self::clamp_rgb(obuffer, output);
525                    }
526                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
527                Arc::new(move |input: &[P], output: &mut [P]| {
528                    let mut ibuffer = [0.0f32; 4 * N];
529                    let mut obuffer = [0.0f32; 4 * N];
530
531                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(4 * N)) {
532                        let n = luma.len() / 2;
533                        let ibuffer = &mut ibuffer[..4 * n];
534                        let obuffer = &mut obuffer[..4 * n];
535                        Self::expand_luma_rgba(luma, ibuffer);
536                        tr44.transform(ibuffer, obuffer).expect("transform failed");
537                        Self::clamp_rgba(obuffer, output);
538                    }
539                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
540            ]
541        };
542
543        // rgb-luma transforms contract Rgb to Luma (and Rgba to LumaAlpha)
544        let rgb_luma = {
545            let [tr33, tr34, tr43, tr44] = f32.clone();
546
547            [
548                Arc::new(move |input: &[P], output: &mut [P]| {
549                    debug_assert_eq!(input.len() / 3, output.len());
550
551                    let mut ibuffer = [0.0f32; 3 * N];
552                    let mut obuffer = [0.0f32; 3 * N];
553
554                    for (rgb, output) in input.chunks(3 * N).zip(output.chunks_mut(N)) {
555                        let n = output.len();
556                        let ibuffer = &mut ibuffer[..3 * n];
557                        let obuffer = &mut obuffer[..3 * n];
558                        Self::expand_rgb(rgb, ibuffer);
559                        tr33.transform(ibuffer, obuffer).expect("transform failed");
560                        Self::clamp_rgb_luma(obuffer, output, output_coef);
561                    }
562                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
563                Arc::new(move |input: &[P], output: &mut [P]| {
564                    debug_assert_eq!(input.len() / 3, output.len() / 2);
565
566                    let mut ibuffer = [0.0f32; 3 * N];
567                    let mut obuffer = [0.0f32; 4 * N];
568
569                    for (rgb, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) {
570                        let n = output.len() / 2;
571                        let ibuffer = &mut ibuffer[..3 * n];
572                        let obuffer = &mut obuffer[..4 * n];
573                        Self::expand_rgb(rgb, ibuffer);
574                        tr34.transform(ibuffer, obuffer).expect("transform failed");
575                        Self::clamp_rgba_luma(obuffer, output, output_coef);
576                    }
577                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
578                Arc::new(move |input: &[P], output: &mut [P]| {
579                    debug_assert_eq!(input.len() / 4, output.len());
580
581                    let mut ibuffer = [0.0f32; 4 * N];
582                    let mut obuffer = [0.0f32; 3 * N];
583
584                    for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(N)) {
585                        let n = output.len();
586                        let ibuffer = &mut ibuffer[..4 * n];
587                        let obuffer = &mut obuffer[..3 * n];
588                        Self::expand_rgba(rgba, ibuffer);
589                        tr43.transform(ibuffer, obuffer).expect("transform failed");
590                        Self::clamp_rgb_luma(obuffer, output, output_coef);
591                    }
592                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
593                Arc::new(move |input: &[P], output: &mut [P]| {
594                    debug_assert_eq!(input.len() / 4, output.len() / 2);
595
596                    let mut ibuffer = [0.0f32; 4 * N];
597                    let mut obuffer = [0.0f32; 4 * N];
598
599                    for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) {
600                        let n = output.len() / 2;
601                        let ibuffer = &mut ibuffer[..4 * n];
602                        let obuffer = &mut obuffer[..4 * n];
603                        Self::expand_rgba(rgba, ibuffer);
604                        tr44.transform(ibuffer, obuffer).expect("transform failed");
605                        Self::clamp_rgba_luma(obuffer, output, output_coef);
606                    }
607                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
608            ]
609        };
610
611        // luma-luma both expand and contract
612        let luma_luma = {
613            let [tr33, tr34, tr43, tr44] = f32.clone();
614
615            [
616                Arc::new(move |input: &[P], output: &mut [P]| {
617                    debug_assert_eq!(input.len(), output.len());
618                    let mut ibuffer = [0.0f32; 3 * N];
619                    let mut obuffer = [0.0f32; 3 * N];
620
621                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(N)) {
622                        let n = luma.len();
623                        let ibuffer = &mut ibuffer[..3 * n];
624                        let obuffer = &mut obuffer[..3 * n];
625                        Self::expand_luma_rgb(luma, ibuffer);
626                        tr33.transform(ibuffer, obuffer).expect("transform failed");
627                        Self::clamp_rgb_luma(obuffer, output, output_coef);
628                    }
629                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
630                Arc::new(move |input: &[P], output: &mut [P]| {
631                    debug_assert_eq!(input.len(), output.len() / 2);
632                    let mut ibuffer = [0.0f32; 3 * N];
633                    let mut obuffer = [0.0f32; 4 * N];
634
635                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(2 * N)) {
636                        let n = luma.len();
637                        let ibuffer = &mut ibuffer[..3 * n];
638                        let obuffer = &mut obuffer[..4 * n];
639                        Self::expand_luma_rgb(luma, ibuffer);
640                        tr34.transform(ibuffer, obuffer).expect("transform failed");
641                        Self::clamp_rgba_luma(obuffer, output, output_coef);
642                    }
643                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
644                Arc::new(move |input: &[P], output: &mut [P]| {
645                    debug_assert_eq!(input.len() / 2, output.len());
646                    let mut ibuffer = [0.0f32; 4 * N];
647                    let mut obuffer = [0.0f32; 3 * N];
648
649                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(N)) {
650                        let n = luma.len() / 2;
651                        let ibuffer = &mut ibuffer[..4 * n];
652                        let obuffer = &mut obuffer[..3 * n];
653                        Self::expand_luma_rgba(luma, ibuffer);
654                        tr43.transform(ibuffer, obuffer).expect("transform failed");
655                        Self::clamp_rgb_luma(obuffer, output, output_coef);
656                    }
657                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
658                Arc::new(move |input: &[P], output: &mut [P]| {
659                    debug_assert_eq!(input.len() / 2, output.len() / 2);
660                    let mut ibuffer = [0.0f32; 4 * N];
661                    let mut obuffer = [0.0f32; 4 * N];
662
663                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(2 * N)) {
664                        let n = luma.len() / 2;
665                        let ibuffer = &mut ibuffer[..4 * n];
666                        let obuffer = &mut obuffer[..4 * n];
667                        Self::expand_luma_rgba(luma, ibuffer);
668                        tr44.transform(ibuffer, obuffer).expect("transform failed");
669                        Self::clamp_rgba_luma(obuffer, output, output_coef);
670                    }
671                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
672            ]
673        };
674
675        Some(RgbTransforms {
676            slices,
677            luma_rgb,
678            rgb_luma,
679            luma_luma,
680        })
681    }
682
683    pub(crate) fn transform_dynamic(&self, lhs: &mut DynamicImage, rhs: &DynamicImage) {
684        const STEP: usize = 256;
685
686        let mut ibuffer = [0.0f32; 4 * STEP];
687        let mut obuffer = [0.0f32; 4 * STEP];
688
689        let pixels = (u64::from(lhs.width()) * u64::from(lhs.height())) as usize;
690
691        let input_samples;
692        let output_samples;
693
694        let inner_transform = match (
695            LayoutWithColor::from(lhs.color()),
696            LayoutWithColor::from(rhs.color()),
697        ) {
698            (
699                LayoutWithColor::Luma | LayoutWithColor::Rgb,
700                LayoutWithColor::Luma | LayoutWithColor::Rgb,
701            ) => {
702                output_samples = 3;
703                input_samples = 3;
704                &*self.f32.slices[0]
705            }
706            (
707                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
708                LayoutWithColor::Luma | LayoutWithColor::Rgb,
709            ) => {
710                output_samples = 4;
711                input_samples = 3;
712                &*self.f32.slices[1]
713            }
714            (
715                LayoutWithColor::Luma | LayoutWithColor::Rgb,
716                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
717            ) => {
718                output_samples = 3;
719                input_samples = 4;
720                &*self.f32.slices[2]
721            }
722            (
723                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
724                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
725            ) => {
726                output_samples = 4;
727                input_samples = 4;
728                &*self.f32.slices[3]
729            }
730        };
731
732        for start_idx in (0..pixels).step_by(STEP) {
733            let end_idx = (start_idx + STEP).min(pixels);
734            let count = end_idx - start_idx;
735
736            // Expand pixels from `other` into `ibuffer`. All of these have different types, so
737            // here's two large switch statements.
738            match rhs {
739                DynamicImage::ImageLuma8(buf) => {
740                    CicpTransform::expand_luma_rgb(
741                        &buf.inner_pixels()[start_idx..end_idx],
742                        &mut ibuffer[..3 * count],
743                    );
744                }
745                DynamicImage::ImageLumaA8(buf) => {
746                    CicpTransform::expand_luma_rgba(
747                        &buf.inner_pixels()[2 * start_idx..2 * end_idx],
748                        &mut ibuffer[..4 * count],
749                    );
750                }
751                DynamicImage::ImageRgb8(buf) => {
752                    CicpTransform::expand_rgb(
753                        &buf.inner_pixels()[3 * start_idx..3 * end_idx],
754                        &mut ibuffer[..3 * count],
755                    );
756                }
757                DynamicImage::ImageRgba8(buf) => {
758                    CicpTransform::expand_rgba(
759                        &buf.inner_pixels()[4 * start_idx..4 * end_idx],
760                        &mut ibuffer[..4 * count],
761                    );
762                }
763                DynamicImage::ImageLuma16(buf) => {
764                    CicpTransform::expand_luma_rgb(
765                        &buf.inner_pixels()[start_idx..end_idx],
766                        &mut ibuffer[..3 * count],
767                    );
768                }
769                DynamicImage::ImageLumaA16(buf) => {
770                    CicpTransform::expand_luma_rgba(
771                        &buf.inner_pixels()[2 * start_idx..2 * end_idx],
772                        &mut ibuffer[..4 * count],
773                    );
774                }
775                DynamicImage::ImageRgb16(buf) => {
776                    CicpTransform::expand_rgb(
777                        &buf.inner_pixels()[3 * start_idx..3 * end_idx],
778                        &mut ibuffer[..3 * count],
779                    );
780                }
781
782                DynamicImage::ImageRgba16(buf) => {
783                    CicpTransform::expand_rgba(
784                        &buf.inner_pixels()[4 * start_idx..4 * end_idx],
785                        &mut ibuffer[..4 * count],
786                    );
787                }
788                DynamicImage::ImageRgb32F(buf) => {
789                    CicpTransform::expand_rgb(
790                        &buf.inner_pixels()[3 * start_idx..3 * end_idx],
791                        &mut ibuffer[..3 * count],
792                    );
793                }
794                DynamicImage::ImageRgba32F(buf) => {
795                    CicpTransform::expand_rgba(
796                        &buf.inner_pixels()[4 * start_idx..4 * end_idx],
797                        &mut ibuffer[..4 * count],
798                    );
799                }
800            }
801
802            let islice = &ibuffer[..input_samples * count];
803            let oslice = &mut obuffer[..output_samples * count];
804
805            inner_transform(islice, oslice);
806
807            match lhs {
808                DynamicImage::ImageLuma8(buf) => {
809                    CicpTransform::clamp_rgb_luma(
810                        &obuffer[..3 * count],
811                        &mut buf.inner_pixels_mut()[start_idx..end_idx],
812                        self.output_coefs,
813                    );
814                }
815                DynamicImage::ImageLumaA8(buf) => {
816                    CicpTransform::clamp_rgba_luma(
817                        &obuffer[..4 * count],
818                        &mut buf.inner_pixels_mut()[2 * start_idx..2 * end_idx],
819                        self.output_coefs,
820                    );
821                }
822                DynamicImage::ImageRgb8(buf) => {
823                    CicpTransform::clamp_rgb(
824                        &obuffer[..3 * count],
825                        &mut buf.inner_pixels_mut()[3 * start_idx..3 * end_idx],
826                    );
827                }
828                DynamicImage::ImageRgba8(buf) => {
829                    CicpTransform::clamp_rgba(
830                        &obuffer[..4 * count],
831                        &mut buf.inner_pixels_mut()[4 * start_idx..4 * end_idx],
832                    );
833                }
834                DynamicImage::ImageLuma16(buf) => {
835                    CicpTransform::clamp_rgb_luma(
836                        &obuffer[..3 * count],
837                        &mut buf.inner_pixels_mut()[start_idx..end_idx],
838                        self.output_coefs,
839                    );
840                }
841                DynamicImage::ImageLumaA16(buf) => {
842                    CicpTransform::clamp_rgba_luma(
843                        &obuffer[..4 * count],
844                        &mut buf.inner_pixels_mut()[2 * start_idx..2 * end_idx],
845                        self.output_coefs,
846                    );
847                }
848                DynamicImage::ImageRgb16(buf) => {
849                    CicpTransform::clamp_rgba(
850                        &obuffer[..3 * count],
851                        &mut buf.inner_pixels_mut()[3 * start_idx..3 * end_idx],
852                    );
853                }
854
855                DynamicImage::ImageRgba16(buf) => {
856                    CicpTransform::clamp_rgba(
857                        &obuffer[..4 * count],
858                        &mut buf.inner_pixels_mut()[4 * start_idx..4 * end_idx],
859                    );
860                }
861                DynamicImage::ImageRgb32F(buf) => {
862                    CicpTransform::clamp_rgb(
863                        &obuffer[..3 * count],
864                        &mut buf.inner_pixels_mut()[3 * start_idx..3 * end_idx],
865                    );
866                }
867                DynamicImage::ImageRgba32F(buf) => {
868                    CicpTransform::clamp_rgba(
869                        &obuffer[..4 * count],
870                        &mut buf.inner_pixels_mut()[4 * start_idx..4 * end_idx],
871                    );
872                }
873            }
874        }
875    }
876
877    // Note on this design: When we dispatch into this function, we have a `Self` type that is
878    // qualified to have the appropriate bound here. However, for the target type of the transform
879    // we have, e.g., `Rgba<Self::Subpixel>`. Now we know that these are also with color for the
880    // most part but we can not convince the compiler (indeed, there is or was an asymmetry with
881    // gray pixels where they do not have float equivalents). It is hence necessary to provide the
882    // output layout as a runtime parameter, not a compile-time type.
883    pub(crate) fn select_transform_u8<P: SealedPixelWithColorType<TransformableSubpixel = u8>>(
884        &self,
885        into: LayoutWithColor,
886    ) -> &Arc<CicpApplicable<'static, u8>> {
887        self.u8.select_transform::<P>(into)
888    }
889
890    pub(crate) fn select_transform_u16<O: SealedPixelWithColorType<TransformableSubpixel = u16>>(
891        &self,
892        into: LayoutWithColor,
893    ) -> &Arc<CicpApplicable<'static, u16>> {
894        self.u16.select_transform::<O>(into)
895    }
896
897    pub(crate) fn select_transform_f32<O: SealedPixelWithColorType<TransformableSubpixel = f32>>(
898        &self,
899        into: LayoutWithColor,
900    ) -> &Arc<CicpApplicable<'static, f32>> {
901        self.f32.select_transform::<O>(into)
902    }
903
904    const LAYOUTS: [(LayoutWithColor, LayoutWithColor); 4] = [
905        (LayoutWithColor::Rgb, LayoutWithColor::Rgb),
906        (LayoutWithColor::Rgb, LayoutWithColor::Rgba),
907        (LayoutWithColor::Rgba, LayoutWithColor::Rgb),
908        (LayoutWithColor::Rgba, LayoutWithColor::Rgba),
909    ];
910
911    pub(crate) fn expand_luma_rgb<P: ColorComponentForCicp>(luma: &[P], rgb: &mut [f32]) {
912        for (&pix, rgb) in luma.iter().zip(rgb.as_chunks_mut::<3>().0.iter_mut()) {
913            let luma = pix.expand_to_f32();
914            rgb[0] = luma;
915            rgb[1] = luma;
916            rgb[2] = luma;
917        }
918    }
919
920    pub(crate) fn expand_luma_rgba<P: ColorComponentForCicp>(luma: &[P], rgb: &mut [f32]) {
921        let luma_chunks = luma.as_chunks::<2>().0.iter();
922        let rgb_chunks = rgb.as_chunks_mut::<4>().0.iter_mut();
923        for (pix, rgb) in luma_chunks.zip(rgb_chunks) {
924            let luma = pix[0].expand_to_f32();
925            rgb[0] = luma;
926            rgb[1] = luma;
927            rgb[2] = luma;
928            rgb[3] = pix[1].expand_to_f32();
929        }
930    }
931
932    pub(crate) fn expand_rgb<P: ColorComponentForCicp>(input: &[P], output: &mut [f32]) {
933        for (&component, val) in input.iter().zip(output) {
934            *val = component.expand_to_f32();
935        }
936    }
937
938    pub(crate) fn expand_rgba<P: ColorComponentForCicp>(input: &[P], output: &mut [f32]) {
939        for (&component, val) in input.iter().zip(output) {
940            *val = component.expand_to_f32();
941        }
942    }
943
944    pub(crate) fn clamp_rgb<P: ColorComponentForCicp>(input: &[f32], output: &mut [P]) {
945        // Everything is mapped..
946        for (&component, val) in input.iter().zip(output) {
947            *val = P::clamp_from_f32(component);
948        }
949    }
950
951    pub(crate) fn clamp_rgba<P: ColorComponentForCicp>(input: &[f32], output: &mut [P]) {
952        for (&component, val) in input.iter().zip(output) {
953            *val = P::clamp_from_f32(component);
954        }
955    }
956
957    pub(crate) fn clamp_rgb_luma<P: ColorComponentForCicp>(
958        input: &[f32],
959        output: &mut [P],
960        coef: [f32; 3],
961    ) {
962        for (rgb, pix) in input.as_chunks::<3>().0.iter().zip(output) {
963            let mut luma = 0.0;
964
965            for (&component, coef) in rgb.iter().zip(coef) {
966                luma = multiply_accumulate(luma, component, coef);
967            }
968
969            *pix = P::clamp_from_f32(luma);
970        }
971    }
972
973    pub(crate) fn clamp_rgba_luma<P: ColorComponentForCicp>(
974        input: &[f32],
975        output: &mut [P],
976        coef: [f32; 3],
977    ) {
978        let input_chunks = input.as_chunks::<4>().0.iter();
979        let output_chunks = output.as_chunks_mut::<2>().0.iter_mut();
980        for (rgba, pix) in input_chunks.zip(output_chunks) {
981            let mut luma = 0.0;
982
983            for (&component, coef) in rgba[..3].iter().zip(coef) {
984                luma = multiply_accumulate(luma, component, coef);
985            }
986
987            pix[0] = P::clamp_from_f32(luma);
988            pix[1] = P::clamp_from_f32(rgba[3]);
989        }
990    }
991}
992
993impl CicpRgb {
994    /// Internal utility for converting color buffers of different pixel representations, assuming
995    /// they have this same cicp. This method returns a buffer, avoiding the pre-zeroing
996    /// the vector.
997    pub(crate) fn cast_pixels<FromColor, IntoColor>(
998        &self,
999        buffer: &[FromColor::Subpixel],
1000        // Since this is not performance sensitive, we can use a dyn closure here instead of an
1001        // impl closure just in case we call this from multiple different paths.
1002        color_space_fallback: &dyn Fn() -> [f32; 3],
1003    ) -> Vec<IntoColor::Subpixel>
1004    where
1005        FromColor: Pixel + SealedPixelWithColorType<TransformableSubpixel = FromColor::Subpixel>,
1006        IntoColor: Pixel,
1007        IntoColor: CicpPixelCast<FromColor>,
1008        FromColor::Subpixel: ColorComponentForCicp,
1009        IntoColor::Subpixel: ColorComponentForCicp + FromPrimitive<FromColor::Subpixel>,
1010    {
1011        use crate::traits::private::PrivateToken;
1012        let from_layout = <FromColor as SealedPixelWithColorType>::layout(PrivateToken);
1013        let into_layout = <IntoColor as SealedPixelWithColorType>::layout(PrivateToken);
1014        // This method is instantiated *a lot*. Consequently every line here matters in terms of
1015        // codegen. We outline all parts into separate methods where they monomorphize only over
1016        // the channel type and not the whole pixel except what we needed here to satisfy the type
1017        // sysstem.
1018        self.cast_pixels_by_layout(buffer, color_space_fallback, from_layout, into_layout)
1019    }
1020
1021    fn cast_pixels_by_layout<FromSubpixel, IntoSubpixel>(
1022        &self,
1023        buffer: &[FromSubpixel],
1024        // Since this is not performance sensitive, we can use a dyn closure here instead of an
1025        // impl closure just in case we call this from multiple different paths.
1026        color_space_fallback: &dyn Fn() -> [f32; 3],
1027        from_layout: LayoutWithColor,
1028        into_layout: LayoutWithColor,
1029    ) -> Vec<IntoSubpixel>
1030    where
1031        FromSubpixel: ColorComponentForCicp + Primitive,
1032        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1033    {
1034        let mut output = match self.cast_pixels_from_subpixels(buffer, from_layout, into_layout) {
1035            Ok(ok) => return ok,
1036            Err(buffer) => buffer,
1037        };
1038
1039        // If we get here we need to transform through Rgb(a) 32F
1040        let color_space_coefs = self
1041            .derived_luminance()
1042            // Since `cast_pixels` must be infallible we have no choice but to fallback to
1043            // something here. This something is chosen by the caller, which would allow them to
1044            // detect it has happened.
1045            .unwrap_or_else(color_space_fallback);
1046
1047        let pixels = buffer.len() / from_layout.channels();
1048
1049        // All of the following is done in-place; so we must allow the buffer space in which the
1050        // output is written ahead of time although such initialization is technically redundant.
1051        // We best do this once to allow for a very efficient memset initialization.
1052        Self::create_output::<IntoSubpixel>(&mut output, pixels, into_layout);
1053
1054        Self::cast_pixels_by_fallback(
1055            buffer,
1056            output.as_mut_slice(),
1057            from_layout,
1058            into_layout,
1059            color_space_coefs,
1060        );
1061
1062        output
1063    }
1064
1065    fn create_output<Into: Primitive>(
1066        output: &mut Vec<Into>,
1067        pixels: usize,
1068        into_layout: LayoutWithColor,
1069    ) {
1070        output.resize(pixels * into_layout.channels(), Into::DEFAULT_MIN_VALUE);
1071    }
1072
1073    fn cast_pixels_by_fallback<
1074        From: Primitive + ColorComponentForCicp,
1075        Into: ColorComponentForCicp,
1076    >(
1077        buffer: &[From],
1078        output: &mut [Into],
1079        from_layout: LayoutWithColor,
1080        into_layout: LayoutWithColor,
1081        color_space_coefs: [f32; 3],
1082    ) {
1083        use LayoutWithColor as Layout;
1084
1085        const STEP: usize = 256;
1086        let pixels = buffer.len() / from_layout.channels();
1087
1088        let mut ibuffer = [0.0f32; 4 * STEP];
1089        let mut obuffer = [0.0f32; 4 * STEP];
1090
1091        let ibuf_step = match from_layout {
1092            Layout::Rgb | Layout::Luma => 3,
1093            Layout::Rgba | Layout::LumaAlpha => 4,
1094        };
1095
1096        let obuf_step = match into_layout {
1097            Layout::Rgb | Layout::Luma => 3,
1098            Layout::Rgba | Layout::LumaAlpha => 4,
1099        };
1100
1101        for start_idx in (0..pixels).step_by(STEP) {
1102            let end_idx = (start_idx + STEP).min(pixels);
1103            let count = end_idx - start_idx;
1104
1105            let ibuffer = &mut ibuffer[..ibuf_step * count];
1106
1107            match from_layout {
1108                Layout::Rgb => {
1109                    CicpTransform::expand_rgb(&buffer[3 * start_idx..3 * end_idx], ibuffer)
1110                }
1111                Layout::Rgba => {
1112                    CicpTransform::expand_rgba(&buffer[4 * start_idx..4 * end_idx], ibuffer)
1113                }
1114                Layout::Luma => {
1115                    CicpTransform::expand_luma_rgb(&buffer[start_idx..end_idx], ibuffer)
1116                }
1117                Layout::LumaAlpha => {
1118                    CicpTransform::expand_luma_rgba(&buffer[2 * start_idx..2 * end_idx], ibuffer)
1119                }
1120            }
1121
1122            // Add or subtract the alpha channel. We could do that as part of the store but this
1123            // keeps the code simpler—there is a one-to-one correspondence with the methods needed
1124            // for a full conversion.
1125            let obuffer = match (ibuf_step, obuf_step) {
1126                (3, 4) => {
1127                    let ibuffer_chunks = ibuffer.as_chunks::<3>().0.iter();
1128                    let obuffer_chunks = obuffer.as_chunks_mut::<4>().0.iter_mut();
1129                    for (rgb, rgba) in ibuffer_chunks.zip(obuffer_chunks).take(count) {
1130                        rgba[0] = rgb[0];
1131                        rgba[1] = rgb[1];
1132                        rgba[2] = rgb[2];
1133                        rgba[3] = 1.0;
1134                    }
1135
1136                    &obuffer[..4 * count]
1137                }
1138                (4, 3) => {
1139                    let ibuffer_chunks = ibuffer.as_chunks::<4>().0.iter();
1140                    let obuffer_chunks = obuffer.as_chunks_mut::<3>().0.iter_mut();
1141                    for (rgba, rgb) in ibuffer_chunks.zip(obuffer_chunks).take(count) {
1142                        rgb[0] = rgba[0];
1143                        rgb[1] = rgba[1];
1144                        rgb[2] = rgba[2];
1145                    }
1146
1147                    &obuffer[..3 * count]
1148                }
1149                (n, m) => {
1150                    debug_assert_eq!(n, m);
1151                    &ibuffer[..m * count]
1152                }
1153            };
1154
1155            match into_layout {
1156                Layout::Rgb => {
1157                    CicpTransform::clamp_rgb(obuffer, &mut output[3 * start_idx..3 * end_idx]);
1158                }
1159                Layout::Rgba => {
1160                    CicpTransform::clamp_rgba(obuffer, &mut output[4 * start_idx..4 * end_idx]);
1161                }
1162                Layout::Luma => {
1163                    CicpTransform::clamp_rgb_luma(
1164                        obuffer,
1165                        &mut output[start_idx..end_idx],
1166                        color_space_coefs,
1167                    );
1168                }
1169                Layout::LumaAlpha => {
1170                    CicpTransform::clamp_rgba_luma(
1171                        obuffer,
1172                        &mut output[2 * start_idx..2 * end_idx],
1173                        color_space_coefs,
1174                    );
1175                }
1176            }
1177        }
1178    }
1179
1180    /// Make sure this is only monomorphized for subpixel combinations, not for every pixel
1181    /// combination! There's ample time to do that in `cast_pixels`.
1182    pub(crate) fn cast_pixels_from_subpixels<FromSubpixel, IntoSubpixel>(
1183        &self,
1184        buffer: &[FromSubpixel],
1185        from_layout: LayoutWithColor,
1186        into_layout: LayoutWithColor,
1187    ) -> Result<Vec<IntoSubpixel>, Vec<IntoSubpixel>>
1188    where
1189        FromSubpixel: ColorComponentForCicp,
1190        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1191    {
1192        use crate::traits::private::LayoutWithColor as Layout;
1193
1194        assert!(buffer.len().is_multiple_of(from_layout.channels()));
1195        let pixels = buffer.len() / from_layout.channels();
1196
1197        let mut output: Vec<IntoSubpixel> = vec_try_with_capacity(pixels * into_layout.channels())
1198            // Not entirely failsafe, if you expand luma to rgba you can get a factor of 4 but at
1199            // least this will not overflow. And that's why I'm a fan of in-place operations.
1200            .expect("input layout already allocated with appropriate layout");
1201
1202        // In most cases we perform a seemingly wasteful initialization, by initializing the output
1203        // before writing. However, we gain it back in codegen. If we did not have the vector exist
1204        // then the only safe way to add elements is via `push` or `extend_from_slice`. These
1205        // methods will check the capacity of the vector on every call and branch to reallocate. In
1206        // many cases this throws off loop analysis. LLVM does not seem to trust our capacity or
1207        // any arithmetic checks we do before to ensure that the len does not increase beyond the
1208        // capacity. The loop bodies that result are catastrophically bad and mostly not
1209        // vectorized.
1210        //
1211        // The one case where this does not apply is when both have the same count of channels. In
1212        // this case we can just extend into the output without worrying about the layout at all
1213        // and the Iterator type (and its size_hint) is trivial to work with.
1214
1215        let map_channel = <IntoSubpixel as FromPrimitive<FromSubpixel>>::from_primitive;
1216
1217        match (from_layout, into_layout) {
1218            // First detect if we can use simple channel-by-channel component conversion.
1219            (Layout::Rgb, Layout::Rgb)
1220            | (Layout::Rgba, Layout::Rgba)
1221            | (Layout::Luma, Layout::Luma)
1222            | (Layout::LumaAlpha, Layout::LumaAlpha) => {
1223                // Every component assigned accordingly. We do not care which as there is no
1224                // conversion do to be done other than numeric one. (no tone mapping etc.).
1225                output.extend(buffer.iter().copied().map(map_channel));
1226            }
1227            (Layout::Rgb, Layout::Rgba) => {
1228                Self::subpixel_cast_rgb_to_rgba(&mut output, buffer);
1229            }
1230            (Layout::Rgba, Layout::Rgb) => {
1231                Self::subpixel_cast_rgba_to_rgb(&mut output, buffer);
1232            }
1233            (Layout::Luma, Layout::LumaAlpha) => {
1234                Self::subpixel_cast_luma_to_luma_alpha(&mut output, buffer);
1235            }
1236            (Layout::LumaAlpha, Layout::Luma) => {
1237                Self::subpixel_cast_luma_alpha_to_luma(&mut output, buffer);
1238            }
1239            _ => return Err(output),
1240        }
1241
1242        Ok(output)
1243    }
1244
1245    fn subpixel_cast_rgb_to_rgba<FromSubpixel, IntoSubpixel>(
1246        output: &mut Vec<IntoSubpixel>,
1247        buffer: &[FromSubpixel],
1248    ) where
1249        FromSubpixel: ColorComponentForCicp,
1250        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1251    {
1252        let pixels = buffer.len() / LayoutWithColor::Rgb.channels();
1253        Self::create_output::<IntoSubpixel>(output, pixels, LayoutWithColor::Rgba);
1254
1255        let map_channel = <IntoSubpixel as FromPrimitive<FromSubpixel>>::from_primitive;
1256        let default_alpha = <IntoSubpixel as Primitive>::DEFAULT_MAX_VALUE;
1257
1258        let buffer_chunks = buffer.as_chunks::<3>().0;
1259        let output_chunks = output.as_chunks_mut::<4>().0;
1260
1261        for (&[r, g, b], out) in buffer_chunks.iter().zip(output_chunks) {
1262            *out = [
1263                map_channel(r),
1264                map_channel(g),
1265                map_channel(b),
1266                default_alpha,
1267            ];
1268        }
1269    }
1270
1271    fn subpixel_cast_rgba_to_rgb<FromSubpixel, IntoSubpixel>(
1272        output: &mut Vec<IntoSubpixel>,
1273        buffer: &[FromSubpixel],
1274    ) where
1275        FromSubpixel: ColorComponentForCicp,
1276        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1277    {
1278        let pixels = buffer.len() / LayoutWithColor::Rgba.channels();
1279        Self::create_output::<IntoSubpixel>(output, pixels, LayoutWithColor::Rgb);
1280
1281        let map_channel = <IntoSubpixel as FromPrimitive<FromSubpixel>>::from_primitive;
1282
1283        let buffer_chunks = buffer.as_chunks::<4>().0;
1284        let output_chunks = output.as_chunks_mut::<3>().0;
1285
1286        for (&[r, g, b, _], out) in buffer_chunks.iter().zip(output_chunks) {
1287            *out = [map_channel(r), map_channel(g), map_channel(b)];
1288        }
1289    }
1290
1291    // Note: ~50% faster than the output-based fallback in Luma8->LumaA8 and Luma8->LumaA16 codegen
1292    // so this one stays with `flat_map` for now.
1293    fn subpixel_cast_luma_to_luma_alpha<FromSubpixel, IntoSubpixel>(
1294        output: &mut Vec<IntoSubpixel>,
1295        buffer: &[FromSubpixel],
1296    ) where
1297        FromSubpixel: ColorComponentForCicp,
1298        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1299    {
1300        let map_channel = <IntoSubpixel as FromPrimitive<FromSubpixel>>::from_primitive;
1301
1302        output.extend(buffer.iter().copied().flat_map(|l| {
1303            [
1304                map_channel(l),
1305                // Crucially inlined here. When I tried to move this out then it no longer
1306                // optimizes any better than the output method. (#2804).
1307                <IntoSubpixel as Primitive>::DEFAULT_MAX_VALUE,
1308            ]
1309        }));
1310    }
1311
1312    fn subpixel_cast_luma_alpha_to_luma<FromSubpixel, IntoSubpixel>(
1313        output: &mut Vec<IntoSubpixel>,
1314        buffer: &[FromSubpixel],
1315    ) where
1316        FromSubpixel: ColorComponentForCicp,
1317        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1318    {
1319        let map_channel = <IntoSubpixel as FromPrimitive<FromSubpixel>>::from_primitive;
1320
1321        let buffer_chunks = buffer.as_chunks::<2>().0;
1322
1323        output.extend(buffer_chunks.iter().map(|&[l, _]| map_channel(l)));
1324    }
1325}
1326
1327/// Color types that can be converted by [`CicpRgb::cast_pixels`].
1328///
1329/// This is a utility to avoid dealing with lots of bounds everywhere. In the actual implementation
1330/// we avoid the concrete pixel types and care just about the layout (as a runtime property) and
1331/// the channel type to be promotable into a float for normalization. If the pixels have layouts
1332/// that are convertible with intra-channel numerics we instead try and promote the channels via
1333/// `Primitive` instead.
1334pub(crate) trait CicpPixelCast<FromColor>
1335where
1336    // Ensure we can get components from both, get the layout, and that all components are
1337    // compatible with our intermediate connection space (rgba32f).
1338    Self: Pixel + SealedPixelWithColorType<TransformableSubpixel = <Self as Pixel>::Subpixel>,
1339    FromColor:
1340        Pixel + SealedPixelWithColorType<TransformableSubpixel = <FromColor as Pixel>::Subpixel>,
1341    Self::Subpixel: ColorComponentForCicp + FromPrimitive<FromColor::Subpixel>,
1342    FromColor::Subpixel: ColorComponentForCicp,
1343{
1344}
1345
1346impl<FromColor, IntoColor> CicpPixelCast<FromColor> for IntoColor
1347where
1348    IntoColor: Pixel + SealedPixelWithColorType<TransformableSubpixel = IntoColor::Subpixel>,
1349    FromColor: Pixel + SealedPixelWithColorType<TransformableSubpixel = FromColor::Subpixel>,
1350    IntoColor::Subpixel: ColorComponentForCicp + FromPrimitive<FromColor::Subpixel>,
1351    FromColor::Subpixel: ColorComponentForCicp,
1352{
1353}
1354
1355pub(crate) trait ColorComponentForCicp: Copy {
1356    fn expand_to_f32(self) -> f32;
1357
1358    fn clamp_from_f32(val: f32) -> Self;
1359}
1360
1361impl ColorComponentForCicp for u8 {
1362    fn expand_to_f32(self) -> f32 {
1363        const R: f32 = 1.0 / u8::MAX as f32;
1364        self as f32 * R
1365    }
1366
1367    #[inline]
1368    fn clamp_from_f32(val: f32) -> Self {
1369        // Note: saturating conversion does the clamp for us
1370        (val * Self::MAX as f32).round() as u8
1371    }
1372}
1373
1374impl ColorComponentForCicp for u16 {
1375    fn expand_to_f32(self) -> f32 {
1376        const R: f32 = 1.0 / u16::MAX as f32;
1377        self as f32 * R
1378    }
1379
1380    #[inline]
1381    fn clamp_from_f32(val: f32) -> Self {
1382        // Note: saturating conversion does the clamp for us
1383        (val * Self::MAX as f32).round() as u16
1384    }
1385}
1386
1387impl ColorComponentForCicp for f32 {
1388    fn expand_to_f32(self) -> f32 {
1389        self
1390    }
1391
1392    fn clamp_from_f32(val: f32) -> Self {
1393        val
1394    }
1395}
1396
1397impl<P> RgbTransforms<P> {
1398    fn select_transform<O: SealedPixelWithColorType>(
1399        &self,
1400        into: LayoutWithColor,
1401    ) -> &Arc<CicpApplicable<'static, P>> {
1402        use crate::traits::private::{LayoutWithColor as Layout, PrivateToken};
1403        let from = O::layout(PrivateToken);
1404
1405        match (from, into) {
1406            (Layout::Rgb, Layout::Rgb) => &self.slices[0],
1407            (Layout::Rgb, Layout::Rgba) => &self.slices[1],
1408            (Layout::Rgba, Layout::Rgb) => &self.slices[2],
1409            (Layout::Rgba, Layout::Rgba) => &self.slices[3],
1410            (Layout::Rgb, Layout::Luma) => &self.rgb_luma[0],
1411            (Layout::Rgb, Layout::LumaAlpha) => &self.rgb_luma[1],
1412            (Layout::Rgba, Layout::Luma) => &self.rgb_luma[2],
1413            (Layout::Rgba, Layout::LumaAlpha) => &self.rgb_luma[3],
1414            (Layout::Luma, Layout::Rgb) => &self.luma_rgb[0],
1415            (Layout::Luma, Layout::Rgba) => &self.luma_rgb[1],
1416            (Layout::LumaAlpha, Layout::Rgb) => &self.luma_rgb[2],
1417            (Layout::LumaAlpha, Layout::Rgba) => &self.luma_rgb[3],
1418            (Layout::Luma, Layout::Luma) => &self.luma_luma[0],
1419            (Layout::Luma, Layout::LumaAlpha) => &self.luma_luma[1],
1420            (Layout::LumaAlpha, Layout::Luma) => &self.luma_luma[2],
1421            (Layout::LumaAlpha, Layout::LumaAlpha) => &self.luma_luma[3],
1422        }
1423    }
1424}
1425
1426impl Cicp {
1427    /// The sRGB color space, BT.709 transfer function and D65 whitepoint.
1428    pub const SRGB: Self = Cicp {
1429        primaries: CicpColorPrimaries::SRgb,
1430        transfer: CicpTransferCharacteristics::SRgb,
1431        matrix: CicpMatrixCoefficients::Identity,
1432        full_range: CicpVideoFullRangeFlag::FullRange,
1433    };
1434
1435    /// SRGB primaries and whitepoint with linear samples.
1436    pub const SRGB_LINEAR: Self = Cicp {
1437        primaries: CicpColorPrimaries::SRgb,
1438        transfer: CicpTransferCharacteristics::Linear,
1439        matrix: CicpMatrixCoefficients::Identity,
1440        full_range: CicpVideoFullRangeFlag::FullRange,
1441    };
1442
1443    /// The  Display-P3 color space, a wide-gamut choice with SMPTE RP 432-2 primaries.
1444    ///
1445    /// Note that this modern Display P3 uses a D65 whitepoint. Use the primaries `SmpteRp431` for
1446    /// the previous standard. The advantage of the new standard is the color system shares its
1447    /// whitepoint with sRGB and BT.2020.
1448    pub const DISPLAY_P3: Self = Cicp {
1449        primaries: CicpColorPrimaries::SmpteRp432,
1450        transfer: CicpTransferCharacteristics::SRgb,
1451        matrix: CicpMatrixCoefficients::Identity,
1452        full_range: CicpVideoFullRangeFlag::FullRange,
1453    };
1454
1455    /// Get an compute representation of an ICC profile for RGB.
1456    ///
1457    /// Note you should *not* be using this profile for export in a file, as discussed below.
1458    ///
1459    /// This is straightforward for Rgb and RgbA representations.
1460    ///
1461    /// Our luma models a Y component of a YCbCr color space. It turns out that ICC V4 does
1462    /// not support pure Luma in any other whitepoint apart from D50 (the native profile
1463    /// connection space). The use of a grayTRC does *not* take the chromatic adaptation
1464    /// matrix into account. Of course we can encode the adaptation into the TRC as a
1465    /// coefficient, the Y component of the product of the whitepoint adaptation matrix
1466    /// inverse and the pcs's whitepoint XYZ, but that is only correct for gray -> gray
1467    /// conversion (and that coefficient should generally be `1`).
1468    ///
1469    /// Hence we use a YCbCr. The data->pcs path could be modelled by ("M" curves, matrix, "B"
1470    /// curves) where B curves or M curves are all the identity, depending on whether constant or
1471    /// non-constant luma is in use. This is a subset of the capabilities that a lutAToBType
1472    /// allows. Unfortunately, this is not implemented in moxcms yet and for efficiency we would
1473    /// like to have a masked `create_transform_*` in which the CbCr channels are discarded /
1474    /// assumed 0 instead of them being in memory. Due to this special case and for supporting
1475    /// conversions between sample types, we implement said promotion as part of conversion to
1476    /// Rgba32F in this crate.
1477    ///
1478    /// For export to file, it would arguably correct to use a carefully crafted gray profile which
1479    /// we may implement in another function. That is, we could setup a tone reproduction curve
1480    /// which maps each sample value (which ICC regards as D50) into XYZ D50 in such a way that it
1481    /// _appears_ with the correct D50 luminance that we would get if we had used the conversion
1482    /// unders its true input whitepoint. The resulting color has a slightly wrong chroma as it is
1483    /// linearly dependent on D50 instead, but it's brightness would be correctly presented. At
1484    /// least for perceptual intent this might be alright.
1485    fn to_moxcms_compute_profile(self) -> Option<ColorProfile> {
1486        let mut rgb = moxcms::ColorProfile::new_srgb::<EpochNow>();
1487
1488        rgb.update_rgb_colorimetry_from_cicp(moxcms::CicpProfile {
1489            color_primaries: self.primaries.to_moxcms(),
1490            transfer_characteristics: self.transfer.to_moxcms(),
1491            matrix_coefficients: self.matrix.to_moxcms()?,
1492            full_range: match self.full_range {
1493                CicpVideoFullRangeFlag::NarrowRange => false,
1494                CicpVideoFullRangeFlag::FullRange => true,
1495            },
1496        });
1497
1498        Some(ColorProfile { rgb })
1499    }
1500
1501    /// Whether we have invested enough testing to ensure that color values can be assumed to be
1502    /// stable and correspond to an intended effect, in particular if there even is a well-defined
1503    /// meaning to these color spaces.
1504    ///
1505    /// For instance, our current code for the 'luma' equivalent space assumes that the color space
1506    /// has a shared transfer function for all its color components. Also the judgment should not
1507    /// depend on whether we can represent the profile in `moxcms` but rather if we understand the
1508    /// profile well enough so that conversion implemented through another library can be derived.
1509    /// (Consider the case of a builtin transform-while-encoding that may be more performant for a
1510    /// format that does not support CICP or ICC profiles.)
1511    ///
1512    /// A stable profile should also have `derived_luminance` implemented.
1513    pub(crate) const fn qualify_stability(&self) -> bool {
1514        const _: () = {
1515            // Out public constants _should_ be stable.
1516            assert!(Cicp::SRGB.qualify_stability());
1517            assert!(Cicp::SRGB_LINEAR.qualify_stability());
1518            assert!(Cicp::DISPLAY_P3.qualify_stability());
1519        };
1520
1521        matches!(self.full_range, CicpVideoFullRangeFlag::FullRange)
1522            && matches!(
1523                self.matrix,
1524                // For pure RGB color
1525                CicpMatrixCoefficients::Identity
1526                    // The equivalent of our Luma color as a type..
1527                    | CicpMatrixCoefficients::ChromaticityDerivedNonConstant
1528            )
1529            && matches!(
1530                self.primaries,
1531                CicpColorPrimaries::SRgb
1532                    | CicpColorPrimaries::SmpteRp431
1533                    | CicpColorPrimaries::SmpteRp432
1534                    | CicpColorPrimaries::Bt601
1535                    | CicpColorPrimaries::Rgb240m
1536            )
1537            && matches!(
1538                self.transfer,
1539                CicpTransferCharacteristics::SRgb
1540                    | CicpTransferCharacteristics::Bt709
1541                    | CicpTransferCharacteristics::Bt601
1542                    | CicpTransferCharacteristics::Linear
1543            )
1544    }
1545
1546    /// Discard matrix and range information.
1547    pub(crate) const fn into_rgb(self) -> CicpRgb {
1548        CicpRgb {
1549            primaries: self.primaries,
1550            transfer: self.transfer,
1551            // NOTE: if we add support for constant luminance (through the CMS having support for
1552            // the Luma->YCbCr->Rgb expansion natively or otherwise) then consider if we should
1553            // track here whether the matrix was `Identity` or `ChromaticityDerivedNonConstant` so
1554            // that the `ImageBuffer::color_space()` function roundtrips the value. It may be
1555            // important to know whether the non-constant chromaticity was an invention by `image`
1556            // or part of the file. The colorimetry is the same either way.
1557            luminance: DerivedLuminance::NonConstant,
1558        }
1559    }
1560
1561    pub(crate) fn try_into_rgb(self) -> Result<CicpRgb, ImageError> {
1562        if Cicp::from(self.into_rgb()) != self {
1563            Err(ImageError::Parameter(ParameterError::from_kind(
1564                ParameterErrorKind::RgbCicpRequired(self),
1565            )))
1566        } else {
1567            Ok(self.into_rgb())
1568        }
1569    }
1570}
1571
1572impl CicpRgb {
1573    /// Calculate the luminance cofactors according to Rec H.273 (39) and (40).
1574    ///
1575    /// Returns cofactors for red, green, and blue in that order.
1576    pub(crate) fn derived_luminance(&self) -> Option<[f32; 3]> {
1577        let primaries = match self.primaries {
1578            CicpColorPrimaries::SRgb => moxcms::ColorPrimaries::BT_709,
1579            CicpColorPrimaries::RgbM => moxcms::ColorPrimaries::BT_470M,
1580            CicpColorPrimaries::RgbB => moxcms::ColorPrimaries::BT_470BG,
1581            CicpColorPrimaries::Bt601 => moxcms::ColorPrimaries::BT_601,
1582            CicpColorPrimaries::Rgb240m => moxcms::ColorPrimaries::SMPTE_240,
1583            CicpColorPrimaries::GenericFilm => moxcms::ColorPrimaries::GENERIC_FILM,
1584            CicpColorPrimaries::Rgb2020 => moxcms::ColorPrimaries::BT_2020,
1585            CicpColorPrimaries::Xyz => moxcms::ColorPrimaries::XYZ,
1586            CicpColorPrimaries::SmpteRp431 => moxcms::ColorPrimaries::DISPLAY_P3,
1587            CicpColorPrimaries::SmpteRp432 => moxcms::ColorPrimaries::DISPLAY_P3,
1588            CicpColorPrimaries::Industry22 => moxcms::ColorPrimaries::EBU_3213,
1589            CicpColorPrimaries::Unspecified => return None,
1590        };
1591
1592        const ILLUMINANT_C: moxcms::Chromaticity = moxcms::Chromaticity::new(0.310, 0.316);
1593
1594        let whitepoint = match self.primaries {
1595            CicpColorPrimaries::SRgb => moxcms::Chromaticity::D65,
1596            CicpColorPrimaries::RgbM => ILLUMINANT_C,
1597            CicpColorPrimaries::RgbB => moxcms::Chromaticity::D65,
1598            CicpColorPrimaries::Bt601 => moxcms::Chromaticity::D65,
1599            CicpColorPrimaries::Rgb240m => moxcms::Chromaticity::D65,
1600            CicpColorPrimaries::GenericFilm => ILLUMINANT_C,
1601            CicpColorPrimaries::Rgb2020 => moxcms::Chromaticity::D65,
1602            CicpColorPrimaries::Xyz => moxcms::Chromaticity::new(1. / 3., 1. / 3.),
1603            CicpColorPrimaries::SmpteRp431 => moxcms::Chromaticity::new(0.314, 0.351),
1604            CicpColorPrimaries::SmpteRp432 => moxcms::Chromaticity::D65,
1605            CicpColorPrimaries::Industry22 => moxcms::Chromaticity::D65,
1606            CicpColorPrimaries::Unspecified => return None,
1607        };
1608
1609        let matrix = primaries.transform_to_xyz(whitepoint);
1610
1611        // Our result is the Y row of this matrix.
1612        Some(matrix.v[1])
1613    }
1614}
1615
1616impl From<CicpRgb> for Cicp {
1617    fn from(cicp: CicpRgb) -> Self {
1618        Cicp {
1619            primaries: cicp.primaries,
1620            transfer: cicp.transfer,
1621            matrix: CicpMatrixCoefficients::Identity,
1622            full_range: CicpVideoFullRangeFlag::FullRange,
1623        }
1624    }
1625}
1626
1627/// An RGB profile with its related (same tone-mapping) gray profile.
1628///
1629/// This is the whole input information which we must be able to pass to the CMS in a support
1630/// transform, to handle all possible combinations of `ColorType` pixels that can be thrown at us.
1631/// For instance, in a previous iteration we had a separate gray profile here (but now handle that
1632/// internally by expansion to RGB through an YCbCr). Future iterations may add additional structs
1633/// to be computed for validating `CicpTransform::new`.
1634struct ColorProfile {
1635    rgb: moxcms::ColorProfile,
1636}
1637
1638impl ColorProfile {
1639    fn map_layout(&self, layout: LayoutWithColor) -> (&moxcms::ColorProfile, moxcms::Layout) {
1640        match layout {
1641            LayoutWithColor::Rgb => (&self.rgb, moxcms::Layout::Rgb),
1642            LayoutWithColor::Rgba => (&self.rgb, moxcms::Layout::Rgba),
1643            // See comment in `to_moxcms_profile`.
1644            LayoutWithColor::Luma | LayoutWithColor::LumaAlpha => unreachable!(),
1645        }
1646    }
1647}
1648
1649#[cfg(test)]
1650#[test]
1651fn moxcms() {
1652    let l = moxcms::TransferCharacteristics::Linear;
1653    assert_eq!(l.linearize(1.0), 1.0);
1654    assert_eq!(l.gamma(1.0), 1.0);
1655
1656    assert_eq!(l.gamma(0.5), 0.5);
1657}
1658
1659#[cfg(test)]
1660#[test]
1661fn derived_luminance() {
1662    let luminance = Cicp::SRGB.into_rgb().derived_luminance();
1663    let [kr, kg, kb] = luminance.unwrap();
1664    assert!((kr - 0.2126).abs() < 1e-4);
1665    assert!((kg - 0.7152).abs() < 1e-4);
1666    assert!((kb - 0.0722).abs() < 1e-4);
1667}
1668
1669#[cfg(test)]
1670mod tests {
1671    use super::{Cicp, CicpTransform};
1672    use crate::{Luma, LumaA, Rgb, Rgba};
1673
1674    #[test]
1675    fn can_create_transforms() {
1676        assert!(CicpTransform::new(Cicp::SRGB, Cicp::SRGB).is_some());
1677        assert!(CicpTransform::new(Cicp::SRGB, Cicp::DISPLAY_P3).is_some());
1678        assert!(CicpTransform::new(Cicp::DISPLAY_P3, Cicp::SRGB).is_some());
1679        assert!(CicpTransform::new(Cicp::DISPLAY_P3, Cicp::DISPLAY_P3).is_some());
1680    }
1681
1682    fn no_coefficient_fallback() -> [f32; 3] {
1683        panic!("Fallback coefficients required")
1684    }
1685
1686    #[test]
1687    fn transform_pixels_srgb() {
1688        // Non-constant luminance so:
1689        // Y = dot(rgb, coefs)
1690        let data = [255, 0, 0, 255];
1691        let color = Cicp::SRGB.into_rgb();
1692        let rgba = color.cast_pixels::<Rgba<u8>, Rgb<u8>>(&data, &no_coefficient_fallback);
1693        assert_eq!(rgba, [255, 0, 0]);
1694        let luma = color.cast_pixels::<Rgba<u8>, Luma<u8>>(&data, &no_coefficient_fallback);
1695        assert_eq!(luma, [54]); // 255 * 0.2126
1696        let luma_a = color.cast_pixels::<Rgba<u8>, LumaA<u8>>(&data, &no_coefficient_fallback);
1697        assert_eq!(luma_a, [54, 255]);
1698    }
1699
1700    #[test]
1701    fn transform_pixels_srgb_16() {
1702        // Non-constant luminance so:
1703        // Y = dot(rgb, coefs)
1704        let data = [u16::MAX / 2];
1705        let color = Cicp::SRGB.into_rgb();
1706        let rgba = color.cast_pixels::<Luma<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1707        assert_eq!(rgba, [127; 3]);
1708        let luma = color.cast_pixels::<Luma<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1709        assert_eq!(luma, [127]);
1710        let luma_a = color.cast_pixels::<Luma<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1711        assert_eq!(luma_a, [127, 255]);
1712
1713        let data = [u16::MAX / 2 + 1];
1714        let color = Cicp::SRGB.into_rgb();
1715        let rgba = color.cast_pixels::<Luma<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1716        assert_eq!(rgba, [128; 3]);
1717        let luma = color.cast_pixels::<Luma<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1718        assert_eq!(luma, [128]);
1719        let luma_a = color.cast_pixels::<Luma<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1720        assert_eq!(luma_a, [128, 255]);
1721    }
1722
1723    #[test]
1724    fn transform_pixels_srgb_luma_alpha() {
1725        // Non-constant luminance so:
1726        // Y = dot(rgb, coefs)
1727        let data = [u16::MAX / 2, u16::MAX];
1728        let color = Cicp::SRGB.into_rgb();
1729        let rgba = color.cast_pixels::<LumaA<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1730        assert_eq!(rgba, [127; 3]);
1731        let luma = color.cast_pixels::<LumaA<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1732        assert_eq!(luma, [127]);
1733        let luma = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1734        assert_eq!(luma, [127, u8::MAX]);
1735        let luma_a = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1736        assert_eq!(luma_a, [127, 255]);
1737
1738        let data = [u16::MAX / 2 + 1, u16::MAX];
1739        let color = Cicp::SRGB.into_rgb();
1740        let rgba = color.cast_pixels::<LumaA<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1741        assert_eq!(rgba, [128; 3]);
1742        let luma = color.cast_pixels::<LumaA<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1743        assert_eq!(luma, [128]);
1744        let luma = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1745        assert_eq!(luma, [128, u8::MAX]);
1746        let luma_a = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1747        assert_eq!(luma_a, [128, 255]);
1748    }
1749}