Coverage Report

Created: 2025-11-05 08:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.7.9/src/lab.rs
Line
Count
Source
1
/*
2
 * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved.
3
 * //
4
 * // Redistribution and use in source and binary forms, with or without modification,
5
 * // are permitted provided that the following conditions are met:
6
 * //
7
 * // 1.  Redistributions of source code must retain the above copyright notice, this
8
 * // list of conditions and the following disclaimer.
9
 * //
10
 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11
 * // this list of conditions and the following disclaimer in the documentation
12
 * // and/or other materials provided with the distribution.
13
 * //
14
 * // 3.  Neither the name of the copyright holder nor the names of its
15
 * // contributors may be used to endorse or promote products derived from
16
 * // this software without specific prior written permission.
17
 * //
18
 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29
use crate::mlaf::{fmla, mlaf};
30
use crate::{Chromaticity, LCh, Xyz};
31
use pxfm::f_cbrtf;
32
33
/// Holds CIE LAB values
34
#[repr(C)]
35
#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
36
pub struct Lab {
37
    /// `l`: lightness component (0 to 100)
38
    pub l: f32,
39
    /// `a`: green (negative) and red (positive) component.
40
    pub a: f32,
41
    /// `b`: blue (negative) and yellow (positive) component
42
    pub b: f32,
43
}
44
45
impl Lab {
46
    /// Create a new CIELAB color.
47
    ///
48
    /// # Arguments
49
    ///
50
    /// * `l`: lightness component (0 to 100).
51
    /// * `a`: green (negative) and red (positive) component.
52
    /// * `b`: blue (negative) and yellow (positive) component.
53
    #[inline]
54
0
    pub const fn new(l: f32, a: f32, b: f32) -> Self {
55
0
        Self { l, a, b }
56
0
    }
57
}
58
59
#[inline(always)]
60
0
const fn f_1(t: f32) -> f32 {
61
0
    if t <= 24.0 / 116.0 {
62
0
        (108.0 / 841.0) * (t - 16.0 / 116.0)
63
    } else {
64
0
        t * t * t
65
    }
66
0
}
67
68
#[inline(always)]
69
0
fn f(t: f32) -> f32 {
70
0
    if t <= 24. / 116. * (24. / 116.) * (24. / 116.) {
71
0
        (841. / 108. * t) + 16. / 116.
72
    } else {
73
0
        f_cbrtf(t)
74
    }
75
0
}
76
77
impl Lab {
78
    /// Converts to CIE Lab from CIE XYZ for PCS encoding
79
    #[inline]
80
0
    pub fn from_pcs_xyz(xyz: Xyz) -> Self {
81
        const WP: Xyz = Chromaticity::D50.to_xyz();
82
0
        let device_x = (xyz.x as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.x as f64) as f32;
83
0
        let device_y = (xyz.y as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.y as f64) as f32;
84
0
        let device_z = (xyz.z as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.z as f64) as f32;
85
86
0
        let fx = f(device_x);
87
0
        let fy = f(device_y);
88
0
        let fz = f(device_z);
89
90
0
        let lb = mlaf(-16.0, 116.0, fy);
91
0
        let a = 500.0 * (fx - fy);
92
0
        let b = 200.0 * (fy - fz);
93
94
0
        let l = lb / 100.0;
95
0
        let a = (a + 128.0) / 255.0;
96
0
        let b = (b + 128.0) / 255.0;
97
0
        Self::new(l, a, b)
98
0
    }
99
100
    /// Converts to CIE Lab from CIE XYZ
101
    #[inline]
102
0
    pub fn from_xyz(xyz: Xyz) -> Self {
103
        const WP: Xyz = Chromaticity::D50.to_xyz();
104
0
        let device_x = (xyz.x as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.x as f64) as f32;
105
0
        let device_y = (xyz.y as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.y as f64) as f32;
106
0
        let device_z = (xyz.z as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.z as f64) as f32;
107
108
0
        let fx = f(device_x);
109
0
        let fy = f(device_y);
110
0
        let fz = f(device_z);
111
112
0
        let lb = mlaf(-16.0, 116.0, fy);
113
0
        let a = 500.0 * (fx - fy);
114
0
        let b = 200.0 * (fy - fz);
115
116
0
        Self::new(lb, a, b)
117
0
    }
118
119
    /// Converts CIE [Lab] into CIE [Xyz] for PCS encoding
120
    #[inline]
121
0
    pub fn to_pcs_xyz(self) -> Xyz {
122
0
        let device_l = self.l * 100.0;
123
0
        let device_a = fmla(self.a, 255.0, -128.0);
124
0
        let device_b = fmla(self.b, 255.0, -128.0);
125
126
0
        let y = (device_l + 16.0) / 116.0;
127
128
        const WP: Xyz = Chromaticity::D50.to_xyz();
129
130
0
        let x = f_1(mlaf(y, 0.002, device_a)) * WP.x;
131
0
        let y1 = f_1(y) * WP.y;
132
0
        let z = f_1(mlaf(y, -0.005, device_b)) * WP.z;
133
134
0
        let x = (x as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
135
0
        let y = (y1 as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
136
0
        let z = (z as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
137
0
        Xyz::new(x, y, z)
138
0
    }
139
140
    /// Converts CIE [Lab] into CIE [Xyz]
141
    #[inline]
142
0
    pub fn to_xyz(self) -> Xyz {
143
0
        let device_l = self.l;
144
0
        let device_a = self.a;
145
0
        let device_b = self.b;
146
147
0
        let y = (device_l + 16.0) / 116.0;
148
149
        const WP: Xyz = Chromaticity::D50.to_xyz();
150
151
0
        let x = f_1(mlaf(y, 0.002, device_a)) * WP.x;
152
0
        let y1 = f_1(y) * WP.y;
153
0
        let z = f_1(mlaf(y, -0.005, device_b)) * WP.z;
154
155
0
        let x = (x as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
156
0
        let y = (y1 as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
157
0
        let z = (z as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
158
0
        Xyz::new(x, y, z)
159
0
    }
160
161
    /// Desaturates out of gamut PCS encoded LAB
162
0
    pub fn desaturate_pcs(self) -> Lab {
163
0
        if self.l < 0. {
164
0
            return Lab::new(0., 0., 0.);
165
0
        }
166
167
0
        let mut new_lab = self;
168
0
        if new_lab.l > 1. {
169
0
            new_lab.l = 1.;
170
0
        }
171
172
0
        let amax = 1.0;
173
0
        let amin = 0.0;
174
0
        let bmin = 0.0;
175
0
        let bmax = 1.0;
176
0
        if self.a < amin || self.a > amax || self.b < bmin || self.b > bmax {
177
0
            if self.a == 0.0 {
178
                // Is hue exactly 90?
179
                // atan will not work, so clamp here
180
0
                return Lab::new(self.l, self.a, self.b);
181
0
            }
182
183
0
            let lch = LCh::from_lab(new_lab);
184
185
0
            let slope = new_lab.b / new_lab.a;
186
0
            let h = lch.h * (180.0 / std::f32::consts::PI);
187
188
            // There are 4 zones
189
0
            if (0. ..45.).contains(&h) || (315. ..=360.).contains(&h) {
190
0
                // clip by amax
191
0
                new_lab.a = amax;
192
0
                new_lab.b = amax * slope;
193
0
            } else if (45. ..135.).contains(&h) {
194
0
                // clip by bmax
195
0
                new_lab.b = bmax;
196
0
                new_lab.a = bmax / slope;
197
0
            } else if (135. ..225.).contains(&h) {
198
0
                // clip by amin
199
0
                new_lab.a = amin;
200
0
                new_lab.b = amin * slope;
201
0
            } else if (225. ..315.).contains(&h) {
202
0
                // clip by bmin
203
0
                new_lab.b = bmin;
204
0
                new_lab.a = bmin / slope;
205
0
            }
206
0
        }
207
0
        new_lab
208
0
    }
209
}
210
211
#[cfg(test)]
212
mod tests {
213
    use super::*;
214
215
    #[test]
216
    fn round_trip() {
217
        let xyz = Xyz::new(0.1, 0.2, 0.3);
218
        let lab = Lab::from_xyz(xyz);
219
        let rolled_back = lab.to_xyz();
220
        let dx = (xyz.x - rolled_back.x).abs();
221
        let dy = (xyz.y - rolled_back.y).abs();
222
        let dz = (xyz.z - rolled_back.z).abs();
223
        assert!(dx < 1e-5);
224
        assert!(dy < 1e-5);
225
        assert!(dz < 1e-5);
226
    }
227
228
    #[test]
229
    fn round_pcs_trip() {
230
        let xyz = Xyz::new(0.1, 0.2, 0.3);
231
        let lab = Lab::from_pcs_xyz(xyz);
232
        let rolled_back = lab.to_pcs_xyz();
233
        let dx = (xyz.x - rolled_back.x).abs();
234
        let dy = (xyz.y - rolled_back.y).abs();
235
        let dz = (xyz.z - rolled_back.z).abs();
236
        assert!(dx < 1e-5);
237
        assert!(dy < 1e-5);
238
        assert!(dz < 1e-5);
239
    }
240
}