use moxcms::{ColorProfile, ToneReprCurve};
use crate::portable_math;
fn pow(base: f32, exponent: f32) -> f32 {
portable_math::powf(f64::from(base), f64::from(exponent)) as f32
}
const fn fixed(v: u32) -> f32 {
v as f32 / 65536.0
}
const SRGB_GAMUT: [[f32; 3]; 3] = [
[fixed(0x6FA2), fixed(0x6299), fixed(0x24A0)],
[fixed(0x38F5), fixed(0xB785), fixed(0x0F84)],
[fixed(0x0390), fixed(0x18DA), fixed(0xB6CF)],
];
const SRGB_TRANSFER: [f32; 7] = [
2.4,
(1.0 / 1.055) as f32,
(0.055 / 1.055) as f32,
(1.0 / 12.92) as f32,
0.04045,
0.0,
0.0,
];
fn parametric(curve: &ToneReprCurve) -> Option<[f32; 7]> {
match curve {
ToneReprCurve::Lut(table) if table.is_empty() => Some([1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
ToneReprCurve::Lut(table) if table.len() == 1 => {
Some([f32::from(table[0]) / 256.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
}
ToneReprCurve::Lut(_) => None,
ToneReprCurve::Parametric(p) => match *p.as_slice() {
[g] => Some([g, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
[g, a, b] => Some([g, a, b, 0.0, -b / a, 0.0, 0.0]),
[g, a, b, c] => Some([g, a, b, 0.0, -b / a, c, c]),
[g, a, b, c, d] => Some([g, a, b, c, d, 0.0, 0.0]),
[g, a, b, c, d, e, f] => Some([g, a, b, c, d, e, f]),
_ => None,
},
}
}
fn eval_table(table: &[u16], x: f32) -> f32 {
let ix = x.clamp(0.0, 1.0) * (table.len() - 1) as f32;
let lo = ix as usize;
let hi = (lo + 1).min(table.len() - 1);
let t = ix - lo as f32;
let (l, h) = (
f32::from(table[lo]) / 65535.0,
f32::from(table[hi]) / 65535.0,
);
l + (h - l) * t
}
fn srgb_inverse(y: f32) -> f32 {
let (g, a, c, d, e) = (
1.0 / 2.4_f32,
pow(1.055, 2.4),
12.92_f32,
0.003_130_8_f32,
-0.055_f32,
);
let magnitude = y.abs();
let value = if magnitude < d {
c * magnitude
} else {
pow(a * magnitude, g) + e
};
value.copysign(y)
}
fn approximately_srgb(curve: &ToneReprCurve) -> bool {
let curve_at: Box<dyn Fn(f32) -> f32> = match curve {
ToneReprCurve::Lut(table) if table.len() > 1 => Box::new(move |x| eval_table(table, x)),
_ => match parametric(curve) {
Some([g, a, b, c, d, e, f]) => Box::new(move |x: f32| {
let m = x.abs();
let v = if m < d {
c * m + f
} else {
pow(a * m + b, g) + e
};
v.copysign(x)
}),
None => return false,
},
};
let n = match curve {
ToneReprCurve::Lut(table) => table.len().max(256),
ToneReprCurve::Parametric(_) => 256,
};
let dx = 1.0 / (n - 1) as f32;
(0..n)
.map(|i| {
let x = i as f32 * dx;
(x - srgb_inverse(curve_at(x))).abs()
})
.fold(0.0_f32, f32::max)
< 1.0 / 512.0
}
pub(super) fn snaps_to_srgb(profile: &ColorProfile) -> bool {
if let Some(cicp) = &profile.cicp {
if cicp.matrix_coefficients as u8 == 0 && cicp.full_range {
return cicp.color_primaries as u8 == 1 && cicp.transfer_characteristics as u8 == 13;
}
}
let columns = [
profile.red_colorant,
profile.green_colorant,
profile.blue_colorant,
];
let matrix_is_srgb = (0..3).all(|row| {
(0..3).all(|column| {
let value = [columns[column].x, columns[column].y, columns[column].z][row] as f32;
(value - SRGB_GAMUT[row][column]).abs() < 0.01
})
});
let (Some(r), Some(g), Some(b)) = (&profile.red_trc, &profile.green_trc, &profile.blue_trc)
else {
return false;
};
let transfer_is_srgb = match (parametric(r), parametric(g), parametric(b)) {
(Some(pr), Some(pg), Some(pb)) if pr == pg && pr == pb => pr
.iter()
.zip(SRGB_TRANSFER)
.all(|(v, s)| (v - s).abs() < 0.001),
_ => [r, g, b].into_iter().all(approximately_srgb),
};
matrix_is_srgb && transfer_is_srgb
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_parametric_srgb_transfer_and_gamut_snap_and_others_do_not() {
let mut profile = ColorProfile::new_srgb();
for trc in [
&mut profile.red_trc,
&mut profile.green_trc,
&mut profile.blue_trc,
] {
*trc = Some(ToneReprCurve::Parametric(SRGB_TRANSFER.to_vec()));
}
assert!(snaps_to_srgb(&profile));
let mut linear = profile.clone();
for trc in [
&mut linear.red_trc,
&mut linear.green_trc,
&mut linear.blue_trc,
] {
*trc = Some(ToneReprCurve::Lut(vec![]));
}
assert!(!snaps_to_srgb(&linear));
let mut shifted = profile.clone();
shifted.red_colorant.x += 0.02;
assert!(!snaps_to_srgb(&shifted));
}
#[test]
fn a_tabulated_srgb_curve_snaps_within_the_round_trip_bound() {
let table: Vec<u16> = (0..1024)
.map(|i| {
let x = i as f64 / 1023.0;
let y = if x < 0.04045 {
x / 12.92
} else {
portable_math::powf((x + 0.055) / 1.055, 2.4)
};
(y * 65535.0).round() as u16
})
.collect();
let mut profile = ColorProfile::new_srgb();
for trc in [
&mut profile.red_trc,
&mut profile.green_trc,
&mut profile.blue_trc,
] {
*trc = Some(ToneReprCurve::Lut(table.clone()));
}
assert!(snaps_to_srgb(&profile));
let gamma: Vec<u16> = (0..1024)
.map(|i| (portable_math::powf(i as f64 / 1023.0, 1.8) * 65535.0).round() as u16)
.collect();
for trc in [
&mut profile.red_trc,
&mut profile.green_trc,
&mut profile.blue_trc,
] {
*trc = Some(ToneReprCurve::Lut(gamma.clone()));
}
assert!(!snaps_to_srgb(&profile));
}
}