pub type Mat3 = [[f32; 3]; 3];
pub const D50: [f32; 3] = [0.9642, 1.0, 0.8249];
pub(crate) fn s15f16(raw: u32) -> f32 {
(raw as i32) as f32 / 65536.0
}
pub(crate) fn u8f8(raw: u16) -> f32 {
raw as f32 / 256.0
}
pub(crate) fn pcs_xyz_scale(v: f32) -> f32 {
v * (65535.0 / 32768.0)
}
pub(crate) fn lab_legacy16(l: f32, a: f32, b: f32) -> [f32; 3] {
[
l * 65535.0 * 100.0 / 65280.0,
a * 65535.0 / 256.0 - 128.0,
b * 65535.0 / 256.0 - 128.0,
]
}
pub(crate) fn lab_v4(l: f32, a: f32, b: f32) -> [f32; 3] {
[l * 100.0, a * 255.0 - 128.0, b * 255.0 - 128.0]
}
const BRADFORD: [[f64; 3]; 3] = [
[0.8951, 0.2664, -0.1614],
[-0.7502, 1.7135, 0.0367],
[0.0389, -0.0685, 1.0296],
];
const D65_XY: (f64, f64) = (0.3127, 0.3290);
const SRGB_XY: [(f64, f64); 3] = [(0.64, 0.33), (0.30, 0.60), (0.15, 0.06)];
fn xyy_to_xyz(x: f64, y: f64) -> [f64; 3] {
[x / y, 1.0, (1.0 - x - y) / y]
}
fn mul64(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut out = [[0.0f64; 3]; 3];
for (row, ar) in out.iter_mut().zip(a) {
for (j, cell) in row.iter_mut().enumerate() {
*cell = (0..3).map(|k| ar[k] * b[k][j]).sum();
}
}
out
}
fn apply64(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[
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],
]
}
fn inv64(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let c = |r: usize, s: usize| -> f64 {
let (r1, r2) = ((r + 1) % 3, (r + 2) % 3);
let (s1, s2) = ((s + 1) % 3, (s + 2) % 3);
m[r1][s1] * m[r2][s2] - m[r1][s2] * m[r2][s1]
};
let det = m[0][0] * c(0, 0) + m[0][1] * c(0, 1) + m[0][2] * c(0, 2);
let mut out = [[0.0f64; 3]; 3];
for (r, row) in out.iter_mut().enumerate() {
for (s, cell) in row.iter_mut().enumerate() {
*cell = c(s, r) / det;
}
}
out
}
fn bradford_to_d65(white: [f32; 3]) -> [[f64; 3]; 3] {
let src = apply64(
&BRADFORD,
[white[0] as f64, white[1] as f64, white[2] as f64],
);
let dst = apply64(&BRADFORD, xyy_to_xyz(D65_XY.0, D65_XY.1));
let scale = [
[dst[0] / src[0], 0.0, 0.0],
[0.0, dst[1] / src[1], 0.0],
[0.0, 0.0, dst[2] / src[2]],
];
mul64(&inv64(&BRADFORD), &mul64(&scale, &BRADFORD))
}
fn srgb_from_xyz_d65() -> [[f64; 3]; 3] {
let p = [
xyy_to_xyz(SRGB_XY[0].0, SRGB_XY[0].1),
xyy_to_xyz(SRGB_XY[1].0, SRGB_XY[1].1),
xyy_to_xyz(SRGB_XY[2].0, SRGB_XY[2].1),
];
let cols = [
[p[0][0], p[1][0], p[2][0]],
[p[0][1], p[1][1], p[2][1]],
[p[0][2], p[1][2], p[2][2]],
];
let s = apply64(&inv64(&cols), xyy_to_xyz(D65_XY.0, D65_XY.1));
let m = [
[cols[0][0] * s[0], cols[0][1] * s[1], cols[0][2] * s[2]],
[cols[1][0] * s[0], cols[1][1] * s[1], cols[1][2] * s[2]],
[cols[2][0] * s[0], cols[2][1] * s[1], cols[2][2] * s[2]],
];
inv64(&m)
}
pub fn xyz_to_linear_srgb(white: [f32; 3]) -> Mat3 {
let m = mul64(&srgb_from_xyz_d65(), &bradford_to_d65(white));
let mut out = [[0.0f32; 3]; 3];
for (row, mr) in out.iter_mut().zip(&m) {
for (cell, v) in row.iter_mut().zip(mr) {
*cell = *v as f32;
}
}
out
}
pub fn mat_mul(a: &Mat3, b: &Mat3) -> Mat3 {
let mut out = [[0.0f32; 3]; 3];
for (row, ar) in out.iter_mut().zip(a) {
for (j, cell) in row.iter_mut().enumerate() {
*cell = (0..3).map(|k| ar[k] * b[k][j]).sum();
}
}
out
}
pub fn mat_apply(m: &Mat3, v: [f32; 3]) -> [f32; 3] {
[
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],
]
}
pub fn srgb_encode(v: f32) -> f32 {
if !v.is_finite() {
return 0.0;
}
let v = v.clamp(0.0, 1.0);
if v <= 0.0031308 {
return 12.92 * v;
}
1.055 * v.powf(1.0 / 2.4) - 0.055
}
pub fn lab_to_xyz(lab: [f32; 3], white: [f32; 3]) -> [f32; 3] {
let finv = |t: f32| -> f32 {
if t > 6.0 / 29.0 {
return t * t * t;
}
(108.0 / 841.0) * (t - 4.0 / 29.0)
};
let fy = (lab[0] + 16.0) / 116.0;
let fx = fy + lab[1] / 500.0;
let fz = fy - lab[2] / 200.0;
[
white[0] * finv(fx),
white[1] * finv(fy),
white[2] * finv(fz),
]
}
#[cfg(test)]
mod tests {
use super::*;
fn near(a: f32, b: f32, tol: f32) -> bool {
(a - b).abs() <= tol
}
#[test]
fn s15fixed16_spec_values() {
assert_eq!(s15f16(0x8000_0000), -32768.0);
assert_eq!(s15f16(0), 0.0);
assert_eq!(s15f16(0x0001_0000), 1.0);
assert!(near(s15f16(0x7FFF_FFFF), 32767.0 + 65535.0 / 65536.0, 1e-3));
assert_eq!(s15f16(0xFFFF_0000), -1.0);
}
#[test]
fn u8fixed8_and_u1fixed15_spec_values() {
assert_eq!(u8f8(0x0000), 0.0);
assert_eq!(u8f8(0x0100), 1.0);
assert_eq!(u8f8(0xFFFF), 255.0 + 255.0 / 256.0);
assert_eq!(u8f8(0x0180), 1.5);
assert_eq!(pcs_xyz_scale(0.0), 0.0);
assert!(near(pcs_xyz_scale(32768.0 / 65535.0), 1.0, 1e-6));
assert!(near(pcs_xyz_scale(1.0), 1.0 + 32767.0 / 32768.0, 1e-4));
}
#[test]
fn legacy_lab16_spec_values() {
let f = |raw: u16| raw as f32 / 65535.0;
let [l, a, b] = lab_legacy16(f(0x0000), f(0x0000), f(0x8000));
assert_eq!(l, 0.0);
assert_eq!(a, -128.0);
assert!(near(b, 0.0, 1e-4));
let [l, a, b] = lab_legacy16(f(0xFF00), f(0xFF00), f(0xFFFF));
assert!(near(l, 100.0, 1e-3));
assert!(near(a, 127.0, 1e-3));
assert!(near(b, 127.0 + 255.0 / 256.0, 1e-3));
let [l, _, _] = lab_legacy16(f(0xFFFF), 0.0, 0.0);
assert!(near(l, 100.0 + 25500.0 / 65280.0, 1e-3));
}
#[test]
fn v4_lab16_spec_values() {
let f = |raw: u16| raw as f32 / 65535.0;
let [l, a, b] = lab_v4(f(0x0000), f(0x0000), f(0x8080));
assert_eq!(l, 0.0);
assert_eq!(a, -128.0);
assert!(near(b, 0.0, 1e-4));
let [l, a, _] = lab_v4(f(0xFFFF), f(0xFFFF), 0.0);
assert_eq!(l, 100.0);
assert_eq!(a, 127.0);
}
#[test]
fn d50_white_adapts_to_srgb_white() {
let m = xyz_to_linear_srgb(D50);
let rgb = mat_apply(&m, D50);
for c in rgb {
assert!(near(c, 1.0, 1e-4), "white -> {rgb:?}");
}
}
#[test]
fn srgb_matrix_maps_primaries_to_axes() {
let d65 = [
(0.3127 / 0.3290) as f32,
1.0,
((1.0 - 0.3127 - 0.3290) / 0.3290) as f32,
];
let m = xyz_to_linear_srgb(d65);
let white = mat_apply(&m, d65);
for c in white {
assert!(near(c, 1.0, 1e-4), "white -> {white:?}");
}
let red_xyz = [0.64 / 0.33, 1.0, (1.0 - 0.64 - 0.33) / 0.33];
let sum: f32 = red_xyz.iter().sum::<f32>();
let rgb = mat_apply(&m, red_xyz);
assert!(rgb[0] > 0.0, "red scales positive");
assert!(near(rgb[1] / sum, 0.0, 1e-4));
assert!(near(rgb[2] / sum, 0.0, 1e-4));
}
#[test]
fn srgb_encode_hand_values() {
assert_eq!(srgb_encode(0.0), 0.0);
assert!(near(srgb_encode(1.0), 1.0, 1e-6));
assert!(near(srgb_encode(0.001), 0.01292, 1e-6));
assert!(near(srgb_encode(0.5), 0.735357, 1e-4));
assert_eq!(srgb_encode(f32::NAN), 0.0);
assert_eq!(srgb_encode(-2.0), 0.0);
}
#[test]
fn lab_to_xyz_hand_values() {
let xyz = lab_to_xyz([100.0, 0.0, 0.0], D50);
for (got, want) in xyz.iter().zip(D50) {
assert!(near(*got, want, 1e-4), "{xyz:?}");
}
let y = lab_to_xyz([50.0, 0.0, 0.0], [1.0, 1.0, 1.0])[1];
let fy = 66.0f32 / 116.0;
assert!(near(y, fy * fy * fy, 1e-5));
let low = lab_to_xyz([4.0, 0.0, 0.0], [1.0, 1.0, 1.0])[1];
assert!(near(
low,
(108.0 / 841.0) * (20.0 / 116.0 - 4.0 / 29.0),
1e-5
));
}
}