kasi_kule/
lib.rs

1#![allow(non_snake_case)]
2#![allow(non_camel_case_types)]
3#![allow(non_upper_case_globals)]
4
5//! kasi-kule is a small rust implementation of the [CIECAM02 color space](https://en.wikipedia.org/wiki/CIECAM02) and conversion to it from standard RGB.
6//! It is based on the [d3-cam02](https://github.com/connorgr/d3-cam02/) and [colorspacious](https://github.com/njsmith/colorspacious).
7//!
8//! The name, kasi-kule, is a translation of 'flower' into toki pona - literally, 'colorful plant'.
9//! o sitelen pona!
10use std::f32::consts::PI;
11use std::marker::PhantomData;
12pub mod consts;
13pub mod utils;
14use consts::VC;
15pub use consts::{LCD, SCD, UCS};
16use utils::*;
17#[cfg(all(feature = "sse", any(target_arch = "x86", target_arch = "x86_64")))]
18pub mod sse;
19
20#[cfg(feature = "approximate_math")]
21#[allow(unused_imports)]
22use micromath::F32Ext;
23
24/// sRGB color, in the 0-255 range.
25#[derive(Default, Debug, Copy, Clone)]
26pub struct sRGB {
27    pub r: u8,
28    pub g: u8,
29    pub b: u8,
30}
31
32impl From<[u8; 3]> for sRGB {
33    fn from(rgb: [u8; 3]) -> sRGB {
34        sRGB {
35            r: rgb[0],
36            g: rgb[1],
37            b: rgb[2],
38        }
39    }
40}
41
42impl From<(u8, u8, u8)> for sRGB {
43    fn from(rgb: (u8, u8, u8)) -> sRGB {
44        sRGB {
45            r: rgb.0,
46            g: rgb.1,
47            b: rgb.2,
48        }
49    }
50}
51
52/// Linearized RGB, scaled from sRGB
53#[derive(Default, Debug, Copy, Clone)]
54pub struct LinearRGB {
55    pub r: f32,
56    pub g: f32,
57    pub b: f32,
58}
59
60impl From<&sRGB> for LinearRGB {
61    fn from(srgb: &sRGB) -> LinearRGB {
62        // safety: bounds checked by type; array is u8::MAX-sized and indexes are u8s
63        unsafe {
64            LinearRGB {
65                r: *consts::sRGB_LOOKUP.get_unchecked(srgb.r as usize),
66                g: *consts::sRGB_LOOKUP.get_unchecked(srgb.g as usize),
67                b: *consts::sRGB_LOOKUP.get_unchecked(srgb.b as usize),
68            }
69        }
70    }
71}
72
73impl<T: Into<sRGB>> From<T> for LinearRGB {
74    fn from(rgb: T) -> LinearRGB {
75        LinearRGB::from(&rgb.into())
76    }
77}
78
79/// CIEXYZ 1931 Color space, in the 0-100 range.
80#[derive(Debug, Copy, Clone)]
81pub struct XYZ {
82    pub x: f32,
83    pub y: f32,
84    pub z: f32,
85}
86
87impl From<&LinearRGB> for XYZ {
88    fn from(rgb: &LinearRGB) -> XYZ {
89        #[cfg(all(feature = "sse",any(target_arch = "x86", target_arch = "x86_64")))]
90        {
91            unsafe {
92                if is_x86_feature_detected!("sse") {
93                    let res = sse::sse_xyz(rgb);
94                    return XYZ {
95                        x: res[0],
96                        y: res[1],
97                        z: res[2],
98                    };
99                }
100            }
101        }
102
103        XYZ {
104            x: ((rgb.r * 0.4124) + (rgb.g * 0.3576) + (rgb.b * 0.1805)) * 100.0,
105            y: ((rgb.r * 0.2126) + (rgb.g * 0.7152) + (rgb.b * 0.0722)) * 100.0,
106            z: ((rgb.r * 0.0193) + (rgb.g * 0.1192) + (rgb.b * 0.9505)) * 100.0,
107        }
108    }
109}
110
111impl<T: Into<sRGB>> From<T> for XYZ {
112    fn from(rgb: T) -> XYZ {
113        XYZ::from(&LinearRGB::from(&rgb.into()))
114    }
115}
116
117/// Long-Medium-Short color space, derived from XYZ using the Mcat02 matrix.
118#[derive(Debug, Copy, Clone)]
119pub struct LMS {
120    pub l: f32,
121    pub m: f32,
122    pub s: f32,
123}
124
125impl From<&XYZ> for LMS {
126    fn from(xyz: &XYZ) -> LMS {
127        #[cfg(all(feature = "sse",any(target_arch = "x86", target_arch = "x86_64")))]
128        {
129            unsafe {
130                if is_x86_feature_detected!("sse") {
131                    let res = sse::sse_lms(xyz);
132                    return LMS {
133                        l: res[0],
134                        m: res[1],
135                        s: res[2],
136                    };
137                }
138            }
139        }
140
141        LMS {
142            l: (0.7328 * xyz.x) + (0.4296 * xyz.y) - (0.1624 * xyz.z),
143            m: (-0.7036 * xyz.x) + (1.6975 * xyz.y) + (0.0061 * xyz.z),
144            s: (0.0030 * xyz.x) + (0.0136 * xyz.y) + (0.9834 * xyz.z),
145        }
146    }
147}
148
149impl<T: Into<sRGB>> From<T> for LMS {
150    fn from(rgb: T) -> LMS {
151        LMS::from(&XYZ::from(&LinearRGB::from(&rgb.into())))
152    }
153}
154
155/// Hunt-Pointer-Estevez space, derived from CAM02 LMS.
156#[derive(Debug, Copy, Clone)]
157pub struct HPE {
158    pub lh: f32,
159    pub mh: f32,
160    pub sh: f32,
161}
162
163impl From<&LMS> for HPE {
164    fn from(lms: &LMS) -> HPE {
165        #[cfg(all(feature = "sse",any(target_arch = "x86", target_arch = "x86_64")))]
166        {
167            unsafe {
168                if is_x86_feature_detected!("sse") {
169                    let res = sse::sse_hpe(lms);
170                    return HPE {
171                        lh: res[0],
172                        mh: res[1],
173                        sh: res[2],
174                    };
175                }
176            }
177        }
178
179        HPE {
180            lh: (0.7409792 * lms.l) + (0.2180250 * lms.m) + (0.0410058 * lms.s),
181            mh: (0.2853532 * lms.l) + (0.6242014 * lms.m) + (0.0904454 * lms.s),
182            sh: (-0.0096280 * lms.l) - (0.0056980 * lms.m) + (1.0153260 * lms.s),
183        }
184    }
185}
186
187impl<T: Into<sRGB>> From<T> for HPE {
188    fn from(rgb: T) -> HPE {
189        HPE::from(&LMS::from(&XYZ::from(&LinearRGB::from(&rgb.into()))))
190    }
191}
192
193/// The CIECAM02 JCh (Lightness, Chroma, Hue) color space, derived from LMS.
194#[derive(Default, Debug, Copy, Clone)]
195pub struct JCh {
196    pub J: f32,
197    pub C: f32,
198    pub H: f32,
199    pub h: f32,
200    pub Q: f32,
201    pub M: f32,
202    pub s: f32,
203}
204
205impl From<&LMS> for JCh {
206    fn from(lms: &LMS) -> JCh {
207        let [lc, mc, sc, _] = transform_cones([lms.l, lms.m, lms.s, 0.0]);
208
209        let hpe_transforms = HPE::from(&LMS {
210            l: lc,
211            m: mc,
212            s: sc,
213        });
214
215        let [lpa, mpa, spa, _] = nonlinear_adaptation(
216            [hpe_transforms.lh, hpe_transforms.mh, hpe_transforms.sh, 0.0],
217            VC::fl,
218        );
219
220        let ca = lpa - ((12.0 * mpa) / 11.0) + (spa / 11.0);
221        let cb = (1.0 / 9.0) * (lpa + mpa - 2.0 * spa);
222
223        let mut result_color = JCh::default();
224
225        result_color.h = (180.0 / PI) * cb.atan2(ca);
226        if result_color.h < 0.0 {
227            result_color.h += 360.0;
228        }
229
230        let H = match result_color.h {
231            h if h < 20.14 => {
232                let temp = ((h + 122.47) / 1.2) + ((20.14 - h) / 0.8);
233                300.0 + (100.0 * ((h + 122.47) / 1.2)) / temp
234            }
235            h if h < 90.0 => {
236                let temp = ((h - 20.14) / 0.8) + ((90.0 - h) / 0.7);
237                (100.0 * ((h - 20.14) / 0.8)) / temp
238            }
239
240            h if h < 164.25 => {
241                let temp = ((h - 90.0) / 0.7) + ((164.25 - h) / 1.0);
242                100.0 + ((100.0 * ((h - 90.0) / 0.7)) / temp)
243            }
244            h if h < 237.53 => {
245                let temp = ((h - 164.25) / 1.0) + ((237.53 - h) / 1.2);
246                200.0 + ((100.0 * ((h - 164.25) / 1.0)) / temp)
247            }
248            h => {
249                let temp = ((h - 237.53) / 1.2) + ((360.0 - h + 20.14) / 0.8);
250                300.0 + ((100.0 * ((h - 237.53) / 1.2)) / temp)
251            }
252        };
253
254        result_color.H = H;
255
256        let a = (2.0 * lpa + mpa + 0.05 * spa - 0.305) * VC::nbb;
257        result_color.J = 100.0 * (a / VC::achromatic_response_to_white).powf(VC::c * VC::z);
258
259        let et = 0.25 * (((result_color.h * PI) / 180.0 + 2.0).cos() + 3.8);
260        let t = (50000.0 / 13.0) * VC::nc * VC::ncb * et * (ca.powi(2) + cb.powi(2)).sqrt()
261            / (lpa + mpa + (21.0 / 20.0) * spa);
262
263        result_color.C = t.powf(0.9f32)
264            * (result_color.J / 100.0).sqrt()
265            * (1.64 - 0.29f32.powf(VC::n)).powf(0.73f32);
266
267        result_color.Q = (4.0 / VC::c)
268            * (result_color.J / 100.0).sqrt()
269            * (VC::achromatic_response_to_white + 4.0f32)
270            * VC::fl.powf(0.25f32);
271
272        result_color.M = result_color.C * VC::fl.powf(0.25f32);
273
274        result_color.s = 100.0 * (result_color.M / result_color.Q).sqrt();
275
276        result_color
277    }
278}
279
280impl<T: Into<sRGB>> From<T> for JCh {
281    fn from(rgb: T) -> JCh {
282        JCh::from(&LMS::from(&XYZ::from(&LinearRGB::from(&rgb.into()))))
283    }
284}
285
286/// the JabSpace defines constants for transformation from JCh space into JabSpace. Used for type-checking comparisons between Jab colors.
287pub trait JabSpace {
288    const k_l: f32;
289    const c1: f32;
290    const c2: f32;
291}
292
293/// The CAM02 Jab color appearance model.
294/// It can be transformed from JCh space into an approximately perceptually uniform space (UCS), or into a space optimized for either LCD (Large Color Differences) or SCD (Small Color Differences).
295/// Subsequent calculations of color difference must be between colors within the same space (UCS/LCD/SCD).
296#[derive(Default, Debug, Copy, Clone)]
297pub struct Jab<S: JabSpace> {
298    pub J: f32,
299    pub a: f32,
300    pub b: f32,
301    space: PhantomData<S>,
302}
303
304impl<S: JabSpace> Jab<S> {
305    pub const fn new_const(J: f32, a: f32, b: f32) -> Jab<S> {
306        Jab {
307            J,
308            a,
309            b,
310            space: PhantomData
311        }
312    }
313}
314
315impl<S: JabSpace> From<&JCh> for Jab<S> {
316    fn from(cam02: &JCh) -> Jab<S> {
317        let j_prime = ((1.0 + 100.0 * S::c1) * cam02.J) / (1.0 + S::c1 * cam02.J) / S::k_l;
318
319        let m_prime = (1.0 / S::c2) * (1.0 + S::c2 * cam02.M).ln();
320
321        Jab {
322            J: j_prime,
323            a: m_prime * ((PI / 180.0) * cam02.h).cos(),
324            b: m_prime * ((PI / 180.0) * cam02.h).sin(),
325            space: PhantomData,
326        }
327    }
328}
329
330impl<T: Into<sRGB>, S: JabSpace> From<T> for Jab<S> {
331    fn from(rgb: T) -> Jab<S> {
332        Jab::<S>::from(&JCh::from(&LMS::from(&XYZ::from(&LinearRGB::from(
333            &rgb.into(),
334        )))))
335    }
336}
337
338impl<S: JabSpace> From<[f32; 3]> for Jab<S> {
339    fn from(jab: [f32; 3]) -> Jab<S> {
340        Jab {
341            J: jab[0],
342            a: jab[1],
343            b: jab[2],
344            space: PhantomData,
345        }
346    }
347}
348
349impl<S: JabSpace> From<(f32, f32, f32)> for Jab<S> {
350    fn from(jab: (f32, f32, f32)) -> Jab<S> {
351        Jab {
352            J: jab.0,
353            a: jab.1,
354            b: jab.2,
355            space: PhantomData,
356        }
357    }
358}
359
360impl<S: JabSpace> Jab<S> {
361    pub fn squared_difference(&self, other: &Jab<S>) -> f32 {
362        let diff_j = (self.J - other.J).abs();
363        let diff_a = (self.a - other.a).abs();
364        let diff_b = (self.b - other.b).abs();
365
366        (diff_j / S::k_l).powi(2) + diff_a.powi(2) + diff_b.powi(2)
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use crate::{consts::UCS, JCh, Jab};
373
374    macro_rules! float_eq {
375        ($lhs:expr, $rhs:expr) => {
376            assert_eq!(format!("{:.2}", $lhs), $rhs)
377        };
378    }
379
380    // based on https://github.com/connorgr/d3-cam02/blob/master/test/cam02-test.js,
381    #[test]
382    fn jch_channels() {
383        float_eq!(JCh::from([0, 0, 0]).J, "0.00");
384        float_eq!(JCh::from([50, 50, 50]).J, "14.92");
385        float_eq!(JCh::from([100, 100, 100]).J, "32.16");
386        float_eq!(JCh::from([150, 150, 150]).J, "52.09");
387        float_eq!(JCh::from([200, 200, 200]).J, "74.02");
388        float_eq!(JCh::from([250, 250, 250]).J, "97.57");
389        float_eq!(JCh::from([255, 255, 255]).J, "100.00");
390
391        let red = JCh::from([255, 0, 0]);
392        float_eq!(red.J, "46.93");
393        float_eq!(red.C, "111.30");
394        float_eq!(red.h, "32.15");
395    }
396
397    #[test]
398    fn jab_channels() {
399        float_eq!(Jab::<UCS>::from([0, 0, 0]).J, "0.00");
400        float_eq!(Jab::<UCS>::from([50, 50, 50]).J, "22.96");
401        float_eq!(Jab::<UCS>::from([150, 150, 150]).J, "64.89");
402        let white = Jab::<UCS>::from([255, 255, 255]);
403        float_eq!(white.J, "100.00");
404        float_eq!(white.a, "-1.91");
405        float_eq!(white.b, "-1.15");
406        let red = Jab::<UCS>::from([255, 0, 0]);
407        float_eq!(red.J, "60.05");
408        float_eq!(red.a, "38.69");
409        float_eq!(red.b, "24.32");
410        let blue = Jab::<UCS>::from([0, 0, 255]);
411        float_eq!(blue.J, "31.22");
412        float_eq!(blue.a, "-8.38");
413        float_eq!(blue.b, "-39.16");
414    }
415}