1use crate::mlaf::{fmla, mlaf};
30use crate::{Chromaticity, LCh, Xyz};
31use pxfm::f_cbrtf;
32
33#[repr(C)]
35#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
36pub struct Lab {
37 pub l: f32,
39 pub a: f32,
41 pub b: f32,
43}
44
45impl Lab {
46 #[inline]
54 pub const fn new(l: f32, a: f32, b: f32) -> Self {
55 Self { l, a, b }
56 }
57}
58
59#[inline(always)]
60const fn f_1(t: f32) -> f32 {
61 if t <= 24.0 / 116.0 {
62 (108.0 / 841.0) * (t - 16.0 / 116.0)
63 } else {
64 t * t * t
65 }
66}
67
68#[inline(always)]
69fn f(t: f32) -> f32 {
70 if t <= 24. / 116. * (24. / 116.) * (24. / 116.) {
71 (841. / 108. * t) + 16. / 116.
72 } else {
73 f_cbrtf(t)
74 }
75}
76
77impl Lab {
78 #[inline]
80 pub fn from_pcs_xyz(xyz: Xyz) -> Self {
81 const WP: Xyz = Chromaticity::D50.to_xyz();
82 let device_x = (xyz.x as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.x as f64) as f32;
83 let device_y = (xyz.y as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.y as f64) as f32;
84 let device_z = (xyz.z as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.z as f64) as f32;
85
86 let fx = f(device_x);
87 let fy = f(device_y);
88 let fz = f(device_z);
89
90 let lb = mlaf(-16.0, 116.0, fy);
91 let a = 500.0 * (fx - fy);
92 let b = 200.0 * (fy - fz);
93
94 let l = lb / 100.0;
95 let a = (a + 128.0) / 255.0;
96 let b = (b + 128.0) / 255.0;
97 Self::new(l, a, b)
98 }
99
100 #[inline]
102 pub fn from_xyz(xyz: Xyz) -> Self {
103 const WP: Xyz = Chromaticity::D50.to_xyz();
104 let device_x = (xyz.x as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.x as f64) as f32;
105 let device_y = (xyz.y as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.y as f64) as f32;
106 let device_z = (xyz.z as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.z as f64) as f32;
107
108 let fx = f(device_x);
109 let fy = f(device_y);
110 let fz = f(device_z);
111
112 let lb = mlaf(-16.0, 116.0, fy);
113 let a = 500.0 * (fx - fy);
114 let b = 200.0 * (fy - fz);
115
116 Self::new(lb, a, b)
117 }
118
119 #[inline]
121 pub fn to_pcs_xyz(self) -> Xyz {
122 let device_l = self.l * 100.0;
123 let device_a = fmla(self.a, 255.0, -128.0);
124 let device_b = fmla(self.b, 255.0, -128.0);
125
126 let y = (device_l + 16.0) / 116.0;
127
128 const WP: Xyz = Chromaticity::D50.to_xyz();
129
130 let x = f_1(mlaf(y, 0.002, device_a)) * WP.x;
131 let y1 = f_1(y) * WP.y;
132 let z = f_1(mlaf(y, -0.005, device_b)) * WP.z;
133
134 let x = (x as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
135 let y = (y1 as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
136 let z = (z as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
137 Xyz::new(x, y, z)
138 }
139
140 #[inline]
142 pub fn to_xyz(self) -> Xyz {
143 let device_l = self.l;
144 let device_a = self.a;
145 let device_b = self.b;
146
147 let y = (device_l + 16.0) / 116.0;
148
149 const WP: Xyz = Chromaticity::D50.to_xyz();
150
151 let x = f_1(mlaf(y, 0.002, device_a)) * WP.x;
152 let y1 = f_1(y) * WP.y;
153 let z = f_1(mlaf(y, -0.005, device_b)) * WP.z;
154
155 let x = (x as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
156 let y = (y1 as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
157 let z = (z as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32;
158 Xyz::new(x, y, z)
159 }
160
161 pub fn desaturate_pcs(self) -> Lab {
163 if self.l < 0. {
164 return Lab::new(0., 0., 0.);
165 }
166
167 let mut new_lab = self;
168 if new_lab.l > 1. {
169 new_lab.l = 1.;
170 }
171
172 let amax = 1.0;
173 let amin = 0.0;
174 let bmin = 0.0;
175 let bmax = 1.0;
176 if self.a < amin || self.a > amax || self.b < bmin || self.b > bmax {
177 if self.a == 0.0 {
178 new_lab.b = if new_lab.b < bmin { bmin } else { bmax };
182 return Lab::new(self.l, self.a, self.b);
183 }
184
185 let lch = LCh::from_lab(new_lab);
186
187 let slope = new_lab.b / new_lab.a;
188 let h = lch.h * (180.0 / std::f32::consts::PI);
189
190 if (0. ..45.).contains(&h) || (315. ..=360.).contains(&h) {
192 new_lab.a = amax;
194 new_lab.b = amax * slope;
195 } else if (45. ..135.).contains(&h) {
196 new_lab.b = bmax;
198 new_lab.a = bmax / slope;
199 } else if (135. ..225.).contains(&h) {
200 new_lab.a = amin;
202 new_lab.b = amin * slope;
203 } else if (225. ..315.).contains(&h) {
204 new_lab.b = bmin;
206 new_lab.a = bmin / slope;
207 }
208 }
209 new_lab
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn round_trip() {
219 let xyz = Xyz::new(0.1, 0.2, 0.3);
220 let lab = Lab::from_xyz(xyz);
221 let rolled_back = lab.to_xyz();
222 let dx = (xyz.x - rolled_back.x).abs();
223 let dy = (xyz.y - rolled_back.y).abs();
224 let dz = (xyz.z - rolled_back.z).abs();
225 assert!(dx < 1e-5);
226 assert!(dy < 1e-5);
227 assert!(dz < 1e-5);
228 }
229
230 #[test]
231 fn round_pcs_trip() {
232 let xyz = Xyz::new(0.1, 0.2, 0.3);
233 let lab = Lab::from_pcs_xyz(xyz);
234 let rolled_back = lab.to_pcs_xyz();
235 let dx = (xyz.x - rolled_back.x).abs();
236 let dy = (xyz.y - rolled_back.y).abs();
237 let dz = (xyz.z - rolled_back.z).abs();
238 assert!(dx < 1e-5);
239 assert!(dy < 1e-5);
240 assert!(dz < 1e-5);
241 }
242}