use moxcms::TransformExecutor;
pub const WP_D50: [f64; 3] = [0.96422, 1.0, 0.82521];
const RGB_TO_XYZ_D65: [[f64; 3]; 3] = [
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041],
];
const XYZ_D65_TO_RGB: [[f64; 3]; 3] = [
[3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[0.0556434, -0.2040259, 1.0572252],
];
const D65_TO_D50: [[f64; 3]; 3] = [
[1.0478112, 0.0228866, -0.0501270],
[0.0295424, 0.9904844, -0.0170491],
[-0.0092345, 0.0150436, 0.7521316],
];
const D50_TO_D65: [[f64; 3]; 3] = [
[0.9555766, -0.0230393, 0.0631636],
[-0.0282895, 1.0099416, 0.0210077],
[0.0122982, -0.0204830, 1.3299098],
];
const LAB_DELTA: f64 = 6.0 / 29.0;
#[derive(Clone, Copy, Debug)]
pub struct BpcParams {
pub ax: f64,
pub ay: f64,
pub az: f64,
pub bx: f64,
pub by: f64,
pub bz: f64,
}
impl BpcParams {
#[inline]
pub fn identity() -> Self {
Self {
ax: 1.0,
ay: 1.0,
az: 1.0,
bx: 0.0,
by: 0.0,
bz: 0.0,
}
}
}
pub fn compute_bpc_params(sbp: [f64; 3], dbp: [f64; 3], wp: [f64; 3]) -> BpcParams {
let tx = sbp[0] - wp[0];
let ty = sbp[1] - wp[1];
let tz = sbp[2] - wp[2];
if tx.abs() < 1e-12 || ty.abs() < 1e-12 || tz.abs() < 1e-12 {
return BpcParams::identity();
}
BpcParams {
ax: (dbp[0] - wp[0]) / tx,
ay: (dbp[1] - wp[1]) / ty,
az: (dbp[2] - wp[2]) / tz,
bx: -wp[0] * (dbp[0] - sbp[0]) / tx,
by: -wp[1] * (dbp[1] - sbp[1]) / ty,
bz: -wp[2] * (dbp[2] - sbp[2]) / tz,
}
}
#[inline]
pub fn apply_bpc_xyz_d50(xyz: [f64; 3], p: &BpcParams) -> [f64; 3] {
[
p.ax * xyz[0] + p.bx,
p.ay * xyz[1] + p.by,
p.az * xyz[2] + p.bz,
]
}
pub fn apply_bpc_f64(rgb: [f64; 3], p: &BpcParams) -> [f64; 3] {
let lin = [
srgb_to_linear(rgb[0]),
srgb_to_linear(rgb[1]),
srgb_to_linear(rgb[2]),
];
let xyz_d65 = matmul3(&RGB_TO_XYZ_D65, lin);
let xyz_d50 = matmul3(&D65_TO_D50, xyz_d65);
let shifted = apply_bpc_xyz_d50(xyz_d50, p);
let xyz_d65_out = matmul3(&D50_TO_D65, shifted);
let lin_out = matmul3(&XYZ_D65_TO_RGB, xyz_d65_out);
[
linear_to_srgb(lin_out[0]).clamp(0.0, 1.0),
linear_to_srgb(lin_out[1]).clamp(0.0, 1.0),
linear_to_srgb(lin_out[2]).clamp(0.0, 1.0),
]
}
pub fn apply_bpc_rgb_u8(rgb: [u8; 3], p: &BpcParams) -> [u8; 3] {
let f = apply_bpc_f64(
[
rgb[0] as f64 / 255.0,
rgb[1] as f64 / 255.0,
rgb[2] as f64 / 255.0,
],
p,
);
[
(f[0] * 255.0).round().clamp(0.0, 255.0) as u8,
(f[1] * 255.0).round().clamp(0.0, 255.0) as u8,
(f[2] * 255.0).round().clamp(0.0, 255.0) as u8,
]
}
pub fn detect_source_black_point(transform_8bit: &dyn TransformExecutor<u8>) -> Option<[f64; 3]> {
let src = [255u8; 4];
let mut dst = [0u8; 3];
transform_8bit.transform(&src, &mut dst).ok()?;
let rgb = [
dst[0] as f64 / 255.0,
dst[1] as f64 / 255.0,
dst[2] as f64 / 255.0,
];
let lin = [
srgb_to_linear(rgb[0]),
srgb_to_linear(rgb[1]),
srgb_to_linear(rgb[2]),
];
let xyz_d65 = matmul3(&RGB_TO_XYZ_D65, lin);
let mut xyz_d50 = matmul3(&D65_TO_D50, xyz_d65);
let mut lab = xyz_d50_to_lab(xyz_d50);
lab[1] = 0.0;
lab[2] = 0.0;
if lab[0] > 50.0 {
lab[0] = 50.0;
}
xyz_d50 = lab_to_xyz_d50(lab);
Some(xyz_d50)
}
#[inline]
pub fn srgb_to_linear(c: f64) -> f64 {
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
#[inline]
pub fn linear_to_srgb(c: f64) -> f64 {
if c <= 0.0031308 {
c * 12.92
} else {
1.055 * c.powf(1.0 / 2.4) - 0.055
}
}
pub fn xyz_d50_to_lab(xyz: [f64; 3]) -> [f64; 3] {
let fx = lab_f(xyz[0] / WP_D50[0]);
let fy = lab_f(xyz[1] / WP_D50[1]);
let fz = lab_f(xyz[2] / WP_D50[2]);
[116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)]
}
pub fn lab_to_xyz_d50(lab: [f64; 3]) -> [f64; 3] {
let fy = (lab[0] + 16.0) / 116.0;
let fx = lab[1] / 500.0 + fy;
let fz = fy - lab[2] / 200.0;
[
WP_D50[0] * lab_f_inv(fx),
WP_D50[1] * lab_f_inv(fy),
WP_D50[2] * lab_f_inv(fz),
]
}
#[inline]
fn lab_f(t: f64) -> f64 {
let d3 = LAB_DELTA * LAB_DELTA * LAB_DELTA;
if t > d3 {
t.cbrt()
} else {
t / (3.0 * LAB_DELTA * LAB_DELTA) + 4.0 / 29.0
}
}
#[inline]
fn lab_f_inv(t: f64) -> f64 {
if t > LAB_DELTA {
t * t * t
} else {
3.0 * LAB_DELTA * LAB_DELTA * (t - 4.0 / 29.0)
}
}
#[inline]
fn matmul3(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],
]
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn rgb_approx_eq(a: [f64; 3], b: [f64; 3], tol: f64) -> bool {
approx_eq(a[0], b[0], tol) && approx_eq(a[1], b[1], tol) && approx_eq(a[2], b[2], tol)
}
#[test]
fn srgb_linear_round_trip() {
for v in [0.0, 0.04, 0.1, 0.5, 0.9, 1.0] {
let r = linear_to_srgb(srgb_to_linear(v));
assert!(approx_eq(r, v, 1e-9), "v={v} round={r}");
}
}
#[test]
fn srgb_to_xyz_round_trip() {
for color in [
[1.0, 1.0, 1.0],
[0.5, 0.5, 0.5],
[0.8, 0.2, 0.4],
[0.0, 0.0, 0.0],
] {
let lin = color.map(srgb_to_linear);
let xyz = matmul3(&RGB_TO_XYZ_D65, lin);
let back_lin = matmul3(&XYZ_D65_TO_RGB, xyz);
let back = back_lin.map(linear_to_srgb);
assert!(
rgb_approx_eq(color, back, 1e-6),
"color={color:?} back={back:?}"
);
}
}
#[test]
fn bradford_round_trip() {
for xyz in [[0.95047, 1.0, 1.08883], [0.5, 0.5, 0.5], [0.1, 0.05, 0.2]] {
let d50 = matmul3(&D65_TO_D50, xyz);
let back = matmul3(&D50_TO_D65, d50);
assert!(rgb_approx_eq(xyz, back, 1e-6), "xyz={xyz:?} back={back:?}");
}
}
#[test]
fn lab_xyz_round_trip() {
for xyz in [
WP_D50,
[0.5, 0.5, 0.5],
[0.04, 0.04, 0.04],
[0.18, 0.18, 0.18],
[0.001, 0.001, 0.001],
] {
let lab = xyz_d50_to_lab(xyz);
let back = lab_to_xyz_d50(lab);
assert!(
rgb_approx_eq(xyz, back, 1e-9),
"xyz={xyz:?} lab={lab:?} back={back:?}"
);
}
}
#[test]
fn lab_clamp_caps_lightness() {
let xyz = [0.5, 0.5, 0.4];
let mut lab = xyz_d50_to_lab(xyz);
lab[1] = 0.0;
lab[2] = 0.0;
if lab[0] > 50.0 {
lab[0] = 50.0;
}
assert!(lab[0] <= 50.0);
assert_eq!(lab[1], 0.0);
assert_eq!(lab[2], 0.0);
let back = lab_to_xyz_d50(lab);
assert!(approx_eq(back[1], 0.184, 0.01), "back Y={}", back[1]);
}
#[test]
fn bpc_params_white_invariant() {
let sbp = [0.0072, 0.0067, 0.0064];
let dbp = [0.0, 0.0, 0.0];
let p = compute_bpc_params(sbp, dbp, WP_D50);
let out = apply_bpc_xyz_d50(WP_D50, &p);
assert!(rgb_approx_eq(out, WP_D50, 1e-9), "out={out:?}");
}
#[test]
fn bpc_params_map_sbp_to_dbp() {
let sbp = [0.0072, 0.0067, 0.0064];
let dbp = [0.0, 0.0, 0.0];
let p = compute_bpc_params(sbp, dbp, WP_D50);
let out = apply_bpc_xyz_d50(sbp, &p);
assert!(rgb_approx_eq(out, dbp, 1e-9), "out={out:?}");
}
#[test]
fn bpc_identity_when_sbp_equals_wp() {
let p = compute_bpc_params(WP_D50, [0.0; 3], WP_D50);
let out = apply_bpc_xyz_d50([0.5, 0.5, 0.5], &p);
assert!(rgb_approx_eq(out, [0.5, 0.5, 0.5], 1e-12));
}
#[test]
fn apply_bpc_f64_white_anchored() {
let sbp = [0.0072, 0.0067, 0.0064];
let p = compute_bpc_params(sbp, [0.0; 3], WP_D50);
let out = apply_bpc_f64([1.0, 1.0, 1.0], &p);
assert!(rgb_approx_eq(out, [1.0, 1.0, 1.0], 0.005), "out={out:?}");
}
#[test]
fn apply_bpc_f64_darkens_grays() {
let sbp = [0.012, 0.013, 0.011];
let p = compute_bpc_params(sbp, [0.0; 3], WP_D50);
let original = [0.3, 0.3, 0.3];
let out = apply_bpc_f64(original, &p);
assert!(out[1] < original[1], "expected darker, got {}", out[1]);
}
#[test]
fn apply_bpc_rgb_u8_matches_f64_path() {
let sbp = [0.0072, 0.0067, 0.0064];
let p = compute_bpc_params(sbp, [0.0; 3], WP_D50);
let v = 128.0 / 255.0;
let f = apply_bpc_f64([v, v, v], &p);
let u = apply_bpc_rgb_u8([128, 128, 128], &p);
let f_u = [
(f[0] * 255.0).round() as u8,
(f[1] * 255.0).round() as u8,
(f[2] * 255.0).round() as u8,
];
assert_eq!(f_u, u);
}
}