Skip to main content

colr_types/
illuminant.rs

1//! CIE standard illuminants and reference white points.
2//!
3//! White point XYZ values are Y=1 normalized tristimulus. Values for the
4//! Cie1931 observer are from ASTM E308 and CIE 015:2018. Values for the
5//! Cie1964 observer are from CIE 015:2018, Table 1.
6//!
7//! ACES white point per Academy TB-2018-001 and SMPTE ST 2065-1:2021.
8//!
9//! SPD data is from CIE 015:2018 Tables T.1 and T.2, normalized to 1.0 at
10//! 560 nm. The `spd` const fn on each illuminant struct linearly interpolates
11//! the 5 nm source table onto any WavelengthGrid and can be used in statics.
12
13#![allow(clippy::excessive_precision)]
14
15use core::marker::PhantomData;
16
17use crate::model::WavelengthGrid;
18use crate::observer::{Cie1931, Cie1964, StandardObserver};
19
20/// A CIE standard illuminant defining a reference white point under a
21/// specific standard observer.
22///
23/// Integrating the same illuminant SPD against different observer CMFs yields
24/// different tristimulus values, so white point constants are observer-relative.
25pub trait Illuminant: 'static {
26    /// The standard observer these white point values are relative to.
27    type Observer: StandardObserver;
28
29    /// xy chromaticity of the reference white under this observer.
30    const WHITE_POINT_XY: [f32; 2];
31
32    /// XYZ reference white normalized to Y = 1 under this observer.
33    const WHITE_POINT_XYZ: [f32; 3];
34}
35
36/// CIE standard illuminant D65 (~6504 K) under observer O.
37///
38/// Reference white for sRGB (IEC 61966-2-1), Rec. 709, Display P3, and
39/// Rec. 2020 (ITU-R BT.2020).
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct D65<O: StandardObserver = Cie1931>(PhantomData<O>);
42
43impl Illuminant for D65<Cie1931> {
44    type Observer = Cie1931;
45    const WHITE_POINT_XY: [f32; 2] = [0.3127, 0.3290];
46    const WHITE_POINT_XYZ: [f32; 3] = [0.95047, 1.00000, 1.08883];
47}
48
49impl Illuminant for D65<Cie1964> {
50    type Observer = Cie1964;
51    /// CIE 015:2018 Table 1.
52    const WHITE_POINT_XY: [f32; 2] = [0.3138, 0.3310];
53    /// CIE 015:2018 Table 1.
54    const WHITE_POINT_XYZ: [f32; 3] = [0.94811, 1.00000, 1.07304];
55}
56
57impl D65<Cie1931> {
58    /// CIE D65 relative SPD at 5 nm, 380-780 nm (81 bands), normalized to
59    /// 1.0 at 560 nm. Source: CIE 015:2018 Table T.2.
60    #[rustfmt::skip]
61    pub const SPD_5NM: &'static [f32; 81] = &[
62        0.499755, 0.523118, 0.546482, 0.687015, 0.827549, 0.871204, 0.914860, 0.924589, 0.934318, 0.900570,
63        0.866823, 0.957736, 1.048650, 1.109360, 1.170080, 1.174100, 1.178120, 1.163360, 1.148610, 1.153920,
64        1.159230, 1.123670, 1.088110, 1.090820, 1.093540, 1.085780, 1.078020, 1.062960, 1.047900, 1.062390,
65        1.076890, 1.060470, 1.044050, 1.042250, 1.040460, 1.020230, 1.000000, 0.981671, 0.963342, 0.960611,
66        0.957880, 0.922368, 0.886856, 0.893459, 0.900062, 0.898026, 0.895991, 0.886489, 0.876987, 0.854936,
67        0.832886, 0.834939, 0.836992, 0.818630, 0.800268, 0.801207, 0.802146, 0.812462, 0.822778, 0.802810,
68        0.782842, 0.740027, 0.697213, 0.706652, 0.716091, 0.729790, 0.743490, 0.679765, 0.616040, 0.657448,
69        0.698856, 0.724862, 0.750869, 0.693398, 0.635927, 0.550054, 0.464182, 0.566118, 0.668054, 0.650942,
70        0.633830,
71    ];
72
73    /// Sample the D65 SPD onto wavelength grid `G`.
74    ///
75    /// Linearly interpolates the 5 nm source table. Bands outside 380-780 nm
76    /// are zero. The result is suitable for use in a `static` initializer.
77    pub const fn spd<const BANDS: usize, G: WavelengthGrid<BANDS>>() -> [f32; BANDS] {
78        let mut out = [0.0f32; BANDS];
79        let mut i = 0;
80        while i < BANDS {
81            let nm = G::START_NM + i as f32 * G::STEP_NM;
82            if nm >= 380.0 && nm <= 780.0 {
83                let t = (nm - 380.0) / 5.0;
84                let lo = t as usize;
85                let hi = if lo < 80 { lo + 1 } else { 80 };
86                let frac = t - lo as f32;
87                out[i] = Self::SPD_5NM[lo] * (1.0 - frac) + Self::SPD_5NM[hi] * frac;
88            }
89            i += 1;
90        }
91        out
92    }
93}
94
95/// CIE standard illuminant D50 (~5003 K) under observer O.
96///
97/// D50 is the reference white for the ICC profile connection space
98/// (ICC.1:2022) and ProPhoto RGB (ROMM RGB, ISO 22028-2), both defined
99/// under Cie1931. Use `D50<Cie1964>` for surface color evaluation.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub struct D50<O: StandardObserver = Cie1931>(PhantomData<O>);
102
103impl Illuminant for D50<Cie1931> {
104    type Observer = Cie1931;
105    const WHITE_POINT_XY: [f32; 2] = [0.3457, 0.3585];
106    const WHITE_POINT_XYZ: [f32; 3] = [0.96422, 1.00000, 0.82521];
107}
108
109impl Illuminant for D50<Cie1964> {
110    type Observer = Cie1964;
111    /// CIE 015:2018 Table 1.
112    const WHITE_POINT_XY: [f32; 2] = [0.3477, 0.3595];
113    /// CIE 015:2018 Table 1.
114    const WHITE_POINT_XYZ: [f32; 3] = [0.96720, 1.00000, 0.81427];
115}
116
117impl D50<Cie1931> {
118    /// CIE D50 relative SPD at 5 nm, 380-780 nm (81 bands), normalized to
119    /// 1.0 at 560 nm. Source: CIE 015:2018 Table T.1.
120    #[rustfmt::skip]
121    pub const SPD_5NM: &'static [f32; 81] = &[
122        0.266024, 0.314300, 0.333033, 0.443867, 0.588167, 0.664700, 0.741233, 0.771244, 0.801267, 0.769711,
123        0.738156, 0.865411, 0.992667, 1.056422, 1.120189, 1.116278, 1.112367, 1.127200, 1.142033, 1.139233,
124        1.136433, 1.111322, 1.086211, 1.089200, 1.092189, 1.083844, 1.075489, 1.064311, 1.053144, 1.067956,
125        1.082767, 1.064311, 1.045833, 1.049189, 1.052544, 1.026278, 1.000000, 0.994489, 0.988978, 0.986311,
126        0.983644, 0.949211, 0.914778, 0.924544, 0.934311, 0.926900, 0.919478, 0.909322, 0.899167, 0.877911,
127        0.856656, 0.858400, 0.860144, 0.844711, 0.829278, 0.829922, 0.830567, 0.839733, 0.848900, 0.830722,
128        0.812533, 0.770511, 0.728478, 0.738467, 0.748456, 0.763511, 0.778578, 0.715111, 0.651644, 0.703344,
129        0.755044, 0.791544, 0.828056, 0.765333, 0.702611, 0.611144, 0.519689, 0.638233, 0.756789, 0.738189,
130        0.719600,
131    ];
132
133    /// Sample the D50 SPD onto wavelength grid `G`.
134    ///
135    /// Linearly interpolates the 5 nm source table. Bands outside 380-780 nm
136    /// are zero. The result is suitable for use in a `static` initializer.
137    pub const fn spd<const BANDS: usize, G: WavelengthGrid<BANDS>>() -> [f32; BANDS] {
138        let mut out = [0.0f32; BANDS];
139        let mut i = 0;
140        while i < BANDS {
141            let nm = G::START_NM + i as f32 * G::STEP_NM;
142            if nm >= 380.0 && nm <= 780.0 {
143                let t = (nm - 380.0) / 5.0;
144                let lo = t as usize;
145                let hi = if lo < 80 { lo + 1 } else { 80 };
146                let frac = t - lo as f32;
147                out[i] = Self::SPD_5NM[lo] * (1.0 - frac) + Self::SPD_5NM[hi] * frac;
148            }
149            i += 1;
150        }
151        out
152    }
153}
154
155/// CIE standard illuminant D60 (~6004 K) under observer O.
156///
157/// A true CIE daylight illuminant at 6000 K nominal CCT. Distinct from
158/// the ACES white point. See [`AcesWhitePoint`].
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
160pub struct D60<O: StandardObserver = Cie1931>(PhantomData<O>);
161
162impl Illuminant for D60<Cie1931> {
163    type Observer = Cie1931;
164    const WHITE_POINT_XY: [f32; 2] = [0.32163, 0.33774];
165    const WHITE_POINT_XYZ: [f32; 3] = [0.95230, 1.00000, 1.00856];
166}
167
168impl Illuminant for D60<Cie1964> {
169    type Observer = Cie1964;
170    /// CIE 015:2018 Table 1.
171    const WHITE_POINT_XY: [f32; 2] = [0.3223, 0.3348];
172    /// CIE 015:2018 Table 1.
173    const WHITE_POINT_XYZ: [f32; 3] = [0.95002, 1.00000, 1.00350];
174}
175
176/// DCI white point for theatrical projection (SMPTE EG 432-1).
177///
178/// Not a CIE D-series illuminant. Defined in Cie1931 XYZ. Used only for
179/// theatrical DCI-P3 projection; consumer Display P3 uses D65 instead.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
181pub struct DciWhite;
182
183impl Illuminant for DciWhite {
184    type Observer = Cie1931;
185    const WHITE_POINT_XY: [f32; 2] = [0.3140, 0.3510];
186    const WHITE_POINT_XYZ: [f32; 3] = [0.89459, 1.00000, 0.95442];
187}
188
189/// ACES white point per SMPTE ST 2065-1:2021 and Academy TB-2018-001.
190///
191/// Commonly called "D60" but is not a true CIE D-series illuminant. Defined
192/// in Cie1931 XYZ. Use [`D60`] for the true CIE D60 illuminant.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194pub struct AcesWhitePoint;
195
196impl Illuminant for AcesWhitePoint {
197    type Observer = Cie1931;
198    const WHITE_POINT_XY: [f32; 2] = [0.32168, 0.33767];
199    const WHITE_POINT_XYZ: [f32; 3] = [0.95265, 1.00000, 1.00883];
200}