Skip to main content

rawshift_image/data/
wb.rs

1//! Standard illuminant reference data.
2//!
3//! CIE standard illuminants used for white balance and color matrix
4//! interpolation. Values are from CIE publications and the DNG specification.
5
6/// Standard illuminant specification.
7#[derive(Debug, Clone, Copy)]
8pub struct Illuminant {
9    /// Human-readable name
10    pub name: &'static str,
11    /// EXIF LightSource tag value
12    pub exif_code: u16,
13    /// Correlated Color Temperature in Kelvin
14    pub cct: u32,
15    /// CIE 1931 chromaticity x coordinate
16    pub x: f64,
17    /// CIE 1931 chromaticity y coordinate
18    pub y: f64,
19}
20
21impl Illuminant {
22    /// Compute the XYZ tristimulus values (Y=1 normalized).
23    pub fn xyz(&self) -> [f64; 3] {
24        let big_x = self.x / self.y;
25        let big_z = (1.0 - self.x - self.y) / self.y;
26        [big_x, 1.0, big_z]
27    }
28}
29
30/// Standard Illuminant A — incandescent/tungsten (~2856K).
31pub const ILLUMINANT_A: Illuminant = Illuminant {
32    name: "Standard Illuminant A",
33    exif_code: 17,
34    cct: 2856,
35    x: 0.44757,
36    y: 0.40745,
37};
38
39/// CIE Illuminant D50 — ICC profile connection space (~5003K).
40pub const D50: Illuminant = Illuminant {
41    name: "D50",
42    exif_code: 23,
43    cct: 5003,
44    x: 0.34567,
45    y: 0.35850,
46};
47
48/// CIE Illuminant D55 (~5503K).
49pub const D55: Illuminant = Illuminant {
50    name: "D55",
51    exif_code: 20,
52    cct: 5503,
53    x: 0.33242,
54    y: 0.34743,
55};
56
57/// CIE Illuminant D65 — daylight reference (~6504K).
58pub const D65: Illuminant = Illuminant {
59    name: "D65",
60    exif_code: 21,
61    cct: 6504,
62    x: 0.31271,
63    y: 0.32902,
64};
65
66/// CIE Illuminant D75 (~7504K).
67pub const D75: Illuminant = Illuminant {
68    name: "D75",
69    exif_code: 22,
70    cct: 7504,
71    x: 0.29902,
72    y: 0.31485,
73};
74
75/// Cool White Fluorescent (~4150K).
76pub const COOL_WHITE_FLUORESCENT: Illuminant = Illuminant {
77    name: "Cool White Fluorescent",
78    exif_code: 15,
79    cct: 4150,
80    x: 0.37510,
81    y: 0.36714,
82};
83
84/// All standard illuminants in the database.
85static ILLUMINANTS: &[Illuminant] = &[ILLUMINANT_A, COOL_WHITE_FLUORESCENT, D50, D55, D65, D75];
86
87/// Look up an illuminant by its EXIF LightSource code.
88pub fn from_exif_code(code: u16) -> Option<&'static Illuminant> {
89    ILLUMINANTS.iter().find(|i| i.exif_code == code)
90}
91
92/// Look up an illuminant by name (case-insensitive).
93pub fn from_name(name: &str) -> Option<&'static Illuminant> {
94    let lower = name.to_lowercase();
95    ILLUMINANTS.iter().find(|i| i.name.to_lowercase() == lower)
96}
97
98/// Find the closest standard illuminant to a given CCT.
99pub fn nearest_by_cct(cct: u32) -> &'static Illuminant {
100    ILLUMINANTS
101        .iter()
102        .min_by_key(|i| (i.cct as i64 - cct as i64).unsigned_abs())
103        .expect("illuminant table is non-empty")
104}
105
106/// Returns all standard illuminants.
107pub fn all_illuminants() -> &'static [Illuminant] {
108    ILLUMINANTS
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_d65_values() {
117        assert_eq!(D65.cct, 6504);
118        assert!((D65.x - 0.31271).abs() < 1e-5);
119        assert!((D65.y - 0.32902).abs() < 1e-5);
120    }
121
122    #[test]
123    fn test_d50_xyz() {
124        let xyz = D50.xyz();
125        // D50: X=0.9642, Y=1.0, Z=0.8251 (approximately)
126        assert!((xyz[0] - 0.9642).abs() < 0.01);
127        assert!((xyz[1] - 1.0).abs() < 1e-10);
128        assert!((xyz[2] - 0.8251).abs() < 0.01);
129    }
130
131    #[test]
132    fn test_exif_lookup() {
133        let d65 = from_exif_code(21).unwrap();
134        assert_eq!(d65.name, "D65");
135
136        let std_a = from_exif_code(17).unwrap();
137        assert_eq!(std_a.name, "Standard Illuminant A");
138    }
139
140    #[test]
141    fn test_unknown_exif_code() {
142        assert!(from_exif_code(255).is_none());
143    }
144
145    #[test]
146    fn test_nearest_cct() {
147        let nearest = nearest_by_cct(6500);
148        assert_eq!(nearest.name, "D65");
149
150        let nearest = nearest_by_cct(3000);
151        assert_eq!(nearest.name, "Standard Illuminant A");
152
153        let nearest = nearest_by_cct(5000);
154        assert_eq!(nearest.name, "D50");
155    }
156
157    #[test]
158    fn test_all_illuminants() {
159        let all = all_illuminants();
160        assert_eq!(all.len(), 6);
161    }
162}