1 #![doc(html_root_url = "https://docs.rs/lab")]
2
3 /*!
4
5 # Lab
6
7 Tools for converting RGB colors to L\*a\*b\* measurements.
8
9 RGB colors, for this crate at least, are considered to be composed of `u8`
10 values from 0 to 255, while L\*a\*b\* colors are represented by its own struct
11 that uses `f32` values.
12
13 # Usage
14
15 ## Converting single values
16
17 To convert a single value, use one of the functions
18
19 * `lab::Lab::from_rgb(rgb: &[u8; 3]) -> Lab`
20 * `lab::Lab::from_rgba(rgba: &[u8; 4]) -> Lab` (drops the fourth alpha byte)
21 * `lab::Lab::to_rgb(&self) -> [u8; 3]`
22
23 ```rust
24 extern crate lab;
25 use lab::Lab;
26
27 let pink_in_lab = Lab::from_rgb(&[253, 120, 138]);
28 // Lab { l: 66.639084, a: 52.251457, b: 14.860654 }
29 ```
30
31 ## Converting multiple values
32
33 To convert slices of values
34
35 * `lab::rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab>`
36 * `lab::labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]>`
37 * `lab::rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab>`
38 * `lab::labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8>`
39
40 ```rust
41 extern crate lab;
42 use lab::rgbs_to_labs;
43
44 let rgbs = vec![
45 [0xFF, 0x69, 0xB6],
46 [0xE7, 0x00, 0x00],
47 [0xFF, 0x8C, 0x00],
48 [0xFF, 0xEF, 0x00],
49 [0x00, 0x81, 0x1F],
50 [0x00, 0xC1, 0xC1],
51 [0x00, 0x44, 0xFF],
52 [0x76, 0x00, 0x89],
53 ];
54
55 let labs = rgbs_to_labs(&rgbs);
56 ```
57
58 ```rust
59 extern crate lab;
60 use lab::rgb_bytes_to_labs;
61
62 let rgbs = vec![
63 0xFF, 0x69, 0xB6,
64 0xE7, 0x00, 0x00,
65 0xFF, 0x8C, 0x00,
66 0xFF, 0xEF, 0x00,
67 0x00, 0x81, 0x1F,
68 0x00, 0xC1, 0xC1,
69 0x00, 0x44, 0xFF,
70 0x76, 0x00, 0x89,
71 ];
72
73 let labs = rgb_bytes_to_labs(&rgbs);
74 ```
75
76 These functions will use x86_64 AVX2 instructions if compiled to a supported target.
77
78 ## Minimum Rust version
79
80 Lab 0.7.0 requires Rust >= 1.31.0 for the [chunks_exact](https://doc.rust-lang.org/std/primitive.slice.html#method.chunks_exact) slice method
81
82 */
83
84 #[cfg(test)]
85 #[macro_use]
86 extern crate pretty_assertions;
87 #[cfg(test)]
88 extern crate approx;
89 #[cfg(test)]
90 extern crate lazy_static;
91 #[cfg(test)]
92 extern crate rand;
93
94 #[cfg(test)]
95 mod approx_impl;
96
97 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
98 mod simd;
99
100 /// Struct representing a color in CIALab, a.k.a. L\*a\*b\*, color space
101 #[derive(Debug, PartialEq, Copy, Clone, Default)]
102 pub struct Lab {
103 pub l: f32,
104 pub a: f32,
105 pub b: f32,
106 }
107
108 /// Struct representing a color in cylindrical CIELCh color space
109 #[derive(Debug, PartialEq, Copy, Clone, Default)]
110 pub struct LCh {
111 pub l: f32,
112 pub c: f32,
113 pub h: f32,
114 }
115
116 // κ and ε parameters used in conversion between XYZ and La*b*. See
117 // http://www.brucelindbloom.com/LContinuity.html for explanation as to why
118 // those are different values than those provided by CIE standard.
119 pub(crate) const KAPPA: f32 = 24389.0 / 27.0;
120 pub(crate) const EPSILON: f32 = 216.0 / 24389.0;
121 pub(crate) const CBRT_EPSILON: f32 = 6.0 / 29.0;
122
123 // S₀ and E₀ thresholds used in sRGB gamma. The latter is scaled for encoded
124 // value in the range 0..255.
125 pub(crate) const S_0: f32 = 0.003130668442500564;
126 pub(crate) const E_0_255: f32 = 3294.6 * S_0;
127
128 // Generated by srgb-matrices.py
129 const WHITE_X: f32 = 0.9504492182750991;
130 const WHITE_Z: f32 = 1.0889166484304715;
131
rgb_to_lab(r: u8, g: u8, b: u8) -> Lab132 fn rgb_to_lab(r: u8, g: u8, b: u8) -> Lab {
133 xyz_to_lab(rgb_to_xyz(r, g, b))
134 }
135
rgb_to_xyz(r: u8, g: u8, b: u8) -> [f32; 3]136 fn rgb_to_xyz(r: u8, g: u8, b: u8) -> [f32; 3] {
137 rgb_to_xyz_inner(r as f32, g as f32, b as f32)
138 }
139
rgb_to_xyz_normalized(rgb: &[f32; 3]) -> [f32; 3]140 fn rgb_to_xyz_normalized(rgb: &[f32; 3]) -> [f32; 3] {
141 rgb_to_xyz_inner(rgb[0] * 255.0, rgb[1] * 255.0, rgb[2] * 255.0)
142 }
143
144 #[inline(always)]
145 #[cfg(any(target_feature = "fma", test))]
mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32146 fn mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32 {
147 a2.mul_add(b2, a1.mul_add(b1, a0 * b0))
148 }
149
150 #[inline(always)]
151 #[cfg(not(any(target_feature = "fma", test)))]
mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32152 fn mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32 {
153 a0 * b0 + a1 * b1 + a2 * b2
154 }
155
156 #[inline]
rgb_to_xyz_inner(r: f32, g: f32, b: f32) -> [f32; 3]157 fn rgb_to_xyz_inner(r: f32, g: f32, b: f32) -> [f32; 3] {
158 #[inline]
rgb_to_xyz_map(c: f32) -> f32159 fn rgb_to_xyz_map(c: f32) -> f32 {
160 if c > E_0_255 {
161 const A: f32 = 0.055 * 255.0;
162 const D: f32 = 1.055 * 255.0;
163 ((c + A) / D).powf(2.4)
164 } else {
165 const D: f32 = 12.92 * 255.0;
166 c / D
167 }
168 }
169
170 let r = rgb_to_xyz_map(r);
171 let g = rgb_to_xyz_map(g);
172 let b = rgb_to_xyz_map(b);
173
174 // Generated by srgb-matrices.py
175 let x = mul3(
176 r,
177 g,
178 b,
179 0.4124108464885388,
180 0.3575845678529519,
181 0.18045380393360833,
182 );
183 let y = mul3(
184 r,
185 g,
186 b,
187 0.21264934272065283,
188 0.7151691357059038,
189 0.07218152157344333,
190 );
191 let z = mul3(
192 r,
193 g,
194 b,
195 0.019331758429150258,
196 0.11919485595098397,
197 0.9503900340503373,
198 );
199
200 [x, y, z]
201 }
202
xyz_to_lab(xyz: [f32; 3]) -> Lab203 fn xyz_to_lab(xyz: [f32; 3]) -> Lab {
204 #[inline]
xyz_to_lab_map(c: f32) -> f32205 fn xyz_to_lab_map(c: f32) -> f32 {
206 if c > EPSILON {
207 c.powf(1.0 / 3.0)
208 } else {
209 (KAPPA * c + 16.0) / 116.0
210 }
211 }
212
213 // It’s tempting to replace the division with a multiplication by inverse,
214 // however that results in slightly worse test_grey_error benchmark.
215 let x = xyz_to_lab_map(xyz[0] / WHITE_X);
216 let y = xyz_to_lab_map(xyz[1]);
217 let z = xyz_to_lab_map(xyz[2] / WHITE_Z);
218
219 Lab {
220 l: (116.0 * y) - 16.0,
221 a: 500.0 * (x - y),
222 b: 200.0 * (y - z),
223 }
224 }
225
lab_to_xyz(lab: &Lab) -> [f32; 3]226 fn lab_to_xyz(lab: &Lab) -> [f32; 3] {
227 let fy = (lab.l + 16.0) / 116.0;
228 let fx = (lab.a / 500.0) + fy;
229 let fz = fy - (lab.b / 200.0);
230 let xr = if fx > CBRT_EPSILON {
231 fx.powi(3)
232 } else {
233 ((fx * 116.0) - 16.0) / KAPPA
234 };
235 let yr = if lab.l > EPSILON * KAPPA {
236 fy.powi(3)
237 } else {
238 lab.l / KAPPA
239 };
240 let zr = if fz > CBRT_EPSILON {
241 fz.powi(3)
242 } else {
243 ((fz * 116.0) - 16.0) / KAPPA
244 };
245
246 [xr * WHITE_X, yr, zr * WHITE_Z]
247 }
248
xyz_to_rgb(xyz: [f32; 3]) -> [u8; 3]249 fn xyz_to_rgb(xyz: [f32; 3]) -> [u8; 3] {
250 let rgb = xyz_to_rgb_normalized(xyz);
251 [
252 (rgb[0] * 255.0).round() as u8,
253 (rgb[1] * 255.0).round() as u8,
254 (rgb[2] * 255.0).round() as u8,
255 ]
256 }
257
xyz_to_rgb_normalized(xyz: [f32; 3]) -> [f32; 3]258 fn xyz_to_rgb_normalized(xyz: [f32; 3]) -> [f32; 3] {
259 let x = xyz[0];
260 let y = xyz[1];
261 let z = xyz[2];
262
263 // Generated by srgb-matrices.py
264 let r = mul3(
265 x,
266 y,
267 z,
268 3.240812398895283,
269 -1.5373084456298136,
270 -0.4985865229069666,
271 );
272 let g = mul3(
273 x,
274 y,
275 z,
276 -0.9692430170086407,
277 1.8759663029085742,
278 0.04155503085668564,
279 );
280 let b = mul3(
281 x,
282 y,
283 z,
284 0.055638398436112804,
285 -0.20400746093241362,
286 1.0571295702861434,
287 );
288
289 #[inline]
xyz_to_rgb_map(c: f32) -> f32290 fn xyz_to_rgb_map(c: f32) -> f32 {
291 (if c > S_0 {
292 1.055 * c.powf(1.0 / 2.4) - 0.055
293 } else {
294 12.92 * c
295 })
296 .min(1.0)
297 .max(0.0)
298 }
299
300 [xyz_to_rgb_map(r), xyz_to_rgb_map(g), xyz_to_rgb_map(b)]
301 }
302
303 /// Convenience function to map a slice of RGB values to Lab values in serial
304 ///
305 /// # Example
306 /// ```
307 /// # extern crate lab;
308 /// # use lab::{Lab, rgbs_to_labs};
309 /// let rgbs = &[[255u8, 0, 0], [255, 0, 255], [0, 255, 255]];
310 /// let labs = lab::rgbs_to_labs(rgbs);
311 /// assert_eq!(labs, vec![
312 /// Lab { l: 53.238235, a: 80.09231, b: 67.202095 },
313 /// Lab { l: 60.322693, a: 98.23698, b: -60.827957 },
314 /// Lab { l: 91.11428, a: -48.08274, b: -14.12958 }
315 /// ]);
316 /// ```
317 #[inline]
rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab>318 pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab> {
319 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
320 let labs = simd::rgbs_to_labs(rgbs);
321
322 #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
323 let labs = __scalar::rgbs_to_labs(rgbs);
324
325 labs
326 }
327
328 /// RGB to Lab conversion that operates on a flat `&[u8]` of consecutive RGB triples.
329 ///
330 /// # Example
331 /// ```
332 /// # extern crate lab;
333 /// # use lab::{Lab, rgb_bytes_to_labs};
334 /// let rgbs = &[255u8, 0, 0, 255, 0, 255, 0, 255, 255];
335 /// let labs = lab::rgb_bytes_to_labs(rgbs);
336 /// assert_eq!(labs, vec![
337 /// Lab { l: 53.238235, a: 80.09231, b: 67.202095 },
338 /// Lab { l: 60.322693, a: 98.23698, b: -60.827957 },
339 /// Lab { l: 91.11428, a: -48.08274, b: -14.12958 }
340 /// ]);
341 /// ```
rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab>342 pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab> {
343 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
344 let labs = simd::rgb_bytes_to_labs(bytes);
345
346 #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
347 let labs = __scalar::rgb_bytes_to_labs(bytes);
348
349 labs
350 }
351
352 /// Convenience function to map a slice of Lab values to RGB values in serial
353 ///
354 /// # Example
355 /// ```
356 /// # extern crate lab;
357 /// # use lab::{Lab, labs_to_rgbs};
358 /// let labs = &[
359 /// Lab { l: 91.11321, a: -48.08751, b: -14.131201 },
360 /// Lab { l: 60.32421, a: 98.23433, b: -60.824894 },
361 /// Lab { l: 97.13926, a: -21.553724, b: 94.47797 },
362 /// ];
363 /// let rgbs = lab::labs_to_rgbs(labs);
364 /// assert_eq!(rgbs, vec![[0u8, 255, 255], [255, 0, 255], [255, 255, 0]]);
365 /// ```
366 #[inline]
labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]>367 pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> {
368 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
369 let rgbs = simd::labs_to_rgbs(labs);
370
371 #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
372 let rgbs = __scalar::labs_to_rgbs(labs);
373
374 rgbs
375 }
376
377 /// Lab to RGB conversion that returns RGB triples flattened into a `Vec<u8>`
378 ///
379 /// # Example
380 /// ```
381 /// # extern crate lab;
382 /// # use lab::{Lab, labs_to_rgb_bytes};
383 /// let labs = &[
384 /// Lab { l: 91.11321, a: -48.08751, b: -14.131201 },
385 /// Lab { l: 60.32421, a: 98.23433, b: -60.824894 },
386 /// Lab { l: 97.13926, a: -21.553724, b: 94.47797 },
387 /// ];
388 /// let rgb_bytes = lab::labs_to_rgb_bytes(labs);
389 /// assert_eq!(rgb_bytes, vec![0, 255, 255, 255, 0, 255, 255, 255, 0]);
390 /// ```
391 #[inline]
labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8>392 pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8> {
393 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
394 let bytes = simd::labs_to_rgb_bytes(labs);
395
396 #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
397 let bytes = __scalar::labs_to_rgb_bytes(labs);
398
399 bytes
400 }
401
402 #[doc(hidden)]
403 pub mod __scalar {
404 use rgb_to_lab;
405 use Lab;
406
407 #[inline]
labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]>408 pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> {
409 labs.iter().map(Lab::to_rgb).collect()
410 }
411
412 #[inline]
labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8>413 pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8> {
414 labs.iter()
415 .map(Lab::to_rgb)
416 .fold(Vec::with_capacity(labs.len() * 3), |mut acc, rgb| {
417 acc.extend_from_slice(&rgb);
418 acc
419 })
420 }
421
422 #[inline]
rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab>423 pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab> {
424 rgbs.iter().map(Lab::from_rgb).collect()
425 }
426
427 #[inline]
rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab>428 pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab> {
429 bytes
430 .chunks_exact(3)
431 .map(|rgb| rgb_to_lab(rgb[0], rgb[1], rgb[2]))
432 .collect()
433 }
434 }
435
436 impl Lab {
437 /// Constructs a new `Lab` from a three-element array of `u8`s
438 ///
439 /// # Examples
440 ///
441 /// ```
442 /// let lab = lab::Lab::from_rgb(&[240, 33, 95]);
443 /// assert_eq!(lab::Lab { l: 52.334686, a: 75.55157, b: 19.995684 }, lab);
444 /// ```
from_rgb(rgb: &[u8; 3]) -> Self445 pub fn from_rgb(rgb: &[u8; 3]) -> Self {
446 rgb_to_lab(rgb[0], rgb[1], rgb[2])
447 }
448
449 #[doc(hidden)]
from_rgb_normalized(rgb: &[f32; 3]) -> Self450 pub fn from_rgb_normalized(rgb: &[f32; 3]) -> Self {
451 xyz_to_lab(rgb_to_xyz_normalized(rgb))
452 }
453
454 /// Constructs a new `Lab` from a four-element array of `u8`s
455 ///
456 /// The `Lab` struct does not store alpha channel information, so the last
457 /// `u8` representing alpha is discarded. This convenience method exists
458 /// in order to easily measure colors already stored in an RGBA array.
459 ///
460 /// # Examples
461 ///
462 /// ```
463 /// let lab = lab::Lab::from_rgba(&[240, 33, 95, 255]);
464 /// assert_eq!(lab::Lab { l: 52.334686, a: 75.55157, b: 19.995684 }, lab);
465 /// ```
from_rgba(rgba: &[u8; 4]) -> Self466 pub fn from_rgba(rgba: &[u8; 4]) -> Self {
467 Lab::from_rgb(&[rgba[0], rgba[1], rgba[2]])
468 }
469
470 #[doc(hidden)]
from_rgba_normalized(rgba: &[f32; 4]) -> Self471 pub fn from_rgba_normalized(rgba: &[f32; 4]) -> Self {
472 Lab::from_rgb_normalized(&[rgba[0], rgba[1], rgba[2]])
473 }
474
475 /// Returns the `Lab`'s color in RGB, in a 3-element array.
476 ///
477 /// # Examples
478 ///
479 /// ```
480 /// let lab = lab::Lab { l: 52.330193, a: 75.56704, b: 19.989174 };
481 /// let rgb = lab.to_rgb();
482 /// assert_eq!([240, 33, 95], rgb);
483 /// ```
to_rgb(&self) -> [u8; 3]484 pub fn to_rgb(&self) -> [u8; 3] {
485 xyz_to_rgb(lab_to_xyz(self))
486 }
487
488 #[doc(hidden)]
to_rgb_normalized(&self) -> [f32; 3]489 pub fn to_rgb_normalized(&self) -> [f32; 3] {
490 xyz_to_rgb_normalized(lab_to_xyz(self))
491 }
492
493 /// Measures the perceptual distance between the colors of one `Lab`
494 /// and an `other`.
495 ///
496 /// # Examples
497 ///
498 /// ```
499 /// # use lab::Lab;
500 /// let pink = Lab { l: 66.6348, a: 52.260696, b: 14.850557 };
501 /// let websafe_pink = Lab { l: 64.2116, a: 62.519463, b: 2.8871894 };
502 /// assert_eq!(254.23636, pink.squared_distance(&websafe_pink));
503 /// ```
squared_distance(&self, other: &Lab) -> f32504 pub fn squared_distance(&self, other: &Lab) -> f32 {
505 (self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2)
506 }
507 }
508
509 impl LCh {
510 /// Constructs a new `LCh` from a three-element array of `u8`s
511 ///
512 /// # Examples
513 ///
514 /// ```
515 /// let lch = lab::LCh::from_rgb(&[240, 33, 95]);
516 /// assert_eq!(lab::LCh { l: 52.334686, c: 78.15284, h: 0.25873056 }, lch);
517 /// ```
from_rgb(rgb: &[u8; 3]) -> Self518 pub fn from_rgb(rgb: &[u8; 3]) -> Self {
519 LCh::from_lab(Lab::from_rgb(rgb))
520 }
521
522 /// Constructs a new `LCh` from a four-element array of `u8`s
523 ///
524 /// The `LCh` struct does not store alpha channel information, so the last
525 /// `u8` representing alpha is discarded. This convenience method exists
526 /// in order to easily measure colors already stored in an RGBA array.
527 ///
528 /// # Examples
529 ///
530 /// ```
531 /// let lch = lab::LCh::from_rgba(&[240, 33, 95, 255]);
532 /// assert_eq!(lab::LCh { l: 52.334686, c: 78.15284, h: 0.25873056 }, lch);
533 /// ```
from_rgba(rgba: &[u8; 4]) -> Self534 pub fn from_rgba(rgba: &[u8; 4]) -> Self {
535 LCh::from_lab(Lab::from_rgba(rgba))
536 }
537
538 /// Constructs a new `LCh` from a `Lab`
539 ///
540 /// # Examples
541 ///
542 /// ```
543 /// let lab = lab::Lab { l: 52.33686, a: 75.5516, b: 19.998878 };
544 /// let lch = lab::LCh::from_lab(lab);
545 /// assert_eq!(lab::LCh { l: 52.33686, c: 78.15369, h: 0.25877 }, lch);
546 ///
547 /// let lab = lab::Lab { l: 52.33686, a: 0.0, b: 0.0 };
548 /// let lch = lab::LCh::from_lab(lab);
549 /// assert_eq!(lab::LCh { l: 52.33686, c: 0.0, h: 0.0 }, lch);
550 /// ```
from_lab(lab: Lab) -> Self551 pub fn from_lab(lab: Lab) -> Self {
552 LCh {
553 l: lab.l,
554 c: lab.a.hypot(lab.b),
555 h: lab.b.atan2(lab.a),
556 }
557 }
558
559 /// Returns the `LCh`'s color in RGB, in a 3-element array
560 ///
561 /// # Examples
562 ///
563 /// ```
564 /// let mut lch = lab::LCh { l: 52.33686, c: 78.15369, h: 0.25877 };
565 /// assert_eq!([240, 33, 95], lch.to_rgb());
566 ///
567 /// lch.h += std::f32::consts::TAU;
568 /// assert_eq!([240, 33, 95], lch.to_rgb());
569 /// ```
to_rgb(&self) -> [u8; 3]570 pub fn to_rgb(&self) -> [u8; 3] {
571 self.to_lab().to_rgb()
572 }
573
574 /// Returns the `LCh`'s color in `Lab`
575 ///
576 /// Note that due to imprecision of floating point arithmetic, conversions
577 /// between Lab and LCh are not stable. A chain of Lab→LCh→Lab or
578 /// LCh→Lab→LCh operations isn’t guaranteed to give back the source colour.
579 ///
580 /// # Examples
581 ///
582 /// ```
583 /// let lch = lab::LCh { l: 52.33686, c: 78.15369, h: 0.25877 };
584 /// let lab = lch.to_lab();
585 /// assert_eq!(lab::Lab { l: 52.33686, a: 75.5516, b: 19.998878 }, lab);
586 ///
587 /// let lch = lab::LCh { l: 52.33686, c: 0.0, h: 0.25877 };
588 /// let lab = lch.to_lab();
589 /// assert_eq!(lab::Lab { l: 52.33686, a: 0.0, b: 0.0 }, lab);
590 ///
591 /// let inp = lab::Lab { l: 29.52658, a: 58.595745, b: -36.281406 };
592 /// let lch = lab::LCh { l: 29.52658, c: 68.91881, h: -0.5544043 };
593 /// let out = lab::Lab { l: 29.52658, a: 58.59575, b: -36.281406 };
594 /// assert_eq!(lch, lab::LCh::from_lab(inp));
595 /// assert_eq!(out, lch.to_lab());
596 /// ```
to_lab(&self) -> Lab597 pub fn to_lab(&self) -> Lab {
598 Lab {
599 l: self.l,
600 a: self.c * self.h.cos(),
601 b: self.c * self.h.sin(),
602 }
603 }
604 }
605
606 #[cfg(test)]
607 mod tests {
608 use super::{labs_to_rgbs, rgbs_to_labs, LCh, Lab};
609 use approx::assert_relative_eq;
610 use rand::Rng;
611
612 const PINK: Lab = Lab {
613 l: 66.637695,
614 a: 52.250145,
615 b: 14.858591,
616 };
617
618 #[rustfmt::skip]
619 static COLOURS: [([u8; 3], Lab, LCh); 17] = [
620 ([253, 120, 138],
621 PINK,
622 LCh { l: 66.637695, c: 54.321777, h: 0.2770602 }),
623
624 ([127, 0, 0],
625 Lab { l: 25.299877, a: 47.77421, b: 37.752514 },
626 LCh { l: 25.299877, c: 60.890293, h: 0.66875386 }),
627 ([ 0, 127, 0],
628 Lab { l: 45.87715, a: -51.405922, b: 49.61748 },
629 LCh { l: 45.87715, c: 71.445526, h: 2.373896 }),
630 ([ 0, 0, 127],
631 Lab { l: 12.809523, a: 47.237186, b: -64.33636 },
632 LCh { l: 12.809523, c: 79.81553, h: -0.93746966 }),
633 ([ 0, 127, 127],
634 Lab { l: 47.892532, a: -28.680845, b: -8.428156 },
635 LCh { l: 47.892532, c: 29.893557, h: -2.8557782 }),
636 ([127, 0, 127],
637 Lab { l: 29.525677, a: 58.597298, b: -36.28323 },
638 LCh { l: 29.525677, c: 68.92109, h: -0.554415 }),
639 ([255, 0, 0],
640 Lab { l: 53.238235, a: 80.09231, b: 67.202095 },
641 LCh { l: 53.238235, c: 104.55094, h: 0.6981073 }),
642 ([ 0, 255, 0],
643 Lab { l: 87.73554, a: -86.18078, b: 83.18251 },
644 LCh { l: 87.73554, c: 119.776695, h: 2.373896 }),
645 ([ 0, 0, 255],
646 Lab { l: 32.298466, a: 79.192, b: -107.858345 },
647 LCh { l: 32.298466, c: 133.8088, h: -0.93746966 }),
648 ([ 0, 255, 255],
649 Lab { l: 91.11428, a: -48.08274, b: -14.12958 },
650 LCh { l: 91.11428, c: 50.115814, h: -2.8557787 }),
651 ([255, 0, 255],
652 Lab { l: 60.322693, a: 98.23698, b: -60.827957 },
653 LCh { l: 60.322693, c: 115.544556, h: -0.55441487 }),
654 ([255, 255, 0],
655 Lab { l: 97.139, a: -21.556675, b: 94.48001 },
656 LCh { l: 97.139, c: 96.90801, h: 1.7951176 }),
657
658 ([ 0, 0, 0],
659 Lab { l: 0.0, a: 0.0, b: 0.0 },
660 LCh { l: 0.0, c: 0.0, h: 0.0 }),
661 ([ 64, 64, 64],
662 Lab { l: 27.09341, a: 0.0, b: 0.0 },
663 LCh { l: 27.09341, c: 0.0, h: 0.0 }),
664 ([127, 127, 127],
665 Lab { l: 53.192772, a: 0.0, b: 0.0 },
666 LCh { l: 53.192772, c: 0.0, h: 0.0 }),
667 ([196, 196, 196],
668 Lab { l: 79.15698, a: 0.0, b: 0.0 },
669 LCh { l: 79.15698, c: 0.0, h: 0.0 }),
670 ([255, 255, 255],
671 Lab { l: 100.0, a: 0.0, b: 0.0 },
672 LCh { l: 100.0, c: 0.0, h: 0.0 }),
673 ];
674
675 #[test]
test_lab_from_rgb()676 fn test_lab_from_rgb() {
677 let expected: Vec<_> = COLOURS.iter().map(|(_, lab, _)| *lab).collect();
678 let actual: Vec<_> = COLOURS
679 .iter()
680 .map(|(rgb, _, _)| Lab::from_rgb(rgb))
681 .collect();
682 assert_eq!(expected, actual);
683 }
684
685 #[test]
test_lab_to_rgb()686 fn test_lab_to_rgb() {
687 let expected: Vec<_> = COLOURS.iter().map(|(rgb, _, _)| *rgb).collect();
688 let actual: Vec<_> = COLOURS.iter().map(|(_, lab, _)| lab.to_rgb()).collect();
689 assert_eq!(expected, actual);
690 }
691
692 #[test]
test_lch_from_rgb()693 fn test_lch_from_rgb() {
694 let expected: Vec<_> = COLOURS.iter().map(|(_, _, lch)| *lch).collect();
695 let actual: Vec<_> = COLOURS
696 .iter()
697 .map(|(rgb, _, _)| LCh::from_rgb(rgb))
698 .collect();
699 assert_relative_eq!(expected.as_slice(), actual.as_slice());
700 }
701
702 #[test]
test_lch_to_rgb()703 fn test_lch_to_rgb() {
704 let expected: Vec<_> = COLOURS.iter().map(|(rgb, _, _)| *rgb).collect();
705 let actual: Vec<_> = COLOURS.iter().map(|(_, _, lch)| lch.to_rgb()).collect();
706 assert_eq!(expected, actual);
707 }
708
709 #[test]
test_lch_from_lab()710 fn test_lch_from_lab() {
711 let expected: Vec<_> = COLOURS.iter().map(|(_, _, lch)| *lch).collect();
712 let actual: Vec<_> = COLOURS
713 .iter()
714 .map(|(_, lab, _)| LCh::from_lab(*lab))
715 .collect();
716 assert_relative_eq!(expected.as_slice(), actual.as_slice());
717 }
718
719 #[test]
test_lch_to_lab()720 fn test_lch_to_lab() {
721 let mut expected: Vec<_> = COLOURS.iter().map(|(_, lab, _)| *lab).collect();
722 let mut actual: Vec<_> = COLOURS.iter().map(|(_, _, lch)| lch.to_lab()).collect();
723
724 // Floating point arithmetic is hard. Due to accumulation of errors (or
725 // perhaps imprecision of trig functions) the Lab→LCh→Lab conversion
726 // produces slightly different colour than what the source. Round a*
727 // and b* to four decimal places to work around this.
728 fn round(vec: &mut Vec<Lab>) {
729 for lab in vec.iter_mut() {
730 lab.a = (lab.a * 100000.0).round() / 100000.0;
731 lab.b = (lab.b * 100000.0).round() / 100000.0;
732 }
733 }
734 round(&mut expected);
735 round(&mut actual);
736
737 assert_eq!(expected, actual);
738 }
739
740 #[test]
test_distance()741 fn test_distance() {
742 let ugly_websafe_pink = Lab {
743 l: 64.2116,
744 a: 62.519463,
745 b: 2.8871894,
746 };
747 assert_eq!(254.65927, PINK.squared_distance(&ugly_websafe_pink));
748 }
749
750 #[test]
test_send()751 fn test_send() {
752 fn assert_send<T: Send>() {}
753 assert_send::<Lab>();
754 }
755
756 #[test]
test_sync()757 fn test_sync() {
758 fn assert_sync<T: Sync>() {}
759 assert_sync::<Lab>();
760 }
761
762 #[test]
test_rgb_to_lab_to_rgb()763 fn test_rgb_to_lab_to_rgb() {
764 let rgbs: Vec<[u8; 3]> = {
765 let rand_seed = [1u8; 32];
766 let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed);
767 rng.sample_iter(&rand::distributions::Standard)
768 .take(2048)
769 .collect()
770 };
771 let labs = rgbs_to_labs(&rgbs);
772 let rgbs2 = labs_to_rgbs(&labs);
773 assert_eq!(rgbs2, rgbs);
774 }
775
776 #[test]
test_grey_error()777 fn test_grey_error() {
778 // Grey colours have a* and b* components equal to zero. This test goes
779 // through all 8-bit greys and calculates squared error. If it goes up,
780 // a change might have worsen the precision of the calculations. If it
781 // goes down, calculations got better.
782 let mut error: f64 = 0.0;
783 let mut count: usize = 0;
784 for i in 0..=255_u32 {
785 let lab = Lab::from_rgb(&[i as u8, i as u8, i as u8]);
786 if lab.a != 0.0 || lab.b != 0.0 {
787 error = (lab.a as f64).mul_add(lab.a as f64, error);
788 error = (lab.b as f64).mul_add(lab.b as f64, error);
789 count += 1;
790 }
791 }
792 assert_eq!((23, 10.627054791711998), (count, error * 1e9));
793 }
794 }
795