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                    .map(Arc::<dyn moxcms::TransformExecutor<f32> + Send + Sync>::from)
371                    .ok()
372            });
373
374            if try_f32.iter().any(Option::is_none) {
375                return None;
376            }
377
378            try_f32.map(Option::unwrap)
379        };
380
381        // TODO: really these should be lazy, eh?
382        Some(CicpTransform {
383            from,
384            into,
385            u8: Self::build_transforms(
386                Self::LAYOUTS.map(|(from_layout, into_layout)| {
387                    let (from, from_layout) = mox_from.map_layout(from_layout);
388                    let (into, into_layout) = mox_into.map_layout(into_layout);
389
390                    from.create_transform_8bit(from_layout, into, into_layout, opt)
391                        .map(Arc::<dyn moxcms::TransformExecutor<_> + Send + Sync>::from)
392                        .ok()
393                }),
394                f32_fallback.clone(),
395                output_coefs,
396            )?,
397            u16: Self::build_transforms(
398                Self::LAYOUTS.map(|(from_layout, into_layout)| {
399                    let (from, from_layout) = mox_from.map_layout(from_layout);
400                    let (into, into_layout) = mox_into.map_layout(into_layout);
401
402                    from.create_transform_16bit(from_layout, into, into_layout, opt)
403                        .map(Arc::<dyn moxcms::TransformExecutor<_> + Send + Sync>::from)
404                        .ok()
405                }),
406                f32_fallback.clone(),
407                output_coefs,
408            )?,
409            f32: Self::build_transforms(
410                f32_fallback.clone().map(Some),
411                f32_fallback.clone(),
412                output_coefs,
413            )?,
414            output_coefs,
415        })
416    }
417
418    /// For a Pixel with known color layout (`ColorType`) get a transform that is accurate.
419    ///
420    /// This returns `None` if we do not support the transform. At writing that is true for
421    /// instance for transforms involved 'Luma` pixels which are interpreted as the `Y` in a
422    /// `YCbCr` color based off the actual whitepoint, with coefficients according to each
423    /// primary's luminance. Only Rgb transforms are supported via `moxcms`.
424    ///
425    /// Maybe provide publicly?
426    pub(crate) fn supported_transform_fn<From: PixelWithColorType, Into: PixelWithColorType>(
427        &self,
428    ) -> &'_ CicpApplicable<'_, From::Subpixel> {
429        use crate::traits::private::double_dispatch_transform_from_sealed;
430        double_dispatch_transform_from_sealed::<From, Into>(self)
431    }
432
433    /// Does this transform realize the conversion `from` to `into`.
434    pub(crate) fn check_applicable(&self, from: Cicp, into: Cicp) -> Result<(), ImageError> {
435        let check_expectation = |expected, found| {
436            if expected == found {
437                Ok(())
438            } else {
439                Err(ParameterError::from_kind(
440                    ParameterErrorKind::CicpMismatch { expected, found },
441                ))
442            }
443        };
444
445        check_expectation(self.from, from).map_err(ImageError::Parameter)?;
446        check_expectation(self.into, into).map_err(ImageError::Parameter)?;
447
448        Ok(())
449    }
450
451    fn build_transforms<P: ColorComponentForCicp + Default + 'static>(
452        trs: [Option<Arc<dyn moxcms::TransformExecutor<P> + Send + Sync>>; 4],
453        f32: [Arc<dyn moxcms::TransformExecutor<f32> + Send + Sync>; 4],
454        output_coef: [f32; 3],
455    ) -> Option<RgbTransforms<P>> {
456        // We would use `[array]::try_map` here, but it is not stable yet.
457        if trs.iter().any(Option::is_none) {
458            return None;
459        }
460
461        let trs = trs.map(Option::unwrap);
462
463        // rgb-rgb transforms are done directly via moxcms.
464        let slices = trs.clone().map(|tr| {
465            Arc::new(move |input: &[P], output: &mut [P]| {
466                tr.transform(input, output).expect("transform failed")
467            }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>
468        });
469
470        const N: usize = 256;
471
472        // luma-rgb transforms expand the Luma to Rgb (and LumaAlpha to Rgba)
473        let luma_rgb = {
474            let [tr33, tr34, tr43, tr44] = f32.clone();
475
476            [
477                Arc::new(move |input: &[P], output: &mut [P]| {
478                    let mut ibuffer = [0.0f32; 3 * N];
479                    let mut obuffer = [0.0f32; 3 * N];
480
481                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(3 * N)) {
482                        let n = luma.len();
483                        let ibuffer = &mut ibuffer[..3 * n];
484                        let obuffer = &mut obuffer[..3 * n];
485                        Self::expand_luma_rgb(luma, ibuffer);
486                        tr33.transform(ibuffer, obuffer).expect("transform failed");
487                        Self::clamp_rgb(obuffer, output);
488                    }
489                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
490                Arc::new(move |input: &[P], output: &mut [P]| {
491                    let mut ibuffer = [0.0f32; 3 * N];
492                    let mut obuffer = [0.0f32; 4 * N];
493
494                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(4 * N)) {
495                        let n = luma.len();
496                        let ibuffer = &mut ibuffer[..3 * n];
497                        let obuffer = &mut obuffer[..4 * n];
498                        Self::expand_luma_rgb(luma, ibuffer);
499                        tr34.transform(ibuffer, obuffer).expect("transform failed");
500                        Self::clamp_rgba(obuffer, output);
501                    }
502                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
503                Arc::new(move |input: &[P], output: &mut [P]| {
504                    let mut ibuffer = [0.0f32; 4 * N];
505                    let mut obuffer = [0.0f32; 3 * N];
506
507                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(3 * N)) {
508                        let n = luma.len() / 2;
509                        let ibuffer = &mut ibuffer[..4 * n];
510                        let obuffer = &mut obuffer[..3 * n];
511                        Self::expand_luma_rgba(luma, ibuffer);
512                        tr43.transform(ibuffer, obuffer).expect("transform failed");
513                        Self::clamp_rgb(obuffer, output);
514                    }
515                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
516                Arc::new(move |input: &[P], output: &mut [P]| {
517                    let mut ibuffer = [0.0f32; 4 * N];
518                    let mut obuffer = [0.0f32; 4 * N];
519
520                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(4 * N)) {
521                        let n = luma.len() / 2;
522                        let ibuffer = &mut ibuffer[..4 * n];
523                        let obuffer = &mut obuffer[..4 * n];
524                        Self::expand_luma_rgba(luma, ibuffer);
525                        tr44.transform(ibuffer, obuffer).expect("transform failed");
526                        Self::clamp_rgba(obuffer, output);
527                    }
528                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
529            ]
530        };
531
532        // rgb-luma transforms contract Rgb to Luma (and Rgba to LumaAlpha)
533        let rgb_luma = {
534            let [tr33, tr34, tr43, tr44] = f32.clone();
535
536            [
537                Arc::new(move |input: &[P], output: &mut [P]| {
538                    debug_assert_eq!(input.len() / 3, output.len());
539
540                    let mut ibuffer = [0.0f32; 3 * N];
541                    let mut obuffer = [0.0f32; 3 * N];
542
543                    for (rgb, output) in input.chunks(3 * N).zip(output.chunks_mut(N)) {
544                        let n = output.len();
545                        let ibuffer = &mut ibuffer[..3 * n];
546                        let obuffer = &mut obuffer[..3 * n];
547                        Self::expand_rgb(rgb, ibuffer);
548                        tr33.transform(ibuffer, obuffer).expect("transform failed");
549                        Self::clamp_rgb_luma(obuffer, output, output_coef);
550                    }
551                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
552                Arc::new(move |input: &[P], output: &mut [P]| {
553                    debug_assert_eq!(input.len() / 3, output.len() / 2);
554
555                    let mut ibuffer = [0.0f32; 3 * N];
556                    let mut obuffer = [0.0f32; 4 * N];
557
558                    for (rgb, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) {
559                        let n = output.len() / 2;
560                        let ibuffer = &mut ibuffer[..3 * n];
561                        let obuffer = &mut obuffer[..4 * n];
562                        Self::expand_rgb(rgb, ibuffer);
563                        tr34.transform(ibuffer, obuffer).expect("transform failed");
564                        Self::clamp_rgba_luma(obuffer, output, output_coef);
565                    }
566                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
567                Arc::new(move |input: &[P], output: &mut [P]| {
568                    debug_assert_eq!(input.len() / 4, output.len());
569
570                    let mut ibuffer = [0.0f32; 4 * N];
571                    let mut obuffer = [0.0f32; 3 * N];
572
573                    for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(N)) {
574                        let n = output.len();
575                        let ibuffer = &mut ibuffer[..4 * n];
576                        let obuffer = &mut obuffer[..3 * n];
577                        Self::expand_rgba(rgba, ibuffer);
578                        tr43.transform(ibuffer, obuffer).expect("transform failed");
579                        Self::clamp_rgb_luma(obuffer, output, output_coef);
580                    }
581                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
582                Arc::new(move |input: &[P], output: &mut [P]| {
583                    debug_assert_eq!(input.len() / 4, output.len() / 2);
584
585                    let mut ibuffer = [0.0f32; 4 * N];
586                    let mut obuffer = [0.0f32; 4 * N];
587
588                    for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) {
589                        let n = output.len() / 2;
590                        let ibuffer = &mut ibuffer[..4 * n];
591                        let obuffer = &mut obuffer[..4 * n];
592                        Self::expand_rgba(rgba, ibuffer);
593                        tr44.transform(ibuffer, obuffer).expect("transform failed");
594                        Self::clamp_rgba_luma(obuffer, output, output_coef);
595                    }
596                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
597            ]
598        };
599
600        // luma-luma both expand and contract
601        let luma_luma = {
602            let [tr33, tr34, tr43, tr44] = f32.clone();
603
604            [
605                Arc::new(move |input: &[P], output: &mut [P]| {
606                    debug_assert_eq!(input.len(), output.len());
607                    let mut ibuffer = [0.0f32; 3 * N];
608                    let mut obuffer = [0.0f32; 3 * N];
609
610                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(N)) {
611                        let n = luma.len();
612                        let ibuffer = &mut ibuffer[..3 * n];
613                        let obuffer = &mut obuffer[..3 * n];
614                        Self::expand_luma_rgb(luma, ibuffer);
615                        tr33.transform(ibuffer, obuffer).expect("transform failed");
616                        Self::clamp_rgb_luma(obuffer, output, output_coef);
617                    }
618                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
619                Arc::new(move |input: &[P], output: &mut [P]| {
620                    debug_assert_eq!(input.len(), output.len() / 2);
621                    let mut ibuffer = [0.0f32; 3 * N];
622                    let mut obuffer = [0.0f32; 4 * N];
623
624                    for (luma, output) in input.chunks(N).zip(output.chunks_mut(2 * N)) {
625                        let n = luma.len();
626                        let ibuffer = &mut ibuffer[..3 * n];
627                        let obuffer = &mut obuffer[..4 * n];
628                        Self::expand_luma_rgb(luma, ibuffer);
629                        tr34.transform(ibuffer, obuffer).expect("transform failed");
630                        Self::clamp_rgba_luma(obuffer, output, output_coef);
631                    }
632                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
633                Arc::new(move |input: &[P], output: &mut [P]| {
634                    debug_assert_eq!(input.len() / 2, output.len());
635                    let mut ibuffer = [0.0f32; 4 * N];
636                    let mut obuffer = [0.0f32; 3 * N];
637
638                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(N)) {
639                        let n = luma.len() / 2;
640                        let ibuffer = &mut ibuffer[..4 * n];
641                        let obuffer = &mut obuffer[..3 * n];
642                        Self::expand_luma_rgba(luma, ibuffer);
643                        tr43.transform(ibuffer, obuffer).expect("transform failed");
644                        Self::clamp_rgb_luma(obuffer, output, output_coef);
645                    }
646                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
647                Arc::new(move |input: &[P], output: &mut [P]| {
648                    debug_assert_eq!(input.len() / 2, output.len() / 2);
649                    let mut ibuffer = [0.0f32; 4 * N];
650                    let mut obuffer = [0.0f32; 4 * N];
651
652                    for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(2 * N)) {
653                        let n = luma.len() / 2;
654                        let ibuffer = &mut ibuffer[..4 * n];
655                        let obuffer = &mut obuffer[..4 * n];
656                        Self::expand_luma_rgba(luma, ibuffer);
657                        tr44.transform(ibuffer, obuffer).expect("transform failed");
658                        Self::clamp_rgba_luma(obuffer, output, output_coef);
659                    }
660                }) as Arc<dyn Fn(&[P], &mut [P]) + Send + Sync>,
661            ]
662        };
663
664        Some(RgbTransforms {
665            slices,
666            luma_rgb,
667            rgb_luma,
668            luma_luma,
669        })
670    }
671
672    pub(crate) fn transform_dynamic(&self, lhs: &mut DynamicImage, rhs: &DynamicImage) {
673        const STEP: usize = 256;
674
675        let mut ibuffer = [0.0f32; 4 * STEP];
676        let mut obuffer = [0.0f32; 4 * STEP];
677
678        let pixels = (u64::from(lhs.width()) * u64::from(lhs.height())) as usize;
679
680        let input_samples;
681        let output_samples;
682
683        let inner_transform = match (
684            LayoutWithColor::from(lhs.color()),
685            LayoutWithColor::from(rhs.color()),
686        ) {
687            (
688                LayoutWithColor::Luma | LayoutWithColor::Rgb,
689                LayoutWithColor::Luma | LayoutWithColor::Rgb,
690            ) => {
691                output_samples = 3;
692                input_samples = 3;
693                &*self.f32.slices[0]
694            }
695            (
696                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
697                LayoutWithColor::Luma | LayoutWithColor::Rgb,
698            ) => {
699                output_samples = 4;
700                input_samples = 3;
701                &*self.f32.slices[1]
702            }
703            (
704                LayoutWithColor::Luma | LayoutWithColor::Rgb,
705                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
706            ) => {
707                output_samples = 3;
708                input_samples = 4;
709                &*self.f32.slices[2]
710            }
711            (
712                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
713                LayoutWithColor::LumaAlpha | LayoutWithColor::Rgba,
714            ) => {
715                output_samples = 4;
716                input_samples = 4;
717                &*self.f32.slices[3]
718            }
719        };
720
721        for start_idx in (0..pixels).step_by(STEP) {
722            let end_idx = (start_idx + STEP).min(pixels);
723            let count = end_idx - start_idx;
724
725            // Expand pixels from `other` into `ibuffer`. All of these have different types, so
726            // here's two large switch statements.
727            match rhs {
728                DynamicImage::ImageLuma8(buf) => {
729                    CicpTransform::expand_luma_rgb(
730                        &buf.inner_pixels()[start_idx..end_idx],
731                        &mut ibuffer[..3 * count],
732                    );
733                }
734                DynamicImage::ImageLumaA8(buf) => {
735                    CicpTransform::expand_luma_rgba(
736                        &buf.inner_pixels()[2 * start_idx..2 * end_idx],
737                        &mut ibuffer[..4 * count],
738                    );
739                }
740                DynamicImage::ImageRgb8(buf) => {
741                    CicpTransform::expand_rgb(
742                        &buf.inner_pixels()[3 * start_idx..3 * end_idx],
743                        &mut ibuffer[..3 * count],
744                    );
745                }
746                DynamicImage::ImageRgba8(buf) => {
747                    CicpTransform::expand_rgba(
748                        &buf.inner_pixels()[4 * start_idx..4 * end_idx],
749                        &mut ibuffer[..4 * count],
750                    );
751                }
752                DynamicImage::ImageLuma16(buf) => {
753                    CicpTransform::expand_luma_rgb(
754                        &buf.inner_pixels()[start_idx..end_idx],
755                        &mut ibuffer[..3 * count],
756                    );
757                }
758                DynamicImage::ImageLumaA16(buf) => {
759                    CicpTransform::expand_luma_rgba(
760                        &buf.inner_pixels()[2 * start_idx..2 * end_idx],
761                        &mut ibuffer[..4 * count],
762                    );
763                }
764                DynamicImage::ImageRgb16(buf) => {
765                    CicpTransform::expand_rgb(
766                        &buf.inner_pixels()[3 * start_idx..3 * end_idx],
767                        &mut ibuffer[..3 * count],
768                    );
769                }
770
771                DynamicImage::ImageRgba16(buf) => {
772                    CicpTransform::expand_rgba(
773                        &buf.inner_pixels()[4 * start_idx..4 * end_idx],
774                        &mut ibuffer[..4 * count],
775                    );
776                }
777                DynamicImage::ImageRgb32F(buf) => {
778                    CicpTransform::expand_rgb(
779                        &buf.inner_pixels()[3 * start_idx..3 * end_idx],
780                        &mut ibuffer[..3 * count],
781                    );
782                }
783                DynamicImage::ImageRgba32F(buf) => {
784                    CicpTransform::expand_rgba(
785                        &buf.inner_pixels()[4 * start_idx..4 * end_idx],
786                        &mut ibuffer[..4 * count],
787                    );
788                }
789            }
790
791            let islice = &ibuffer[..input_samples * count];
792            let oslice = &mut obuffer[..output_samples * count];
793
794            inner_transform(islice, oslice);
795
796            match lhs {
797                DynamicImage::ImageLuma8(buf) => {
798                    CicpTransform::clamp_rgb_luma(
799                        &obuffer[..3 * count],
800                        &mut buf.inner_pixels_mut()[start_idx..end_idx],
801                        self.output_coefs,
802                    );
803                }
804                DynamicImage::ImageLumaA8(buf) => {
805                    CicpTransform::clamp_rgba_luma(
806                        &obuffer[..4 * count],
807                        &mut buf.inner_pixels_mut()[2 * start_idx..2 * end_idx],
808                        self.output_coefs,
809                    );
810                }
811                DynamicImage::ImageRgb8(buf) => {
812                    CicpTransform::clamp_rgb(
813                        &obuffer[..3 * count],
814                        &mut buf.inner_pixels_mut()[3 * start_idx..3 * end_idx],
815                    );
816                }
817                DynamicImage::ImageRgba8(buf) => {
818                    CicpTransform::clamp_rgba(
819                        &obuffer[..4 * count],
820                        &mut buf.inner_pixels_mut()[4 * start_idx..4 * end_idx],
821                    );
822                }
823                DynamicImage::ImageLuma16(buf) => {
824                    CicpTransform::clamp_rgb_luma(
825                        &obuffer[..3 * count],
826                        &mut buf.inner_pixels_mut()[start_idx..end_idx],
827                        self.output_coefs,
828                    );
829                }
830                DynamicImage::ImageLumaA16(buf) => {
831                    CicpTransform::clamp_rgba_luma(
832                        &obuffer[..4 * count],
833                        &mut buf.inner_pixels_mut()[2 * start_idx..2 * end_idx],
834                        self.output_coefs,
835                    );
836                }
837                DynamicImage::ImageRgb16(buf) => {
838                    CicpTransform::clamp_rgba(
839                        &obuffer[..3 * count],
840                        &mut buf.inner_pixels_mut()[3 * start_idx..3 * end_idx],
841                    );
842                }
843
844                DynamicImage::ImageRgba16(buf) => {
845                    CicpTransform::clamp_rgba(
846                        &obuffer[..4 * count],
847                        &mut buf.inner_pixels_mut()[4 * start_idx..4 * end_idx],
848                    );
849                }
850                DynamicImage::ImageRgb32F(buf) => {
851                    CicpTransform::clamp_rgb(
852                        &obuffer[..3 * count],
853                        &mut buf.inner_pixels_mut()[3 * start_idx..3 * end_idx],
854                    );
855                }
856                DynamicImage::ImageRgba32F(buf) => {
857                    CicpTransform::clamp_rgba(
858                        &obuffer[..4 * count],
859                        &mut buf.inner_pixels_mut()[4 * start_idx..4 * end_idx],
860                    );
861                }
862            }
863        }
864    }
865
866    // Note on this design: When we dispatch into this function, we have a `Self` type that is
867    // qualified to have the appropriate bound here. However, for the target type of the transform
868    // we have, e.g., `Rgba<Self::Subpixel>`. Now we know that these are also with color for the
869    // most part but we can not convince the compiler (indeed, there is or was an asymmetry with
870    // gray pixels where they do not have float equivalents). It is hence necessary to provide the
871    // output layout as a runtime parameter, not a compile-time type.
872    pub(crate) fn select_transform_u8<P: SealedPixelWithColorType<TransformableSubpixel = u8>>(
873        &self,
874        into: LayoutWithColor,
875    ) -> &Arc<CicpApplicable<'static, u8>> {
876        self.u8.select_transform::<P>(into)
877    }
878
879    pub(crate) fn select_transform_u16<O: SealedPixelWithColorType<TransformableSubpixel = u16>>(
880        &self,
881        into: LayoutWithColor,
882    ) -> &Arc<CicpApplicable<'static, u16>> {
883        self.u16.select_transform::<O>(into)
884    }
885
886    pub(crate) fn select_transform_f32<O: SealedPixelWithColorType<TransformableSubpixel = f32>>(
887        &self,
888        into: LayoutWithColor,
889    ) -> &Arc<CicpApplicable<'static, f32>> {
890        self.f32.select_transform::<O>(into)
891    }
892
893    const LAYOUTS: [(LayoutWithColor, LayoutWithColor); 4] = [
894        (LayoutWithColor::Rgb, LayoutWithColor::Rgb),
895        (LayoutWithColor::Rgb, LayoutWithColor::Rgba),
896        (LayoutWithColor::Rgba, LayoutWithColor::Rgb),
897        (LayoutWithColor::Rgba, LayoutWithColor::Rgba),
898    ];
899
900    pub(crate) fn expand_luma_rgb<P: ColorComponentForCicp>(luma: &[P], rgb: &mut [f32]) {
901        for (&pix, rgb) in luma.iter().zip(rgb.chunks_exact_mut(3)) {
902            let luma = pix.expand_to_f32();
903            rgb[0] = luma;
904            rgb[1] = luma;
905            rgb[2] = luma;
906        }
907    }
908
909    pub(crate) fn expand_luma_rgba<P: ColorComponentForCicp>(luma: &[P], rgb: &mut [f32]) {
910        for (pix, rgb) in luma.chunks_exact(2).zip(rgb.chunks_exact_mut(4)) {
911            let luma = pix[0].expand_to_f32();
912            rgb[0] = luma;
913            rgb[1] = luma;
914            rgb[2] = luma;
915            rgb[3] = pix[1].expand_to_f32();
916        }
917    }
918
919    pub(crate) fn expand_rgb<P: ColorComponentForCicp>(input: &[P], output: &mut [f32]) {
920        for (&component, val) in input.iter().zip(output) {
921            *val = component.expand_to_f32();
922        }
923    }
924
925    pub(crate) fn expand_rgba<P: ColorComponentForCicp>(input: &[P], output: &mut [f32]) {
926        for (&component, val) in input.iter().zip(output) {
927            *val = component.expand_to_f32();
928        }
929    }
930
931    pub(crate) fn clamp_rgb<P: ColorComponentForCicp>(input: &[f32], output: &mut [P]) {
932        // Everything is mapped..
933        for (&component, val) in input.iter().zip(output) {
934            *val = P::clamp_from_f32(component);
935        }
936    }
937
938    pub(crate) fn clamp_rgba<P: ColorComponentForCicp>(input: &[f32], output: &mut [P]) {
939        for (&component, val) in input.iter().zip(output) {
940            *val = P::clamp_from_f32(component);
941        }
942    }
943
944    pub(crate) fn clamp_rgb_luma<P: ColorComponentForCicp>(
945        input: &[f32],
946        output: &mut [P],
947        coef: [f32; 3],
948    ) {
949        for (rgb, pix) in input.chunks_exact(3).zip(output) {
950            let mut luma = 0.0;
951
952            for (&component, coef) in rgb.iter().zip(coef) {
953                luma = multiply_accumulate(luma, component, coef);
954            }
955
956            *pix = P::clamp_from_f32(luma);
957        }
958    }
959
960    pub(crate) fn clamp_rgba_luma<P: ColorComponentForCicp>(
961        input: &[f32],
962        output: &mut [P],
963        coef: [f32; 3],
964    ) {
965        for (rgba, pix) in input.chunks_exact(4).zip(output.chunks_exact_mut(2)) {
966            let mut luma = 0.0;
967
968            for (&component, coef) in rgba[..3].iter().zip(coef) {
969                luma = multiply_accumulate(luma, component, coef);
970            }
971
972            pix[0] = P::clamp_from_f32(luma);
973            pix[1] = P::clamp_from_f32(rgba[3]);
974        }
975    }
976}
977
978impl CicpRgb {
979    /// Internal utility for converting color buffers of different pixel representations, assuming
980    /// they have this same cicp. This method returns a buffer, avoiding the pre-zeroing
981    /// the vector.
982    pub(crate) fn cast_pixels<FromColor, IntoColor>(
983        &self,
984        buffer: &[FromColor::Subpixel],
985        // Since this is not performance sensitive, we can use a dyn closure here instead of an
986        // impl closure just in case we call this from multiple different paths.
987        color_space_fallback: &dyn Fn() -> [f32; 3],
988    ) -> Vec<IntoColor::Subpixel>
989    where
990        FromColor: Pixel + SealedPixelWithColorType<TransformableSubpixel = FromColor::Subpixel>,
991        IntoColor: Pixel,
992        IntoColor: CicpPixelCast<FromColor>,
993        FromColor::Subpixel: ColorComponentForCicp,
994        IntoColor::Subpixel: ColorComponentForCicp + FromPrimitive<FromColor::Subpixel>,
995    {
996        use crate::traits::private::PrivateToken;
997        let from_layout = <FromColor as SealedPixelWithColorType>::layout(PrivateToken);
998        let into_layout = <IntoColor as SealedPixelWithColorType>::layout(PrivateToken);
999
1000        let mut output = match self.cast_pixels_from_subpixels(buffer, from_layout, into_layout) {
1001            Ok(ok) => return ok,
1002            Err(buffer) => buffer,
1003        };
1004
1005        // If we get here we need to transform through Rgb(a) 32F
1006        let color_space_coefs = self
1007            .derived_luminance()
1008            // Since `cast_pixels` must be infallible we have no choice but to fallback to
1009            // something here. This something is chosen by the caller, which would allow them to
1010            // detect it has happened.
1011            .unwrap_or_else(color_space_fallback);
1012
1013        let pixels = buffer.len() / from_layout.channels();
1014
1015        // All of the following is done in-place; so we must allow the buffer space in which the
1016        // output is written ahead of time although such initialization is technically redundant.
1017        // We best do this once to allow for a very efficient memset initialization.
1018        output.resize(
1019            pixels * into_layout.channels(),
1020            <IntoColor::Subpixel as Primitive>::DEFAULT_MIN_VALUE,
1021        );
1022
1023        Self::cast_pixels_by_fallback(
1024            buffer,
1025            output.as_mut_slice(),
1026            from_layout,
1027            into_layout,
1028            color_space_coefs,
1029        );
1030        output
1031    }
1032
1033    fn cast_pixels_by_fallback<
1034        From: Primitive + ColorComponentForCicp,
1035        Into: ColorComponentForCicp,
1036    >(
1037        buffer: &[From],
1038        output: &mut [Into],
1039        from_layout: LayoutWithColor,
1040        into_layout: LayoutWithColor,
1041        color_space_coefs: [f32; 3],
1042    ) {
1043        use LayoutWithColor as Layout;
1044
1045        const STEP: usize = 256;
1046        let pixels = buffer.len() / from_layout.channels();
1047
1048        let mut ibuffer = [0.0f32; 4 * STEP];
1049        let mut obuffer = [0.0f32; 4 * STEP];
1050
1051        let ibuf_step = match from_layout {
1052            Layout::Rgb | Layout::Luma => 3,
1053            Layout::Rgba | Layout::LumaAlpha => 4,
1054        };
1055
1056        let obuf_step = match into_layout {
1057            Layout::Rgb | Layout::Luma => 3,
1058            Layout::Rgba | Layout::LumaAlpha => 4,
1059        };
1060
1061        for start_idx in (0..pixels).step_by(STEP) {
1062            let end_idx = (start_idx + STEP).min(pixels);
1063            let count = end_idx - start_idx;
1064
1065            let ibuffer = &mut ibuffer[..ibuf_step * count];
1066
1067            match from_layout {
1068                Layout::Rgb => {
1069                    CicpTransform::expand_rgb(&buffer[3 * start_idx..3 * end_idx], ibuffer)
1070                }
1071                Layout::Rgba => {
1072                    CicpTransform::expand_rgba(&buffer[4 * start_idx..4 * end_idx], ibuffer)
1073                }
1074                Layout::Luma => {
1075                    CicpTransform::expand_luma_rgb(&buffer[start_idx..end_idx], ibuffer)
1076                }
1077                Layout::LumaAlpha => {
1078                    CicpTransform::expand_luma_rgba(&buffer[2 * start_idx..2 * end_idx], ibuffer)
1079                }
1080            }
1081
1082            // Add or subtract the alpha channel. We could do that as part of the store but this
1083            // keeps the code simpler—there is a one-to-one correspondence with the methods needed
1084            // for a full conversion.
1085            let obuffer = match (ibuf_step, obuf_step) {
1086                (3, 4) => {
1087                    for (rgb, rgba) in ibuffer
1088                        .chunks_exact(3)
1089                        .zip(obuffer.chunks_exact_mut(4))
1090                        .take(count)
1091                    {
1092                        rgba[0] = rgb[0];
1093                        rgba[1] = rgb[1];
1094                        rgba[2] = rgb[2];
1095                        rgba[3] = 1.0;
1096                    }
1097
1098                    &obuffer[..4 * count]
1099                }
1100                (4, 3) => {
1101                    for (rgba, rgb) in ibuffer
1102                        .chunks_exact(4)
1103                        .zip(obuffer.chunks_exact_mut(3))
1104                        .take(count)
1105                    {
1106                        rgb[0] = rgba[0];
1107                        rgb[1] = rgba[1];
1108                        rgb[2] = rgba[2];
1109                    }
1110
1111                    &obuffer[..3 * count]
1112                }
1113                (n, m) => {
1114                    debug_assert_eq!(n, m);
1115                    &ibuffer[..m * count]
1116                }
1117            };
1118
1119            match into_layout {
1120                Layout::Rgb => {
1121                    CicpTransform::clamp_rgb(obuffer, &mut output[3 * start_idx..3 * end_idx]);
1122                }
1123                Layout::Rgba => {
1124                    CicpTransform::clamp_rgba(obuffer, &mut output[4 * start_idx..4 * end_idx]);
1125                }
1126                Layout::Luma => {
1127                    CicpTransform::clamp_rgb_luma(
1128                        obuffer,
1129                        &mut output[start_idx..end_idx],
1130                        color_space_coefs,
1131                    );
1132                }
1133                Layout::LumaAlpha => {
1134                    CicpTransform::clamp_rgba_luma(
1135                        obuffer,
1136                        &mut output[2 * start_idx..2 * end_idx],
1137                        color_space_coefs,
1138                    );
1139                }
1140            }
1141        }
1142    }
1143
1144    /// Make sure this is only monomorphized for subpixel combinations, not for every pixel
1145    /// combination! There's ample time to do that in `cast_pixels`.
1146    pub(crate) fn cast_pixels_from_subpixels<FromSubpixel, IntoSubpixel>(
1147        &self,
1148        buffer: &[FromSubpixel],
1149        from_layout: LayoutWithColor,
1150        into_layout: LayoutWithColor,
1151    ) -> Result<Vec<IntoSubpixel>, Vec<IntoSubpixel>>
1152    where
1153        FromSubpixel: ColorComponentForCicp,
1154        IntoSubpixel: ColorComponentForCicp + FromPrimitive<FromSubpixel> + Primitive,
1155    {
1156        use crate::traits::private::LayoutWithColor as Layout;
1157
1158        assert!(buffer.len() % from_layout.channels() == 0);
1159        let pixels = buffer.len() / from_layout.channels();
1160
1161        let mut output: Vec<IntoSubpixel> = vec_try_with_capacity(pixels * into_layout.channels())
1162            // Not entirely failsafe, if you expand luma to rgba you can get a factor of 4 but at
1163            // least this will not overflow. And that's why I'm a fan of in-place operations.
1164            .expect("input layout already allocated with appropriate layout");
1165        let map_channel = <IntoSubpixel as FromPrimitive<FromSubpixel>>::from_primitive;
1166
1167        match (from_layout, into_layout) {
1168            // First detect if we can use simple channel-by-channel component conversion.
1169            (Layout::Rgb, Layout::Rgb)
1170            | (Layout::Rgba, Layout::Rgba)
1171            | (Layout::Luma, Layout::Luma)
1172            | (Layout::LumaAlpha, Layout::LumaAlpha) => {
1173                output.extend(buffer.iter().copied().map(map_channel));
1174            }
1175            (Layout::Rgb, Layout::Rgba) => {
1176                // Use `as_chunks` with Rust 1.88
1177                output.extend(buffer.chunks_exact(3).flat_map(|rgb| {
1178                    let &rgb: &[_; 3] = rgb.try_into().unwrap();
1179                    let [r, g, b] = rgb.map(map_channel);
1180                    let a = <IntoSubpixel as Primitive>::DEFAULT_MAX_VALUE;
1181                    [r, g, b, a]
1182                }));
1183            }
1184            (Layout::Rgba, Layout::Rgb) => {
1185                output.extend(buffer.chunks_exact(4).flat_map(|rgb| {
1186                    let &[r, g, b, _]: &[_; 4] = rgb.try_into().unwrap();
1187                    [r, g, b].map(map_channel)
1188                }));
1189            }
1190            (Layout::Luma, Layout::LumaAlpha) => {
1191                output.extend(buffer.iter().copied().flat_map(|luma| {
1192                    let l = map_channel(luma);
1193                    let a = <IntoSubpixel as Primitive>::DEFAULT_MAX_VALUE;
1194                    [l, a]
1195                }));
1196            }
1197            (Layout::LumaAlpha, Layout::Luma) => {
1198                output.extend(buffer.chunks_exact(2).map(|rgb| {
1199                    let &[luma, _]: &[_; 2] = rgb.try_into().unwrap();
1200                    map_channel(luma)
1201                }));
1202            }
1203            _ => return Err(output),
1204        }
1205
1206        Ok(output)
1207    }
1208}
1209
1210/// Color types that can be converted by [`CicpRgb::cast_pixels`].
1211///
1212/// This is a utility to avoid dealing with lots of bounds everywhere. In the actual implementation
1213/// we avoid the concrete pixel types and care just about the layout (as a runtime property) and
1214/// the channel type to be promotable into a float for normalization. If the pixels have layouts
1215/// that are convertible with intra-channel numerics we instead try and promote the channels via
1216/// `Primitive` instead.
1217pub(crate) trait CicpPixelCast<FromColor>
1218where
1219    // Ensure we can get components from both, get the layout, and that all components are
1220    // compatible with our intermediate connection space (rgba32f).
1221    Self: Pixel + SealedPixelWithColorType<TransformableSubpixel = <Self as Pixel>::Subpixel>,
1222    FromColor:
1223        Pixel + SealedPixelWithColorType<TransformableSubpixel = <FromColor as Pixel>::Subpixel>,
1224    Self::Subpixel: ColorComponentForCicp + FromPrimitive<FromColor::Subpixel>,
1225    FromColor::Subpixel: ColorComponentForCicp,
1226{
1227}
1228
1229impl<FromColor, IntoColor> CicpPixelCast<FromColor> for IntoColor
1230where
1231    IntoColor: Pixel + SealedPixelWithColorType<TransformableSubpixel = IntoColor::Subpixel>,
1232    FromColor: Pixel + SealedPixelWithColorType<TransformableSubpixel = FromColor::Subpixel>,
1233    IntoColor::Subpixel: ColorComponentForCicp + FromPrimitive<FromColor::Subpixel>,
1234    FromColor::Subpixel: ColorComponentForCicp,
1235{
1236}
1237
1238pub(crate) trait ColorComponentForCicp: Copy {
1239    fn expand_to_f32(self) -> f32;
1240
1241    fn clamp_from_f32(val: f32) -> Self;
1242}
1243
1244impl ColorComponentForCicp for u8 {
1245    fn expand_to_f32(self) -> f32 {
1246        const R: f32 = 1.0 / u8::MAX as f32;
1247        self as f32 * R
1248    }
1249
1250    #[inline]
1251    fn clamp_from_f32(val: f32) -> Self {
1252        // Note: saturating conversion does the clamp for us
1253        (val * Self::MAX as f32).round() as u8
1254    }
1255}
1256
1257impl ColorComponentForCicp for u16 {
1258    fn expand_to_f32(self) -> f32 {
1259        const R: f32 = 1.0 / u16::MAX as f32;
1260        self as f32 * R
1261    }
1262
1263    #[inline]
1264    fn clamp_from_f32(val: f32) -> Self {
1265        // Note: saturating conversion does the clamp for us
1266        (val * Self::MAX as f32).round() as u16
1267    }
1268}
1269
1270impl ColorComponentForCicp for f32 {
1271    fn expand_to_f32(self) -> f32 {
1272        self
1273    }
1274
1275    fn clamp_from_f32(val: f32) -> Self {
1276        val
1277    }
1278}
1279
1280impl<P> RgbTransforms<P> {
1281    fn select_transform<O: SealedPixelWithColorType>(
1282        &self,
1283        into: LayoutWithColor,
1284    ) -> &Arc<CicpApplicable<'static, P>> {
1285        use crate::traits::private::{LayoutWithColor as Layout, PrivateToken};
1286        let from = O::layout(PrivateToken);
1287
1288        match (from, into) {
1289            (Layout::Rgb, Layout::Rgb) => &self.slices[0],
1290            (Layout::Rgb, Layout::Rgba) => &self.slices[1],
1291            (Layout::Rgba, Layout::Rgb) => &self.slices[2],
1292            (Layout::Rgba, Layout::Rgba) => &self.slices[3],
1293            (Layout::Rgb, Layout::Luma) => &self.rgb_luma[0],
1294            (Layout::Rgb, Layout::LumaAlpha) => &self.rgb_luma[1],
1295            (Layout::Rgba, Layout::Luma) => &self.rgb_luma[2],
1296            (Layout::Rgba, Layout::LumaAlpha) => &self.rgb_luma[3],
1297            (Layout::Luma, Layout::Rgb) => &self.luma_rgb[0],
1298            (Layout::Luma, Layout::Rgba) => &self.luma_rgb[1],
1299            (Layout::LumaAlpha, Layout::Rgb) => &self.luma_rgb[2],
1300            (Layout::LumaAlpha, Layout::Rgba) => &self.luma_rgb[3],
1301            (Layout::Luma, Layout::Luma) => &self.luma_luma[0],
1302            (Layout::Luma, Layout::LumaAlpha) => &self.luma_luma[1],
1303            (Layout::LumaAlpha, Layout::Luma) => &self.luma_luma[2],
1304            (Layout::LumaAlpha, Layout::LumaAlpha) => &self.luma_luma[3],
1305        }
1306    }
1307}
1308
1309impl Cicp {
1310    /// The sRGB color space, BT.709 transfer function and D65 whitepoint.
1311    pub const SRGB: Self = Cicp {
1312        primaries: CicpColorPrimaries::SRgb,
1313        transfer: CicpTransferCharacteristics::SRgb,
1314        matrix: CicpMatrixCoefficients::Identity,
1315        full_range: CicpVideoFullRangeFlag::FullRange,
1316    };
1317
1318    /// SRGB primaries and whitepoint with linear samples.
1319    pub const SRGB_LINEAR: Self = Cicp {
1320        primaries: CicpColorPrimaries::SRgb,
1321        transfer: CicpTransferCharacteristics::Linear,
1322        matrix: CicpMatrixCoefficients::Identity,
1323        full_range: CicpVideoFullRangeFlag::FullRange,
1324    };
1325
1326    /// The  Display-P3 color space, a wide-gamut choice with SMPTE RP 432-2 primaries.
1327    ///
1328    /// Note that this modern Display P3 uses a D65 whitepoint. Use the primaries `SmpteRp431` for
1329    /// the previous standard. The advantage of the new standard is the color system shares its
1330    /// whitepoint with sRGB and BT.2020.
1331    pub const DISPLAY_P3: Self = Cicp {
1332        primaries: CicpColorPrimaries::SmpteRp432,
1333        transfer: CicpTransferCharacteristics::SRgb,
1334        matrix: CicpMatrixCoefficients::Identity,
1335        full_range: CicpVideoFullRangeFlag::FullRange,
1336    };
1337
1338    /// Get an compute representation of an ICC profile for RGB.
1339    ///
1340    /// Note you should *not* be using this profile for export in a file, as discussed below.
1341    ///
1342    /// This is straightforward for Rgb and RgbA representations.
1343    ///
1344    /// Our luma models a Y component of a YCbCr color space. It turns out that ICC V4 does
1345    /// not support pure Luma in any other whitepoint apart from D50 (the native profile
1346    /// connection space). The use of a grayTRC does *not* take the chromatic adaptation
1347    /// matrix into account. Of course we can encode the adaptation into the TRC as a
1348    /// coefficient, the Y component of the product of the whitepoint adaptation matrix
1349    /// inverse and the pcs's whitepoint XYZ, but that is only correct for gray -> gray
1350    /// conversion (and that coefficient should generally be `1`).
1351    ///
1352    /// Hence we use a YCbCr. The data->pcs path could be modelled by ("M" curves, matrix, "B"
1353    /// curves) where B curves or M curves are all the identity, depending on whether constant or
1354    /// non-constant luma is in use. This is a subset of the capabilities that a lutAToBType
1355    /// allows. Unfortunately, this is not implemented in moxcms yet and for efficiency we would
1356    /// like to have a masked `create_transform_*` in which the CbCr channels are discarded /
1357    /// assumed 0 instead of them being in memory. Due to this special case and for supporting
1358    /// conversions between sample types, we implement said promotion as part of conversion to
1359    /// Rgba32F in this crate.
1360    ///
1361    /// For export to file, it would arguably correct to use a carefully crafted gray profile which
1362    /// we may implement in another function. That is, we could setup a tone reproduction curve
1363    /// which maps each sample value (which ICC regards as D50) into XYZ D50 in such a way that it
1364    /// _appears_ with the correct D50 luminance that we would get if we had used the conversion
1365    /// unders its true input whitepoint. The resulting color has a slightly wrong chroma as it is
1366    /// linearly dependent on D50 instead, but it's brightness would be correctly presented. At
1367    /// least for perceptual intent this might be alright.
1368    fn to_moxcms_compute_profile(self) -> Option<ColorProfile> {
1369        let mut rgb = moxcms::ColorProfile::new_srgb();
1370
1371        rgb.update_rgb_colorimetry_from_cicp(moxcms::CicpProfile {
1372            color_primaries: self.primaries.to_moxcms(),
1373            transfer_characteristics: self.transfer.to_moxcms(),
1374            matrix_coefficients: self.matrix.to_moxcms()?,
1375            full_range: match self.full_range {
1376                CicpVideoFullRangeFlag::NarrowRange => false,
1377                CicpVideoFullRangeFlag::FullRange => true,
1378            },
1379        });
1380
1381        Some(ColorProfile { rgb })
1382    }
1383
1384    /// Whether we have invested enough testing to ensure that color values can be assumed to be
1385    /// stable and correspond to an intended effect, in particular if there even is a well-defined
1386    /// meaning to these color spaces.
1387    ///
1388    /// For instance, our current code for the 'luma' equivalent space assumes that the color space
1389    /// has a shared transfer function for all its color components. Also the judgment should not
1390    /// depend on whether we can represent the profile in `moxcms` but rather if we understand the
1391    /// profile well enough so that conversion implemented through another library can be derived.
1392    /// (Consider the case of a builtin transform-while-encoding that may be more performant for a
1393    /// format that does not support CICP or ICC profiles.)
1394    ///
1395    /// A stable profile should also have `derived_luminance` implemented.
1396    pub(crate) const fn qualify_stability(&self) -> bool {
1397        const _: () = {
1398            // Out public constants _should_ be stable.
1399            assert!(Cicp::SRGB.qualify_stability());
1400            assert!(Cicp::SRGB_LINEAR.qualify_stability());
1401            assert!(Cicp::DISPLAY_P3.qualify_stability());
1402        };
1403
1404        matches!(self.full_range, CicpVideoFullRangeFlag::FullRange)
1405            && matches!(
1406                self.matrix,
1407                // For pure RGB color
1408                CicpMatrixCoefficients::Identity
1409                    // The equivalent of our Luma color as a type..
1410                    | CicpMatrixCoefficients::ChromaticityDerivedNonConstant
1411            )
1412            && matches!(
1413                self.primaries,
1414                CicpColorPrimaries::SRgb
1415                    | CicpColorPrimaries::SmpteRp431
1416                    | CicpColorPrimaries::SmpteRp432
1417                    | CicpColorPrimaries::Bt601
1418                    | CicpColorPrimaries::Rgb240m
1419            )
1420            && matches!(
1421                self.transfer,
1422                CicpTransferCharacteristics::SRgb
1423                    | CicpTransferCharacteristics::Bt709
1424                    | CicpTransferCharacteristics::Bt601
1425                    | CicpTransferCharacteristics::Linear
1426            )
1427    }
1428
1429    /// Discard matrix and range information.
1430    pub(crate) const fn into_rgb(self) -> CicpRgb {
1431        CicpRgb {
1432            primaries: self.primaries,
1433            transfer: self.transfer,
1434            // NOTE: if we add support for constant luminance (through the CMS having support for
1435            // the Luma->YCbCr->Rgb expansion natively or otherwise) then consider if we should
1436            // track here whether the matrix was `Identity` or `ChromaticityDerivedNonConstant` so
1437            // that the `ImageBuffer::color_space()` function roundtrips the value. It may be
1438            // important to know whether the non-constant chromaticity was an invention by `image`
1439            // or part of the file. The colorimetry is the same either way.
1440            luminance: DerivedLuminance::NonConstant,
1441        }
1442    }
1443
1444    pub(crate) fn try_into_rgb(self) -> Result<CicpRgb, ImageError> {
1445        if Cicp::from(self.into_rgb()) != self {
1446            Err(ImageError::Parameter(ParameterError::from_kind(
1447                ParameterErrorKind::RgbCicpRequired(self),
1448            )))
1449        } else {
1450            Ok(self.into_rgb())
1451        }
1452    }
1453}
1454
1455impl CicpRgb {
1456    /// Calculate the luminance cofactors according to Rec H.273 (39) and (40).
1457    ///
1458    /// Returns cofactors for red, green, and blue in that order.
1459    pub(crate) fn derived_luminance(&self) -> Option<[f32; 3]> {
1460        let primaries = match self.primaries {
1461            CicpColorPrimaries::SRgb => moxcms::ColorPrimaries::BT_709,
1462            CicpColorPrimaries::RgbM => moxcms::ColorPrimaries::BT_470M,
1463            CicpColorPrimaries::RgbB => moxcms::ColorPrimaries::BT_470BG,
1464            CicpColorPrimaries::Bt601 => moxcms::ColorPrimaries::BT_601,
1465            CicpColorPrimaries::Rgb240m => moxcms::ColorPrimaries::SMPTE_240,
1466            CicpColorPrimaries::GenericFilm => moxcms::ColorPrimaries::GENERIC_FILM,
1467            CicpColorPrimaries::Rgb2020 => moxcms::ColorPrimaries::BT_2020,
1468            CicpColorPrimaries::Xyz => moxcms::ColorPrimaries::XYZ,
1469            CicpColorPrimaries::SmpteRp431 => moxcms::ColorPrimaries::DISPLAY_P3,
1470            CicpColorPrimaries::SmpteRp432 => moxcms::ColorPrimaries::DISPLAY_P3,
1471            CicpColorPrimaries::Industry22 => moxcms::ColorPrimaries::EBU_3213,
1472            CicpColorPrimaries::Unspecified => return None,
1473        };
1474
1475        const ILLUMINANT_C: moxcms::Chromaticity = moxcms::Chromaticity::new(0.310, 0.316);
1476
1477        let whitepoint = match self.primaries {
1478            CicpColorPrimaries::SRgb => moxcms::Chromaticity::D65,
1479            CicpColorPrimaries::RgbM => ILLUMINANT_C,
1480            CicpColorPrimaries::RgbB => moxcms::Chromaticity::D65,
1481            CicpColorPrimaries::Bt601 => moxcms::Chromaticity::D65,
1482            CicpColorPrimaries::Rgb240m => moxcms::Chromaticity::D65,
1483            CicpColorPrimaries::GenericFilm => ILLUMINANT_C,
1484            CicpColorPrimaries::Rgb2020 => moxcms::Chromaticity::D65,
1485            CicpColorPrimaries::Xyz => moxcms::Chromaticity::new(1. / 3., 1. / 3.),
1486            CicpColorPrimaries::SmpteRp431 => moxcms::Chromaticity::new(0.314, 0.351),
1487            CicpColorPrimaries::SmpteRp432 => moxcms::Chromaticity::D65,
1488            CicpColorPrimaries::Industry22 => moxcms::Chromaticity::D65,
1489            CicpColorPrimaries::Unspecified => return None,
1490        };
1491
1492        let matrix = primaries.transform_to_xyz(whitepoint);
1493
1494        // Our result is the Y row of this matrix.
1495        Some(matrix.v[1])
1496    }
1497}
1498
1499impl From<CicpRgb> for Cicp {
1500    fn from(cicp: CicpRgb) -> Self {
1501        Cicp {
1502            primaries: cicp.primaries,
1503            transfer: cicp.transfer,
1504            matrix: CicpMatrixCoefficients::Identity,
1505            full_range: CicpVideoFullRangeFlag::FullRange,
1506        }
1507    }
1508}
1509
1510/// An RGB profile with its related (same tone-mapping) gray profile.
1511///
1512/// This is the whole input information which we must be able to pass to the CMS in a support
1513/// transform, to handle all possible combinations of `ColorType` pixels that can be thrown at us.
1514/// For instance, in a previous iteration we had a separate gray profile here (but now handle that
1515/// internally by expansion to RGB through an YCbCr). Future iterations may add additional structs
1516/// to be computed for validating `CicpTransform::new`.
1517struct ColorProfile {
1518    rgb: moxcms::ColorProfile,
1519}
1520
1521impl ColorProfile {
1522    fn map_layout(&self, layout: LayoutWithColor) -> (&moxcms::ColorProfile, moxcms::Layout) {
1523        match layout {
1524            LayoutWithColor::Rgb => (&self.rgb, moxcms::Layout::Rgb),
1525            LayoutWithColor::Rgba => (&self.rgb, moxcms::Layout::Rgba),
1526            // See comment in `to_moxcms_profile`.
1527            LayoutWithColor::Luma | LayoutWithColor::LumaAlpha => unreachable!(),
1528        }
1529    }
1530}
1531
1532#[cfg(test)]
1533#[test]
1534fn moxcms() {
1535    let l = moxcms::TransferCharacteristics::Linear;
1536    assert_eq!(l.linearize(1.0), 1.0);
1537    assert_eq!(l.gamma(1.0), 1.0);
1538
1539    assert_eq!(l.gamma(0.5), 0.5);
1540}
1541
1542#[cfg(test)]
1543#[test]
1544fn derived_luminance() {
1545    let luminance = Cicp::SRGB.into_rgb().derived_luminance();
1546    let [kr, kg, kb] = luminance.unwrap();
1547    assert!((kr - 0.2126).abs() < 1e-4);
1548    assert!((kg - 0.7152).abs() < 1e-4);
1549    assert!((kb - 0.0722).abs() < 1e-4);
1550}
1551
1552#[cfg(test)]
1553mod tests {
1554    use super::{Cicp, CicpTransform};
1555    use crate::{Luma, LumaA, Rgb, Rgba};
1556
1557    #[test]
1558    fn can_create_transforms() {
1559        assert!(CicpTransform::new(Cicp::SRGB, Cicp::SRGB).is_some());
1560        assert!(CicpTransform::new(Cicp::SRGB, Cicp::DISPLAY_P3).is_some());
1561        assert!(CicpTransform::new(Cicp::DISPLAY_P3, Cicp::SRGB).is_some());
1562        assert!(CicpTransform::new(Cicp::DISPLAY_P3, Cicp::DISPLAY_P3).is_some());
1563    }
1564
1565    fn no_coefficient_fallback() -> [f32; 3] {
1566        panic!("Fallback coefficients required")
1567    }
1568
1569    #[test]
1570    fn transform_pixels_srgb() {
1571        // Non-constant luminance so:
1572        // Y = dot(rgb, coefs)
1573        let data = [255, 0, 0, 255];
1574        let color = Cicp::SRGB.into_rgb();
1575        let rgba = color.cast_pixels::<Rgba<u8>, Rgb<u8>>(&data, &no_coefficient_fallback);
1576        assert_eq!(rgba, [255, 0, 0]);
1577        let luma = color.cast_pixels::<Rgba<u8>, Luma<u8>>(&data, &no_coefficient_fallback);
1578        assert_eq!(luma, [54]); // 255 * 0.2126
1579        let luma_a = color.cast_pixels::<Rgba<u8>, LumaA<u8>>(&data, &no_coefficient_fallback);
1580        assert_eq!(luma_a, [54, 255]);
1581    }
1582
1583    #[test]
1584    fn transform_pixels_srgb_16() {
1585        // Non-constant luminance so:
1586        // Y = dot(rgb, coefs)
1587        let data = [u16::MAX / 2];
1588        let color = Cicp::SRGB.into_rgb();
1589        let rgba = color.cast_pixels::<Luma<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1590        assert_eq!(rgba, [127; 3]);
1591        let luma = color.cast_pixels::<Luma<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1592        assert_eq!(luma, [127]);
1593        let luma_a = color.cast_pixels::<Luma<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1594        assert_eq!(luma_a, [127, 255]);
1595
1596        let data = [u16::MAX / 2 + 1];
1597        let color = Cicp::SRGB.into_rgb();
1598        let rgba = color.cast_pixels::<Luma<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1599        assert_eq!(rgba, [128; 3]);
1600        let luma = color.cast_pixels::<Luma<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1601        assert_eq!(luma, [128]);
1602        let luma_a = color.cast_pixels::<Luma<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1603        assert_eq!(luma_a, [128, 255]);
1604    }
1605
1606    #[test]
1607    fn transform_pixels_srgb_luma_alpha() {
1608        // Non-constant luminance so:
1609        // Y = dot(rgb, coefs)
1610        let data = [u16::MAX / 2, u16::MAX];
1611        let color = Cicp::SRGB.into_rgb();
1612        let rgba = color.cast_pixels::<LumaA<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1613        assert_eq!(rgba, [127; 3]);
1614        let luma = color.cast_pixels::<LumaA<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1615        assert_eq!(luma, [127]);
1616        let luma = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1617        assert_eq!(luma, [127, u8::MAX]);
1618        let luma_a = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1619        assert_eq!(luma_a, [127, 255]);
1620
1621        let data = [u16::MAX / 2 + 1, u16::MAX];
1622        let color = Cicp::SRGB.into_rgb();
1623        let rgba = color.cast_pixels::<LumaA<u16>, Rgb<u8>>(&data, &no_coefficient_fallback);
1624        assert_eq!(rgba, [128; 3]);
1625        let luma = color.cast_pixels::<LumaA<u16>, Luma<u8>>(&data, &no_coefficient_fallback);
1626        assert_eq!(luma, [128]);
1627        let luma = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1628        assert_eq!(luma, [128, u8::MAX]);
1629        let luma_a = color.cast_pixels::<LumaA<u16>, LumaA<u8>>(&data, &no_coefficient_fallback);
1630        assert_eq!(luma_a, [128, 255]);
1631    }
1632}