1use crate::{Illuminant, Observer};
2
3pub const X_BAR_2: [f32; 41] = [
5 0.0014, 0.0042, 0.0143, 0.0435, 0.1344, 0.2839, 0.3483, 0.3362, 0.2908, 0.1954, 0.0956, 0.0320,
6 0.0049, 0.0093, 0.0633, 0.1655, 0.2904, 0.4334, 0.5945, 0.7621, 0.9163, 1.0263, 1.0622, 1.0026,
7 0.8524, 0.6424, 0.4479, 0.2835, 0.1649, 0.0874, 0.0468, 0.0227, 0.0114, 0.0058, 0.0029, 0.0014,
8 0.00069, 0.00033, 0.00017, 0.00008, 0.00004,
9];
10
11pub const Y_BAR_2: [f32; 41] = [
12 0.0000, 0.0001, 0.0004, 0.0012, 0.0040, 0.0116, 0.0230, 0.0380, 0.0600, 0.0910, 0.1390, 0.2080,
13 0.3230, 0.5030, 0.7100, 0.8620, 0.9540, 0.9950, 0.9950, 0.9520, 0.8700, 0.7570, 0.6310, 0.5030,
14 0.3810, 0.2650, 0.1750, 0.1070, 0.0610, 0.0320, 0.0170, 0.0082, 0.0041, 0.0021, 0.0010,
15 0.00052, 0.00025, 0.00012, 0.00006, 0.00003, 0.00001,
16];
17
18pub const Z_BAR_2: [f32; 41] = [
19 0.0065, 0.0201, 0.0679, 0.2074, 0.6456, 1.3856, 1.7471, 1.7721, 1.5794, 1.1143, 0.5701, 0.1970,
20 0.0415, 0.0052, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
21 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
22 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
23];
24
25#[expect(
27 clippy::approx_constant,
28 reason = "Standard CIE constant with prescribed precision"
29)]
30pub const X_BAR_10: [f32; 41] = [
31 0.0002, 0.0011, 0.0061, 0.0315, 0.1241, 0.3023, 0.5045, 0.6931, 0.8177, 0.7530, 0.5314, 0.3345,
32 0.1570, 0.0538, 0.0331, 0.1117, 0.2230, 0.4243, 0.6627, 0.8690, 1.0107, 1.0743, 1.0257, 0.8724,
33 0.6553, 0.4456, 0.2800, 0.1622, 0.0869, 0.0434, 0.0218, 0.0107, 0.0053, 0.0026, 0.0013, 0.0006,
34 0.0003, 0.0001, 0.0000, 0.0000, 0.0000,
35];
36pub const Y_BAR_10: [f32; 41] = [
37 0.0000, 0.0000, 0.0002, 0.0010, 0.0041, 0.0105, 0.0207, 0.0407, 0.0702, 0.1120, 0.1852, 0.2904,
38 0.4190, 0.5764, 0.7435, 0.8872, 0.9666, 0.9983, 0.9873, 0.9331, 0.8420, 0.7163, 0.5596, 0.4203,
39 0.3021, 0.2003, 0.1245, 0.0713, 0.0380, 0.0189, 0.0094, 0.0046, 0.0023, 0.0111, 0.0006, 0.0003,
40 0.0001, 0.0000, 0.0000, 0.0000, 0.0000,
41];
42pub const Z_BAR_10: [f32; 41] = [
43 0.0007, 0.0045, 0.0259, 0.1343, 0.5285, 1.3003, 2.1932, 3.0334, 3.5534, 3.2392, 2.2235, 1.3400,
44 0.5752, 0.1866, 0.0427, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
45 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
46 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
47];
48
49pub const L_BAR_2015: [f32; 41] = [
55 0.0001, 0.0004, 0.0019, 0.0084, 0.0292, 0.0544, 0.0652, 0.0660, 0.0536, 0.0336, 0.0253, 0.0435,
56 0.0906, 0.1834, 0.3541, 0.5363, 0.7024, 0.8358, 0.9328, 0.9859, 1.0000, 0.9575, 0.8524, 0.7081,
57 0.5480, 0.3952, 0.2644, 0.1651, 0.0967, 0.0538, 0.0284, 0.0143, 0.0068, 0.0031, 0.0014, 0.0006,
58 0.0003, 0.0001, 0.0001, 0.0000, 0.0000,
59];
60pub const M_BAR_2015: [f32; 41] = [
61 0.0000, 0.0001, 0.0006, 0.0028, 0.0121, 0.0298, 0.0450, 0.0526, 0.0519, 0.0440, 0.0494, 0.0772,
62 0.1345, 0.2319, 0.3802, 0.5312, 0.6724, 0.7974, 0.8926, 0.9515, 0.9757, 0.9592, 0.8995, 0.7963,
63 0.6621, 0.5134, 0.3698, 0.2486, 0.1557, 0.0917, 0.0511, 0.0270, 0.0135, 0.0064, 0.0030, 0.0013,
64 0.0006, 0.0003, 0.0001, 0.0001, 0.0000,
65];
66pub const S_BAR_2015: [f32; 41] = [
67 0.0019, 0.0101, 0.0469, 0.1648, 0.4449, 0.8443, 0.9930, 0.8970, 0.6171, 0.3392, 0.1505, 0.0532,
68 0.1042, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
69 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
70 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
71];
72
73pub mod illuminant {
76 use super::XYZ;
77
78 pub const D50: XYZ = XYZ {
81 x: 0.96422,
82 y: 1.0,
83 z: 0.82521,
84 };
85 pub const D55: XYZ = XYZ {
87 x: 0.95682,
88 y: 1.0,
89 z: 0.92149,
90 };
91 pub const D65: XYZ = XYZ {
93 x: 0.95047,
94 y: 1.0,
95 z: 1.08883,
96 };
97 pub const D75: XYZ = XYZ {
99 x: 0.94972,
100 y: 1.0,
101 z: 1.22638,
102 };
103 pub const A: XYZ = XYZ {
105 x: 1.09850,
106 y: 1.0,
107 z: 0.35585,
108 };
109 pub const F2: XYZ = XYZ {
111 x: 0.99186,
112 y: 1.0,
113 z: 0.67393,
114 };
115 pub const F7: XYZ = XYZ {
117 x: 0.95041,
118 y: 1.0,
119 z: 1.08747,
120 };
121 pub const F11: XYZ = XYZ {
123 x: 1.00962,
124 y: 1.0,
125 z: 0.64350,
126 };
127
128 pub const D50_10: XYZ = XYZ {
131 x: 0.96720,
132 y: 1.0,
133 z: 0.81427,
134 };
135 pub const D55_10: XYZ = XYZ {
137 x: 0.95799,
138 y: 1.0,
139 z: 0.90926,
140 };
141 pub const D65_10: XYZ = XYZ {
143 x: 0.94811,
144 y: 1.0,
145 z: 1.07304,
146 };
147 pub const D75_10: XYZ = XYZ {
149 x: 0.94416,
150 y: 1.0,
151 z: 1.20641,
152 };
153 pub const A_10: XYZ = XYZ {
155 x: 1.11144,
156 y: 1.0,
157 z: 0.35200,
158 };
159
160 pub mod led {
163 use super::super::XYZ;
164 pub const B1: XYZ = XYZ {
166 x: 1.0967,
167 y: 1.0,
168 z: 0.3533,
169 };
170 pub const B3: XYZ = XYZ {
172 x: 1.0031,
173 y: 1.0,
174 z: 0.5361,
175 };
176 pub const B5: XYZ = XYZ {
178 x: 0.9482,
179 y: 1.0,
180 z: 1.0642,
181 };
182 pub const BH1: XYZ = XYZ {
184 x: 1.0824,
185 y: 1.0,
186 z: 0.3592,
187 };
188 }
189
190 pub const D55_2: XYZ = D55;
192 pub const D65_2: XYZ = D65;
193
194 pub mod spd {
196 pub const A: [f32; 41] = [
198 9.80, 12.09, 14.71, 17.68, 21.00, 24.67, 28.70, 33.09, 37.81, 42.87, 48.24, 53.91,
199 59.86, 66.06, 72.50, 79.13, 85.95, 92.91, 100.00, 107.18, 114.44, 121.73, 129.04,
200 136.35, 143.62, 150.84, 157.98, 165.03, 171.96, 178.77, 185.43, 191.93, 198.26, 204.41,
201 210.37, 216.12, 221.67, 227.00, 232.12, 237.01, 241.68,
202 ];
203
204 pub const D50: [f32; 41] = [
206 24.49, 29.87, 49.31, 56.51, 60.03, 57.82, 74.82, 87.25, 90.61, 91.37, 95.11, 91.96,
207 95.72, 96.61, 97.13, 102.10, 100.75, 102.32, 100.00, 97.74, 98.92, 93.50, 97.69, 99.27,
208 99.04, 95.72, 98.86, 95.67, 98.19, 103.00, 99.13, 87.38, 91.60, 92.89, 76.85, 86.51,
209 92.58, 78.23, 57.69, 82.92, 78.27,
210 ];
211
212 pub const D65: [f32; 41] = [
214 49.98, 54.65, 82.75, 91.49, 93.43, 86.68, 104.87, 117.01, 117.81, 114.86, 115.92,
215 108.81, 109.35, 107.80, 104.79, 107.69, 104.41, 104.05, 100.00, 96.33, 95.79, 88.69,
216 90.01, 89.60, 87.70, 83.29, 83.70, 80.03, 80.21, 82.28, 78.28, 69.72, 71.61, 74.35,
217 61.60, 69.89, 75.09, 63.59, 46.42, 66.81, 63.38,
218 ];
219
220 pub const F2: [f32; 41] = [
222 1.87, 2.94, 5.17, 6.13, 7.01, 8.56, 43.67, 16.94, 11.35, 12.37, 13.00, 13.23, 13.13,
223 12.52, 11.83, 11.22, 11.03, 11.53, 27.74, 17.05, 14.33, 15.52, 19.55, 14.91, 13.22,
224 11.12, 8.95, 7.02, 5.42, 4.15, 3.20, 2.47, 1.93, 1.67, 1.29, 1.08, 0.88, 0.77, 0.73,
225 0.69, 0.68,
226 ];
227
228 pub const F7: [f32; 41] = [
230 1.87, 2.92, 5.10, 6.00, 6.85, 8.31, 40.76, 16.06, 10.91, 11.83, 12.40, 12.58, 12.47,
231 11.89, 11.33, 10.96, 11.16, 12.12, 27.78, 17.73, 15.20, 16.10, 19.50, 14.64, 12.69,
232 10.45, 8.29, 6.41, 4.90, 3.72, 2.83, 2.19, 1.71, 1.43, 1.13, 0.96, 0.78, 0.68, 0.65,
233 0.62, 0.62,
234 ];
235
236 pub const F11: [f32; 41] = [
238 1.87, 2.35, 2.92, 3.45, 5.10, 18.91, 6.00, 6.11, 6.85, 7.58, 8.31, 40.76, 16.06, 10.32,
239 10.91, 11.40, 11.83, 12.17, 12.40, 12.54, 12.58, 12.52, 12.47, 12.20, 11.89, 11.61,
240 11.33, 11.10, 10.96, 10.97, 11.16, 11.54, 12.12, 27.78, 17.73, 14.47, 15.20, 15.77,
241 16.10, 18.54, 19.50,
242 ];
243 }
244}
245
246impl Illuminant {
247 pub fn get_spd(&self) -> &'static [f32; 41] {
249 match self {
250 Illuminant::A => &illuminant::spd::A,
251 Illuminant::D50 => &illuminant::spd::D50,
252 Illuminant::D55 => &illuminant::spd::D65, Illuminant::D65 => &illuminant::spd::D65,
254 Illuminant::D75 => &illuminant::spd::D65, Illuminant::F2 => &illuminant::spd::F2,
256 Illuminant::F7 => &illuminant::spd::F7,
257 Illuminant::F11 => &illuminant::spd::F11,
258 }
259 }
260
261 pub fn get_white_point(&self, observer: Observer) -> XYZ {
262 match observer {
263 Observer::CIE1931_2 => match self {
264 Illuminant::D50 => illuminant::D50,
265 Illuminant::D55 => illuminant::D55,
266 Illuminant::D65 => illuminant::D65,
267 Illuminant::D75 => illuminant::D75,
268 Illuminant::A => illuminant::A,
269 Illuminant::F2 => illuminant::F2,
270 Illuminant::F7 => illuminant::F7,
271 Illuminant::F11 => illuminant::F11,
272 },
273 Observer::CIE1964_10 => match self {
274 Illuminant::D50 => illuminant::D50_10,
275 Illuminant::D55 => illuminant::D55_10,
276 Illuminant::D65 => illuminant::D65_10,
277 Illuminant::D75 => illuminant::D75_10,
278 Illuminant::A => illuminant::A_10,
279 Illuminant::F2 => illuminant::F2,
281 Illuminant::F7 => illuminant::F7,
282 Illuminant::F11 => illuminant::F11,
283 },
284 }
285 }
286}
287
288impl Observer {
289 pub fn get_cmfs(&self) -> (&'static [f32; 41], &'static [f32; 41], &'static [f32; 41]) {
290 match self {
291 Observer::CIE1931_2 => (&X_BAR_2, &Y_BAR_2, &Z_BAR_2),
292 Observer::CIE1964_10 => (&X_BAR_10, &Y_BAR_10, &Z_BAR_10),
293 }
294 }
295}
296
297#[rustfmt::skip]
301pub mod weighting {
302 pub const WX_D65_2_10: [f32; 41] = [
304 0.007, 0.022, 0.112, 0.377, 1.188, 2.330, 3.459, 3.724, 3.243, 2.126,
305 1.049, 0.330, 0.051, 0.095, 0.628, 1.687, 2.870, 4.267, 5.628, 6.948,
306 8.310, 8.618, 9.050, 8.505, 7.077, 5.066, 3.549, 2.147, 1.252, 0.681,
307 0.347, 0.150, 0.077, 0.041, 0.017, 0.009, 0.005, 0.002, 0.001, 0.001,
308 0.000
309 ];
310
311 pub const WY_D65_2_10: [f32; 41] = [
313 0.000, 0.001, 0.003, 0.010, 0.035, 0.095, 0.228, 0.421, 0.669, 0.989,
314 1.524, 2.141, 3.344, 5.131, 7.041, 8.785, 9.425, 9.792, 9.416, 8.675,
315 7.887, 6.354, 5.374, 4.265, 3.162, 2.089, 1.386, 0.810, 0.463, 0.249,
316 0.126, 0.054, 0.028, 0.015, 0.006, 0.003, 0.002, 0.001, 0.000, 0.000,
317 0.000
318 ];
319
320 pub const WZ_D65_2_10: [f32; 41] = [
322 0.035, 0.119, 0.610, 2.059, 6.541, 13.031, 19.880, 22.491, 20.182, 13.888,
323 7.167, 2.325, 0.492, 0.061, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
324 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
325 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
326 0.000
327 ];
328
329 pub const SUM_WY_D65_2_10: f32 = 100.000;
332
333 pub const WX_D50_2_10: [f32; 41] = [
338 0.003, 0.012, 0.067, 0.235, 0.770, 1.566, 2.483, 2.794, 2.510, 1.700,
339 0.866, 0.280, 0.045, 0.086, 0.585, 1.608, 2.785, 4.221, 5.659, 7.090,
340 8.627, 9.137, 9.882, 9.480, 8.042, 5.858, 4.219, 2.585, 1.543, 0.858,
341 0.442, 0.189, 0.100, 0.051, 0.021, 0.012, 0.006, 0.002, 0.001, 0.001,
342 0.000
343 ];
344
345 pub const WY_D50_2_10: [f32; 41] = [
347 0.000, 0.000, 0.002, 0.006, 0.023, 0.064, 0.164, 0.316, 0.518, 0.792,
348 1.259, 1.821, 2.943, 4.626, 6.563, 8.376, 9.148, 9.688, 9.469, 8.854,
349 8.190, 6.738, 5.869, 4.755, 3.594, 2.416, 1.648, 0.975, 0.571, 0.314,
350 0.161, 0.068, 0.036, 0.019, 0.007, 0.004, 0.002, 0.001, 0.001, 0.000,
351 0.000
352 ];
353
354 pub const WZ_D50_2_10: [f32; 41] = [
356 0.018, 0.067, 0.373, 1.306, 4.318, 8.921, 14.544, 17.196, 15.915, 11.320,
357 6.028, 2.014, 0.442, 0.056, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
358 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
359 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
360 0.000
361 ];
362
363 pub const SUM_WY_D50_2_10: f32 = 100.000;
365}
366
367pub mod chromatic_adaptation {
373 use super::XYZ;
374
375 #[expect(
384 clippy::excessive_precision,
385 reason = "The Bradford transform matrix requires high precision as defined in the ICC/Lindbloom standard"
386 )]
387 pub fn bradford_adapt(xyz: XYZ, src_wp: XYZ, dst_wp: XYZ) -> XYZ {
388 #[rustfmt::skip]
391 let m = [
392 [ 0.8951000, 0.2664000, -0.1614000],
393 [-0.7502000, 1.7135000, 0.0367000],
394 [ 0.0389000, -0.0685000, 1.0296000],
395 ];
396 #[rustfmt::skip]
398 let m_inv = [
399 [ 0.9869929, -0.1470543, 0.1599627],
400 [ 0.4323053, 0.5183603, 0.0492912],
401 [-0.0085287, 0.0400428, 0.9684867],
402 ];
403
404 let src_lms = [
406 m[0][0] * src_wp.x + m[0][1] * src_wp.y + m[0][2] * src_wp.z,
407 m[1][0] * src_wp.x + m[1][1] * src_wp.y + m[1][2] * src_wp.z,
408 m[2][0] * src_wp.x + m[2][1] * src_wp.y + m[2][2] * src_wp.z,
409 ];
410 let dst_lms = [
411 m[0][0] * dst_wp.x + m[0][1] * dst_wp.y + m[0][2] * dst_wp.z,
412 m[1][0] * dst_wp.x + m[1][1] * dst_wp.y + m[1][2] * dst_wp.z,
413 m[2][0] * dst_wp.x + m[2][1] * dst_wp.y + m[2][2] * dst_wp.z,
414 ];
415
416 let scale = [
418 dst_lms[0] / src_lms[0],
419 dst_lms[1] / src_lms[1],
420 dst_lms[2] / src_lms[2],
421 ];
422
423 let lms = [
425 m[0][0] * xyz.x + m[0][1] * xyz.y + m[0][2] * xyz.z,
426 m[1][0] * xyz.x + m[1][1] * xyz.y + m[1][2] * xyz.z,
427 m[2][0] * xyz.x + m[2][1] * xyz.y + m[2][2] * xyz.z,
428 ];
429
430 let lms_adapted = [lms[0] * scale[0], lms[1] * scale[1], lms[2] * scale[2]];
432
433 XYZ {
435 x: m_inv[0][0] * lms_adapted[0]
436 + m_inv[0][1] * lms_adapted[1]
437 + m_inv[0][2] * lms_adapted[2],
438 y: m_inv[1][0] * lms_adapted[0]
439 + m_inv[1][1] * lms_adapted[1]
440 + m_inv[1][2] * lms_adapted[2],
441 z: m_inv[2][0] * lms_adapted[0]
442 + m_inv[2][1] * lms_adapted[1]
443 + m_inv[2][2] * lms_adapted[2],
444 }
445 }
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
449pub struct XYZ {
450 pub x: f32,
451 pub y: f32,
452 pub z: f32,
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
456pub struct Lab {
457 pub l: f32,
458 pub a: f32,
459 pub b: f32,
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
465pub struct LMS {
466 pub l: f32,
467 pub m: f32,
468 pub s: f32,
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
476pub struct Jzazbz {
477 pub jz: f32,
479 pub az: f32,
481 pub bz: f32,
483}
484
485pub fn calculate_cct(spd: &crate::spectrum::SpectralData) -> (f32, f32) {
486 let xyz = spd.to_xyz();
487 let sum = xyz.x + xyz.y + xyz.z;
488 if sum == 0.0 {
489 return (0.0, 0.0);
490 }
491 let x = xyz.x / sum;
492 let y = xyz.y / sum;
493
494 let n = (x - 0.3320) / (0.1858 - y);
496 let cct = 449.0 * n.powi(3) + 3525.0 * n.powi(2) + 6823.3 * n + 5524.3;
497
498 (cct, 0.0)
499}
500
501impl XYZ {
502 pub fn to_lab(&self, wp: XYZ) -> Lab {
505 const EPSILON: f32 = 216.0 / 24389.0; const KAPPA: f32 = 24389.0 / 27.0; let f = |t: f32| -> f32 {
510 if t > EPSILON {
511 t.powf(1.0 / 3.0)
512 } else {
513 (KAPPA * t + 16.0) / 116.0
514 }
515 };
516
517 let fx = f(self.x / wp.x);
518 let fy = f(self.y / wp.y);
519 let fz = f(self.z / wp.z);
520
521 Lab {
522 l: 116.0 * fy - 16.0,
523 a: 500.0 * (fx - fy),
524 b: 200.0 * (fy - fz),
525 }
526 }
527
528 #[expect(
536 clippy::excessive_precision,
537 reason = "High precision is required for accurate sRGB gamut conversion"
538 )]
539 pub fn to_srgb(&self) -> (u8, u8, u8) {
540 let r_lin = 3.2404542 * self.x - 1.5371385 * self.y - 0.4985314 * self.z;
542 let g_lin = -0.9692660 * self.x + 1.8760108 * self.y + 0.0415560 * self.z;
543 let b_lin = 0.0556434 * self.x - 0.2040259 * self.y + 1.0572252 * self.z;
544
545 fn gamma(c: f32) -> f32 {
547 if c <= 0.0031308 {
548 12.92 * c
549 } else {
550 1.055 * c.powf(1.0 / 2.4) - 0.055
551 }
552 }
553
554 let r = (gamma(r_lin).clamp(0.0, 1.0) * 255.0).round() as u8;
555 let g = (gamma(g_lin).clamp(0.0, 1.0) * 255.0).round() as u8;
556 let b = (gamma(b_lin).clamp(0.0, 1.0) * 255.0).round() as u8;
557
558 (r, g, b)
559 }
560
561 pub fn to_srgb_safe(&self, current_wp: XYZ) -> (u8, u8, u8) {
564 if (self.x - current_wp.x).abs() < 1e-5
565 && (self.y - current_wp.y).abs() < 1e-5
566 && (self.z - current_wp.z).abs() < 1e-5
567 {
568 }
570
571 if current_wp == illuminant::D65 {
572 self.to_srgb()
573 } else {
574 let adapted = chromatic_adaptation::bradford_adapt(*self, current_wp, illuminant::D65);
575 adapted.to_srgb()
576 }
577 }
578
579 #[expect(
587 clippy::excessive_precision,
588 reason = "Jzazbz conversion matrices depend on exact coefficients from the original paper for perceptual uniformity"
589 )]
590 pub fn to_jzazbz(&self, luminance_scale: f32) -> Jzazbz {
591 let x = self.x * luminance_scale;
593 let y = self.y * luminance_scale;
594 let z = self.z * luminance_scale;
595
596 let l = 0.41478972 * x + 0.57999905 * y + 0.01464805 * z;
598 let m = -0.20151003 * x + 1.12064859 * y + 0.05310084 * z;
599 let s = -0.01660078 * x + 0.26480015 * y + 0.66847986 * z;
600
601 fn pq(v: f32) -> f32 {
603 let v_abs = (v.max(0.0) / 10000.0) as f64; let n = 2610.0 / 16384.0;
605 let p = 1.7 * (2523.0 / 32.0);
606 let c1 = 3424.0 / 4096.0;
607 let c2 = 2413.0 / 128.0;
608 let c3 = 2392.0 / 128.0;
609
610 let v_pow_n = v_abs.powf(n);
611 (((c1 + c2 * v_pow_n) / (1.0 + c3 * v_pow_n)).powf(p)) as f32
612 }
613
614 let lp = pq(l);
615 let mp = pq(m);
616 let sp = pq(s);
617
618 let iz = 0.5 * lp + 0.5 * mp;
620 let az = 3.524000 * lp - 4.066708 * mp + 0.542708 * sp;
621 let bz = 0.199076 * lp + 1.096799 * mp - 1.295875 * sp;
622
623 let d = -0.56;
625 let jz = ((1.0 + d) * iz) / (1.0 + d * iz) - 0.005605;
626
627 Jzazbz {
628 jz: jz.max(0.0),
629 az,
630 bz,
631 }
632 }
633
634 pub fn from_reflectance_10nm(reflectance: &[f32; 41]) -> Self {
642 let (x, y, z) = reflectance
643 .iter()
644 .zip(weighting::WX_D65_2_10.iter())
645 .zip(weighting::WY_D65_2_10.iter())
646 .zip(weighting::WZ_D65_2_10.iter())
647 .fold(
648 (0.0f32, 0.0f32, 0.0f32),
649 |(x, y, z), (((r, wx), wy), wz)| (x + r * wx, y + r * wy, z + r * wz),
650 );
651
652 Self {
655 x: x * 100.0 / weighting::SUM_WY_D65_2_10,
656 y: y * 100.0 / weighting::SUM_WY_D65_2_10,
657 z: z * 100.0 / weighting::SUM_WY_D65_2_10,
658 }
659 }
660
661 pub fn to_uv_1960(&self) -> (f32, f32) {
664 let denom = self.x + 15.0 * self.y + 3.0 * self.z;
665 if denom.abs() < 1e-9 {
666 return (0.0, 0.0);
667 }
668 let u = (4.0 * self.x) / denom;
669 let v = (6.0 * self.y) / denom;
670 (u, v)
671 }
672
673 pub fn to_uvw_1964(&self, white_uv: (f32, f32)) -> (f32, f32, f32) {
676 let (u, v) = self.to_uv_1960();
677 let w_star = 25.0 * self.y.powf(1.0 / 3.0) - 17.0;
678 let u_star = 13.0 * w_star * (u - white_uv.0);
679 let v_star = 13.0 * w_star * (v - white_uv.1);
680 (u_star, v_star, w_star)
681 }
682}
683
684pub mod generation {
685 pub const S0: [f32; 41] = [
689 0.0, 0.0, 33.4, 37.4, 117.4, 117.8, 114.9, 115.9, 108.8, 109.3, 107.8, 104.8, 107.7, 104.4,
690 104.0, 100.0, 96.0, 95.1, 89.1, 90.5, 90.3, 88.4, 84.0, 85.1, 81.9, 82.6, 84.9, 81.3, 71.9,
691 74.3, 76.4, 63.3, 71.7, 77.0, 65.2, 47.7, 68.6, 65.0, 66.0, 61.0, 53.3,
692 ];
693 pub const S1: [f32; 41] = [
694 0.0, 0.0, -1.1, -0.5, -0.7, -1.2, -2.6, -2.9, -2.8, -4.5, -6.1, -7.6, -9.7, -11.7, -12.2,
695 -13.6, -12.0, -13.3, -12.9, -10.6, -11.6, -10.8, -8.1, -10.3, -11.0, -11.5, -10.8, -10.9,
696 -8.8, -7.3, -12.9, -15.8, -15.1, -12.2, -10.2, -8.6, -12.0, -14.6, -15.1, -14.9, -13.7,
697 ];
698 pub const S2: [f32; 41] = [
699 0.0, 0.0, -2.1, -1.9, -1.1, -2.2, -3.5, -3.5, -3.3, -2.0, -1.2, -1.1, -0.5, 0.2, 0.5, 2.1,
700 3.2, 4.1, 4.7, 5.1, 6.7, 7.3, 8.6, 9.8, 10.2, 14.9, 18.1, 15.9, 16.8, 24.2, 31.7, 15.3,
701 18.9, 21.2, 15.6, 8.3, 18.9, 14.6, 15.5, 15.4, 14.6,
702 ];
703
704 pub fn generate_planckian(cct: f32, wavelengths: &[f32]) -> Vec<f32> {
706 let c1 = 3.741771e-16;
707 let c2 = 1.4388e-2;
708 wavelengths
709 .iter()
710 .map(|&wl| {
711 let wl_m = wl * 1e-9;
712 if wl_m == 0.0 {
713 0.0
714 } else {
715 c1 * wl_m.powi(-5) / ((c2 / (wl_m * cct)).exp() - 1.0)
716 }
717 })
718 .collect()
719 }
720
721 pub fn generate_daylight(cct: f32, wavelengths: &[f32]) -> Vec<f32> {
723 let x_d = if cct <= 7000.0 {
724 -4.6070e9 / cct.powi(3) + 2.9678e6 / cct.powi(2) + 0.09911e3 / cct + 0.244063
725 } else {
726 -2.0064e9 / cct.powi(3) + 1.9018e6 / cct.powi(2) + 0.24748e3 / cct + 0.237040
727 };
728
729 let y_d = -3.000 * x_d * x_d + 2.870 * x_d - 0.275;
730
731 let m1 = (-1.3515 - 1.7703 * x_d + 5.9114 * y_d) / (0.0241 + 0.2562 * x_d - 0.7341 * y_d);
732 let m2 = (0.0300 - 31.4424 * x_d + 30.0717 * y_d) / (0.0241 + 0.2562 * x_d - 0.7341 * y_d);
733
734 wavelengths
735 .iter()
736 .map(|&wl| {
737 let t = (wl - 380.0) / 10.0;
739 let idx = t.floor() as i32;
740 let x = t - idx as f32;
741
742 let get_val = |table: &[f32; 41]| {
743 if idx < 0 {
744 table[0] } else if idx >= 40 {
746 table[40] } else {
748 let v0 = table[idx as usize];
749 let v1 = table[(idx + 1) as usize];
750 v0 + x * (v1 - v0)
751 }
752 };
753
754 let s0 = get_val(&S0);
755 let s1 = get_val(&S1);
756 let s2 = get_val(&S2);
757
758 let val = s0 + m1 * s1 + m2 * s2;
759 if val < 0.0 { 0.0 } else { val }
760 })
761 .collect()
762 }
763}
764
765impl Lab {
766 pub fn to_xyz(&self, wp: XYZ) -> XYZ {
769 const EPSILON: f32 = 216.0 / 24389.0; const KAPPA: f32 = 24389.0 / 27.0; let fy = (self.l + 16.0) / 116.0;
774 let fx = self.a / 500.0 + fy;
775 let fz = fy - self.b / 200.0;
776
777 let f_inv = |t: f32| -> f32 {
778 let t3 = t.powi(3);
779 if t3 > EPSILON {
780 t3
781 } else {
782 (116.0 * t - 16.0) / KAPPA
783 }
784 };
785
786 XYZ {
787 x: wp.x * f_inv(fx),
788 y: wp.y * f_inv(fy),
789 z: wp.z * f_inv(fz),
790 }
791 }
792
793 pub fn to_srgb(&self) -> (u8, u8, u8) {
796 self.to_xyz(illuminant::D65_2).to_srgb()
797 }
798
799 pub fn delta_e_76(&self, other: &Lab) -> f32 {
801 ((self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2))
802 .sqrt()
803 }
804
805 pub fn delta_e_2000(&self, other: &Lab) -> f32 {
808 let k_l = 1.0;
810 let k_c = 1.0;
811 let k_h = 1.0;
812
813 let c1 = (self.a.powi(2) + self.b.powi(2)).sqrt();
814 let c2 = (other.a.powi(2) + other.b.powi(2)).sqrt();
815 let avg_c = (c1 + c2) / 2.0;
816
817 let g = 0.5 * (1.0 - (avg_c.powi(7) / (avg_c.powi(7) + 25.0f32.powi(7))).sqrt());
819 let a1p = (1.0 + g) * self.a;
820 let a2p = (1.0 + g) * other.a;
821
822 let c1p = (a1p.powi(2) + self.b.powi(2)).sqrt();
823 let c2p = (a2p.powi(2) + other.b.powi(2)).sqrt();
824
825 let get_hp = |b: f32, ap: f32| -> f32 {
827 if b == 0.0 && ap == 0.0 {
828 0.0
829 } else {
830 let h = b.atan2(ap).to_degrees();
831 if h < 0.0 { h + 360.0 } else { h }
832 }
833 };
834 let h1p = get_hp(self.b, a1p);
835 let h2p = get_hp(other.b, a2p);
836
837 let d_lp = other.l - self.l;
839 let d_cp = c2p - c1p;
840
841 let mut d_hp_deg = h2p - h1p;
842 if c1p * c2p != 0.0 {
843 if d_hp_deg.abs() > 180.0 {
844 if h2p <= h1p {
845 d_hp_deg += 360.0;
846 } else {
847 d_hp_deg -= 360.0;
848 }
849 }
850 } else {
851 d_hp_deg = 0.0;
852 }
853 let d_hp = 2.0 * (c1p * c2p).sqrt() * (d_hp_deg / 2.0).to_radians().sin();
854
855 let avg_lp = (self.l + other.l) / 2.0;
857 let avg_cp = (c1p + c2p) / 2.0;
858
859 let mut avg_hp = h1p + h2p;
860 if c1p * c2p != 0.0 {
861 if (h1p - h2p).abs() > 180.0 {
862 if h1p + h2p < 360.0 {
863 avg_hp += 360.0;
864 } else {
865 avg_hp -= 360.0;
866 }
867 }
868 avg_hp /= 2.0;
869 } else {
870 avg_hp = h1p + h2p;
871 }
872
873 let t = 1.0 - 0.17 * (avg_hp - 30.0).to_radians().cos()
875 + 0.24 * (2.0 * avg_hp).to_radians().cos()
876 + 0.32 * (3.0 * avg_hp + 6.0).to_radians().cos()
877 - 0.20 * (4.0 * avg_hp - 63.0).to_radians().cos();
878
879 let s_l = 1.0 + (0.015 * (avg_lp - 50.0).powi(2)) / (20.0 + (avg_lp - 50.0).powi(2)).sqrt();
881 let s_c = 1.0 + 0.045 * avg_cp;
882 let s_h = 1.0 + 0.015 * avg_cp * t;
883
884 let d_theta = 30.0 * (-((avg_hp - 275.0) / 25.0).powi(2)).exp();
886 let rc = 2.0 * (avg_cp.powi(7) / (avg_cp.powi(7) + 25.0f32.powi(7))).sqrt();
887 let rt = -rc * (2.0 * d_theta.to_radians()).sin();
888
889 ((d_lp / (k_l * s_l)).powi(2)
891 + (d_cp / (k_c * s_c)).powi(2)
892 + (d_hp / (k_h * s_h)).powi(2)
893 + rt * (d_cp / (k_c * s_c)) * (d_hp / (k_h * s_h)))
894 .sqrt()
895 }
896
897 pub fn mix(&self, other: &Lab, ratio: f32) -> Lab {
899 let ratio = ratio.clamp(0.0, 1.0);
900 Lab {
901 l: self.l * (1.0 - ratio) + other.l * ratio,
902 a: self.a * (1.0 - ratio) + other.a * ratio,
903 b: self.b * (1.0 - ratio) + other.b * ratio,
904 }
905 }
906
907 pub fn chroma(&self) -> f32 {
909 (self.a.powi(2) + self.b.powi(2)).sqrt()
910 }
911
912 pub fn hue(&self) -> f32 {
914 let h = self.b.atan2(self.a).to_degrees();
915 if h < 0.0 { h + 360.0 } else { h }
916 }
917}
918
919impl Jzazbz {
920 pub fn delta_ez(&self, other: &Jzazbz) -> f32 {
925 ((self.jz - other.jz).powi(2) + (self.az - other.az).powi(2) + (self.bz - other.bz).powi(2))
926 .sqrt()
927 }
928
929 pub fn chroma(&self) -> f32 {
931 (self.az.powi(2) + self.bz.powi(2)).sqrt()
932 }
933
934 pub fn hue(&self) -> f32 {
936 let h = self.bz.atan2(self.az).to_degrees();
937 if h < 0.0 { h + 360.0 } else { h }
938 }
939
940 pub fn mix(&self, other: &Jzazbz, ratio: f32) -> Jzazbz {
942 let ratio = ratio.clamp(0.0, 1.0);
943 Jzazbz {
944 jz: self.jz * (1.0 - ratio) + other.jz * ratio,
945 az: self.az * (1.0 - ratio) + other.az * ratio,
946 bz: self.bz * (1.0 - ratio) + other.bz * ratio,
947 }
948 }
949}
950
951pub mod metrics {
953 use super::*;
954 use crate::spectrum::SpectralData;
955
956 #[rustfmt::skip]
959 pub const TCS: [[f32; 41]; 9] = [
960 [0.22, 0.25, 0.26, 0.25, 0.24, 0.24, 0.23, 0.23, 0.22, 0.22, 0.21, 0.22, 0.22, 0.23, 0.23, 0.23, 0.24, 0.25, 0.27, 0.30, 0.34, 0.39, 0.42, 0.44, 0.45, 0.45, 0.45, 0.45, 0.45, 0.45, 0.46, 0.46, 0.46, 0.46, 0.46, 0.47, 0.47, 0.47, 0.47, 0.47, 0.47],
962 [0.07, 0.09, 0.11, 0.12, 0.12, 0.12, 0.12, 0.12, 0.13, 0.13, 0.14, 0.15, 0.17, 0.21, 0.24, 0.26, 0.27, 0.27, 0.28, 0.30, 0.32, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.33, 0.33, 0.33, 0.33, 0.33, 0.32, 0.32, 0.32, 0.32, 0.32, 0.31, 0.31],
964 [0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.07, 0.08, 0.09, 0.11, 0.15, 0.20, 0.24, 0.28, 0.34, 0.39, 0.40, 0.38, 0.35, 0.30, 0.26, 0.25, 0.24, 0.22, 0.22, 0.22, 0.23, 0.25, 0.29, 0.34, 0.39, 0.43, 0.46, 0.48, 0.49, 0.50, 0.51, 0.52, 0.52, 0.53, 0.54],
966 [0.07, 0.09, 0.12, 0.12, 0.13, 0.14, 0.14, 0.16, 0.19, 0.23, 0.28, 0.33, 0.37, 0.39, 0.39, 0.38, 0.34, 0.31, 0.28, 0.25, 0.21, 0.18, 0.16, 0.16, 0.15, 0.15, 0.15, 0.15, 0.16, 0.17, 0.17, 0.17, 0.17, 0.17, 0.18, 0.19, 0.19, 0.20, 0.21, 0.23, 0.25],
968 [0.30, 0.31, 0.31, 0.32, 0.33, 0.34, 0.36, 0.38, 0.40, 0.42, 0.42, 0.41, 0.40, 0.39, 0.37, 0.35, 0.31, 0.28, 0.25, 0.22, 0.19, 0.19, 0.18, 0.18, 0.18, 0.18, 0.18, 0.19, 0.19, 0.20, 0.20, 0.20, 0.20, 0.20, 0.21, 0.21, 0.22, 0.22, 0.23, 0.24, 0.27],
970 [0.15, 0.27, 0.41, 0.49, 0.52, 0.53, 0.54, 0.56, 0.55, 0.54, 0.52, 0.49, 0.45, 0.41, 0.36, 0.31, 0.25, 0.23, 0.23, 0.22, 0.22, 0.22, 0.22, 0.23, 0.24, 0.26, 0.27, 0.28, 0.28, 0.29, 0.30, 0.33, 0.35, 0.38, 0.40, 0.43, 0.45, 0.47, 0.49, 0.51, 0.53],
972 [0.38, 0.52, 0.55, 0.56, 0.56, 0.56, 0.54, 0.51, 0.47, 0.43, 0.39, 0.34, 0.31, 0.30, 0.27, 0.26, 0.26, 0.26, 0.26, 0.25, 0.26, 0.28, 0.32, 0.36, 0.39, 0.41, 0.43, 0.44, 0.45, 0.47, 0.47, 0.48, 0.49, 0.50, 0.51, 0.53, 0.54, 0.55, 0.57, 0.58, 0.59],
974 [0.10, 0.17, 0.32, 0.46, 0.49, 0.48, 0.45, 0.43, 0.40, 0.37, 0.34, 0.31, 0.29, 0.28, 0.26, 0.25, 0.26, 0.27, 0.27, 0.28, 0.32, 0.38, 0.48, 0.57, 0.63, 0.66, 0.69, 0.70, 0.71, 0.71, 0.72, 0.72, 0.72, 0.72, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73, 0.73],
976 [0.066, 0.058, 0.052, 0.051, 0.050, 0.048, 0.046, 0.041, 0.035, 0.030, 0.028, 0.028, 0.030, 0.031, 0.032, 0.033, 0.041, 0.048, 0.060, 0.102, 0.190, 0.336, 0.505, 0.641, 0.717, 0.758, 0.781, 0.797, 0.809, 0.819, 0.828, 0.831, 0.835, 0.836, 0.838, 0.839, 0.839, 0.839, 0.839, 0.839, 0.838],
978 ];
979
980 pub fn calculate_cri(spd: &SpectralData) -> (f32, f32) {
982 let xyz = spd.to_xyz_emissive_2();
983 let cct = xyz.to_cct();
984
985 let ref_spd = if cct < 5000.0 {
987 generate_planckian(cct)
988 } else {
989 generate_daylight(cct)
990 };
991
992 let mut ris = [0.0f32; 9];
994 for i in 0..9 {
995 ris[i] = calculate_ri(spd, &ref_spd, &TCS[i]);
996 }
997
998 let ra = ris[0..8].iter().sum::<f32>() / 8.0;
1000 let r9 = ris[8];
1001
1002 (ra, r9)
1003 }
1004
1005 fn calculate_ri(test_spd: &SpectralData, ref_spd: &[f32; 41], tcs: &[f32; 41]) -> f32 {
1006 let (xb, yb, zb) = Observer::CIE1931_2.get_cmfs();
1007
1008 let calc_xyz = |spd_vals: &[f32], tcs_vals: &[f32]| -> XYZ {
1009 let mut x = 0.0;
1010 let mut y = 0.0;
1011 let mut z = 0.0;
1012 for i in 0..41 {
1013 let val = spd_vals[i] * tcs_vals[i];
1014 x += val * xb[i];
1015 y += val * yb[i];
1016 z += val * zb[i];
1017 }
1018 XYZ { x, y, z }
1019 };
1020
1021 let test_xyz = calc_xyz(&test_spd.values, tcs);
1022 let ref_xyz = calc_xyz(ref_spd, tcs);
1023
1024 let test_lab = test_xyz.to_lab(test_spd.to_xyz_emissive_2());
1028
1029 let mut ref_white_x = 0.0;
1030 let mut ref_white_y = 0.0;
1031 let mut ref_white_z = 0.0;
1032 for i in 0..41 {
1033 ref_white_x += ref_spd[i] * xb[i];
1034 ref_white_y += ref_spd[i] * yb[i];
1035 ref_white_z += ref_spd[i] * zb[i];
1036 }
1037 let ref_white = XYZ {
1038 x: ref_white_x,
1039 y: ref_white_y,
1040 z: ref_white_z,
1041 };
1042 let ref_lab = ref_xyz.to_lab(ref_white);
1043
1044 let de = test_lab.delta_e_76(&ref_lab);
1045 100.0 - 4.6 * de
1046 }
1047
1048 fn generate_planckian(cct: f32) -> [f32; 41] {
1049 let mut spd = [0.0f32; 41];
1050 let c1 = 3.741771e-16_f32;
1051 let c2 = 1.4388e-2_f32;
1052 for (i, val) in spd.iter_mut().enumerate() {
1053 let wl = (380 + i * 10) as f32 * 1e-9_f32;
1054 *val = c1 / (wl.powi(5) * ((c2 / (wl * cct)).exp() - 1.0));
1055 }
1056 spd
1057 }
1058
1059 fn generate_daylight(cct: f32) -> [f32; 41] {
1060 let xd = if cct <= 7000.0 {
1062 -4.6070e9 / cct.powi(3) + 2.9678e6 / cct.powi(2) + 0.09911e3 / cct + 0.244063
1063 } else {
1064 -2.0064e9 / cct.powi(3) + 1.9018e6 / cct.powi(2) + 0.24748e3 / cct + 0.237040
1065 };
1066
1067 let yd = -3.000 * xd * xd + 2.870 * xd - 0.275;
1068
1069 let m1 = (-1.3515 - 1.7703 * xd + 5.9114 * yd) / (0.0241 + 0.2562 * xd - 0.7341 * yd);
1070 let m2 = (0.0300 - 31.4424 * xd + 30.0717 * yd) / (0.0241 + 0.2562 * xd - 0.7341 * yd);
1071
1072 let mut spd = [0.0f32; 41];
1073 for (i, val) in spd.iter_mut().enumerate() {
1074 *val = illuminant::spd::D65[i] * (1.0 + m1 * 0.01 + m2 * 0.01);
1076 }
1077 spd
1078 }
1079}
1080
1081pub mod appearance {
1083 use super::{Lab, XYZ, illuminant};
1084 use crate::spectrum::SpectralData;
1085
1086 pub fn metamerism_index(
1093 sample1: &SpectralData,
1094 sample2: &SpectralData,
1095 ref_illuminant: XYZ,
1096 test_illuminant: XYZ,
1097 ) -> f32 {
1098 let xyz1_ref = sample1.to_xyz();
1100 let xyz2_ref = sample2.to_xyz();
1101 let lab1_ref = XYZ {
1102 x: xyz1_ref.x / 100.0,
1103 y: xyz1_ref.y / 100.0,
1104 z: xyz1_ref.z / 100.0,
1105 }
1106 .to_lab(ref_illuminant);
1107 let lab2_ref = XYZ {
1108 x: xyz2_ref.x / 100.0,
1109 y: xyz2_ref.y / 100.0,
1110 z: xyz2_ref.z / 100.0,
1111 }
1112 .to_lab(ref_illuminant);
1113
1114 let xyz1_test =
1116 super::chromatic_adaptation::bradford_adapt(xyz1_ref, illuminant::D65, test_illuminant);
1117 let xyz2_test =
1118 super::chromatic_adaptation::bradford_adapt(xyz2_ref, illuminant::D65, test_illuminant);
1119
1120 let lab1_test = XYZ {
1121 x: xyz1_test.x / 100.0,
1122 y: xyz1_test.y / 100.0,
1123 z: xyz1_test.z / 100.0,
1124 }
1125 .to_lab(test_illuminant);
1126 let lab2_test = XYZ {
1127 x: xyz2_test.x / 100.0,
1128 y: xyz2_test.y / 100.0,
1129 z: xyz2_test.z / 100.0,
1130 }
1131 .to_lab(test_illuminant);
1132
1133 let de_ref = lab1_ref.delta_e_2000(&lab2_ref);
1135 let de_test = lab1_test.delta_e_2000(&lab2_test);
1137
1138 (de_test - de_ref).abs()
1140 }
1141
1142 pub fn simulate_illuminant(lab: &Lab, from: XYZ, to: XYZ) -> Lab {
1145 let xyz = lab.to_xyz(from);
1146 let adapted = super::chromatic_adaptation::bradford_adapt(xyz, from, to);
1147 adapted.to_lab(to)
1148 }
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154
1155 #[test]
1156 fn test_d65_white_point_from_weighting() {
1157 let reflectance = [1.0f32; 41];
1159 let xyz = XYZ::from_reflectance_10nm(&reflectance);
1160
1161 assert!(
1166 (xyz.y - 100.0).abs() < 0.05,
1167 "Y should be 100, got {}",
1168 xyz.y
1169 );
1170 assert!(
1171 (xyz.x - 95.05).abs() < 0.05,
1172 "X should be ~95.05, got {}",
1173 xyz.x
1174 );
1175 assert!(
1176 (xyz.z - 108.88).abs() < 0.05,
1177 "Z should be ~108.88, got {}",
1178 xyz.z
1179 );
1180 }
1181
1182 #[test]
1183 fn test_xyz_to_lab_d65() {
1184 let white = illuminant::D65;
1185 let lab = white.to_lab(white);
1186
1187 assert!((lab.l - 100.0).abs() < 1e-4);
1189 assert!(lab.a.abs() < 1e-4);
1190 assert!(lab.b.abs() < 1e-4);
1191 }
1192}