Skip to main content

colr/
rgb.rs

1//! Standard RGB color spaces, channel layouts, and their transforms.
2//!
3//! Layout is a parameter of the space: `RgbSpace<P, TF, L>` where `L` defaults
4//! to `Rgba`. Chromatic adaptation uses Bradford per ICC, ACES, and CSS Color 4.
5//!
6//! # Space catalogue
7//!
8//! | Alias          | Primaries | Transfer fn | White |
9//! |----------------|-----------|-------------|-------|
10//! | Srgb           | sRGB/709  | sRGB        | D65   |
11//! | LinearSrgb     | sRGB/709  | Linear      | D65   |
12//! | Rec709         | sRGB/709  | Rec. 709    | D65   |
13//! | DisplayP3      | P3        | sRGB        | D65   |
14//! | LinearP3       | P3        | Linear      | D65   |
15//! | Hdr10          | Rec. 2020 | PQ          | D65   |
16//! | Hlg            | Rec. 2020 | HLG         | D65   |
17//! | LinearRec2020  | Rec. 2020 | Linear      | D65   |
18//! | AcesCg         | AP1       | Linear      | ACES  |
19//! | Aces2065       | AP0       | Linear      | ACES  |
20//! | AcesCc         | AP1       | ACEScc      | ACES  |
21//! | AcesCct        | AP1       | ACEScct     | ACES  |
22//! | ProPhoto       | ProPhoto  | ProPhoto    | D50   |
23//! | LinearProPhoto | ProPhoto  | Linear      | D50   |
24//! | DciP3          | DCI-P3    | gamma 2.6   | DCI   |
25//! | P3D65Gamma26   | P3        | gamma 2.6   | D65   |
26
27use core::marker::PhantomData;
28
29use crate::adaptation::Bradford;
30use crate::illuminant::Illuminant;
31use crate::math::{DefaultMath, Mat3};
32use crate::primaries::{
33    AcesAp0Primaries, AcesAp1Primaries, DciP3Primaries, P3Primaries, Primaries, PrimariesToXyz, ProPhotoPrimaries,
34    Rec2020Primaries, SrgbPrimaries,
35};
36use crate::transfer::{
37    AcesCcTf, AcesCctTf, DciP3Tf, HlgTf, IsDisplayReferred, IsLinearEncoding, IsSceneReferred, LinearTf, PqTf,
38    ProPhotoTf, Rec709Tf, SrgbTf, TransferFunction,
39};
40use crate::xyz::Xyz;
41use crate::{
42    Asserts, Color, ColorSpace, DisplayReferred, LinearLight, OutOfGamut, SceneReferred, Transform, TryTransform,
43};
44
45/// Maps all N channels to their storage indices.
46///
47/// INDICES contains the storage position of each logical channel in order.
48/// For RGB layouts the first three are R, G, B. ALPHA points to which
49/// position within INDICES holds alpha, if any.
50pub trait ChannelMap<const N: usize>: 'static {
51    /// Storage positions of all N channels in logical order.
52    const INDICES: [usize; N];
53    /// Which position within INDICES holds alpha, if any.
54    const ALPHA: Option<usize>;
55}
56
57macro_rules! define_layout {
58    ($name:ident, [$r:expr, $g:expr, $b:expr, $a:expr]) => {
59        #[doc = concat!("Four-channel layout. R=`", stringify!($r), "` G=`", stringify!($g), "` B=`", stringify!($b), "` A=`", stringify!($a), "`.")]
60        #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61        pub struct $name;
62        impl Asserts<[f32; 4]> for $name {}
63        #[cfg(feature = "glam")]
64        impl Asserts<glam::Vec4> for $name {}
65        impl ChannelMap<4> for $name {
66            const INDICES: [usize; 4] = [$r, $g, $b, $a];
67            const ALPHA: Option<usize> = Some(3);
68        }
69    };
70    ($name:ident, [$r:expr, $g:expr, $b:expr]) => {
71        #[doc = concat!("Three-channel layout. R=`", stringify!($r), "` G=`", stringify!($g), "` B=`", stringify!($b), "`. No alpha.")]
72        #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73        pub struct $name;
74        impl Asserts<[f32; 3]> for $name {}
75        #[cfg(feature = "glam")]
76        impl Asserts<glam::Vec3> for $name {}
77        #[cfg(feature = "glam")]
78        impl Asserts<glam::Vec3A> for $name {}
79        impl ChannelMap<3> for $name {
80            const INDICES: [usize; 3] = [$r, $g, $b];
81            const ALPHA: Option<usize> = None;
82        }
83    };
84    ($name:ident, [$a:expr, $b:expr]) => {
85        /// Two-channel layout.
86        #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87        pub struct $name;
88        impl Asserts<[f32; 2]> for $name {}
89        #[cfg(feature = "glam")]
90        impl Asserts<glam::Vec2> for $name {}
91        impl ChannelMap<2> for $name {
92            const INDICES: [usize; 2] = [$a, $b];
93            const ALPHA: Option<usize> = None;
94        }
95    };
96    ($name:ident, [$a:expr], alpha) => {
97        /// Single alpha channel layout.
98        #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99        pub struct $name;
100        impl Asserts<[f32; 1]> for $name {}
101        impl Asserts<f32> for $name {}
102        impl ChannelMap<1> for $name {
103            const INDICES: [usize; 1] = [$a];
104            const ALPHA: Option<usize> = Some(0);
105        }
106    };
107    ($name:ident, [$a:expr]) => {
108        /// Single-channel layout.
109        #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110        pub struct $name;
111        impl Asserts<[f32; 1]> for $name {}
112        impl Asserts<f32> for $name {}
113        impl ChannelMap<1> for $name {
114            const INDICES: [usize; 1] = [$a];
115            const ALPHA: Option<usize> = None;
116        }
117    };
118}
119
120define_layout!(Rgba, [0, 1, 2, 3]);
121define_layout!(Bgra, [2, 1, 0, 3]);
122define_layout!(Argb, [1, 2, 3, 0]);
123define_layout!(Abgr, [3, 2, 1, 0]);
124define_layout!(Rgb, [0, 1, 2]);
125define_layout!(Bgr, [2, 1, 0]);
126define_layout!(Rg, [0, 1]);
127define_layout!(Ra, [0, 1]);
128define_layout!(R, [0]);
129define_layout!(G, [0]);
130define_layout!(B, [0]);
131define_layout!(A, [0], alpha);
132
133/// A color space formed by composing primaries P, transfer function TF,
134/// and channel layout L. Layout defaults to Rgba.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
136pub struct RgbSpace<P: Primaries, TF: TransferFunction, L = Rgba>(PhantomData<(P, TF, L)>);
137
138/// Marker for color spaces using the RGB channel model.
139pub trait RgbColorSpace: ColorSpace {}
140
141impl<P, TF, L> RgbColorSpace for RgbSpace<P, TF, L>
142where
143    P: Primaries,
144    TF: TransferFunction,
145    L: 'static,
146{
147}
148
149impl<P, TF, L> ColorSpace for RgbSpace<P, TF, L>
150where
151    P: Primaries,
152    TF: TransferFunction,
153    L: 'static,
154{
155    const CHANNELS: usize = 3;
156    const LUMINANCE_WEIGHTS: Option<[f32; 3]> = Some(P::TO_XYZ_NATIVE.luminance_weights());
157}
158
159impl<P, TF, L, S> Asserts<S> for RgbSpace<P, TF, L>
160where
161    P: Primaries,
162    TF: TransferFunction,
163    L: Asserts<S> + 'static,
164    S: 'static,
165{
166}
167
168impl<P, TF, L> LinearLight for RgbSpace<P, TF, L>
169where
170    P: Primaries,
171    TF: TransferFunction + IsLinearEncoding,
172    L: 'static,
173{
174}
175
176impl<P, TF, L> SceneReferred for RgbSpace<P, TF, L>
177where
178    P: Primaries,
179    TF: TransferFunction + IsSceneReferred,
180    L: 'static,
181{
182}
183
184impl<P, TF, L> DisplayReferred for RgbSpace<P, TF, L>
185where
186    P: Primaries,
187    TF: TransferFunction + IsDisplayReferred,
188    L: 'static,
189{
190}
191
192// Compile-time matrix for direct primaries-to-primaries transform.
193const fn direct_matrix<P1: Primaries, P2: Primaries>() -> Mat3 {
194    use crate::adaptation::adapt;
195    let a = adapt::<Bradford>(P1::Native::WHITE_POINT_XYZ, P2::Native::WHITE_POINT_XYZ);
196    let adapted = Mat3::mul(&a, &P1::TO_XYZ_NATIVE);
197    Mat3::mul(&P2::FROM_XYZ_NATIVE, &adapted)
198}
199
200// Layout reorder, four channel. Infallible.
201impl<P, TF, L1, L2> Transform<Color<[f32; 4], RgbSpace<P, TF, L1>>> for Color<[f32; 4], RgbSpace<P, TF, L2>>
202where
203    P: Primaries,
204    TF: TransferFunction,
205    L1: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
206    L2: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
207{
208    fn transform_from(src: Color<[f32; 4], RgbSpace<P, TF, L1>>, _: &()) -> Self {
209        let s = src.inner();
210        let [r1, g1, b1, a1] = L1::INDICES;
211        let [r2, g2, b2, a2] = L2::INDICES;
212        let mut out = [0.0f32; 4];
213        out[r2] = s[r1];
214        out[g2] = s[g1];
215        out[b2] = s[b1];
216        out[a2] = s[a1];
217        Color::new_unchecked(out)
218    }
219}
220
221// Layout reorder, three channel. Infallible.
222impl<P, TF, L1, L2> Transform<Color<[f32; 3], RgbSpace<P, TF, L1>>> for Color<[f32; 3], RgbSpace<P, TF, L2>>
223where
224    P: Primaries,
225    TF: TransferFunction,
226    L1: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
227    L2: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
228{
229    fn transform_from(src: Color<[f32; 3], RgbSpace<P, TF, L1>>, _: &()) -> Self {
230        let s = src.inner();
231        let [r1, g1, b1] = L1::INDICES;
232        let [r2, g2, b2] = L2::INDICES;
233        let mut out = [0.0f32; 3];
234        out[r2] = s[r1];
235        out[g2] = s[g1];
236        out[b2] = s[b1];
237        Color::new_unchecked(out)
238    }
239}
240
241// RGB to XYZ, infallible, [f32; 4].
242impl<P, TF, W, L> Transform<Color<[f32; 4], RgbSpace<P, TF, L>>> for Color<[f32; 4], Xyz<W>>
243where
244    P: Primaries + PrimariesToXyz<W, Bradford>,
245    TF: TransferFunction,
246    W: Illuminant,
247    L: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
248    Xyz<W>: Asserts<[f32; 4]>,
249{
250    fn transform_from(src: Color<[f32; 4], RgbSpace<P, TF, L>>, _: &()) -> Self {
251        let s = src.inner();
252        let [ri, gi, bi, ai] = L::INDICES;
253        let rgb = TF::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
254        let xyz = <P as PrimariesToXyz<W, Bradford>>::TO_XYZ.apply(rgb);
255        Color::new_unchecked([xyz[0], xyz[1], xyz[2], s[ai]])
256    }
257}
258
259// XYZ to RGB, fallible, [f32; 4].
260impl<P, TF, W, L> TryTransform<Color<[f32; 4], Xyz<W>>> for Color<[f32; 4], RgbSpace<P, TF, L>>
261where
262    P: Primaries + PrimariesToXyz<W, Bradford>,
263    TF: TransferFunction,
264    W: Illuminant,
265    L: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
266    Xyz<W>: Asserts<[f32; 4]>,
267{
268    fn try_transform_from(src: Color<[f32; 4], Xyz<W>>, _: &()) -> Result<Self, OutOfGamut<Self>> {
269        let x = src.inner();
270        let rgb = <P as PrimariesToXyz<W, Bradford>>::FROM_XYZ.apply([x[0], x[1], x[2]]);
271        let enc = TF::encode::<DefaultMath>(rgb);
272        let [ri, gi, bi, ai] = L::INDICES;
273        let mut s = [0.0f32; 4];
274        s[ri] = enc[0];
275        s[gi] = enc[1];
276        s[bi] = enc[2];
277        s[ai] = x[3];
278        let min = TF::ENCODED_MIN;
279        let max = TF::ENCODED_MAX;
280        let clipped = enc[0] < min[0]
281            || enc[0] > max[0]
282            || enc[1] < min[1]
283            || enc[1] > max[1]
284            || enc[2] < min[2]
285            || enc[2] > max[2]
286            || enc[0].is_nan()
287            || enc[1].is_nan()
288            || enc[2].is_nan();
289        if clipped {
290            let c = |v: f32, lo: f32| if v.is_nan() { lo } else { v };
291            s[ri] = c(enc[0], min[0]).clamp(min[0], max[0]);
292            s[gi] = c(enc[1], min[1]).clamp(min[1], max[1]);
293            s[bi] = c(enc[2], min[2]).clamp(min[2], max[2]);
294            Err(OutOfGamut {
295                clamped: Color::new_unchecked(s),
296            })
297        } else {
298            Ok(Color::new_unchecked(s))
299        }
300    }
301}
302
303// RGB to XYZ, infallible, [f32; 3].
304impl<P, TF, W, L> Transform<Color<[f32; 3], RgbSpace<P, TF, L>>> for Color<[f32; 3], Xyz<W>>
305where
306    P: Primaries + PrimariesToXyz<W, Bradford>,
307    TF: TransferFunction,
308    W: Illuminant,
309    L: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
310    Xyz<W>: Asserts<[f32; 3]>,
311{
312    fn transform_from(src: Color<[f32; 3], RgbSpace<P, TF, L>>, _: &()) -> Self {
313        let s = src.inner();
314        let [ri, gi, bi] = L::INDICES;
315        let rgb = TF::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
316        let xyz = <P as PrimariesToXyz<W, Bradford>>::TO_XYZ.apply(rgb);
317        Color::new_unchecked([xyz[0], xyz[1], xyz[2]])
318    }
319}
320
321// XYZ to RGB, fallible, [f32; 3].
322impl<P, TF, W, L> TryTransform<Color<[f32; 3], Xyz<W>>> for Color<[f32; 3], RgbSpace<P, TF, L>>
323where
324    P: Primaries + PrimariesToXyz<W, Bradford>,
325    TF: TransferFunction,
326    W: Illuminant,
327    L: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
328    Xyz<W>: Asserts<[f32; 3]>,
329{
330    fn try_transform_from(src: Color<[f32; 3], Xyz<W>>, _: &()) -> Result<Self, OutOfGamut<Self>> {
331        let x = src.inner();
332        let rgb = <P as PrimariesToXyz<W, Bradford>>::FROM_XYZ.apply([x[0], x[1], x[2]]);
333        let enc = TF::encode::<DefaultMath>(rgb);
334        let [ri, gi, bi] = L::INDICES;
335        let mut s = [0.0f32; 3];
336        s[ri] = enc[0];
337        s[gi] = enc[1];
338        s[bi] = enc[2];
339        let min = TF::ENCODED_MIN;
340        let max = TF::ENCODED_MAX;
341        let clipped = enc[0] < min[0]
342            || enc[0] > max[0]
343            || enc[1] < min[1]
344            || enc[1] > max[1]
345            || enc[2] < min[2]
346            || enc[2] > max[2]
347            || enc[0].is_nan()
348            || enc[1].is_nan()
349            || enc[2].is_nan();
350        if clipped {
351            let c = |v: f32, lo: f32| if v.is_nan() { lo } else { v };
352            s[ri] = c(enc[0], min[0]).clamp(min[0], max[0]);
353            s[gi] = c(enc[1], min[1]).clamp(min[1], max[1]);
354            s[bi] = c(enc[2], min[2]).clamp(min[2], max[2]);
355            Err(OutOfGamut {
356                clamped: Color::new_unchecked(s),
357            })
358        } else {
359            Ok(Color::new_unchecked(s))
360        }
361    }
362}
363
364#[cfg(feature = "glam")]
365impl<P, TF, W, L> Transform<Color<glam::Vec4, RgbSpace<P, TF, L>>> for Color<glam::Vec4, Xyz<W>>
366where
367    P: Primaries + PrimariesToXyz<W, Bradford>,
368    TF: TransferFunction,
369    W: Illuminant,
370    L: Asserts<glam::Vec4> + ChannelMap<4> + 'static,
371    Xyz<W>: Asserts<glam::Vec4>,
372{
373    fn transform_from(src: Color<glam::Vec4, RgbSpace<P, TF, L>>, _: &()) -> Self {
374        let s = src.inner().to_array();
375        let [ri, gi, bi, ai] = L::INDICES;
376        let rgb = TF::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
377        let xyz = <P as PrimariesToXyz<W, Bradford>>::TO_XYZ.apply(rgb);
378        Color::new_unchecked(glam::Vec4::new(xyz[0], xyz[1], xyz[2], s[ai]))
379    }
380}
381
382#[cfg(feature = "glam")]
383impl<P, TF, W, L> TryTransform<Color<glam::Vec4, Xyz<W>>> for Color<glam::Vec4, RgbSpace<P, TF, L>>
384where
385    P: Primaries + PrimariesToXyz<W, Bradford>,
386    TF: TransferFunction,
387    W: Illuminant,
388    L: Asserts<glam::Vec4> + ChannelMap<4> + 'static,
389    Xyz<W>: Asserts<glam::Vec4>,
390{
391    fn try_transform_from(src: Color<glam::Vec4, Xyz<W>>, _: &()) -> Result<Self, OutOfGamut<Self>> {
392        let x = src.inner().to_array();
393        let rgb = <P as PrimariesToXyz<W, Bradford>>::FROM_XYZ.apply([x[0], x[1], x[2]]);
394        let enc = TF::encode::<DefaultMath>(rgb);
395        let [ri, gi, bi, ai] = L::INDICES;
396        let mut s = [0.0f32; 4];
397        s[ri] = enc[0];
398        s[gi] = enc[1];
399        s[bi] = enc[2];
400        s[ai] = x[3];
401        let min = TF::ENCODED_MIN;
402        let max = TF::ENCODED_MAX;
403        let clipped = enc[0] < min[0]
404            || enc[0] > max[0]
405            || enc[1] < min[1]
406            || enc[1] > max[1]
407            || enc[2] < min[2]
408            || enc[2] > max[2]
409            || enc[0].is_nan()
410            || enc[1].is_nan()
411            || enc[2].is_nan();
412        if clipped {
413            let c = |v: f32, lo: f32| if v.is_nan() { lo } else { v };
414            s[ri] = c(enc[0], min[0]).clamp(min[0], max[0]);
415            s[gi] = c(enc[1], min[1]).clamp(min[1], max[1]);
416            s[bi] = c(enc[2], min[2]).clamp(min[2], max[2]);
417            Err(OutOfGamut {
418                clamped: Color::new_unchecked(glam::Vec4::from_array(s)),
419            })
420        } else {
421            Ok(Color::new_unchecked(glam::Vec4::from_array(s)))
422        }
423    }
424}
425
426#[cfg(feature = "glam")]
427impl<P, TF, W, L> Transform<Color<glam::Vec3A, RgbSpace<P, TF, L>>> for Color<glam::Vec3A, Xyz<W>>
428where
429    P: Primaries + PrimariesToXyz<W, Bradford>,
430    TF: TransferFunction,
431    W: Illuminant,
432    L: Asserts<glam::Vec3A> + ChannelMap<3> + 'static,
433    Xyz<W>: Asserts<glam::Vec3A>,
434{
435    fn transform_from(src: Color<glam::Vec3A, RgbSpace<P, TF, L>>, _: &()) -> Self {
436        let s = src.inner().to_array();
437        let [ri, gi, bi] = L::INDICES;
438        let rgb = TF::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
439        let xyz = <P as PrimariesToXyz<W, Bradford>>::TO_XYZ.apply(rgb);
440        Color::new_unchecked(glam::Vec3A::new(xyz[0], xyz[1], xyz[2]))
441    }
442}
443
444#[cfg(feature = "glam")]
445impl<P, TF, W, L> TryTransform<Color<glam::Vec3A, Xyz<W>>> for Color<glam::Vec3A, RgbSpace<P, TF, L>>
446where
447    P: Primaries + PrimariesToXyz<W, Bradford>,
448    TF: TransferFunction,
449    W: Illuminant,
450    L: Asserts<glam::Vec3A> + ChannelMap<3> + 'static,
451    Xyz<W>: Asserts<glam::Vec3A>,
452{
453    fn try_transform_from(src: Color<glam::Vec3A, Xyz<W>>, _: &()) -> Result<Self, OutOfGamut<Self>> {
454        let x = src.inner().to_array();
455        let rgb = <P as PrimariesToXyz<W, Bradford>>::FROM_XYZ.apply([x[0], x[1], x[2]]);
456        let enc = TF::encode::<DefaultMath>(rgb);
457        let [ri, gi, bi] = L::INDICES;
458        let mut s = [0.0f32; 3];
459        s[ri] = enc[0];
460        s[gi] = enc[1];
461        s[bi] = enc[2];
462        let min = TF::ENCODED_MIN;
463        let max = TF::ENCODED_MAX;
464        let clipped = enc[0] < min[0]
465            || enc[0] > max[0]
466            || enc[1] < min[1]
467            || enc[1] > max[1]
468            || enc[2] < min[2]
469            || enc[2] > max[2]
470            || enc[0].is_nan()
471            || enc[1].is_nan()
472            || enc[2].is_nan();
473        if clipped {
474            let c = |v: f32, lo: f32| if v.is_nan() { lo } else { v };
475            s[ri] = c(enc[0], min[0]).clamp(min[0], max[0]);
476            s[gi] = c(enc[1], min[1]).clamp(min[1], max[1]);
477            s[bi] = c(enc[2], min[2]).clamp(min[2], max[2]);
478            Err(OutOfGamut {
479                clamped: Color::new_unchecked(glam::Vec3A::from_array(s)),
480            })
481        } else {
482            Ok(Color::new_unchecked(glam::Vec3A::from_array(s)))
483        }
484    }
485}
486
487// sRGB <-> Display P3 (both use sRGB transfer function, different primaries).
488impl<L> Transform<Color<[f32; 4], Srgb<L>>> for Color<[f32; 4], DisplayP3<L>>
489where
490    L: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
491{
492    fn transform_from(src: Color<[f32; 4], Srgb<L>>, _: &()) -> Self {
493        let s = src.inner();
494        let [ri, gi, bi, _ai] = L::INDICES;
495        let lin = SrgbTf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
496        let out = const { direct_matrix::<SrgbPrimaries, P3Primaries>() }.apply(lin);
497        let enc = SrgbTf::encode::<DefaultMath>(out);
498        let mut r = s;
499        r[ri] = enc[0];
500        r[gi] = enc[1];
501        r[bi] = enc[2];
502        Color::new_unchecked(r)
503    }
504}
505
506impl<L> Transform<Color<[f32; 3], Srgb<L>>> for Color<[f32; 3], DisplayP3<L>>
507where
508    L: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
509{
510    fn transform_from(src: Color<[f32; 3], Srgb<L>>, _: &()) -> Self {
511        let s = src.inner();
512        let [ri, gi, bi] = L::INDICES;
513        let lin = SrgbTf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
514        let out = const { direct_matrix::<SrgbPrimaries, P3Primaries>() }.apply(lin);
515        let enc = SrgbTf::encode::<DefaultMath>(out);
516        let mut r = s;
517        r[ri] = enc[0];
518        r[gi] = enc[1];
519        r[bi] = enc[2];
520        Color::new_unchecked(r)
521    }
522}
523
524impl<L> Transform<Color<[f32; 4], DisplayP3<L>>> for Color<[f32; 4], Srgb<L>>
525where
526    L: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
527{
528    fn transform_from(src: Color<[f32; 4], DisplayP3<L>>, _: &()) -> Self {
529        let s = src.inner();
530        let [ri, gi, bi, _ai] = L::INDICES;
531        let lin = SrgbTf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
532        let out = const { direct_matrix::<P3Primaries, SrgbPrimaries>() }.apply(lin);
533        let enc = SrgbTf::encode::<DefaultMath>(out);
534        let mut r = s;
535        r[ri] = enc[0];
536        r[gi] = enc[1];
537        r[bi] = enc[2];
538        Color::new_unchecked(r)
539    }
540}
541
542impl<L> Transform<Color<[f32; 3], DisplayP3<L>>> for Color<[f32; 3], Srgb<L>>
543where
544    L: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
545{
546    fn transform_from(src: Color<[f32; 3], DisplayP3<L>>, _: &()) -> Self {
547        let s = src.inner();
548        let [ri, gi, bi] = L::INDICES;
549        let lin = SrgbTf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
550        let out = const { direct_matrix::<P3Primaries, SrgbPrimaries>() }.apply(lin);
551        let enc = SrgbTf::encode::<DefaultMath>(out);
552        let mut r = s;
553        r[ri] = enc[0];
554        r[gi] = enc[1];
555        r[bi] = enc[2];
556        Color::new_unchecked(r)
557    }
558}
559
560// sRGB <-> Rec709 (same primaries, transfer function only).
561impl<L> Transform<Color<[f32; 4], Srgb<L>>> for Color<[f32; 4], Rec709<L>>
562where
563    L: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
564{
565    fn transform_from(src: Color<[f32; 4], Srgb<L>>, _: &()) -> Self {
566        let s = src.inner();
567        let [ri, gi, bi, _ai] = L::INDICES;
568        let lin = SrgbTf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
569        let enc = Rec709Tf::encode::<DefaultMath>(lin);
570        let mut r = s;
571        r[ri] = enc[0];
572        r[gi] = enc[1];
573        r[bi] = enc[2];
574        Color::new_unchecked(r)
575    }
576}
577
578impl<L> Transform<Color<[f32; 3], Srgb<L>>> for Color<[f32; 3], Rec709<L>>
579where
580    L: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
581{
582    fn transform_from(src: Color<[f32; 3], Srgb<L>>, _: &()) -> Self {
583        let s = src.inner();
584        let [ri, gi, bi] = L::INDICES;
585        let lin = SrgbTf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
586        let enc = Rec709Tf::encode::<DefaultMath>(lin);
587        let mut r = s;
588        r[ri] = enc[0];
589        r[gi] = enc[1];
590        r[bi] = enc[2];
591        Color::new_unchecked(r)
592    }
593}
594
595impl<L> Transform<Color<[f32; 4], Rec709<L>>> for Color<[f32; 4], Srgb<L>>
596where
597    L: Asserts<[f32; 4]> + ChannelMap<4> + 'static,
598{
599    fn transform_from(src: Color<[f32; 4], Rec709<L>>, _: &()) -> Self {
600        let s = src.inner();
601        let [ri, gi, bi, _ai] = L::INDICES;
602        let lin = Rec709Tf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
603        let enc = SrgbTf::encode::<DefaultMath>(lin);
604        let mut r = s;
605        r[ri] = enc[0];
606        r[gi] = enc[1];
607        r[bi] = enc[2];
608        Color::new_unchecked(r)
609    }
610}
611
612impl<L> Transform<Color<[f32; 3], Rec709<L>>> for Color<[f32; 3], Srgb<L>>
613where
614    L: Asserts<[f32; 3]> + ChannelMap<3> + 'static,
615{
616    fn transform_from(src: Color<[f32; 3], Rec709<L>>, _: &()) -> Self {
617        let s = src.inner();
618        let [ri, gi, bi] = L::INDICES;
619        let lin = Rec709Tf::decode::<DefaultMath>([s[ri], s[gi], s[bi]]);
620        let enc = SrgbTf::encode::<DefaultMath>(lin);
621        let mut r = s;
622        r[ri] = enc[0];
623        r[gi] = enc[1];
624        r[bi] = enc[2];
625        Color::new_unchecked(r)
626    }
627}
628
629/// Standard sRGB (IEC 61966-2-1). Web, consumer images, and the default
630/// assumption when no color space is specified.
631pub type Srgb<L = Rgba> = RgbSpace<SrgbPrimaries, SrgbTf, L>;
632/// Linear sRGB. The correct space for premultiplied alpha compositing.
633pub type LinearSrgb<L = Rgba> = RgbSpace<SrgbPrimaries, LinearTf, L>;
634/// Rec. ITU-R BT.709. HD broadcast. Same primaries as sRGB, different curve.
635pub type Rec709<L = Rgba> = RgbSpace<SrgbPrimaries, Rec709Tf, L>;
636/// Display P3 (D65). Apple and modern Android wide-gamut displays.
637/// Roughly 25% larger gamut than sRGB with the sRGB transfer function.
638pub type DisplayP3<L = Rgba> = RgbSpace<P3Primaries, SrgbTf, L>;
639/// Linear Display P3. Wide-gamut SDR compositing on P3-capable displays.
640pub type LinearP3<L = Rgba> = RgbSpace<P3Primaries, LinearTf, L>;
641/// HDR10. Rec. 2020 primaries with PQ (SMPTE ST 2084).
642/// Encoded 1.0 represents 10,000 cd/m2.
643pub type Hdr10<L = Rgba> = RgbSpace<Rec2020Primaries, PqTf, L>;
644/// HLG (ITU-R BT.2100). Rec. 2020 with Hybrid Log-Gamma.
645/// SDR-backward-compatible HDR for broadcast.
646pub type Hlg<L = Rgba> = RgbSpace<Rec2020Primaries, HlgTf, L>;
647/// Linear Rec. 2020. Ultra-wide gamut scene-linear compositing space.
648pub type LinearRec2020<L = Rgba> = RgbSpace<Rec2020Primaries, LinearTf, L>;
649/// ACEScg (AP1 linear, Academy S-2014-004). VFX and animation rendering
650/// working space. All real-world colors have positive values.
651pub type AcesCg<L = Rgba> = RgbSpace<AcesAp1Primaries, LinearTf, L>;
652/// ACES 2065-1 (AP0 linear, SMPTE ST 2065-1). Full-gamut scene-referred
653/// archival and interchange space.
654pub type Aces2065<L = Rgba> = RgbSpace<AcesAp0Primaries, LinearTf, L>;
655/// ACEScc (AP1 logarithmic, Academy S-2014-003). Color grading working space.
656pub type AcesCc<L = Rgba> = RgbSpace<AcesAp1Primaries, AcesCcTf, L>;
657/// ACEScct (AP1 quasi-logarithmic, Academy S-2016-001). Grading with toe.
658pub type AcesCct<L = Rgba> = RgbSpace<AcesAp1Primaries, AcesCctTf, L>;
659/// ProPhoto (ROMM RGB, ISO 22028-2). Extremely wide gamut with gamma 1.8.
660/// D50 native illuminant. Used in camera raw and professional photo workflows.
661pub type ProPhoto<L = Rgba> = RgbSpace<ProPhotoPrimaries, ProPhotoTf, L>;
662/// Linear ProPhoto. D50 native illuminant. Lightroom internal working space.
663pub type LinearProPhoto<L = Rgba> = RgbSpace<ProPhotoPrimaries, LinearTf, L>;
664/// Theatrical DCI-P3 (SMPTE EG 432-1). Gamma 2.6, DCI white point.
665/// Distinct from Display P3 in both white point and transfer function.
666pub type DciP3<L = Rgba> = RgbSpace<DciP3Primaries, DciP3Tf, L>;
667/// P3 primaries with gamma 2.6 and D65 white point. Non-standard combination.
668/// Most users want DisplayP3 (sRGB curve) or DciP3 (DCI white) instead.
669pub type P3D65Gamma26<L = Rgba> = RgbSpace<P3Primaries, DciP3Tf, L>;