1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! A module that implements the [CIELAB color
//! space](https://en.wikipedia.org/wiki/Lab_color_space#CIELAB). The CIELAB color space is used as a
//! device-independent color space that has an L value for luminance and two opponent color axes for
//! chromaticity (loosely, hue). Formally, the three values that define a CIELAB color are called
//! L\*, A\*, and B\* to distinguish them from [generic
//! Lab](https://en.wikipedia.org/wiki/Lab_color_space), but for convenience they are just `L`, `a`,
//! and `b` in this module.

use color::{Color, XYZColor};
use coord::Coord;
use illuminants::Illuminant;

/// A color in the CIELAB color space.
/// # Example
/// Unlike spaces such as HSV and RGB, moving a and b linearly will create roughly smooth change in
/// color.
///
/// ```
/// # use scarlet::prelude::*;
/// # use scarlet::colors::CIELABColor;
/// // roughly blue-green
/// let mut color = CIELABColor{l: 50., a: -100., b: -100.};
/// for _i in 0..10 {
///     // make warmer: positive a and b direction
///     color.a = color.a + 20.;
///     color.b = color.b + 20.;
///     println!("{}", color.convert::<RGBColor>().to_string());
/// }
/// // prints the following:
/// // #0098FF
/// // #0092DD
/// // #008ABA
/// // #2F8298
/// // #777777
/// // #9E6956
/// // #BD5735
/// // #D73C0A
/// // #F00000
/// // #FF0000
/// // note that the end might have been truncated to fit in sRGB's gamut on either side
/// ```
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct CIELABColor {
    /// The luminance (loosely, brightness) of a given color. 0 is the lowest visible value and gives
    /// black, whereas 100 is the value of diffuse white: it is perhaps possible to have a higher
    /// value for reflective surfaces.
    pub l: f64,
    /// The first opponent color axis. By convention, this is usually between -128 and 127, with -128
    /// being fully green and 127 being fully magenta, but note that it is still possible to create
    /// "imaginary" colors (ones that cannot normally be seen by the human eye). Additionally,
    /// depending on the other two dimensions, many colors with a value in this range will still not
    /// be in the range of human vision.
    pub a: f64,
    /// The second opponent color axis. This is, like `a`, between -128 and 127 by convention for most
    /// visible colors, although it is possible to work with imaginary colors as well and many colors
    /// with a value in this range are not in the range of human vision. -128 is fully blue; 127 is
    /// fully yellow.
    pub b: f64,
}

impl Color for CIELABColor {
    /// Converts a given CIE XYZ color to CIELAB. Because CIELAB is implicitly in a given illuminant
    /// space, and because the linear conversions within CIELAB that it uses conflict with the
    /// transform used in the rest of Scarlet, this is explicitly CIELAB D50: any other illuminant is
    /// converted to D50 outside of CIELAB conversion. This in line with programs like Photoshop,
    /// which also use CIELAB D50.
    fn from_xyz(xyz: XYZColor) -> CIELABColor {
        // TODO: are the bounds for a and b right? -128 to 127?
        // https://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions
        let f = |x: &f64| {
            let delta: f64 = 6.0 / 29.0;
            if *x <= delta.powf(3.0) {
                x / (3.0 * delta * delta) + 4.0 / 29.0
            } else {
                x.powf(1.0 / 3.0)
            }
        };
        // now get the XYZ coordinates normalized using D50: convert to that beforehand if not
        let white_point = Illuminant::D50.white_point();
        let xyz_adapted = xyz.color_adapt(Illuminant::D50);
        let xyz_scaled = [
            xyz_adapted.x / white_point[0],
            xyz_adapted.y / white_point[1],
            xyz_adapted.z / white_point[2],
        ];
        let xyz_transformed: Vec<f64> = xyz_scaled.iter().map(f).collect();

        // xyz_transformed was modified to allow for human nonlinearity of color vision
        // so this is just simple linear formulae
        // note how a and b are opponent color axes
        let l = 116.0 * xyz_transformed[1] - 16.0;
        let a = 500.0 * (xyz_transformed[0] - xyz_transformed[1]);
        let b = 200.0 * (xyz_transformed[1] - xyz_transformed[2]);
        CIELABColor { l, a, b }
    }
    /// Returns an XYZ color that corresponds to the CIELAB color. Note that, because implicitly every
    /// CIELAB color is D50, conversion is done by first converting to a D50 XYZ color and then using
    /// a chromatic adaptation transform.
    fn to_xyz(&self, illuminant: Illuminant) -> XYZColor {
        // for implementation details see from_xyz
        // we need the inverse function of the nonlinearity we introduced earlier
        let f_inv = |x: f64| {
            let delta: f64 = 6.0 / 29.0;
            if x > delta {
                x * x * x
            } else {
                3.0 * delta * delta * (x - 4.0 / 29.0)
            }
        };
        // need to undo normalization with D50 white point
        let xyz_n = Illuminant::D50.white_point();
        let x = xyz_n[0] * f_inv((self.l + 16.0) / 116.0 + (self.a / 500.0));
        let y = xyz_n[1] * f_inv((self.l + 16.0) / 116.0);
        let z = xyz_n[2] * f_inv((self.l + 16.0) / 116.0 - (self.b / 200.0));
        // this is CIELAB D50, so to use custom illuminant do chromatic adaptation
        XYZColor {
            x,
            y,
            z,
            illuminant: Illuminant::D50,
        }
        .color_adapt(illuminant)
    }
}

impl From<Coord> for CIELABColor {
    fn from(c: Coord) -> CIELABColor {
        CIELABColor {
            l: c.x,
            a: c.y,
            b: c.z,
        }
    }
}

impl From<CIELABColor> for Coord {
    fn from(val: CIELABColor) -> Self {
        Coord {
            x: val.l,
            y: val.a,
            z: val.b,
        }
    }
}

#[cfg(test)]
mod tests {
    #[allow(unused_imports)]
    use super::*;
    use color::RGBColor;
    use consts::TEST_PRECISION;

    #[test]
    fn test_cielab_xyz_conversion_d50() {
        let xyz = XYZColor {
            x: 0.4,
            y: 0.2,
            z: 0.6,
            illuminant: Illuminant::D50,
        };
        let lab = CIELABColor::from_xyz(xyz);
        let xyz2 = lab.to_xyz(Illuminant::D50);
        assert!(xyz.approx_equal(&xyz2));
        assert!(xyz.distance(&xyz2) <= TEST_PRECISION);
    }
    #[test]
    fn test_cielab_xyz_conversion() {
        let xyz = XYZColor {
            x: 0.4,
            y: 0.2,
            z: 0.6,
            illuminant: Illuminant::D65,
        };
        let lab = CIELABColor::from_xyz(xyz);
        let xyz_d50 = lab.to_xyz(Illuminant::D50);
        let xyz2 = xyz_d50.color_adapt(Illuminant::D65);
        assert!(xyz.approx_equal(&xyz2));
        assert!(xyz.distance(&xyz2) <= TEST_PRECISION);
    }
    #[test]
    fn test_out_of_gamut() {
        // this color doesn't exist in sRGB! (that's probably a good thing, this can't really be represented)
        let _color1 = CIELABColor {
            l: 0.0,
            a: 100.0,
            b: 100.0,
        };
        let _color2: RGBColor = _color1.convert();
        let _color3: CIELABColor = _color2.convert();
    }
}