1#[cfg(feature = "std")]
5extern crate std;
6
7use core::any::TypeId;
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 [12_831. / 3_959., -329. / 214., -1_974. / 3_959.],
1138 [
1139 -851_781. / 878_810.,
1140 1_648_619. / 878_810.,
1141 36_519. / 878_810.,
1142 ],
1143 [705. / 12_673., -2_585. / 12_673., 705. / 667.],
1144 ];
1145 matvecmul(&XYZ_TO_LINEAR_SRGB, src)
1146 }
1147
1148 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1149 const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1150 [506_752. / 1_228_815., 87_881. / 245_763., 12_673. / 70_218.],
1151 [87_098. / 409_605., 175_762. / 245_763., 12_673. / 175_545.],
1152 [
1153 7_918. / 409_605.,
1154 87_881. / 737_289.,
1155 100_1167. / 1_053_270.,
1156 ],
1157 ];
1158 matvecmul(&LINEAR_SRGB_TO_XYZ, src)
1159 }
1160
1161 fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
1162 [x, y, z]
1163 }
1164}
1165
1166impl From<XyzD65> for ColorSpaceTag {
1167 fn from(_: XyzD65) -> Self {
1168 Self::XyzD65
1169 }
1170}
1171
1172#[derive(Clone, Copy, Debug)]
1192pub struct Oklab;
1193
1194const OKLAB_LAB_TO_LMS: [[f32; 3]; 3] = [
1198 [1.0, 0.396_337_78, 0.215_803_76],
1199 [1.0, -0.105_561_346, -0.063_854_17],
1200 [1.0, -0.089_484_18, -1.291_485_5],
1201];
1202
1203const OKLAB_LMS_TO_SRGB: [[f32; 3]; 3] = [
1204 [4.076_741_7, -3.307_711_6, 0.230_969_94],
1205 [-1.268_438, 2.609_757_4, -0.341_319_38],
1206 [-0.004_196_086_3, -0.703_418_6, 1.707_614_7],
1207];
1208
1209const OKLAB_SRGB_TO_LMS: [[f32; 3]; 3] = [
1210 [0.412_221_46, 0.536_332_55, 0.051_445_995],
1211 [0.211_903_5, 0.680_699_5, 0.107_396_96],
1212 [0.088_302_46, 0.281_718_85, 0.629_978_7],
1213];
1214
1215const OKLAB_LMS_TO_LAB: [[f32; 3]; 3] = [
1216 [0.210_454_26, 0.793_617_8, -0.004_072_047],
1217 [1.977_998_5, -2.428_592_2, 0.450_593_7],
1218 [0.025_904_037, 0.782_771_77, -0.808_675_77],
1219];
1220
1221impl ColorSpace for Oklab {
1222 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklab);
1223
1224 const WHITE_COMPONENTS: [f32; 3] = [1., 0., 0.];
1225
1226 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1227 let lms = matvecmul(&OKLAB_LAB_TO_LMS, src).map(|x| x * x * x);
1228 matvecmul(&OKLAB_LMS_TO_SRGB, lms)
1229 }
1230
1231 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1232 let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
1233 matvecmul(&OKLAB_LMS_TO_LAB, lms)
1234 }
1235
1236 fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1237 [l, a * scale, b * scale]
1238 }
1239
1240 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1241 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1242 src
1243 } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklch>() {
1244 lab_to_lch(src)
1245 } else {
1246 let lin_rgb = Self::to_linear_srgb(src);
1247 TargetCS::from_linear_srgb(lin_rgb)
1248 }
1249 }
1250
1251 fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1252 [l.clamp(0., 1.), a, b]
1253 }
1254}
1255
1256impl From<Oklab> for ColorSpaceTag {
1257 fn from(_: Oklab) -> Self {
1258 Self::Oklab
1259 }
1260}
1261
1262fn lab_to_lch([l, a, b]: [f32; 3]) -> [f32; 3] {
1264 let mut h = b.atan2(a).to_degrees();
1265 if h < 0.0 {
1266 h += 360.0;
1267 }
1268 let c = b.hypot(a);
1269 [l, c, h]
1270}
1271
1272fn lch_to_lab([l, c, h]: [f32; 3]) -> [f32; 3] {
1274 let (sin, cos) = h.to_radians().sin_cos();
1275 let a = c * cos;
1276 let b = c * sin;
1277 [l, a, b]
1278}
1279
1280#[derive(Clone, Copy, Debug)]
1288pub struct Oklch;
1289
1290impl ColorSpace for Oklch {
1291 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklch);
1292
1293 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1294
1295 const WHITE_COMPONENTS: [f32; 3] = [1., 0., 90.];
1296
1297 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1298 lab_to_lch(Oklab::from_linear_srgb(src))
1299 }
1300
1301 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1302 Oklab::to_linear_srgb(lch_to_lab(src))
1303 }
1304
1305 fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1306 [l, c * scale, h]
1307 }
1308
1309 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1310 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1311 src
1312 } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklab>() {
1313 lch_to_lab(src)
1314 } else {
1315 let lin_rgb = Self::to_linear_srgb(src);
1316 TargetCS::from_linear_srgb(lin_rgb)
1317 }
1318 }
1319
1320 fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1321 [l.clamp(0., 1.), c.max(0.), h]
1322 }
1323}
1324
1325impl From<Oklch> for ColorSpaceTag {
1326 fn from(_: Oklch) -> Self {
1327 Self::Oklch
1328 }
1329}
1330
1331#[derive(Clone, Copy, Debug)]
1361pub struct Lab;
1362
1363const LAB_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1368 [0.452_211_65, 0.399_412_24, 0.148_376_09],
1369 [0.222_493_17, 0.716_887, 0.060_619_81],
1370 [0.016_875_342, 0.117_659_41, 0.865_465_2],
1371];
1372
1373const LAB_XYZ_TO_SRGB: [[f32; 3]; 3] = [
1375 [3.022_233_7, -1.617_386, -0.404_847_65],
1376 [-0.943_848_25, 1.916_254_4, 0.027_593_868],
1377 [0.069_386_27, -0.228_976_76, 1.159_590_5],
1378];
1379
1380const EPSILON: f32 = 216. / 24389.;
1381const KAPPA: f32 = 24389. / 27.;
1382
1383impl ColorSpace for Lab {
1384 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lab);
1385
1386 const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1387
1388 fn to_linear_srgb([l, a, b]: [f32; 3]) -> [f32; 3] {
1389 let f1 = l * (1. / 116.) + (16. / 116.);
1390 let f0 = a * (1. / 500.) + f1;
1391 let f2 = f1 - b * (1. / 200.);
1392 let xyz = [f0, f1, f2].map(|value| {
1393 const EPSILON_CBRT: f32 = 0.206_896_56;
1395 if value > EPSILON_CBRT {
1396 value * value * value
1397 } else {
1398 (116. / KAPPA) * value - (16. / KAPPA)
1399 }
1400 });
1401 matvecmul(&LAB_XYZ_TO_SRGB, xyz)
1402 }
1403
1404 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1405 let xyz = matvecmul(&LAB_SRGB_TO_XYZ, src);
1406 let f = xyz.map(|value| {
1407 if value > EPSILON {
1408 value.cbrt()
1409 } else {
1410 (KAPPA / 116.) * value + (16. / 116.)
1411 }
1412 });
1413 let l = 116. * f[1] - 16.;
1414 let a = 500. * (f[0] - f[1]);
1415 let b = 200. * (f[1] - f[2]);
1416 [l, a, b]
1417 }
1418
1419 fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1420 [l, a * scale, b * scale]
1421 }
1422
1423 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1424 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1425 src
1426 } else if TypeId::of::<TargetCS>() == TypeId::of::<Lch>() {
1427 lab_to_lch(src)
1428 } else {
1429 let lin_rgb = Self::to_linear_srgb(src);
1430 TargetCS::from_linear_srgb(lin_rgb)
1431 }
1432 }
1433
1434 fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1435 [l.clamp(0., 100.), a, b]
1436 }
1437}
1438
1439impl From<Lab> for ColorSpaceTag {
1440 fn from(_: Lab) -> Self {
1441 Self::Lab
1442 }
1443}
1444
1445#[derive(Clone, Copy, Debug)]
1455pub struct Lch;
1456
1457impl ColorSpace for Lch {
1458 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lch);
1459
1460 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1461
1462 const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1463
1464 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1465 lab_to_lch(Lab::from_linear_srgb(src))
1466 }
1467
1468 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1469 Lab::to_linear_srgb(lch_to_lab(src))
1470 }
1471
1472 fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1473 [l, c * scale, h]
1474 }
1475
1476 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1477 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1478 src
1479 } else if TypeId::of::<TargetCS>() == TypeId::of::<Lab>() {
1480 lch_to_lab(src)
1481 } else {
1482 let lin_rgb = Self::to_linear_srgb(src);
1483 TargetCS::from_linear_srgb(lin_rgb)
1484 }
1485 }
1486
1487 fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1488 [l.clamp(0., 100.), c.max(0.), h]
1489 }
1490}
1491
1492impl From<Lch> for ColorSpaceTag {
1493 fn from(_: Lch) -> Self {
1494 Self::Lch
1495 }
1496}
1497
1498#[derive(Clone, Copy, Debug)]
1513pub struct Hsl;
1514
1515fn hsl_to_rgb([h, s, l]: [f32; 3]) -> [f32; 3] {
1519 let sat = s * 0.01;
1521 let light = l * 0.01;
1522 let a = sat * light.min(1.0 - light);
1523 [0.0, 8.0, 4.0].map(|n| {
1524 let x = n + h * (1.0 / 30.0);
1525 let k = x - 12.0 * (x * (1.0 / 12.0)).floor();
1526 light - a * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0)
1527 })
1528}
1529
1530fn rgb_to_hsl([r, g, b]: [f32; 3], hue_hack: bool) -> [f32; 3] {
1537 let max = r.max(g).max(b);
1538 let min = r.min(g).min(b);
1539 let mut hue = 0.0;
1540 let mut sat = 0.0;
1541 let light = 0.5 * (min + max);
1542 let d = max - min;
1543
1544 const EPSILON: f32 = 1e-6;
1545 if d > EPSILON {
1546 let denom = light.min(1.0 - light);
1547 if denom.abs() > EPSILON {
1548 sat = (max - light) / denom;
1549 }
1550 hue = if max == r {
1551 (g - b) / d
1552 } else if max == g {
1553 (b - r) / d + 2.0
1554 } else {
1555 (r - g) / d + 4.0
1557 };
1558 hue *= 60.0;
1559 if hue_hack && sat < 0.0 {
1561 hue += 180.0;
1562 sat = sat.abs();
1563 }
1564 hue -= 360. * (hue * (1.0 / 360.0)).floor();
1565 }
1566 [hue, sat * 100.0, light * 100.0]
1567}
1568
1569impl ColorSpace for Hsl {
1570 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hsl);
1571
1572 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1573
1574 const WHITE_COMPONENTS: [f32; 3] = [0., 0., 100.];
1575
1576 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1577 let rgb = Srgb::from_linear_srgb(src);
1578 rgb_to_hsl(rgb, true)
1579 }
1580
1581 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1582 let rgb = hsl_to_rgb(src);
1583 Srgb::to_linear_srgb(rgb)
1584 }
1585
1586 fn scale_chroma([h, s, l]: [f32; 3], scale: f32) -> [f32; 3] {
1587 [h, s * scale, l]
1588 }
1589
1590 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1591 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1592 src
1593 } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1594 hsl_to_rgb(src)
1595 } else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
1596 rgb_to_hwb(hsl_to_rgb(src))
1597 } else {
1598 let lin_rgb = Self::to_linear_srgb(src);
1599 TargetCS::from_linear_srgb(lin_rgb)
1600 }
1601 }
1602
1603 fn clip([h, s, l]: [f32; 3]) -> [f32; 3] {
1604 [h, s.max(0.), l.clamp(0., 100.)]
1605 }
1606}
1607
1608impl From<Hsl> for ColorSpaceTag {
1609 fn from(_: Hsl) -> Self {
1610 Self::Hsl
1611 }
1612}
1613
1614#[derive(Clone, Copy, Debug)]
1635pub struct Hwb;
1636
1637fn hwb_to_rgb([h, w, b]: [f32; 3]) -> [f32; 3] {
1641 let white = w * 0.01;
1642 let black = b * 0.01;
1643 if white + black >= 1.0 {
1644 let gray = white / (white + black);
1645 [gray, gray, gray]
1646 } else {
1647 let rgb = hsl_to_rgb([h, 100., 50.]);
1648 rgb.map(|x| white + x * (1.0 - white - black))
1649 }
1650}
1651
1652fn rgb_to_hwb([r, g, b]: [f32; 3]) -> [f32; 3] {
1656 let hsl = rgb_to_hsl([r, g, b], false);
1657 let white = r.min(g).min(b);
1658 let black = 1.0 - r.max(g).max(b);
1659 [hsl[0], white * 100., black * 100.]
1660}
1661
1662impl ColorSpace for Hwb {
1663 const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hwb);
1664
1665 const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1666
1667 const WHITE_COMPONENTS: [f32; 3] = [0., 100., 0.];
1668
1669 fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1670 let rgb = Srgb::from_linear_srgb(src);
1671 rgb_to_hwb(rgb)
1672 }
1673
1674 fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1675 let rgb = hwb_to_rgb(src);
1676 Srgb::to_linear_srgb(rgb)
1677 }
1678
1679 fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1680 if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1681 src
1682 } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1683 hwb_to_rgb(src)
1684 } else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
1685 rgb_to_hsl(hwb_to_rgb(src), true)
1686 } else {
1687 let lin_rgb = Self::to_linear_srgb(src);
1688 TargetCS::from_linear_srgb(lin_rgb)
1689 }
1690 }
1691
1692 fn clip([h, w, b]: [f32; 3]) -> [f32; 3] {
1693 [h, w.clamp(0., 100.), b.clamp(0., 100.)]
1694 }
1695}
1696
1697impl From<Hwb> for ColorSpaceTag {
1698 fn from(_: Hwb) -> Self {
1699 Self::Hwb
1700 }
1701}
1702
1703#[cfg(test)]
1704mod tests {
1705 extern crate alloc;
1706
1707 use crate::{
1708 A98Rgb, Aces2065_1, AcesCg, Chromaticity, ColorSpace, DisplayP3, Hsl, Hwb, Lab, Lch,
1709 LinearSrgb, Oklab, Oklch, OpaqueColor, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65,
1710 };
1711 use alloc::vec::Vec;
1712
1713 #[must_use]
1714 fn almost_equal<CS: ColorSpace>(col1: [f32; 3], col2: [f32; 3], absolute_epsilon: f32) -> bool {
1715 OpaqueColor::<CS>::new(col1).difference(OpaqueColor::new(col2)) <= absolute_epsilon
1716 }
1717
1718 fn magnitude(col: [f32; 3]) -> f32 {
1720 col[0].abs().max(col[1].abs()).max(col[2].abs())
1721 }
1722
1723 #[test]
1724 fn roundtrip() {
1725 fn test_roundtrips<Source: ColorSpace, Dest: ColorSpace>(colors: &[[f32; 3]]) {
1726 const RELATIVE_EPSILON: f32 = f32::EPSILON * 16.;
1735
1736 for color in colors {
1737 let intermediate = Source::convert::<Dest>(*color);
1738 let roundtripped = Dest::convert::<Source>(intermediate);
1739
1740 let linsrgb_color = Source::to_linear_srgb(*color);
1743 let linsrgb_roundtripped = Source::to_linear_srgb(roundtripped);
1744
1745 let absolute_epsilon = magnitude(linsrgb_color).max(1.) * RELATIVE_EPSILON;
1749 assert!(almost_equal::<LinearSrgb>(
1750 linsrgb_color,
1751 linsrgb_roundtripped,
1752 absolute_epsilon,
1753 ));
1754 }
1755 }
1756
1757 let rectangular_values = {
1759 let components = [
1760 0., 1., -1., 0.5, 1234., -1234., 1.000_001, 0.000_001, -0.000_001,
1761 ];
1762 let mut values = Vec::new();
1763 for c0 in components {
1764 for c1 in components {
1765 for c2 in components {
1766 values.push([c0, c1, c2]);
1767 }
1768 }
1769 }
1770 values
1771 };
1772
1773 test_roundtrips::<LinearSrgb, Srgb>(&rectangular_values);
1774 test_roundtrips::<DisplayP3, Srgb>(&rectangular_values);
1775 test_roundtrips::<A98Rgb, Srgb>(&rectangular_values);
1776 test_roundtrips::<ProphotoRgb, Srgb>(&rectangular_values);
1777 test_roundtrips::<Rec2020, Srgb>(&rectangular_values);
1778 test_roundtrips::<Aces2065_1, Srgb>(&rectangular_values);
1779 test_roundtrips::<AcesCg, Srgb>(&rectangular_values);
1780 test_roundtrips::<XyzD50, Srgb>(&rectangular_values);
1781 test_roundtrips::<XyzD65, Srgb>(&rectangular_values);
1782
1783 test_roundtrips::<Oklab, Srgb>(&[
1784 [0., 0., 0.],
1785 [1., 0., 0.],
1786 [0.2, 0.2, -0.1],
1787 [2.0, 0., -0.4],
1788 ]);
1789 }
1790
1791 #[test]
1792 fn white_components() {
1793 fn check_white<CS: ColorSpace>() {
1794 assert!(almost_equal::<Srgb>(
1795 Srgb::WHITE_COMPONENTS,
1796 CS::convert::<Srgb>(CS::WHITE_COMPONENTS),
1797 1e-4,
1798 ));
1799 assert!(almost_equal::<CS>(
1800 CS::WHITE_COMPONENTS,
1801 Srgb::convert::<CS>(Srgb::WHITE_COMPONENTS),
1802 1e-4,
1803 ));
1804 }
1805
1806 check_white::<A98Rgb>();
1807 check_white::<DisplayP3>();
1808 check_white::<Hsl>();
1809 check_white::<Hwb>();
1810 check_white::<Lab>();
1811 check_white::<Lch>();
1812 check_white::<LinearSrgb>();
1813 check_white::<Oklab>();
1814 check_white::<Oklch>();
1815 check_white::<ProphotoRgb>();
1816 check_white::<Rec2020>();
1817 check_white::<Aces2065_1>();
1818 check_white::<AcesCg>();
1819 check_white::<XyzD50>();
1820 check_white::<XyzD65>();
1821 }
1822
1823 #[test]
1824 fn a98rgb_srgb() {
1825 for (srgb, a98) in [
1826 ([0.1, 0.2, 0.3], [0.155_114, 0.212_317, 0.301_498]),
1827 ([0., 1., 0.], [0.564_972, 1., 0.234_424]),
1828 ] {
1829 assert!(almost_equal::<Srgb>(
1830 srgb,
1831 A98Rgb::convert::<Srgb>(a98),
1832 1e-4
1833 ));
1834 assert!(almost_equal::<A98Rgb>(
1835 a98,
1836 Srgb::convert::<A98Rgb>(srgb),
1837 1e-4
1838 ));
1839 }
1840 }
1841
1842 #[test]
1843 fn prophotorgb_srgb() {
1844 for (srgb, prophoto) in [
1845 ([0.1, 0.2, 0.3], [0.133136, 0.147659, 0.223581]),
1846 ([0., 1., 0.], [0.540282, 0.927599, 0.304566]),
1847 ] {
1848 assert!(almost_equal::<Srgb>(
1849 srgb,
1850 ProphotoRgb::convert::<Srgb>(prophoto),
1851 1e-4
1852 ));
1853 assert!(almost_equal::<ProphotoRgb>(
1854 prophoto,
1855 Srgb::convert::<ProphotoRgb>(srgb),
1856 1e-4
1857 ));
1858 }
1859 }
1860
1861 #[test]
1862 fn rec2020_srgb() {
1863 for (srgb, rec2020) in [
1864 ([0.1, 0.2, 0.3], [0.091284, 0.134169, 0.230056]),
1865 ([0.05, 0.1, 0.15], [0.029785, 0.043700, 0.083264]),
1866 ([0., 1., 0.], [0.567542, 0.959279, 0.268969]),
1867 ] {
1868 assert!(almost_equal::<Srgb>(
1869 srgb,
1870 Rec2020::convert::<Srgb>(rec2020),
1871 1e-4
1872 ));
1873 assert!(almost_equal::<Rec2020>(
1874 rec2020,
1875 Srgb::convert::<Rec2020>(srgb),
1876 1e-4
1877 ));
1878 }
1879 }
1880
1881 #[test]
1882 fn aces2065_1_srgb() {
1883 for (srgb, aces2065_1) in [
1884 ([0.6, 0.5, 0.4], [0.245_59, 0.215_57, 0.145_18]),
1885 ([0.0, 0.5, 1.0], [0.259_35, 0.270_89, 0.894_79]),
1886 ] {
1887 assert!(almost_equal::<Srgb>(
1888 srgb,
1889 Aces2065_1::convert::<Srgb>(aces2065_1),
1890 1e-4
1891 ));
1892 assert!(almost_equal::<Aces2065_1>(
1893 aces2065_1,
1894 Srgb::convert::<Aces2065_1>(srgb),
1895 1e-4
1896 ));
1897 }
1898 }
1899
1900 #[test]
1901 fn absolute_conversion() {
1902 assert!(almost_equal::<AcesCg>(
1903 Srgb::convert_absolute::<AcesCg>([0.5, 0.2, 0.4]),
1904 [0.14628284, 0.04714393, 0.13361104],
1907 1e-4,
1908 ));
1909
1910 assert!(almost_equal::<XyzD65>(
1911 Srgb::convert_absolute::<XyzD50>([0.5, 0.2, 0.4]),
1912 Srgb::convert::<XyzD65>([0.5, 0.2, 0.4]),
1913 1e-4,
1914 ));
1915 }
1916
1917 #[test]
1918 fn chromatic_adaptation() {
1919 assert!(almost_equal::<Srgb>(
1920 XyzD50::convert_absolute::<Srgb>(Srgb::convert::<XyzD50>([0.5, 0.2, 0.4])),
1921 Srgb::chromatically_adapt([0.5, 0.2, 0.4], Chromaticity::D65, Chromaticity::D50),
1922 1e-4,
1923 ));
1924 }
1925
1926 #[test]
1930 fn implicit_vs_explicit_chromatic_adaptation() {
1931 fn test<Source: ColorSpace, Dest: ColorSpace>(src: [f32; 3]) {
1932 let convert = Source::convert::<Dest>(src);
1933 let convert_absolute_then_adapt = Dest::chromatically_adapt(
1934 Source::convert_absolute::<Dest>(src),
1935 Source::WHITE_POINT,
1936 Dest::WHITE_POINT,
1937 );
1938 let adapt_then_convert_absolute = Source::convert_absolute::<Dest>(
1939 Source::chromatically_adapt(src, Source::WHITE_POINT, Dest::WHITE_POINT),
1940 );
1941
1942 assert!(almost_equal::<LinearSrgb>(
1945 Dest::to_linear_srgb(convert),
1946 Dest::to_linear_srgb(convert_absolute_then_adapt),
1947 1e-4,
1948 ));
1949 assert!(almost_equal::<LinearSrgb>(
1950 Dest::to_linear_srgb(convert),
1951 Dest::to_linear_srgb(adapt_then_convert_absolute),
1952 1e-4,
1953 ));
1954 }
1955
1956 test::<Srgb, LinearSrgb>([0.5, 0.2, 0.4]);
1958 test::<Srgb, Lab>([0.5, 0.2, 0.4]);
1959 test::<Srgb, Lch>([0.5, 0.2, 0.4]);
1960 test::<Srgb, Hsl>([0.5, 0.2, 0.4]);
1961 test::<Srgb, Hwb>([0.5, 0.2, 0.4]);
1962 test::<Srgb, Oklab>([0.5, 0.2, 0.4]);
1963 test::<Srgb, Oklch>([0.5, 0.2, 0.4]);
1964 test::<Srgb, DisplayP3>([0.5, 0.2, 0.4]);
1965 test::<Srgb, A98Rgb>([0.5, 0.2, 0.4]);
1966 test::<Srgb, ProphotoRgb>([0.5, 0.2, 0.4]);
1967 test::<Srgb, Rec2020>([0.5, 0.2, 0.4]);
1968 test::<Srgb, Aces2065_1>([0.5, 0.2, 0.4]);
1969 test::<Srgb, AcesCg>([0.5, 0.2, 0.4]);
1970 test::<Srgb, XyzD50>([0.5, 0.2, 0.4]);
1971 test::<Srgb, XyzD65>([0.5, 0.2, 0.4]);
1972
1973 test::<AcesCg, Srgb>([0.5, 0.2, 0.4]);
1975 test::<AcesCg, LinearSrgb>([0.5, 0.2, 0.4]);
1976 test::<AcesCg, Lab>([0.5, 0.2, 0.4]);
1977 test::<AcesCg, Lch>([0.5, 0.2, 0.4]);
1978 test::<AcesCg, Hsl>([0.5, 0.2, 0.4]);
1979 test::<AcesCg, Hwb>([0.5, 0.2, 0.4]);
1980 test::<AcesCg, Oklab>([0.5, 0.2, 0.4]);
1981 test::<AcesCg, Oklch>([0.5, 0.2, 0.4]);
1982 test::<AcesCg, DisplayP3>([0.5, 0.2, 0.4]);
1983 test::<AcesCg, A98Rgb>([0.5, 0.2, 0.4]);
1984 test::<AcesCg, ProphotoRgb>([0.5, 0.2, 0.4]);
1985 test::<AcesCg, Rec2020>([0.5, 0.2, 0.4]);
1986 test::<AcesCg, Aces2065_1>([0.5, 0.2, 0.4]);
1987 test::<AcesCg, XyzD50>([0.5, 0.2, 0.4]);
1988 test::<AcesCg, XyzD65>([0.5, 0.2, 0.4]);
1989 }
1990}