const XYB_OPSIN_ABSORBANCE_MATRIX: [f32; 9] = [
0.30,
0.622,
0.078, 0.23,
0.692,
0.078, 0.243_422_69,
0.204_767_44,
0.551_809_87, ];
const XYB_OPSIN_ABSORBANCE_BIAS: [f32; 3] = [0.003_793_073_3, 0.003_793_073_3, 0.003_793_073_3];
const XYB_NEG_OPSIN_ABSORBANCE_BIAS_CBRT: [f32; 3] = [
-0.155_954_12, -0.155_954_12,
-0.155_954_12,
];
const INV_OPSIN_MATRIX: [f32; 9] = [
11.031_567, -9.866_944, -0.164_623, -3.254_147, 4.418_77, -0.164_623, -3.658_851, 2.712_923,
1.945_928,
];
#[inline]
fn srgb_to_linear_f32(v: f32) -> f32 {
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
#[inline]
fn linear_to_srgb_f32(v: f32) -> f32 {
if v <= 0.003_130_8 {
v * 12.92
} else {
1.055 * v.powf(1.0 / 2.4) - 0.055
}
}
#[inline]
fn srgb_u8_to_linear(v: u8) -> f32 {
srgb_to_linear_f32(f32::from(v) / 255.0)
}
#[inline]
fn linear_to_srgb_u8(v: f32) -> u8 {
(linear_to_srgb_f32(v.clamp(0.0, 1.0)) * 255.0).round() as u8
}
#[inline]
fn mixed_cbrt(v: f32) -> f32 {
if v < 0.0 { -((-v).cbrt()) } else { v.cbrt() }
}
#[inline]
fn mixed_cube(v: f32) -> f32 {
if v < 0.0 { -((-v).powi(3)) } else { v.powi(3) }
}
#[allow(clippy::many_single_char_names)] fn linear_rgb_to_xyb(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let m = &XYB_OPSIN_ABSORBANCE_MATRIX;
let bias = &XYB_OPSIN_ABSORBANCE_BIAS;
let opsin_r = m[0] * r + m[1] * g + m[2] * b + bias[0];
let opsin_g = m[3] * r + m[4] * g + m[5] * b + bias[1];
let opsin_b = m[6] * r + m[7] * g + m[8] * b + bias[2];
let cbrt_r = mixed_cbrt(opsin_r);
let cbrt_g = mixed_cbrt(opsin_g);
let cbrt_b = mixed_cbrt(opsin_b);
let neg_bias = &XYB_NEG_OPSIN_ABSORBANCE_BIAS_CBRT;
let cbrt_r = cbrt_r + neg_bias[0];
let cbrt_g = cbrt_g + neg_bias[1];
let cbrt_b = cbrt_b + neg_bias[2];
let x = 0.5 * (cbrt_r - cbrt_g);
let y = 0.5 * (cbrt_r + cbrt_g);
(x, y, cbrt_b)
}
#[allow(clippy::many_single_char_names)] fn xyb_to_linear_rgb(x: f32, y: f32, b: f32) -> (f32, f32, f32) {
let neg_bias = &XYB_NEG_OPSIN_ABSORBANCE_BIAS_CBRT;
let cbrt_r = y + x;
let cbrt_g = y - x;
let cbrt_b = b;
let cbrt_r = cbrt_r - neg_bias[0];
let cbrt_g = cbrt_g - neg_bias[1];
let cbrt_b = cbrt_b - neg_bias[2];
let opsin_r = mixed_cube(cbrt_r);
let opsin_g = mixed_cube(cbrt_g);
let opsin_b = mixed_cube(cbrt_b);
let bias = &XYB_OPSIN_ABSORBANCE_BIAS;
let opsin_r = opsin_r - bias[0];
let opsin_g = opsin_g - bias[1];
let opsin_b = opsin_b - bias[2];
let inv = &INV_OPSIN_MATRIX;
let r = inv[0] * opsin_r + inv[1] * opsin_g + inv[2] * opsin_b;
let g = inv[3] * opsin_r + inv[4] * opsin_g + inv[5] * opsin_b;
let b_out = inv[6] * opsin_r + inv[7] * opsin_g + inv[8] * opsin_b;
(r, g, b_out)
}
fn srgb_to_xyb(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
let lr = srgb_u8_to_linear(r);
let lg = srgb_u8_to_linear(g);
let lb = srgb_u8_to_linear(b);
linear_rgb_to_xyb(lr, lg, lb)
}
fn xyb_to_srgb(x: f32, y: f32, b: f32) -> (u8, u8, u8) {
let (lr, lg, lb) = xyb_to_linear_rgb(x, y, b);
(
linear_to_srgb_u8(lr),
linear_to_srgb_u8(lg),
linear_to_srgb_u8(lb),
)
}
const X_MIN: f32 = -0.016; const X_MAX: f32 = 0.029; const Y_MIN: f32 = 0.0;
const Y_MAX: f32 = 0.846; const B_MIN: f32 = 0.0;
const B_MAX: f32 = 0.846;
#[inline]
fn quantize_to_u8(value: f32, min: f32, max: f32) -> f32 {
let range = max - min;
let normalized = (value - min) / range;
let quantized = (normalized * 255.0).round().clamp(0.0, 255.0) / 255.0;
quantized * range + min
}
#[must_use]
#[allow(clippy::many_single_char_names)] pub fn xyb_roundtrip(rgb: &[u8], width: usize, height: usize) -> Vec<u8> {
let num_pixels = width * height;
assert_eq!(rgb.len(), num_pixels * 3, "Buffer size mismatch");
let mut result = vec![0u8; num_pixels * 3];
for i in 0..num_pixels {
let r = rgb[i * 3];
let g = rgb[i * 3 + 1];
let b = rgb[i * 3 + 2];
let (x, y, b_xyb) = srgb_to_xyb(r, g, b);
let x_q = quantize_to_u8(x, X_MIN, X_MAX);
let y_q = quantize_to_u8(y, Y_MIN, Y_MAX);
let b_q = quantize_to_u8(b_xyb, B_MIN, B_MAX);
let (r_out, g_out, b_out) = xyb_to_srgb(x_q, y_q, b_q);
result[i * 3] = r_out;
result[i * 3 + 1] = g_out;
result[i * 3 + 2] = b_out;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xyb_roundtrip_preserves_size() {
let rgb: Vec<u8> = (0..64 * 64 * 3).map(|i| (i % 256) as u8).collect();
let result = xyb_roundtrip(&rgb, 64, 64);
assert_eq!(result.len(), rgb.len());
}
#[test]
fn test_xyb_roundtrip_deterministic() {
let rgb: Vec<u8> = (0..32 * 32 * 3).map(|i| ((i * 7) % 256) as u8).collect();
let result1 = xyb_roundtrip(&rgb, 32, 32);
let result2 = xyb_roundtrip(&rgb, 32, 32);
assert_eq!(result1, result2);
}
#[test]
fn test_xyb_roundtrip_has_quantization_loss() {
let mut max_diff = 0i32;
for r in (0..=255u8).step_by(16) {
for g in (0..=255u8).step_by(16) {
for b in (0..=255u8).step_by(16) {
let rgb = vec![r, g, b];
let result = xyb_roundtrip(&rgb, 1, 1);
let dr = (result[0] as i32 - r as i32).abs();
let dg = (result[1] as i32 - g as i32).abs();
let db = (result[2] as i32 - b as i32).abs();
max_diff = max_diff.max(dr).max(dg).max(db);
}
}
}
assert!(
max_diff <= 30,
"Max diff {} exceeds expected bound",
max_diff
);
}
}