use crate::{Cicp, ColorPrimaries, TransferFunction};
const ICC_MIN_SIZE: usize = 132;
const ICC_SIGNATURE_OFFSET: usize = 36;
const ICC_TAG_COUNT_OFFSET: usize = 128;
const ICC_TAG_TABLE_OFFSET: usize = 132;
const ICC_TAG_ENTRY_SIZE: usize = 12;
const ICC_MAX_TAG_COUNT: usize = 200;
const ICC_CMM_TYPE: core::ops::Range<usize> = 4..8;
const ICC_DATE_TIME: core::ops::Range<usize> = 24..36;
const ICC_PLATFORM: core::ops::Range<usize> = 40..44;
const ICC_DEVICE: core::ops::Range<usize> = 48..56;
const ICC_CREATOR_ID: core::ops::Range<usize> = 80..100;
const ICC_HEADER_NORMALIZE_END: usize = 100;
pub fn extract_cicp(data: &[u8]) -> Option<Cicp> {
if data.len() < ICC_MIN_SIZE {
return None;
}
if data.get(ICC_SIGNATURE_OFFSET..ICC_SIGNATURE_OFFSET + 4)? != b"acsp" {
return None;
}
let tag_count = u32::from_be_bytes(
data[ICC_TAG_COUNT_OFFSET..ICC_TAG_COUNT_OFFSET + 4]
.try_into()
.ok()?,
) as usize;
let tag_count = tag_count.min(ICC_MAX_TAG_COUNT);
for i in 0..tag_count {
let entry_offset = ICC_TAG_TABLE_OFFSET + i * ICC_TAG_ENTRY_SIZE;
let entry = data.get(entry_offset..entry_offset + ICC_TAG_ENTRY_SIZE)?;
if entry[..4] != *b"cicp" {
continue;
}
let data_offset = u32::from_be_bytes(entry[4..8].try_into().ok()?) as usize;
let data_size = u32::from_be_bytes(entry[8..12].try_into().ok()?) as usize;
if data_size < 12 {
return None;
}
let tag_data = data.get(data_offset..data_offset + 12)?;
if tag_data[..4] != *b"cicp" {
return None;
}
return Some(Cicp::new(
tag_data[8],
tag_data[9],
tag_data[10],
tag_data[11] != 0,
));
}
None
}
#[allow(dead_code)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub(crate) struct ProfileFeatures {
pub(crate) pcs_is_lab: bool,
pub(crate) has_chad: bool,
pub(crate) chad_is_bradford: bool,
pub(crate) has_a2b0: bool,
pub(crate) has_a2b1: bool,
pub(crate) has_a2b2: bool,
pub(crate) has_b2a0: bool,
pub(crate) has_b2a1: bool,
pub(crate) has_b2a2: bool,
pub(crate) has_matrix_shaper: bool,
}
impl ProfileFeatures {
#[inline]
#[allow(dead_code)] pub(crate) fn is_safe_matrix_shaper(&self) -> bool {
self.has_matrix_shaper
&& !self.pcs_is_lab
&& !self.has_a2b0
&& !self.has_a2b1
&& !self.has_a2b2
&& !self.has_b2a0
&& !self.has_b2a1
&& !self.has_b2a2
&& (!self.has_chad || self.chad_is_bradford)
}
}
#[allow(dead_code)] const BRADFORD_CHAD_D65_TO_D50: [f64; 9] = [
1.0478, 0.0229, -0.0501, 0.0295, 0.9905, -0.0171, -0.0092, 0.0151, 0.7517,
];
#[allow(dead_code)] const CHAD_TOL: f64 = 0.005;
#[allow(dead_code)] pub(crate) fn inspect_profile(data: &[u8]) -> Option<ProfileFeatures> {
if data.len() < ICC_MIN_SIZE {
return None;
}
if data.get(ICC_SIGNATURE_OFFSET..ICC_SIGNATURE_OFFSET + 4)? != b"acsp" {
return None;
}
let mut feat = ProfileFeatures {
pcs_is_lab: data.get(20..24)? == b"Lab ",
..ProfileFeatures::default()
};
let tag_count = u32::from_be_bytes(
data[ICC_TAG_COUNT_OFFSET..ICC_TAG_COUNT_OFFSET + 4]
.try_into()
.ok()?,
) as usize;
let tag_count = tag_count.min(ICC_MAX_TAG_COUNT);
let mut has_rxyz = false;
let mut has_gxyz = false;
let mut has_bxyz = false;
let mut has_rtrc = false;
let mut has_gtrc = false;
let mut has_btrc = false;
let mut chad_off = None;
for i in 0..tag_count {
let entry_offset = ICC_TAG_TABLE_OFFSET + i * ICC_TAG_ENTRY_SIZE;
let entry = data.get(entry_offset..entry_offset + ICC_TAG_ENTRY_SIZE)?;
let sig = &entry[..4];
let d_off = u32::from_be_bytes(entry[4..8].try_into().ok()?) as usize;
match sig {
b"rXYZ" => has_rxyz = true,
b"gXYZ" => has_gxyz = true,
b"bXYZ" => has_bxyz = true,
b"rTRC" => has_rtrc = true,
b"gTRC" => has_gtrc = true,
b"bTRC" => has_btrc = true,
b"A2B0" => feat.has_a2b0 = true,
b"A2B1" => feat.has_a2b1 = true,
b"A2B2" => feat.has_a2b2 = true,
b"B2A0" => feat.has_b2a0 = true,
b"B2A1" => feat.has_b2a1 = true,
b"B2A2" => feat.has_b2a2 = true,
b"chad" => chad_off = Some(d_off),
_ => {}
}
}
feat.has_matrix_shaper = has_rxyz && has_gxyz && has_bxyz && has_rtrc && has_gtrc && has_btrc;
if let Some(off) = chad_off {
feat.has_chad = true;
if data.get(off..off + 8)? == b"sf32\0\0\0\0" && data.len() >= off + 8 + 36 {
let mut m = [0.0f64; 9];
let mut ok = true;
for (i, slot) in m.iter_mut().enumerate() {
let o = off + 8 + i * 4;
if let Ok(bytes) = data[o..o + 4].try_into() {
*slot = i32::from_be_bytes(bytes) as f64 / 65536.0;
} else {
ok = false;
break;
}
}
if ok {
let mut max_diff = 0.0f64;
for (mi, bi) in m.iter().zip(BRADFORD_CHAD_D65_TO_D50.iter()) {
let d = (mi - bi).abs();
if d > max_diff {
max_diff = d;
}
}
feat.chad_is_bradford = max_diff < CHAD_TOL;
}
}
}
Some(feat)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct IccIdentification {
pub primaries: ColorPrimaries,
pub transfer: TransferFunction,
pub valid_use: IdentificationUse,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IdentificationUse {
MetadataOnly,
MatrixTrcSubstitution,
}
impl IccIdentification {
#[inline]
pub(crate) fn new(
primaries: ColorPrimaries,
transfer: TransferFunction,
valid_use: IdentificationUse,
) -> Self {
Self {
primaries,
transfer,
valid_use,
}
}
#[inline]
pub fn to_cicp(&self) -> Option<crate::Cicp> {
let cp = self.primaries.to_cicp()?;
let tc = self.transfer.to_cicp()?;
Some(crate::Cicp::new(cp, tc, 0, true))
}
#[inline]
pub fn is_srgb(&self) -> bool {
matches!(self.primaries, ColorPrimaries::Bt709)
&& matches!(self.transfer, TransferFunction::Srgb)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[must_use]
#[allow(dead_code)] pub(crate) enum Tolerance {
Exact = 1,
Precise = 3,
Approximate = 13,
Intent = 56,
}
pub(crate) const INTENT_COLORIMETRIC_SAFE: u8 = 1 << 0;
pub(crate) const INTENT_PERCEPTUAL_SAFE: u8 = 1 << 1;
pub(crate) const INTENT_SATURATION_SAFE: u8 = 1 << 2;
#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub(crate) enum CoalesceForUse {
AnyIntent,
RelativeColorimetric,
AbsoluteColorimetric,
Perceptual,
Saturation,
}
impl CoalesceForUse {
#[inline]
const fn required_mask(self) -> u8 {
match self {
Self::AnyIntent => {
INTENT_COLORIMETRIC_SAFE | INTENT_PERCEPTUAL_SAFE | INTENT_SATURATION_SAFE
}
Self::RelativeColorimetric | Self::AbsoluteColorimetric => INTENT_COLORIMETRIC_SAFE,
Self::Perceptual => INTENT_PERCEPTUAL_SAFE,
Self::Saturation => INTENT_SATURATION_SAFE,
}
}
}
pub fn identify_common(icc_bytes: &[u8]) -> Option<IccIdentification> {
identify_common_at(icc_bytes, Tolerance::Intent)
}
fn identify_common_at(icc_bytes: &[u8], tolerance: Tolerance) -> Option<IccIdentification> {
let hash = fnv1a_64_normalized(icc_bytes);
if let Ok(idx) = KNOWN_RGB_PROFILES.binary_search_by_key(&hash, |e| e.0) {
let entry = &KNOWN_RGB_PROFILES[idx];
if entry.3 <= tolerance as u8 {
return Some(IccIdentification::new(
entry.1,
entry.2,
use_from_mask(entry.4),
));
}
}
if let Ok(idx) = KNOWN_GRAY_PROFILES.binary_search_by_key(&hash, |e| e.0) {
let entry = &KNOWN_GRAY_PROFILES[idx];
if entry.2 <= tolerance as u8 {
return Some(IccIdentification::new(
ColorPrimaries::Bt709,
entry.1,
use_from_mask(entry.3),
));
}
}
None
}
#[inline]
fn use_from_mask(mask: u8) -> IdentificationUse {
const ALL: u8 = INTENT_COLORIMETRIC_SAFE | INTENT_PERCEPTUAL_SAFE | INTENT_SATURATION_SAFE;
if mask == ALL {
IdentificationUse::MatrixTrcSubstitution
} else {
IdentificationUse::MetadataOnly
}
}
#[allow(dead_code)] pub(crate) fn identify_common_for(
icc_bytes: &[u8],
tolerance: Tolerance,
use_for: CoalesceForUse,
) -> Option<IccIdentification> {
let hash = fnv1a_64_normalized(icc_bytes);
let required = use_for.required_mask();
if let Ok(idx) = KNOWN_RGB_PROFILES.binary_search_by_key(&hash, |e| e.0) {
let entry = &KNOWN_RGB_PROFILES[idx];
if entry.3 <= tolerance as u8 && (entry.4 & required) == required {
return Some(IccIdentification::new(
entry.1,
entry.2,
IdentificationUse::MatrixTrcSubstitution,
));
}
}
if let Ok(idx) = KNOWN_GRAY_PROFILES.binary_search_by_key(&hash, |e| e.0) {
let entry = &KNOWN_GRAY_PROFILES[idx];
if entry.2 <= tolerance as u8 && (entry.3 & required) == required {
return Some(IccIdentification::new(
ColorPrimaries::Bt709,
entry.1,
IdentificationUse::MatrixTrcSubstitution,
));
}
}
None
}
#[inline]
pub fn is_common_srgb(icc_bytes: &[u8]) -> bool {
identify_common(icc_bytes).is_some_and(|id| id.is_srgb())
}
fn fnv1a_64_normalized(data: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
fn is_metadata_field(i: usize) -> bool {
ICC_CMM_TYPE.contains(&i)
|| ICC_DATE_TIME.contains(&i)
|| ICC_PLATFORM.contains(&i)
|| ICC_DEVICE.contains(&i)
|| ICC_CREATOR_ID.contains(&i)
}
let header_len = data.len().min(ICC_HEADER_NORMALIZE_END);
let mut i = 0;
while i < header_len {
let b = if is_metadata_field(i) { 0u8 } else { data[i] };
hash ^= b as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
while i < data.len() {
hash ^= data[i] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
hash
}
use ColorPrimaries as CP;
use TransferFunction as TF;
#[allow(non_upper_case_globals)]
struct Safe;
#[allow(non_upper_case_globals, dead_code)]
impl Safe {
const AnyIntent: u8 =
INTENT_COLORIMETRIC_SAFE | INTENT_PERCEPTUAL_SAFE | INTENT_SATURATION_SAFE;
const IdOnly: u8 = 0;
const Colorimetric: u8 = INTENT_COLORIMETRIC_SAFE;
const Perceptual: u8 = INTENT_PERCEPTUAL_SAFE;
const Saturation: u8 = INTENT_SATURATION_SAFE;
const ColorimetricPerceptual: u8 = INTENT_COLORIMETRIC_SAFE | INTENT_PERCEPTUAL_SAFE;
const ColorimetricSaturation: u8 = INTENT_COLORIMETRIC_SAFE | INTENT_SATURATION_SAFE;
const PerceptualSaturation: u8 = INTENT_PERCEPTUAL_SAFE | INTENT_SATURATION_SAFE;
}
#[rustfmt::skip]
const KNOWN_RGB_PROFILES: &[(u64, CP, TF, u8, u8)] =
include!("icc_table_rgb.inc");
#[rustfmt::skip]
const KNOWN_GRAY_PROFILES: &[(u64, TF, u8, u8)] =
include!("icc_table_gray.inc");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_table_sorted() {
for i in 1..KNOWN_RGB_PROFILES.len() {
assert!(
KNOWN_RGB_PROFILES[i - 1].0 < KNOWN_RGB_PROFILES[i].0,
"KNOWN_RGB_PROFILES not sorted at index {i}: 0x{:016x} >= 0x{:016x}",
KNOWN_RGB_PROFILES[i - 1].0,
KNOWN_RGB_PROFILES[i].0,
);
}
}
#[test]
fn gray_table_sorted() {
for i in 1..KNOWN_GRAY_PROFILES.len() {
assert!(
KNOWN_GRAY_PROFILES[i - 1].0 < KNOWN_GRAY_PROFILES[i].0,
"KNOWN_GRAY_PROFILES not sorted at index {i}: 0x{:016x} >= 0x{:016x}",
KNOWN_GRAY_PROFILES[i - 1].0,
KNOWN_GRAY_PROFILES[i].0,
);
}
}
#[test]
fn no_duplicate_hashes() {
let mut seen = alloc::collections::BTreeSet::new();
for entry in KNOWN_RGB_PROFILES {
assert!(
seen.insert(entry.0),
"duplicate RGB hash 0x{:016x}",
entry.0
);
}
for entry in KNOWN_GRAY_PROFILES {
assert!(
seen.insert(entry.0),
"duplicate gray hash 0x{:016x}",
entry.0
);
}
}
#[test]
fn all_errors_within_intent() {
for &(h, _, _, err, _) in KNOWN_RGB_PROFILES {
assert!(err <= 56, "RGB 0x{h:016x} err={err} > 56");
}
for &(h, _, err, _) in KNOWN_GRAY_PROFILES {
assert!(err <= 56, "gray 0x{h:016x} err={err} > 56");
}
}
#[test]
fn no_unknown_variants_in_tables() {
for &(h, cp, tc, _, _) in KNOWN_RGB_PROFILES {
assert_ne!(cp, ColorPrimaries::Unknown, "RGB 0x{h:016x}");
assert_ne!(tc, TransferFunction::Unknown, "RGB 0x{h:016x}");
}
for &(h, tc, _, _) in KNOWN_GRAY_PROFILES {
assert_ne!(tc, TransferFunction::Unknown, "gray 0x{h:016x}");
}
}
#[test]
fn intent_mask_reserved_bits_zero() {
const ALL_DEFINED: u8 =
INTENT_COLORIMETRIC_SAFE | INTENT_PERCEPTUAL_SAFE | INTENT_SATURATION_SAFE;
for &(h, _, _, _, mask) in KNOWN_RGB_PROFILES {
assert_eq!(
mask & !ALL_DEFINED,
0,
"RGB 0x{h:016x} has reserved bits set: 0x{mask:02x}"
);
}
for &(h, _, _, mask) in KNOWN_GRAY_PROFILES {
assert_eq!(
mask & !ALL_DEFINED,
0,
"gray 0x{h:016x} has reserved bits set: 0x{mask:02x}"
);
}
}
#[test]
fn table_coverage() {
let rgb_count = |cp: ColorPrimaries, tc: TransferFunction| {
KNOWN_RGB_PROFILES
.iter()
.filter(|e| e.1 == cp && e.2 == tc)
.count()
};
assert!(
rgb_count(CP::Bt709, TF::Srgb) >= 25,
"sRGB: {}",
rgb_count(CP::Bt709, TF::Srgb)
);
assert!(
rgb_count(CP::DisplayP3, TF::Srgb) >= 25,
"Display P3: {}",
rgb_count(CP::DisplayP3, TF::Srgb)
);
assert!(
rgb_count(CP::AdobeRgb, TF::Gamma22) >= 15,
"Adobe RGB: {}",
rgb_count(CP::AdobeRgb, TF::Gamma22)
);
assert!(
KNOWN_RGB_PROFILES.len() >= 90,
"RGB total: {}",
KNOWN_RGB_PROFILES.len()
);
assert!(
KNOWN_GRAY_PROFILES.len() >= 10,
"Gray total: {}",
KNOWN_GRAY_PROFILES.len()
);
}
#[test]
fn zero_filled_no_false_positive() {
for len in [410, 456, 480, 524, 548, 656, 736, 3024, 3144] {
let zeros = alloc::vec![0u8; len];
assert!(
identify_common(&zeros).is_none(),
"zeros({len}) falsely matched"
);
}
}
#[test]
fn hash_deterministic() {
let data = b"test data for hashing";
assert_eq!(fnv1a_64_normalized(data), fnv1a_64_normalized(data));
}
#[test]
fn hash_distinct() {
assert_ne!(fnv1a_64_normalized(b"abc"), fnv1a_64_normalized(b"abd"));
}
#[test]
fn normalization_zeroes_metadata() {
let mut a = alloc::vec![0u8; 200];
let mut b = a.clone();
a[30] = 0xFF; b[30] = 0x01;
assert_eq!(fnv1a_64_normalized(&a), fnv1a_64_normalized(&b));
let mut c = a.clone();
c[20] = 0xFF;
assert_ne!(fnv1a_64_normalized(&a), fnv1a_64_normalized(&c));
}
#[test]
fn is_common_srgb_rejects_empty() {
assert!(!is_common_srgb(&[]));
assert!(!is_common_srgb(&[0; 100]));
}
fn build_icc_with_cicp(cp: u8, tc: u8, mc: u8, fr: bool) -> alloc::vec::Vec<u8> {
let mut data = alloc::vec![0u8; 256];
data[0..4].copy_from_slice(&256u32.to_be_bytes());
data[36..40].copy_from_slice(b"acsp");
data[128..132].copy_from_slice(&1u32.to_be_bytes());
data[132..136].copy_from_slice(b"cicp");
data[136..140].copy_from_slice(&144u32.to_be_bytes());
data[140..144].copy_from_slice(&12u32.to_be_bytes());
data[144..148].copy_from_slice(b"cicp");
data[152] = cp;
data[153] = tc;
data[154] = mc;
data[155] = u8::from(fr);
data
}
#[test]
fn extract_cicp_srgb() {
let icc = build_icc_with_cicp(1, 13, 0, true);
let cicp = extract_cicp(&icc).unwrap();
assert_eq!(cicp.color_primaries, 1);
assert_eq!(cicp.transfer_characteristics, 13);
assert_eq!(cicp.matrix_coefficients, 0);
assert!(cicp.full_range);
}
#[test]
fn extract_cicp_pq() {
let icc = build_icc_with_cicp(9, 16, 0, true);
let cicp = extract_cicp(&icc).unwrap();
assert_eq!(cicp.color_primaries, 9);
assert_eq!(cicp.transfer_characteristics, 16);
}
#[test]
fn extract_cicp_empty() {
assert!(extract_cicp(&[]).is_none());
assert!(extract_cicp(&[0; 100]).is_none());
}
#[test]
fn extract_cicp_no_acsp() {
let mut icc = build_icc_with_cicp(1, 13, 0, true);
icc[36..40].copy_from_slice(b"xxxx");
assert!(extract_cicp(&icc).is_none());
}
#[test]
fn extract_cicp_no_tag() {
let mut data = alloc::vec![0u8; 256];
data[0..4].copy_from_slice(&256u32.to_be_bytes());
data[36..40].copy_from_slice(b"acsp");
data[128..132].copy_from_slice(&1u32.to_be_bytes());
data[132..136].copy_from_slice(b"desc"); data[136..140].copy_from_slice(&144u32.to_be_bytes());
data[140..144].copy_from_slice(&12u32.to_be_bytes());
assert!(extract_cicp(&data).is_none());
}
#[test]
fn extract_cicp_tag_too_small() {
let mut icc = build_icc_with_cicp(1, 13, 0, true);
icc[140..144].copy_from_slice(&8u32.to_be_bytes()); assert!(extract_cicp(&icc).is_none());
}
#[test]
fn extract_cicp_type_mismatch() {
let mut icc = build_icc_with_cicp(1, 13, 0, true);
icc[144..148].copy_from_slice(b"xxxx"); assert!(extract_cicp(&icc).is_none());
}
#[test]
fn extract_cicp_malicious_tag_count() {
let mut data = alloc::vec![0u8; 256];
data[0..4].copy_from_slice(&256u32.to_be_bytes());
data[36..40].copy_from_slice(b"acsp");
data[128..132].copy_from_slice(&u32::MAX.to_be_bytes());
assert!(extract_cicp(&data).is_none());
}
#[cfg(feature = "std")]
#[test]
#[ignore] fn survey_corpus_features() {
let cache = std::env::var("ICC_CACHE").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_default();
format!("{home}/.cache/zenpixels-icc")
});
let mut safe = 0;
let mut unsafe_lab = 0;
let mut unsafe_lut = 0;
let mut unsafe_chad = 0;
let mut no_matrix = 0;
let mut total = 0;
let entries = match std::fs::read_dir(&cache) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if !matches!(
path.extension().and_then(|s| s.to_str()),
Some("icc" | "icm")
) {
continue;
}
let data = match std::fs::read(&path) {
Ok(d) => d,
Err(_) => continue,
};
total += 1;
if let Some(feat) = inspect_profile(&data) {
if feat.is_safe_matrix_shaper() {
safe += 1;
} else if !feat.has_matrix_shaper {
no_matrix += 1;
} else if feat.pcs_is_lab {
unsafe_lab += 1;
} else if feat.has_a2b0
|| feat.has_a2b1
|| feat.has_a2b2
|| feat.has_b2a0
|| feat.has_b2a1
|| feat.has_b2a2
{
unsafe_lut += 1;
} else if feat.has_chad && !feat.chad_is_bradford {
unsafe_chad += 1;
}
}
}
eprintln!("\n=== ICC Profile Features Survey (cache: {cache}) ===");
eprintln!("Total profiles: {total}");
eprintln!("Safe matrix-shaper: {safe}");
eprintln!("Has matrix tags + LUTs: {unsafe_lut}");
eprintln!("Lab PCS: {unsafe_lab}");
eprintln!("Non-Bradford chad: {unsafe_chad}");
eprintln!("No matrix-shaper tags: {no_matrix}");
}
}