palette/
lab.rs

1//! Types for the CIE L\*a\*b\* (CIELAB) color space.
2
3use core::{
4    marker::PhantomData,
5    ops::{Add, BitAnd, BitOr, Mul, Neg},
6};
7
8use crate::{
9    angle::RealAngle,
10    bool_mask::{HasBoolMask, LazySelect},
11    color_difference::{
12        get_ciede2000_difference, Ciede2000, DeltaE, EuclideanDistance, ImprovedDeltaE,
13        LabColorDiff,
14    },
15    convert::FromColorUnclamped,
16    num::{
17        Abs, Arithmetics, Cbrt, Exp, Hypot, MinMax, One, PartialCmp, Powf, Powi, Real, Sqrt,
18        Trigonometry, Zero,
19    },
20    white_point::{WhitePoint, D65},
21    Alpha, FromColor, GetHue, LabHue, Lch, Xyz,
22};
23
24/// CIE L\*a\*b\* (CIELAB) with an alpha component. See the [`Laba`
25/// implementation in `Alpha`](crate::Alpha#Laba).
26pub type Laba<Wp = D65, T = f32> = Alpha<Lab<Wp, T>, T>;
27
28/// The CIE L\*a\*b\* (CIELAB) color space.
29///
30/// CIE L\*a\*b\* is a device independent color space which includes all
31/// perceivable colors. It's sometimes used to convert between other color
32/// spaces, because of its ability to represent all of their colors, and
33/// sometimes in color manipulation, because of its perceptual uniformity. This
34/// means that the perceptual difference between two colors is equal to their
35/// numerical difference. It was, however, [never designed for the perceptual
36/// qualities required for gamut mapping](http://www.brucelindbloom.com/UPLab.html).
37/// For perceptually uniform color manipulation the newer color spaces based on
38/// [`Oklab`](crate::Oklab) are preferable:
39/// [`Oklch`](crate::Oklch), [`Okhsv`](crate::Okhsv), [`Okhsl`](crate::Okhsl),
40/// [`Okhwb`](crate::Okhwb) (Note that the latter three are tied to the sRGB gamut
41/// and reference white).
42///
43/// The parameters of L\*a\*b\* are quite different, compared to many other
44/// color spaces, so manipulating them manually may be unintuitive.
45#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
46#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
47#[palette(
48    palette_internal,
49    white_point = "Wp",
50    component = "T",
51    skip_derives(Xyz, Lab, Lch)
52)]
53#[repr(C)]
54pub struct Lab<Wp = D65, T = f32> {
55    /// L\* is the lightness of the color. 0.0 gives absolute black and 100
56    /// give the brightest white.
57    pub l: T,
58
59    /// a\* goes from red at -128 to green at 127.
60    pub a: T,
61
62    /// b\* goes from yellow at -128 to blue at 127.
63    pub b: T,
64
65    /// The white point associated with the color's illuminant and observer.
66    /// D65 for 2 degree observer is used by default.
67    #[cfg_attr(feature = "serializing", serde(skip))]
68    #[palette(unsafe_zero_sized)]
69    pub white_point: PhantomData<Wp>,
70}
71
72impl<Wp, T> Lab<Wp, T> {
73    /// Create a CIE L\*a\*b\* color.
74    pub const fn new(l: T, a: T, b: T) -> Lab<Wp, T> {
75        Lab {
76            l,
77            a,
78            b,
79            white_point: PhantomData,
80        }
81    }
82
83    /// Convert to a `(L\*, a\*, b\*)` tuple.
84    pub fn into_components(self) -> (T, T, T) {
85        (self.l, self.a, self.b)
86    }
87
88    /// Convert from a `(L\*, a\*, b\*)` tuple.
89    pub fn from_components((l, a, b): (T, T, T)) -> Self {
90        Self::new(l, a, b)
91    }
92}
93
94impl<Wp, T> Lab<Wp, T>
95where
96    T: Zero + Real,
97{
98    /// Return the `l` value minimum.
99    pub fn min_l() -> T {
100        T::zero()
101    }
102
103    /// Return the `l` value maximum.
104    pub fn max_l() -> T {
105        T::from_f64(100.0)
106    }
107
108    /// Return the `a` value minimum.
109    pub fn min_a() -> T {
110        T::from_f64(-128.0)
111    }
112
113    /// Return the `a` value maximum.
114    pub fn max_a() -> T {
115        T::from_f64(127.0)
116    }
117
118    /// Return the `b` value minimum.
119    pub fn min_b() -> T {
120        T::from_f64(-128.0)
121    }
122
123    /// Return the `b` value maximum.
124    pub fn max_b() -> T {
125        T::from_f64(127.0)
126    }
127}
128
129///<span id="Laba"></span>[`Laba`](crate::Laba) implementations.
130impl<Wp, T, A> Alpha<Lab<Wp, T>, A> {
131    /// Create a CIE L\*a\*b\* with transparency.
132    pub const fn new(l: T, a: T, b: T, alpha: A) -> Self {
133        Alpha {
134            color: Lab::new(l, a, b),
135            alpha,
136        }
137    }
138
139    /// Convert to a `(L\*, a\*, b\*, alpha)` tuple.
140    pub fn into_components(self) -> (T, T, T, A) {
141        (self.color.l, self.color.a, self.color.b, self.alpha)
142    }
143
144    /// Convert from a `(L\*, a\*, b\*, alpha)` tuple.
145    pub fn from_components((l, a, b, alpha): (T, T, T, A)) -> Self {
146        Self::new(l, a, b, alpha)
147    }
148}
149
150impl_reference_component_methods!(Lab<Wp>, [l, a, b], white_point);
151impl_struct_of_arrays_methods!(Lab<Wp>, [l, a, b], white_point);
152
153impl<Wp, T> FromColorUnclamped<Lab<Wp, T>> for Lab<Wp, T> {
154    fn from_color_unclamped(color: Lab<Wp, T>) -> Self {
155        color
156    }
157}
158
159impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Lab<Wp, T>
160where
161    Wp: WhitePoint<T>,
162    T: Real + Powi + Cbrt + Arithmetics + PartialCmp + Clone,
163    T::Mask: LazySelect<T>,
164{
165    fn from_color_unclamped(color: Xyz<Wp, T>) -> Self {
166        let Xyz { x, y, z, .. } = color / Wp::get_xyz().with_white_point();
167
168        let epsilon = T::from_f64(6.0 / 29.0).powi(3);
169        let kappa: T = T::from_f64(841.0 / 108.0);
170        let delta: T = T::from_f64(4.0 / 29.0);
171
172        let convert = |c: T| {
173            lazy_select! {
174                if c.gt(&epsilon) => c.clone().cbrt(),
175                else => (kappa.clone() * &c) + &delta,
176            }
177        };
178
179        let x = convert(x);
180        let y = convert(y);
181        let z = convert(z);
182
183        Lab {
184            l: ((y.clone() * T::from_f64(116.0)) - T::from_f64(16.0)),
185            a: ((x - &y) * T::from_f64(500.0)),
186            b: ((y - z) * T::from_f64(200.0)),
187            white_point: PhantomData,
188        }
189    }
190}
191
192impl<Wp, T> FromColorUnclamped<Lch<Wp, T>> for Lab<Wp, T>
193where
194    T: RealAngle + Zero + MinMax + Trigonometry + Mul<Output = T> + Clone,
195{
196    fn from_color_unclamped(color: Lch<Wp, T>) -> Self {
197        let (a, b) = color.hue.into_cartesian();
198        let chroma = color.chroma.max(T::zero());
199
200        Lab {
201            l: color.l,
202            a: a * chroma.clone(),
203            b: b * chroma,
204            white_point: PhantomData,
205        }
206    }
207}
208
209impl_tuple_conversion!(Lab<Wp> as (T, T, T));
210
211impl_is_within_bounds! {
212    Lab<Wp> {
213        l => [Self::min_l(), Self::max_l()],
214        a => [Self::min_a(), Self::max_a()],
215        b => [Self::min_b(), Self::max_b()]
216    }
217    where T: Real + Zero
218}
219impl_clamp! {
220    Lab<Wp> {
221        l => [Self::min_l(), Self::max_l()],
222        a => [Self::min_a(), Self::max_a()],
223        b => [Self::min_b(), Self::max_b()]
224    }
225    other {white_point}
226    where T: Real + Zero
227}
228
229impl_mix!(Lab<Wp>);
230impl_lighten!(Lab<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {a, b} phantom: white_point);
231impl_premultiply!(Lab<Wp> {l, a, b} phantom: white_point);
232impl_euclidean_distance!(Lab<Wp> {l, a, b});
233impl_hyab!(Lab<Wp> {lightness: l, chroma1: a, chroma2: b});
234impl_lab_color_schemes!(Lab<Wp>[l, white_point]);
235
236impl<Wp, T> GetHue for Lab<Wp, T>
237where
238    T: RealAngle + Trigonometry + Add<T, Output = T> + Neg<Output = T> + Clone,
239{
240    type Hue = LabHue<T>;
241
242    fn get_hue(&self) -> LabHue<T> {
243        LabHue::from_cartesian(self.a.clone(), self.b.clone())
244    }
245}
246
247impl<Wp, T> DeltaE for Lab<Wp, T>
248where
249    Self: EuclideanDistance<Scalar = T>,
250    T: Sqrt,
251{
252    type Scalar = T;
253
254    #[inline]
255    fn delta_e(self, other: Self) -> Self::Scalar {
256        self.distance(other)
257    }
258}
259
260impl<Wp, T> ImprovedDeltaE for Lab<Wp, T>
261where
262    Self: DeltaE<Scalar = T> + EuclideanDistance<Scalar = T>,
263    T: Real + Mul<T, Output = T> + Powf,
264{
265    #[inline]
266    fn improved_delta_e(self, other: Self) -> Self::Scalar {
267        // Coefficients from "Power functions improving the performance of
268        // color-difference formulas" by Huang et al.
269        // https://opg.optica.org/oe/fulltext.cfm?uri=oe-23-1-597&id=307643
270        //
271        // The multiplication of 0.5 in the exponent makes it square root the
272        // squared distance.
273        T::from_f64(1.26) * self.distance_squared(other).powf(T::from_f64(0.55 * 0.5))
274    }
275}
276
277#[allow(deprecated)]
278impl<Wp, T> crate::ColorDifference for Lab<Wp, T>
279where
280    T: Real
281        + RealAngle
282        + One
283        + Zero
284        + Powi
285        + Exp
286        + Trigonometry
287        + Abs
288        + Sqrt
289        + Arithmetics
290        + PartialCmp
291        + Clone,
292    T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>,
293    Self: Into<LabColorDiff<T>>,
294{
295    type Scalar = T;
296
297    #[inline]
298    fn get_color_difference(self, other: Lab<Wp, T>) -> Self::Scalar {
299        get_ciede2000_difference(self.into(), other.into())
300    }
301}
302
303impl<Wp, T> Ciede2000 for Lab<Wp, T>
304where
305    T: Real
306        + RealAngle
307        + One
308        + Zero
309        + Powi
310        + Exp
311        + Trigonometry
312        + Abs
313        + Sqrt
314        + Arithmetics
315        + PartialCmp
316        + Hypot
317        + Clone,
318    T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>,
319{
320    type Scalar = T;
321
322    #[inline]
323    fn difference(self, other: Self) -> Self::Scalar {
324        get_ciede2000_difference(self.into(), other.into())
325    }
326}
327
328impl<Wp, T> HasBoolMask for Lab<Wp, T>
329where
330    T: HasBoolMask,
331{
332    type Mask = T::Mask;
333}
334
335impl<Wp, T> Default for Lab<Wp, T>
336where
337    T: Zero,
338{
339    fn default() -> Lab<Wp, T> {
340        Lab::new(T::zero(), T::zero(), T::zero())
341    }
342}
343
344impl_color_add!(Lab<Wp>, [l, a, b], white_point);
345impl_color_sub!(Lab<Wp>, [l, a, b], white_point);
346impl_color_mul!(Lab<Wp>, [l, a, b], white_point);
347impl_color_div!(Lab<Wp>, [l, a, b], white_point);
348
349impl_array_casts!(Lab<Wp, T>, [T; 3]);
350impl_simd_array_conversion!(Lab<Wp>, [l, a, b], white_point);
351impl_struct_of_array_traits!(Lab<Wp>, [l, a, b], white_point);
352
353impl_eq!(Lab<Wp>, [l, a, b]);
354impl_copy_clone!(Lab<Wp>, [l, a, b], white_point);
355
356#[allow(deprecated)]
357impl<Wp, T> crate::RelativeContrast for Lab<Wp, T>
358where
359    T: Real + Arithmetics + PartialCmp,
360    T::Mask: LazySelect<T>,
361    Xyz<Wp, T>: FromColor<Self>,
362{
363    type Scalar = T;
364
365    #[inline]
366    fn get_contrast_ratio(self, other: Self) -> T {
367        let xyz1 = Xyz::from_color(self);
368        let xyz2 = Xyz::from_color(other);
369
370        crate::contrast_ratio(xyz1.y, xyz2.y)
371    }
372}
373
374impl_rand_traits_cartesian!(
375    UniformLab,
376    Lab<Wp> {
377        l => [|x| x * T::from_f64(100.0)],
378        a => [|x| x * T::from_f64(255.0) - T::from_f64(128.0)],
379        b => [|x| x * T::from_f64(255.0) - T::from_f64(128.0)]
380    }
381    phantom: white_point: PhantomData<Wp>
382    where T: Real + core::ops::Sub<Output = T> + core::ops::Mul<Output = T>
383);
384
385#[cfg(feature = "bytemuck")]
386unsafe impl<Wp, T> bytemuck::Zeroable for Lab<Wp, T> where T: bytemuck::Zeroable {}
387
388#[cfg(feature = "bytemuck")]
389unsafe impl<Wp: 'static, T> bytemuck::Pod for Lab<Wp, T> where T: bytemuck::Pod {}
390
391#[cfg(test)]
392mod test {
393    use super::Lab;
394    use crate::white_point::D65;
395
396    #[cfg(feature = "approx")]
397    use crate::Lch;
398
399    test_convert_into_from_xyz!(Lab);
400
401    #[cfg(feature = "approx")]
402    mod conversion {
403        use crate::{FromColor, Lab, LinSrgb};
404
405        #[test]
406        fn red() {
407            let a = Lab::from_color(LinSrgb::new(1.0, 0.0, 0.0));
408            let b = Lab::new(53.23288, 80.09246, 67.2031);
409            assert_relative_eq!(a, b, epsilon = 0.01);
410        }
411
412        #[test]
413        fn green() {
414            let a = Lab::from_color(LinSrgb::new(0.0, 1.0, 0.0));
415            let b = Lab::new(87.73704, -86.184654, 83.18117);
416            assert_relative_eq!(a, b, epsilon = 0.01);
417        }
418
419        #[test]
420        fn blue() {
421            let a = Lab::from_color(LinSrgb::new(0.0, 0.0, 1.0));
422            let b = Lab::new(32.302586, 79.19668, -107.863686);
423            assert_relative_eq!(a, b, epsilon = 0.01);
424        }
425    }
426
427    #[test]
428    fn ranges() {
429        assert_ranges! {
430            Lab<D65, f64>;
431            clamped {
432                l: 0.0 => 100.0,
433                a: -128.0 => 127.0,
434                b: -128.0 => 127.0
435            }
436            clamped_min {}
437            unclamped {}
438        }
439    }
440
441    raw_pixel_conversion_tests!(Lab<D65>: l, a, b);
442    raw_pixel_conversion_fail_tests!(Lab<D65>: l, a, b);
443
444    #[test]
445    fn check_min_max_components() {
446        assert_eq!(Lab::<D65, f32>::min_l(), 0.0);
447        assert_eq!(Lab::<D65, f32>::min_a(), -128.0);
448        assert_eq!(Lab::<D65, f32>::min_b(), -128.0);
449        assert_eq!(Lab::<D65, f32>::max_l(), 100.0);
450        assert_eq!(Lab::<D65, f32>::max_a(), 127.0);
451        assert_eq!(Lab::<D65, f32>::max_b(), 127.0);
452    }
453
454    struct_of_arrays_tests!(
455        Lab<D65>[l, a, b] phantom: white_point,
456        super::Laba::new(0.1f32, 0.2, 0.3, 0.4),
457        super::Laba::new(0.2, 0.3, 0.4, 0.5),
458        super::Laba::new(0.3, 0.4, 0.5, 0.6)
459    );
460
461    #[cfg(feature = "serializing")]
462    #[test]
463    fn serialize() {
464        let serialized = ::serde_json::to_string(&Lab::<D65>::new(0.3, 0.8, 0.1)).unwrap();
465
466        assert_eq!(serialized, r#"{"l":0.3,"a":0.8,"b":0.1}"#);
467    }
468
469    #[cfg(feature = "serializing")]
470    #[test]
471    fn deserialize() {
472        let deserialized: Lab = ::serde_json::from_str(r#"{"l":0.3,"a":0.8,"b":0.1}"#).unwrap();
473
474        assert_eq!(deserialized, Lab::new(0.3, 0.8, 0.1));
475    }
476
477    test_uniform_distribution! {
478        Lab<D65, f32> {
479            l: (0.0, 100.0),
480            a: (-128.0, 127.0),
481            b: (-128.0, 127.0)
482        },
483        min: Lab::new(0.0f32, -128.0, -128.0),
484        max: Lab::new(100.0, 127.0, 127.0)
485    }
486
487    test_lab_color_schemes!(Lab/Lch [l, white_point]);
488}