colcon/
lib.rs

1#![warn(missing_docs)]
2
3//! Comprehensive colorspace conversions in pure Rust
4//!
5//! The working data structure is `[DType; ValidChannels]`, where DType is one of
6//! `f32` or `f64` and ValidChannels is either 3 or 4, with the 4th channel representing
7//! alpha and being unprocessed outside of typing conversions
8//!
9//! Formulae are generally taken from their research papers or Wikipedia and validated against
10//! colour-science <https://github.com/colour-science/colour>
11//!
12//! This crate references CIE Standard Illuminant D65 for functions to/from CIE XYZ
13
14#[cfg(test)]
15mod tests;
16
17mod generated_quantiles;
18
19use core::cmp::PartialOrd;
20use core::ffi::{c_char, CStr};
21use core::fmt::{Debug, Display};
22use core::ops::{Add, Div, Mul, Neg, Rem, Sub};
23
24// DType {{{
25
26/// 3 channels, or 4 with alpha.
27/// Alpha ignored during space conversions.
28pub struct Channels<const N: usize>;
29/// 3 channels, or 4 with alpha.
30/// Alpha ignored during space conversions.
31pub trait ValidChannels {}
32impl ValidChannels for Channels<3> {}
33impl ValidChannels for Channels<4> {}
34
35#[allow(missing_docs)]
36/// Convert an F32 ito any supported DType
37pub trait FromF32: Sized {
38    fn ff32(f: f32) -> Self;
39}
40
41impl FromF32 for f32 {
42    fn ff32(f: f32) -> Self {
43        f
44    }
45}
46
47impl FromF32 for f64 {
48    fn ff32(f: f32) -> Self {
49        f.into()
50    }
51}
52
53trait ToDType<T>: Sized {
54    fn to_dt(self) -> T;
55}
56
57impl<U> ToDType<U> for f32
58where
59    U: FromF32 + Sized,
60{
61    fn to_dt(self) -> U {
62        FromF32::ff32(self)
63    }
64}
65
66#[allow(missing_docs)]
67/// Trait for all supported data types in colcon
68pub trait DType:
69    Sized
70    + Copy
71    + Add<Output = Self>
72    + Div<Output = Self>
73    + Mul<Output = Self>
74    + Neg<Output = Self>
75    + Rem<Output = Self>
76    + Sub<Output = Self>
77    + PartialOrd
78    + Debug
79    + Display
80    + FromF32
81{
82    fn powi(self, rhs: i32) -> Self;
83    fn powf(self, rhs: Self) -> Self;
84    /// Sign-agnostic powf
85    fn spowf(self, rhs: Self) -> Self;
86    fn rem_euclid(self, rhs: Self) -> Self;
87
88    fn abs(self) -> Self;
89    fn trunc(self) -> Self;
90    fn max(self, other: Self) -> Self;
91    fn min(self, other: Self) -> Self;
92
93    fn sin(self) -> Self;
94    fn cos(self) -> Self;
95    fn to_degrees(self) -> Self;
96    fn to_radians(self) -> Self;
97    fn atan2(self, rhs: Self) -> Self;
98
99    fn sqrt(self) -> Self {
100        self.powf((1.0 / 2.0).to_dt())
101    }
102    fn cbrt(self) -> Self {
103        self.powf((1.0 / 3.0).to_dt())
104    }
105    fn ssqrt(self) -> Self {
106        self.spowf((1.0 / 2.0).to_dt())
107    }
108    fn scbrt(self) -> Self {
109        self.spowf((1.0 / 3.0).to_dt())
110    }
111
112    fn _fma(self, mul: Self, add: Self) -> Self;
113    /// Fused multiply-add if "fma" is enabled in rustc
114    fn fma(self, mul: Self, add: Self) -> Self {
115        // other non-x86 names?
116        if cfg!(target_feature = "fma") {
117            self._fma(mul, add) // crazy slow without FMA3
118        } else {
119            self * mul + add
120        }
121    }
122}
123
124macro_rules! impl_float {
125    ($type:ident) => {
126        impl DType for $type {
127            fn powi(self, rhs: i32) -> Self {
128                self.powi(rhs)
129            }
130            fn powf(self, rhs: Self) -> Self {
131                self.powf(rhs)
132            }
133            fn spowf(self, rhs: Self) -> Self {
134                self.abs().powf(rhs).copysign(self)
135            }
136            fn rem_euclid(self, rhs: Self) -> Self {
137                self.rem_euclid(rhs)
138            }
139            fn abs(self) -> Self {
140                self.abs()
141            }
142            fn trunc(self) -> Self {
143                self.trunc()
144            }
145            fn max(self, other: Self) -> Self {
146                self.max(other)
147            }
148            fn min(self, other: Self) -> Self {
149                self.min(other)
150            }
151            fn sin(self) -> Self {
152                self.sin()
153            }
154            fn cos(self) -> Self {
155                self.cos()
156            }
157            fn to_degrees(self) -> Self {
158                self.to_degrees()
159            }
160            fn to_radians(self) -> Self {
161                self.to_radians()
162            }
163            fn atan2(self, rhs: Self) -> Self {
164                self.atan2(rhs)
165            }
166            fn sqrt(self) -> Self {
167                self.sqrt()
168            }
169            // 50% slower than powf/spowf?
170            //fn cbrt(self) -> Self {
171            //    self.cbrt()
172            //}
173            fn _fma(self, mul: Self, add: Self) -> Self {
174                self.mul_add(mul, add)
175            }
176        }
177    };
178}
179
180impl_float!(f32);
181impl_float!(f64);
182
183// }}}
184
185/// Create an array of separate channel buffers from a single interwoven buffer.
186/// Copies the data.
187pub fn unweave<T, const N: usize>(slice: &[T]) -> [Box<[T]>; N]
188where
189    T: Debug + Copy,
190{
191    let len = slice.len() / N;
192    let mut result: [Vec<T>; N] = (0..N)
193        .map(|_| Vec::with_capacity(len))
194        .collect::<Vec<Vec<T>>>()
195        .try_into()
196        .unwrap();
197
198    slice.chunks_exact(N).for_each(|chunk| {
199        chunk.iter().zip(result.iter_mut()).for_each(|(v, arr)| arr.push(*v));
200    });
201
202    result.map(|v| v.into_boxed_slice())
203}
204
205/// Create a monolithic woven buffer using unwoven independent channel buffers.
206/// Copies the data.
207pub fn weave<T, const N: usize>(array: [Box<[T]>; N]) -> Box<[T]>
208where
209    T: Debug + Copy,
210{
211    let len = array[0].len();
212    (0..len)
213        .into_iter()
214        .fold(Vec::with_capacity(len * N), |mut acc, it| {
215            (0..N).into_iter().for_each(|n| acc.push(array[n][it]));
216            acc
217        })
218        .into_boxed_slice()
219}
220
221// ### CONSTS ### {{{
222
223/// Standard Illuminant D65.
224pub const D65: [f32; 3] = [0.9504559270516716, 1.0, 1.0890577507598784];
225
226const SRGBEOTF_ALPHA: f32 = 0.055;
227const SRGBEOTF_GAMMA: f32 = 2.4;
228// more precise older specs
229// const SRGBEOTF_PHI: f32 = 12.9232102;
230// const SRGBEOTF_CHI: f32 = 0.0392857;
231// const SRGBEOTF_CHI_INV: f32 = 0.0030399;
232// less precise but basically official now
233const SRGBEOTF_PHI: f32 = 12.92;
234const SRGBEOTF_CHI: f32 = 0.04045;
235const SRGBEOTF_CHI_INV: f32 = 0.0031308;
236
237// CIE LAB
238const LAB_DELTA: f32 = 6.0 / 29.0;
239
240// <PQ EOTF Table 4 <https://www.itu.int/rec/R-REC-BT.2100/en>
241const PQEOTF_M1: f32 = 2610. / 16384.;
242const PQEOTF_M2: f32 = 2523. / 4096. * 128.;
243const PQEOTF_C1: f32 = 3424. / 4096.;
244const PQEOTF_C2: f32 = 2413. / 4096. * 32.;
245const PQEOTF_C3: f32 = 2392. / 4096. * 32.;
246
247// JzAzBz
248const JZAZBZ_B: f32 = 1.15;
249const JZAZBZ_G: f32 = 0.66;
250const JZAZBZ_D: f32 = -0.56;
251const JZAZBZ_D0: f32 = 1.6295499532821566e-11;
252const JZAZBZ_P: f32 = 1.7 * PQEOTF_M2;
253
254// ### CONSTS ### }}}
255
256// ### MATRICES ### {{{
257
258/// Its easier to write matricies visually then transpose them so they can be indexed per vector
259/// [X1, X2] -> [X1, Y1]
260/// [Y1, Y2]    [X2, Y2]
261const fn t(m: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
262    [
263        [m[0][0], m[1][0], m[2][0]],
264        [m[0][1], m[1][1], m[2][1]],
265        [m[0][2], m[1][2], m[2][2]],
266    ]
267}
268
269/// Matrix Multiply
270fn mm<T: DType>(m: [[f32; 3]; 3], p: [T; 3]) -> [T; 3] {
271    [
272        p[0].fma(m[0][0].to_dt(), p[1].fma(m[1][0].to_dt(), p[2] * m[2][0].to_dt())),
273        p[0].fma(m[0][1].to_dt(), p[1].fma(m[1][1].to_dt(), p[2] * m[2][1].to_dt())),
274        p[0].fma(m[0][2].to_dt(), p[1].fma(m[1][2].to_dt(), p[2] * m[2][2].to_dt())),
275    ]
276}
277
278// CIE XYZ
279const XYZ65_MAT: [[f32; 3]; 3] = t([
280    [0.4124, 0.3576, 0.1805],
281    [0.2126, 0.7152, 0.0722],
282    [0.0193, 0.1192, 0.9505],
283]);
284
285// Original commonly used inverted array
286// const XYZ65_MAT_INV: [[f32; 3]; 3] = [
287//     [3.2406, -1.5372, -0.4986],
288//     [-0.9689, 1.8758, 0.0415],
289//     [0.0557, -0.2040, 1.0570],
290// ];
291
292// Higher precision invert using numpy. Helps with back conversions
293const XYZ65_MAT_INV: [[f32; 3]; 3] = t([
294    [3.2406254773, -1.5372079722, -0.4986285987],
295    [-0.9689307147, 1.8757560609, 0.0415175238],
296    [0.0557101204, -0.2040210506, 1.0569959423],
297]);
298
299// OKLAB
300// They appear to be provided already transposed for code in the blog post
301const OKLAB_M1: [[f32; 3]; 3] = [
302    [0.8189330101, 0.0329845436, 0.0482003018],
303    [0.3618667424, 0.9293118715, 0.2643662691],
304    [-0.1288597137, 0.0361456387, 0.6338517070],
305];
306const OKLAB_M2: [[f32; 3]; 3] = [
307    [0.2104542553, 1.9779984951, 0.0259040371],
308    [0.7936177850, -2.4285922050, 0.7827717662],
309    [-0.0040720468, 0.4505937099, -0.8086757660],
310];
311const OKLAB_M1_INV: [[f32; 3]; 3] = [
312    [1.2270138511, -0.0405801784, -0.0763812845],
313    [-0.5577999807, 1.1122568696, -0.4214819784],
314    [0.281256149, -0.0716766787, 1.5861632204],
315];
316const OKLAB_M2_INV: [[f32; 3]; 3] = [
317    [0.9999999985, 1.0000000089, 1.0000000547],
318    [0.3963377922, -0.1055613423, -0.0894841821],
319    [0.2158037581, -0.0638541748, -1.2914855379],
320];
321
322// JzAzBz
323const JZAZBZ_M1: [[f32; 3]; 3] = t([
324    [0.41478972, 0.579999, 0.0146480],
325    [-0.2015100, 1.120649, 0.0531008],
326    [-0.0166008, 0.264800, 0.6684799],
327]);
328const JZAZBZ_M2: [[f32; 3]; 3] = t([
329    [0.500000, 0.500000, 0.000000],
330    [3.524000, -4.066708, 0.542708],
331    [0.199076, 1.096799, -1.295875],
332]);
333
334const JZAZBZ_M1_INV: [[f32; 3]; 3] = t([
335    [1.9242264358, -1.0047923126, 0.037651404],
336    [0.3503167621, 0.7264811939, -0.0653844229],
337    [-0.090982811, -0.3127282905, 1.5227665613],
338]);
339const JZAZBZ_M2_INV: [[f32; 3]; 3] = t([
340    [1., 0.1386050433, 0.0580473162],
341    [1., -0.1386050433, -0.0580473162],
342    [1., -0.096019242, -0.8118918961],
343]);
344
345// ICtCp
346const ICTCP_M1: [[f32; 3]; 3] = t([
347    [1688. / 4096., 2146. / 4096., 262. / 4096.],
348    [683. / 4096., 2951. / 4096., 462. / 4096.],
349    [99. / 4096., 309. / 4096., 3688. / 4096.],
350]);
351const ICTCP_M2: [[f32; 3]; 3] = t([
352    [2048. / 4096., 2048. / 4096., 0. / 4096.],
353    [6610. / 4096., -13613. / 4096., 7003. / 4096.],
354    [17933. / 4096., -17390. / 4096., -543. / 4096.],
355]);
356
357const ICTCP_M1_INV: [[f32; 3]; 3] = t([
358    [3.4366066943, -2.5064521187, 0.0698454243],
359    [-0.7913295556, 1.9836004518, -0.1922708962],
360    [-0.0259498997, -0.0989137147, 1.1248636144],
361]);
362const ICTCP_M2_INV: [[f32; 3]; 3] = t([
363    [1., 0.008609037, 0.111029625],
364    [1., -0.008609037, -0.111029625],
365    [1., 0.5600313357, -0.320627175],
366]);
367// ### MATRICES ### }}}
368
369// ### TRANSFER FUNCTIONS ### {{{
370
371/// sRGB Electro-Optical Transfer Function
372///
373/// <https://en.wikipedia.org/wiki/SRGB#Computing_the_transfer_function>
374pub fn srgb_eotf<T: DType>(n: T) -> T {
375    if n <= SRGBEOTF_CHI.to_dt() {
376        n / SRGBEOTF_PHI.to_dt()
377    } else {
378        ((n + SRGBEOTF_ALPHA.to_dt()) / (SRGBEOTF_ALPHA + 1.0).to_dt()).powf(SRGBEOTF_GAMMA.to_dt())
379    }
380}
381
382/// Inverse sRGB Electro-Optical Transfer Function
383///
384/// <https://en.wikipedia.org/wiki/SRGB#Computing_the_transfer_function>
385pub fn srgb_oetf<T: DType>(n: T) -> T {
386    if n <= SRGBEOTF_CHI_INV.to_dt() {
387        n * SRGBEOTF_PHI.to_dt()
388    } else {
389        (n.powf((1.0 / SRGBEOTF_GAMMA).to_dt())).fma((1.0 + SRGBEOTF_ALPHA).to_dt(), (-SRGBEOTF_ALPHA).to_dt())
390    }
391}
392
393// <https://www.itu.int/rec/R-REC-BT.2100/en> Table 4 "Reference PQ EOTF"
394fn pq_eotf_common<T: DType>(e: T, m2: T) -> T {
395    let ep_pow_1divm2 = e.spowf(T::ff32(1.0) / m2);
396
397    let numerator: T = (ep_pow_1divm2 - PQEOTF_C1.to_dt()).max(0.0.to_dt());
398    let denominator: T = ep_pow_1divm2.fma(T::ff32(-PQEOTF_C3), PQEOTF_C2.to_dt());
399
400    let y = (numerator / denominator).spowf((1.0 / PQEOTF_M1).to_dt());
401
402    y * 10000.0.to_dt()
403}
404
405// <https://www.itu.int/rec/R-REC-BT.2100/en> Table 4 "Reference PQ OETF"
406fn pq_oetf_common<T: DType>(f: T, m2: T) -> T {
407    let y = f / 10000.0.to_dt();
408    let y_pow_m1 = y.spowf(PQEOTF_M1.to_dt());
409
410    let numerator: T = T::ff32(PQEOTF_C2).fma(y_pow_m1, PQEOTF_C1.to_dt());
411    let denominator: T = T::ff32(PQEOTF_C3).fma(y_pow_m1, 1.0.to_dt());
412
413    (numerator / denominator).spowf(m2)
414}
415
416/// Dolby Perceptual Quantizer Electro-Optical Transfer Function primarily used for ICtCP
417///
418/// <https://www.itu.int/rec/R-REC-BT.2100/en> Table 4 "Reference PQ EOTF"
419pub fn pq_eotf<T: DType>(e: T) -> T {
420    pq_eotf_common(e, PQEOTF_M2.to_dt())
421}
422
423/// Dolby Perceptual Quantizer Optical-Electro Transfer Function primarily used for ICtCP
424///
425/// <https://www.itu.int/rec/R-REC-BT.2100/en> Table 4 "Reference PQ OETF"
426pub fn pq_oetf<T: DType>(f: T) -> T {
427    pq_oetf_common(f, PQEOTF_M2.to_dt())
428}
429
430/// Dolby Perceptual Quantizer Electro-Optical Transfer Function modified for JzAzBz
431///
432/// Replaced PQEOTF_M2 with JZAZBZ_P
433///
434/// <https://www.itu.int/rec/R-REC-BT.2100/en> Table 4 "Reference PQ EOTF"
435pub fn pqz_eotf<T: DType>(e: T) -> T {
436    pq_eotf_common(e, JZAZBZ_P.to_dt())
437}
438
439/// Dolby Perceptual Quantizer Optical-Electro Transfer Function modified for JzAzBz
440///
441/// Replaced PQEOTF_M2 with JZAZBZ_P
442///
443/// <https://www.itu.int/rec/R-REC-BT.2100/en> Table 4 "Reference PQ OETF"
444pub fn pqz_oetf<T: DType>(f: T) -> T {
445    pq_oetf_common(f, JZAZBZ_P.to_dt())
446}
447
448// ### TRANSFER FUNCTIONS ### }}}
449
450// ### Helmholtz-Kohlrausch ### {{{
451
452/// Extended K-values from High et al 2021/2022
453const K_HIGH2022: [f32; 4] = [0.1644, 0.0603, 0.1307, 0.0060];
454
455/// Mean value of the HK delta for CIE LCH(ab), High et al 2023 implementation.
456///
457/// Measured with 36000 steps in the hk_exmample file @ 100 C(ab).
458/// Cannot make a const fn: <https://github.com/rust-lang/rust/issues/57241>
459pub const HIGH2023_MEAN: f32 = 20.956442;
460
461/// Returns difference in perceptual lightness based on hue, aka the Helmholtz-Kohlrausch effect.
462/// High et al 2023 implementation.
463pub fn hk_high2023<T: DType, const N: usize>(lch: &[T; N]) -> T
464where
465    Channels<N>: ValidChannels,
466{
467    let fby: T = T::ff32(K_HIGH2022[0]).fma(
468        ((lch[2] - 90.0.to_dt()) / 2.0.to_dt()).to_radians().sin().abs(),
469        K_HIGH2022[1].to_dt(),
470    );
471
472    let fr: T = if lch[2] <= 90.0.to_dt() || lch[2] >= 270.0.to_dt() {
473        T::ff32(K_HIGH2022[2]).fma(lch[2].to_radians().cos().abs(), K_HIGH2022[3].to_dt())
474    } else {
475        0.0.to_dt()
476    };
477
478    (fby + fr) * lch[1]
479}
480
481/// Compensates CIE LCH's L value for the Helmholtz-Kohlrausch effect.
482/// High et al 2023 implementation.
483pub fn hk_high2023_comp<T: DType, const N: usize>(lch: &mut [T; N])
484where
485    Channels<N>: ValidChannels,
486{
487    lch[0] = lch[0] + (T::ff32(HIGH2023_MEAN) - hk_high2023(lch)) * (lch[1] / 100.0.to_dt())
488}
489
490// ### Helmholtz-Kohlrausch ### }}}
491
492// ### Space ### {{{
493
494/// Defines colorspace pixels will take.
495#[derive(Clone, Copy, PartialEq, Eq, Debug)]
496pub enum Space {
497    /// Gamma-corrected sRGB.
498    SRGB,
499
500    /// Hue Saturation Value.
501    ///
502    /// A UCS typically preferred for modern applications
503    HSV,
504
505    /// Linear RGB. IEC 61966-2-1:1999 transferred
506    LRGB,
507
508    /// 1931 CIE XYZ @ D65.
509    XYZ,
510
511    /// CIE LAB. Lightness, red/green chromacity, yellow/blue chromacity.
512    ///
513    /// 1976 UCS with many known flaws. Most other LAB spaces derive from this
514    CIELAB,
515
516    /// CIE LCH(ab). Lightness, Chroma, Hue
517    ///
518    /// Cylindrical version of CIE LAB.
519    CIELCH,
520
521    /// Oklab
522    ///
523    /// <https://bottosson.github.io/posts/oklab/>
524    ///
525    /// 2020 UCS, used in CSS Color Module Level 4
526    OKLAB,
527
528    /// Cylindrical version of OKLAB.
529    OKLCH,
530
531    /// JzAzBz
532    ///
533    /// <https://opg.optica.org/oe/fulltext.cfm?uri=oe-25-13-15131>
534    ///
535    /// 2017 UCS, intended for uniform hue and HDR colors
536    JZAZBZ,
537
538    /// Cylindrical version of JzAzBz
539    JZCZHZ,
540}
541
542impl TryFrom<&str> for Space {
543    type Error = ();
544    fn try_from(value: &str) -> Result<Self, ()> {
545        match value.to_ascii_lowercase().trim() {
546            "srgb" => Ok(Space::SRGB),
547            "hsv" => Ok(Space::HSV),
548            "lrgb" | "rgb" => Ok(Space::LRGB),
549            "xyz" | "cie xyz" | "ciexyz" => Ok(Space::XYZ),
550            // extra values so you can move to/from str
551            "lab" | "cie lab" | "cielab" => Ok(Space::CIELAB),
552            "lch" | "cie lch" | "cielch" => Ok(Space::CIELCH),
553            "oklab" => Ok(Space::OKLAB),
554            "oklch" => Ok(Space::OKLCH),
555            "jzazbz" => Ok(Space::JZAZBZ),
556            "jzczhz" => Ok(Space::JZCZHZ),
557            _ => Err(()),
558        }
559    }
560}
561
562impl TryFrom<*const c_char> for Space {
563    type Error = ();
564    fn try_from(value: *const c_char) -> Result<Self, ()> {
565        if value.is_null() {
566            Err(())
567        } else {
568            unsafe { CStr::from_ptr(value) }
569                .to_str()
570                .ok()
571                .map(|s| Self::try_from(s).ok())
572                .flatten()
573                .ok_or(())
574        }
575    }
576}
577
578impl Display for Space {
579    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
580        core::fmt::write(
581            f,
582            core::format_args!(
583                "{}",
584                match self {
585                    Self::SRGB => "sRGB",
586                    Self::HSV => "HSV",
587                    Self::LRGB => "RGB",
588                    Self::XYZ => "CIE XYZ",
589                    Self::CIELAB => "CIE LAB",
590                    Self::CIELCH => "CIE LCH",
591                    Self::OKLAB => "Oklab",
592                    Self::OKLCH => "Oklch",
593                    Self::JZAZBZ => "JzAzBz",
594                    Self::JZCZHZ => "JzCzHz",
595                }
596            ),
597        )
598    }
599}
600
601impl Space {
602    /// Returns 3 channels letters for user-facing colorspace controls
603    pub fn channels(&self) -> [char; 3] {
604        match self {
605            Space::SRGB => ['r', 'g', 'b'],
606            Space::HSV => ['h', 's', 'v'],
607            Space::LRGB => ['r', 'g', 'b'],
608            Space::XYZ => ['x', 'y', 'z'],
609            Space::CIELAB => ['l', 'a', 'b'],
610            Space::CIELCH => ['l', 'c', 'h'],
611            Space::OKLAB => ['l', 'a', 'b'],
612            Space::OKLCH => ['l', 'c', 'h'],
613            Space::JZAZBZ => ['j', 'a', 'b'],
614            Space::JZCZHZ => ['j', 'c', 'h'],
615        }
616    }
617
618    /// All color spaces
619    pub const ALL: &'static [Space] = &[
620        Space::SRGB,
621        Space::HSV,
622        Space::LRGB,
623        Space::XYZ,
624        Space::CIELAB,
625        Space::CIELCH,
626        Space::OKLAB,
627        Space::OKLCH,
628        Space::JZAZBZ,
629        Space::JZCZHZ,
630    ];
631
632    /// Uniform color spaces
633    pub const UCS: &'static [Space] = &[Space::CIELAB, Space::OKLAB, Space::JZAZBZ];
634
635    /// Uniform color spaces in cylindrical/polar format
636    pub const UCS_POLAR: &'static [Space] = &[Space::CIELCH, Space::OKLCH, Space::JZCZHZ];
637
638    /// RGB/Tristimulus color spaces
639    pub const TRI: &'static [Space] = &[Space::SRGB, Space::LRGB, Space::XYZ];
640
641    /// Retrieves a map from a given Space back to SRGB.
642    ///
643    /// This is useful for things like creating adjustable values in Space
644    /// that represent most of the SRGB range without clipping.
645    /// Wrapping Hue values are set to f32::INFINITY
646    pub const fn srgb_quants(&self) -> [[f32; 3]; 101] {
647        //[[0.0; 3]; 101]
648        generated_quantiles::srgb_quants(self)
649    }
650}
651
652// ### Space ### }}}
653
654// ### Convert Space ### {{{
655
656macro_rules! op_single {
657    ($func:ident, $data:expr) => {
658        $func($data)
659    };
660}
661
662macro_rules! op_chunk {
663    ($func:ident, $data:expr) => {
664        $data.iter_mut().for_each(|pixel| $func(pixel))
665    };
666}
667
668#[rustfmt::skip]
669macro_rules! graph {
670    ($recurse:ident, $data:expr, $from:expr, $to:expr, $op:ident) => {
671        match ($from, $to) {
672            // no-ops
673            (Space::HSV, Space::HSV) => (),
674            (Space::SRGB, Space::SRGB) => (),
675            (Space::LRGB, Space::LRGB) => (),
676            (Space::XYZ, Space::XYZ) => (),
677            (Space::CIELAB, Space::CIELAB) => (),
678            (Space::CIELCH, Space::CIELCH) => (),
679            (Space::OKLAB, Space::OKLAB) => (),
680            (Space::OKLCH, Space::OKLCH) => (),
681            (Space::JZAZBZ, Space::JZAZBZ) => (),
682            (Space::JZCZHZ, Space::JZCZHZ) => (),
683
684            //endcaps
685            (Space::SRGB, Space::HSV) => $op!(srgb_to_hsv, $data),
686            (Space::CIELAB, Space::CIELCH)
687            | (Space::OKLAB, Space::OKLCH)
688            | (Space::JZAZBZ, Space::JZCZHZ) => $op!(lab_to_lch, $data),
689
690            // Reverse Endcaps
691            (Space::HSV, _) => { $op!(hsv_to_srgb, $data); $recurse(Space::SRGB, $to, $data) }
692            (Space::CIELCH, _) => { $op!(lch_to_lab, $data); $recurse(Space::CIELAB, $to, $data) }
693            (Space::OKLCH, _) => { $op!(lch_to_lab, $data); $recurse(Space::OKLAB, $to, $data) }
694            (Space::JZCZHZ, _) => { $op!(lch_to_lab, $data); $recurse(Space::JZAZBZ, $to, $data) }
695
696            // SRGB Up
697            (Space::SRGB, _) => { $op!(srgb_to_lrgb, $data); $recurse(Space::LRGB, $to, $data) }
698
699            // LRGB Down
700            (Space::LRGB, Space::SRGB | Space::HSV) => { $op!(lrgb_to_srgb, $data); $recurse(Space::SRGB, $to, $data) }
701            // LRGB Up
702            (Space::LRGB, _) => { $op!(lrgb_to_xyz, $data); $recurse(Space::XYZ, $to, $data) }
703
704            // XYZ Down
705            (Space::XYZ, Space::SRGB | Space::LRGB | Space::HSV) => { $op!(xyz_to_lrgb, $data); $recurse(Space::LRGB, $to, $data) }
706            // XYZ Up
707            (Space::XYZ, Space::CIELAB | Space::CIELCH) => { $op!(xyz_to_cielab, $data); $recurse(Space::CIELAB, $to, $data) }
708            (Space::XYZ, Space::OKLAB | Space::OKLCH) => { $op!(xyz_to_oklab, $data); $recurse(Space::OKLAB, $to, $data) }
709            (Space::XYZ, Space::JZAZBZ | Space::JZCZHZ) => { $op!(xyz_to_jzazbz, $data); $recurse(Space::JZAZBZ, $to, $data) }
710
711            // LAB Down
712            (Space::CIELAB, _) => { $op!(cielab_to_xyz, $data); $recurse(Space::XYZ, $to, $data) }
713            (Space::OKLAB, _) => { $op!(oklab_to_xyz, $data); $recurse(Space::XYZ, $to, $data) }
714            (Space::JZAZBZ, _) => { $op!(jzazbz_to_xyz, $data); $recurse(Space::XYZ, $to, $data) }
715        }
716    };
717}
718
719/// Runs conversion functions to convert `pixel` from one `Space` to another
720/// in the least possible moves.
721pub fn convert_space<T: DType, const N: usize>(from: Space, to: Space, pixel: &mut [T; N])
722where
723    Channels<N>: ValidChannels,
724{
725    graph!(convert_space, pixel, from, to, op_single);
726}
727
728/// Runs conversion functions to convert `pixel` from one `Space` to another
729/// in the least possible moves.
730///
731/// Caches conversion graph for faster iteration.
732pub fn convert_space_chunked<T: DType, const N: usize>(from: Space, to: Space, pixels: &mut [[T; N]])
733where
734    Channels<N>: ValidChannels,
735{
736    graph!(convert_space_chunked, pixels, from, to, op_chunk);
737}
738
739/// Runs conversion functions to convert `pixel` from one `Space` to another
740/// in the least possible moves.
741///
742/// Caches conversion graph for faster iteration and ignores remainder values in slice.
743pub fn convert_space_sliced<T: DType, const N: usize>(from: Space, to: Space, pixels: &mut [T])
744where
745    Channels<N>: ValidChannels,
746{
747    // Inline std::slice::as_chunks_mut without the asserts as its already guarded by ValidChannels
748    let (mut_chunks, _remainder): (&mut [[T; N]], &mut [T]) = {
749        let len = pixels.len() / N;
750        let (multiple_of_n, remainder) = pixels.split_at_mut(len * N);
751        let array_slice = {
752            let this = &mut *multiple_of_n;
753            let new_len = this.len() / N;
754            unsafe { core::slice::from_raw_parts_mut(this.as_mut_ptr().cast(), new_len) }
755        };
756        (array_slice, remainder)
757    };
758    graph!(convert_space_chunked, mut_chunks, from, to, op_chunk);
759}
760
761/// Same as `convert_space_sliced` but with FFI types.
762///
763/// Returns 0 on success, 1 on invalid `from`, 2 on invalid `to`, 3 on invalid `pixels`
764///
765/// `len` is in elements rather than bytes
766pub fn convert_space_ffi<T: DType, const N: usize>(
767    from: *const c_char,
768    to: *const c_char,
769    pixels: *mut T,
770    len: usize,
771) -> i32
772where
773    Channels<N>: ValidChannels,
774{
775    let Ok(from) = Space::try_from(from) else { return 1 };
776    let Ok(to) = Space::try_from(to) else { return 2 };
777    let pixels = unsafe {
778        if pixels.is_null() {
779            return 3;
780        } else {
781            core::slice::from_raw_parts_mut(pixels, len)
782        }
783    };
784    convert_space_sliced::<T, N>(from, to, pixels);
785    0
786}
787
788// ### Convert Space ### }}}
789
790// ### Str2Col ### {{{
791fn rm_paren<'a>(s: &'a str) -> &'a str {
792    if let (Some(f), Some(l)) = (s.chars().next(), s.chars().last()) {
793        if ['(', '[', '{'].contains(&f) && [')', ']', '}'].contains(&l) {
794            return &s[1..(s.len() - 1)];
795        }
796    }
797    s
798}
799
800/// Convert a string into a space/array combo.
801/// Separated with spaces, ';', ':', or ','
802///
803/// Can additionally be set as a % of SDR range.
804///
805/// Alpha will be NaN if only 3 values are provided.
806///
807/// # Examples
808///
809/// ```
810/// use colcon::{str2col, Space};
811///
812/// assert_eq!(str2col("0.2, 0.5, 0.6"), Some((Space::SRGB, [0.2f32, 0.5, 0.6])));
813/// assert_eq!(str2col("lch:50;20;120"), Some((Space::CIELCH, [50.0f32, 20.0, 120.0])));
814/// assert_eq!(str2col("oklab(0.2, 0.6, -0.5)"), Some((Space::OKLAB, [0.2f32, 0.6, -0.5])));
815/// assert_eq!(str2col("srgb 100% 50% 25%"), Some((Space::SRGB, [1.0f32, 0.5, 0.25])));
816/// ```
817pub fn str2col<T: DType, const N: usize>(mut s: &str) -> Option<(Space, [T; N])>
818where
819    Channels<N>: ValidChannels,
820{
821    s = rm_paren(s.trim());
822    let mut space = Space::SRGB;
823    let mut result = [f32::NAN; N];
824
825    // Return hex if valid
826    if let Ok(irgb) = hex_to_irgb(s) {
827        return Some((space, irgb_to_srgb(irgb)));
828    }
829
830    let seps = [',', ':', ';'];
831
832    // Find Space at front then trim
833    if let Some(i) = s.find(|c: char| c.is_whitespace() || seps.contains(&c) || ['(', '[', '{'].contains(&c)) {
834        if let Ok(sp) = Space::try_from(&s[..i]) {
835            space = sp;
836            s = rm_paren(s[i..].trim_start_matches(|c: char| c.is_whitespace() || seps.contains(&c)));
837        }
838    }
839
840    // Split by separators + whitespace and parse
841    for (n, split) in s
842        .split(|c: char| c.is_whitespace() || seps.contains(&c))
843        .filter(|s| !s.is_empty())
844        .enumerate()
845    {
846        if n > 3 {
847            return None;
848        } else if n >= result.len() {
849            continue;
850        } else if let Ok(value) = split.parse::<f32>() {
851            result[n] = value;
852        } else if split.ends_with('%') {
853            if let Ok(percent) = split[0..(split.len() - 1)].parse::<f32>() {
854                // alpha
855                if n == 3 {
856                    result[n] = percent / 100.0;
857                    continue;
858                }
859                let (q0, q100) = (space.srgb_quants()[0][n], space.srgb_quants()[100][n]);
860                if q0.is_finite() && q100.is_finite() {
861                    result[n] = percent / 100.0 * (q100 - q0) + q0;
862                } else if Space::UCS_POLAR.contains(&space) {
863                    result[n] = percent / 100.0 * 360.0
864                } else if space == Space::HSV {
865                    result[n] = percent / 100.0
866                } else {
867                    return None;
868                }
869            } else {
870                return None;
871            }
872        } else {
873            return None;
874        }
875    }
876    if result.iter().take(3).all(|v| v.is_finite()) {
877        Some((space, result.map(|c| c.to_dt())))
878    } else {
879        None
880    }
881}
882
883/// Convert a string into a pixel of the requested Space.
884///
885/// Shorthand for str2col() -> convert_space()
886pub fn str2space<T: DType, const N: usize>(s: &str, to: Space) -> Option<[T; N]>
887where
888    Channels<N>: ValidChannels,
889{
890    str2col(s).map(|(from, mut col)| {
891        convert_space(from, to, &mut col);
892        col
893    })
894}
895
896/// Same as `str2space` but with FFI types
897///
898/// Returns an N-length pointer to T on success or null on failure
899pub fn str2space_ffi<T: DType, const N: usize>(s: *const c_char, to: *const c_char) -> *const T
900where
901    Channels<N>: ValidChannels,
902{
903    if s.is_null() {
904        return core::ptr::null();
905    };
906    let Some(s) = unsafe { CStr::from_ptr(s) }.to_str().ok() else {
907        return core::ptr::null();
908    };
909    let Ok(to) = Space::try_from(to) else {
910        return core::ptr::null();
911    };
912    str2space::<T, N>(s, to).map_or(core::ptr::null(), |b| Box::into_raw(Box::new(b)).cast())
913}
914// ### Str2Col ### }}}
915
916// ### FORWARD ### {{{
917
918/// Convert floating (0.0..1.0) RGB to integer (0..255) RGB.
919pub fn srgb_to_irgb<const N: usize>(pixel: [f32; N]) -> [u8; N]
920where
921    Channels<N>: ValidChannels,
922{
923    pixel.map(|c| ((c * 255.0).round().max(0.0).min(255.0) as u8))
924}
925
926/// Create a hexadecimal string from integer RGB.
927pub fn irgb_to_hex<const N: usize>(pixel: [u8; N]) -> String
928where
929    Channels<N>: ValidChannels,
930{
931    let mut hex = String::with_capacity(N * 2 + 1);
932    hex.push('#');
933
934    pixel.into_iter().for_each(|c| {
935        [c / 16, c % 16]
936            .into_iter()
937            .for_each(|n| hex.push(if n >= 10 { n + 55 } else { n + 48 } as char))
938    });
939
940    hex
941}
942
943/// Convert from sRGB to HSV.
944pub fn srgb_to_hsv<T: DType, const N: usize>(pixel: &mut [T; N])
945where
946    Channels<N>: ValidChannels,
947{
948    let vmin = pixel[0].min(pixel[1]).min(pixel[2]);
949    let vmax = pixel[0].max(pixel[1]).max(pixel[2]);
950    let dmax = vmax - vmin;
951
952    let v = vmax;
953
954    let (h, s): (T, T) = if dmax == 0.0.to_dt() {
955        (0.0.to_dt(), 0.0.to_dt())
956    } else {
957        let s = dmax / vmax;
958
959        let [branch_0, branch_1] = [pixel[0] == vmax, pixel[1] == vmax];
960
961        pixel.iter_mut().take(3).for_each(|c| {
962            *c = (((vmax - *c) / 6.0.to_dt()) + (dmax / 2.0.to_dt())) / dmax;
963        });
964
965        let h = if branch_0 {
966            pixel[2] - pixel[1]
967        } else if branch_1 {
968            T::ff32(1.0 / 3.0) + pixel[0] - pixel[2]
969        } else {
970            T::ff32(2.0 / 3.0) + pixel[1] - pixel[0]
971        }
972        .rem_euclid(1.0.to_dt());
973        (h, s)
974    };
975    pixel[0] = h;
976    pixel[1] = s;
977    pixel[2] = v;
978}
979
980/// Convert from sRGB to Linear RGB by applying the sRGB EOTF
981///
982/// <https://www.color.org/chardata/rgb/srgb.xalter>
983pub fn srgb_to_lrgb<T: DType, const N: usize>(pixel: &mut [T; N])
984where
985    Channels<N>: ValidChannels,
986{
987    pixel.iter_mut().take(3).for_each(|c| *c = srgb_eotf(*c));
988}
989
990/// Convert from Linear Light RGB to CIE XYZ, D65 standard illuminant
991///
992/// <https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ>
993pub fn lrgb_to_xyz<T: DType, const N: usize>(pixel: &mut [T; N])
994where
995    Channels<N>: ValidChannels,
996{
997    [pixel[0], pixel[1], pixel[2]] = mm(XYZ65_MAT, [pixel[0], pixel[1], pixel[2]])
998}
999
1000/// Convert from CIE XYZ to CIE LAB.
1001///
1002/// <https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB>
1003pub fn xyz_to_cielab<T: DType, const N: usize>(pixel: &mut [T; N])
1004where
1005    Channels<N>: ValidChannels,
1006{
1007    // Reverse D65 standard illuminant
1008    pixel.iter_mut().take(3).zip(D65).for_each(|(c, d)| *c = *c / d.to_dt());
1009
1010    pixel.iter_mut().take(3).for_each(|c| {
1011        if *c > T::ff32(LAB_DELTA).powi(3) {
1012            *c = c.cbrt()
1013        } else {
1014            *c = *c / (3.0 * LAB_DELTA.powi(2)).to_dt() + (4f32 / 29f32).to_dt()
1015        }
1016    });
1017
1018    [pixel[0], pixel[1], pixel[2]] = [
1019        T::ff32(116.0).fma(pixel[1], T::ff32(-16.0)),
1020        T::ff32(500.0) * (pixel[0] - pixel[1]),
1021        T::ff32(200.0) * (pixel[1] - pixel[2]),
1022    ]
1023}
1024
1025/// Convert from CIE XYZ to OKLAB.
1026///
1027/// <https://bottosson.github.io/posts/oklab/>
1028pub fn xyz_to_oklab<T: DType, const N: usize>(pixel: &mut [T; N])
1029where
1030    Channels<N>: ValidChannels,
1031{
1032    let mut lms = mm(OKLAB_M1, [pixel[0], pixel[1], pixel[2]]);
1033    lms.iter_mut().for_each(|c| *c = c.scbrt());
1034    [pixel[0], pixel[1], pixel[2]] = mm(OKLAB_M2, lms);
1035}
1036
1037/// Convert CIE XYZ to JzAzBz
1038///
1039/// <https://opg.optica.org/oe/fulltext.cfm?uri=oe-25-13-15131>
1040pub fn xyz_to_jzazbz<T: DType, const N: usize>(pixel: &mut [T; N])
1041where
1042    Channels<N>: ValidChannels,
1043{
1044    let mut lms = mm(
1045        JZAZBZ_M1,
1046        [
1047            pixel[0].fma(JZAZBZ_B.to_dt(), T::ff32(-JZAZBZ_B + 1.0) * pixel[2]),
1048            pixel[1].fma(JZAZBZ_G.to_dt(), T::ff32(-JZAZBZ_G + 1.0) * pixel[0]),
1049            pixel[2],
1050        ],
1051    );
1052
1053    lms.iter_mut().for_each(|e| *e = pqz_oetf(*e));
1054
1055    let lab = mm(JZAZBZ_M2, lms);
1056
1057    pixel[0] = (T::ff32(1.0 + JZAZBZ_D) * lab[0]) / lab[0].fma(JZAZBZ_D.to_dt(), 1.0.to_dt()) - JZAZBZ_D0.to_dt();
1058    pixel[1] = lab[1];
1059    pixel[2] = lab[2];
1060}
1061
1062// Disabled for now as all the papers are paywalled
1063// /// Convert CIE XYZ to CAM16-UCS
1064// #[no_mangle]
1065// pub extern "C" fn xyz_to_cam16ucs(pixel: &mut [f32; 3]) {
1066
1067// }
1068
1069/// Convert LRGB to ICtCp. Unvalidated, WIP
1070///
1071/// <https://www.itu.int/rec/R-REC-BT.2100/en>
1072pub fn _lrgb_to_ictcp<T: DType, const N: usize>(pixel: &mut [T; N])
1073where
1074    Channels<N>: ValidChannels,
1075{
1076    // <https://www.itu.int/rec/R-REC-BT.2020/en>
1077    // let alpha = 1.09929682680944;
1078    // let beta = 0.018053968510807;
1079    // let bt2020 = |e: &mut f32| {
1080    //     *e = if *e < beta {4.5 * *e}
1081    //     else {alpha * e.powf(0.45) - (alpha - 1.0)}
1082    // };
1083    // pixel.iter_mut().for_each(|c| bt2020(c));
1084
1085    let mut lms = mm(ICTCP_M1, [pixel[0], pixel[1], pixel[2]]);
1086    // lms prime
1087    lms.iter_mut().for_each(|c| *c = pq_oetf(*c));
1088    [pixel[0], pixel[1], pixel[2]] = mm(ICTCP_M2, lms);
1089}
1090
1091/// Converts an LAB based space to a cylindrical representation.
1092///
1093/// <https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_model>
1094pub fn lab_to_lch<T: DType, const N: usize>(pixel: &mut [T; N])
1095where
1096    Channels<N>: ValidChannels,
1097{
1098    [pixel[0], pixel[1], pixel[2]] = [
1099        pixel[0],
1100        (pixel[1].powi(2) + pixel[2].powi(2)).sqrt(),
1101        pixel[2].atan2(pixel[1]).to_degrees().rem_euclid(360.0.to_dt()),
1102    ];
1103}
1104
1105// ### FORWARD ### }}}
1106
1107// ### BACKWARD ### {{{
1108
1109/// Convert integer (0..255) RGB to floating (0.0..1.0) RGB.
1110pub fn irgb_to_srgb<T: DType, const N: usize>(pixel: [u8; N]) -> [T; N]
1111where
1112    Channels<N>: ValidChannels,
1113{
1114    pixel.map(|c| T::ff32(c as f32 / 255.0))
1115}
1116
1117/// Create integer RGB set from hex string.
1118/// `DEFAULT` is only used when 4 channels are requested but 3 is given.
1119pub fn hex_to_irgb_default<const N: usize, const DEFAULT: u8>(hex: &str) -> Result<[u8; N], String>
1120where
1121    Channels<N>: ValidChannels,
1122{
1123    let mut chars = hex.trim().chars();
1124    if chars.as_str().starts_with('#') {
1125        chars.next();
1126    }
1127
1128    let ids: Vec<u32> = match chars.as_str().len() {
1129        6 | 8 => chars
1130            .map(|c| {
1131                let u = c as u32;
1132                // numeric
1133                if 57 >= u && u >= 48 {
1134                    Ok(u - 48)
1135                // uppercase
1136                } else if 70 >= u && u >= 65 {
1137                    Ok(u - 55)
1138                // lowercase
1139                } else if 102 >= u && u >= 97 {
1140                    Ok(u - 87)
1141                } else {
1142                    Err(String::from("Hex character '") + &String::from(c) + "' out of bounds")
1143                }
1144            })
1145            .collect(),
1146        n => Err(String::from("Incorrect hex length ") + &n.to_string()),
1147    }?;
1148
1149    let mut result = [DEFAULT; N];
1150
1151    ids.chunks(2)
1152        .take(result.len())
1153        .enumerate()
1154        .for_each(|(n, chunk)| result[n] = ((chunk[0]) * 16 + chunk[1]) as u8);
1155
1156    Ok(result)
1157}
1158
1159/// Create integer RGB set from hex string.
1160/// Will default to 255 for alpha if 4 channels requested but hex length is 6.
1161/// Use `hex_to_irgb_default` to customize this.
1162pub fn hex_to_irgb<const N: usize>(hex: &str) -> Result<[u8; N], String>
1163where
1164    Channels<N>: ValidChannels,
1165{
1166    hex_to_irgb_default::<N, 255>(hex)
1167}
1168
1169/// Convert from HSV to sRGB.
1170pub fn hsv_to_srgb<T: DType, const N: usize>(pixel: &mut [T; N])
1171where
1172    Channels<N>: ValidChannels,
1173{
1174    if pixel[1] == 0.0.to_dt() {
1175        [pixel[0], pixel[1]] = [pixel[2]; 2];
1176    } else {
1177        let mut var_h = pixel[0] * 6.0.to_dt();
1178        if var_h == 6.0.to_dt() {
1179            var_h = 0.0.to_dt()
1180        }
1181        let var_i = var_h.trunc();
1182        let var_1 = pixel[2] * (T::ff32(1.0) - pixel[1]);
1183        let var_2 = pixel[2] * (-var_h + var_i).fma(pixel[1], 1.0.to_dt());
1184        let var_3 = pixel[2] * (T::ff32(-1.0) + (var_h - var_i)).fma(pixel[1], T::ff32(1.0));
1185
1186        [pixel[0], pixel[1], pixel[2]] = if var_i == 0.0.to_dt() {
1187            [pixel[2], var_3, var_1]
1188        } else if var_i == 1.0.to_dt() {
1189            [var_2, pixel[2], var_1]
1190        } else if var_i == 2.0.to_dt() {
1191            [var_1, pixel[2], var_3]
1192        } else if var_i == 3.0.to_dt() {
1193            [var_1, var_2, pixel[2]]
1194        } else if var_i == 4.0.to_dt() {
1195            [var_3, var_1, pixel[2]]
1196        } else {
1197            [pixel[2], var_1, var_2]
1198        }
1199    }
1200}
1201
1202/// Convert from Linear RGB to sRGB by applying the inverse sRGB EOTF
1203///
1204/// <https://www.color.org/chardata/rgb/srgb.xalter>
1205pub fn lrgb_to_srgb<T: DType, const N: usize>(pixel: &mut [T; N])
1206where
1207    Channels<N>: ValidChannels,
1208{
1209    pixel.iter_mut().take(3).for_each(|c| *c = srgb_oetf(*c));
1210}
1211
1212/// Convert from CIE XYZ to Linear Light RGB.
1213///
1214/// <https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB>
1215pub fn xyz_to_lrgb<T: DType, const N: usize>(pixel: &mut [T; N])
1216where
1217    Channels<N>: ValidChannels,
1218{
1219    [pixel[0], pixel[1], pixel[2]] = mm(XYZ65_MAT_INV, [pixel[0], pixel[1], pixel[2]])
1220}
1221
1222/// Convert from CIE LAB to CIE XYZ.
1223///
1224/// <https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIELAB_to_CIEXYZ>
1225pub fn cielab_to_xyz<T: DType, const N: usize>(pixel: &mut [T; N])
1226where
1227    Channels<N>: ValidChannels,
1228{
1229    pixel[0] = pixel[0].fma((1.0 / 116.0).to_dt(), (16.0 / 116.0).to_dt());
1230    [pixel[0], pixel[1], pixel[2]] = [
1231        pixel[0] + pixel[1] / 500.0.to_dt(),
1232        pixel[0],
1233        pixel[0] - pixel[2] / 200.0.to_dt(),
1234    ];
1235
1236    pixel.iter_mut().take(3).for_each(|c| {
1237        if *c > LAB_DELTA.to_dt() {
1238            *c = c.powi(3)
1239        } else {
1240            *c = T::ff32(3.0) * LAB_DELTA.powi(2).to_dt() * (*c - (4f32 / 29f32).to_dt())
1241        }
1242    });
1243
1244    pixel.iter_mut().take(3).zip(D65).for_each(|(c, d)| *c = *c * d.to_dt());
1245}
1246
1247/// Convert from OKLAB to CIE XYZ.
1248///
1249/// <https://bottosson.github.io/posts/oklab/>
1250pub fn oklab_to_xyz<T: DType, const N: usize>(pixel: &mut [T; N])
1251where
1252    Channels<N>: ValidChannels,
1253{
1254    let mut lms = mm(OKLAB_M2_INV, [pixel[0], pixel[1], pixel[2]]);
1255    lms.iter_mut().for_each(|c| *c = c.powi(3));
1256    [pixel[0], pixel[1], pixel[2]] = mm(OKLAB_M1_INV, lms);
1257}
1258
1259/// Convert JzAzBz to CIE XYZ
1260///
1261/// <https://opg.optica.org/oe/fulltext.cfm?uri=oe-25-13-15131>
1262pub fn jzazbz_to_xyz<T: DType, const N: usize>(pixel: &mut [T; N])
1263where
1264    Channels<N>: ValidChannels,
1265{
1266    let mut lms = mm(
1267        JZAZBZ_M2_INV,
1268        [
1269            (pixel[0] + JZAZBZ_D0.to_dt())
1270                / (pixel[0] + JZAZBZ_D0.to_dt()).fma(T::ff32(-JZAZBZ_D), T::ff32(1.0 + JZAZBZ_D)),
1271            pixel[1],
1272            pixel[2],
1273        ],
1274    );
1275
1276    lms.iter_mut().for_each(|c| *c = pqz_eotf(*c));
1277
1278    [pixel[0], pixel[1], pixel[2]] = mm(JZAZBZ_M1_INV, lms);
1279
1280    pixel[0] = pixel[2].fma((JZAZBZ_B - 1.0).to_dt(), pixel[0]) / JZAZBZ_B.to_dt();
1281    pixel[1] = pixel[0].fma((JZAZBZ_G - 1.0).to_dt(), pixel[1]) / JZAZBZ_G.to_dt();
1282}
1283
1284// Disabled for now as all the papers are paywalled
1285// /// Convert CAM16-UCS to CIE XYZ
1286// #[no_mangle]
1287// pub extern "C" fn cam16ucs_to_xyz(pixel: &mut [f32; 3]) {
1288
1289// }
1290
1291/// Convert ICtCp to LRGB. Unvalidated, WIP
1292///
1293/// <https://www.itu.int/rec/R-REC-BT.2100/en>
1294// #[no_mangle]
1295pub fn _ictcp_to_lrgb<T: DType, const N: usize>(pixel: &mut [T; N])
1296where
1297    Channels<N>: ValidChannels,
1298{
1299    // lms prime
1300    let mut lms = mm(ICTCP_M2_INV, [pixel[0], pixel[1], pixel[2]]);
1301    // non-prime lms
1302    lms.iter_mut().for_each(|c| *c = pq_eotf(*c));
1303    [pixel[0], pixel[1], pixel[2]] = mm(ICTCP_M1_INV, lms);
1304}
1305
1306/// Retrieves an LAB based space from its cylindrical representation.
1307///
1308/// <https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_model>
1309pub fn lch_to_lab<T: DType, const N: usize>(pixel: &mut [T; N])
1310where
1311    Channels<N>: ValidChannels,
1312{
1313    [pixel[0], pixel[1], pixel[2]] = [
1314        pixel[0],
1315        pixel[1] * pixel[2].to_radians().cos(),
1316        pixel[1] * pixel[2].to_radians().sin(),
1317    ]
1318}
1319
1320// BACKWARD }}}
1321
1322// ### MONOTYPED EXTERNAL FUNCTIONS ### {{{
1323
1324#[no_mangle]
1325extern "C" fn convert_space_3f32(from: *const c_char, to: *const c_char, pixels: *mut f32, len: usize) -> i32 {
1326    convert_space_ffi::<_, 3>(from, to, pixels, len)
1327}
1328#[no_mangle]
1329extern "C" fn convert_space_4f32(from: *const c_char, to: *const c_char, pixels: *mut f32, len: usize) -> i32 {
1330    convert_space_ffi::<_, 4>(from, to, pixels, len)
1331}
1332#[no_mangle]
1333extern "C" fn convert_space_3f64(from: *const c_char, to: *const c_char, pixels: *mut f64, len: usize) -> i32 {
1334    convert_space_ffi::<_, 3>(from, to, pixels, len)
1335}
1336#[no_mangle]
1337extern "C" fn convert_space_4f64(from: *const c_char, to: *const c_char, pixels: *mut f64, len: usize) -> i32 {
1338    convert_space_ffi::<_, 4>(from, to, pixels, len)
1339}
1340
1341#[no_mangle]
1342extern "C" fn str2space_3f32(s: *const c_char, to: *const c_char) -> *const f32 {
1343    str2space_ffi::<f32, 3>(s, to)
1344}
1345#[no_mangle]
1346extern "C" fn str2space_4f32(s: *const c_char, to: *const c_char) -> *const f32 {
1347    str2space_ffi::<f32, 4>(s, to)
1348}
1349#[no_mangle]
1350extern "C" fn str2space_3f64(s: *const c_char, to: *const c_char) -> *const f64 {
1351    str2space_ffi::<f64, 3>(s, to)
1352}
1353#[no_mangle]
1354extern "C" fn str2space_4f64(s: *const c_char, to: *const c_char) -> *const f64 {
1355    str2space_ffi::<f64, 4>(s, to)
1356}
1357
1358macro_rules! cdef1 {
1359    ($base:ident, $f32:ident, $f64:ident) => {
1360        #[no_mangle]
1361        extern "C" fn $f32(value: f32) -> f32 {
1362            $base(value)
1363        }
1364        #[no_mangle]
1365        extern "C" fn $f64(value: f64) -> f64 {
1366            $base(value)
1367        }
1368    };
1369}
1370
1371macro_rules! cdef3 {
1372    ($base:ident, $f32_3:ident, $f64_3:ident, $f32_4:ident, $f64_4:ident) => {
1373        #[no_mangle]
1374        extern "C" fn $f32_3(pixel: &mut [f32; 3]) {
1375            $base(pixel)
1376        }
1377        #[no_mangle]
1378        extern "C" fn $f64_3(pixel: &mut [f64; 3]) {
1379            $base(pixel)
1380        }
1381        #[no_mangle]
1382        extern "C" fn $f32_4(pixel: &mut [f32; 4]) {
1383            $base(pixel)
1384        }
1385        #[no_mangle]
1386        extern "C" fn $f64_4(pixel: &mut [f64; 4]) {
1387            $base(pixel)
1388        }
1389    };
1390}
1391
1392macro_rules! cdef31 {
1393    ($base:ident, $f32_3:ident, $f64_3:ident, $f32_4:ident, $f64_4:ident) => {
1394        #[no_mangle]
1395        extern "C" fn $f32_3(pixel: &[f32; 3]) -> f32 {
1396            $base(pixel)
1397        }
1398        #[no_mangle]
1399        extern "C" fn $f64_3(pixel: &[f64; 3]) -> f64 {
1400            $base(pixel)
1401        }
1402        #[no_mangle]
1403        extern "C" fn $f32_4(pixel: &[f32; 4]) -> f32 {
1404            $base(pixel)
1405        }
1406        #[no_mangle]
1407        extern "C" fn $f64_4(pixel: &[f64; 4]) -> f64 {
1408            $base(pixel)
1409        }
1410    };
1411}
1412
1413// Transfer Functions
1414cdef1!(srgb_eotf, srgb_eotf_f32, srgb_eotf_f64);
1415cdef1!(srgb_oetf, srgb_oetf_f32, srgb_oetf_f64);
1416cdef1!(pq_eotf, pq_eotf_f32, pq_eotf_f64);
1417cdef1!(pqz_eotf, pqz_eotf_f32, pqz_eotf_f64);
1418cdef1!(pq_oetf, pq_oetf_f32, pq_oetf_f64);
1419cdef1!(pqz_oetf, pqz_oetf_f32, pqz_oetf_f64);
1420
1421// Helmholtz-Kohlrausch
1422cdef31!(
1423    hk_high2023,
1424    hk_high2023_3f32,
1425    hk_high2023_3f64,
1426    hk_high2023_4f32,
1427    hk_high2023_4f64
1428);
1429cdef3!(
1430    hk_high2023_comp,
1431    hk_high2023_comp_3f32,
1432    hk_high2023_comp_3f64,
1433    hk_high2023_comp_4f32,
1434    hk_high2023_comp_4f64
1435);
1436
1437// Forward
1438cdef3!(
1439    srgb_to_hsv,
1440    srgb_to_hsv_3f32,
1441    srgb_to_hsv_3f64,
1442    srgb_to_hsv_4f32,
1443    srgb_to_hsv_4f64
1444);
1445cdef3!(
1446    srgb_to_lrgb,
1447    srgb_to_lrgb_3f32,
1448    srgb_to_lrgb_3f64,
1449    srgb_to_lrgb_4f32,
1450    srgb_to_lrgb_4f64
1451);
1452cdef3!(
1453    lrgb_to_xyz,
1454    lrgb_to_xyz_3f32,
1455    lrgb_to_xyz_3f64,
1456    lrgb_to_xyz_4f32,
1457    lrgb_to_xyz_4f64
1458);
1459cdef3!(
1460    xyz_to_cielab,
1461    xyz_to_cielab_3f32,
1462    xyz_to_cielab_3f64,
1463    xyz_to_cielab_4f32,
1464    xyz_to_cielab_4f64
1465);
1466cdef3!(
1467    xyz_to_oklab,
1468    xyz_to_oklab_3f32,
1469    xyz_to_oklab_3f64,
1470    xyz_to_oklab_4f32,
1471    xyz_to_oklab_4f64
1472);
1473cdef3!(
1474    xyz_to_jzazbz,
1475    xyz_to_jzazbz_3f32,
1476    xyz_to_jzazbz_3f64,
1477    xyz_to_jzazbz_4f32,
1478    xyz_to_jzazbz_4f64
1479);
1480cdef3!(
1481    lab_to_lch,
1482    lab_to_lch_3f32,
1483    lab_to_lch_3f64,
1484    lab_to_lch_4f32,
1485    lab_to_lch_4f64
1486);
1487cdef3!(
1488    _lrgb_to_ictcp,
1489    _lrgb_to_ictcp_3f32,
1490    _lrgb_to_ictcp_3f64,
1491    _lrgb_to_ictcp_4f32,
1492    _lrgb_to_ictcp_4f64
1493);
1494
1495// Backward
1496cdef3!(
1497    hsv_to_srgb,
1498    hsv_to_srgb_3f32,
1499    hsv_to_srgb_3f64,
1500    hsv_to_srgb_4f32,
1501    hsv_to_srgb_4f64
1502);
1503cdef3!(
1504    lrgb_to_srgb,
1505    lrgb_to_srgb_3f32,
1506    lrgb_to_srgb_3f64,
1507    lrgb_to_srgb_4f32,
1508    lrgb_to_srgb_4f64
1509);
1510cdef3!(
1511    xyz_to_lrgb,
1512    xyz_to_lrgb_3f32,
1513    xyz_to_lrgb_3f64,
1514    xyz_to_lrgb_4f32,
1515    xyz_to_lrgb_4f64
1516);
1517cdef3!(
1518    cielab_to_xyz,
1519    cielab_to_xyz_3f32,
1520    cielab_to_xyz_3f64,
1521    cielab_to_xyz_4f32,
1522    cielab_to_xyz_4f64
1523);
1524cdef3!(
1525    oklab_to_xyz,
1526    oklab_to_xyz_3f32,
1527    oklab_to_xyz_3f64,
1528    oklab_to_xyz_4f32,
1529    oklab_to_xyz_4f64
1530);
1531cdef3!(
1532    jzazbz_to_xyz,
1533    jzazbz_to_xyz_3f32,
1534    jzazbz_to_xyz_3f64,
1535    jzazbz_to_xyz_4f32,
1536    jzazbz_to_xyz_4f64
1537);
1538cdef3!(
1539    lch_to_lab,
1540    lch_to_lab_3f32,
1541    lch_to_lab_3f64,
1542    lch_to_lab_4f32,
1543    lch_to_lab_4f64
1544);
1545cdef3!(
1546    _ictcp_to_lrgb,
1547    _ictcp_to_lrgb_3f32,
1548    _ictcp_to_lrgb_3f64,
1549    _ictcp_to_lrgb_4f32,
1550    _ictcp_to_lrgb_4f64
1551);
1552
1553// }}}