palette/
xyz.rs

1//! Types for the CIE 1931 XYZ color space.
2
3use core::{marker::PhantomData, ops::Mul};
4
5use crate::{
6    angle::{RealAngle, SignedAngle},
7    bool_mask::{HasBoolMask, LazySelect},
8    cam16::{
9        Cam16, Cam16IntoUnclamped, Cam16Jch, Cam16Jmh, Cam16Jsh, Cam16Qch, Cam16Qmh, Cam16Qsh,
10        FromCam16Unclamped, WhitePointParameter,
11    },
12    convert::{FromColorUnclamped, IntoColorUnclamped},
13    encoding::IntoLinear,
14    luma::LumaStandard,
15    matrix::{matrix_map, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix},
16    num::{
17        Abs, Arithmetics, FromScalar, IsValidDivisor, One, PartialCmp, Powf, Powi, Real, Recip,
18        Signum, Sqrt, Trigonometry, Zero,
19    },
20    oklab,
21    rgb::{Primaries, Rgb, RgbSpace, RgbStandard},
22    stimulus::{Stimulus, StimulusColor},
23    white_point::{Any, WhitePoint, D65},
24    Alpha, Lab, Luma, Luv, Oklab, Yxy,
25};
26
27/// CIE 1931 XYZ with an alpha component. See the [`Xyza` implementation in
28/// `Alpha`](crate::Alpha#Xyza).
29pub type Xyza<Wp = D65, T = f32> = Alpha<Xyz<Wp, T>, T>;
30
31/// The CIE 1931 XYZ color space.
32///
33/// XYZ links the perceived colors to their wavelengths and simply makes it
34/// possible to describe the way we see colors as numbers. It's often used when
35/// converting from one color space to an other, and requires a standard
36/// illuminant and a standard observer to be defined.
37///
38/// Conversions and operations on this color space depend on the defined white
39/// point
40#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
41#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
42#[palette(
43    palette_internal,
44    white_point = "Wp",
45    component = "T",
46    skip_derives(Xyz, Yxy, Luv, Rgb, Lab, Oklab, Luma)
47)]
48#[repr(C)]
49pub struct Xyz<Wp = D65, T = f32> {
50    /// X is the scale of what can be seen as a response curve for the cone
51    /// cells in the human eye. Its range depends
52    /// on the white point and goes from 0.0 to 0.95047 for the default D65.
53    pub x: T,
54
55    /// Y is the luminance of the color, where 0.0 is black and 1.0 is white.
56    pub y: T,
57
58    /// Z is the scale of what can be seen as the blue stimulation. Its range
59    /// depends on the white point and goes from 0.0 to 1.08883 for the
60    /// default D65.
61    pub z: T,
62
63    /// The white point associated with the color's illuminant and observer.
64    /// D65 for 2 degree observer is used by default.
65    #[cfg_attr(feature = "serializing", serde(skip))]
66    #[palette(unsafe_zero_sized)]
67    pub white_point: PhantomData<Wp>,
68}
69
70impl<Wp, T> Xyz<Wp, T> {
71    /// Create a CIE XYZ color.
72    pub const fn new(x: T, y: T, z: T) -> Xyz<Wp, T> {
73        Xyz {
74            x,
75            y,
76            z,
77            white_point: PhantomData,
78        }
79    }
80
81    /// Convert to a `(X, Y, Z)` tuple.
82    pub fn into_components(self) -> (T, T, T) {
83        (self.x, self.y, self.z)
84    }
85
86    /// Convert from a `(X, Y, Z)` tuple.
87    pub fn from_components((x, y, z): (T, T, T)) -> Self {
88        Self::new(x, y, z)
89    }
90
91    /// Changes the reference white point without changing the color value.
92    ///
93    /// This function doesn't change the numerical values, and thus the color it
94    /// represents in an absolute sense. However, the appearance of the color
95    /// may not be the same when observed with the new white point. The effect
96    /// would be similar to taking a photo with an incorrect white balance.
97    ///
98    /// See [chromatic_adaptation](crate::chromatic_adaptation) for operations
99    /// that can change the white point while preserving the color's appearance.
100    #[inline]
101    pub fn with_white_point<NewWp>(self) -> Xyz<NewWp, T> {
102        Xyz::new(self.x, self.y, self.z)
103    }
104}
105
106impl<Wp, T> Xyz<Wp, T>
107where
108    T: Zero,
109    Wp: WhitePoint<T>,
110{
111    /// Return the `x` value minimum.
112    pub fn min_x() -> T {
113        T::zero()
114    }
115
116    /// Return the `x` value maximum.
117    pub fn max_x() -> T {
118        Wp::get_xyz().x
119    }
120
121    /// Return the `y` value minimum.
122    pub fn min_y() -> T {
123        T::zero()
124    }
125
126    /// Return the `y` value maximum.
127    pub fn max_y() -> T {
128        Wp::get_xyz().y
129    }
130
131    /// Return the `z` value minimum.
132    pub fn min_z() -> T {
133        T::zero()
134    }
135
136    /// Return the `z` value maximum.
137    pub fn max_z() -> T {
138        Wp::get_xyz().z
139    }
140}
141
142///<span id="Xyza"></span>[`Xyza`](crate::Xyza) implementations.
143impl<Wp, T, A> Alpha<Xyz<Wp, T>, A> {
144    /// Create a CIE XYZ color with transparency.
145    pub const fn new(x: T, y: T, z: T, alpha: A) -> Self {
146        Alpha {
147            color: Xyz::new(x, y, z),
148            alpha,
149        }
150    }
151
152    /// Convert to a `(X, Y, Z, alpha)` tuple.
153    pub fn into_components(self) -> (T, T, T, A) {
154        (self.color.x, self.color.y, self.color.z, self.alpha)
155    }
156
157    /// Convert from a `(X, Y, Z, alpha)` tuple.
158    pub fn from_components((x, y, z, alpha): (T, T, T, A)) -> Self {
159        Self::new(x, y, z, alpha)
160    }
161
162    /// Changes the reference white point without changing the color value.
163    ///
164    /// This function doesn't change the numerical values, and thus the color it
165    /// represents in an absolute sense. However, the appearance of the color
166    /// may not be the same when observed with the new white point. The effect
167    /// would be similar to taking a photo with an incorrect white balance.
168    ///
169    /// See [chromatic_adaptation](crate::chromatic_adaptation) for operations
170    /// that can change the white point while preserving the color's appearance.
171    #[inline]
172    pub fn with_white_point<NewWp>(self) -> Alpha<Xyz<NewWp, T>, A> {
173        Alpha::<Xyz<NewWp, T>, A>::new(self.color.x, self.color.y, self.color.z, self.alpha)
174    }
175}
176
177impl_reference_component_methods!(Xyz<Wp>, [x, y, z], white_point);
178impl_struct_of_arrays_methods!(Xyz<Wp>, [x, y, z], white_point);
179
180impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Xyz<Wp, T> {
181    fn from_color_unclamped(color: Xyz<Wp, T>) -> Self {
182        color
183    }
184}
185
186impl<Wp, T, S> FromColorUnclamped<Rgb<S, T>> for Xyz<Wp, T>
187where
188    T: Arithmetics + FromScalar,
189    T::Scalar: Real
190        + Recip
191        + IsValidDivisor<Mask = bool>
192        + Arithmetics
193        + FromScalar<Scalar = T::Scalar>
194        + Clone,
195    Wp: WhitePoint<T::Scalar>,
196    S: RgbStandard,
197    S::TransferFn: IntoLinear<T, T>,
198    S::Space: RgbSpace<WhitePoint = Wp>,
199    <S::Space as RgbSpace>::Primaries: Primaries<T::Scalar>,
200    Yxy<Any, T::Scalar>: IntoColorUnclamped<Xyz<Any, T::Scalar>>,
201{
202    fn from_color_unclamped(color: Rgb<S, T>) -> Self {
203        let transform_matrix = S::Space::rgb_to_xyz_matrix()
204            .map_or_else(rgb_to_xyz_matrix::<S::Space, T::Scalar>, |matrix| {
205                matrix_map(matrix, T::Scalar::from_f64)
206            });
207        multiply_rgb_to_xyz(transform_matrix, color.into_linear())
208    }
209}
210
211impl<Wp, T> FromColorUnclamped<Yxy<Wp, T>> for Xyz<Wp, T>
212where
213    T: Zero + One + IsValidDivisor + Arithmetics + Clone,
214    T::Mask: LazySelect<T> + Clone,
215{
216    fn from_color_unclamped(color: Yxy<Wp, T>) -> Self {
217        let Yxy { x, y, luma, .. } = color;
218
219        // If denominator is zero, NAN or INFINITE leave x and z at the default 0
220        let mask = y.is_valid_divisor();
221        let xyz = Xyz {
222            z: lazy_select! {
223                if mask.clone() => (T::one() - &x - &y) / &y,
224                else => T::zero(),
225            },
226            x: lazy_select! {
227                if mask => x / y,
228                else => T::zero(),
229            },
230            y: T::one(),
231            white_point: PhantomData,
232        };
233
234        xyz * luma
235    }
236}
237
238impl<Wp, T> FromColorUnclamped<Lab<Wp, T>> for Xyz<Wp, T>
239where
240    T: Real + Recip + Powi + Arithmetics + PartialCmp + Clone,
241    T::Mask: LazySelect<T>,
242    Wp: WhitePoint<T>,
243{
244    fn from_color_unclamped(color: Lab<Wp, T>) -> Self {
245        // Recip call shows performance benefits in benchmarks for this function
246        let y = (color.l + T::from_f64(16.0)) * T::from_f64(116.0).recip();
247        let x = y.clone() + (color.a * T::from_f64(500.0).recip());
248        let z = y.clone() - (color.b * T::from_f64(200.0).recip());
249
250        let epsilon: T = T::from_f64(6.0 / 29.0);
251        let kappa: T = T::from_f64(108.0 / 841.0);
252        let delta: T = T::from_f64(4.0 / 29.0);
253
254        let convert = |c: T| {
255            lazy_select! {
256                if c.gt(&epsilon) => c.clone().powi(3),
257                else => (c.clone() - &delta) * &kappa
258            }
259        };
260
261        Xyz::new(convert(x), convert(y), convert(z)) * Wp::get_xyz().with_white_point()
262    }
263}
264
265impl<Wp, T> FromColorUnclamped<Luv<Wp, T>> for Xyz<Wp, T>
266where
267    T: Real + Zero + Recip + Powi + Arithmetics + PartialOrd + Clone + HasBoolMask<Mask = bool>,
268    Wp: WhitePoint<T>,
269{
270    fn from_color_unclamped(color: Luv<Wp, T>) -> Self {
271        let kappa = T::from_f64(29.0 / 3.0).powi(3);
272
273        let w = Wp::get_xyz();
274        let ref_denom_recip =
275            (w.x.clone() + T::from_f64(15.0) * &w.y + T::from_f64(3.0) * w.z).recip();
276        let u_ref = T::from_f64(4.0) * w.x * &ref_denom_recip;
277        let v_ref = T::from_f64(9.0) * &w.y * ref_denom_recip;
278
279        if color.l < T::from_f64(1e-5) {
280            return Xyz::new(T::zero(), T::zero(), T::zero());
281        }
282
283        let y = if color.l > T::from_f64(8.0) {
284            ((color.l.clone() + T::from_f64(16.0)) * T::from_f64(116.0).recip()).powi(3)
285        } else {
286            color.l.clone() * kappa.recip()
287        } * w.y;
288
289        let u_prime = color.u / (T::from_f64(13.0) * &color.l) + u_ref;
290        let v_prime = color.v / (T::from_f64(13.0) * color.l) + v_ref;
291
292        let x = y.clone() * T::from_f64(2.25) * &u_prime / &v_prime;
293        let z = y.clone()
294            * (T::from_f64(3.0) - T::from_f64(0.75) * u_prime - T::from_f64(5.0) * &v_prime)
295            / v_prime;
296        Xyz::new(x, y, z)
297    }
298}
299
300impl<T> FromColorUnclamped<Oklab<T>> for Xyz<D65, T>
301where
302    T: Real + Powi + Arithmetics,
303{
304    fn from_color_unclamped(color: Oklab<T>) -> Self {
305        let m1_inv = oklab::m1_inv();
306        let m2_inv = oklab::m2_inv();
307
308        let Xyz {
309            x: l, y: m, z: s, ..
310        } = multiply_xyz(m2_inv, Xyz::new(color.l, color.a, color.b));
311
312        let lms = Xyz::new(l.powi(3), m.powi(3), s.powi(3));
313        multiply_xyz(m1_inv, lms).with_white_point()
314    }
315}
316
317impl<Wp, T, S> FromColorUnclamped<Luma<S, T>> for Xyz<Wp, T>
318where
319    Self: Mul<T, Output = Self>,
320    Wp: WhitePoint<T>,
321    S: LumaStandard<WhitePoint = Wp>,
322    S::TransferFn: IntoLinear<T, T>,
323{
324    fn from_color_unclamped(color: Luma<S, T>) -> Self {
325        Wp::get_xyz().with_white_point::<Wp>() * color.into_linear().luma
326    }
327}
328
329impl<WpParam, T> FromCam16Unclamped<WpParam, Cam16<T>> for Xyz<WpParam::StaticWp, T>
330where
331    WpParam: WhitePointParameter<T>,
332    T: FromScalar,
333    Cam16Jch<T>: Cam16IntoUnclamped<WpParam, Self, Scalar = T::Scalar>,
334{
335    type Scalar = T::Scalar;
336
337    fn from_cam16_unclamped(
338        cam16: Cam16<T>,
339        parameters: crate::cam16::BakedParameters<WpParam, Self::Scalar>,
340    ) -> Self {
341        Cam16Jch::from(cam16).cam16_into_unclamped(parameters)
342    }
343}
344
345macro_rules! impl_from_cam16_partial {
346    ($($name: ident),+) => {
347        $(
348            impl<WpParam, T> FromCam16Unclamped<WpParam, $name<T>> for Xyz<WpParam::StaticWp, T>
349            where
350                WpParam: WhitePointParameter<T>,
351                T: Real
352                    + FromScalar
353                    + One
354                    + Zero
355                    + Sqrt
356                    + Powf
357                    + Abs
358                    + Signum
359                    + Arithmetics
360                    + Trigonometry
361                    + RealAngle
362                    + SignedAngle
363                    + PartialCmp
364                    + Clone,
365                T::Mask: LazySelect<T> + Clone,
366                T::Scalar: Clone,
367            {
368                type Scalar = T::Scalar;
369
370                fn from_cam16_unclamped(
371                    cam16: $name<T>,
372                    parameters: crate::cam16::BakedParameters<WpParam, Self::Scalar>,
373                ) -> Self {
374                    crate::cam16::math::cam16_to_xyz(cam16.into_dynamic(), parameters.inner)
375                        .with_white_point()
376                }
377            }
378        )+
379    };
380}
381
382impl_from_cam16_partial!(Cam16Jmh, Cam16Jch, Cam16Jsh, Cam16Qmh, Cam16Qch, Cam16Qsh);
383
384impl_tuple_conversion!(Xyz<Wp> as (T, T, T));
385
386impl_is_within_bounds! {
387    Xyz<Wp> {
388        x => [Self::min_x(), Self::max_x()],
389        y => [Self::min_y(), Self::max_y()],
390        z => [Self::min_z(), Self::max_z()]
391    }
392    where
393        T: Zero,
394        Wp: WhitePoint<T>
395}
396impl_clamp! {
397    Xyz<Wp> {
398        x => [Self::min_x(), Self::max_x()],
399        y => [Self::min_y(), Self::max_y()],
400        z => [Self::min_z(), Self::max_z()]
401    }
402    other {white_point}
403    where
404        T: Zero,
405        Wp: WhitePoint<T>
406}
407
408impl_mix!(Xyz<Wp>);
409impl_lighten! {
410    Xyz<Wp>
411    increase {
412        x => [Self::min_x(), Self::max_x()],
413        y => [Self::min_y(), Self::max_y()],
414        z => [Self::min_z(), Self::max_z()]
415    }
416    other {}
417    phantom: white_point
418    where Wp: WhitePoint<T>
419}
420impl_premultiply!(Xyz<Wp> {x, y, z} phantom: white_point);
421impl_euclidean_distance!(Xyz<Wp> {x, y, z});
422
423impl<Wp, T> StimulusColor for Xyz<Wp, T> where T: Stimulus {}
424
425impl<Wp, T> HasBoolMask for Xyz<Wp, T>
426where
427    T: HasBoolMask,
428{
429    type Mask = T::Mask;
430}
431
432impl<Wp, T> Default for Xyz<Wp, T>
433where
434    T: Zero,
435{
436    fn default() -> Xyz<Wp, T> {
437        Xyz::new(T::zero(), T::zero(), T::zero())
438    }
439}
440
441impl_color_add!(Xyz<Wp>, [x, y, z], white_point);
442impl_color_sub!(Xyz<Wp>, [x, y, z], white_point);
443impl_color_mul!(Xyz<Wp>, [x, y, z], white_point);
444impl_color_div!(Xyz<Wp>, [x, y, z], white_point);
445
446impl_array_casts!(Xyz<Wp, T>, [T; 3]);
447impl_simd_array_conversion!(Xyz<Wp>, [x, y, z], white_point);
448impl_struct_of_array_traits!(Xyz<Wp>, [x, y, z], white_point);
449
450impl_copy_clone!(Xyz<Wp>, [x, y, z], white_point);
451impl_eq!(Xyz<Wp>, [x, y, z]);
452
453#[allow(deprecated)]
454impl<Wp, T> crate::RelativeContrast for Xyz<Wp, T>
455where
456    T: Real + Arithmetics + PartialCmp,
457    T::Mask: LazySelect<T>,
458{
459    type Scalar = T;
460
461    #[inline]
462    fn get_contrast_ratio(self, other: Self) -> T {
463        crate::contrast_ratio(self.y, other.y)
464    }
465}
466
467impl_rand_traits_cartesian!(
468    UniformXyz,
469    Xyz<Wp> {
470        x => [|x| x * Wp::get_xyz().x],
471        y => [|y| y * Wp::get_xyz().y],
472        z => [|z| z * Wp::get_xyz().z]
473    }
474    phantom: white_point: PhantomData<Wp>
475    where T: core::ops::Mul<Output = T>, Wp: WhitePoint<T>
476);
477
478#[cfg(feature = "bytemuck")]
479unsafe impl<Wp, T> bytemuck::Zeroable for Xyz<Wp, T> where T: bytemuck::Zeroable {}
480
481#[cfg(feature = "bytemuck")]
482unsafe impl<Wp: 'static, T> bytemuck::Pod for Xyz<Wp, T> where T: bytemuck::Pod {}
483
484#[cfg(test)]
485mod test {
486    use super::Xyz;
487    use crate::white_point::D65;
488
489    #[cfg(feature = "random")]
490    use crate::white_point::WhitePoint;
491
492    const X_N: f64 = 0.95047;
493    const Y_N: f64 = 1.0;
494    const Z_N: f64 = 1.08883;
495
496    test_convert_into_from_xyz!(Xyz);
497
498    #[cfg(feature = "approx")]
499    mod conversion {
500        use crate::{white_point::D65, FromColor, LinLuma, LinSrgb, Xyz};
501
502        #[test]
503        fn luma() {
504            let a = Xyz::<D65>::from_color(LinLuma::new(0.5));
505            let b = Xyz::new(0.475235, 0.5, 0.544415);
506            assert_relative_eq!(a, b, epsilon = 0.0001);
507        }
508
509        #[test]
510        fn red() {
511            let a = Xyz::from_color(LinSrgb::new(1.0, 0.0, 0.0));
512            let b = Xyz::new(0.41240, 0.21260, 0.01930);
513            assert_relative_eq!(a, b, epsilon = 0.0001);
514        }
515
516        #[test]
517        fn green() {
518            let a = Xyz::from_color(LinSrgb::new(0.0, 1.0, 0.0));
519            let b = Xyz::new(0.35760, 0.71520, 0.11920);
520            assert_relative_eq!(a, b, epsilon = 0.0001);
521        }
522
523        #[test]
524        fn blue() {
525            let a = Xyz::from_color(LinSrgb::new(0.0, 0.0, 1.0));
526            let b = Xyz::new(0.18050, 0.07220, 0.95030);
527            assert_relative_eq!(a, b, epsilon = 0.0001);
528        }
529    }
530
531    #[test]
532    fn ranges() {
533        assert_ranges! {
534            Xyz<D65, f64>;
535            clamped {
536                x: 0.0 => X_N,
537                y: 0.0 => Y_N,
538                z: 0.0 => Z_N
539            }
540            clamped_min {}
541            unclamped {}
542        }
543    }
544
545    raw_pixel_conversion_tests!(Xyz<D65>: x, y, z);
546    raw_pixel_conversion_fail_tests!(Xyz<D65>: x, y, z);
547
548    #[test]
549    fn check_min_max_components() {
550        assert_eq!(Xyz::<D65>::min_x(), 0.0);
551        assert_eq!(Xyz::<D65>::min_y(), 0.0);
552        assert_eq!(Xyz::<D65>::min_z(), 0.0);
553        assert_eq!(Xyz::<D65, f64>::max_x(), X_N);
554        assert_eq!(Xyz::<D65, f64>::max_y(), Y_N);
555        assert_eq!(Xyz::<D65, f64>::max_z(), Z_N);
556    }
557
558    struct_of_arrays_tests!(
559        Xyz<D65>[x, y, z] phantom: white_point,
560        super::Xyza::new(0.1f32, 0.2, 0.3, 0.4),
561        super::Xyza::new(0.2, 0.3, 0.4, 0.5),
562        super::Xyza::new(0.3, 0.4, 0.5, 0.6)
563    );
564
565    #[cfg(feature = "serializing")]
566    #[test]
567    fn serialize() {
568        let serialized = ::serde_json::to_string(&Xyz::<D65>::new(0.3, 0.8, 0.1)).unwrap();
569
570        assert_eq!(serialized, r#"{"x":0.3,"y":0.8,"z":0.1}"#);
571    }
572
573    #[cfg(feature = "serializing")]
574    #[test]
575    fn deserialize() {
576        let deserialized: Xyz = ::serde_json::from_str(r#"{"x":0.3,"y":0.8,"z":0.1}"#).unwrap();
577
578        assert_eq!(deserialized, Xyz::new(0.3, 0.8, 0.1));
579    }
580
581    test_uniform_distribution! {
582        Xyz<D65, f32> {
583            x: (0.0, D65::get_xyz().x),
584            y: (0.0, D65::get_xyz().y),
585            z: (0.0, D65::get_xyz().z)
586        },
587        min: Xyz::new(0.0f32, 0.0, 0.0),
588        max: D65::get_xyz().with_white_point()
589    }
590}