/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 | | } |