#![doc(html_root_url = "https://docs.rs/lab")]
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[cfg(test)]
extern crate approx;
#[cfg(test)]
extern crate lazy_static;
#[cfg(test)]
extern crate rand;
#[cfg(test)]
mod approx_impl;
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
mod simd;
#[derive(Debug, PartialEq, Copy, Clone, Default)]
pub struct Lab {
pub l: f32,
pub a: f32,
pub b: f32,
}
#[derive(Debug, PartialEq, Copy, Clone, Default)]
pub struct LCh {
pub l: f32,
pub c: f32,
pub h: f32,
}
pub(crate) const KAPPA: f32 = 24389.0 / 27.0;
pub(crate) const EPSILON: f32 = 216.0 / 24389.0;
pub(crate) const CBRT_EPSILON: f32 = 6.0 / 29.0;
pub(crate) const S_0: f32 = 0.003130668442500564;
pub(crate) const E_0_255: f32 = 3294.6 * S_0;
const WHITE_X: f32 = 0.9504492182750991;
const WHITE_Z: f32 = 1.0889166484304715;
fn rgb_to_lab(r: u8, g: u8, b: u8) -> Lab {
xyz_to_lab(rgb_to_xyz(r, g, b))
}
fn rgb_to_xyz(r: u8, g: u8, b: u8) -> [f32; 3] {
rgb_to_xyz_inner(r as f32, g as f32, b as f32)
}
fn rgb_to_xyz_normalized(rgb: &[f32; 3]) -> [f32; 3] {
rgb_to_xyz_inner(rgb[0] * 255.0, rgb[1] * 255.0, rgb[2] * 255.0)
}
#[inline(always)]
#[cfg(any(target_feature = "fma", test))]
fn mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32 {
a2.mul_add(b2, a1.mul_add(b1, a0 * b0))
}
#[inline(always)]
#[cfg(not(any(target_feature = "fma", test)))]
fn mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32 {
a0 * b0 + a1 * b1 + a2 * b2
}
#[inline]
fn rgb_to_xyz_inner(r: f32, g: f32, b: f32) -> [f32; 3] {
#[inline]
fn rgb_to_xyz_map(c: f32) -> f32 {
if c > E_0_255 {
const A: f32 = 0.055 * 255.0;
const D: f32 = 1.055 * 255.0;
((c + A) / D).powf(2.4)
} else {
const D: f32 = 12.92 * 255.0;
c / D
}
}
let r = rgb_to_xyz_map(r);
let g = rgb_to_xyz_map(g);
let b = rgb_to_xyz_map(b);
let x = mul3(
r,
g,
b,
0.4124108464885388,
0.3575845678529519,
0.18045380393360833,
);
let y = mul3(
r,
g,
b,
0.21264934272065283,
0.7151691357059038,
0.07218152157344333,
);
let z = mul3(
r,
g,
b,
0.019331758429150258,
0.11919485595098397,
0.9503900340503373,
);
[x, y, z]
}
fn xyz_to_lab(xyz: [f32; 3]) -> Lab {
#[inline]
fn xyz_to_lab_map(c: f32) -> f32 {
if c > EPSILON {
c.powf(1.0 / 3.0)
} else {
(KAPPA * c + 16.0) / 116.0
}
}
let x = xyz_to_lab_map(xyz[0] / WHITE_X);
let y = xyz_to_lab_map(xyz[1]);
let z = xyz_to_lab_map(xyz[2] / WHITE_Z);
Lab {
l: (116.0 * y) - 16.0,
a: 500.0 * (x - y),
b: 200.0 * (y - z),
}
}
fn lab_to_xyz(lab: &Lab) -> [f32; 3] {
let fy = (lab.l + 16.0) / 116.0;
let fx = (lab.a / 500.0) + fy;
let fz = fy - (lab.b / 200.0);
let xr = if fx > CBRT_EPSILON {
fx.powi(3)
} else {
((fx * 116.0) - 16.0) / KAPPA
};
let yr = if lab.l > EPSILON * KAPPA {
fy.powi(3)
} else {
lab.l / KAPPA
};
let zr = if fz > CBRT_EPSILON {
fz.powi(3)
} else {
((fz * 116.0) - 16.0) / KAPPA
};
[xr * WHITE_X, yr, zr * WHITE_Z]
}
fn xyz_to_rgb(xyz: [f32; 3]) -> [u8; 3] {
let rgb = xyz_to_rgb_normalized(xyz);
[
(rgb[0] * 255.0).round() as u8,
(rgb[1] * 255.0).round() as u8,
(rgb[2] * 255.0).round() as u8,
]
}
fn xyz_to_rgb_normalized(xyz: [f32; 3]) -> [f32; 3] {
let x = xyz[0];
let y = xyz[1];
let z = xyz[2];
let r = mul3(
x,
y,
z,
3.240812398895283,
-1.5373084456298136,
-0.4985865229069666,
);
let g = mul3(
x,
y,
z,
-0.9692430170086407,
1.8759663029085742,
0.04155503085668564,
);
let b = mul3(
x,
y,
z,
0.055638398436112804,
-0.20400746093241362,
1.0571295702861434,
);
#[inline]
fn xyz_to_rgb_map(c: f32) -> f32 {
(if c > S_0 {
1.055 * c.powf(1.0 / 2.4) - 0.055
} else {
12.92 * c
})
.min(1.0)
.max(0.0)
}
[xyz_to_rgb_map(r), xyz_to_rgb_map(g), xyz_to_rgb_map(b)]
}
#[inline]
pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab> {
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
let labs = simd::rgbs_to_labs(rgbs);
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
let labs = __scalar::rgbs_to_labs(rgbs);
labs
}
pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab> {
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
let labs = simd::rgb_bytes_to_labs(bytes);
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
let labs = __scalar::rgb_bytes_to_labs(bytes);
labs
}
#[inline]
pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> {
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
let rgbs = simd::labs_to_rgbs(labs);
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
let rgbs = __scalar::labs_to_rgbs(labs);
rgbs
}
#[inline]
pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8> {
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
let bytes = simd::labs_to_rgb_bytes(labs);
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
let bytes = __scalar::labs_to_rgb_bytes(labs);
bytes
}
#[doc(hidden)]
pub mod __scalar {
use rgb_to_lab;
use Lab;
#[inline]
pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> {
labs.iter().map(Lab::to_rgb).collect()
}
#[inline]
pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec<u8> {
labs.iter()
.map(Lab::to_rgb)
.fold(Vec::with_capacity(labs.len() * 3), |mut acc, rgb| {
acc.extend_from_slice(&rgb);
acc
})
}
#[inline]
pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec<Lab> {
rgbs.iter().map(Lab::from_rgb).collect()
}
#[inline]
pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec<Lab> {
bytes
.chunks_exact(3)
.map(|rgb| rgb_to_lab(rgb[0], rgb[1], rgb[2]))
.collect()
}
}
impl Lab {
pub fn from_rgb(rgb: &[u8; 3]) -> Self {
rgb_to_lab(rgb[0], rgb[1], rgb[2])
}
#[doc(hidden)]
pub fn from_rgb_normalized(rgb: &[f32; 3]) -> Self {
xyz_to_lab(rgb_to_xyz_normalized(rgb))
}
pub fn from_rgba(rgba: &[u8; 4]) -> Self {
Lab::from_rgb(&[rgba[0], rgba[1], rgba[2]])
}
#[doc(hidden)]
pub fn from_rgba_normalized(rgba: &[f32; 4]) -> Self {
Lab::from_rgb_normalized(&[rgba[0], rgba[1], rgba[2]])
}
pub fn to_rgb(&self) -> [u8; 3] {
xyz_to_rgb(lab_to_xyz(self))
}
#[doc(hidden)]
pub fn to_rgb_normalized(&self) -> [f32; 3] {
xyz_to_rgb_normalized(lab_to_xyz(self))
}
pub fn squared_distance(&self, other: &Lab) -> f32 {
(self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2)
}
}
impl LCh {
pub fn from_rgb(rgb: &[u8; 3]) -> Self {
LCh::from_lab(Lab::from_rgb(rgb))
}
pub fn from_rgba(rgba: &[u8; 4]) -> Self {
LCh::from_lab(Lab::from_rgba(rgba))
}
pub fn from_lab(lab: Lab) -> Self {
LCh {
l: lab.l,
c: lab.a.hypot(lab.b),
h: lab.b.atan2(lab.a),
}
}
pub fn to_rgb(&self) -> [u8; 3] {
self.to_lab().to_rgb()
}
pub fn to_lab(&self) -> Lab {
Lab {
l: self.l,
a: self.c * self.h.cos(),
b: self.c * self.h.sin(),
}
}
}
#[cfg(test)]
mod tests {
use super::{labs_to_rgbs, rgbs_to_labs, LCh, Lab};
use approx::assert_relative_eq;
use rand::Rng;
const PINK: Lab = Lab {
l: 66.637695,
a: 52.250145,
b: 14.858591,
};
#[rustfmt::skip]
static COLOURS: [([u8; 3], Lab, LCh); 17] = [
([253, 120, 138],
PINK,
LCh { l: 66.637695, c: 54.321777, h: 0.2770602 }),
([127, 0, 0],
Lab { l: 25.299877, a: 47.77421, b: 37.752514 },
LCh { l: 25.299877, c: 60.890293, h: 0.66875386 }),
([ 0, 127, 0],
Lab { l: 45.87715, a: -51.405922, b: 49.61748 },
LCh { l: 45.87715, c: 71.445526, h: 2.373896 }),
([ 0, 0, 127],
Lab { l: 12.809523, a: 47.237186, b: -64.33636 },
LCh { l: 12.809523, c: 79.81553, h: -0.93746966 }),
([ 0, 127, 127],
Lab { l: 47.892532, a: -28.680845, b: -8.428156 },
LCh { l: 47.892532, c: 29.893557, h: -2.8557782 }),
([127, 0, 127],
Lab { l: 29.525677, a: 58.597298, b: -36.28323 },
LCh { l: 29.525677, c: 68.92109, h: -0.554415 }),
([255, 0, 0],
Lab { l: 53.238235, a: 80.09231, b: 67.202095 },
LCh { l: 53.238235, c: 104.55094, h: 0.6981073 }),
([ 0, 255, 0],
Lab { l: 87.73554, a: -86.18078, b: 83.18251 },
LCh { l: 87.73554, c: 119.776695, h: 2.373896 }),
([ 0, 0, 255],
Lab { l: 32.298466, a: 79.192, b: -107.858345 },
LCh { l: 32.298466, c: 133.8088, h: -0.93746966 }),
([ 0, 255, 255],
Lab { l: 91.11428, a: -48.08274, b: -14.12958 },
LCh { l: 91.11428, c: 50.115814, h: -2.8557787 }),
([255, 0, 255],
Lab { l: 60.322693, a: 98.23698, b: -60.827957 },
LCh { l: 60.322693, c: 115.544556, h: -0.55441487 }),
([255, 255, 0],
Lab { l: 97.139, a: -21.556675, b: 94.48001 },
LCh { l: 97.139, c: 96.90801, h: 1.7951176 }),
([ 0, 0, 0],
Lab { l: 0.0, a: 0.0, b: 0.0 },
LCh { l: 0.0, c: 0.0, h: 0.0 }),
([ 64, 64, 64],
Lab { l: 27.09341, a: 0.0, b: 0.0 },
LCh { l: 27.09341, c: 0.0, h: 0.0 }),
([127, 127, 127],
Lab { l: 53.192772, a: 0.0, b: 0.0 },
LCh { l: 53.192772, c: 0.0, h: 0.0 }),
([196, 196, 196],
Lab { l: 79.15698, a: 0.0, b: 0.0 },
LCh { l: 79.15698, c: 0.0, h: 0.0 }),
([255, 255, 255],
Lab { l: 100.0, a: 0.0, b: 0.0 },
LCh { l: 100.0, c: 0.0, h: 0.0 }),
];
#[test]
fn test_lab_from_rgb() {
let expected: Vec<_> = COLOURS.iter().map(|(_, lab, _)| *lab).collect();
let actual: Vec<_> = COLOURS
.iter()
.map(|(rgb, _, _)| Lab::from_rgb(rgb))
.collect();
assert_eq!(expected, actual);
}
#[test]
fn test_lab_to_rgb() {
let expected: Vec<_> = COLOURS.iter().map(|(rgb, _, _)| *rgb).collect();
let actual: Vec<_> = COLOURS.iter().map(|(_, lab, _)| lab.to_rgb()).collect();
assert_eq!(expected, actual);
}
#[test]
fn test_lch_from_rgb() {
let expected: Vec<_> = COLOURS.iter().map(|(_, _, lch)| *lch).collect();
let actual: Vec<_> = COLOURS
.iter()
.map(|(rgb, _, _)| LCh::from_rgb(rgb))
.collect();
assert_relative_eq!(expected.as_slice(), actual.as_slice());
}
#[test]
fn test_lch_to_rgb() {
let expected: Vec<_> = COLOURS.iter().map(|(rgb, _, _)| *rgb).collect();
let actual: Vec<_> = COLOURS.iter().map(|(_, _, lch)| lch.to_rgb()).collect();
assert_eq!(expected, actual);
}
#[test]
fn test_lch_from_lab() {
let expected: Vec<_> = COLOURS.iter().map(|(_, _, lch)| *lch).collect();
let actual: Vec<_> = COLOURS
.iter()
.map(|(_, lab, _)| LCh::from_lab(*lab))
.collect();
assert_relative_eq!(expected.as_slice(), actual.as_slice());
}
#[test]
fn test_lch_to_lab() {
let mut expected: Vec<_> = COLOURS.iter().map(|(_, lab, _)| *lab).collect();
let mut actual: Vec<_> = COLOURS.iter().map(|(_, _, lch)| lch.to_lab()).collect();
fn round(vec: &mut Vec<Lab>) {
for lab in vec.iter_mut() {
lab.a = (lab.a * 100000.0).round() / 100000.0;
lab.b = (lab.b * 100000.0).round() / 100000.0;
}
}
round(&mut expected);
round(&mut actual);
assert_eq!(expected, actual);
}
#[test]
fn test_distance() {
let ugly_websafe_pink = Lab {
l: 64.2116,
a: 62.519463,
b: 2.8871894,
};
assert_eq!(254.65927, PINK.squared_distance(&ugly_websafe_pink));
}
#[test]
fn test_send() {
fn assert_send<T: Send>() {}
assert_send::<Lab>();
}
#[test]
fn test_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Lab>();
}
#[test]
fn test_rgb_to_lab_to_rgb() {
let rgbs: Vec<[u8; 3]> = {
let rand_seed = [1u8; 32];
let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed);
rng.sample_iter(&rand::distributions::Standard)
.take(2048)
.collect()
};
let labs = rgbs_to_labs(&rgbs);
let rgbs2 = labs_to_rgbs(&labs);
assert_eq!(rgbs2, rgbs);
}
#[test]
fn test_grey_error() {
let mut error: f64 = 0.0;
let mut count: usize = 0;
for i in 0..=255_u32 {
let lab = Lab::from_rgb(&[i as u8, i as u8, i as u8]);
if lab.a != 0.0 || lab.b != 0.0 {
error = (lab.a as f64).mul_add(lab.a as f64, error);
error = (lab.b as f64).mul_add(lab.b as f64, error);
count += 1;
}
}
assert_eq!((23, 10.627054791711998), (count, error * 1e9));
}
}