use super::{ColorSpace, Rgb};
use std::sync::Arc;
const SRGB_PROFILE_LEN: usize = 3144;
const SRGB_TAG_OFFSET: usize = 400;
const SRGB_TAG: &[u8; 17] = b"sRGB IEC61966-2.1";
#[must_use]
pub fn is_valid_icc_components(n: i64) -> bool {
matches!(n, 1 | 3 | 4)
}
pub struct IccProfile {
transform: Option<Arc<moxcms::TransformF32Executor>>,
srgb: bool,
components: usize,
normal: bool,
}
impl std::fmt::Debug for IccProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IccProfile")
.field("supported", &self.transform.is_some())
.field("srgb", &self.srgb)
.field("components", &self.components)
.field("normal", &self.normal)
.finish()
}
}
impl PartialEq for IccProfile {
fn eq(&self, other: &Self) -> bool {
self.srgb == other.srgb
&& self.components == other.components
&& self.normal == other.normal
&& self.transform.is_some() == other.transform.is_some()
}
}
impl IccProfile {
#[must_use]
pub fn load(data: &[u8], expected: usize) -> Self {
if expected == 3 && is_srgb_profile(data) {
return Self {
transform: None,
srgb: true,
components: 3,
normal: true,
};
}
let Ok(profile) = moxcms::ColorProfile::new_from_slice(data) else {
return Self::rejected(expected);
};
let components = channels_of(profile.color_space);
if components != expected || !is_valid_icc_components(components.try_into().unwrap_or(0)) {
return Self::rejected(expected);
}
let srgb = moxcms::ColorProfile::new_srgb();
let options = moxcms::TransformOptions {
rendering_intent: moxcms::RenderingIntent::Perceptual,
..Default::default()
};
let layout = match components {
1 => moxcms::Layout::Gray,
4 => moxcms::Layout::Rgba,
_ => moxcms::Layout::Rgb,
};
let transform = profile
.create_transform_f32(layout, &srgb, moxcms::Layout::Rgb, options)
.ok();
Self {
normal: matches!(
profile.color_space,
moxcms::DataColorSpace::Gray
| moxcms::DataColorSpace::Rgb
| moxcms::DataColorSpace::Cmyk
),
transform,
srgb: false,
components,
}
}
fn rejected(expected: usize) -> Self {
Self {
transform: None,
srgb: false,
components: expected,
normal: false,
}
}
#[must_use]
pub fn is_supported(&self) -> bool {
self.transform.is_some()
}
#[must_use]
pub fn is_srgb(&self) -> bool {
self.srgb
}
#[must_use]
pub fn is_normal(&self) -> bool {
self.srgb || self.normal
}
#[must_use]
pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
let transform = self.transform.as_ref()?;
let mut src = [0f32; 4];
for (slot, v) in src.iter_mut().zip(comps.iter()) {
*slot = v.clamp(0.0, 1.0);
}
let mut dst = [0f32; 3];
transform
.transform(src.get(..self.components)?, &mut dst)
.ok()?;
Some(Rgb {
r: dst[0],
g: dst[1],
b: dst[2],
})
}
}
fn channels_of(space: moxcms::DataColorSpace) -> usize {
match space {
moxcms::DataColorSpace::Gray => 1,
moxcms::DataColorSpace::Cmyk | moxcms::DataColorSpace::Color4 => 4,
moxcms::DataColorSpace::Color2 => 2,
moxcms::DataColorSpace::Color5 => 5,
moxcms::DataColorSpace::Color6 => 6,
moxcms::DataColorSpace::Color7 => 7,
moxcms::DataColorSpace::Color8 => 8,
_ => 3,
}
}
#[must_use]
pub fn is_srgb_profile(data: &[u8]) -> bool {
data.len() == SRGB_PROFILE_LEN
&& data.get(SRGB_TAG_OFFSET..SRGB_TAG_OFFSET + SRGB_TAG.len()) == Some(&SRGB_TAG[..])
}
#[derive(Debug, Clone, PartialEq)]
pub struct IccBased {
pub profile: Arc<IccProfile>,
pub n: u8,
pub base: Option<Box<ColorSpace>>,
pub ranges: Box<[f32]>,
}
impl IccBased {
#[must_use]
pub fn to_rgb(&self, comps: &[f32]) -> Rgb {
if self.profile.is_srgb() {
return Rgb {
r: comps.first().copied().unwrap_or(0.0),
g: comps.get(1).copied().unwrap_or(0.0),
b: comps.get(2).copied().unwrap_or(0.0),
};
}
if let Some(rgb) = self.profile.to_rgb(comps) {
return rgb;
}
if let Some(base) = &self.base {
return base.to_rgb(comps);
}
Rgb {
r: 0.0,
g: 0.0,
b: 0.0,
}
}
#[must_use]
pub fn is_normal(&self) -> bool {
if self.profile.is_srgb() || self.profile.is_supported() {
return self.profile.is_normal();
}
self.base.as_ref().is_some_and(|b| b.is_normal())
}
#[must_use]
pub fn stock_alternate(n: u8) -> Option<ColorSpace> {
Some(match n {
1 => ColorSpace::DeviceGray,
3 => ColorSpace::DeviceRgb,
4 => ColorSpace::DeviceCmyk,
_ => return None,
})
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{IccProfile, is_srgb_profile, is_valid_icc_components};
#[test]
fn only_one_three_and_four_components_are_valid() {
assert!(is_valid_icc_components(1));
assert!(is_valid_icc_components(3));
assert!(is_valid_icc_components(4));
for n in [0, 2, 5, -1, 100] {
assert!(!is_valid_icc_components(n), "{n} should be rejected");
}
}
#[test]
fn srgb_detection_needs_the_exact_length_and_tag() {
let mut data = vec![0u8; 3144];
assert!(!is_srgb_profile(&data));
data[400..417].copy_from_slice(b"sRGB IEC61966-2.1");
assert!(is_srgb_profile(&data));
data.push(0);
assert!(!is_srgb_profile(&data));
let mut data = vec![0u8; 3144];
data[399..416].copy_from_slice(b"sRGB IEC61966-2.1");
assert!(!is_srgb_profile(&data));
}
#[test]
fn the_srgb_special_case_only_applies_to_three_components() {
let mut data = vec![0u8; 3144];
data[400..417].copy_from_slice(b"sRGB IEC61966-2.1");
assert!(IccProfile::load(&data, 3).is_srgb());
let one = IccProfile::load(&data, 1);
assert!(!one.is_srgb());
assert!(!one.is_supported());
}
#[test]
fn garbage_profiles_are_rejected_not_panicked_on() {
for data in [&b""[..], b"\x00", b"not an icc profile at all"] {
let p = IccProfile::load(data, 3);
assert!(!p.is_supported());
assert!(!p.is_srgb());
assert!(p.to_rgb(&[0.5, 0.5, 0.5]).is_none());
}
}
}
#[must_use]
pub fn srgb_profile_bytes() -> Option<Vec<u8>> {
moxcms::ColorProfile::new_srgb().encode().ok()
}
const CGATS_CMYK_PROFILE: &[u8] = include_bytes!("../../assets/CGATS001Compat-v2-micro.icc");
#[must_use]
pub fn cmyk_profile_bytes() -> Option<Vec<u8>> {
let mut profile = moxcms::ColorProfile::new_from_slice(CGATS_CMYK_PROFILE).ok()?;
profile.profile_class = moxcms::ProfileClass::OutputDevice;
profile.encode().ok()
}
#[cfg(test)]
mod output_profile_tests {
use super::{cmyk_profile_bytes, srgb_profile_bytes};
fn header_of(bytes: &[u8]) -> (usize, &[u8], &[u8], &[u8]) {
let field = |at: usize| bytes.get(at..at + 4).expect("an ICC header is 128 bytes");
let declared =
u32::from_be_bytes(field(0).try_into().expect("four bytes are four bytes")) as usize;
(declared, field(12), field(16), field(36))
}
#[test]
fn the_srgb_profile_encodes_as_a_well_formed_icc_profile() {
let bytes = srgb_profile_bytes().expect("the built-in sRGB profile encodes");
assert!(bytes.len() > 128, "an ICC profile is at least a header");
let (declared, class, space, signature) = header_of(&bytes);
assert_eq!(
declared,
bytes.len(),
"the header's size field is the truth"
);
assert_eq!(class, b"mntr", "a display device class");
assert_eq!(space, b"RGB ", "an RGB data colour space");
assert_eq!(signature, b"acsp", "the ICC signature");
assert!(moxcms::ColorProfile::new_from_slice(&bytes).is_ok());
}
#[test]
fn the_cmyk_profile_re_encodes_as_an_output_class_icc_profile() {
let bytes = cmyk_profile_bytes().expect("the vendored CMYK profile re-encodes");
let (declared, class, space, signature) = header_of(&bytes);
assert_eq!(
declared,
bytes.len(),
"the header's size field is the truth"
);
assert_eq!(
class, b"prtr",
"an output device class, which 6.2.3 requires"
);
assert_eq!(space, b"CMYK", "a CMYK data colour space");
assert_eq!(signature, b"acsp", "the ICC signature");
assert!(moxcms::ColorProfile::new_from_slice(&bytes).is_ok());
}
#[test]
fn the_vendored_cmyk_asset_is_a_cmyk_input_profile() {
let profile = moxcms::ColorProfile::new_from_slice(super::CGATS_CMYK_PROFILE)
.expect("the vendored asset parses");
assert_eq!(profile.color_space, moxcms::DataColorSpace::Cmyk);
assert_eq!(profile.profile_class, moxcms::ProfileClass::InputDevice);
}
}