1use 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
27pub type Xyza<Wp = D65, T = f32> = Alpha<Xyz<Wp, T>, T>;
30
31#[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 pub x: T,
54
55 pub y: T,
57
58 pub z: T,
62
63 #[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 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 pub fn into_components(self) -> (T, T, T) {
83 (self.x, self.y, self.z)
84 }
85
86 pub fn from_components((x, y, z): (T, T, T)) -> Self {
88 Self::new(x, y, z)
89 }
90
91 #[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 pub fn min_x() -> T {
113 T::zero()
114 }
115
116 pub fn max_x() -> T {
118 Wp::get_xyz().x
119 }
120
121 pub fn min_y() -> T {
123 T::zero()
124 }
125
126 pub fn max_y() -> T {
128 Wp::get_xyz().y
129 }
130
131 pub fn min_z() -> T {
133 T::zero()
134 }
135
136 pub fn max_z() -> T {
138 Wp::get_xyz().z
139 }
140}
141
142impl<Wp, T, A> Alpha<Xyz<Wp, T>, A> {
144 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 pub fn into_components(self) -> (T, T, T, A) {
154 (self.color.x, self.color.y, self.color.z, self.alpha)
155 }
156
157 pub fn from_components((x, y, z, alpha): (T, T, T, A)) -> Self {
159 Self::new(x, y, z, alpha)
160 }
161
162 #[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 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 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}