1#[cfg(feature = "std")]
5extern crate std;
6
7use core::{any::TypeId, f32::consts::PI};
8
9use crate::{matvecmul, tag::ColorSpaceTag, Chromaticity};
10
11#[cfg(all(not(feature = "std"), not(test)))]
12use crate::floatfuncs::FloatFuncs;
13
14pub trait ColorSpace: Clone + Copy + 'static {
77 const IS_LINEAR: bool = false;
83
84 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::Rectangular;
89
90 const TAG: Option<ColorSpaceTag> = None;
92
93 const WHITE_POINT: Chromaticity = Chromaticity::D65;
98
99 const WHITE_COMPONENTS: [f32; 3];
101
102 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3];
111
112 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3];
116
117 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
126 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
127 src
128 } else {
129 let lin_rgb = Self::to_linear_srgb(src);
130 TargetCS::from_linear_srgb(lin_rgb)
131 }
132 }
133
134 fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
152 let lin_srgb = Self::to_linear_srgb(src);
153 if Self::WHITE_POINT == Chromaticity::D65 {
154 lin_srgb
155 } else {
156 let lin_srgb_adaptation_matrix = const {
157 Chromaticity::D65.linear_srgb_chromatic_adaptation_matrix(Self::WHITE_POINT)
158 };
159 matvecmul(&lin_srgb_adaptation_matrix, lin_srgb)
160 }
161 }
162
163 fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
181 let lin_srgb_adapted = if Self::WHITE_POINT == Chromaticity::D65 {
182 src
183 } else {
184 let lin_srgb_adaptation_matrix = const {
185 Self::WHITE_POINT.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D65)
186 };
187 matvecmul(&lin_srgb_adaptation_matrix, src)
188 };
189 Self::from_linear_srgb(lin_srgb_adapted)
190 }
191
192 fn convert_absolute<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
207 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
208 src
209 } else {
210 let lin_rgb = Self::to_linear_srgb_absolute(src);
211 TargetCS::from_linear_srgb_absolute(lin_rgb)
212 }
213 }
214
215 fn chromatically_adapt(src: [f32; 3], from: Chromaticity, to: Chromaticity) -> [f32; 3] {
221 if from == to {
222 return src;
223 }
224
225 let lin_srgb_adaptation_matrix = if from == Chromaticity::D65 && to == Chromaticity::D50 {
226 Chromaticity::D65.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D50)
227 } else if from == Chromaticity::D50 && to == Chromaticity::D65 {
228 Chromaticity::D50.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D65)
229 } else {
230 from.linear_srgb_chromatic_adaptation_matrix(to)
231 };
232
233 let lin_srgb_adapted = matvecmul(
234 &lin_srgb_adaptation_matrix,
235 Self::to_linear_srgb_absolute(src),
236 );
237 Self::from_linear_srgb_absolute(lin_srgb_adapted)
238 }
239
240 fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] {
246 let rgb = Self::to_linear_srgb(src);
247 let scaled = LinearSrgb::scale_chroma(rgb, scale);
248 Self::from_linear_srgb(scaled)
249 }
250
251 fn clip(src: [f32; 3]) -> [f32; 3];
270}
271
272#[derive(Clone, Copy, PartialEq, Eq, Debug)]
274#[non_exhaustive]
275pub enum ColorSpaceLayout {
276 Rectangular,
278 HueFirst,
280 HueThird,
282}
283
284impl ColorSpaceLayout {
285 pub(crate) const fn scale(self, components: [f32; 3], scale: f32) -> [f32; 3] {
290 match self {
291 Self::Rectangular => [
292 components[0] * scale,
293 components[1] * scale,
294 components[2] * scale,
295 ],
296 Self::HueFirst => [components[0], components[1] * scale, components[2] * scale],
297 Self::HueThird => [components[0] * scale, components[1] * scale, components[2]],
298 }
299 }
300
301 pub(crate) const fn hue_channel(self) -> Option<usize> {
302 match self {
303 Self::Rectangular => None,
304 Self::HueFirst => Some(0),
305 Self::HueThird => Some(2),
306 }
307 }
308}
309
310#[derive(Clone, Copy, Debug)]
322pub struct LinearSrgb;
323
324impl ColorSpace for LinearSrgb {
325 const IS_LINEAR: bool = true;
326
327 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::LinearSrgb);
328
329 const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
330
331 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
332 src
333 }
334
335 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
336 src
337 }
338
339 fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] {
340 let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
341 let l = OKLAB_LMS_TO_LAB[0];
342 let lightness = l[0] * lms[0] + l[1] * lms[1] + l[2] * lms[2];
343 let lms_scaled = [
344 lightness + scale * (lms[0] - lightness),
345 lightness + scale * (lms[1] - lightness),
346 lightness + scale * (lms[2] - lightness),
347 ];
348 matvecmul(&OKLAB_LMS_TO_SRGB, lms_scaled.map(|x| x * x * x))
349 }
350
351 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
352 [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
353 }
354}
355
356impl From<LinearSrgb> for ColorSpaceTag {
357 fn from(_: LinearSrgb) -> Self {
358 Self::LinearSrgb
359 }
360}
361
362#[derive(Clone, Copy, Debug)]
372pub struct Srgb;
373
374fn srgb_to_lin(x: f32) -> f32 {
375 if x.abs() <= 0.04045 {
376 x * (1.0 / 12.92)
377 } else {
378 ((x.abs() + 0.055) * (1.0 / 1.055)).powf(2.4).copysign(x)
379 }
380}
381
382fn lin_to_srgb(x: f32) -> f32 {
383 if x.abs() <= 0.0031308 {
384 x * 12.92
385 } else {
386 (1.055 * x.abs().powf(1.0 / 2.4) - 0.055).copysign(x)
387 }
388}
389
390impl ColorSpace for Srgb {
391 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Srgb);
392
393 const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
394
395 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
396 src.map(srgb_to_lin)
397 }
398
399 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
400 src.map(lin_to_srgb)
401 }
402
403 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
404 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
405 src
406 } else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
407 rgb_to_hsl(src, true)
408 } else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
409 rgb_to_hwb(src)
410 } else {
411 let lin_rgb = Self::to_linear_srgb(src);
412 TargetCS::from_linear_srgb(lin_rgb)
413 }
414 }
415
416 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
417 [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
418 }
419}
420
421impl From<Srgb> for ColorSpaceTag {
422 fn from(_: Srgb) -> Self {
423 Self::Srgb
424 }
425}
426
427#[derive(Clone, Copy, Debug)]
443pub struct DisplayP3;
444
445impl ColorSpace for DisplayP3 {
446 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::DisplayP3);
447
448 const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
449
450 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
451 const LINEAR_DISPLAYP3_TO_SRGB: [[f32; 3]; 3] = [
452 [1.224_940_2, -0.224_940_18, 0.0],
453 [-0.042_056_955, 1.042_056_9, 0.0],
454 [-0.019_637_555, -0.078_636_04, 1.098_273_6],
455 ];
456 matvecmul(&LINEAR_DISPLAYP3_TO_SRGB, src.map(srgb_to_lin))
457 }
458
459 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
460 const LINEAR_SRGB_TO_DISPLAYP3: [[f32; 3]; 3] = [
461 [0.822_461_96, 0.177_538_04, 0.0],
462 [0.033_194_2, 0.966_805_8, 0.0],
463 [0.017_082_632, 0.072_397_44, 0.910_519_96],
464 ];
465 matvecmul(&LINEAR_SRGB_TO_DISPLAYP3, src).map(lin_to_srgb)
466 }
467
468 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
469 [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
470 }
471}
472
473impl From<DisplayP3> for ColorSpaceTag {
474 fn from(_: DisplayP3) -> Self {
475 Self::DisplayP3
476 }
477}
478
479#[derive(Clone, Copy, Debug)]
494pub struct A98Rgb;
495
496impl ColorSpace for A98Rgb {
497 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::A98Rgb);
498
499 const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
500
501 fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
502 #[expect(
504 clippy::cast_possible_truncation,
505 reason = "exact rational, truncate at compile-time"
506 )]
507 const LINEAR_A98RGB_TO_SRGB: [[f32; 3]; 3] = [
508 [
509 (66_942_405. / 47_872_228.) as f32,
510 (-19_070_177. / 47_872_228.) as f32,
511 0.,
512 ],
513 [0., 1., 0.],
514 [
515 0.,
516 (-11_512_411. / 268_173_353.) as f32,
517 (279_685_764. / 268_173_353.) as f32,
518 ],
519 ];
520 matvecmul(
521 &LINEAR_A98RGB_TO_SRGB,
522 [r, g, b].map(|x| x.abs().powf(563. / 256.).copysign(x)),
523 )
524 }
525
526 fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
527 #[expect(
529 clippy::cast_possible_truncation,
530 reason = "exact rational, truncate at compile-time"
531 )]
532 const LINEAR_SRGB_TO_A98RGB: [[f32; 3]; 3] = [
533 [
534 (47_872_228. / 66_942_405.) as f32,
535 (19_070_177. / 66_942_405.) as f32,
536 0.0,
537 ],
538 [0., 1., 0.],
539 [
540 0.,
541 (11_512_411. / 279_685_764.) as f32,
542 (268_173_353. / 279_685_764.) as f32,
543 ],
544 ];
545 matvecmul(&LINEAR_SRGB_TO_A98RGB, [r, g, b]).map(|x| x.abs().powf(256. / 563.).copysign(x))
546 }
547
548 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
549 [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
550 }
551}
552
553impl From<A98Rgb> for ColorSpaceTag {
554 fn from(_: A98Rgb) -> Self {
555 Self::A98Rgb
556 }
557}
558
559#[derive(Clone, Copy, Debug)]
577pub struct ProphotoRgb;
578
579impl ProphotoRgb {
580 fn transfer_to_linear(x: f32) -> f32 {
581 if x.abs() <= 16. / 512. {
582 x / 16.
583 } else {
584 x.abs().powf(1.8).copysign(x)
585 }
586 }
587
588 fn transfer_from_linear(x: f32) -> f32 {
589 if x.abs() <= 1. / 512. {
590 x * 16.
591 } else {
592 x.abs().powf(1. / 1.8).copysign(x)
593 }
594 }
595}
596
597impl ColorSpace for ProphotoRgb {
598 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::ProphotoRgb);
599
600 const WHITE_POINT: Chromaticity = Chromaticity::D50;
601 const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
602
603 fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
604 const LINEAR_PROPHOTORGB_TO_SRGB: [[f32; 3]; 3] = [
606 [2.034_367_6, -0.727_634_5, -0.306_733_07],
607 [-0.228_826_79, 1.231_753_3, -0.002_926_598],
608 [-0.008_558_424, -0.153_268_2, 1.161_826_6],
609 ];
610
611 matvecmul(
612 &LINEAR_PROPHOTORGB_TO_SRGB,
613 [r, g, b].map(Self::transfer_to_linear),
614 )
615 }
616
617 fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
618 const LINEAR_SRGB_TO_PROPHOTORGB: [[f32; 3]; 3] = [
620 [0.529_280_4, 0.330_153, 0.140_566_6],
621 [0.098_366_22, 0.873_463_9, 0.028_169_824],
622 [0.016_875_342, 0.117_659_41, 0.865_465_2],
623 ];
624
625 matvecmul(&LINEAR_SRGB_TO_PROPHOTORGB, [r, g, b]).map(Self::transfer_from_linear)
626 }
627
628 fn to_linear_srgb_absolute([r, g, b]: [f32; 3]) -> [f32; 3] {
629 const LINEAR_PROPHOTORGB_TO_SRGB: [[f32; 3]; 3] = [
631 [
632 11_822_636_894_621. / 5_517_784_378_314.,
633 -2_646_118_971_832. / 4_032_227_045_691.,
634 -2_824_985_149. / 9_114_754_233.,
635 ],
636 [
637 -270_896_603_412_176. / 1_163_584_209_404_097.,
638 107_798_623_831_136. / 89_506_477_646_469.,
639 822_014_396. / 202_327_283_847.,
640 ],
641 [
642 -2412976100974. / 167_796_255_001_401.,
643 -1_777_081_293_536. / 12_907_404_230_877.,
644 879_168_464. / 1_006_099_419.,
645 ],
646 ];
647
648 matvecmul(
649 &LINEAR_PROPHOTORGB_TO_SRGB,
650 [r, g, b].map(Self::transfer_to_linear),
651 )
652 }
653
654 fn from_linear_srgb_absolute([r, g, b]: [f32; 3]) -> [f32; 3] {
655 const LINEAR_SRGB_TO_PROPHOTORGB: [[f32; 3]; 3] = [
657 [
658 7_356_071_250_722. / 14_722_127_359_275.,
659 25_825_157_007_599. / 88_332_764_155_650.,
660 1_109_596_896_521. / 6_309_483_153_975.,
661 ],
662 [
663 170_513_936_009. / 1_766_822_975_400.,
664 18_792_073_269_331. / 21_201_875_704_800.,
665 91_195_554_323. / 3_028_839_386_400.,
666 ],
667 [
668 946_201. / 40_387_053.,
669 105_017_795. / 726_966_954.,
670 8_250_997. / 7_162_236.,
671 ],
672 ];
673
674 matvecmul(&LINEAR_SRGB_TO_PROPHOTORGB, [r, g, b]).map(Self::transfer_from_linear)
675 }
676
677 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
678 [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
679 }
680}
681
682impl From<ProphotoRgb> for ColorSpaceTag {
683 fn from(_: ProphotoRgb) -> Self {
684 Self::ProphotoRgb
685 }
686}
687
688#[derive(Clone, Copy, Debug)]
704pub struct Rec2020;
705
706impl Rec2020 {
707 const A: f32 = 1.099_296_8;
710 const B: f32 = 0.018_053_97;
711}
712
713impl ColorSpace for Rec2020 {
714 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Rec2020);
715
716 const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
717
718 fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
719 #[expect(
721 clippy::cast_possible_truncation,
722 reason = "exact rational, truncate at compile-time"
723 )]
724 const LINEAR_REC2020_TO_SRGB: [[f32; 3]; 3] = [
725 [
726 (2_785_571_537. / 1_677_558_947.) as f32,
727 (-985_802_650. / 1_677_558_947.) as f32,
728 (-122_209_940. / 1_677_558_947.) as f32,
729 ],
730 [
731 (-4_638_020_506. / 37_238_079_773.) as f32,
732 (42_187_016_744. / 37_238_079_773.) as f32,
733 (-310_916_465. / 37_238_079_773.) as f32,
734 ],
735 [
736 (-97_469_024. / 5_369_968_309.) as f32,
737 (-3_780_738_464. / 37_589_778_163.) as f32,
738 (42_052_799_795. / 37_589_778_163.) as f32,
739 ],
740 ];
741
742 fn transfer(x: f32) -> f32 {
743 if x.abs() < Rec2020::B * 4.5 {
744 x * (1. / 4.5)
745 } else {
746 ((x.abs() + (Rec2020::A - 1.)) / Rec2020::A)
747 .powf(1. / 0.45)
748 .copysign(x)
749 }
750 }
751
752 matvecmul(&LINEAR_REC2020_TO_SRGB, [r, g, b].map(transfer))
753 }
754
755 fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
756 #[expect(
758 clippy::cast_possible_truncation,
759 reason = "exact rational, truncate at compile-time"
760 )]
761 const LINEAR_SRGB_TO_REC2020: [[f32; 3]; 3] = [
762 [
763 (2_939_026_994. / 4_684_425_795.) as f32,
764 (9_255_011_753. / 28_106_554_770.) as f32,
765 (173_911_579. / 4_015_222_110.) as f32,
766 ],
767 [
768 (76_515_593. / 1_107_360_270.) as f32,
769 (6_109_575_001. / 6_644_161_620.) as f32,
770 (75_493_061. / 6_644_161_620.) as f32,
771 ],
772 [
773 (12_225_392. / 745_840_075.) as f32,
774 (1_772_384_008. / 20_137_682_025.) as f32,
775 (18_035_212_433. / 20_137_682_025.) as f32,
776 ],
777 ];
778
779 fn transfer(x: f32) -> f32 {
780 if x.abs() < Rec2020::B {
781 x * 4.5
782 } else {
783 (Rec2020::A * x.abs().powf(0.45) - (Rec2020::A - 1.)).copysign(x)
784 }
785 }
786 matvecmul(&LINEAR_SRGB_TO_REC2020, [r, g, b]).map(transfer)
787 }
788
789 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
790 [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
791 }
792}
793
794impl From<Rec2020> for ColorSpaceTag {
795 fn from(_: Rec2020) -> Self {
796 Self::Rec2020
797 }
798}
799
800#[derive(Clone, Copy, Debug)]
821pub struct Aces2065_1;
822
823impl ColorSpace for Aces2065_1 {
824 const IS_LINEAR: bool = true;
825
826 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Aces2065_1);
827
828 const WHITE_POINT: Chromaticity = Chromaticity::ACES;
829 const WHITE_COMPONENTS: [f32; 3] = [1.0, 1.0, 1.0];
830
831 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
832 const ACES2065_1_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
834 [2.521_686, -1.134_131, -0.387_555_2],
835 [-0.276_479_9, 1.372_719, -0.096_239_17],
836 [-0.015_378_065, -0.152_975_34, 1.168_353_4],
837 ];
838 matvecmul(&ACES2065_1_TO_LINEAR_SRGB, src)
839 }
840
841 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
842 const LINEAR_SRGB_TO_ACES2065_1: [[f32; 3]; 3] = [
844 [0.439_632_98, 0.382_988_7, 0.177_378_33],
845 [0.089_776_44, 0.813_439_4, 0.096_784_13],
846 [0.017_541_17, 0.111_546_55, 0.870_912_25],
847 ];
848 matvecmul(&LINEAR_SRGB_TO_ACES2065_1, src)
849 }
850
851 fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
852 const ACES2065_1_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
854 [
855 54_120_196_967_290_615. / 21_154_043_450_084_358.,
856 -320_017_885_460_000. / 285_865_452_028_167.,
857 -564_067_687_050. / 1_439_638_182_257.,
858 ],
859 [
860 -65_267_199_138_999_760. / 234_786_371_866_236_861.,
861 320_721_924_808_012_000. / 234_786_371_866_236_861.,
862 -2_987_552_619_450. / 31_956_767_642_063.,
863 ],
864 [
865 -581_359_048_862_990. / 33_857_690_407_037_013.,
866 -457_168_407_800_000. / 3_077_971_855_185_183.,
867 4_981_730_664_150. / 4_608_369_457_879.,
868 ],
869 ];
870 matvecmul(&ACES2065_1_TO_LINEAR_SRGB, src)
871 }
872
873 fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
874 const LINEAR_SRGB_TO_ACES2065_1: [[f32; 3]; 3] = [
876 [
877 26_324_697_889_654. / 60_805_826_029_215.,
878 95_867_335_448_462. / 255_384_469_322_703.,
879 34_545_867_731_048. / 182_417_478_087_645.,
880 ],
881 [
882 1_068_725_544_495_979. / 11_952_668_021_931_000.,
883 9_008_998_273_654_297. / 11_033_232_020_244_000.,
884 2_110_950_307_239_113. / 20_490_288_037_596_000.,
885 ],
886 [
887 267_367_106. / 13_953_194_325.,
888 2_967_477_727. / 25_115_749_785.,
889 33_806_406_089. / 35_879_642_550.,
890 ],
891 ];
892 matvecmul(&LINEAR_SRGB_TO_ACES2065_1, src)
893 }
894
895 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
896 [
897 r.clamp(-65504., 65504.),
898 g.clamp(-65504., 65504.),
899 b.clamp(-65504., 65504.),
900 ]
901 }
902}
903
904impl From<Aces2065_1> for ColorSpaceTag {
905 fn from(_: Aces2065_1) -> Self {
906 Self::Aces2065_1
907 }
908}
909
910#[derive(Clone, Copy, Debug)]
929pub struct AcesCg;
930
931impl ColorSpace for AcesCg {
932 const IS_LINEAR: bool = true;
933
934 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::AcesCg);
935
936 const WHITE_POINT: Chromaticity = Chromaticity::ACES;
937 const WHITE_COMPONENTS: [f32; 3] = [1.0, 1.0, 1.0];
938
939 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
940 const ACESCG_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
942 [1.705_051, -0.621_792_14, -0.083_258_875],
943 [-0.130_256_41, 1.140_804_8, -0.010_548_319],
944 [-0.024_003_357, -0.128_968_97, 1.152_972_3],
945 ];
946 matvecmul(&ACESCG_TO_LINEAR_SRGB, src)
947 }
948
949 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
950 const LINEAR_SRGB_TO_ACESCG: [[f32; 3]; 3] = [
952 [0.613_097_4, 0.339_523_14, 0.047_379_453],
953 [0.070_193_72, 0.916_353_9, 0.013_452_399],
954 [0.020_615_593, 0.109_569_77, 0.869_814_63],
955 ];
956 matvecmul(&LINEAR_SRGB_TO_ACESCG, src)
957 }
958
959 fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
960 const ACESCG_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
962 [
963 9_932_023_100_445. / 5_736_895_993_442.,
964 -1_732_666_183_650. / 2_868_447_996_721.,
965 -229_784_797_280. / 2_868_447_996_721.,
966 ],
967 [
968 -194_897_543_280. / 1_480_771_385_773.,
969 72_258_955_647_750. / 63_673_169_588_239.,
970 -552_646_980_800. / 63_673_169_588_239.,
971 ],
972 [
973 -68_657_089_110. / 2_794_545_067_783.,
974 -8082548957250. / 64_274_536_559_009.,
975 14_669_805_440. / 13_766_231_861.,
976 ],
977 ];
978 matvecmul(&ACESCG_TO_LINEAR_SRGB, src)
979 }
980
981 fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
982 const LINEAR_SRGB_TO_ACESCG: [[f32; 3]; 3] = [
984 [
985 2_095_356_009_722. / 3_474_270_183_447.,
986 17_006_614_853_437. / 52_114_052_751_705.,
987 71_464_174_897. / 1_488_972_935_763.,
988 ],
989 [
990 1_774_515_482_522. / 25_307_573_950_575.,
991 69_842_555_782_672. / 75_922_721_851_725.,
992 276_870_186_577. / 21_692_206_243_350.,
993 ],
994 [
995 101_198_449_621. / 4_562_827_993_584.,
996 31_778_718_978_443. / 273_769_679_615_040.,
997 1_600_138_878_851. / 1_700_432_792_640.,
998 ],
999 ];
1000 matvecmul(&LINEAR_SRGB_TO_ACESCG, src)
1001 }
1002
1003 fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
1004 [
1005 r.clamp(-65504., 65504.),
1006 g.clamp(-65504., 65504.),
1007 b.clamp(-65504., 65504.),
1008 ]
1009 }
1010}
1011
1012impl From<AcesCg> for ColorSpaceTag {
1013 fn from(_: AcesCg) -> Self {
1014 Self::AcesCg
1015 }
1016}
1017
1018#[derive(Clone, Copy, Debug)]
1034pub struct XyzD50;
1035
1036impl ColorSpace for XyzD50 {
1037 const IS_LINEAR: bool = true;
1038
1039 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::XyzD50);
1040
1041 const WHITE_POINT: Chromaticity = Chromaticity::D50;
1042 const WHITE_COMPONENTS: [f32; 3] = [3457. / 3585., 1., 986. / 1195.];
1043
1044 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1045 const XYZ_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
1047 [3.134_136, -1.617_386, -0.490_662_22],
1048 [-0.978_795_47, 1.916_254_4, 0.033_442_874],
1049 [0.071_955_39, -0.228_976_76, 1.405_386_1],
1050 ];
1051 matvecmul(&XYZ_TO_LINEAR_SRGB, src)
1052 }
1053
1054 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1055 const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1057 [0.436_065_73, 0.385_151_5, 0.143_078_42],
1058 [0.222_493_17, 0.716_887, 0.060_619_81],
1059 [0.013_923_922, 0.097_081_326, 0.714_099_35],
1060 ];
1061 matvecmul(&LINEAR_SRGB_TO_XYZ, src)
1062 }
1063
1064 fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
1065 [x, y, z]
1066 }
1067}
1068
1069impl From<XyzD50> for ColorSpaceTag {
1070 fn from(_: XyzD50) -> Self {
1071 Self::XyzD50
1072 }
1073}
1074
1075#[derive(Clone, Copy, Debug)]
1126pub struct XyzD65;
1127
1128impl ColorSpace for XyzD65 {
1129 const IS_LINEAR: bool = true;
1130
1131 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::XyzD65);
1132
1133 const WHITE_COMPONENTS: [f32; 3] = [3127. / 3290., 1., 3583. / 3290.];
1134
1135 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1136 const XYZ_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
1137 [3.240_97, -1.537_383_2, -0.498_610_76],
1138 [-0.969_243_65, 1.875_967_5, 0.041_555_06],
1139 [0.055_630_08, -0.203_976_96, 1.056_971_5],
1140 ];
1141 matvecmul(&XYZ_TO_LINEAR_SRGB, src)
1142 }
1143
1144 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1145 const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1146 [0.412_390_8, 0.357_584_33, 0.180_480_8],
1147 [0.212_639, 0.715_168_65, 0.072_192_32],
1148 [0.019_330_818, 0.119_194_78, 0.950_532_14],
1149 ];
1150 matvecmul(&LINEAR_SRGB_TO_XYZ, src)
1151 }
1152
1153 fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
1154 [x, y, z]
1155 }
1156}
1157
1158impl From<XyzD65> for ColorSpaceTag {
1159 fn from(_: XyzD65) -> Self {
1160 Self::XyzD65
1161 }
1162}
1163
1164#[derive(Clone, Copy, Debug)]
1184pub struct Oklab;
1185
1186const OKLAB_LAB_TO_LMS: [[f32; 3]; 3] = [
1190 [1.0, 0.396_337_78, 0.215_803_76],
1191 [1.0, -0.105_561_346, -0.063_854_17],
1192 [1.0, -0.089_484_18, -1.291_485_5],
1193];
1194
1195const OKLAB_LMS_TO_SRGB: [[f32; 3]; 3] = [
1196 [4.076_741_7, -3.307_711_6, 0.230_969_94],
1197 [-1.268_438, 2.609_757_4, -0.341_319_38],
1198 [-0.004_196_086_3, -0.703_418_6, 1.707_614_7],
1199];
1200
1201const OKLAB_SRGB_TO_LMS: [[f32; 3]; 3] = [
1202 [0.412_221_46, 0.536_332_55, 0.051_445_995],
1203 [0.211_903_5, 0.680_699_5, 0.107_396_96],
1204 [0.088_302_46, 0.281_718_85, 0.629_978_7],
1205];
1206
1207const OKLAB_LMS_TO_LAB: [[f32; 3]; 3] = [
1208 [0.210_454_26, 0.793_617_8, -0.004_072_047],
1209 [1.977_998_5, -2.428_592_2, 0.450_593_7],
1210 [0.025_904_037, 0.782_771_77, -0.808_675_77],
1211];
1212
1213impl ColorSpace for Oklab {
1214 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklab);
1215
1216 const WHITE_COMPONENTS: [f32; 3] = [1., 0., 0.];
1217
1218 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1219 let lms = matvecmul(&OKLAB_LAB_TO_LMS, src).map(|x| x * x * x);
1220 matvecmul(&OKLAB_LMS_TO_SRGB, lms)
1221 }
1222
1223 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1224 let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
1225 matvecmul(&OKLAB_LMS_TO_LAB, lms)
1226 }
1227
1228 fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1229 [l, a * scale, b * scale]
1230 }
1231
1232 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1233 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1234 src
1235 } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklch>() {
1236 lab_to_lch(src)
1237 } else {
1238 let lin_rgb = Self::to_linear_srgb(src);
1239 TargetCS::from_linear_srgb(lin_rgb)
1240 }
1241 }
1242
1243 fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1244 [l.clamp(0., 1.), a, b]
1245 }
1246}
1247
1248impl From<Oklab> for ColorSpaceTag {
1249 fn from(_: Oklab) -> Self {
1250 Self::Oklab
1251 }
1252}
1253
1254fn lab_to_lch([l, a, b]: [f32; 3]) -> [f32; 3] {
1256 let mut h = b.atan2(a) * (180. / PI);
1257 if h < 0.0 {
1258 h += 360.0;
1259 }
1260 let c = b.hypot(a);
1261 [l, c, h]
1262}
1263
1264fn lch_to_lab([l, c, h]: [f32; 3]) -> [f32; 3] {
1266 let (sin, cos) = (h * (PI / 180.)).sin_cos();
1267 let a = c * cos;
1268 let b = c * sin;
1269 [l, a, b]
1270}
1271
1272#[derive(Clone, Copy, Debug)]
1280pub struct Oklch;
1281
1282impl ColorSpace for Oklch {
1283 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklch);
1284
1285 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1286
1287 const WHITE_COMPONENTS: [f32; 3] = [1., 0., 90.];
1288
1289 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1290 lab_to_lch(Oklab::from_linear_srgb(src))
1291 }
1292
1293 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1294 Oklab::to_linear_srgb(lch_to_lab(src))
1295 }
1296
1297 fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1298 [l, c * scale, h]
1299 }
1300
1301 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1302 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1303 src
1304 } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklab>() {
1305 lch_to_lab(src)
1306 } else {
1307 let lin_rgb = Self::to_linear_srgb(src);
1308 TargetCS::from_linear_srgb(lin_rgb)
1309 }
1310 }
1311
1312 fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1313 [l.clamp(0., 1.), c.max(0.), h]
1314 }
1315}
1316
1317impl From<Oklch> for ColorSpaceTag {
1318 fn from(_: Oklch) -> Self {
1319 Self::Oklch
1320 }
1321}
1322
1323#[derive(Clone, Copy, Debug)]
1353pub struct Lab;
1354
1355const LAB_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1360 [0.452_211_65, 0.399_412_24, 0.148_376_09],
1361 [0.222_493_17, 0.716_887, 0.060_619_81],
1362 [0.016_875_342, 0.117_659_41, 0.865_465_2],
1363];
1364
1365const LAB_XYZ_TO_SRGB: [[f32; 3]; 3] = [
1367 [3.022_233_7, -1.617_386, -0.404_847_65],
1368 [-0.943_848_25, 1.916_254_4, 0.027_593_868],
1369 [0.069_386_27, -0.228_976_76, 1.159_590_5],
1370];
1371
1372const EPSILON: f32 = 216. / 24389.;
1373const KAPPA: f32 = 24389. / 27.;
1374
1375impl ColorSpace for Lab {
1376 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lab);
1377
1378 const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1379
1380 fn to_linear_srgb([l, a, b]: [f32; 3]) -> [f32; 3] {
1381 let f1 = l * (1. / 116.) + (16. / 116.);
1382 let f0 = a * (1. / 500.) + f1;
1383 let f2 = f1 - b * (1. / 200.);
1384 let xyz = [f0, f1, f2].map(|value| {
1385 const EPSILON_CBRT: f32 = 0.206_896_56;
1387 if value > EPSILON_CBRT {
1388 value * value * value
1389 } else {
1390 (116. / KAPPA) * value - (16. / KAPPA)
1391 }
1392 });
1393 matvecmul(&LAB_XYZ_TO_SRGB, xyz)
1394 }
1395
1396 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1397 let xyz = matvecmul(&LAB_SRGB_TO_XYZ, src);
1398 let f = xyz.map(|value| {
1399 if value > EPSILON {
1400 value.cbrt()
1401 } else {
1402 (KAPPA / 116.) * value + (16. / 116.)
1403 }
1404 });
1405 let l = 116. * f[1] - 16.;
1406 let a = 500. * (f[0] - f[1]);
1407 let b = 200. * (f[1] - f[2]);
1408 [l, a, b]
1409 }
1410
1411 fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1412 [l, a * scale, b * scale]
1413 }
1414
1415 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1416 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1417 src
1418 } else if TypeId::of::<TargetCS>() == TypeId::of::<Lch>() {
1419 lab_to_lch(src)
1420 } else {
1421 let lin_rgb = Self::to_linear_srgb(src);
1422 TargetCS::from_linear_srgb(lin_rgb)
1423 }
1424 }
1425
1426 fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1427 [l.clamp(0., 100.), a, b]
1428 }
1429}
1430
1431impl From<Lab> for ColorSpaceTag {
1432 fn from(_: Lab) -> Self {
1433 Self::Lab
1434 }
1435}
1436
1437#[derive(Clone, Copy, Debug)]
1447pub struct Lch;
1448
1449impl ColorSpace for Lch {
1450 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lch);
1451
1452 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1453
1454 const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1455
1456 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1457 lab_to_lch(Lab::from_linear_srgb(src))
1458 }
1459
1460 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1461 Lab::to_linear_srgb(lch_to_lab(src))
1462 }
1463
1464 fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1465 [l, c * scale, h]
1466 }
1467
1468 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1469 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1470 src
1471 } else if TypeId::of::<TargetCS>() == TypeId::of::<Lab>() {
1472 lch_to_lab(src)
1473 } else {
1474 let lin_rgb = Self::to_linear_srgb(src);
1475 TargetCS::from_linear_srgb(lin_rgb)
1476 }
1477 }
1478
1479 fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1480 [l.clamp(0., 100.), c.max(0.), h]
1481 }
1482}
1483
1484impl From<Lch> for ColorSpaceTag {
1485 fn from(_: Lch) -> Self {
1486 Self::Lch
1487 }
1488}
1489
1490#[derive(Clone, Copy, Debug)]
1505pub struct Hsl;
1506
1507fn hsl_to_rgb([h, s, l]: [f32; 3]) -> [f32; 3] {
1511 let sat = s * 0.01;
1513 let light = l * 0.01;
1514 let a = sat * light.min(1.0 - light);
1515 [0.0, 8.0, 4.0].map(|n| {
1516 let x = n + h * (1.0 / 30.0);
1517 let k = x - 12.0 * (x * (1.0 / 12.0)).floor();
1518 light - a * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0)
1519 })
1520}
1521
1522fn rgb_to_hsl([r, g, b]: [f32; 3], hue_hack: bool) -> [f32; 3] {
1529 let max = r.max(g).max(b);
1530 let min = r.min(g).min(b);
1531 let mut hue = 0.0;
1532 let mut sat = 0.0;
1533 let light = 0.5 * (min + max);
1534 let d = max - min;
1535
1536 const EPSILON: f32 = 1e-6;
1537 if d > EPSILON {
1538 let denom = light.min(1.0 - light);
1539 if denom.abs() > EPSILON {
1540 sat = (max - light) / denom;
1541 }
1542 hue = if max == r {
1543 (g - b) / d
1544 } else if max == g {
1545 (b - r) / d + 2.0
1546 } else {
1547 (r - g) / d + 4.0
1549 };
1550 hue *= 60.0;
1551 if hue_hack && sat < 0.0 {
1553 hue += 180.0;
1554 sat = sat.abs();
1555 }
1556 hue -= 360. * (hue * (1.0 / 360.0)).floor();
1557 }
1558 [hue, sat * 100.0, light * 100.0]
1559}
1560
1561impl ColorSpace for Hsl {
1562 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hsl);
1563
1564 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1565
1566 const WHITE_COMPONENTS: [f32; 3] = [0., 0., 100.];
1567
1568 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1569 let rgb = Srgb::from_linear_srgb(src);
1570 rgb_to_hsl(rgb, true)
1571 }
1572
1573 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1574 let rgb = hsl_to_rgb(src);
1575 Srgb::to_linear_srgb(rgb)
1576 }
1577
1578 fn scale_chroma([h, s, l]: [f32; 3], scale: f32) -> [f32; 3] {
1579 [h, s * scale, l]
1580 }
1581
1582 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1583 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1584 src
1585 } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1586 hsl_to_rgb(src)
1587 } else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
1588 rgb_to_hwb(hsl_to_rgb(src))
1589 } else {
1590 let lin_rgb = Self::to_linear_srgb(src);
1591 TargetCS::from_linear_srgb(lin_rgb)
1592 }
1593 }
1594
1595 fn clip([h, s, l]: [f32; 3]) -> [f32; 3] {
1596 [h, s.max(0.), l.clamp(0., 100.)]
1597 }
1598}
1599
1600impl From<Hsl> for ColorSpaceTag {
1601 fn from(_: Hsl) -> Self {
1602 Self::Hsl
1603 }
1604}
1605
1606#[derive(Clone, Copy, Debug)]
1627pub struct Hwb;
1628
1629fn hwb_to_rgb([h, w, b]: [f32; 3]) -> [f32; 3] {
1633 let white = w * 0.01;
1634 let black = b * 0.01;
1635 if white + black >= 1.0 {
1636 let gray = white / (white + black);
1637 [gray, gray, gray]
1638 } else {
1639 let rgb = hsl_to_rgb([h, 100., 50.]);
1640 rgb.map(|x| white + x * (1.0 - white - black))
1641 }
1642}
1643
1644fn rgb_to_hwb([r, g, b]: [f32; 3]) -> [f32; 3] {
1648 let hsl = rgb_to_hsl([r, g, b], false);
1649 let white = r.min(g).min(b);
1650 let black = 1.0 - r.max(g).max(b);
1651 [hsl[0], white * 100., black * 100.]
1652}
1653
1654impl ColorSpace for Hwb {
1655 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hwb);
1656
1657 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1658
1659 const WHITE_COMPONENTS: [f32; 3] = [0., 100., 0.];
1660
1661 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1662 let rgb = Srgb::from_linear_srgb(src);
1663 rgb_to_hwb(rgb)
1664 }
1665
1666 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1667 let rgb = hwb_to_rgb(src);
1668 Srgb::to_linear_srgb(rgb)
1669 }
1670
1671 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1672 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1673 src
1674 } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1675 hwb_to_rgb(src)
1676 } else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
1677 rgb_to_hsl(hwb_to_rgb(src), true)
1678 } else {
1679 let lin_rgb = Self::to_linear_srgb(src);
1680 TargetCS::from_linear_srgb(lin_rgb)
1681 }
1682 }
1683
1684 fn clip([h, w, b]: [f32; 3]) -> [f32; 3] {
1685 [h, w.clamp(0., 100.), b.clamp(0., 100.)]
1686 }
1687}
1688
1689impl From<Hwb> for ColorSpaceTag {
1690 fn from(_: Hwb) -> Self {
1691 Self::Hwb
1692 }
1693}
1694
1695#[cfg(test)]
1696mod tests {
1697 extern crate alloc;
1698
1699 use crate::{
1700 A98Rgb, Aces2065_1, AcesCg, Chromaticity, ColorSpace, DisplayP3, Hsl, Hwb, Lab, Lch,
1701 LinearSrgb, Oklab, Oklch, OpaqueColor, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65,
1702 };
1703 use alloc::vec::Vec;
1704
1705 #[must_use]
1706 fn almost_equal<CS: ColorSpace>(col1: [f32; 3], col2: [f32; 3], absolute_epsilon: f32) -> bool {
1707 OpaqueColor::<CS>::new(col1).difference(OpaqueColor::new(col2)) <= absolute_epsilon
1708 }
1709
1710 fn magnitude(col: [f32; 3]) -> f32 {
1712 col[0].abs().max(col[1].abs()).max(col[2].abs())
1713 }
1714
1715 #[test]
1716 fn roundtrip() {
1717 fn test_roundtrips<Source: ColorSpace, Dest: ColorSpace>(colors: &[[f32; 3]]) {
1718 const RELATIVE_EPSILON: f32 = f32::EPSILON * 16.;
1720
1721 for color in colors {
1722 let intermediate = Source::convert::<Dest>(*color);
1723 let roundtripped = Dest::convert::<Source>(intermediate);
1724
1725 let linsrgb_color = Source::to_linear_srgb(*color);
1728 let linsrgb_roundtripped = Source::to_linear_srgb(roundtripped);
1729
1730 let absolute_epsilon = magnitude(linsrgb_color).max(1.) * RELATIVE_EPSILON;
1734 assert!(almost_equal::<LinearSrgb>(
1735 linsrgb_color,
1736 linsrgb_roundtripped,
1737 absolute_epsilon,
1738 ));
1739 }
1740 }
1741
1742 let rectangular_values = {
1744 let components = [
1745 0., 1., -1., 0.5, 1234., -1234., 1.000_001, 0.000_001, -0.000_001,
1746 ];
1747 let mut values = Vec::new();
1748 for c0 in components {
1749 for c1 in components {
1750 for c2 in components {
1751 values.push([c0, c1, c2]);
1752 }
1753 }
1754 }
1755 values
1756 };
1757
1758 test_roundtrips::<LinearSrgb, Srgb>(&rectangular_values);
1759 test_roundtrips::<DisplayP3, Srgb>(&rectangular_values);
1760 test_roundtrips::<A98Rgb, Srgb>(&rectangular_values);
1761 test_roundtrips::<ProphotoRgb, Srgb>(&rectangular_values);
1762 test_roundtrips::<Rec2020, Srgb>(&rectangular_values);
1763 test_roundtrips::<Aces2065_1, Srgb>(&rectangular_values);
1764 test_roundtrips::<AcesCg, Srgb>(&rectangular_values);
1765 test_roundtrips::<XyzD50, Srgb>(&rectangular_values);
1766 test_roundtrips::<XyzD65, Srgb>(&rectangular_values);
1767
1768 test_roundtrips::<Oklab, Srgb>(&[
1769 [0., 0., 0.],
1770 [1., 0., 0.],
1771 [0.2, 0.2, -0.1],
1772 [2.0, 0., -0.4],
1773 ]);
1774 }
1775
1776 #[test]
1777 fn white_components() {
1778 fn check_white<CS: ColorSpace>() {
1779 assert!(almost_equal::<Srgb>(
1780 Srgb::WHITE_COMPONENTS,
1781 CS::convert::<Srgb>(CS::WHITE_COMPONENTS),
1782 1e-4,
1783 ));
1784 assert!(almost_equal::<CS>(
1785 CS::WHITE_COMPONENTS,
1786 Srgb::convert::<CS>(Srgb::WHITE_COMPONENTS),
1787 1e-4,
1788 ));
1789 }
1790
1791 check_white::<A98Rgb>();
1792 check_white::<DisplayP3>();
1793 check_white::<Hsl>();
1794 check_white::<Hwb>();
1795 check_white::<Lab>();
1796 check_white::<Lch>();
1797 check_white::<LinearSrgb>();
1798 check_white::<Oklab>();
1799 check_white::<Oklch>();
1800 check_white::<ProphotoRgb>();
1801 check_white::<Rec2020>();
1802 check_white::<Aces2065_1>();
1803 check_white::<AcesCg>();
1804 check_white::<XyzD50>();
1805 check_white::<XyzD65>();
1806 }
1807
1808 #[test]
1809 fn a98rgb_srgb() {
1810 for (srgb, a98) in [
1811 ([0.1, 0.2, 0.3], [0.155_114, 0.212_317, 0.301_498]),
1812 ([0., 1., 0.], [0.564_972, 1., 0.234_424]),
1813 ] {
1814 assert!(almost_equal::<Srgb>(
1815 srgb,
1816 A98Rgb::convert::<Srgb>(a98),
1817 1e-4
1818 ));
1819 assert!(almost_equal::<A98Rgb>(
1820 a98,
1821 Srgb::convert::<A98Rgb>(srgb),
1822 1e-4
1823 ));
1824 }
1825 }
1826
1827 #[test]
1828 fn prophotorgb_srgb() {
1829 for (srgb, prophoto) in [
1830 ([0.1, 0.2, 0.3], [0.133136, 0.147659, 0.223581]),
1831 ([0., 1., 0.], [0.540282, 0.927599, 0.304566]),
1832 ] {
1833 assert!(almost_equal::<Srgb>(
1834 srgb,
1835 ProphotoRgb::convert::<Srgb>(prophoto),
1836 1e-4
1837 ));
1838 assert!(almost_equal::<ProphotoRgb>(
1839 prophoto,
1840 Srgb::convert::<ProphotoRgb>(srgb),
1841 1e-4
1842 ));
1843 }
1844 }
1845
1846 #[test]
1847 fn rec2020_srgb() {
1848 for (srgb, rec2020) in [
1849 ([0.1, 0.2, 0.3], [0.091284, 0.134169, 0.230056]),
1850 ([0.05, 0.1, 0.15], [0.029785, 0.043700, 0.083264]),
1851 ([0., 1., 0.], [0.567542, 0.959279, 0.268969]),
1852 ] {
1853 assert!(almost_equal::<Srgb>(
1854 srgb,
1855 Rec2020::convert::<Srgb>(rec2020),
1856 1e-4
1857 ));
1858 assert!(almost_equal::<Rec2020>(
1859 rec2020,
1860 Srgb::convert::<Rec2020>(srgb),
1861 1e-4
1862 ));
1863 }
1864 }
1865
1866 #[test]
1867 fn aces2065_1_srgb() {
1868 for (srgb, aces2065_1) in [
1869 ([0.6, 0.5, 0.4], [0.245_59, 0.215_57, 0.145_18]),
1870 ([0.0, 0.5, 1.0], [0.259_35, 0.270_89, 0.894_79]),
1871 ] {
1872 assert!(almost_equal::<Srgb>(
1873 srgb,
1874 Aces2065_1::convert::<Srgb>(aces2065_1),
1875 1e-4
1876 ));
1877 assert!(almost_equal::<Aces2065_1>(
1878 aces2065_1,
1879 Srgb::convert::<Aces2065_1>(srgb),
1880 1e-4
1881 ));
1882 }
1883 }
1884
1885 #[test]
1886 fn absolute_conversion() {
1887 assert!(almost_equal::<AcesCg>(
1888 Srgb::convert_absolute::<AcesCg>([0.5, 0.2, 0.4]),
1889 [0.14628284, 0.04714393, 0.13361104],
1892 1e-4,
1893 ));
1894
1895 assert!(almost_equal::<XyzD65>(
1896 Srgb::convert_absolute::<XyzD50>([0.5, 0.2, 0.4]),
1897 Srgb::convert::<XyzD65>([0.5, 0.2, 0.4]),
1898 1e-4,
1899 ));
1900 }
1901
1902 #[test]
1903 fn chromatic_adaptation() {
1904 assert!(almost_equal::<Srgb>(
1905 XyzD50::convert_absolute::<Srgb>(Srgb::convert::<XyzD50>([0.5, 0.2, 0.4])),
1906 Srgb::chromatically_adapt([0.5, 0.2, 0.4], Chromaticity::D65, Chromaticity::D50),
1907 1e-4,
1908 ));
1909 }
1910
1911 #[test]
1915 fn implicit_vs_explicit_chromatic_adaptation() {
1916 fn test<Source: ColorSpace, Dest: ColorSpace>(src: [f32; 3]) {
1917 let convert = Source::convert::<Dest>(src);
1918 let convert_absolute_then_adapt = Dest::chromatically_adapt(
1919 Source::convert_absolute::<Dest>(src),
1920 Source::WHITE_POINT,
1921 Dest::WHITE_POINT,
1922 );
1923 let adapt_then_convert_absolute = Source::convert_absolute::<Dest>(
1924 Source::chromatically_adapt(src, Source::WHITE_POINT, Dest::WHITE_POINT),
1925 );
1926
1927 assert!(almost_equal::<LinearSrgb>(
1930 Dest::to_linear_srgb(convert),
1931 Dest::to_linear_srgb(convert_absolute_then_adapt),
1932 1e-4,
1933 ));
1934 assert!(almost_equal::<LinearSrgb>(
1935 Dest::to_linear_srgb(convert),
1936 Dest::to_linear_srgb(adapt_then_convert_absolute),
1937 1e-4,
1938 ));
1939 }
1940
1941 test::<Srgb, LinearSrgb>([0.5, 0.2, 0.4]);
1943 test::<Srgb, Lab>([0.5, 0.2, 0.4]);
1944 test::<Srgb, Lch>([0.5, 0.2, 0.4]);
1945 test::<Srgb, Hsl>([0.5, 0.2, 0.4]);
1946 test::<Srgb, Hwb>([0.5, 0.2, 0.4]);
1947 test::<Srgb, Oklab>([0.5, 0.2, 0.4]);
1948 test::<Srgb, Oklch>([0.5, 0.2, 0.4]);
1949 test::<Srgb, DisplayP3>([0.5, 0.2, 0.4]);
1950 test::<Srgb, A98Rgb>([0.5, 0.2, 0.4]);
1951 test::<Srgb, ProphotoRgb>([0.5, 0.2, 0.4]);
1952 test::<Srgb, Rec2020>([0.5, 0.2, 0.4]);
1953 test::<Srgb, Aces2065_1>([0.5, 0.2, 0.4]);
1954 test::<Srgb, AcesCg>([0.5, 0.2, 0.4]);
1955 test::<Srgb, XyzD50>([0.5, 0.2, 0.4]);
1956 test::<Srgb, XyzD65>([0.5, 0.2, 0.4]);
1957
1958 test::<AcesCg, Srgb>([0.5, 0.2, 0.4]);
1960 test::<AcesCg, LinearSrgb>([0.5, 0.2, 0.4]);
1961 test::<AcesCg, Lab>([0.5, 0.2, 0.4]);
1962 test::<AcesCg, Lch>([0.5, 0.2, 0.4]);
1963 test::<AcesCg, Hsl>([0.5, 0.2, 0.4]);
1964 test::<AcesCg, Hwb>([0.5, 0.2, 0.4]);
1965 test::<AcesCg, Oklab>([0.5, 0.2, 0.4]);
1966 test::<AcesCg, Oklch>([0.5, 0.2, 0.4]);
1967 test::<AcesCg, DisplayP3>([0.5, 0.2, 0.4]);
1968 test::<AcesCg, A98Rgb>([0.5, 0.2, 0.4]);
1969 test::<AcesCg, ProphotoRgb>([0.5, 0.2, 0.4]);
1970 test::<AcesCg, Rec2020>([0.5, 0.2, 0.4]);
1971 test::<AcesCg, Aces2065_1>([0.5, 0.2, 0.4]);
1972 test::<AcesCg, XyzD50>([0.5, 0.2, 0.4]);
1973 test::<AcesCg, XyzD65>([0.5, 0.2, 0.4]);
1974 }
1975}