use image::Rgb32FImage;
use lcms2::{CIExyY, CIExyYTRIPLE, Intent, PixelFormat, Profile, ToneCurve, Transform};
use crate::error::{AgxError, Result};
const D65: CIExyY = CIExyY {
x: 0.3127,
y: 0.3290,
Y: 1.0,
};
fn build_linear_rec2020_profile() -> Profile {
let primaries = CIExyYTRIPLE {
Red: CIExyY {
x: 0.708,
y: 0.292,
Y: 1.0,
},
Green: CIExyY {
x: 0.170,
y: 0.797,
Y: 1.0,
},
Blue: CIExyY {
x: 0.131,
y: 0.046,
Y: 1.0,
},
};
let linear = ToneCurve::new(1.0);
Profile::new_rgb(&D65, &primaries, &[&linear, &linear, &linear])
.expect("lcms2 failed to build the constant linear Rec.2020 destination profile")
}
thread_local! {
static DEST_PROFILE: Profile = build_linear_rec2020_profile();
}
pub(crate) fn convert_to_working_space(buf: &mut Rgb32FImage, icc_bytes: &[u8]) -> Result<()> {
let input = Profile::new_icc(icc_bytes)
.map_err(|_| AgxError::Decode("malformed or unsupported ICC profile".into()))?;
DEST_PROFILE.with(|dest| {
let transform = Transform::new(
&input,
PixelFormat::RGB_FLT,
dest,
PixelFormat::RGB_FLT,
Intent::RelativeColorimetric,
)
.map_err(|_| AgxError::Decode("failed to build ICC transform".into()))?;
let pixels: &mut [[f32; 3]] = bytemuck::cast_slice_mut(buf);
transform.transform_in_place(pixels);
Ok(())
})
}
#[cfg(test)]
pub(crate) fn adobe_rgb_icc() -> Vec<u8> {
let primaries = CIExyYTRIPLE {
Red: CIExyY {
x: 0.6400,
y: 0.3300,
Y: 1.0,
},
Green: CIExyY {
x: 0.2100,
y: 0.7100,
Y: 1.0,
},
Blue: CIExyY {
x: 0.1500,
y: 0.0600,
Y: 1.0,
},
};
let gamma = ToneCurve::new(2.19921875);
Profile::new_rgb(&D65, &primaries, &[&gamma, &gamma, &gamma])
.expect("build adobe rgb profile")
.icc()
.expect("serialize adobe rgb icc")
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, Rgb};
#[test]
fn srgb_profile_matches_builtin_path() {
use crate::color_space::LINEAR_SRGB_TO_LINEAR_REC2020;
use palette::{LinSrgb, Srgb};
let srgb_icc = Profile::new_srgb().icc().expect("srgb icc");
let samples = [[0.5_f32, 0.5, 0.5], [0.8, 0.2, 0.4], [0.1, 0.6, 0.9]];
for s in samples {
let mut buf: Rgb32FImage = ImageBuffer::from_pixel(1, 1, Rgb(s));
convert_to_working_space(&mut buf, &srgb_icc).expect("convert");
let got = buf.get_pixel(0, 0).0;
let lin: LinSrgb<f32> = Srgb::new(s[0], s[1], s[2]).into_linear();
let m = &LINEAR_SRGB_TO_LINEAR_REC2020;
let expected = [
m[0][0] * lin.red + m[0][1] * lin.green + m[0][2] * lin.blue,
m[1][0] * lin.red + m[1][1] * lin.green + m[1][2] * lin.blue,
m[2][0] * lin.red + m[2][1] * lin.green + m[2][2] * lin.blue,
];
for c in 0..3 {
assert!(
(got[c] - expected[c]).abs() < 3e-3,
"channel {c}: got {} expected {}",
got[c],
expected[c]
);
}
}
}
#[test]
fn adobe_rgb_differs_from_srgb_assumption() {
use crate::color_space::LINEAR_SRGB_TO_LINEAR_REC2020;
use palette::{LinSrgb, Srgb};
let icc = adobe_rgb_icc();
let red = [1.0_f32, 0.0, 0.0];
let mut buf: Rgb32FImage = ImageBuffer::from_pixel(1, 1, Rgb(red));
convert_to_working_space(&mut buf, &icc).expect("convert");
let adobe = buf.get_pixel(0, 0).0;
let lin: LinSrgb<f32> = Srgb::new(red[0], red[1], red[2]).into_linear();
let m = &LINEAR_SRGB_TO_LINEAR_REC2020;
let srgb_assumed = [
m[0][0] * lin.red + m[0][1] * lin.green + m[0][2] * lin.blue,
m[1][0] * lin.red + m[1][1] * lin.green + m[1][2] * lin.blue,
m[2][0] * lin.red + m[2][1] * lin.green + m[2][2] * lin.blue,
];
let max_diff = (0..3)
.map(|c| (adobe[c] - srgb_assumed[c]).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff > 0.02,
"Adobe RGB red should differ from sRGB-assumed red; max_diff={max_diff}"
);
for (c, &v) in adobe.iter().enumerate() {
assert!(v.is_finite(), "channel {c} not finite");
}
}
#[test]
fn malformed_profile_returns_err() {
let mut buf: Rgb32FImage = ImageBuffer::from_pixel(2, 2, Rgb([0.5_f32, 0.5, 0.5]));
let result = convert_to_working_space(&mut buf, b"this is not an icc profile");
assert!(result.is_err());
}
}