#[must_use]
pub fn matrix_from_q428(name: &str, src_ty: &str, dst_ty: &str, coefs: [[i32; 3]; 3]) -> String {
format!(
"pub const {name}: ::ph_color::Matrix3<{src_ty}, {dst_ty}> = ::ph_color::Matrix3::from_q428(::ph_color::Q4_28::mat3_from_raw({coefs:?}));\n"
)
}
#[must_use]
pub fn interp_lut<const N: usize>(name: &str, knots: [u16; N], max_err_lsb: u16) -> String {
let err_name = format!("{name}_MAX_ERR_LSB");
format!(
"pub const {err_name}: u16 = {max_err_lsb};\npub const {name}: ::ph_color::InterpLut<{N}> = ::ph_color::InterpLut::from_knots(::ph_color::Q0_16::array_from_raw({knots:?}), {err_name});\n"
)
}
#[must_use]
pub fn srgb_oetf(name: &str, knots: [u16; 257], max_err_lsb: u16) -> String {
let err_name = format!("{name}_MAX_ERR_LSB");
format!(
"pub const {err_name}: u16 = {max_err_lsb};\npub const {name}: ::ph_color::SrgbOetf = ::ph_color::SrgbOetf::from_knots(::ph_color::Q0_16::array_from_raw({knots:?}), {err_name});\n"
)
}
#[must_use]
pub fn oklab_tables() -> String {
let cbrt = crate::oklab::bake_cbrt257();
let mut src = interp_lut("CBRT", cbrt.knots, cbrt.max_err_lsb);
src.push_str(&format!(
"pub const OKLAB_M1: [[::ph_color::Q4_28; 3]; 3] = ::ph_color::Q4_28::mat3_from_raw({:?});\n",
crate::oklab::to_q428_mat(crate::oklab::m1())
));
src.push_str(&format!(
"pub const OKLAB_M2: [[::ph_color::Q4_28; 3]; 3] = ::ph_color::Q4_28::mat3_from_raw({:?});\n",
crate::oklab::to_q428_mat(crate::oklab::m2())
));
src.push_str(&format!(
"pub const OKLAB_M1_INV: [[::ph_color::Q4_28; 3]; 3] = ::ph_color::Q4_28::mat3_from_raw({:?});\n",
crate::oklab::to_q428_mat(crate::oklab::m1_inv())
));
src.push_str(&format!(
"pub const OKLAB_M2_INV: [[::ph_color::Q4_28; 3]; 3] = ::ph_color::Q4_28::mat3_from_raw({:?});\n",
crate::oklab::to_q428_mat(crate::oklab::m2_inv())
));
src.push_str(&format!(
"pub const OKLAB_M2_INV_OFFSET: [::ph_color::Q4_28; 3] = ::ph_color::Q4_28::row_from_raw({:?});\n",
crate::oklab::m2_inv_offset_q428()
));
src
}
#[must_use]
pub fn oklab_module() -> String {
let cbrt = crate::oklab::bake_cbrt257();
let mut src = String::from(
"// @generated by `cargo xtask generate`; do not edit by hand.\n\
// Formulas: crates/color-bake/src/oklab.rs\n\n\
use crate::fixed::{Q0_16, Q4_28};\n\
use crate::interp::InterpLut;\n\n",
);
push_scalar(
&mut src,
"/// Q4.28 encoding of 0.5, used to bias Oklab a/b into UQ0.16.",
"A_B_BIAS_Q428",
"Q4_28",
&format!("Q4_28::from_raw({})", crate::matrix::to_q428(0.5)),
);
push_matrix(
&mut src,
"/// Linear sRGB to LMS, Q4.28.",
"OKLAB_M1",
crate::oklab::to_q428_mat(crate::oklab::m1()),
);
push_matrix(
&mut src,
"/// LMS-prime to Lab, Q4.28.",
"OKLAB_M2",
crate::oklab::to_q428_mat(crate::oklab::m2()),
);
push_matrix(
&mut src,
"/// LMS to linear sRGB, Q4.28.",
"OKLAB_M1_INV",
crate::oklab::to_q428_mat(crate::oklab::m1_inv()),
);
push_matrix(
&mut src,
"/// Lab to LMS-prime, Q4.28; expects offset a/b.",
"OKLAB_M2_INV",
crate::oklab::to_q428_mat(crate::oklab::m2_inv()),
);
push_row(
&mut src,
"/// Affine addend compensating for the stored +0.5 a/b bias.",
"OKLAB_M2_INV_OFFSET",
crate::oklab::m2_inv_offset_q428(),
);
push_scalar(
&mut src,
"/// Maximum absolute cube-root LUT error in UQ0.16 LSBs.",
"CBRT_MAX_ERR_LSB",
"u16",
&cbrt.max_err_lsb.to_string(),
);
src.push_str("/// Segmented cube-root table on the unit interval, 257 knots.\n");
src.push_str("#[rustfmt::skip]\n");
src.push_str(
"pub const CBRT: InterpLut<257> = InterpLut::from_knots(Q0_16::array_from_raw([\n",
);
for knot in cbrt.knots {
src.push_str(&format!(" {knot},\n"));
}
src.push_str("]), CBRT_MAX_ERR_LSB);\n");
src
}
fn push_scalar(src: &mut String, docs: &str, name: &str, ty: &str, value: &str) {
src.push_str(docs);
src.push('\n');
src.push_str(&format!("pub const {name}: {ty} = {value};\n\n"));
}
fn push_matrix(src: &mut String, docs: &str, name: &str, rows: [[i32; 3]; 3]) {
src.push_str(docs);
src.push('\n');
src.push_str("#[rustfmt::skip]\n");
src.push_str(&format!(
"pub const {name}: [[Q4_28; 3]; 3] = Q4_28::mat3_from_raw([\n"
));
for row in rows {
src.push_str(&format!(" [{}, {}, {}],\n", row[0], row[1], row[2]));
}
src.push_str("]);\n\n");
}
fn push_row(src: &mut String, docs: &str, name: &str, row: [i32; 3]) {
src.push_str(docs);
src.push('\n');
src.push_str("#[rustfmt::skip]\n");
src.push_str(&format!(
"pub const {name}: [Q4_28; 3] = Q4_28::row_from_raw([{}, {}, {}]);\n\n",
row[0], row[1], row[2]
));
}
#[must_use]
pub fn srgb_eotf(name: &str, knots: [u16; 257], max_err_lsb: u16) -> String {
let err_name = format!("{name}_MAX_ERR_LSB");
format!(
"pub const {err_name}: u16 = {max_err_lsb};\npub const {name}: ::ph_color::SrgbEotf = ::ph_color::SrgbEotf::from_knots(::ph_color::Q0_16::array_from_raw({knots:?}), {err_name});\n"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Primaries;
use crate::lut::{bake_lut, identity};
use crate::matrix::{identity_q428, rgb_to_rgb};
#[test]
fn emitted_matrix_constructs() {
let coefs = rgb_to_rgb(Primaries::srgb(), Primaries::srgb()).expect("identity");
assert_eq!(coefs, identity_q428());
let src = matrix_from_q428("IDENTITY", "::ph_color::Srgb", "::ph_color::Srgb", coefs);
assert!(src.contains("Matrix3::from_q428"));
let m = ::ph_color::Matrix3::<::ph_color::Srgb, ::ph_color::Srgb>::from_q428(
::ph_color::Q4_28::mat3_from_raw(coefs),
);
let c = ::ph_color::Color::<::ph_color::Srgb, ::ph_color::Linear>::new(
::ph_color::Q0_16::array_from_raw([9, 8, 7]),
);
assert_eq!(m.apply(c).ch, ::ph_color::Q0_16::array_from_raw([9, 8, 7]));
}
#[test]
fn emitted_lut_constructs_with_max_err() {
let baked = bake_lut::<17>(identity);
let src = interp_lut("ID17", baked.knots, baked.max_err_lsb);
assert!(src.contains("InterpLut::from_knots"));
assert!(src.contains("ID17_MAX_ERR_LSB"));
let lut = ::ph_color::InterpLut::<17>::from_knots(
::ph_color::Q0_16::array_from_raw(baked.knots),
baked.max_err_lsb,
);
assert_eq!(lut.max_err_lsb(), baked.max_err_lsb);
assert_eq!(lut.lookup(::ph_color::Q0_16::ZERO), ::ph_color::Q0_16::ZERO);
assert_eq!(lut.lookup(::ph_color::Q0_16::ONE), ::ph_color::Q0_16::ONE);
}
#[test]
fn emitted_srgb_tables_are_typed_wrappers() {
let o = bake_lut::<257>(crate::srgb::oetf);
let e = bake_lut::<257>(crate::srgb::eotf);
let o_src = srgb_oetf("SRGB_OETF", o.knots, o.max_err_lsb);
let e_src = srgb_eotf("SRGB_EOTF", e.knots, e.max_err_lsb);
assert!(o_src.contains("SrgbOetf::from_knots"));
assert!(!o_src.contains("InterpLut<257>"));
assert!(e_src.contains("SrgbEotf::from_knots"));
assert!(!e_src.contains("InterpLut<257>"));
let oetf = ::ph_color::SrgbOetf::from_knots(
::ph_color::Q0_16::array_from_raw(o.knots),
o.max_err_lsb,
);
let eotf = ::ph_color::SrgbEotf::from_knots(
::ph_color::Q0_16::array_from_raw(e.knots),
e.max_err_lsb,
);
assert_eq!(oetf.max_err_lsb(), o.max_err_lsb);
assert_eq!(eotf.max_err_lsb(), e.max_err_lsb);
}
#[test]
fn emitted_oklab_tables_match_shipped_constants() {
let src = oklab_tables();
assert!(src.contains("InterpLut::from_knots"));
assert!(src.contains("CBRT_MAX_ERR_LSB"));
assert!(src.contains(&format!(
"pub const OKLAB_M1: [[::ph_color::Q4_28; 3]; 3] = ::ph_color::Q4_28::mat3_from_raw({:?});",
crate::oklab::to_q428_mat(crate::oklab::m1())
)));
assert!(src.contains(&format!(
"pub const OKLAB_M2_INV_OFFSET: [::ph_color::Q4_28; 3] = ::ph_color::Q4_28::row_from_raw({:?});",
crate::oklab::m2_inv_offset_q428()
)));
let baked = crate::oklab::bake_cbrt257();
let lut = ::ph_color::InterpLut::<257>::from_knots(
::ph_color::Q0_16::array_from_raw(baked.knots),
baked.max_err_lsb,
);
assert_eq!(lut.max_err_lsb(), ::ph_color::CBRT_MAX_ERR_LSB);
assert_eq!(lut.lookup(::ph_color::Q0_16::ZERO), ::ph_color::Q0_16::ZERO);
assert_eq!(lut.lookup(::ph_color::Q0_16::ONE), ::ph_color::Q0_16::ONE);
}
}