image/metadata/
cicp.rs

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