use moxcms::{
CmsError, ColorProfile, Cube, DataColorSpace, Hypercube, Lab, LutStore, LutType, LutWarehouse,
RenderingIntent, ToneCurveEvaluator, TransformExecutor, Xyz,
};
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
}
}
enum SourceA2BSampler {
Lut(OwnedLutSampler),
Shaper(ShaperMatrix),
}
impl SourceA2BSampler {
fn new(profile: &ColorProfile, intent: RenderingIntent) -> Option<Self> {
if profile.color_space != DataColorSpace::Rgb {
return None;
}
let primary = match intent {
RenderingIntent::Perceptual => profile.lut_a_to_b_perceptual.as_ref(),
RenderingIntent::RelativeColorimetric | RenderingIntent::AbsoluteColorimetric => {
profile.lut_a_to_b_colorimetric.as_ref()
}
RenderingIntent::Saturation => profile.lut_a_to_b_saturation.as_ref(),
};
let warehouse = primary
.or(profile.lut_a_to_b_perceptual.as_ref())
.or(profile.lut_a_to_b_colorimetric.as_ref())
.or(profile.lut_a_to_b_saturation.as_ref());
if let Some(wh) = warehouse
&& profile.pcs == DataColorSpace::Lab
&& let Some(lut) = OwnedLutSampler::from_warehouse(wh, 3, 3)
{
return Some(SourceA2BSampler::Lut(lut));
}
ShaperMatrix::new(profile).map(SourceA2BSampler::Shaper)
}
fn sample_pcs_lab(&self, r: f32, g: f32, b: f32) -> [f32; 3] {
match self {
SourceA2BSampler::Lut(lut) => lut.sample_rgb_to_pcs_lab_raw(r, g, b),
SourceA2BSampler::Shaper(sm) => sm.sample_pcs_lab(r, g, b),
}
}
}
struct ShaperMatrix {
trc_r: Box<dyn ToneCurveEvaluator + Send + Sync>,
trc_g: Box<dyn ToneCurveEvaluator + Send + Sync>,
trc_b: Box<dyn ToneCurveEvaluator + Send + Sync>,
matrix: [[f64; 3]; 3],
}
const PCS_XYZ_DENOM: f64 = 1.0 + 32767.0 / 32768.0;
impl ShaperMatrix {
fn new(profile: &ColorProfile) -> Option<Self> {
let red_trc = profile.red_trc.as_ref()?;
let green_trc = profile.green_trc.as_ref()?;
let blue_trc = profile.blue_trc.as_ref()?;
let trc_r = red_trc.make_linear_evaluator().ok()?;
let trc_g = green_trc.make_linear_evaluator().ok()?;
let trc_b = blue_trc.make_linear_evaluator().ok()?;
let m = profile.colorant_matrix();
let s = 1.0 / PCS_XYZ_DENOM;
let matrix = [
[m.v[0][0] * s, m.v[0][1] * s, m.v[0][2] * s],
[m.v[1][0] * s, m.v[1][1] * s, m.v[1][2] * s],
[m.v[2][0] * s, m.v[2][1] * s, m.v[2][2] * s],
];
Some(ShaperMatrix {
trc_r,
trc_g,
trc_b,
matrix,
})
}
fn sample_pcs_lab(&self, r: f32, g: f32, b: f32) -> [f32; 3] {
let lin_r = self.trc_r.evaluate_value(r) as f64;
let lin_g = self.trc_g.evaluate_value(g) as f64;
let lin_b = self.trc_b.evaluate_value(b) as f64;
let x = self.matrix[0][0] * lin_r + self.matrix[0][1] * lin_g + self.matrix[0][2] * lin_b;
let y = self.matrix[1][0] * lin_r + self.matrix[1][1] * lin_g + self.matrix[1][2] * lin_b;
let z = self.matrix[2][0] * lin_r + self.matrix[2][1] * lin_g + self.matrix[2][2] * lin_b;
let lab = Lab::from_pcs_xyz(Xyz::new(x as f32, y as f32, z as f32));
let scale = PCS_LAB_DENOM / 65535.0;
[
(lab.l * scale).clamp(0.0, 1.0),
(lab.a * scale).clamp(0.0, 1.0),
(lab.b * scale).clamp(0.0, 1.0),
]
}
}
pub struct LabToCmykSampler {
lut: OwnedLutSampler,
}
impl LabToCmykSampler {
pub(super) fn new(profile: &ColorProfile, intent: RenderingIntent) -> Option<Self> {
if profile.color_space != DataColorSpace::Cmyk || profile.pcs != DataColorSpace::Lab {
return None;
}
let primary = match intent {
RenderingIntent::Perceptual => profile.lut_b_to_a_perceptual.as_ref(),
RenderingIntent::RelativeColorimetric | RenderingIntent::AbsoluteColorimetric => {
profile.lut_b_to_a_colorimetric.as_ref()
}
RenderingIntent::Saturation => profile.lut_b_to_a_saturation.as_ref(),
};
let warehouse = primary
.or(profile.lut_b_to_a_perceptual.as_ref())
.or(profile.lut_b_to_a_colorimetric.as_ref())
.or(profile.lut_b_to_a_saturation.as_ref())?;
let lut = OwnedLutSampler::from_warehouse(warehouse, 3, 4)?;
Some(LabToCmykSampler { lut })
}
pub(super) fn build(
profile: &ColorProfile,
intent: RenderingIntent,
) -> Option<LabToCmykSampler> {
Self::new(profile, intent)
}
fn sample_pcs_lab(&self, pcs_lab: [f32; 3]) -> [f32; 4] {
self.lut.sample_pcs_lab_to_cmyk(pcs_lab)
}
pub fn sample_pdf_lab(&self, l_star: f64, a_star: f64, b_star: f64) -> [f64; 4] {
let l_norm = (l_star / 100.0).clamp(0.0, 1.0) as f32;
let a_norm = ((a_star + 128.0) / 255.0).clamp(0.0, 1.0) as f32;
let b_norm = ((b_star + 128.0) / 255.0).clamp(0.0, 1.0) as f32;
let scale = PCS_LAB_DENOM / 65535.0;
let pcs = [
(l_norm * scale).clamp(0.0, 1.0),
(a_norm * scale).clamp(0.0, 1.0),
(b_norm * scale).clamp(0.0, 1.0),
];
let cmyk = self.sample_pcs_lab(pcs);
[
cmyk[0] as f64,
cmyk[1] as f64,
cmyk[2] as f64,
cmyk[3] as f64,
]
}
}
struct OwnedLutSampler {
input_table: Vec<f32>,
output_table: Vec<f32>,
n_in_entries: usize,
n_out_entries: usize,
cube_data: Vec<f32>,
cube_grid: usize,
}
impl OwnedLutSampler {
fn from_warehouse(
warehouse: &LutWarehouse,
expected_n_in: usize,
expected_n_out: usize,
) -> 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 as usize != expected_n_in
|| lut.num_output_channels as usize != expected_n_out
{
return None;
}
let input_table_u16 = match &lut.input_table {
LutStore::Store16(v) => v.as_slice(),
LutStore::Store8(_) => return None,
};
let output_table_u16 = match &lut.output_table {
LutStore::Store16(v) => v.as_slice(),
LutStore::Store8(_) => return None,
};
let clut_table_u16 = 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;
}
let in_total = n_in_entries.checked_mul(expected_n_in)?;
if input_table_u16.len() < in_total {
return None;
}
let out_total = n_out_entries.checked_mul(expected_n_out)?;
if output_table_u16.len() < out_total {
return None;
}
let mut cube_total: usize = expected_n_out;
for _ in 0..expected_n_in {
cube_total = cube_total.checked_mul(cube_grid)?;
}
if clut_table_u16.len() < cube_total {
return None;
}
let input_table: Vec<f32> = input_table_u16[..in_total]
.iter()
.map(|&v| v as f32 / 65535.0)
.collect();
let output_table: Vec<f32> = output_table_u16[..out_total]
.iter()
.map(|&v| v as f32 / 65535.0)
.collect();
let cube_data: Vec<f32> = clut_table_u16[..cube_total]
.iter()
.map(|&v| v as f32 / 65535.0)
.collect();
Some(OwnedLutSampler {
input_table,
output_table,
n_in_entries,
n_out_entries,
cube_data,
cube_grid,
})
}
fn sample_rgb_to_pcs_lab_raw(&self, r: f32, g: f32, b: f32) -> [f32; 3] {
let r_in = sample_curve_f32(&self.input_table, 0, self.n_in_entries, r);
let g_in = sample_curve_f32(&self.input_table, 1, self.n_in_entries, g);
let b_in = sample_curve_f32(&self.input_table, 2, self.n_in_entries, b);
let cube = match Cube::new(&self.cube_data, self.cube_grid, 3) {
Ok(c) => c,
Err(_) => return [0.0, 0.5, 0.5],
};
let pcs = cube.trilinear_vec3(r_in, g_in, b_in);
let l_post = sample_curve_f32(&self.output_table, 0, self.n_out_entries, pcs.v[0]);
let a_post = sample_curve_f32(&self.output_table, 1, self.n_out_entries, pcs.v[1]);
let b_post = sample_curve_f32(&self.output_table, 2, self.n_out_entries, pcs.v[2]);
[
l_post.clamp(0.0, 1.0),
a_post.clamp(0.0, 1.0),
b_post.clamp(0.0, 1.0),
]
}
fn sample_pcs_lab_to_cmyk(&self, pcs_lab: [f32; 3]) -> [f32; 4] {
let l_in = sample_curve_f32(
&self.input_table,
0,
self.n_in_entries,
pcs_lab[0].clamp(0.0, 1.0),
);
let a_in = sample_curve_f32(
&self.input_table,
1,
self.n_in_entries,
pcs_lab[1].clamp(0.0, 1.0),
);
let b_in = sample_curve_f32(
&self.input_table,
2,
self.n_in_entries,
pcs_lab[2].clamp(0.0, 1.0),
);
let cube = match Cube::new(&self.cube_data, self.cube_grid, 4) {
Ok(c) => c,
Err(_) => return [0.0; 4],
};
let pcs = cube.trilinear_vec4(l_in, a_in, b_in);
let c = sample_curve_f32(&self.output_table, 0, self.n_out_entries, pcs.v[0]);
let m = sample_curve_f32(&self.output_table, 1, self.n_out_entries, pcs.v[1]);
let y = sample_curve_f32(&self.output_table, 2, self.n_out_entries, pcs.v[2]);
let k = sample_curve_f32(&self.output_table, 3, self.n_out_entries, pcs.v[3]);
[
c.clamp(0.0, 1.0),
m.clamp(0.0, 1.0),
y.clamp(0.0, 1.0),
k.clamp(0.0, 1.0),
]
}
}
#[inline]
fn sample_curve_f32(table: &[f32], 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];
let v1 = table[base + i1];
v0 + (v1 - v0) * t
}
pub(super) struct HandRolledChainStage1Rgb {
src: SourceA2BSampler,
oi: LabToCmykSampler,
}
impl HandRolledChainStage1Rgb {
pub(super) fn new(
source: &ColorProfile,
output_intent: &ColorProfile,
intent: RenderingIntent,
) -> Option<Self> {
if source.color_space != DataColorSpace::Rgb {
return None;
}
let src = SourceA2BSampler::new(source, intent)?;
let oi = LabToCmykSampler::new(output_intent, intent)?;
Some(Self { src, oi })
}
#[inline]
fn sample(&self, r: f32, g: f32, b: f32) -> [f32; 4] {
let pcs = self.src.sample_pcs_lab(r, g, b);
self.oi.sample_pcs_lab(pcs)
}
pub(super) fn sample_cmyk_f64(&self, r: f64, g: f64, b: f64) -> [f64; 4] {
let cmyk = self.sample(r as f32, g as f32, b as f32);
[
cmyk[0] as f64,
cmyk[1] as f64,
cmyk[2] as f64,
cmyk[3] as f64,
]
}
}
impl TransformExecutor<u8> for HandRolledChainStage1Rgb {
fn transform(&self, src: &[u8], dst: &mut [u8]) -> Result<(), CmsError> {
let pixel_count = dst.len() / 4;
for px in 0..pixel_count {
let r = src[px * 3] as f32 / 255.0;
let g = src[px * 3 + 1] as f32 / 255.0;
let b = src[px * 3 + 2] as f32 / 255.0;
let cmyk = self.sample(r, g, b);
dst[px * 4] = (cmyk[0] * 255.0).round().clamp(0.0, 255.0) as u8;
dst[px * 4 + 1] = (cmyk[1] * 255.0).round().clamp(0.0, 255.0) as u8;
dst[px * 4 + 2] = (cmyk[2] * 255.0).round().clamp(0.0, 255.0) as u8;
dst[px * 4 + 3] = (cmyk[3] * 255.0).round().clamp(0.0, 255.0) as u8;
}
Ok(())
}
}
impl TransformExecutor<f64> for HandRolledChainStage1Rgb {
fn transform(&self, src: &[f64], dst: &mut [f64]) -> Result<(), CmsError> {
let pixel_count = dst.len() / 4;
for px in 0..pixel_count {
let r = src[px * 3] as f32;
let g = src[px * 3 + 1] as f32;
let b = src[px * 3 + 2] as f32;
let cmyk = self.sample(r, g, b);
dst[px * 4] = cmyk[0] as f64;
dst[px * 4 + 1] = cmyk[1] as f64;
dst[px * 4 + 2] = cmyk[2] as f64;
dst[px * 4 + 3] = cmyk[3] as f64;
}
Ok(())
}
}