use moxcms::{ColorProfile, DataColorSpace, Hypercube, Lab, LutStore, LutType, LutWarehouse};
use super::Clut4;
use super::bpc::{WP_D50, apply_bpc_rgb_u8, compute_bpc_params, lab_to_xyz_d50};
const PCS_LAB_DENOM: f32 = 65280.0;
pub(super) fn bake_clut4_perceptual(
profile: &ColorProfile,
grid_n: usize,
bpc_enabled: bool,
) -> Option<Clut4> {
if profile.color_space != DataColorSpace::Cmyk || profile.pcs != DataColorSpace::Lab {
return None;
}
if !(2..=33).contains(&grid_n) {
return None;
}
let colorimetric = SampledLut::from_warehouse(profile.lut_a_to_b_colorimetric.as_ref()?)?;
let bpc = if bpc_enabled {
let lab_k = colorimetric.sample(1.0, 1.0, 1.0, 1.0);
let l_star = (lab_k.l as f64 * 100.0).clamp(0.0, 100.0);
let neutral = [l_star.min(50.0), 0.0, 0.0];
let sbp = lab_to_xyz_d50(neutral);
Some(compute_bpc_params(sbp, [0.0; 3], WP_D50))
} else {
None
};
let bpc = bpc.as_ref();
let total = grid_n
.checked_mul(grid_n)?
.checked_mul(grid_n)?
.checked_mul(grid_n)?;
let mut data = vec![0u8; total * 3];
let denom = (grid_n - 1) as f32;
for k_i in 0..grid_n {
for y_i in 0..grid_n {
for m_i in 0..grid_n {
for c_i in 0..grid_n {
let c = c_i as f32 / denom;
let m = m_i as f32 / denom;
let y = y_i as f32 / denom;
let k = k_i as f32 / denom;
let lab_c = colorimetric.sample(c, m, y, k);
let lin = lab_to_linear_srgb(lab_c);
let mut rgb = encode_linear_srgb(lin);
if let Some(p) = bpc {
rgb = apply_bpc_rgb_u8(rgb, p);
}
let off = (((k_i * grid_n + y_i) * grid_n + m_i) * grid_n + c_i) * 3;
data[off] = rgb[0];
data[off + 1] = rgb[1];
data[off + 2] = rgb[2];
}
}
}
}
Some(Clut4::from_baked(grid_n as u8, data))
}
struct SampledLut<'a> {
input_table: &'a [u16],
output_table: &'a [u16],
n_in_entries: usize,
n_out_entries: usize,
cube_data: Vec<f32>,
cube_grid: usize,
}
impl<'a> SampledLut<'a> {
fn from_warehouse(warehouse: &'a LutWarehouse) -> Option<Self> {
let lut = match warehouse {
LutWarehouse::Lut(l) => l,
LutWarehouse::Multidimensional(_) => return None,
};
if lut.lut_type != LutType::Lut16 {
return None;
}
if lut.num_input_channels != 4 || lut.num_output_channels != 3 {
return None;
}
let input_table = match &lut.input_table {
LutStore::Store16(v) => v.as_slice(),
LutStore::Store8(_) => return None,
};
let output_table = match &lut.output_table {
LutStore::Store16(v) => v.as_slice(),
LutStore::Store8(_) => return None,
};
let clut_table = match &lut.clut_table {
LutStore::Store16(v) => v.as_slice(),
LutStore::Store8(_) => return None,
};
let n_in_entries = lut.num_input_table_entries as usize;
let n_out_entries = lut.num_output_table_entries as usize;
let cube_grid = lut.num_clut_grid_points as usize;
if cube_grid < 2 || n_in_entries < 2 || n_out_entries < 2 {
return None;
}
if input_table.len() < n_in_entries.checked_mul(4)? {
return None;
}
if output_table.len() < n_out_entries.checked_mul(3)? {
return None;
}
let cube_total = cube_grid
.checked_mul(cube_grid)?
.checked_mul(cube_grid)?
.checked_mul(cube_grid)?
.checked_mul(3)?;
if clut_table.len() < cube_total {
return None;
}
let cube_data: Vec<f32> = clut_table[..cube_total]
.iter()
.map(|&v| v as f32 / 65535.0)
.collect();
Some(SampledLut {
input_table,
output_table,
n_in_entries,
n_out_entries,
cube_data,
cube_grid,
})
}
fn sample(&self, c: f32, m: f32, y: f32, k: f32) -> Lab {
let c_in = sample_curve(self.input_table, 0, self.n_in_entries, c);
let m_in = sample_curve(self.input_table, 1, self.n_in_entries, m);
let y_in = sample_curve(self.input_table, 2, self.n_in_entries, y);
let k_in = sample_curve(self.input_table, 3, self.n_in_entries, k);
let hypercube = match Hypercube::new(&self.cube_data, self.cube_grid, 3) {
Ok(h) => h,
Err(_) => return Lab::new(1.0, 0.5, 0.5),
};
let pcs = hypercube.quadlinear_vec3(c_in, m_in, y_in, k_in);
let l_post = sample_curve(self.output_table, 0, self.n_out_entries, pcs.v[0]);
let a_post = sample_curve(self.output_table, 1, self.n_out_entries, pcs.v[1]);
let b_post = sample_curve(self.output_table, 2, self.n_out_entries, pcs.v[2]);
let scale = 65535.0 / PCS_LAB_DENOM;
Lab::new(
(l_post * scale).clamp(0.0, 1.0),
(a_post * scale).clamp(0.0, 1.0),
(b_post * scale).clamp(0.0, 1.0),
)
}
}
#[inline]
fn sample_curve(table: &[u16], ch: usize, n_entries: usize, x: f32) -> f32 {
let base = ch * n_entries;
let scale = (n_entries - 1) as f32;
let pos = x.clamp(0.0, 1.0) * scale;
let i0 = pos.floor() as usize;
let i1 = (i0 + 1).min(n_entries - 1);
let t = pos - i0 as f32;
let v0 = table[base + i0] as f32 / 65535.0;
let v1 = table[base + i1] as f32 / 65535.0;
v0 + (v1 - v0) * t
}
fn lab_to_linear_srgb(lab: Lab) -> [f64; 3] {
let xyz = lab.to_pcs_xyz();
const PCS_UNDO: f64 = 1.0 + 32767.0 / 32768.0;
let xyz = [
xyz.x as f64 * PCS_UNDO,
xyz.y as f64 * PCS_UNDO,
xyz.z as f64 * PCS_UNDO,
];
xyz_d50_to_linear_srgb_d65(xyz)
}
fn encode_linear_srgb(lin: [f64; 3]) -> [u8; 3] {
let r = linear_to_srgb(lin[0].clamp(0.0, 1.0));
let g = linear_to_srgb(lin[1].clamp(0.0, 1.0));
let b = linear_to_srgb(lin[2].clamp(0.0, 1.0));
[
(r * 255.0).round().clamp(0.0, 255.0) as u8,
(g * 255.0).round().clamp(0.0, 255.0) as u8,
(b * 255.0).round().clamp(0.0, 255.0) as u8,
]
}
#[inline]
fn xyz_d50_to_linear_srgb_d65(xyz: [f64; 3]) -> [f64; 3] {
const M: [[f64; 3]; 3] = [
[3.133_856_1, -1.616_866_7, -0.490_614_6],
[-0.978_768_4, 1.916_141_5, 0.033_454_0],
[0.071_945_3, -0.228_991_4, 1.405_242_7],
];
[
M[0][0] * xyz[0] + M[0][1] * xyz[1] + M[0][2] * xyz[2],
M[1][0] * xyz[0] + M[1][1] * xyz[1] + M[1][2] * xyz[2],
M[2][0] * xyz[0] + M[2][1] * xyz[1] + M[2][2] * xyz[2],
]
}
#[inline]
fn linear_to_srgb(v: f64) -> f64 {
if v <= 0.003_130_8 {
12.92 * v
} else {
1.055 * v.powf(1.0 / 2.4) - 0.055
}
}