1use crate::cam::{
2 cam16_forward, cam16_ucs_forward, cam16_ucs_inverse, cam_forward, cam_inverse, cam_ucs_forward,
3 ciecam02_forward, ciecam02_ucs_forward, ciecam02_ucs_inverse, CamAppearance, CamUcsAppearance,
4 CamUcsType, CamViewingConditions as ModelCamViewingConditions,
5};
6use crate::error::{LuxError, LuxResult};
7use crate::spectrum::Spectrum;
8use std::fmt::{Display, Formatter};
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Observer {
13 Cie1931_2,
14 Cie1964_10,
15 Cie2006_2,
16 Cie2006_10,
17 Cie2015_2,
18 Cie2015_10,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct TristimulusObserver {
23 pub wavelengths: Vec<f64>,
24 pub x_bar: Vec<f64>,
25 pub y_bar: Vec<f64>,
26 pub z_bar: Vec<f64>,
27 pub k: f64,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub struct MesopicLuminousEfficiency {
32 pub curves: Spectrum,
33 pub k_mesopic: Vec<f64>,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct Tristimulus {
38 values: Vec<[f64; 3]>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum DeltaEFormula {
43 Cie76,
44 Ciede2000,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum CatTransform {
49 Bradford,
50 Cat02,
51 Cat16,
52 Sharp,
53 Bianco,
54 Cmc,
55 Kries,
56 Judd1945,
57 Judd1945Cie016,
58 Judd1935,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum CatSurround {
63 Average,
64 Dim,
65 Dark,
66 Display,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum CatMode {
71 OneStep,
72 SourceToBaseline,
73 BaselineToTarget,
74 TwoStep,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct CatViewingConditions {
79 pub surround: CatSurround,
80 pub adapting_luminance: f64,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct CatContext {
85 pub source_white: [f64; 3],
86 pub target_white: [f64; 3],
87 pub baseline_white: Option<[f64; 3]>,
88 pub transform: CatTransform,
89 pub mode: CatMode,
90 pub source_conditions: CatViewingConditions,
91 pub target_conditions: CatViewingConditions,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct CatConditionPair {
96 pub source: CatViewingConditions,
97 pub target: CatViewingConditions,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq)]
101pub struct CatAdapter {
102 matrix: Matrix3,
103}
104
105pub type Matrix3 = [[f64; 3]; 3];
106
107const EPSILON: f64 = 1e-15;
108const LAB_LINEAR_THRESHOLD: f64 = (24.0 / 116.0) * (24.0 / 116.0) * (24.0 / 116.0);
109const LAB_LINEAR_SCALE: f64 = 841.0 / 108.0;
110const LAB_INVERSE_LINEAR_SCALE: f64 = 108.0 / 841.0;
111const LUV_LINEAR_THRESHOLD: f64 = (6.0 / 29.0) * (6.0 / 29.0) * (6.0 / 29.0);
112const LUV_LINEAR_SCALE: f64 = (29.0 / 3.0) * (29.0 / 3.0) * (29.0 / 3.0);
113const SRGB_XYZ_TO_RGB: Matrix3 = [
114 [3.2404542, -1.5371385, -0.4985314],
115 [-0.9692660, 1.8760108, 0.0415560],
116 [0.0556434, -0.2040259, 1.0572252],
117];
118const SRGB_RGB_TO_XYZ: Matrix3 = [
119 [0.4124564, 0.3575761, 0.1804375],
120 [0.2126729, 0.7151522, 0.0721750],
121 [0.0193339, 0.1191920, 0.9503041],
122];
123const XYZ_TO_LMS_CIE1931_2: Matrix3 = [
124 [0.38971, 0.68898, -0.07868],
125 [-0.22981, 1.1834, 0.04641],
126 [0.0, 0.0, 1.0],
127];
128const XYZ_TO_LMS_CIE1964_10: Matrix3 = [
129 [
130 0.217_010_449_691_388_16,
131 0.835_733_670_117_584_4,
132 -0.043_510_597_212_556_935,
133 ],
134 [
135 -0.429_979_507_573_619_8,
136 1.203_889_456_462_98,
137 0.086_210_895_329_211_28,
138 ],
139 [0.0, 0.0, 0.465_792_338_736_113],
140];
141const XYZ_TO_LMS_CIE2006_2: Matrix3 = [
142 [
143 0.444_040_252_514_163_8,
144 0.263_446_288_529_080_84,
145 -0.025_183_902_796_622_027,
146 ],
147 [0.877_340_233_784_257_2, 1.909_499_428_404_36, 0.0],
148 [0.0, 0.0, 0.516_835_881_576_572_3],
149];
150const XYZ_TO_LMS_CIE2006_10: Matrix3 = [
151 [
152 0.434_511_873_864_139_4,
153 0.239_073_351_151_345_67,
154 -0.087_584_241_936_804_45,
155 ],
156 [0.860_328_562_152_002_9, 1.858_437_464_814_796_6, 0.0],
157 [0.0, 0.0, 0.465_793_720_749_544_95],
158];
159
160#[derive(Debug, Clone, Copy)]
161struct ObserverSpec {
162 name: &'static str,
163 data: &'static str,
164 k: f64,
165 xyz_to_lms: Matrix3,
166}
167
168impl Observer {
169 pub const fn all() -> &'static [Self] {
170 &[
171 Self::Cie1931_2,
172 Self::Cie1964_10,
173 Self::Cie2006_2,
174 Self::Cie2006_10,
175 Self::Cie2015_2,
176 Self::Cie2015_10,
177 ]
178 }
179
180 pub fn name(self) -> &'static str {
181 self.spec().name
182 }
183
184 pub fn from_name(name: &str) -> LuxResult<Self> {
185 canonicalize_observer_name(name)
186 .and_then(observer_from_canonical_name)
187 .ok_or(LuxError::UnsupportedObserver("observer name"))
188 }
189
190 fn spec(self) -> ObserverSpec {
191 match self {
192 Self::Cie1931_2 => ObserverSpec {
193 name: "1931_2",
194 data: include_str!("../data/cmfs/ciexyz_1931_2.dat"),
195 k: 683.002,
196 xyz_to_lms: XYZ_TO_LMS_CIE1931_2,
197 },
198 Self::Cie1964_10 => ObserverSpec {
199 name: "1964_10",
200 data: include_str!("../data/cmfs/ciexyz_1964_10.dat"),
201 k: 683.599,
202 xyz_to_lms: XYZ_TO_LMS_CIE1964_10,
203 },
204 Self::Cie2006_2 => ObserverSpec {
205 name: "2006_2",
206 data: include_str!("../data/cmfs/ciexyz_2006_2.dat"),
207 k: 683.358,
208 xyz_to_lms: XYZ_TO_LMS_CIE2006_2,
209 },
210 Self::Cie2006_10 => ObserverSpec {
211 name: "2006_10",
212 data: include_str!("../data/cmfs/ciexyz_2006_10.dat"),
213 k: 683.144,
214 xyz_to_lms: XYZ_TO_LMS_CIE2006_10,
215 },
216 Self::Cie2015_2 => ObserverSpec {
217 name: "2015_2",
218 data: include_str!("../data/cmfs/ciexyz_2006_2.dat"),
220 k: 683.358,
221 xyz_to_lms: XYZ_TO_LMS_CIE2006_2,
222 },
223 Self::Cie2015_10 => ObserverSpec {
224 name: "2015_10",
225 data: include_str!("../data/cmfs/ciexyz_2006_10.dat"),
227 k: 683.144,
228 xyz_to_lms: XYZ_TO_LMS_CIE2006_10,
229 },
230 }
231 }
232
233 pub fn standard(self) -> LuxResult<TristimulusObserver> {
234 let spec = self.spec();
235 TristimulusObserver::from_csv(spec.data, spec.k)
236 }
237
238 pub fn xyzbar(self) -> LuxResult<Spectrum> {
239 self.standard()?.xyz_spectra()
240 }
241
242 pub fn xyzbar_linear(self, target_wavelengths: &[f64]) -> LuxResult<Spectrum> {
243 self.xyzbar()?.cie_interp_linear(target_wavelengths, false)
244 }
245
246 pub fn vlbar(self) -> LuxResult<(Spectrum, f64)> {
247 let observer = self.standard()?;
248 Ok((observer.vl_spectrum()?, observer.k))
249 }
250
251 pub fn vlbar_linear(self, target_wavelengths: &[f64]) -> LuxResult<(Spectrum, f64)> {
252 let (vl, k) = self.vlbar()?;
253 Ok((vl.cie_interp_linear(target_wavelengths, false)?, k))
254 }
255
256 pub fn xyz_to_lms_matrix(self) -> LuxResult<Matrix3> {
257 Ok(self.spec().xyz_to_lms)
258 }
259}
260
261impl Display for Observer {
262 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
263 f.write_str(self.name())
264 }
265}
266
267impl FromStr for Observer {
268 type Err = LuxError;
269
270 fn from_str(s: &str) -> Result<Self, Self::Err> {
271 Self::from_name(s)
272 }
273}
274
275fn canonicalize_observer_name(name: &str) -> Option<&'static str> {
276 let trimmed = name.trim();
277 if trimmed.is_empty() {
278 return None;
279 }
280
281 let normalized = trimmed
282 .to_ascii_lowercase()
283 .replace([' ', '\t', '\n', '\r', '-', '_'], "");
284 let normalized = normalized.strip_prefix("cie").unwrap_or(&normalized);
285
286 match normalized {
287 "19312" | "1931" => Some("1931_2"),
288 "196410" | "1964" => Some("1964_10"),
289 "20062" => Some("2006_2"),
290 "200610" => Some("2006_10"),
291 "20152" => Some("2015_2"),
292 "201510" => Some("2015_10"),
293 _ => None,
294 }
295}
296
297fn observer_from_canonical_name(name: &'static str) -> Option<Observer> {
298 match name {
299 "1931_2" => Some(Observer::Cie1931_2),
300 "1964_10" => Some(Observer::Cie1964_10),
301 "2006_2" => Some(Observer::Cie2006_2),
302 "2006_10" => Some(Observer::Cie2006_10),
303 "2015_2" => Some(Observer::Cie2015_2),
304 "2015_10" => Some(Observer::Cie2015_10),
305 _ => None,
306 }
307}
308
309impl Tristimulus {
310 pub fn new(values: Vec<[f64; 3]>) -> Self {
311 Self { values }
312 }
313
314 pub fn from_single(value: [f64; 3]) -> Self {
315 Self::new(vec![value])
316 }
317
318 pub fn values(&self) -> &[[f64; 3]] {
319 &self.values
320 }
321
322 pub fn iter(&self) -> impl Iterator<Item = [f64; 3]> + '_ {
323 self.values.iter().copied()
324 }
325
326 pub fn len(&self) -> usize {
327 self.values.len()
328 }
329
330 pub fn is_empty(&self) -> bool {
331 self.values.is_empty()
332 }
333
334 pub fn into_vec(self) -> Vec<[f64; 3]> {
335 self.values
336 }
337
338 pub fn xyz_to_yxy(&self) -> Self {
339 Self::new(
340 self.values
341 .iter()
342 .copied()
343 .map(xyz_to_yxy)
344 .collect::<Vec<_>>(),
345 )
346 }
347
348 pub fn yxy_to_xyz(&self) -> Self {
349 Self::new(
350 self.values
351 .iter()
352 .copied()
353 .map(yxy_to_xyz)
354 .collect::<Vec<_>>(),
355 )
356 }
357
358 pub fn xyz_to_yuv(&self) -> Self {
359 Self::new(
360 self.values
361 .iter()
362 .copied()
363 .map(xyz_to_yuv)
364 .collect::<Vec<_>>(),
365 )
366 }
367
368 pub fn yuv_to_xyz(&self) -> Self {
369 Self::new(
370 self.values
371 .iter()
372 .copied()
373 .map(yuv_to_xyz)
374 .collect::<Vec<_>>(),
375 )
376 }
377
378 pub fn xyz_to_lab(&self, white_point: [f64; 3]) -> Self {
379 Self::new(
380 self.values
381 .iter()
382 .copied()
383 .map(|value| xyz_to_lab(value, white_point))
384 .collect::<Vec<_>>(),
385 )
386 }
387
388 pub fn lab_to_xyz(&self, white_point: [f64; 3]) -> Self {
389 Self::new(
390 self.values
391 .iter()
392 .copied()
393 .map(|value| lab_to_xyz(value, white_point))
394 .collect::<Vec<_>>(),
395 )
396 }
397
398 pub fn xyz_to_oklab(&self) -> Self {
399 Self::new(
400 self.values
401 .iter()
402 .copied()
403 .map(xyz_to_oklab)
404 .collect::<Vec<_>>(),
405 )
406 }
407
408 pub fn oklab_to_xyz(&self) -> Self {
409 Self::new(
410 self.values
411 .iter()
412 .copied()
413 .map(oklab_to_xyz)
414 .collect::<Vec<_>>(),
415 )
416 }
417
418 pub fn oklab_to_oklch(&self) -> Self {
419 Self::new(
420 self.values
421 .iter()
422 .copied()
423 .map(oklab_to_oklch)
424 .collect::<Vec<_>>(),
425 )
426 }
427
428 pub fn oklch_to_oklab(&self) -> Self {
429 Self::new(
430 self.values
431 .iter()
432 .copied()
433 .map(oklch_to_oklab)
434 .collect::<Vec<_>>(),
435 )
436 }
437
438 pub fn xyz_to_oklch(&self) -> Self {
439 Self::new(
440 self.values
441 .iter()
442 .copied()
443 .map(xyz_to_oklch)
444 .collect::<Vec<_>>(),
445 )
446 }
447
448 pub fn oklch_to_xyz(&self) -> Self {
449 Self::new(
450 self.values
451 .iter()
452 .copied()
453 .map(oklch_to_xyz)
454 .collect::<Vec<_>>(),
455 )
456 }
457
458 pub fn xyz_to_luv(&self, white_point: [f64; 3]) -> Self {
459 Self::new(
460 self.values
461 .iter()
462 .copied()
463 .map(|value| xyz_to_luv(value, white_point))
464 .collect::<Vec<_>>(),
465 )
466 }
467
468 pub fn luv_to_xyz(&self, white_point: [f64; 3]) -> Self {
469 Self::new(
470 self.values
471 .iter()
472 .copied()
473 .map(|value| luv_to_xyz(value, white_point))
474 .collect::<Vec<_>>(),
475 )
476 }
477
478 pub fn xyz_to_lms(&self, observer: Observer) -> LuxResult<Self> {
479 Ok(Self::new(
480 self.values
481 .iter()
482 .copied()
483 .map(|value| xyz_to_lms(value, observer))
484 .collect::<LuxResult<Vec<_>>>()?,
485 ))
486 }
487
488 pub fn lms_to_xyz(&self, observer: Observer) -> LuxResult<Self> {
489 Ok(Self::new(
490 self.values
491 .iter()
492 .copied()
493 .map(|value| lms_to_xyz(value, observer))
494 .collect::<LuxResult<Vec<_>>>()?,
495 ))
496 }
497
498 pub fn xyz_to_srgb(&self, gamma: f64, offset: f64, use_linear_part: bool) -> Self {
499 Self::new(
500 self.values
501 .iter()
502 .copied()
503 .map(|value| xyz_to_srgb(value, gamma, offset, use_linear_part))
504 .collect::<Vec<_>>(),
505 )
506 }
507
508 pub fn srgb_to_xyz(&self, gamma: f64, offset: f64, use_linear_part: bool) -> Self {
509 Self::new(
510 self.values
511 .iter()
512 .copied()
513 .map(|value| srgb_to_xyz(value, gamma, offset, use_linear_part))
514 .collect::<Vec<_>>(),
515 )
516 }
517
518 pub fn rgb_to_xyz(&self, space: RgbColorSpace) -> Self {
520 Self::new(
521 self.values
522 .iter()
523 .copied()
524 .map(|value| rgb_to_xyz(value, space))
525 .collect::<Vec<_>>(),
526 )
527 }
528
529 pub fn xyz_to_rgb(&self, space: RgbColorSpace) -> Self {
531 Self::new(
532 self.values
533 .iter()
534 .copied()
535 .map(|value| xyz_to_rgb(value, space))
536 .collect::<Vec<_>>(),
537 )
538 }
539
540 pub fn cat_apply(
541 &self,
542 source_white: [f64; 3],
543 target_white: [f64; 3],
544 transform: CatTransform,
545 degree_of_adaptation: f64,
546 ) -> LuxResult<Self> {
547 self.cat_apply_adapter(CatAdapter::from_degree(
548 source_white,
549 target_white,
550 transform,
551 degree_of_adaptation,
552 )?)
553 }
554
555 pub fn cat_apply_mode(
556 &self,
557 source_white: [f64; 3],
558 target_white: [f64; 3],
559 baseline_white: Option<[f64; 3]>,
560 transform: CatTransform,
561 mode: CatMode,
562 degrees_of_adaptation: [f64; 2],
563 ) -> LuxResult<Self> {
564 self.cat_apply_adapter(CatAdapter::from_mode(
565 source_white,
566 target_white,
567 baseline_white,
568 transform,
569 mode,
570 degrees_of_adaptation,
571 )?)
572 }
573
574 pub fn cat_apply_with_conditions(
575 &self,
576 source_white: [f64; 3],
577 target_white: [f64; 3],
578 transform: CatTransform,
579 surround: CatSurround,
580 adapting_luminance: f64,
581 ) -> LuxResult<Self> {
582 self.cat_apply_adapter(CatAdapter::from_degree(
583 source_white,
584 target_white,
585 transform,
586 cat_degree_of_adaptation(surround, adapting_luminance)?,
587 )?)
588 }
589
590 pub fn cat_apply_mode_with_conditions(
591 &self,
592 source_white: [f64; 3],
593 target_white: [f64; 3],
594 baseline_white: Option<[f64; 3]>,
595 transform: CatTransform,
596 mode: CatMode,
597 conditions: CatConditionPair,
598 ) -> LuxResult<Self> {
599 self.cat_apply_adapter(CatAdapter::from_conditions(
600 source_white,
601 target_white,
602 baseline_white,
603 transform,
604 mode,
605 conditions.source,
606 conditions.target,
607 )?)
608 }
609
610 pub fn cat_apply_context(&self, context: CatContext) -> LuxResult<Self> {
611 self.cat_apply_adapter(CatAdapter::from_context(context)?)
612 }
613
614 pub fn cat_apply_adapter(&self, adapter: CatAdapter) -> LuxResult<Self> {
615 Ok(Self::new(
616 self.values
617 .iter()
618 .copied()
619 .map(|value| adapter.apply(value))
620 .collect::<LuxResult<Vec<_>>>()?,
621 ))
622 }
623
624 pub fn delta_e(
625 &self,
626 other: &Self,
627 white_point: [f64; 3],
628 formula: DeltaEFormula,
629 ) -> LuxResult<Vec<f64>> {
630 if self.len() != other.len() {
631 return Err(LuxError::MismatchedLengths {
632 wavelengths: self.len(),
633 values: other.len(),
634 });
635 }
636
637 Ok(self
638 .values
639 .iter()
640 .copied()
641 .zip(other.values.iter().copied())
642 .map(|(left, right)| delta_e(left, right, white_point, formula))
643 .collect::<Vec<_>>())
644 }
645
646 pub fn cam_forward(
647 &self,
648 conditions: ModelCamViewingConditions,
649 ) -> LuxResult<Vec<CamAppearance>> {
650 self.values
651 .iter()
652 .copied()
653 .map(|value| cam_forward(value, conditions))
654 .collect::<LuxResult<Vec<_>>>()
655 }
656
657 pub fn cam16_forward(
658 &self,
659 conditions: ModelCamViewingConditions,
660 ) -> LuxResult<Vec<CamAppearance>> {
661 self.values
662 .iter()
663 .copied()
664 .map(|value| cam16_forward(value, conditions))
665 .collect::<LuxResult<Vec<_>>>()
666 }
667
668 pub fn ciecam02_forward(
669 &self,
670 conditions: ModelCamViewingConditions,
671 ) -> LuxResult<Vec<CamAppearance>> {
672 self.values
673 .iter()
674 .copied()
675 .map(|value| ciecam02_forward(value, conditions))
676 .collect::<LuxResult<Vec<_>>>()
677 }
678
679 pub fn cam_ucs_forward(
680 &self,
681 conditions: ModelCamViewingConditions,
682 ucs_type: CamUcsType,
683 ) -> LuxResult<Vec<CamUcsAppearance>> {
684 self.values
685 .iter()
686 .copied()
687 .map(|value| cam_ucs_forward(value, conditions, ucs_type))
688 .collect::<LuxResult<Vec<_>>>()
689 }
690
691 pub fn cam16_ucs_forward(
692 &self,
693 conditions: ModelCamViewingConditions,
694 ucs_type: CamUcsType,
695 ) -> LuxResult<Vec<CamUcsAppearance>> {
696 self.values
697 .iter()
698 .copied()
699 .map(|value| cam16_ucs_forward(value, conditions, ucs_type))
700 .collect::<LuxResult<Vec<_>>>()
701 }
702
703 pub fn ciecam02_ucs_forward(
704 &self,
705 conditions: ModelCamViewingConditions,
706 ucs_type: CamUcsType,
707 ) -> LuxResult<Vec<CamUcsAppearance>> {
708 self.values
709 .iter()
710 .copied()
711 .map(|value| ciecam02_ucs_forward(value, conditions, ucs_type))
712 .collect::<LuxResult<Vec<_>>>()
713 }
714
715 pub fn cam_inverse(&self, conditions: ModelCamViewingConditions) -> LuxResult<Self> {
716 Ok(Self::new(
717 self.values
718 .iter()
719 .copied()
720 .map(|value| {
721 cam_inverse(
722 CamAppearance {
723 lightness: value[0],
724 brightness: 0.0,
725 chroma: 0.0,
726 colorfulness: (value[1] * value[1] + value[2] * value[2]).sqrt(),
727 saturation: 0.0,
728 hue_angle: 0.0,
729 a_m: value[1],
730 b_m: value[2],
731 a_c: 0.0,
732 b_c: 0.0,
733 },
734 conditions,
735 )
736 })
737 .collect::<LuxResult<Vec<_>>>()?,
738 ))
739 }
740
741 pub fn cam16_ucs_inverse(
742 &self,
743 conditions: ModelCamViewingConditions,
744 ucs_type: CamUcsType,
745 ) -> LuxResult<Self> {
746 Ok(Self::new(
747 self.values
748 .iter()
749 .copied()
750 .map(|value| {
751 cam16_ucs_inverse(
752 CamUcsAppearance {
753 j_prime: value[0],
754 a_prime: value[1],
755 b_prime: value[2],
756 },
757 conditions,
758 ucs_type,
759 )
760 })
761 .collect::<LuxResult<Vec<_>>>()?,
762 ))
763 }
764
765 pub fn ciecam02_ucs_inverse(
766 &self,
767 conditions: ModelCamViewingConditions,
768 ucs_type: CamUcsType,
769 ) -> LuxResult<Self> {
770 Ok(Self::new(
771 self.values
772 .iter()
773 .copied()
774 .map(|value| {
775 ciecam02_ucs_inverse(
776 CamUcsAppearance {
777 j_prime: value[0],
778 a_prime: value[1],
779 b_prime: value[2],
780 },
781 conditions,
782 ucs_type,
783 )
784 })
785 .collect::<LuxResult<Vec<_>>>()?,
786 ))
787 }
788}
789
790impl From<Vec<[f64; 3]>> for Tristimulus {
791 fn from(values: Vec<[f64; 3]>) -> Self {
792 Self::new(values)
793 }
794}
795
796impl From<[f64; 3]> for Tristimulus {
797 fn from(value: [f64; 3]) -> Self {
798 Self::from_single(value)
799 }
800}
801
802impl CatTransform {
803 pub fn matrix(self) -> Matrix3 {
804 match self {
805 Self::Bradford => [
806 [0.8951, 0.2664, -0.1614],
807 [-0.7502, 1.7135, 0.0367],
808 [0.0389, -0.0685, 1.0296],
809 ],
810 Self::Cat02 => [
811 [0.7328, 0.4296, -0.1624],
812 [-0.7036, 1.6975, 0.0061],
813 [0.0030, 0.0136, 0.9834],
814 ],
815 Self::Cat16 => [
816 [0.401288, 0.650173, -0.051461],
817 [-0.250268, 1.204414, 0.045854],
818 [-0.002079, 0.048952, 0.953127],
819 ],
820 Self::Sharp => [
821 [1.2694, -0.0988, -0.1706],
822 [-0.8364, 1.8006, 0.0357],
823 [0.0297, -0.0315, 1.0018],
824 ],
825 Self::Bianco => [
826 [0.8752, 0.2787, -0.1539],
827 [-0.8904, 1.8709, 0.0195],
828 [-0.0061, 0.0162, 0.9899],
829 ],
830 Self::Cmc => [
831 [0.7982, 0.3389, -0.1371],
832 [-0.5918, 1.5512, 0.0406],
833 [0.0008, 0.0239, 0.9753],
834 ],
835 Self::Kries => [
836 [0.40024, 0.70760, -0.08081],
837 [-0.22630, 1.16532, 0.04570],
838 [0.0, 0.0, 0.91822],
839 ],
840 Self::Judd1945 => [[0.0, 1.0, 0.0], [-0.460, 1.359, 0.101], [0.0, 0.0, 1.0]],
841 Self::Judd1945Cie016 => [[0.0, 1.0, 0.0], [-0.460, 1.360, 0.102], [0.0, 0.0, 1.0]],
842 Self::Judd1935 => [
843 [3.1956, 2.4478, -0.1434],
844 [-2.5455, 7.0942, 0.9963],
845 [0.0, 0.0, 1.0],
846 ],
847 }
848 }
849}
850
851impl CatSurround {
852 pub fn factor(self) -> f64 {
853 match self {
854 Self::Average => 1.0,
855 Self::Dim => 0.9,
856 Self::Dark => 0.8,
857 Self::Display => 0.0,
858 }
859 }
860}
861
862impl CatMode {
863 pub fn default_baseline_white(self) -> [f64; 3] {
864 match self {
865 Self::SourceToBaseline | Self::BaselineToTarget | Self::TwoStep => {
866 [100.0, 100.0, 100.0]
867 }
868 Self::OneStep => [100.0, 100.0, 100.0],
869 }
870 }
871}
872
873impl CatViewingConditions {
874 pub fn new(surround: CatSurround, adapting_luminance: f64) -> LuxResult<Self> {
875 validate_adapting_luminance(adapting_luminance)?;
876 Ok(Self {
877 surround,
878 adapting_luminance,
879 })
880 }
881
882 pub fn degree_of_adaptation(self) -> LuxResult<f64> {
883 cat_degree_of_adaptation(self.surround, self.adapting_luminance)
884 }
885}
886
887impl CatContext {
888 pub fn new(
889 source_white: [f64; 3],
890 target_white: [f64; 3],
891 baseline_white: Option<[f64; 3]>,
892 transform: CatTransform,
893 mode: CatMode,
894 source_conditions: CatViewingConditions,
895 target_conditions: CatViewingConditions,
896 ) -> Self {
897 Self {
898 source_white,
899 target_white,
900 baseline_white,
901 transform,
902 mode,
903 source_conditions,
904 target_conditions,
905 }
906 }
907
908 pub fn baseline_white_or_default(self) -> [f64; 3] {
909 self.baseline_white
910 .unwrap_or(self.mode.default_baseline_white())
911 }
912
913 pub fn degrees_of_adaptation(self) -> LuxResult<[f64; 2]> {
914 cat_mode_degrees_from_conditions(self.mode, self.source_conditions, self.target_conditions)
915 }
916}
917
918impl CatConditionPair {
919 pub const fn new(source: CatViewingConditions, target: CatViewingConditions) -> Self {
920 Self { source, target }
921 }
922}
923
924impl CatAdapter {
925 pub fn new(matrix: Matrix3) -> Self {
926 Self { matrix }
927 }
928
929 pub fn matrix(self) -> Matrix3 {
930 self.matrix
931 }
932
933 pub fn apply(self, xyz: [f64; 3]) -> LuxResult<[f64; 3]> {
934 validate_xyz_triplet(xyz, "xyz values must be finite")?;
935 Ok(multiply_matrix3_vector3(self.matrix, xyz))
936 }
937
938 pub fn from_degree(
939 source_white: [f64; 3],
940 target_white: [f64; 3],
941 transform: CatTransform,
942 degree_of_adaptation: f64,
943 ) -> LuxResult<Self> {
944 validate_xyz_triplet(source_white, "source white values must be finite")?;
945 validate_xyz_triplet(target_white, "target white values must be finite")?;
946 validate_degree(
947 degree_of_adaptation,
948 "degree_of_adaptation must be finite and within 0..=1",
949 )?;
950
951 let sensor_matrix = transform.matrix();
952 let inverse = invert_matrix3(sensor_matrix);
953 let rgbw_source = multiply_matrix3_vector3(sensor_matrix, source_white);
954 let rgbw_target = multiply_matrix3_vector3(sensor_matrix, target_white);
955 let mut diagonal = [[0.0; 3]; 3];
956
957 for index in 0..3 {
958 if rgbw_source[index].abs() <= EPSILON {
959 return Err(LuxError::InvalidInput(
960 "source white produces zero CAT sensor response",
961 ));
962 }
963 let ratio = rgbw_target[index] / rgbw_source[index];
964 diagonal[index][index] = degree_of_adaptation * ratio + (1.0 - degree_of_adaptation);
965 }
966
967 Ok(Self::new(multiply_matrix3(
968 inverse,
969 multiply_matrix3(diagonal, sensor_matrix),
970 )))
971 }
972
973 pub fn from_mode(
974 source_white: [f64; 3],
975 target_white: [f64; 3],
976 baseline_white: Option<[f64; 3]>,
977 transform: CatTransform,
978 mode: CatMode,
979 degrees_of_adaptation: [f64; 2],
980 ) -> LuxResult<Self> {
981 validate_xyz_triplet(source_white, "source white values must be finite")?;
982 validate_xyz_triplet(target_white, "target white values must be finite")?;
983 validate_degree(
984 degrees_of_adaptation[0],
985 "degrees_of_adaptation[0] must be finite and within 0..=1",
986 )?;
987 validate_degree(
988 degrees_of_adaptation[1],
989 "degrees_of_adaptation[1] must be finite and within 0..=1",
990 )?;
991
992 let baseline_white = baseline_white.unwrap_or(mode.default_baseline_white());
993 validate_xyz_triplet(baseline_white, "baseline white values must be finite")?;
994
995 match mode {
996 CatMode::OneStep => Self::from_degree(
997 source_white,
998 target_white,
999 transform,
1000 degrees_of_adaptation[0],
1001 ),
1002 CatMode::SourceToBaseline => Self::from_degree(
1003 source_white,
1004 baseline_white,
1005 transform,
1006 degrees_of_adaptation[0],
1007 ),
1008 CatMode::BaselineToTarget => Self::from_degree(
1009 baseline_white,
1010 target_white,
1011 transform,
1012 degrees_of_adaptation[0],
1013 ),
1014 CatMode::TwoStep => {
1015 let sensor_matrix = transform.matrix();
1016 let inverse = invert_matrix3(sensor_matrix);
1017 let rgbw1 = multiply_matrix3_vector3(sensor_matrix, source_white);
1018 let rgbw2 = multiply_matrix3_vector3(sensor_matrix, target_white);
1019 let rgbw0 = multiply_matrix3_vector3(sensor_matrix, baseline_white);
1020 let mut diagonal = [[0.0; 3]; 3];
1021
1022 for index in 0..3 {
1023 if rgbw1[index].abs() <= EPSILON {
1024 return Err(LuxError::InvalidInput(
1025 "source white produces zero CAT sensor response",
1026 ));
1027 }
1028 if rgbw2[index].abs() <= EPSILON {
1029 return Err(LuxError::InvalidInput(
1030 "target white produces zero CAT sensor response",
1031 ));
1032 }
1033 let scale10 = degrees_of_adaptation[0] * (rgbw0[index] / rgbw1[index])
1034 + (1.0 - degrees_of_adaptation[0]);
1035 let scale20 = degrees_of_adaptation[1] * (rgbw0[index] / rgbw2[index])
1036 + (1.0 - degrees_of_adaptation[1]);
1037 diagonal[index][index] = scale10 / scale20;
1038 }
1039
1040 Ok(Self::new(multiply_matrix3(
1041 inverse,
1042 multiply_matrix3(diagonal, sensor_matrix),
1043 )))
1044 }
1045 }
1046 }
1047
1048 pub fn from_conditions(
1049 source_white: [f64; 3],
1050 target_white: [f64; 3],
1051 baseline_white: Option<[f64; 3]>,
1052 transform: CatTransform,
1053 mode: CatMode,
1054 source_conditions: CatViewingConditions,
1055 target_conditions: CatViewingConditions,
1056 ) -> LuxResult<Self> {
1057 let degrees = cat_mode_degrees_from_conditions(mode, source_conditions, target_conditions)?;
1058 Self::from_mode(
1059 source_white,
1060 target_white,
1061 baseline_white,
1062 transform,
1063 mode,
1064 degrees,
1065 )
1066 }
1067
1068 pub fn from_context(context: CatContext) -> LuxResult<Self> {
1069 Self::from_conditions(
1070 context.source_white,
1071 context.target_white,
1072 context.baseline_white,
1073 context.transform,
1074 context.mode,
1075 context.source_conditions,
1076 context.target_conditions,
1077 )
1078 }
1079}
1080
1081impl TristimulusObserver {
1082 pub fn from_csv(csv: &str, k: f64) -> LuxResult<Self> {
1083 let mut wavelengths = Vec::new();
1084 let mut x_bar = Vec::new();
1085 let mut y_bar = Vec::new();
1086 let mut z_bar = Vec::new();
1087
1088 for line in csv.lines() {
1089 let trimmed = line.trim();
1090 if trimmed.is_empty() {
1091 continue;
1092 }
1093 let mut parts = trimmed.split(',');
1094 let wl = parts
1095 .next()
1096 .ok_or(LuxError::ParseError("missing wavelength"))?
1097 .trim()
1098 .parse::<f64>()
1099 .map_err(|_| LuxError::ParseError("invalid wavelength"))?;
1100 let x = parts
1101 .next()
1102 .ok_or(LuxError::ParseError("missing x_bar"))?
1103 .trim()
1104 .parse::<f64>()
1105 .map_err(|_| LuxError::ParseError("invalid x_bar"))?;
1106 let y = parts
1107 .next()
1108 .ok_or(LuxError::ParseError("missing y_bar"))?
1109 .trim()
1110 .parse::<f64>()
1111 .map_err(|_| LuxError::ParseError("invalid y_bar"))?;
1112 let z = parts
1113 .next()
1114 .ok_or(LuxError::ParseError("missing z_bar"))?
1115 .trim()
1116 .parse::<f64>()
1117 .map_err(|_| LuxError::ParseError("invalid z_bar"))?;
1118
1119 wavelengths.push(wl);
1120 x_bar.push(x);
1121 y_bar.push(y);
1122 z_bar.push(z);
1123 }
1124
1125 if wavelengths.is_empty() {
1126 return Err(LuxError::EmptyInput);
1127 }
1128
1129 Ok(Self {
1130 wavelengths,
1131 x_bar,
1132 y_bar,
1133 z_bar,
1134 k,
1135 })
1136 }
1137
1138 pub fn vl_spectrum(&self) -> LuxResult<Spectrum> {
1139 Spectrum::new(self.wavelengths.clone(), self.y_bar.clone())
1140 }
1141
1142 pub fn xyz_spectra(&self) -> LuxResult<Spectrum> {
1143 Spectrum::new(
1144 self.wavelengths.clone(),
1145 vec![self.x_bar.clone(), self.y_bar.clone(), self.z_bar.clone()],
1146 )
1147 }
1148
1149 pub fn x_bar_spectrum(&self) -> LuxResult<Spectrum> {
1150 Spectrum::new(self.wavelengths.clone(), self.x_bar.clone())
1151 }
1152
1153 pub fn z_bar_spectrum(&self) -> LuxResult<Spectrum> {
1154 Spectrum::new(self.wavelengths.clone(), self.z_bar.clone())
1155 }
1156}
1157
1158fn nonzero(value: f64) -> f64 {
1159 if value == 0.0 {
1160 EPSILON
1161 } else {
1162 value
1163 }
1164}
1165
1166fn lab_response_curve(value: f64, white: f64) -> f64 {
1167 let ratio = value / white;
1168 if ratio <= LAB_LINEAR_THRESHOLD {
1169 LAB_LINEAR_SCALE * ratio + 16.0 / 116.0
1170 } else {
1171 ratio.cbrt()
1172 }
1173}
1174
1175fn lab_inverse_response_curve(response: f64, white: f64) -> f64 {
1176 if response <= 24.0 / 116.0 {
1177 white * ((response - 16.0 / 116.0) * LAB_INVERSE_LINEAR_SCALE)
1178 } else {
1179 white * response.powi(3)
1180 }
1181}
1182
1183fn cie_lightness_from_ratio(y_ratio: f64) -> f64 {
1184 if y_ratio <= LUV_LINEAR_THRESHOLD {
1185 LUV_LINEAR_SCALE * y_ratio
1186 } else {
1187 116.0 * y_ratio.cbrt() - 16.0
1188 }
1189}
1190
1191fn cie_y_ratio_from_lightness(lightness: f64) -> f64 {
1192 let y_ratio = ((lightness + 16.0) / 116.0).powi(3);
1193 if y_ratio < LUV_LINEAR_THRESHOLD {
1194 lightness / LUV_LINEAR_SCALE
1195 } else {
1196 y_ratio
1197 }
1198}
1199
1200fn multiply_matrix3_vector3(matrix: Matrix3, vector: [f64; 3]) -> [f64; 3] {
1201 [
1202 matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
1203 matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
1204 matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2],
1205 ]
1206}
1207
1208fn multiply_matrix3(left: Matrix3, right: Matrix3) -> Matrix3 {
1209 let mut out = [[0.0; 3]; 3];
1210 for row in 0..3 {
1211 for col in 0..3 {
1212 out[row][col] = left[row][0] * right[0][col]
1213 + left[row][1] * right[1][col]
1214 + left[row][2] * right[2][col];
1215 }
1216 }
1217 out
1218}
1219
1220fn clamp(value: f64, min: f64, max: f64) -> f64 {
1221 value.max(min).min(max)
1222}
1223
1224fn degrees_to_radians(value: f64) -> f64 {
1225 value * std::f64::consts::PI / 180.0
1226}
1227
1228fn hue_angle_degrees(a: f64, b: f64) -> f64 {
1229 let angle = b.atan2(a).to_degrees();
1230 if angle < 0.0 {
1231 angle + 360.0
1232 } else {
1233 angle
1234 }
1235}
1236
1237fn validate_xyz_triplet(xyz: [f64; 3], label: &'static str) -> LuxResult<()> {
1238 if xyz.iter().all(|value| value.is_finite()) {
1239 Ok(())
1240 } else {
1241 Err(LuxError::InvalidInput(label))
1242 }
1243}
1244
1245fn validate_degree(value: f64, label: &'static str) -> LuxResult<()> {
1246 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
1247 Err(LuxError::InvalidInput(label))
1248 } else {
1249 Ok(())
1250 }
1251}
1252
1253fn validate_adapting_luminance(adapting_luminance: f64) -> LuxResult<()> {
1254 if !adapting_luminance.is_finite() || adapting_luminance < 0.0 {
1255 Err(LuxError::InvalidInput(
1256 "adapting_luminance must be finite and non-negative",
1257 ))
1258 } else {
1259 Ok(())
1260 }
1261}
1262
1263pub(crate) fn invert_matrix3(matrix: Matrix3) -> Matrix3 {
1264 let a = matrix[0][0];
1265 let b = matrix[0][1];
1266 let c = matrix[0][2];
1267 let d = matrix[1][0];
1268 let e = matrix[1][1];
1269 let f = matrix[1][2];
1270 let g = matrix[2][0];
1271 let h = matrix[2][1];
1272 let i = matrix[2][2];
1273
1274 let cofactor00 = e * i - f * h;
1275 let cofactor01 = -(d * i - f * g);
1276 let cofactor02 = d * h - e * g;
1277 let cofactor10 = -(b * i - c * h);
1278 let cofactor11 = a * i - c * g;
1279 let cofactor12 = -(a * h - b * g);
1280 let cofactor20 = b * f - c * e;
1281 let cofactor21 = -(a * f - c * d);
1282 let cofactor22 = a * e - b * d;
1283
1284 let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
1285 let inv_det = 1.0 / determinant;
1286
1287 [
1288 [
1289 cofactor00 * inv_det,
1290 cofactor10 * inv_det,
1291 cofactor20 * inv_det,
1292 ],
1293 [
1294 cofactor01 * inv_det,
1295 cofactor11 * inv_det,
1296 cofactor21 * inv_det,
1297 ],
1298 [
1299 cofactor02 * inv_det,
1300 cofactor12 * inv_det,
1301 cofactor22 * inv_det,
1302 ],
1303 ]
1304}
1305
1306pub fn xyz_to_yxy(xyz: [f64; 3]) -> [f64; 3] {
1309 let sum = xyz[0] + xyz[1] + xyz[2];
1310 let denominator = nonzero(sum);
1311 [xyz[1], xyz[0] / denominator, xyz[1] / denominator]
1312}
1313
1314pub fn yxy_to_xyz(yxy: [f64; 3]) -> [f64; 3] {
1315 let y = nonzero(yxy[2]);
1316 [
1317 yxy[0] * yxy[1] / y,
1318 yxy[0],
1319 yxy[0] * (1.0 - yxy[1] - yxy[2]) / y,
1320 ]
1321}
1322
1323pub fn xyz_to_yuv(xyz: [f64; 3]) -> [f64; 3] {
1324 let denominator = xyz[0] + 15.0 * xyz[1] + 3.0 * xyz[2];
1325 let denominator = nonzero(denominator);
1326 [
1327 xyz[1],
1328 4.0 * xyz[0] / denominator,
1329 9.0 * xyz[1] / denominator,
1330 ]
1331}
1332
1333pub fn yuv_to_xyz(yuv: [f64; 3]) -> [f64; 3] {
1334 let v = nonzero(yuv[2]);
1335 [
1336 yuv[0] * (9.0 * yuv[1]) / (4.0 * v),
1337 yuv[0],
1338 yuv[0] * (12.0 - 3.0 * yuv[1] - 20.0 * yuv[2]) / (4.0 * v),
1339 ]
1340}
1341
1342pub fn xyz_to_lms_with_matrix(xyz: [f64; 3], matrix: Matrix3) -> [f64; 3] {
1345 multiply_matrix3_vector3(matrix, xyz)
1346}
1347
1348pub fn lms_to_xyz_with_matrix(lms: [f64; 3], matrix: Matrix3) -> [f64; 3] {
1349 multiply_matrix3_vector3(invert_matrix3(matrix), lms)
1350}
1351
1352pub fn xyz_to_lms(xyz: [f64; 3], observer: Observer) -> LuxResult<[f64; 3]> {
1353 Ok(xyz_to_lms_with_matrix(xyz, observer.xyz_to_lms_matrix()?))
1354}
1355
1356pub fn lms_to_xyz(lms: [f64; 3], observer: Observer) -> LuxResult<[f64; 3]> {
1357 Ok(lms_to_xyz_with_matrix(lms, observer.xyz_to_lms_matrix()?))
1358}
1359
1360pub fn cat_apply(
1363 xyz: [f64; 3],
1364 source_white: [f64; 3],
1365 target_white: [f64; 3],
1366 transform: CatTransform,
1367 degree_of_adaptation: f64,
1368) -> LuxResult<[f64; 3]> {
1369 cat_compile(source_white, target_white, transform, degree_of_adaptation)?.apply(xyz)
1370}
1371
1372pub fn cat_apply_mode(
1373 xyz: [f64; 3],
1374 source_white: [f64; 3],
1375 target_white: [f64; 3],
1376 baseline_white: Option<[f64; 3]>,
1377 transform: CatTransform,
1378 mode: CatMode,
1379 degrees_of_adaptation: [f64; 2],
1380) -> LuxResult<[f64; 3]> {
1381 cat_compile_mode(
1382 source_white,
1383 target_white,
1384 baseline_white,
1385 transform,
1386 mode,
1387 degrees_of_adaptation,
1388 )?
1389 .apply(xyz)
1390}
1391
1392pub fn cat_compile(
1393 source_white: [f64; 3],
1394 target_white: [f64; 3],
1395 transform: CatTransform,
1396 degree_of_adaptation: f64,
1397) -> LuxResult<CatAdapter> {
1398 CatAdapter::from_degree(source_white, target_white, transform, degree_of_adaptation)
1399}
1400
1401pub fn cat_compile_mode(
1402 source_white: [f64; 3],
1403 target_white: [f64; 3],
1404 baseline_white: Option<[f64; 3]>,
1405 transform: CatTransform,
1406 mode: CatMode,
1407 degrees_of_adaptation: [f64; 2],
1408) -> LuxResult<CatAdapter> {
1409 CatAdapter::from_mode(
1410 source_white,
1411 target_white,
1412 baseline_white,
1413 transform,
1414 mode,
1415 degrees_of_adaptation,
1416 )
1417}
1418
1419pub fn cat_degree_of_adaptation(surround: CatSurround, adapting_luminance: f64) -> LuxResult<f64> {
1420 validate_adapting_luminance(adapting_luminance)?;
1421
1422 let factor = surround.factor();
1423 let degree = factor * (1.0 - (1.0 / 3.6) * ((-adapting_luminance - 42.0) / 92.0).exp());
1424 Ok(clamp(degree, 0.0, 1.0))
1425}
1426
1427pub fn cat_mode_degrees_from_conditions(
1428 mode: CatMode,
1429 source_conditions: CatViewingConditions,
1430 target_conditions: CatViewingConditions,
1431) -> LuxResult<[f64; 2]> {
1432 let source_degree = source_conditions.degree_of_adaptation()?;
1433 let target_degree = target_conditions.degree_of_adaptation()?;
1434
1435 Ok(match mode {
1436 CatMode::OneStep | CatMode::SourceToBaseline => [source_degree, source_degree],
1437 CatMode::BaselineToTarget => [target_degree, target_degree],
1438 CatMode::TwoStep => [source_degree, target_degree],
1439 })
1440}
1441
1442pub fn cat_apply_with_conditions(
1443 xyz: [f64; 3],
1444 source_white: [f64; 3],
1445 target_white: [f64; 3],
1446 transform: CatTransform,
1447 surround: CatSurround,
1448 adapting_luminance: f64,
1449) -> LuxResult<[f64; 3]> {
1450 cat_compile_with_conditions(
1451 source_white,
1452 target_white,
1453 transform,
1454 surround,
1455 adapting_luminance,
1456 )?
1457 .apply(xyz)
1458}
1459
1460pub fn cat_apply_mode_with_conditions(
1461 xyz: [f64; 3],
1462 source_white: [f64; 3],
1463 target_white: [f64; 3],
1464 baseline_white: Option<[f64; 3]>,
1465 transform: CatTransform,
1466 mode: CatMode,
1467 conditions: CatConditionPair,
1468) -> LuxResult<[f64; 3]> {
1469 cat_compile_mode_with_conditions(
1470 source_white,
1471 target_white,
1472 baseline_white,
1473 transform,
1474 mode,
1475 conditions.source,
1476 conditions.target,
1477 )
1478 .and_then(|adapter| adapter.apply(xyz))
1479}
1480
1481pub fn cat_apply_context(xyz: [f64; 3], context: CatContext) -> LuxResult<[f64; 3]> {
1482 cat_compile_context(context)?.apply(xyz)
1483}
1484
1485pub fn cat_compile_with_conditions(
1486 source_white: [f64; 3],
1487 target_white: [f64; 3],
1488 transform: CatTransform,
1489 surround: CatSurround,
1490 adapting_luminance: f64,
1491) -> LuxResult<CatAdapter> {
1492 let degree = cat_degree_of_adaptation(surround, adapting_luminance)?;
1493 cat_compile(source_white, target_white, transform, degree)
1494}
1495
1496pub fn cat_compile_mode_with_conditions(
1497 source_white: [f64; 3],
1498 target_white: [f64; 3],
1499 baseline_white: Option<[f64; 3]>,
1500 transform: CatTransform,
1501 mode: CatMode,
1502 source_conditions: CatViewingConditions,
1503 target_conditions: CatViewingConditions,
1504) -> LuxResult<CatAdapter> {
1505 CatAdapter::from_conditions(
1506 source_white,
1507 target_white,
1508 baseline_white,
1509 transform,
1510 mode,
1511 source_conditions,
1512 target_conditions,
1513 )
1514}
1515
1516pub fn cat_compile_context(context: CatContext) -> LuxResult<CatAdapter> {
1517 CatAdapter::from_context(context)
1518}
1519
1520pub const D65_XY: [f64; 2] = [0.3127, 0.3290];
1533pub const D65_XYZ: [f64; 3] = [95.047, 100.0, 108.883];
1535
1536pub const SRGB_PRIMARIES: [[f64; 2]; 3] = [[0.640, 0.330], [0.300, 0.600], [0.150, 0.060]];
1538pub const DISPLAY_P3_PRIMARIES: [[f64; 2]; 3] = [[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]];
1540pub const BT2020_PRIMARIES: [[f64; 2]; 3] = [[0.708, 0.292], [0.170, 0.797], [0.131, 0.046]];
1542
1543#[derive(Debug, Clone, Copy, PartialEq)]
1548pub enum TransferFunction {
1549 SRgb,
1551 SegmentedGamma {
1555 gamma: f64,
1556 offset: f64,
1557 linear_part: bool,
1558 },
1559 Gamma(f64),
1561 Linear,
1563 Pq,
1566 Hlg { peak_luminance: f64 },
1569}
1570
1571impl TransferFunction {
1572 pub fn eotf(self, encoded: f64) -> f64 {
1575 match self {
1576 TransferFunction::SRgb => segmented_gamma_eotf(encoded, 2.4, -0.055, true),
1577 TransferFunction::SegmentedGamma {
1578 gamma,
1579 offset,
1580 linear_part,
1581 } => segmented_gamma_eotf(encoded, gamma, offset, linear_part),
1582 TransferFunction::Gamma(gamma) => encoded.powf(gamma),
1583 TransferFunction::Linear => encoded,
1584 TransferFunction::Pq => pq_eotf(encoded),
1585 TransferFunction::Hlg { peak_luminance } => hlg_eotf(encoded, peak_luminance),
1586 }
1587 }
1588
1589 pub fn oetf(self, linear: f64) -> f64 {
1591 match self {
1592 TransferFunction::SRgb => segmented_gamma_oetf(linear, 2.4, -0.055, true),
1593 TransferFunction::SegmentedGamma {
1594 gamma,
1595 offset,
1596 linear_part,
1597 } => segmented_gamma_oetf(linear, gamma, offset, linear_part),
1598 TransferFunction::Gamma(gamma) => linear.powf(1.0 / gamma),
1599 TransferFunction::Linear => linear,
1600 TransferFunction::Pq => pq_oetf(linear),
1601 TransferFunction::Hlg { peak_luminance } => hlg_oetf(linear, peak_luminance),
1602 }
1603 }
1604}
1605
1606fn segmented_gamma_eotf(encoded: f64, gamma: f64, offset: f64, linear_part: bool) -> f64 {
1607 let mut value = ((encoded - offset) / (1.0 - offset)).powf(gamma);
1608 if linear_part && value < 0.0031308 {
1609 value = encoded / 12.92;
1610 }
1611 value
1612}
1613
1614fn segmented_gamma_oetf(linear: f64, gamma: f64, offset: f64, linear_part: bool) -> f64 {
1615 let clamped = clamp(linear, 0.0, 1.0);
1616 let mut encoded = (1.0 - offset) * clamped.powf(1.0 / gamma) + offset;
1617 if linear_part && clamped <= 0.0031308 {
1618 encoded = clamped * 12.92;
1619 }
1620 encoded
1621}
1622
1623fn pq_constants() -> (f64, f64, f64, f64, f64) {
1624 (0.1593017578125, 78.84375, 0.8359375, 18.8515625, 18.6875)
1625}
1626
1627fn pq_eotf(encoded: f64) -> f64 {
1628 let (m1, m2, c1, c2, c3) = pq_constants();
1629 let em = encoded.powf(1.0 / m2);
1630 let num = (em - c1).max(0.0);
1631 let den = c2 - c3 * em;
1632 (num / den).powf(1.0 / m1)
1633}
1634
1635fn pq_oetf(linear: f64) -> f64 {
1636 let (m1, m2, c1, c2, c3) = pq_constants();
1637 let lm = linear.powf(m1);
1638 ((c1 + c2 * lm) / (1.0 + c3 * lm)).powf(m2)
1639}
1640
1641const HLG_A: f64 = 0.17883277;
1642const HLG_B: f64 = 0.28466892;
1643const HLG_C: f64 = 0.55991073;
1644
1645fn hlg_system_gamma(peak_luminance: f64) -> f64 {
1646 1.2 + 0.42 * (peak_luminance / 1000.0).log10()
1647}
1648
1649fn hlg_oetf_inverse(encoded: f64) -> f64 {
1650 if encoded <= 0.5 {
1651 encoded * encoded / 3.0
1652 } else {
1653 (((encoded - HLG_C) / HLG_A).exp() + HLG_B) / 12.0
1654 }
1655}
1656
1657fn hlg_oetf_scene(scene: f64) -> f64 {
1658 if scene <= 1.0 / 12.0 {
1659 (3.0 * scene).sqrt()
1660 } else {
1661 HLG_A * (12.0 * scene - HLG_B).ln() + HLG_C
1662 }
1663}
1664
1665fn hlg_eotf(encoded: f64, peak_luminance: f64) -> f64 {
1666 let gamma = hlg_system_gamma(peak_luminance);
1667 hlg_oetf_inverse(encoded).powf(gamma)
1668}
1669
1670fn hlg_oetf(linear: f64, peak_luminance: f64) -> f64 {
1671 let gamma = hlg_system_gamma(peak_luminance);
1672 hlg_oetf_scene(linear.powf(1.0 / gamma))
1673}
1674
1675fn primaries_to_matrix(primaries: [[f64; 2]; 3], white_xy: [f64; 2]) -> Matrix3 {
1679 let columns: [[f64; 3]; 3] = primaries.map(|[x, y]| [x / y, 1.0, (1.0 - x - y) / y]);
1680 let p: Matrix3 = [
1681 [columns[0][0], columns[1][0], columns[2][0]],
1682 [columns[0][1], columns[1][1], columns[2][1]],
1683 [columns[0][2], columns[1][2], columns[2][2]],
1684 ];
1685 let [wx, wy] = white_xy;
1686 let white = [wx / wy, 1.0, (1.0 - wx - wy) / wy];
1687 let scale = multiply_matrix3_vector3(invert_matrix3(p), white);
1688 [
1689 [p[0][0] * scale[0], p[0][1] * scale[1], p[0][2] * scale[2]],
1690 [p[1][0] * scale[0], p[1][1] * scale[1], p[1][2] * scale[2]],
1691 [p[2][0] * scale[0], p[2][1] * scale[1], p[2][2] * scale[2]],
1692 ]
1693}
1694
1695#[derive(Debug, Clone, Copy, PartialEq)]
1713pub struct RgbColorSpace {
1714 name: &'static str,
1715 rgb_to_xyz: Matrix3,
1716 xyz_to_rgb: Matrix3,
1717 transfer: TransferFunction,
1718}
1719
1720impl RgbColorSpace {
1721 pub fn from_primaries(
1741 name: &'static str,
1742 primaries: [[f64; 2]; 3],
1743 white_xy: [f64; 2],
1744 transfer: TransferFunction,
1745 ) -> Self {
1746 let rgb_to_xyz = primaries_to_matrix(primaries, white_xy);
1747 let xyz_to_rgb = invert_matrix3(rgb_to_xyz);
1748 Self {
1749 name,
1750 rgb_to_xyz,
1751 xyz_to_rgb,
1752 transfer,
1753 }
1754 }
1755
1756 pub fn name(self) -> &'static str {
1758 self.name
1759 }
1760
1761 pub fn transfer(self) -> TransferFunction {
1763 self.transfer
1764 }
1765
1766 pub fn rgb_to_xyz_matrix(self) -> Matrix3 {
1768 self.rgb_to_xyz
1769 }
1770}
1771
1772pub fn srgb_space() -> RgbColorSpace {
1774 RgbColorSpace::from_primaries("srgb", SRGB_PRIMARIES, D65_XY, TransferFunction::SRgb)
1775}
1776
1777pub fn display_p3_space() -> RgbColorSpace {
1779 RgbColorSpace::from_primaries(
1780 "display-p3",
1781 DISPLAY_P3_PRIMARIES,
1782 D65_XY,
1783 TransferFunction::SRgb,
1784 )
1785}
1786
1787pub fn rec2100_pq_space() -> RgbColorSpace {
1789 RgbColorSpace::from_primaries(
1790 "rec2100-pq",
1791 BT2020_PRIMARIES,
1792 D65_XY,
1793 TransferFunction::Pq,
1794 )
1795}
1796
1797pub fn rec2100_hlg_space() -> RgbColorSpace {
1800 RgbColorSpace::from_primaries(
1801 "rec2100-hlg",
1802 BT2020_PRIMARIES,
1803 D65_XY,
1804 TransferFunction::Hlg {
1805 peak_luminance: 1000.0,
1806 },
1807 )
1808}
1809
1810pub fn rgb_to_xyz(rgb: [f64; 3], space: RgbColorSpace) -> [f64; 3] {
1823 let linear = [
1824 space.transfer.eotf(rgb[0]),
1825 space.transfer.eotf(rgb[1]),
1826 space.transfer.eotf(rgb[2]),
1827 ];
1828 let xyz = multiply_matrix3_vector3(space.rgb_to_xyz, linear);
1829 [xyz[0] * 100.0, xyz[1] * 100.0, xyz[2] * 100.0]
1830}
1831
1832pub fn xyz_to_rgb(xyz: [f64; 3], space: RgbColorSpace) -> [f64; 3] {
1851 let linear = multiply_matrix3_vector3(
1852 space.xyz_to_rgb,
1853 [xyz[0] / 100.0, xyz[1] / 100.0, xyz[2] / 100.0],
1854 );
1855 [
1856 space.transfer.oetf(linear[0]),
1857 space.transfer.oetf(linear[1]),
1858 space.transfer.oetf(linear[2]),
1859 ]
1860}
1861
1862fn srgb_parameterized_space(gamma: f64, offset: f64, linear_part: bool) -> RgbColorSpace {
1869 RgbColorSpace {
1870 name: "srgb-parameterized",
1871 rgb_to_xyz: SRGB_RGB_TO_XYZ,
1872 xyz_to_rgb: SRGB_XYZ_TO_RGB,
1873 transfer: TransferFunction::SegmentedGamma {
1874 gamma,
1875 offset,
1876 linear_part,
1877 },
1878 }
1879}
1880
1881pub fn xyz_to_srgb(xyz: [f64; 3], gamma: f64, offset: f64, use_linear_part: bool) -> [f64; 3] {
1882 let space = srgb_parameterized_space(gamma, offset, use_linear_part);
1883 let rgb = xyz_to_rgb(xyz, space);
1884 [
1885 clamp(rgb[0] * 255.0, 0.0, 255.0),
1886 clamp(rgb[1] * 255.0, 0.0, 255.0),
1887 clamp(rgb[2] * 255.0, 0.0, 255.0),
1888 ]
1889}
1890
1891pub fn srgb_to_xyz(rgb: [f64; 3], gamma: f64, offset: f64, use_linear_part: bool) -> [f64; 3] {
1892 let space = srgb_parameterized_space(gamma, offset, use_linear_part);
1893 rgb_to_xyz([rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0], space)
1894}
1895
1896pub fn xyz_to_lab(xyz: [f64; 3], white_point: [f64; 3]) -> [f64; 3] {
1899 let fx = lab_response_curve(xyz[0], white_point[0]);
1900 let fy = lab_response_curve(xyz[1], white_point[1]);
1901 let fz = lab_response_curve(xyz[2], white_point[2]);
1902 let l = cie_lightness_from_ratio(xyz[1] / white_point[1]);
1903
1904 [l, 500.0 * (fx - fy), 200.0 * (fy - fz)]
1905}
1906
1907pub fn lab_to_xyz(lab: [f64; 3], white_point: [f64; 3]) -> [f64; 3] {
1908 let fy = (lab[0] + 16.0) / 116.0;
1909 let fx = lab[1] / 500.0 + fy;
1910 let fz = fy - lab[2] / 200.0;
1911
1912 [
1913 lab_inverse_response_curve(fx, white_point[0]),
1914 lab_inverse_response_curve(fy, white_point[1]),
1915 lab_inverse_response_curve(fz, white_point[2]),
1916 ]
1917}
1918
1919pub fn xyz_to_oklab(xyz: [f64; 3]) -> [f64; 3] {
1924 let x = xyz[0] / 100.0;
1925 let y = xyz[1] / 100.0;
1926 let z = xyz[2] / 100.0;
1927
1928 let l = (0.819_022_437_996_703 * x
1929 + 0.361_906_260_052_890_4 * y
1930 - 0.128_873_781_520_987_9 * z)
1931 .cbrt();
1932 let m = (0.032_983_653_932_388_5 * x
1933 + 0.929_286_861_586_343_4 * y
1934 + 0.036_144_666_350_642_4 * z)
1935 .cbrt();
1936 let s = (0.048_177_189_359_624_2 * x
1937 + 0.264_239_531_752_730_8 * y
1938 + 0.633_547_828_469_430_9 * z)
1939 .cbrt();
1940
1941 [
1942 0.210_454_255_3 * l + 0.793_617_785 * m - 0.004_072_046_8 * s,
1943 1.977_998_495_1 * l - 2.428_592_205 * m + 0.450_593_709_9 * s,
1944 0.025_904_037_1 * l + 0.782_771_766_2 * m - 0.808_675_766 * s,
1945 ]
1946}
1947
1948pub fn oklab_to_xyz(oklab: [f64; 3]) -> [f64; 3] {
1950 let l = (oklab[0] + 0.396_337_777_4 * oklab[1] + 0.215_803_757_3 * oklab[2]).powi(3);
1951 let m = (oklab[0] - 0.105_561_345_8 * oklab[1] - 0.063_854_172_8 * oklab[2]).powi(3);
1952 let s = (oklab[0] - 0.089_484_177_5 * oklab[1] - 1.291_485_548 * oklab[2]).powi(3);
1953
1954 [
1955 (1.226_879_873_374_155_7 * l
1956 - 0.557_814_996_555_481_3 * m
1957 + 0.281_391_050_177_215_83 * s)
1958 * 100.0,
1959 (-0.040_575_762_624_313_72 * l
1960 + 1.112_286_829_397_059_4 * m
1961 - 0.071_711_066_661_517_01 * s)
1962 * 100.0,
1963 (-0.076_372_949_746_721_42 * l
1964 - 0.421_493_323_962_791_4 * m
1965 + 1.586_924_024_427_242_2 * s)
1966 * 100.0,
1967 ]
1968}
1969
1970pub fn oklab_to_oklch(oklab: [f64; 3]) -> [f64; 3] {
1972 let chroma = oklab[1].hypot(oklab[2]);
1973 let hue = oklab[2].atan2(oklab[1]).to_degrees().rem_euclid(360.0);
1974 [oklab[0], chroma, hue]
1975}
1976
1977pub fn oklch_to_oklab(oklch: [f64; 3]) -> [f64; 3] {
1979 let hue = oklch[2].to_radians();
1980 [oklch[0], oklch[1] * hue.cos(), oklch[1] * hue.sin()]
1981}
1982
1983pub fn xyz_to_oklch(xyz: [f64; 3]) -> [f64; 3] {
1985 oklab_to_oklch(xyz_to_oklab(xyz))
1986}
1987
1988pub fn oklch_to_xyz(oklch: [f64; 3]) -> [f64; 3] {
1990 oklab_to_xyz(oklch_to_oklab(oklch))
1991}
1992
1993pub fn xyz_to_luv(xyz: [f64; 3], white_point: [f64; 3]) -> [f64; 3] {
1994 let yuv = xyz_to_yuv(xyz);
1995 let white_yuv = xyz_to_yuv(white_point);
1996 let y_ratio = yuv[0] / white_yuv[0];
1997 let l = cie_lightness_from_ratio(y_ratio);
1998
1999 [
2000 l,
2001 13.0 * l * (yuv[1] - white_yuv[1]),
2002 13.0 * l * (yuv[2] - white_yuv[2]),
2003 ]
2004}
2005
2006pub fn luv_to_xyz(luv: [f64; 3], white_point: [f64; 3]) -> [f64; 3] {
2007 let white_yuv = xyz_to_yuv(white_point);
2008 let mut yuv = [0.0; 3];
2009 if luv[0] == 0.0 {
2010 yuv[1] = 0.0;
2011 yuv[2] = 0.0;
2012 } else {
2013 yuv[1] = luv[1] / (13.0 * luv[0]) + white_yuv[1];
2014 yuv[2] = luv[2] / (13.0 * luv[0]) + white_yuv[2];
2015 }
2016
2017 yuv[0] = white_yuv[0] * cie_y_ratio_from_lightness(luv[0]);
2018
2019 yuv_to_xyz(yuv)
2020}
2021
2022fn delta_e_lab(lab1: [f64; 3], lab2: [f64; 3], formula: DeltaEFormula) -> f64 {
2025 match formula {
2026 DeltaEFormula::Cie76 => delta_e_cie76_lab(lab1, lab2),
2027 DeltaEFormula::Ciede2000 => delta_e_ciede2000_lab(lab1, lab2),
2028 }
2029}
2030
2031pub fn delta_e(
2032 xyz1: [f64; 3],
2033 xyz2: [f64; 3],
2034 white_point: [f64; 3],
2035 formula: DeltaEFormula,
2036) -> f64 {
2037 delta_e_lab(
2038 xyz_to_lab(xyz1, white_point),
2039 xyz_to_lab(xyz2, white_point),
2040 formula,
2041 )
2042}
2043
2044pub fn delta_e_cie76(xyz1: [f64; 3], xyz2: [f64; 3], white_point: [f64; 3]) -> f64 {
2045 delta_e(xyz1, xyz2, white_point, DeltaEFormula::Cie76)
2046}
2047
2048pub fn delta_e_ciede2000(xyz1: [f64; 3], xyz2: [f64; 3], white_point: [f64; 3]) -> f64 {
2049 delta_e(xyz1, xyz2, white_point, DeltaEFormula::Ciede2000)
2050}
2051
2052fn delta_e_cie76_lab(lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
2053 let dl = lab1[0] - lab2[0];
2054 let da = lab1[1] - lab2[1];
2055 let db = lab1[2] - lab2[2];
2056 (dl * dl + da * da + db * db).sqrt()
2057}
2058
2059fn delta_e_ciede2000_lab(lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
2060 let (l1, a1, b1) = (lab1[0], lab1[1], lab1[2]);
2061 let (l2, a2, b2) = (lab2[0], lab2[1], lab2[2]);
2062
2063 let c1 = (a1 * a1 + b1 * b1).sqrt();
2064 let c2 = (a2 * a2 + b2 * b2).sqrt();
2065 let c_bar = (c1 + c2) / 2.0;
2066 let c_bar7 = c_bar.powi(7);
2067 let g = 0.5 * (1.0 - (c_bar7 / (c_bar7 + 25_f64.powi(7))).sqrt());
2068
2069 let a1_prime = (1.0 + g) * a1;
2070 let a2_prime = (1.0 + g) * a2;
2071 let c1_prime = (a1_prime * a1_prime + b1 * b1).sqrt();
2072 let c2_prime = (a2_prime * a2_prime + b2 * b2).sqrt();
2073 let h1_prime = if c1_prime == 0.0 {
2074 0.0
2075 } else {
2076 hue_angle_degrees(a1_prime, b1)
2077 };
2078 let h2_prime = if c2_prime == 0.0 {
2079 0.0
2080 } else {
2081 hue_angle_degrees(a2_prime, b2)
2082 };
2083
2084 let delta_l_prime = l2 - l1;
2085 let delta_c_prime = c2_prime - c1_prime;
2086
2087 let delta_h_prime = if c1_prime == 0.0 || c2_prime == 0.0 {
2088 0.0
2089 } else {
2090 let mut delta = h2_prime - h1_prime;
2091 if delta > 180.0 {
2092 delta -= 360.0;
2093 } else if delta < -180.0 {
2094 delta += 360.0;
2095 }
2096 delta
2097 };
2098 let delta_big_h_prime =
2099 2.0 * (c1_prime * c2_prime).sqrt() * degrees_to_radians(delta_h_prime / 2.0).sin();
2100
2101 let l_bar_prime = (l1 + l2) / 2.0;
2102 let c_bar_prime = (c1_prime + c2_prime) / 2.0;
2103 let h_bar_prime = if c1_prime == 0.0 || c2_prime == 0.0 {
2104 h1_prime + h2_prime
2105 } else if (h1_prime - h2_prime).abs() > 180.0 {
2106 if h1_prime + h2_prime < 360.0 {
2107 (h1_prime + h2_prime + 360.0) / 2.0
2108 } else {
2109 (h1_prime + h2_prime - 360.0) / 2.0
2110 }
2111 } else {
2112 (h1_prime + h2_prime) / 2.0
2113 };
2114
2115 let t = 1.0 - 0.17 * degrees_to_radians(h_bar_prime - 30.0).cos()
2116 + 0.24 * degrees_to_radians(2.0 * h_bar_prime).cos()
2117 + 0.32 * degrees_to_radians(3.0 * h_bar_prime + 6.0).cos()
2118 - 0.20 * degrees_to_radians(4.0 * h_bar_prime - 63.0).cos();
2119 let delta_theta = 30.0 * (-(((h_bar_prime - 275.0) / 25.0).powi(2))).exp();
2120 let c_bar_prime7 = c_bar_prime.powi(7);
2121 let r_c = 2.0 * (c_bar_prime7 / (c_bar_prime7 + 25_f64.powi(7))).sqrt();
2122 let s_l =
2123 1.0 + (0.015 * (l_bar_prime - 50.0).powi(2)) / (20.0 + (l_bar_prime - 50.0).powi(2)).sqrt();
2124 let s_c = 1.0 + 0.045 * c_bar_prime;
2125 let s_h = 1.0 + 0.015 * c_bar_prime * t;
2126 let r_t = -degrees_to_radians(2.0 * delta_theta).sin() * r_c;
2127
2128 let l_term = delta_l_prime / s_l;
2129 let c_term = delta_c_prime / s_c;
2130 let h_term = delta_big_h_prime / s_h;
2131
2132 (l_term * l_term + c_term * c_term + h_term * h_term + r_t * c_term * h_term).sqrt()
2133}
2134
2135pub fn get_cie_mesopic_adaptation(
2136 photopic_luminance: &[f64],
2137 scotopic_luminance: Option<&[f64]>,
2138 s_p_ratio: Option<&[f64]>,
2139) -> LuxResult<(Vec<f64>, Vec<f64>)> {
2140 if photopic_luminance.is_empty() {
2141 return Err(LuxError::EmptyInput);
2142 }
2143 if scotopic_luminance.is_some() == s_p_ratio.is_some() {
2144 return Err(LuxError::InvalidInput(
2145 "provide exactly one of scotopic_luminance or s_p_ratio",
2146 ));
2147 }
2148
2149 let len = photopic_luminance.len();
2150 if let Some(ls) = scotopic_luminance {
2151 if ls.len() != len {
2152 return Err(LuxError::MismatchedLengths {
2153 wavelengths: len,
2154 values: ls.len(),
2155 });
2156 }
2157 }
2158 if let Some(sp) = s_p_ratio {
2159 if sp.len() != len {
2160 return Err(LuxError::MismatchedLengths {
2161 wavelengths: len,
2162 values: sp.len(),
2163 });
2164 }
2165 }
2166
2167 let mut lmes = Vec::with_capacity(len);
2168 let mut m_values = Vec::with_capacity(len);
2169
2170 for index in 0..len {
2171 let lp = photopic_luminance[index];
2172 if !lp.is_finite() || lp <= 0.0 {
2173 return Err(LuxError::InvalidInput(
2174 "photopic luminance values must be finite and positive",
2175 ));
2176 }
2177
2178 let sp = if let Some(ls) = scotopic_luminance {
2179 let scotopic = ls[index];
2180 if !scotopic.is_finite() || scotopic < 0.0 {
2181 return Err(LuxError::InvalidInput(
2182 "scotopic luminance values must be finite and non-negative",
2183 ));
2184 }
2185 scotopic / lp
2186 } else {
2187 let ratio = s_p_ratio.unwrap()[index];
2188 if !ratio.is_finite() || ratio < 0.0 {
2189 return Err(LuxError::InvalidInput(
2190 "S/P ratio values must be finite and non-negative",
2191 ));
2192 }
2193 ratio
2194 };
2195
2196 let f_lmes = |m: f64| {
2197 ((m * lp) + (1.0 - m) * sp * 683.0 / 1699.0) / (m + (1.0 - m) * 683.0 / 1699.0)
2198 };
2199 let f_m = |m: f64| 0.767 + 0.3334 * f_lmes(m).log10();
2200
2201 let mut previous = 0.5;
2202 let mut current = f_m(previous);
2203 let mut iterations = 0;
2204 while (current - previous).abs() > 1e-12 && iterations < 100 {
2205 previous = current;
2206 current = f_m(previous);
2207 iterations += 1;
2208 }
2209
2210 lmes.push(f_lmes(current));
2211 m_values.push(current.clamp(0.0, 1.0));
2212 }
2213
2214 Ok((lmes, m_values))
2215}
2216
2217pub fn vlbar_cie_mesopic(
2218 m_values: &[f64],
2219 target_wavelengths: Option<&[f64]>,
2220) -> LuxResult<MesopicLuminousEfficiency> {
2221 if m_values.is_empty() {
2222 return Err(LuxError::EmptyInput);
2223 }
2224
2225 let photopic = Observer::Cie1931_2.vlbar()?.0;
2226 let wavelengths = photopic.wavelengths().to_vec();
2227 let scotopic = load_scotopic_vlbar_on(&wavelengths)?;
2228 let peak_index = wavelengths
2229 .iter()
2230 .position(|&wavelength| (wavelength - 555.0).abs() < 1e-12)
2231 .ok_or(LuxError::ParseError(
2232 "missing 555 nm in mesopic source data",
2233 ))?;
2234
2235 let mut curves = Vec::with_capacity(m_values.len());
2236 let mut k_mesopic = Vec::with_capacity(m_values.len());
2237
2238 for &m in m_values {
2239 let m = m.clamp(0.0, 1.0);
2240 let values: Vec<f64> = photopic
2241 .values()
2242 .iter()
2243 .zip(scotopic.values().iter())
2244 .map(|(vp, vs)| m * vp + (1.0 - m) * vs)
2245 .collect::<Vec<_>>();
2246
2247 let k = 683.0 / values[peak_index];
2248 curves.push(values);
2249 k_mesopic.push(k);
2250 }
2251
2252 let curves = Spectrum::new(wavelengths, curves)?;
2253 let curves = if let Some(target_wavelengths) = target_wavelengths {
2254 curves.cie_interp_linear(target_wavelengths, false)?
2255 } else {
2256 curves
2257 };
2258 let normalization =
2259 vec![crate::spectrum::SpectrumNormalization::Max(1.0); curves.spectrum_count()];
2260 let curves = curves.normalize_each(&normalization, None)?;
2261
2262 Ok(MesopicLuminousEfficiency { curves, k_mesopic })
2263}
2264
2265fn load_scotopic_vlbar_on(target_wavelengths: &[f64]) -> LuxResult<Spectrum> {
2266 let base = TristimulusObserver::from_csv(
2267 include_str!("../data/cmfs/ciexyz_1951_20_scotopic.dat"),
2268 1699.0,
2269 )?
2270 .vl_spectrum()?;
2271
2272 let source_wavelengths = base.wavelengths().to_vec();
2273 let interpolated = base.cie_interp_linear(target_wavelengths, false)?;
2274 let clipped = target_wavelengths
2275 .iter()
2276 .zip(interpolated.values().iter())
2277 .map(|(&wavelength, &value)| {
2278 if wavelength < source_wavelengths[0]
2279 || wavelength > source_wavelengths[source_wavelengths.len() - 1]
2280 || value.is_sign_negative()
2281 {
2282 0.0
2283 } else {
2284 value
2285 }
2286 })
2287 .collect::<Vec<_>>();
2288
2289 Spectrum::new(target_wavelengths.to_vec(), clipped)
2290}
2291
2292#[cfg(test)]
2293mod tests {
2294 use super::{
2295 cat_apply, cat_apply_context, cat_apply_mode, cat_apply_mode_with_conditions,
2296 cat_apply_with_conditions, cat_compile, cat_compile_context, cat_compile_mode,
2297 cat_compile_mode_with_conditions, cat_compile_with_conditions, cat_degree_of_adaptation,
2298 cat_mode_degrees_from_conditions, delta_e_cie76, delta_e_cie76_lab, delta_e_ciede2000,
2299 delta_e_ciede2000_lab, lab_to_xyz, CatAdapter, CatConditionPair, CatContext, CatMode,
2300 CatSurround, CatTransform, CatViewingConditions, Tristimulus,
2301 };
2302
2303 #[test]
2304 fn internal_lab_paths_match_xyz_paths() {
2305 let white = [95.047, 100.0, 108.883];
2306 let lab1 = [50.0, 2.6772, -79.7751];
2307 let lab2 = [50.0, 0.0, -82.7485];
2308 let xyz1 = lab_to_xyz(lab1, white);
2309 let xyz2 = lab_to_xyz(lab2, white);
2310 assert!((delta_e_cie76(xyz1, xyz2, white) - delta_e_cie76_lab(lab1, lab2)).abs() < 1e-12);
2311 assert!(
2312 (delta_e_ciede2000(xyz1, xyz2, white) - delta_e_ciede2000_lab(lab1, lab2)).abs()
2313 < 1e-12
2314 );
2315 }
2316
2317 #[test]
2318 fn applies_bradford_chromatic_adaptation() {
2319 let adapted = cat_apply(
2320 [19.01, 20.0, 21.78],
2321 [95.047, 100.0, 108.883],
2322 [109.85, 100.0, 35.585],
2323 CatTransform::Bradford,
2324 1.0,
2325 )
2326 .unwrap();
2327 assert!((adapted[0] - 21.970_203_102_921_214).abs() < 1e-12);
2328 assert!((adapted[1] - 19.999_901_615_516_674).abs() < 1e-12);
2329 assert!((adapted[2] - 7.118_055_791_689_174).abs() < 1e-12);
2330 }
2331
2332 #[test]
2333 fn applies_cat02_chromatic_adaptation() {
2334 let adapted = cat_apply(
2335 [19.01, 20.0, 21.78],
2336 [95.047, 100.0, 108.883],
2337 [109.85, 100.0, 35.585],
2338 CatTransform::Cat02,
2339 1.0,
2340 )
2341 .unwrap();
2342 assert!((adapted[0] - 21.970_153_635_389_728).abs() < 1e-12);
2343 assert!((adapted[1] - 19.999_847_882_170_943).abs() < 1e-12);
2344 assert!((adapted[2] - 7.118_149_458_933_564).abs() < 1e-12);
2345 }
2346
2347 #[test]
2348 fn applies_cat16_chromatic_adaptation() {
2349 let adapted = cat_apply(
2350 [19.01, 20.0, 21.78],
2351 [95.047, 100.0, 108.883],
2352 [109.85, 100.0, 35.585],
2353 CatTransform::Cat16,
2354 1.0,
2355 )
2356 .unwrap();
2357 assert!((adapted[0] - 21.970_301_223_531_525).abs() < 1e-12);
2358 assert!((adapted[1] - 20.000_021_021_038_33).abs() < 1e-12);
2359 assert!((adapted[2] - 7.118_208_448_159_319).abs() < 1e-12);
2360 }
2361
2362 #[test]
2363 fn applies_sharp_chromatic_adaptation() {
2364 let adapted = cat_apply(
2365 [19.01, 20.0, 21.78],
2366 [95.047, 100.0, 108.883],
2367 [109.85, 100.0, 35.585],
2368 CatTransform::Sharp,
2369 1.0,
2370 )
2371 .unwrap();
2372 assert!((adapted[0] - 21.970_337_526_821_627).abs() < 1e-12);
2373 assert!((adapted[1] - 19.999_953_773_116_744).abs() < 1e-12);
2374 assert!((adapted[2] - 7.118_112_433_644_779).abs() < 1e-12);
2375 }
2376
2377 #[test]
2378 fn applies_bianco_chromatic_adaptation() {
2379 let adapted = cat_apply(
2380 [19.01, 20.0, 21.78],
2381 [95.047, 100.0, 108.883],
2382 [109.85, 100.0, 35.585],
2383 CatTransform::Bianco,
2384 1.0,
2385 )
2386 .unwrap();
2387 assert!((adapted[0] - 21.970_237_997_793_32).abs() < 1e-12);
2388 assert!((adapted[1] - 19.999_886_291_132_23).abs() < 1e-12);
2389 assert!((adapted[2] - 7.118_132_338_899_736).abs() < 1e-12);
2390 }
2391
2392 #[test]
2393 fn applies_cmc_chromatic_adaptation() {
2394 let adapted = cat_apply(
2395 [19.01, 20.0, 21.78],
2396 [95.047, 100.0, 108.883],
2397 [109.85, 100.0, 35.585],
2398 CatTransform::Cmc,
2399 1.0,
2400 )
2401 .unwrap();
2402 assert!((adapted[0] - 21.970_245_614_232_79).abs() < 1e-12);
2403 assert!((adapted[1] - 19.999_939_194_170_24).abs() < 1e-12);
2404 assert!((adapted[2] - 7.118_164_955_975_901).abs() < 1e-12);
2405 }
2406
2407 #[test]
2408 fn applies_kries_chromatic_adaptation() {
2409 let adapted = cat_apply(
2410 [19.01, 20.0, 21.78],
2411 [95.047, 100.0, 108.883],
2412 [109.85, 100.0, 35.585],
2413 CatTransform::Kries,
2414 1.0,
2415 )
2416 .unwrap();
2417 assert!((adapted[0] - 21.970_131_711_895_99).abs() < 1e-12);
2418 assert!((adapted[1] - 19.999_997_693_482_22).abs() < 1e-12);
2419 assert!((adapted[2] - 7.118_111_183_564_011).abs() < 1e-12);
2420 }
2421
2422 #[test]
2423 fn applies_judd1945_chromatic_adaptation() {
2424 let adapted = cat_apply(
2425 [19.01, 20.0, 21.78],
2426 [95.047, 100.0, 108.883],
2427 [109.85, 100.0, 35.585],
2428 CatTransform::Judd1945,
2429 1.0,
2430 )
2431 .unwrap();
2432 assert!((adapted[0] - 21.970_117_638_953_994).abs() < 1e-12);
2433 assert!((adapted[1] - 20.0).abs() < 1e-12);
2434 assert!((adapted[2] - 7.118_111_183_564_01).abs() < 1e-12);
2435 }
2436
2437 #[test]
2438 fn applies_judd1945_cie016_chromatic_adaptation() {
2439 let adapted = cat_apply(
2440 [19.01, 20.0, 21.78],
2441 [95.047, 100.0, 108.883],
2442 [109.85, 100.0, 35.585],
2443 CatTransform::Judd1945Cie016,
2444 1.0,
2445 )
2446 .unwrap();
2447 assert!((adapted[0] - 21.970_113_747_706_712).abs() < 1e-12);
2448 assert!((adapted[1] - 20.0).abs() < 1e-12);
2449 assert!((adapted[2] - 7.118_111_183_564_01).abs() < 1e-12);
2450 }
2451
2452 #[test]
2453 fn applies_judd1935_chromatic_adaptation() {
2454 let adapted = cat_apply(
2455 [19.01, 20.0, 21.78],
2456 [95.047, 100.0, 108.883],
2457 [109.85, 100.0, 35.585],
2458 CatTransform::Judd1935,
2459 1.0,
2460 )
2461 .unwrap();
2462 assert!((adapted[0] - 21.970_394_658_200_817).abs() < 1e-12);
2463 assert!((adapted[1] - 20.000_197_359_403_444).abs() < 1e-12);
2464 assert!((adapted[2] - 7.118_111_183_564_01).abs() < 1e-12);
2465 }
2466
2467 #[test]
2468 fn rejects_invalid_adaptation_degree() {
2469 let err = cat_apply(
2470 [19.01, 20.0, 21.78],
2471 [95.047, 100.0, 108.883],
2472 [109.85, 100.0, 35.585],
2473 CatTransform::Bradford,
2474 1.5,
2475 )
2476 .unwrap_err();
2477 assert_eq!(
2478 err.to_string(),
2479 "invalid input: degree_of_adaptation must be finite and within 0..=1"
2480 );
2481 }
2482
2483 #[test]
2484 fn computes_degree_of_adaptation_for_average_surround() {
2485 let degree = cat_degree_of_adaptation(CatSurround::Average, 318.31).unwrap();
2486 assert!((degree - 0.994_468_780_088_437_4).abs() < 1e-12);
2487 }
2488
2489 #[test]
2490 fn computes_degree_of_adaptation_for_dim_surround() {
2491 let degree = cat_degree_of_adaptation(CatSurround::Dim, 20.0).unwrap();
2492 assert!((degree - 0.772_572_461_903_455_1).abs() < 1e-12);
2493 }
2494
2495 #[test]
2496 fn computes_degree_of_adaptation_for_dark_surround() {
2497 let degree = cat_degree_of_adaptation(CatSurround::Dark, 0.0).unwrap();
2498 assert!((degree - 0.659_225_947_140_2).abs() < 1e-12);
2499 }
2500
2501 #[test]
2502 fn computes_degree_of_adaptation_from_viewing_conditions() {
2503 let conditions = CatViewingConditions::new(CatSurround::Average, 318.31).unwrap();
2504 let degree = conditions.degree_of_adaptation().unwrap();
2505 assert!((degree - 0.994_468_780_088_437_4).abs() < 1e-12);
2506 }
2507
2508 #[test]
2509 fn applies_chromatic_adaptation_with_conditions() {
2510 let adapted = cat_apply_with_conditions(
2511 [19.01, 20.0, 21.78],
2512 [95.047, 100.0, 108.883],
2513 [109.85, 100.0, 35.585],
2514 CatTransform::Bradford,
2515 CatSurround::Average,
2516 318.31,
2517 )
2518 .unwrap();
2519 assert!((adapted[0] - 21.953_829_568_576_072).abs() < 1e-12);
2520 assert!((adapted[1] - 19.999_902_159_702_89).abs() < 1e-12);
2521 assert!((adapted[2] - 7.199_154_229_436_402).abs() < 1e-12);
2522 }
2523
2524 #[test]
2525 fn resolves_mode_degrees_from_viewing_conditions() {
2526 let source = CatViewingConditions::new(CatSurround::Average, 318.31).unwrap();
2527 let target = CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap();
2528 let degrees = cat_mode_degrees_from_conditions(CatMode::TwoStep, source, target).unwrap();
2529 assert!((degrees[0] - 0.994_468_780_088_437_4).abs() < 1e-12);
2530 assert!((degrees[1] - 0.772_572_461_903_455_1).abs() < 1e-12);
2531
2532 let target_only =
2533 cat_mode_degrees_from_conditions(CatMode::BaselineToTarget, source, target).unwrap();
2534 assert!((target_only[0] - 0.772_572_461_903_455_1).abs() < 1e-12);
2535 }
2536
2537 #[test]
2538 fn applies_mode_chromatic_adaptation_with_conditions() {
2539 let source = CatViewingConditions::new(CatSurround::Average, 318.31).unwrap();
2540 let target = CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap();
2541 let adapted = cat_apply_mode_with_conditions(
2542 [19.01, 20.0, 21.78],
2543 [95.047, 100.0, 108.883],
2544 [109.85, 100.0, 35.585],
2545 Some([100.0, 100.0, 100.0]),
2546 CatTransform::Bradford,
2547 CatMode::TwoStep,
2548 CatConditionPair::new(source, target),
2549 )
2550 .unwrap();
2551 let manual = cat_apply_mode(
2552 [19.01, 20.0, 21.78],
2553 [95.047, 100.0, 108.883],
2554 [109.85, 100.0, 35.585],
2555 Some([100.0, 100.0, 100.0]),
2556 CatTransform::Bradford,
2557 CatMode::TwoStep,
2558 [
2559 source.degree_of_adaptation().unwrap(),
2560 target.degree_of_adaptation().unwrap(),
2561 ],
2562 )
2563 .unwrap();
2564 assert_eq!(adapted, manual);
2565 }
2566
2567 #[test]
2568 fn baseline_to_target_mode_uses_target_conditions_degree() {
2569 let source = CatViewingConditions::new(CatSurround::Average, 318.31).unwrap();
2570 let target = CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap();
2571 let adapted = cat_apply_mode_with_conditions(
2572 [19.01, 20.0, 21.78],
2573 [95.047, 100.0, 108.883],
2574 [109.85, 100.0, 35.585],
2575 Some([100.0, 100.0, 100.0]),
2576 CatTransform::Bradford,
2577 CatMode::BaselineToTarget,
2578 CatConditionPair::new(source, target),
2579 )
2580 .unwrap();
2581 let manual = cat_apply_mode(
2582 [19.01, 20.0, 21.78],
2583 [95.047, 100.0, 108.883],
2584 [109.85, 100.0, 35.585],
2585 Some([100.0, 100.0, 100.0]),
2586 CatTransform::Bradford,
2587 CatMode::BaselineToTarget,
2588 [
2589 target.degree_of_adaptation().unwrap(),
2590 target.degree_of_adaptation().unwrap(),
2591 ],
2592 )
2593 .unwrap();
2594 assert_eq!(adapted, manual);
2595 }
2596
2597 #[test]
2598 fn context_exposes_default_baseline_and_mode_degrees() {
2599 let context = CatContext::new(
2600 [95.047, 100.0, 108.883],
2601 [109.85, 100.0, 35.585],
2602 None,
2603 CatTransform::Bradford,
2604 CatMode::TwoStep,
2605 CatViewingConditions::new(CatSurround::Average, 318.31).unwrap(),
2606 CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap(),
2607 );
2608 assert_eq!(context.baseline_white_or_default(), [100.0, 100.0, 100.0]);
2609 let degrees = context.degrees_of_adaptation().unwrap();
2610 assert!((degrees[0] - 0.994_468_780_088_437_4).abs() < 1e-12);
2611 assert!((degrees[1] - 0.772_572_461_903_455_1).abs() < 1e-12);
2612 }
2613
2614 #[test]
2615 fn applies_chromatic_adaptation_from_context() {
2616 let context = CatContext::new(
2617 [95.047, 100.0, 108.883],
2618 [109.85, 100.0, 35.585],
2619 Some([100.0, 100.0, 100.0]),
2620 CatTransform::Bradford,
2621 CatMode::TwoStep,
2622 CatViewingConditions::new(CatSurround::Average, 318.31).unwrap(),
2623 CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap(),
2624 );
2625 let adapted = cat_apply_context([19.01, 20.0, 21.78], context).unwrap();
2626 let manual = cat_apply_mode_with_conditions(
2627 [19.01, 20.0, 21.78],
2628 context.source_white,
2629 context.target_white,
2630 context.baseline_white,
2631 context.transform,
2632 context.mode,
2633 CatConditionPair::new(context.source_conditions, context.target_conditions),
2634 )
2635 .unwrap();
2636 assert_eq!(adapted, manual);
2637 }
2638
2639 #[test]
2640 fn compiled_adapter_matches_single_step_helper() {
2641 let adapter = CatAdapter::from_degree(
2642 [95.047, 100.0, 108.883],
2643 [109.85, 100.0, 35.585],
2644 CatTransform::Bradford,
2645 1.0,
2646 )
2647 .unwrap();
2648 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2649 let helper = cat_apply(
2650 [19.01, 20.0, 21.78],
2651 [95.047, 100.0, 108.883],
2652 [109.85, 100.0, 35.585],
2653 CatTransform::Bradford,
2654 1.0,
2655 )
2656 .unwrap();
2657 assert_eq!(adapted, helper);
2658 }
2659
2660 #[test]
2661 fn cat_compile_matches_single_step_helper() {
2662 let adapter = cat_compile(
2663 [95.047, 100.0, 108.883],
2664 [109.85, 100.0, 35.585],
2665 CatTransform::Bradford,
2666 1.0,
2667 )
2668 .unwrap();
2669 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2670 let helper = cat_apply(
2671 [19.01, 20.0, 21.78],
2672 [95.047, 100.0, 108.883],
2673 [109.85, 100.0, 35.585],
2674 CatTransform::Bradford,
2675 1.0,
2676 )
2677 .unwrap();
2678 assert_eq!(adapted, helper);
2679 }
2680
2681 #[test]
2682 fn compiled_mode_adapter_matches_mode_helper() {
2683 let adapter = CatAdapter::from_mode(
2684 [95.047, 100.0, 108.883],
2685 [109.85, 100.0, 35.585],
2686 Some([100.0, 100.0, 100.0]),
2687 CatTransform::Bradford,
2688 CatMode::TwoStep,
2689 [0.8, 0.6],
2690 )
2691 .unwrap();
2692 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2693 let helper = cat_apply_mode(
2694 [19.01, 20.0, 21.78],
2695 [95.047, 100.0, 108.883],
2696 [109.85, 100.0, 35.585],
2697 Some([100.0, 100.0, 100.0]),
2698 CatTransform::Bradford,
2699 CatMode::TwoStep,
2700 [0.8, 0.6],
2701 )
2702 .unwrap();
2703 assert_eq!(adapted, helper);
2704 }
2705
2706 #[test]
2707 fn cat_compile_mode_matches_mode_helper() {
2708 let adapter = cat_compile_mode(
2709 [95.047, 100.0, 108.883],
2710 [109.85, 100.0, 35.585],
2711 Some([100.0, 100.0, 100.0]),
2712 CatTransform::Bradford,
2713 CatMode::TwoStep,
2714 [0.8, 0.6],
2715 )
2716 .unwrap();
2717 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2718 let helper = cat_apply_mode(
2719 [19.01, 20.0, 21.78],
2720 [95.047, 100.0, 108.883],
2721 [109.85, 100.0, 35.585],
2722 Some([100.0, 100.0, 100.0]),
2723 CatTransform::Bradford,
2724 CatMode::TwoStep,
2725 [0.8, 0.6],
2726 )
2727 .unwrap();
2728 assert_eq!(adapted, helper);
2729 }
2730
2731 #[test]
2732 fn compiled_context_adapter_matches_context_helper() {
2733 let context = CatContext::new(
2734 [95.047, 100.0, 108.883],
2735 [109.85, 100.0, 35.585],
2736 Some([100.0, 100.0, 100.0]),
2737 CatTransform::Bradford,
2738 CatMode::TwoStep,
2739 CatViewingConditions::new(CatSurround::Average, 318.31).unwrap(),
2740 CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap(),
2741 );
2742 let adapter = CatAdapter::from_context(context).unwrap();
2743 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2744 let helper = cat_apply_context([19.01, 20.0, 21.78], context).unwrap();
2745 assert_eq!(adapted, helper);
2746 }
2747
2748 #[test]
2749 fn cat_compile_with_conditions_matches_helper() {
2750 let adapter = cat_compile_with_conditions(
2751 [95.047, 100.0, 108.883],
2752 [109.85, 100.0, 35.585],
2753 CatTransform::Bradford,
2754 CatSurround::Average,
2755 318.31,
2756 )
2757 .unwrap();
2758 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2759 let helper = cat_apply_with_conditions(
2760 [19.01, 20.0, 21.78],
2761 [95.047, 100.0, 108.883],
2762 [109.85, 100.0, 35.585],
2763 CatTransform::Bradford,
2764 CatSurround::Average,
2765 318.31,
2766 )
2767 .unwrap();
2768 assert_eq!(adapted, helper);
2769 }
2770
2771 #[test]
2772 fn cat_compile_mode_with_conditions_matches_helper() {
2773 let source = CatViewingConditions::new(CatSurround::Average, 318.31).unwrap();
2774 let target = CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap();
2775 let adapter = cat_compile_mode_with_conditions(
2776 [95.047, 100.0, 108.883],
2777 [109.85, 100.0, 35.585],
2778 Some([100.0, 100.0, 100.0]),
2779 CatTransform::Bradford,
2780 CatMode::TwoStep,
2781 source,
2782 target,
2783 )
2784 .unwrap();
2785 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2786 let helper = cat_apply_mode_with_conditions(
2787 [19.01, 20.0, 21.78],
2788 [95.047, 100.0, 108.883],
2789 [109.85, 100.0, 35.585],
2790 Some([100.0, 100.0, 100.0]),
2791 CatTransform::Bradford,
2792 CatMode::TwoStep,
2793 CatConditionPair::new(source, target),
2794 )
2795 .unwrap();
2796 assert_eq!(adapted, helper);
2797 }
2798
2799 #[test]
2800 fn cat_compile_context_matches_helper() {
2801 let context = CatContext::new(
2802 [95.047, 100.0, 108.883],
2803 [109.85, 100.0, 35.585],
2804 Some([100.0, 100.0, 100.0]),
2805 CatTransform::Bradford,
2806 CatMode::TwoStep,
2807 CatViewingConditions::new(CatSurround::Average, 318.31).unwrap(),
2808 CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap(),
2809 );
2810 let adapter = cat_compile_context(context).unwrap();
2811 let adapted = adapter.apply([19.01, 20.0, 21.78]).unwrap();
2812 let helper = cat_apply_context([19.01, 20.0, 21.78], context).unwrap();
2813 assert_eq!(adapted, helper);
2814 }
2815
2816 #[test]
2817 fn tristimulus_adapter_wrapper_matches_adapter() {
2818 let adapter = CatAdapter::from_degree(
2819 [95.047, 100.0, 108.883],
2820 [109.85, 100.0, 35.585],
2821 CatTransform::Bradford,
2822 1.0,
2823 )
2824 .unwrap();
2825 let adapted = Tristimulus::new(vec![[19.01, 20.0, 21.78]])
2826 .cat_apply_adapter(adapter)
2827 .unwrap()
2828 .into_vec();
2829 assert_eq!(adapted[0], adapter.apply([19.01, 20.0, 21.78]).unwrap());
2830 }
2831
2832 #[test]
2833 fn applies_two_step_bradford_chromatic_adaptation() {
2834 let adapted = cat_apply_mode(
2835 [19.01, 20.0, 21.78],
2836 [95.047, 100.0, 108.883],
2837 [109.85, 100.0, 35.585],
2838 Some([100.0, 100.0, 100.0]),
2839 CatTransform::Bradford,
2840 CatMode::TwoStep,
2841 [0.8, 0.6],
2842 )
2843 .unwrap();
2844 assert!((adapted[0] - 20.321_183_547_718_547).abs() < 1e-12);
2845 assert!((adapted[1] - 19.738_985_345_802_1).abs() < 1e-12);
2846 assert!((adapted[2] - 9.694_619_002_109_818).abs() < 1e-12);
2847 }
2848
2849 #[test]
2850 fn applies_two_step_cat16_chromatic_adaptation() {
2851 let adapted = cat_apply_mode(
2852 [19.01, 20.0, 21.78],
2853 [95.047, 100.0, 108.883],
2854 [109.85, 100.0, 35.585],
2855 Some([100.0, 100.0, 100.0]),
2856 CatTransform::Cat16,
2857 CatMode::TwoStep,
2858 [0.8, 0.6],
2859 )
2860 .unwrap();
2861 assert!((adapted[0] - 20.564_644_514_788_387).abs() < 1e-12);
2862 assert!((adapted[1] - 20.001_575_611_836_01).abs() < 1e-12);
2863 assert!((adapted[2] - 9.934_161_728_245_801).abs() < 1e-12);
2864 }
2865
2866 #[test]
2867 fn source_to_baseline_mode_matches_one_step_helper() {
2868 let adapted = cat_apply_mode(
2869 [19.01, 20.0, 21.78],
2870 [95.047, 100.0, 108.883],
2871 [109.85, 100.0, 35.585],
2872 Some([100.0, 100.0, 100.0]),
2873 CatTransform::Bradford,
2874 CatMode::SourceToBaseline,
2875 [1.0, 1.0],
2876 )
2877 .unwrap();
2878 let helper = cat_apply(
2879 [19.01, 20.0, 21.78],
2880 [95.047, 100.0, 108.883],
2881 [100.0, 100.0, 100.0],
2882 CatTransform::Bradford,
2883 1.0,
2884 )
2885 .unwrap();
2886 assert_eq!(adapted, helper);
2887 }
2888
2889 #[test]
2890 fn batch_cat_transforms_match_scalar_versions() {
2891 let xyz = [[19.01, 20.0, 21.78], [20.0, 21.0, 22.0]];
2892 let source_conditions = CatViewingConditions::new(CatSurround::Average, 318.31).unwrap();
2893 let target_conditions = CatViewingConditions::new(CatSurround::Dim, 20.0).unwrap();
2894 let context = CatContext::new(
2895 [95.047, 100.0, 108.883],
2896 [109.85, 100.0, 35.585],
2897 Some([100.0, 100.0, 100.0]),
2898 CatTransform::Bradford,
2899 CatMode::TwoStep,
2900 source_conditions,
2901 target_conditions,
2902 );
2903 let many = Tristimulus::new(xyz.to_vec())
2904 .cat_apply(
2905 [95.047, 100.0, 108.883],
2906 [109.85, 100.0, 35.585],
2907 CatTransform::Bradford,
2908 1.0,
2909 )
2910 .unwrap()
2911 .into_vec();
2912 assert_eq!(
2913 many,
2914 vec![
2915 cat_apply(
2916 xyz[0],
2917 [95.047, 100.0, 108.883],
2918 [109.85, 100.0, 35.585],
2919 CatTransform::Bradford,
2920 1.0
2921 )
2922 .unwrap(),
2923 cat_apply(
2924 xyz[1],
2925 [95.047, 100.0, 108.883],
2926 [109.85, 100.0, 35.585],
2927 CatTransform::Bradford,
2928 1.0
2929 )
2930 .unwrap()
2931 ]
2932 );
2933 let context_many = Tristimulus::new(xyz.to_vec())
2934 .cat_apply_context(context)
2935 .unwrap()
2936 .into_vec();
2937 assert_eq!(context_many[0], cat_apply_context(xyz[0], context).unwrap());
2938 let adapter = CatAdapter::from_context(context).unwrap();
2939 let adapter_many = Tristimulus::new(xyz.to_vec())
2940 .cat_apply_adapter(adapter)
2941 .unwrap()
2942 .into_vec();
2943 assert_eq!(adapter_many[0], adapter.apply(xyz[0]).unwrap());
2944 assert_eq!(adapter_many[1], adapter.apply(xyz[1]).unwrap());
2945 let conditioned = Tristimulus::new(xyz.to_vec())
2946 .cat_apply_with_conditions(
2947 [95.047, 100.0, 108.883],
2948 [109.85, 100.0, 35.585],
2949 CatTransform::Bradford,
2950 CatSurround::Average,
2951 318.31,
2952 )
2953 .unwrap()
2954 .into_vec();
2955 assert_eq!(
2956 conditioned[0],
2957 cat_apply_with_conditions(
2958 xyz[0],
2959 [95.047, 100.0, 108.883],
2960 [109.85, 100.0, 35.585],
2961 CatTransform::Bradford,
2962 CatSurround::Average,
2963 318.31
2964 )
2965 .unwrap()
2966 );
2967 let mode_many = Tristimulus::new(xyz.to_vec())
2968 .cat_apply_mode(
2969 [95.047, 100.0, 108.883],
2970 [109.85, 100.0, 35.585],
2971 Some([100.0, 100.0, 100.0]),
2972 CatTransform::Bradford,
2973 CatMode::TwoStep,
2974 [0.8, 0.6],
2975 )
2976 .unwrap()
2977 .into_vec();
2978 assert_eq!(
2979 mode_many[0],
2980 cat_apply_mode(
2981 xyz[0],
2982 [95.047, 100.0, 108.883],
2983 [109.85, 100.0, 35.585],
2984 Some([100.0, 100.0, 100.0]),
2985 CatTransform::Bradford,
2986 CatMode::TwoStep,
2987 [0.8, 0.6]
2988 )
2989 .unwrap()
2990 );
2991 let mode_conditioned = Tristimulus::new(xyz.to_vec())
2992 .cat_apply_mode_with_conditions(
2993 [95.047, 100.0, 108.883],
2994 [109.85, 100.0, 35.585],
2995 Some([100.0, 100.0, 100.0]),
2996 CatTransform::Bradford,
2997 CatMode::TwoStep,
2998 CatConditionPair::new(source_conditions, target_conditions),
2999 )
3000 .unwrap()
3001 .into_vec();
3002 assert_eq!(
3003 mode_conditioned[0],
3004 cat_apply_mode_with_conditions(
3005 xyz[0],
3006 [95.047, 100.0, 108.883],
3007 [109.85, 100.0, 35.585],
3008 Some([100.0, 100.0, 100.0]),
3009 CatTransform::Bradford,
3010 CatMode::TwoStep,
3011 CatConditionPair::new(source_conditions, target_conditions)
3012 )
3013 .unwrap()
3014 );
3015 }
3016}