use super::ToRgb;
use hayro_syntax::object::Dict;
use hayro_syntax::object::dict::keys::{BLACK_POINT, GAMMA, MATRIX, WHITE_POINT};
#[derive(Debug, Clone)]
pub(crate) struct CalRgb {
white_point: [f32; 3],
black_point: [f32; 3],
matrix: [f32; 9],
gamma: [f32; 3],
}
impl CalRgb {
pub(super) fn new(dict: &Dict<'_>) -> Option<Self> {
let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
let matrix = dict
.get::<[f32; 9]>(MATRIX)
.unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]);
Some(Self {
white_point,
black_point,
matrix,
gamma,
})
}
const BRADFORD_SCALE_MATRIX: [f32; 9] = [
0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296,
];
const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [
0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428,
0.9684867,
];
const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [
3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259,
1.0572252,
];
const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0];
const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883];
fn decode_l_constant() -> f32 {
((8.0_f32 + 16.0) / 116.0).powi(3) / 8.0
}
fn srgb_transfer_function(color: f32) -> f32 {
if color <= 0.0031308 {
(12.92 * color).clamp(0.0, 1.0)
} else if color >= 0.99554525 {
1.0
} else {
((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
}
}
fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] {
[
a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
a[6] * b[0] + a[7] * b[1] + a[8] * b[2],
]
}
fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
[
lms[0] / source_white_point[0],
lms[1] / source_white_point[1],
lms[2] / source_white_point[2],
]
}
fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
[
lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0],
lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1],
lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2],
]
}
fn decode_l(l: f32) -> f32 {
if l < 0.0 {
-Self::decode_l(-l)
} else if l > 8.0 {
((l + 16.0) / 116.0).powi(3)
} else {
l * Self::decode_l_constant()
}
}
fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] {
if source_bp == &[0.0, 0.0, 0.0] {
return *xyz_flat;
}
let zero_decode_l = Self::decode_l(0.0);
let mut out = [0.0; 3];
for i in 0..3 {
let src = Self::decode_l(source_bp[i]);
let scale = (1.0 - zero_decode_l) / (1.0 - src);
let offset = 1.0 - scale;
out[i] = xyz_flat[i] * scale + offset;
}
out
}
fn normalize_white_point_to_flat(
&self,
source_white_point: &[f32; 3],
xyz: &[f32; 3],
) -> [f32; 3] {
if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 {
return *xyz;
}
let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
let lms_flat = Self::to_flat(source_white_point, &lms);
Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
}
fn normalize_white_point_to_d65(
&self,
source_white_point: &[f32; 3],
xyz: &[f32; 3],
) -> [f32; 3] {
let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
let lms_d65 = Self::to_d65(source_white_point, &lms);
Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
}
fn convert_pixel(&self, input: [u8; 3]) -> [u8; 3] {
let input = [
input[0] as f32 / 255.0,
input[1] as f32 / 255.0,
input[2] as f32 / 255.0,
];
let [r, g, b] = input;
let [gr, gg, gb] = self.gamma;
let [agr, bgg, cgb] = [
if r == 1.0 { 1.0 } else { r.powf(gr) },
if g == 1.0 { 1.0 } else { g.powf(gg) },
if b == 1.0 { 1.0 } else { b.powf(gb) },
];
let m = &self.matrix;
let x = m[0] * agr + m[3] * bgg + m[6] * cgb;
let y = m[1] * agr + m[4] * bgg + m[7] * cgb;
let z = m[2] * agr + m[5] * bgg + m[8] * cgb;
let xyz = [x, y, z];
let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
[
(Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8,
(Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8,
(Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8,
]
}
}
impl ToRgb for CalRgb {
fn convert(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) {
output.copy_from_slice(&self.convert_pixel([input[0], input[1], input[2]]));
}
Some(())
}
fn convert_in_place(&self, input: &mut [u8]) -> Option<()> {
for input in input.chunks_exact_mut(3) {
input.copy_from_slice(&self.convert_pixel([input[0], input[1], input[2]]));
}
Some(())
}
}