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 to_pdf_array(&self) -> Vec<Object> {
72 let mut array = vec![Object::Name("Lab".to_string())];
73
74 let mut dict = Dictionary::new();
75
76 dict.set(
78 "WhitePoint",
79 Object::Array(self.white_point.iter().map(|&x| Object::Real(x)).collect()),
80 );
81
82 if self.black_point != [0.0, 0.0, 0.0] {
84 dict.set(
85 "BlackPoint",
86 Object::Array(self.black_point.iter().map(|&x| Object::Real(x)).collect()),
87 );
88 }
89
90 if self.range != [-100.0, 100.0, -100.0, 100.0] {
92 dict.set(
93 "Range",
94 Object::Array(self.range.iter().map(|&x| Object::Real(x)).collect()),
95 );
96 }
97
98 array.push(Object::Dictionary(dict));
99 array
100 }
101
102 pub fn lab_to_xyz(&self, l: f64, a: f64, b: f64) -> [f64; 3] {
105 const EPSILON: f64 = 216.0 / 24389.0; const KAPPA: f64 = 24389.0 / 27.0; let fy = (l + 16.0) / 116.0;
111 let fx = fy + (a / 500.0);
112 let fz = fy - (b / 200.0);
113
114 let x = if fx.powi(3) > EPSILON {
116 fx.powi(3)
117 } else {
118 (116.0 * fx - 16.0) / KAPPA
119 };
120
121 let y = if l > KAPPA * EPSILON {
122 fy.powi(3)
123 } else {
124 l / KAPPA
125 };
126
127 let z = if fz.powi(3) > EPSILON {
128 fz.powi(3)
129 } else {
130 (116.0 * fz - 16.0) / KAPPA
131 };
132
133 [
135 x * self.white_point[0],
136 y * self.white_point[1],
137 z * self.white_point[2],
138 ]
139 }
140
141 pub fn xyz_to_lab(&self, x: f64, y: f64, z: f64) -> [f64; 3] {
143 const EPSILON: f64 = 216.0 / 24389.0; const KAPPA: f64 = 24389.0 / 27.0; let xn = x / self.white_point[0];
149 let yn = y / self.white_point[1];
150 let zn = z / self.white_point[2];
151
152 let fx = if xn > EPSILON {
154 xn.cbrt()
155 } else {
156 (KAPPA * xn + 16.0) / 116.0
157 };
158
159 let fy = if yn > EPSILON {
160 yn.cbrt()
161 } else {
162 (KAPPA * yn + 16.0) / 116.0
163 };
164
165 let fz = if zn > EPSILON {
166 zn.cbrt()
167 } else {
168 (KAPPA * zn + 16.0) / 116.0
169 };
170
171 let l = 116.0 * fy - 16.0;
173 let a = 500.0 * (fx - fy);
174 let b = 200.0 * (fy - fz);
175
176 [l, a, b]
177 }
178
179 pub fn lab_to_rgb(&self, l: f64, a: f64, b: f64) -> [f64; 3] {
182 let [x, y, z] = self.lab_to_xyz(l, a, b);
183
184 let r = 3.2406 * x - 1.5372 * y - 0.4986 * z;
186 let g = -0.9689 * x + 1.8758 * y + 0.0415 * z;
187 let b = 0.0557 * x - 0.2040 * y + 1.0570 * z;
188
189 [
191 gamma_correct(r).clamp(0.0, 1.0),
192 gamma_correct(g).clamp(0.0, 1.0),
193 gamma_correct(b).clamp(0.0, 1.0),
194 ]
195 }
196
197 pub fn rgb_to_lab(&self, r: f64, g: f64, b: f64) -> [f64; 3] {
199 let r_linear = inverse_gamma_correct(r);
201 let g_linear = inverse_gamma_correct(g);
202 let b_linear = inverse_gamma_correct(b);
203
204 let x = 0.4124 * r_linear + 0.3576 * g_linear + 0.1805 * b_linear;
206 let y = 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear;
207 let z = 0.0193 * r_linear + 0.1192 * g_linear + 0.9505 * b_linear;
208
209 self.xyz_to_lab(x, y, z)
210 }
211
212 pub fn delta_e(&self, lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
215 let dl = lab1[0] - lab2[0];
216 let da = lab1[1] - lab2[1];
217 let db = lab1[2] - lab2[2];
218
219 (dl * dl + da * da + db * db).sqrt()
220 }
221
222 pub fn delta_e_2000(&self, lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
225 let [l1, a1, b1] = lab1;
228 let [l2, a2, b2] = lab2;
229
230 let dl = l2 - l1;
231 let l_avg = (l1 + l2) / 2.0;
232
233 let c1 = (a1 * a1 + b1 * b1).sqrt();
234 let c2 = (a2 * a2 + b2 * b2).sqrt();
235 let c_avg = (c1 + c2) / 2.0;
236
237 let g = 0.5 * (1.0 - (c_avg.powi(7) / (c_avg.powi(7) + 25.0_f64.powi(7))).sqrt());
238 let a1_prime = a1 * (1.0 + g);
239 let a2_prime = a2 * (1.0 + g);
240
241 let c1_prime = (a1_prime * a1_prime + b1 * b1).sqrt();
242 let c2_prime = (a2_prime * a2_prime + b2 * b2).sqrt();
243 let dc_prime = c2_prime - c1_prime;
244
245 let h1_prime = b1.atan2(a1_prime).to_degrees();
246 let h2_prime = b2.atan2(a2_prime).to_degrees();
247
248 let dh_prime = if (h2_prime - h1_prime).abs() <= 180.0 {
249 h2_prime - h1_prime
250 } else if h2_prime - h1_prime > 180.0 {
251 h2_prime - h1_prime - 360.0
252 } else {
253 h2_prime - h1_prime + 360.0
254 };
255
256 let dh_prime_rad = dh_prime.to_radians();
257 let dh = 2.0 * (c1_prime * c2_prime).sqrt() * (dh_prime_rad / 2.0).sin();
258
259 let kl = 1.0;
261 let kc = 1.0;
262 let kh = 1.0;
263
264 let sl = 1.0 + (0.015 * (l_avg - 50.0).powi(2) / (20.0 + (l_avg - 50.0).powi(2)).sqrt());
265 let sc = 1.0 + 0.045 * c_avg;
266 let sh = 1.0 + 0.015 * c_avg;
267
268 let dl_scaled = dl / (kl * sl);
269 let dc_scaled = dc_prime / (kc * sc);
270 let dh_scaled = dh / (kh * sh);
271
272 (dl_scaled.powi(2) + dc_scaled.powi(2) + dh_scaled.powi(2)).sqrt()
273 }
274}
275
276fn gamma_correct(linear: f64) -> f64 {
278 if linear <= 0.0031308 {
279 12.92 * linear
280 } else {
281 1.055 * linear.powf(1.0 / 2.4) - 0.055
282 }
283}
284
285fn inverse_gamma_correct(srgb: f64) -> f64 {
287 if srgb <= 0.04045 {
288 srgb / 12.92
289 } else {
290 ((srgb + 0.055) / 1.055).powf(2.4)
291 }
292}
293
294#[derive(Debug, Clone, PartialEq)]
296pub struct LabColor {
297 pub l: f64,
299 pub a: f64,
301 pub b: f64,
303 pub color_space: LabColorSpace,
305}
306
307impl LabColor {
308 pub fn new(l: f64, a: f64, b: f64, color_space: LabColorSpace) -> Self {
310 let l = l.clamp(0.0, 100.0);
312
313 let a = a.clamp(color_space.range[0], color_space.range[1]);
315 let b = b.clamp(color_space.range[2], color_space.range[3]);
316
317 Self {
318 l,
319 a,
320 b,
321 color_space,
322 }
323 }
324
325 pub fn with_default(l: f64, a: f64, b: f64) -> Self {
327 Self::new(l, a, b, LabColorSpace::default())
328 }
329
330 pub fn color_space_array(&self) -> Vec<Object> {
332 self.color_space.to_pdf_array()
333 }
334
335 pub fn values(&self) -> Vec<f64> {
337 let a_normalized = (self.a - self.color_space.range[0])
341 / (self.color_space.range[1] - self.color_space.range[0]);
342 let b_normalized = (self.b - self.color_space.range[2])
343 / (self.color_space.range[3] - self.color_space.range[2]);
344
345 vec![self.l / 100.0, a_normalized, b_normalized]
346 }
347
348 pub fn to_xyz(&self) -> [f64; 3] {
350 self.color_space.lab_to_xyz(self.l, self.a, self.b)
351 }
352
353 pub fn to_rgb(&self) -> [f64; 3] {
355 self.color_space.lab_to_rgb(self.l, self.a, self.b)
356 }
357
358 pub fn delta_e(&self, other: &LabColor) -> f64 {
360 self.color_space
361 .delta_e([self.l, self.a, self.b], [other.l, other.a, other.b])
362 }
363
364 pub fn delta_e_2000(&self, other: &LabColor) -> f64 {
366 self.color_space
367 .delta_e_2000([self.l, self.a, self.b], [other.l, other.a, other.b])
368 }
369}
370
371impl LabColor {
373 pub fn white() -> Self {
375 Self::with_default(100.0, 0.0, 0.0)
376 }
377
378 pub fn black() -> Self {
380 Self::with_default(0.0, 0.0, 0.0)
381 }
382
383 pub fn gray() -> Self {
385 Self::with_default(50.0, 0.0, 0.0)
386 }
387
388 pub fn red() -> Self {
390 Self::with_default(53.0, 80.0, 67.0)
391 }
392
393 pub fn green() -> Self {
395 Self::with_default(87.0, -86.0, 83.0)
396 }
397
398 pub fn blue() -> Self {
400 Self::with_default(32.0, 79.0, -108.0)
401 }
402
403 pub fn yellow() -> Self {
405 Self::with_default(97.0, -22.0, 94.0)
406 }
407
408 pub fn cyan() -> Self {
410 Self::with_default(91.0, -48.0, -14.0)
411 }
412
413 pub fn magenta() -> Self {
415 Self::with_default(60.0, 98.0, -61.0)
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn test_lab_default() {
425 let cs = LabColorSpace::new();
426
427 assert_eq!(cs.white_point, [0.9505, 1.0000, 1.0890]);
428 assert_eq!(cs.black_point, [0.0, 0.0, 0.0]);
429 assert_eq!(cs.range, [-100.0, 100.0, -100.0, 100.0]);
430 }
431
432 #[test]
433 fn test_lab_custom() {
434 let cs = LabColorSpace::new()
435 .with_white_point([0.95, 1.0, 1.09])
436 .with_black_point([0.01, 0.01, 0.01])
437 .with_range(-128.0, 127.0, -128.0, 127.0);
438
439 assert_eq!(cs.white_point, [0.95, 1.0, 1.09]);
440 assert_eq!(cs.black_point, [0.01, 0.01, 0.01]);
441 assert_eq!(cs.range, [-128.0, 127.0, -128.0, 127.0]);
442 }
443
444 #[test]
445 fn test_lab_to_pdf() {
446 let cs = LabColorSpace::new()
447 .with_range(-128.0, 127.0, -128.0, 127.0)
448 .with_black_point([0.01, 0.01, 0.01]);
449
450 let pdf_array = cs.to_pdf_array();
451
452 assert_eq!(pdf_array.len(), 2);
453 assert_eq!(pdf_array[0], Object::Name("Lab".to_string()));
454
455 if let Object::Dictionary(dict) = &pdf_array[1] {
456 assert!(dict.get("WhitePoint").is_some());
457 assert!(dict.get("Range").is_some());
458 assert!(dict.get("BlackPoint").is_some());
459 } else {
460 panic!("Second element should be a dictionary");
461 }
462 }
463
464 #[test]
465 fn test_lab_color_creation() {
466 let color = LabColor::with_default(50.0, 25.0, -25.0);
467
468 assert_eq!(color.l, 50.0);
469 assert_eq!(color.a, 25.0);
470 assert_eq!(color.b, -25.0);
471 }
472
473 #[test]
474 fn test_lab_color_clamping() {
475 let color = LabColor::with_default(150.0, 200.0, -200.0);
476
477 assert_eq!(color.l, 100.0); assert_eq!(color.a, 100.0); assert_eq!(color.b, -100.0); }
481
482 #[test]
483 fn test_lab_to_xyz_conversion() {
484 let cs = LabColorSpace::new();
485 let [_x, y, _z] = cs.lab_to_xyz(50.0, 0.0, 0.0);
486
487 assert!((y - 0.184).abs() < 0.01);
489 }
490
491 #[test]
492 fn test_xyz_to_lab_conversion() {
493 let cs = LabColorSpace::new();
494 let original_lab = [50.0, 25.0, -25.0];
495 let xyz = cs.lab_to_xyz(original_lab[0], original_lab[1], original_lab[2]);
496 let converted_lab = cs.xyz_to_lab(xyz[0], xyz[1], xyz[2]);
497
498 assert!((original_lab[0] - converted_lab[0]).abs() < 0.1);
500 assert!((original_lab[1] - converted_lab[1]).abs() < 0.1);
501 assert!((original_lab[2] - converted_lab[2]).abs() < 0.1);
502 }
503
504 #[test]
505 fn test_lab_to_rgb_approximation() {
506 let cs = LabColorSpace::new();
507
508 let rgb_white = cs.lab_to_rgb(100.0, 0.0, 0.0);
510 assert!(rgb_white[0] > 0.99);
511 assert!(rgb_white[1] > 0.99);
512 assert!(rgb_white[2] > 0.99);
513
514 let rgb_black = cs.lab_to_rgb(0.0, 0.0, 0.0);
516 assert!(rgb_black[0] < 0.01);
517 assert!(rgb_black[1] < 0.01);
518 assert!(rgb_black[2] < 0.01);
519 }
520
521 #[test]
522 fn test_delta_e() {
523 let cs = LabColorSpace::new();
524 let lab1 = [50.0, 0.0, 0.0];
525 let lab2 = [55.0, 0.0, 0.0];
526
527 let delta = cs.delta_e(lab1, lab2);
528 assert_eq!(delta, 5.0); }
530
531 #[test]
532 fn test_common_colors() {
533 let white = LabColor::white();
534 assert_eq!(white.l, 100.0);
535
536 let black = LabColor::black();
537 assert_eq!(black.l, 0.0);
538
539 let gray = LabColor::gray();
540 assert_eq!(gray.l, 50.0);
541 }
542
543 #[test]
544 fn test_d65_illuminant() {
545 let cs = LabColorSpace::d65();
546 assert_eq!(cs.white_point, [0.9504, 1.0000, 1.0888]);
547 }
548
549 #[test]
550 fn test_color_values_normalization() {
551 let cs = LabColorSpace::new().with_range(-128.0, 127.0, -128.0, 127.0);
552 let color = LabColor::new(50.0, 0.0, 0.0, cs);
553
554 let values = color.values();
555 assert_eq!(values[0], 0.5); assert!((values[1] - 0.5).abs() < 0.01); assert!((values[2] - 0.5).abs() < 0.01); }
559
560 #[test]
561 fn test_rgb_to_lab_conversion() {
562 let cs = LabColorSpace::new();
563
564 let lab = cs.rgb_to_lab(0.5, 0.5, 0.5);
566 assert!((lab[0] - 53.0).abs() < 2.0); assert!(lab[1].abs() < 1.0); assert!(lab[2].abs() < 1.0); }
570}