1use crate::objects::{Dictionary, Object};
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct LabColorSpace {
15 pub white_point: [f64; 3],
18 pub black_point: [f64; 3],
21 pub range: [f64; 4],
24}
25
26impl Default for LabColorSpace {
27 fn default() -> Self {
28 Self {
29 white_point: [0.9505, 1.0000, 1.0890], black_point: [0.0, 0.0, 0.0],
31 range: [-100.0, 100.0, -100.0, 100.0],
32 }
33 }
34}
35
36impl LabColorSpace {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn with_white_point(mut self, white_point: [f64; 3]) -> Self {
44 self.white_point = white_point;
45 self
46 }
47
48 pub fn with_black_point(mut self, black_point: [f64; 3]) -> Self {
50 self.black_point = black_point;
51 self
52 }
53
54 pub fn with_range(mut self, a_min: f64, a_max: f64, b_min: f64, b_max: f64) -> Self {
56 self.range = [a_min, a_max, b_min, b_max];
57 self
58 }
59
60 pub fn d50() -> Self {
62 Self::new()
63 }
64
65 pub fn d65() -> Self {
67 Self::new().with_white_point([0.9504, 1.0000, 1.0888])
68 }
69
70 pub fn params_dictionary(&self) -> Dictionary {
75 let mut dict = Dictionary::new();
76
77 dict.set(
79 "WhitePoint",
80 Object::Array(self.white_point.iter().map(|&x| Object::Real(x)).collect()),
81 );
82
83 if self.black_point != [0.0, 0.0, 0.0] {
85 dict.set(
86 "BlackPoint",
87 Object::Array(self.black_point.iter().map(|&x| Object::Real(x)).collect()),
88 );
89 }
90
91 if self.range != [-100.0, 100.0, -100.0, 100.0] {
93 dict.set(
94 "Range",
95 Object::Array(self.range.iter().map(|&x| Object::Real(x)).collect()),
96 );
97 }
98
99 dict
100 }
101
102 pub fn to_pdf_array(&self) -> Vec<Object> {
104 vec![
105 Object::Name("Lab".to_string()),
106 Object::Dictionary(self.params_dictionary()),
107 ]
108 }
109
110 pub fn lab_to_xyz(&self, l: f64, a: f64, b: f64) -> [f64; 3] {
113 const EPSILON: f64 = 216.0 / 24389.0; const KAPPA: f64 = 24389.0 / 27.0; let fy = (l + 16.0) / 116.0;
119 let fx = fy + (a / 500.0);
120 let fz = fy - (b / 200.0);
121
122 let x = if fx.powi(3) > EPSILON {
124 fx.powi(3)
125 } else {
126 (116.0 * fx - 16.0) / KAPPA
127 };
128
129 let y = if l > KAPPA * EPSILON {
130 fy.powi(3)
131 } else {
132 l / KAPPA
133 };
134
135 let z = if fz.powi(3) > EPSILON {
136 fz.powi(3)
137 } else {
138 (116.0 * fz - 16.0) / KAPPA
139 };
140
141 [
143 x * self.white_point[0],
144 y * self.white_point[1],
145 z * self.white_point[2],
146 ]
147 }
148
149 pub fn xyz_to_lab(&self, x: f64, y: f64, z: f64) -> [f64; 3] {
151 const EPSILON: f64 = 216.0 / 24389.0; const KAPPA: f64 = 24389.0 / 27.0; let xn = x / self.white_point[0];
157 let yn = y / self.white_point[1];
158 let zn = z / self.white_point[2];
159
160 let fx = if xn > EPSILON {
162 xn.cbrt()
163 } else {
164 (KAPPA * xn + 16.0) / 116.0
165 };
166
167 let fy = if yn > EPSILON {
168 yn.cbrt()
169 } else {
170 (KAPPA * yn + 16.0) / 116.0
171 };
172
173 let fz = if zn > EPSILON {
174 zn.cbrt()
175 } else {
176 (KAPPA * zn + 16.0) / 116.0
177 };
178
179 let l = 116.0 * fy - 16.0;
181 let a = 500.0 * (fx - fy);
182 let b = 200.0 * (fy - fz);
183
184 [l, a, b]
185 }
186
187 pub fn lab_to_rgb(&self, l: f64, a: f64, b: f64) -> [f64; 3] {
190 let [x, y, z] = self.lab_to_xyz(l, a, b);
191
192 let r = 3.2406 * x - 1.5372 * y - 0.4986 * z;
194 let g = -0.9689 * x + 1.8758 * y + 0.0415 * z;
195 let b = 0.0557 * x - 0.2040 * y + 1.0570 * z;
196
197 [
199 gamma_correct(r).clamp(0.0, 1.0),
200 gamma_correct(g).clamp(0.0, 1.0),
201 gamma_correct(b).clamp(0.0, 1.0),
202 ]
203 }
204
205 pub fn rgb_to_lab(&self, r: f64, g: f64, b: f64) -> [f64; 3] {
207 let r_linear = inverse_gamma_correct(r);
209 let g_linear = inverse_gamma_correct(g);
210 let b_linear = inverse_gamma_correct(b);
211
212 let x = 0.4124 * r_linear + 0.3576 * g_linear + 0.1805 * b_linear;
214 let y = 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear;
215 let z = 0.0193 * r_linear + 0.1192 * g_linear + 0.9505 * b_linear;
216
217 self.xyz_to_lab(x, y, z)
218 }
219
220 pub fn delta_e(&self, lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
223 let dl = lab1[0] - lab2[0];
224 let da = lab1[1] - lab2[1];
225 let db = lab1[2] - lab2[2];
226
227 (dl * dl + da * da + db * db).sqrt()
228 }
229
230 pub fn delta_e_2000(&self, lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
233 let [l1, a1, b1] = lab1;
236 let [l2, a2, b2] = lab2;
237
238 let dl = l2 - l1;
239 let l_avg = (l1 + l2) / 2.0;
240
241 let c1 = (a1 * a1 + b1 * b1).sqrt();
242 let c2 = (a2 * a2 + b2 * b2).sqrt();
243 let c_avg = (c1 + c2) / 2.0;
244
245 let g = 0.5 * (1.0 - (c_avg.powi(7) / (c_avg.powi(7) + 25.0_f64.powi(7))).sqrt());
246 let a1_prime = a1 * (1.0 + g);
247 let a2_prime = a2 * (1.0 + g);
248
249 let c1_prime = (a1_prime * a1_prime + b1 * b1).sqrt();
250 let c2_prime = (a2_prime * a2_prime + b2 * b2).sqrt();
251 let dc_prime = c2_prime - c1_prime;
252
253 let h1_prime = b1.atan2(a1_prime).to_degrees();
254 let h2_prime = b2.atan2(a2_prime).to_degrees();
255
256 let dh_prime = if (h2_prime - h1_prime).abs() <= 180.0 {
257 h2_prime - h1_prime
258 } else if h2_prime - h1_prime > 180.0 {
259 h2_prime - h1_prime - 360.0
260 } else {
261 h2_prime - h1_prime + 360.0
262 };
263
264 let dh_prime_rad = dh_prime.to_radians();
265 let dh = 2.0 * (c1_prime * c2_prime).sqrt() * (dh_prime_rad / 2.0).sin();
266
267 let kl = 1.0;
269 let kc = 1.0;
270 let kh = 1.0;
271
272 let sl = 1.0 + (0.015 * (l_avg - 50.0).powi(2) / (20.0 + (l_avg - 50.0).powi(2)).sqrt());
273 let sc = 1.0 + 0.045 * c_avg;
274 let sh = 1.0 + 0.015 * c_avg;
275
276 let dl_scaled = dl / (kl * sl);
277 let dc_scaled = dc_prime / (kc * sc);
278 let dh_scaled = dh / (kh * sh);
279
280 (dl_scaled.powi(2) + dc_scaled.powi(2) + dh_scaled.powi(2)).sqrt()
281 }
282}
283
284fn gamma_correct(linear: f64) -> f64 {
286 if linear <= 0.0031308 {
287 12.92 * linear
288 } else {
289 1.055 * linear.powf(1.0 / 2.4) - 0.055
290 }
291}
292
293fn inverse_gamma_correct(srgb: f64) -> f64 {
295 if srgb <= 0.04045 {
296 srgb / 12.92
297 } else {
298 ((srgb + 0.055) / 1.055).powf(2.4)
299 }
300}
301
302#[derive(Debug, Clone, PartialEq)]
304pub struct LabColor {
305 pub l: f64,
307 pub a: f64,
309 pub b: f64,
311 pub color_space: LabColorSpace,
313}
314
315impl LabColor {
316 pub fn new(l: f64, a: f64, b: f64, color_space: LabColorSpace) -> Self {
318 let l = l.clamp(0.0, 100.0);
320
321 let a = a.clamp(color_space.range[0], color_space.range[1]);
323 let b = b.clamp(color_space.range[2], color_space.range[3]);
324
325 Self {
326 l,
327 a,
328 b,
329 color_space,
330 }
331 }
332
333 pub fn with_default(l: f64, a: f64, b: f64) -> Self {
335 Self::new(l, a, b, LabColorSpace::default())
336 }
337
338 pub fn color_space_array(&self) -> Vec<Object> {
340 self.color_space.to_pdf_array()
341 }
342
343 pub fn values(&self) -> Vec<f64> {
345 let a_normalized = (self.a - self.color_space.range[0])
349 / (self.color_space.range[1] - self.color_space.range[0]);
350 let b_normalized = (self.b - self.color_space.range[2])
351 / (self.color_space.range[3] - self.color_space.range[2]);
352
353 vec![self.l / 100.0, a_normalized, b_normalized]
354 }
355
356 pub fn to_xyz(&self) -> [f64; 3] {
358 self.color_space.lab_to_xyz(self.l, self.a, self.b)
359 }
360
361 pub fn to_rgb(&self) -> [f64; 3] {
363 self.color_space.lab_to_rgb(self.l, self.a, self.b)
364 }
365
366 pub fn delta_e(&self, other: &LabColor) -> f64 {
368 self.color_space
369 .delta_e([self.l, self.a, self.b], [other.l, other.a, other.b])
370 }
371
372 pub fn delta_e_2000(&self, other: &LabColor) -> f64 {
374 self.color_space
375 .delta_e_2000([self.l, self.a, self.b], [other.l, other.a, other.b])
376 }
377}
378
379impl LabColor {
381 pub fn white() -> Self {
383 Self::with_default(100.0, 0.0, 0.0)
384 }
385
386 pub fn black() -> Self {
388 Self::with_default(0.0, 0.0, 0.0)
389 }
390
391 pub fn gray() -> Self {
393 Self::with_default(50.0, 0.0, 0.0)
394 }
395
396 pub fn red() -> Self {
398 Self::with_default(53.0, 80.0, 67.0)
399 }
400
401 pub fn green() -> Self {
403 Self::with_default(87.0, -86.0, 83.0)
404 }
405
406 pub fn blue() -> Self {
408 Self::with_default(32.0, 79.0, -108.0)
409 }
410
411 pub fn yellow() -> Self {
413 Self::with_default(97.0, -22.0, 94.0)
414 }
415
416 pub fn cyan() -> Self {
418 Self::with_default(91.0, -48.0, -14.0)
419 }
420
421 pub fn magenta() -> Self {
423 Self::with_default(60.0, 98.0, -61.0)
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_lab_default() {
433 let cs = LabColorSpace::new();
434
435 assert_eq!(cs.white_point, [0.9505, 1.0000, 1.0890]);
436 assert_eq!(cs.black_point, [0.0, 0.0, 0.0]);
437 assert_eq!(cs.range, [-100.0, 100.0, -100.0, 100.0]);
438 }
439
440 #[test]
441 fn test_lab_custom() {
442 let cs = LabColorSpace::new()
443 .with_white_point([0.95, 1.0, 1.09])
444 .with_black_point([0.01, 0.01, 0.01])
445 .with_range(-128.0, 127.0, -128.0, 127.0);
446
447 assert_eq!(cs.white_point, [0.95, 1.0, 1.09]);
448 assert_eq!(cs.black_point, [0.01, 0.01, 0.01]);
449 assert_eq!(cs.range, [-128.0, 127.0, -128.0, 127.0]);
450 }
451
452 #[test]
453 fn test_lab_to_pdf() {
454 let cs = LabColorSpace::new()
455 .with_range(-128.0, 127.0, -128.0, 127.0)
456 .with_black_point([0.01, 0.01, 0.01]);
457
458 let pdf_array = cs.to_pdf_array();
459
460 assert_eq!(pdf_array.len(), 2);
461 assert_eq!(pdf_array[0], Object::Name("Lab".to_string()));
462
463 if let Object::Dictionary(dict) = &pdf_array[1] {
464 assert!(dict.get("WhitePoint").is_some());
465 assert!(dict.get("Range").is_some());
466 assert!(dict.get("BlackPoint").is_some());
467 } else {
468 panic!("Second element should be a dictionary");
469 }
470 }
471
472 #[test]
473 fn test_lab_color_creation() {
474 let color = LabColor::with_default(50.0, 25.0, -25.0);
475
476 assert_eq!(color.l, 50.0);
477 assert_eq!(color.a, 25.0);
478 assert_eq!(color.b, -25.0);
479 }
480
481 #[test]
482 fn test_lab_color_clamping() {
483 let color = LabColor::with_default(150.0, 200.0, -200.0);
484
485 assert_eq!(color.l, 100.0); assert_eq!(color.a, 100.0); assert_eq!(color.b, -100.0); }
489
490 #[test]
491 fn test_lab_to_xyz_conversion() {
492 let cs = LabColorSpace::new();
493 let [_x, y, _z] = cs.lab_to_xyz(50.0, 0.0, 0.0);
494
495 assert!((y - 0.184).abs() < 0.01);
497 }
498
499 #[test]
500 fn test_xyz_to_lab_conversion() {
501 let cs = LabColorSpace::new();
502 let original_lab = [50.0, 25.0, -25.0];
503 let xyz = cs.lab_to_xyz(original_lab[0], original_lab[1], original_lab[2]);
504 let converted_lab = cs.xyz_to_lab(xyz[0], xyz[1], xyz[2]);
505
506 assert!((original_lab[0] - converted_lab[0]).abs() < 0.1);
508 assert!((original_lab[1] - converted_lab[1]).abs() < 0.1);
509 assert!((original_lab[2] - converted_lab[2]).abs() < 0.1);
510 }
511
512 #[test]
513 fn test_lab_to_rgb_approximation() {
514 let cs = LabColorSpace::new();
515
516 let rgb_white = cs.lab_to_rgb(100.0, 0.0, 0.0);
518 assert!(rgb_white[0] > 0.99);
519 assert!(rgb_white[1] > 0.99);
520 assert!(rgb_white[2] > 0.99);
521
522 let rgb_black = cs.lab_to_rgb(0.0, 0.0, 0.0);
524 assert!(rgb_black[0] < 0.01);
525 assert!(rgb_black[1] < 0.01);
526 assert!(rgb_black[2] < 0.01);
527 }
528
529 #[test]
530 fn test_delta_e() {
531 let cs = LabColorSpace::new();
532 let lab1 = [50.0, 0.0, 0.0];
533 let lab2 = [55.0, 0.0, 0.0];
534
535 let delta = cs.delta_e(lab1, lab2);
536 assert_eq!(delta, 5.0); }
538
539 #[test]
540 fn test_common_colors() {
541 let white = LabColor::white();
542 assert_eq!(white.l, 100.0);
543
544 let black = LabColor::black();
545 assert_eq!(black.l, 0.0);
546
547 let gray = LabColor::gray();
548 assert_eq!(gray.l, 50.0);
549 }
550
551 #[test]
552 fn test_d65_illuminant() {
553 let cs = LabColorSpace::d65();
554 assert_eq!(cs.white_point, [0.9504, 1.0000, 1.0888]);
555 }
556
557 #[test]
558 fn test_color_values_normalization() {
559 let cs = LabColorSpace::new().with_range(-128.0, 127.0, -128.0, 127.0);
560 let color = LabColor::new(50.0, 0.0, 0.0, cs);
561
562 let values = color.values();
563 assert_eq!(values[0], 0.5); assert!((values[1] - 0.5).abs() < 0.01); assert!((values[2] - 0.5).abs() < 0.01); }
567
568 #[test]
569 fn test_rgb_to_lab_conversion() {
570 let cs = LabColorSpace::new();
571
572 let lab = cs.rgb_to_lab(0.5, 0.5, 0.5);
574 assert!((lab[0] - 53.0).abs() < 2.0); assert!(lab[1].abs() < 1.0); assert!(lab[2].abs() < 1.0); }
578}