#![allow(clippy::approx_constant, clippy::excessive_precision)]
use crate::musl_math::pow;
use crate::value::ColorSpace;
#[allow(clippy::unreadable_literal)]
mod matrices {
include!("colorspace_matrices.rs");
}
use matrices::*;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Lin {
Srgb,
DisplayP3,
A98,
Prophoto,
Rec2020,
XyzD65,
XyzD50,
Lms,
}
fn matrix(src: Lin, dst: Lin) -> &'static [f64; 9] {
use Lin::*;
match (src, dst) {
(Srgb, DisplayP3) => &LINEAR_SRGB_TO_LINEAR_DISPLAY_P3,
(Srgb, A98) => &LINEAR_SRGB_TO_LINEAR_A98_RGB,
(Srgb, Prophoto) => &LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB,
(Srgb, Rec2020) => &LINEAR_SRGB_TO_LINEAR_REC2020,
(Srgb, XyzD65) => &LINEAR_SRGB_TO_XYZ_D65,
(Srgb, XyzD50) => &LINEAR_SRGB_TO_XYZ_D50,
(Srgb, Lms) => &LINEAR_SRGB_TO_LMS,
(DisplayP3, Srgb) => &LINEAR_DISPLAY_P3_TO_LINEAR_SRGB,
(DisplayP3, A98) => &LINEAR_DISPLAY_P3_TO_LINEAR_A98_RGB,
(DisplayP3, Prophoto) => &LINEAR_DISPLAY_P3_TO_LINEAR_PROPHOTO_RGB,
(DisplayP3, Rec2020) => &LINEAR_DISPLAY_P3_TO_LINEAR_REC2020,
(DisplayP3, XyzD65) => &LINEAR_DISPLAY_P3_TO_XYZ_D65,
(DisplayP3, XyzD50) => &LINEAR_DISPLAY_P3_TO_XYZ_D50,
(DisplayP3, Lms) => &LINEAR_DISPLAY_P3_TO_LMS,
(A98, Srgb) => &LINEAR_A98_RGB_TO_LINEAR_SRGB,
(A98, DisplayP3) => &LINEAR_A98_RGB_TO_LINEAR_DISPLAY_P3,
(A98, Prophoto) => &LINEAR_A98_RGB_TO_LINEAR_PROPHOTO_RGB,
(A98, Rec2020) => &LINEAR_A98_RGB_TO_LINEAR_REC2020,
(A98, XyzD65) => &LINEAR_A98_RGB_TO_XYZ_D65,
(A98, XyzD50) => &LINEAR_A98_RGB_TO_XYZ_D50,
(A98, Lms) => &LINEAR_A98_RGB_TO_LMS,
(Prophoto, Srgb) => &LINEAR_PROPHOTO_RGB_TO_LINEAR_SRGB,
(Prophoto, DisplayP3) => &LINEAR_PROPHOTO_RGB_TO_LINEAR_DISPLAY_P3,
(Prophoto, A98) => &LINEAR_PROPHOTO_RGB_TO_LINEAR_A98_RGB,
(Prophoto, Rec2020) => &LINEAR_PROPHOTO_RGB_TO_LINEAR_REC2020,
(Prophoto, XyzD65) => &LINEAR_PROPHOTO_RGB_TO_XYZ_D65,
(Prophoto, XyzD50) => &LINEAR_PROPHOTO_RGB_TO_XYZ_D50,
(Prophoto, Lms) => &LINEAR_PROPHOTO_RGB_TO_LMS,
(Rec2020, Srgb) => &LINEAR_REC2020_TO_LINEAR_SRGB,
(Rec2020, DisplayP3) => &LINEAR_REC2020_TO_LINEAR_DISPLAY_P3,
(Rec2020, A98) => &LINEAR_REC2020_TO_LINEAR_A98_RGB,
(Rec2020, Prophoto) => &LINEAR_REC2020_TO_LINEAR_PROPHOTO_RGB,
(Rec2020, XyzD65) => &LINEAR_REC2020_TO_XYZ_D65,
(Rec2020, XyzD50) => &LINEAR_REC2020_TO_XYZ_D50,
(Rec2020, Lms) => &LINEAR_REC2020_TO_LMS,
(XyzD65, Srgb) => &XYZ_D65_TO_LINEAR_SRGB,
(XyzD65, DisplayP3) => &XYZ_D65_TO_LINEAR_DISPLAY_P3,
(XyzD65, A98) => &XYZ_D65_TO_LINEAR_A98_RGB,
(XyzD65, Prophoto) => &XYZ_D65_TO_LINEAR_PROPHOTO_RGB,
(XyzD65, Rec2020) => &XYZ_D65_TO_LINEAR_REC2020,
(XyzD65, XyzD50) => &XYZ_D65_TO_XYZ_D50,
(XyzD65, Lms) => &XYZ_D65_TO_LMS,
(XyzD50, Srgb) => &XYZ_D50_TO_LINEAR_SRGB,
(XyzD50, DisplayP3) => &XYZ_D50_TO_LINEAR_DISPLAY_P3,
(XyzD50, A98) => &XYZ_D50_TO_LINEAR_A98_RGB,
(XyzD50, Prophoto) => &XYZ_D50_TO_LINEAR_PROPHOTO_RGB,
(XyzD50, Rec2020) => &XYZ_D50_TO_LINEAR_REC2020,
(XyzD50, XyzD65) => &XYZ_D50_TO_XYZ_D65,
(XyzD50, Lms) => &XYZ_D50_TO_LMS,
(Lms, Srgb) => &LMS_TO_LINEAR_SRGB,
(Lms, DisplayP3) => &LMS_TO_LINEAR_DISPLAY_P3,
(Lms, A98) => &LMS_TO_LINEAR_A98_RGB,
(Lms, Prophoto) => &LMS_TO_LINEAR_PROPHOTO_RGB,
(Lms, Rec2020) => &LMS_TO_LINEAR_REC2020,
(Lms, XyzD65) => &LMS_TO_XYZ_D65,
(Lms, XyzD50) => &LMS_TO_XYZ_D50,
_ => unreachable!("no transformation matrix for identical linear nodes"),
}
}
fn srgb_to_linear(c: f64) -> f64 {
let abs = c.abs();
if abs <= 0.04045 {
c / 12.92
} else {
c.signum() * pow((abs + 0.055) / 1.055, 2.4)
}
}
fn srgb_from_linear(c: f64) -> f64 {
let abs = c.abs();
if abs <= 0.0031308 {
c * 12.92
} else {
c.signum() * (1.055 * pow(abs, 0.4166666666666667) - 0.055)
}
}
fn a98_to_linear(c: f64) -> f64 {
c.signum() * pow(c.abs(), 2.19921875)
}
fn a98_from_linear(c: f64) -> f64 {
c.signum() * pow(c.abs(), 0.4547069271758437)
}
fn prophoto_to_linear(c: f64) -> f64 {
let abs = c.abs();
if abs <= 0.03125 {
c / 16.0
} else {
c.signum() * pow(abs, 1.8)
}
}
fn prophoto_from_linear(c: f64) -> f64 {
let abs = c.abs();
if abs >= 0.001953125 {
c.signum() * pow(abs, 0.5555555555555556)
} else {
16.0 * c
}
}
fn rec2020_to_linear(c: f64) -> f64 {
let abs = c.abs();
if abs < 0.08124285829863151 {
c / 4.5
} else {
c.signum()
* pow(
(abs + 1.09929682680944 - 1.0) / 1.09929682680944,
2.2222222222222223,
)
}
}
fn rec2020_from_linear(c: f64) -> f64 {
let abs = c.abs();
if abs > 0.018053968510807 {
c.signum() * (1.09929682680944 * pow(abs, 0.45) - 0.09929682680944008)
} else {
4.5 * c
}
}
type LinTransfer = (Lin, fn(f64) -> f64, fn(f64) -> f64);
fn lin_of(space: ColorSpace) -> LinTransfer {
fn id(c: f64) -> f64 {
c
}
match space {
ColorSpace::Rgb | ColorSpace::Srgb | ColorSpace::Hsl | ColorSpace::Hwb => {
(Lin::Srgb, srgb_to_linear, srgb_from_linear)
}
ColorSpace::SrgbLinear => (Lin::Srgb, id, id),
ColorSpace::DisplayP3 => (Lin::DisplayP3, srgb_to_linear, srgb_from_linear),
ColorSpace::DisplayP3Linear => (Lin::DisplayP3, id, id),
ColorSpace::A98Rgb => (Lin::A98, a98_to_linear, a98_from_linear),
ColorSpace::ProphotoRgb => (Lin::Prophoto, prophoto_to_linear, prophoto_from_linear),
ColorSpace::Rec2020 => (Lin::Rec2020, rec2020_to_linear, rec2020_from_linear),
ColorSpace::XyzD65 => (Lin::XyzD65, id, id),
ColorSpace::XyzD50 => (Lin::XyzD50, id, id),
ColorSpace::Lab | ColorSpace::Lch | ColorSpace::Oklab | ColorSpace::Oklch => {
unreachable!("lab-family spaces are handled before the matrix graph")
}
}
}
fn js_max(a: f64, b: f64) -> f64 {
if a.is_nan() || b.is_nan() {
f64::NAN
} else {
a.max(b)
}
}
fn js_min(a: f64, b: f64) -> f64 {
if a.is_nan() || b.is_nan() {
f64::NAN
} else {
a.min(b)
}
}
fn hue_to_rgb(m1: f64, m2: f64, mut hue: f64) -> f64 {
if hue < 0.0 {
hue += 1.0;
}
if hue > 1.0 {
hue -= 1.0;
}
if hue < 0.16666666666666666 {
m1 + (m2 - m1) * hue * 6.0
} else if hue < 0.5 {
m2
} else if hue < 0.6666666666666666 {
m1 + (m2 - m1) * (0.6666666666666666 - hue) * 6.0
} else {
m1
}
}
pub(super) fn hsl_to_srgb(hsl: [f64; 3]) -> [f64; 3] {
let scaled_hue = (hsl[0] / 360.0).rem_euclid(1.0);
let scaled_saturation = hsl[1] / 100.0;
let scaled_lightness = hsl[2] / 100.0;
let m2 = if scaled_lightness <= 0.5 {
scaled_lightness * (scaled_saturation + 1.0)
} else {
scaled_lightness + scaled_saturation - scaled_lightness * scaled_saturation
};
let m1 = scaled_lightness * 2.0 - m2;
[
hue_to_rgb(m1, m2, scaled_hue + 0.3333333333333333),
hue_to_rgb(m1, m2, scaled_hue),
hue_to_rgb(m1, m2, scaled_hue - 0.3333333333333333),
]
}
pub(crate) fn hwb_to_srgb(hwb: [f64; 3]) -> [f64; 3] {
let scaled_hue = hwb[0].rem_euclid(360.0) / 360.0;
let mut scaled_whiteness = hwb[1] / 100.0;
let mut scaled_blackness = hwb[2] / 100.0;
let sum = scaled_whiteness + scaled_blackness;
if sum > 1.0 {
scaled_whiteness /= sum;
scaled_blackness /= sum;
}
let factor = 1.0 - scaled_whiteness - scaled_blackness;
let to_rgb = |hue: f64| hue_to_rgb(0.0, 1.0, hue) * factor + scaled_whiteness;
[
to_rgb(scaled_hue + 0.3333333333333333),
to_rgb(scaled_hue),
to_rgb(scaled_hue - 0.3333333333333333),
]
}
pub(crate) fn srgb_to_hsl(rgb: [f64; 3]) -> [f64; 3] {
let (red, green, blue) = (rgb[0], rgb[1], rgb[2]);
let max = js_max(js_max(red, green), blue);
let min = js_min(js_min(red, green), blue);
let delta = max - min;
let mut hue = if max == min {
0.0
} else if max == red {
60.0 * (green - blue) / delta + 360.0
} else if max == green {
60.0 * (blue - red) / delta + 120.0
} else {
60.0 * (red - green) / delta + 240.0
};
let lightness = (min + max) / 2.0;
let mut saturation = if lightness == 0.0 || lightness == 1.0 {
0.0
} else {
100.0 * (max - lightness) / lightness.min(1.0 - lightness)
};
if saturation < 0.0 {
hue += 180.0;
saturation = saturation.abs();
}
[hue.rem_euclid(360.0), saturation, lightness * 100.0]
}
pub(super) fn srgb_to_hwb(rgb: [f64; 3]) -> [f64; 3] {
let (red, green, blue) = (rgb[0], rgb[1], rgb[2]);
let max = js_max(js_max(red, green), blue);
let min = js_min(js_min(red, green), blue);
let delta = max - min;
let hue = if max == min {
0.0
} else if max == red {
60.0 * (green - blue) / delta + 360.0
} else if max == green {
60.0 * (blue - red) / delta + 120.0
} else {
60.0 * (red - green) / delta + 240.0
};
[hue.rem_euclid(360.0), min * 100.0, 100.0 - max * 100.0]
}
#[inline]
fn cube(t: f64) -> f64 {
t * t * t
}
#[inline]
fn square(t: f64) -> f64 {
t * t
}
const LAB_EPSILON: f64 = 0.008856451679035631; const LAB_KAPPA: f64 = 903.2962962962963; const D50_X: f64 = 0.9642956764295677;
const D50_Z: f64 = 0.8251046025104602;
fn lab_f_to_x_or_z(component: f64) -> f64 {
let cubed = cube(component);
if cubed > LAB_EPSILON {
cubed
} else {
(116.0 * component - 16.0) / LAB_KAPPA
}
}
fn lab_to_xyz_d50(lab: [f64; 3]) -> [f64; 3] {
let f1 = (lab[0] + 16.0) / 116.0;
let x = lab_f_to_x_or_z(lab[1] / 500.0 + f1) * D50_X;
let y = if lab[0] > 8.0 {
cube(f1)
} else {
lab[0] / LAB_KAPPA
};
let z = lab_f_to_x_or_z(f1 - lab[2] / 200.0) * D50_Z;
[x, y, z]
}
fn xyz_component_to_lab_f(component: f64) -> f64 {
if component > LAB_EPSILON {
pow(component, 0.3333333333333333)
} else {
(LAB_KAPPA * component + 16.0) / 116.0
}
}
fn xyz_d50_to_lab(xyz: [f64; 3]) -> [f64; 3] {
let f0 = xyz_component_to_lab_f(xyz[0] / D50_X);
let f1 = xyz_component_to_lab_f(xyz[1] / 1.0);
let f2 = xyz_component_to_lab_f(xyz[2] / D50_Z);
[116.0 * f1 - 16.0, 500.0 * (f0 - f1), 200.0 * (f1 - f2)]
}
const PI: f64 = 3.141592653589793;
pub(super) fn lch_to_lab(lch: [f64; 3]) -> [f64; 3] {
let hue_radians = lch[2] * PI / 180.0;
[lch[0], lch[1] * hue_radians.cos(), lch[1] * hue_radians.sin()]
}
pub(super) fn lab_to_lch(lab: [f64; 3]) -> [f64; 3] {
let chroma = (square(lab[1]) + square(lab[2])).sqrt();
let mut hue = lab[2].atan2(lab[1]) * 180.0 / PI;
if hue < 0.0 {
hue += 360.0;
}
[lab[0], chroma, hue]
}
fn oklab_to_lms(oklab: [f64; 3]) -> [f64; 3] {
let m = &OKLAB_TO_LMS;
[
cube(m[0] * oklab[0] + m[1] * oklab[1] + m[2] * oklab[2]),
cube(m[3] * oklab[0] + m[4] * oklab[1] + m[5] * oklab[2]),
cube(m[6] * oklab[0] + m[7] * oklab[1] + m[8] * oklab[2]),
]
}
fn lms_to_oklab(lms: [f64; 3]) -> [f64; 3] {
let scale = |t: f64| pow(t.abs(), 0.3333333333333333) * t.signum();
let l = scale(lms[0]);
let m = scale(lms[1]);
let s = scale(lms[2]);
let t = &LMS_TO_OKLAB;
[
t[0] * l + t[1] * m + t[2] * s,
t[3] * l + t[4] * m + t[5] * s,
t[6] * l + t[7] * m + t[8] * s,
]
}
pub(super) fn convert(src: ColorSpace, dest: ColorSpace, ch: [f64; 3]) -> [f64; 3] {
if src == dest {
return ch;
}
match src {
ColorSpace::Hsl => from_srgb_source(hsl_to_srgb(ch), dest),
ColorSpace::Hwb => from_srgb_source(hwb_to_srgb(ch), dest),
ColorSpace::Rgb => from_srgb_source([ch[0] / 255.0, ch[1] / 255.0, ch[2] / 255.0], dest),
ColorSpace::Lab => from_lab_source(ch, dest),
ColorSpace::Lch => {
let lab = lch_to_lab(ch);
if dest == ColorSpace::Lab {
lab
} else {
from_lab_source(lab, dest)
}
}
ColorSpace::Oklab => from_oklab_source(ch, dest),
ColorSpace::Oklch => {
from_oklab_source(lch_to_lab(ch), dest)
}
_ => convert_linear(src, ch, dest),
}
}
fn from_srgb_source(rgb: [f64; 3], dest: ColorSpace) -> [f64; 3] {
match dest {
ColorSpace::Hsl => srgb_to_hsl(rgb),
ColorSpace::Hwb => srgb_to_hwb(rgb),
ColorSpace::Rgb => [rgb[0] * 255.0, rgb[1] * 255.0, rgb[2] * 255.0],
ColorSpace::Srgb => rgb,
ColorSpace::SrgbLinear => [
srgb_to_linear(rgb[0]),
srgb_to_linear(rgb[1]),
srgb_to_linear(rgb[2]),
],
_ => convert_linear(ColorSpace::Srgb, rgb, dest),
}
}
fn from_lab_source(lab: [f64; 3], dest: ColorSpace) -> [f64; 3] {
match dest {
ColorSpace::Lab => lab,
ColorSpace::Lch => lab_to_lch(lab),
_ => convert_linear(ColorSpace::XyzD50, lab_to_xyz_d50(lab), dest),
}
}
fn from_oklab_source(oklab: [f64; 3], dest: ColorSpace) -> [f64; 3] {
match dest {
ColorSpace::Oklch => lab_to_lch(oklab),
_ => convert_linear_from_lms(oklab_to_lms(oklab), dest),
}
}
fn convert_linear_from_lms(lms: [f64; 3], dest: ColorSpace) -> [f64; 3] {
let (through_lin, finalize) = through_of(dest);
let transformed = if through_lin == Lin::Lms {
lms
} else {
let m = matrix(Lin::Lms, through_lin);
let (_, _, from_linear) = lin_of_through(through_lin, dest);
apply(m, lms, from_linear)
};
finalize_dest(transformed, dest, finalize)
}
fn convert_linear(src: ColorSpace, ch: [f64; 3], dest: ColorSpace) -> [f64; 3] {
let (src_lin, to_linear, _) = lin_of(src);
let (through_lin, finalize) = through_of(dest);
let same_space = src_lin == through_lin && through_space_equals_src(src, dest);
let transformed = if same_space {
ch
} else {
let linear = [to_linear(ch[0]), to_linear(ch[1]), to_linear(ch[2])];
let (_, _, from_linear) = lin_of_through(through_lin, dest);
if src_lin == through_lin {
[
from_linear(linear[0]),
from_linear(linear[1]),
from_linear(linear[2]),
]
} else {
apply(matrix(src_lin, through_lin), linear, from_linear)
}
};
finalize_dest(transformed, dest, finalize)
}
#[inline]
fn apply(m: &[f64; 9], v: [f64; 3], from_linear: fn(f64) -> f64) -> [f64; 3] {
[
from_linear(m[0] * v[0] + m[1] * v[1] + m[2] * v[2]),
from_linear(m[3] * v[0] + m[4] * v[1] + m[5] * v[2]),
from_linear(m[6] * v[0] + m[7] * v[1] + m[8] * v[2]),
]
}
#[derive(Clone, Copy, PartialEq)]
enum Finalize {
None,
Hsl,
Hwb,
Rgb255,
Lab,
Lch,
Oklab,
Oklch,
}
fn through_of(dest: ColorSpace) -> (Lin, Finalize) {
match dest {
ColorSpace::Hsl => (Lin::Srgb, Finalize::Hsl),
ColorSpace::Hwb => (Lin::Srgb, Finalize::Hwb),
ColorSpace::Rgb => (Lin::Srgb, Finalize::Rgb255),
ColorSpace::Lab => (Lin::XyzD50, Finalize::Lab),
ColorSpace::Lch => (Lin::XyzD50, Finalize::Lch),
ColorSpace::Oklab => (Lin::Lms, Finalize::Oklab),
ColorSpace::Oklch => (Lin::Lms, Finalize::Oklch),
_ => (lin_of(dest).0, Finalize::None),
}
}
fn through_space_equals_src(src: ColorSpace, dest: ColorSpace) -> bool {
let through_space = match dest {
ColorSpace::Hsl | ColorSpace::Hwb | ColorSpace::Rgb => ColorSpace::Srgb,
ColorSpace::Lab | ColorSpace::Lch => ColorSpace::XyzD50,
ColorSpace::Oklab | ColorSpace::Oklch => return false,
other => other,
};
through_space == src
}
fn lin_of_through(through: Lin, dest: ColorSpace) -> LinTransfer {
fn id(c: f64) -> f64 {
c
}
match through {
Lin::Srgb => match dest {
ColorSpace::SrgbLinear => (Lin::Srgb, id, id),
_ => (Lin::Srgb, srgb_to_linear, srgb_from_linear),
},
Lin::DisplayP3 => match dest {
ColorSpace::DisplayP3Linear => (Lin::DisplayP3, id, id),
_ => (Lin::DisplayP3, srgb_to_linear, srgb_from_linear),
},
Lin::A98 => (Lin::A98, a98_to_linear, a98_from_linear),
Lin::Prophoto => (Lin::Prophoto, prophoto_to_linear, prophoto_from_linear),
Lin::Rec2020 => (Lin::Rec2020, rec2020_to_linear, rec2020_from_linear),
Lin::XyzD65 | Lin::XyzD50 | Lin::Lms => (through, id, id),
}
}
fn finalize_dest(transformed: [f64; 3], _dest: ColorSpace, finalize: Finalize) -> [f64; 3] {
match finalize {
Finalize::None => transformed,
Finalize::Hsl => srgb_to_hsl(transformed),
Finalize::Hwb => srgb_to_hwb(transformed),
Finalize::Rgb255 => [
transformed[0] * 255.0,
transformed[1] * 255.0,
transformed[2] * 255.0,
],
Finalize::Lab => xyz_d50_to_lab(transformed),
Finalize::Lch => lab_to_lch(xyz_d50_to_lab(transformed)),
Finalize::Oklab => lms_to_oklab(transformed),
Finalize::Oklch => lab_to_lch(lms_to_oklab(transformed)),
}
}