#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Primaries {
pub rx: f64,
pub ry: f64,
pub gx: f64,
pub gy: f64,
pub bx: f64,
pub by: f64,
pub wx: f64,
pub wy: f64,
}
impl Primaries {
#[must_use]
pub const fn rec709() -> Self {
Self {
rx: 0.640,
ry: 0.330,
gx: 0.300,
gy: 0.600,
bx: 0.150,
by: 0.060,
wx: 0.3127,
wy: 0.3290, }
}
#[must_use]
pub const fn rec2020() -> Self {
Self {
rx: 0.708,
ry: 0.292,
gx: 0.170,
gy: 0.797,
bx: 0.131,
by: 0.046,
wx: 0.3127,
wy: 0.3290, }
}
#[must_use]
pub const fn dci_p3() -> Self {
Self {
rx: 0.680,
ry: 0.320,
gx: 0.265,
gy: 0.690,
bx: 0.150,
by: 0.060,
wx: 0.3140,
wy: 0.3510, }
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ColorMatrix3x3 {
pub m: [[f64; 3]; 3],
}
impl ColorMatrix3x3 {
#[must_use]
pub const fn identity() -> Self {
Self {
m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
}
}
#[must_use]
pub fn multiply(&self, v: [f64; 3]) -> [f64; 3] {
let m = &self.m;
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
#[must_use]
pub fn compose(&self, other: &ColorMatrix3x3) -> ColorMatrix3x3 {
let a = &self.m;
let b = &other.m;
let mut out = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
out[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
ColorMatrix3x3 { m: out }
}
}
#[must_use]
pub fn delta_e_76(lab1: [f64; 3], lab2: [f64; 3]) -> f64 {
let dl = lab1[0] - lab2[0];
let da = lab1[1] - lab2[1];
let db = lab1[2] - lab2[2];
(dl * dl + da * da + db * db).sqrt()
}
#[must_use]
pub fn xyz_to_lab(xyz: [f64; 3]) -> [f64; 3] {
const XN: f64 = 0.950_47;
const YN: f64 = 1.000_00;
const ZN: f64 = 1.088_83;
let f = |t: f64| -> f64 {
const DELTA: f64 = 6.0 / 29.0;
const DELTA2: f64 = DELTA * DELTA;
const DELTA3: f64 = DELTA * DELTA * DELTA;
if t > DELTA3 {
t.cbrt()
} else {
t / (3.0 * DELTA2) + 4.0 / 29.0
}
};
let fx = f(xyz[0] / XN);
let fy = f(xyz[1] / YN);
let fz = f(xyz[2] / ZN);
let l = 116.0 * fy - 16.0;
let a = 500.0 * (fx - fy);
let b = 200.0 * (fy - fz);
[l, a, b]
}
#[must_use]
pub fn rgb_to_xyz(rgb: [f64; 3], matrix: &ColorMatrix3x3) -> [f64; 3] {
matrix.multiply(rgb)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ColorSpace {
Rec709,
Rec2020,
DciP3,
DciP3D65,
EqualEnergy,
}
impl ColorSpace {
#[must_use]
pub fn primaries(self) -> Primaries {
match self {
Self::Rec709 => Primaries::rec709(),
Self::Rec2020 => Primaries::rec2020(),
Self::DciP3 => Primaries::dci_p3(),
Self::DciP3D65 => Primaries {
rx: 0.680,
ry: 0.320,
gx: 0.265,
gy: 0.690,
bx: 0.150,
by: 0.060,
wx: 0.3127,
wy: 0.3290, },
Self::EqualEnergy => Primaries {
rx: 0.700,
ry: 0.300,
gx: 0.200,
gy: 0.600,
bx: 0.150,
by: 0.060,
wx: 1.0 / 3.0,
wy: 1.0 / 3.0, },
}
}
#[must_use]
pub fn white_point_xyz(self) -> [f64; 3] {
match self {
Self::Rec709 | Self::Rec2020 | Self::DciP3D65 => {
[0.950_47, 1.000_00, 1.088_83] }
Self::DciP3 => {
let x = 0.3140_f64;
let y = 0.3510_f64;
[x / y, 1.0, (1.0 - x - y) / y]
}
Self::EqualEnergy => [1.0, 1.0, 1.0],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rec709_white_point_d65() {
let p = Primaries::rec709();
assert!((p.wx - 0.3127).abs() < 1e-4, "wx={}", p.wx);
assert!((p.wy - 0.3290).abs() < 1e-4, "wy={}", p.wy);
}
#[test]
fn test_rec2020_wider_gamut_red() {
let r709 = Primaries::rec709();
let r2020 = Primaries::rec2020();
assert!(
r2020.rx > r709.rx,
"2020 red x={}, 709 red x={}",
r2020.rx,
r709.rx
);
}
#[test]
fn test_dci_p3_primaries_values() {
let p = Primaries::dci_p3();
assert!((p.rx - 0.680).abs() < 1e-4);
assert!((p.gy - 0.690).abs() < 1e-4);
}
#[test]
fn test_identity_multiply_unchanged() {
let m = ColorMatrix3x3::identity();
let v = [0.2, 0.5, 0.8];
let out = m.multiply(v);
assert!((out[0] - 0.2).abs() < 1e-12);
assert!((out[1] - 0.5).abs() < 1e-12);
assert!((out[2] - 0.8).abs() < 1e-12);
}
#[test]
fn test_multiply_scales_red_channel() {
let mut m = ColorMatrix3x3::identity();
m.m[0][0] = 2.0; let out = m.multiply([1.0, 1.0, 1.0]);
assert!((out[0] - 2.0).abs() < 1e-12);
assert!((out[1] - 1.0).abs() < 1e-12);
assert!((out[2] - 1.0).abs() < 1e-12);
}
#[test]
fn test_compose_identity_with_self() {
let m = ColorMatrix3x3::identity();
let composed = m.compose(&m);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((composed.m[i][j] - expected).abs() < 1e-12);
}
}
}
#[test]
fn test_compose_two_scales() {
let mut a = ColorMatrix3x3::identity();
a.m[0][0] = 2.0;
let mut b = ColorMatrix3x3::identity();
b.m[0][0] = 3.0;
let c = a.compose(&b);
assert!((c.m[0][0] - 6.0).abs() < 1e-12);
}
#[test]
fn test_delta_e_76_identical_colours() {
let de = delta_e_76([50.0, 20.0, -10.0], [50.0, 20.0, -10.0]);
assert!(
de.abs() < 1e-12,
"ΔE should be 0 for identical colours, got {de}"
);
}
#[test]
fn test_delta_e_76_known_distance() {
let de = delta_e_76([0.0, 3.0, 0.0], [0.0, 0.0, 4.0]);
assert!((de - 5.0).abs() < 1e-9, "ΔE={de}");
}
#[test]
fn test_delta_e_76_white_vs_black() {
let de = delta_e_76([100.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert!((de - 100.0).abs() < 1e-9, "ΔE={de}");
}
#[test]
fn test_xyz_to_lab_d65_white_is_l100() {
let lab = xyz_to_lab([0.950_47, 1.000_00, 1.088_83]);
assert!((lab[0] - 100.0).abs() < 0.01, "L*={}", lab[0]);
assert!(lab[1].abs() < 0.01, "a*={}", lab[1]);
assert!(lab[2].abs() < 0.01, "b*={}", lab[2]);
}
#[test]
fn test_xyz_to_lab_black_is_l0() {
let lab = xyz_to_lab([0.0, 0.0, 0.0]);
assert!(
lab[0].abs() < 0.01,
"L* of black should be 0, got {}",
lab[0]
);
}
#[test]
fn test_xyz_to_lab_l_positive_for_nonzero_y() {
let lab = xyz_to_lab([0.2, 0.2, 0.2]);
assert!(lab[0] > 0.0, "L* should be positive for nonzero XYZ");
}
#[test]
fn test_rgb_to_xyz_black_stays_black() {
let m = ColorMatrix3x3::identity();
let xyz = rgb_to_xyz([0.0, 0.0, 0.0], &m);
for c in xyz {
assert!(c.abs() < 1e-12, "XYZ of black should be [0,0,0], got {c}");
}
}
#[test]
fn test_rgb_to_xyz_identity_passes_through() {
let m = ColorMatrix3x3::identity();
let rgb = [0.3, 0.6, 0.9];
let xyz = rgb_to_xyz(rgb, &m);
assert!((xyz[0] - 0.3).abs() < 1e-12);
assert!((xyz[1] - 0.6).abs() < 1e-12);
assert!((xyz[2] - 0.9).abs() < 1e-12);
}
#[test]
fn test_rgb_to_xyz_then_lab_roundtrip_lightness() {
let m = ColorMatrix3x3::identity();
let xyz = rgb_to_xyz([0.0, 1.0, 0.0], &m);
let lab = xyz_to_lab(xyz);
assert!(lab[0] > 90.0, "L* should be high for Y=1, got {}", lab[0]);
}
#[test]
fn test_color_space_rec709_white_point_d65() {
let cs = ColorSpace::Rec709;
let p = cs.primaries();
assert!((p.wx - 0.3127).abs() < 1e-4);
assert!((p.wy - 0.3290).abs() < 1e-4);
}
#[test]
fn test_color_space_rec2020_wider_red_primary() {
let p709 = ColorSpace::Rec709.primaries();
let p2020 = ColorSpace::Rec2020.primaries();
assert!(
p2020.rx > p709.rx,
"Rec.2020 red x should exceed Rec.709: {} vs {}",
p2020.rx,
p709.rx
);
}
#[test]
fn test_color_space_dci_p3_primaries() {
let p = ColorSpace::DciP3.primaries();
assert!((p.rx - 0.680).abs() < 1e-4, "rx={}", p.rx);
assert!((p.gy - 0.690).abs() < 1e-4, "gy={}", p.gy);
}
#[test]
fn test_color_space_white_point_xyz_d65_rec709() {
let wp = ColorSpace::Rec709.white_point_xyz();
assert!((wp[0] - 0.9505).abs() < 0.001, "X={}", wp[0]);
assert!((wp[1] - 1.000).abs() < 0.001, "Y={}", wp[1]);
}
#[test]
fn test_color_space_equal_energy_white_point() {
let wp = ColorSpace::EqualEnergy.white_point_xyz();
assert!((wp[0] - 1.0).abs() < 1e-9);
assert!((wp[1] - 1.0).abs() < 1e-9);
assert!((wp[2] - 1.0).abs() < 1e-9);
}
}