use crate::types::{Chromaticity, GamutInfo};
use ultrahdr_core::ColorGamut;
#[must_use]
pub fn display_p3() -> &'static [u8] {
include_bytes!("../assets/icc/DisplayP3.icc")
}
const TAG_TABLE_OFFSET: usize = 128;
const ICC_XYZ_TYPE: &[u8; 4] = b"XYZ ";
const ICC_SF32_TYPE: &[u8; 4] = b"sf32";
const ICC_CHAD_TAG: &[u8; 4] = b"chad";
const ICC_RXYZ_TAG: &[u8; 4] = b"rXYZ";
const ICC_GXYZ_TAG: &[u8; 4] = b"gXYZ";
const ICC_BXYZ_TAG: &[u8; 4] = b"bXYZ";
const ICC_WTPT_TAG: &[u8; 4] = b"wtpt";
const GAMUT_MATCH_EPSILON: f32 = 0.015;
#[derive(Clone, Copy)]
struct Xyz {
x: f64,
y: f64,
z: f64,
}
#[derive(Clone, Copy)]
struct Matrix3([[f64; 3]; 3]);
pub(crate) fn gamut_info_from_profile(profile: &[u8]) -> Option<GamutInfo> {
let mut red = parse_xyz_tag(profile, ICC_RXYZ_TAG)?;
let mut green = parse_xyz_tag(profile, ICC_GXYZ_TAG)?;
let mut blue = parse_xyz_tag(profile, ICC_BXYZ_TAG)?;
let mut white = parse_xyz_tag(profile, ICC_WTPT_TAG)?;
if let Some(chad) = parse_matrix_tag(profile, ICC_CHAD_TAG)
&& let Some(inverse) = chad.inverse()
{
red = inverse.apply(red);
green = inverse.apply(green);
blue = inverse.apply(blue);
white = inverse.apply(white);
}
let red = xyz_to_xy(red)?;
let green = xyz_to_xy(green)?;
let blue = xyz_to_xy(blue)?;
let white = xyz_to_xy(white)?;
let mut info = GamutInfo {
standard: None,
red,
green,
blue,
white,
};
info.standard = classify_gamut(&info);
Some(info)
}
fn parse_xyz_tag(profile: &[u8], signature: &[u8; 4]) -> Option<Xyz> {
let payload = find_tag_payload(profile, signature)?;
if payload.len() < 20 || &payload[..4] != ICC_XYZ_TYPE {
return None;
}
Some(Xyz {
x: parse_s15fixed16(&payload[8..12])?,
y: parse_s15fixed16(&payload[12..16])?,
z: parse_s15fixed16(&payload[16..20])?,
})
}
fn parse_matrix_tag(profile: &[u8], signature: &[u8; 4]) -> Option<Matrix3> {
let payload = find_tag_payload(profile, signature)?;
if payload.len() < 44 || &payload[..4] != ICC_SF32_TYPE {
return None;
}
let mut values = [0.0f64; 9];
for (index, value) in values.iter_mut().enumerate() {
let start = 8 + index * 4;
*value = parse_s15fixed16(&payload[start..start + 4])?;
}
Some(Matrix3([
[values[0], values[1], values[2]],
[values[3], values[4], values[5]],
[values[6], values[7], values[8]],
]))
}
fn find_tag_payload<'a>(profile: &'a [u8], signature: &[u8; 4]) -> Option<&'a [u8]> {
if profile.len() < TAG_TABLE_OFFSET + 4 {
return None;
}
let tag_count = u32::from_be_bytes(
profile[TAG_TABLE_OFFSET..TAG_TABLE_OFFSET + 4]
.try_into()
.ok()?,
) as usize;
let table_len = tag_count.checked_mul(12)?;
let table_end = TAG_TABLE_OFFSET.checked_add(4 + table_len)?;
if profile.len() < table_end {
return None;
}
for index in 0..tag_count {
let entry = TAG_TABLE_OFFSET + 4 + index * 12;
if &profile[entry..entry + 4] != signature {
continue;
}
let offset = u32::from_be_bytes(profile[entry + 4..entry + 8].try_into().ok()?) as usize;
let len = u32::from_be_bytes(profile[entry + 8..entry + 12].try_into().ok()?) as usize;
let end = offset.checked_add(len)?;
if end > profile.len() {
return None;
}
return Some(&profile[offset..end]);
}
None
}
fn parse_s15fixed16(bytes: &[u8]) -> Option<f64> {
let raw = i32::from_be_bytes(bytes.try_into().ok()?);
Some(f64::from(raw) / 65536.0)
}
fn xyz_to_xy(xyz: Xyz) -> Option<Chromaticity> {
let sum = xyz.x + xyz.y + xyz.z;
if !sum.is_finite() || sum.abs() <= f64::EPSILON {
return None;
}
Some(Chromaticity {
x: (xyz.x / sum) as f32,
y: (xyz.y / sum) as f32,
})
}
fn classify_gamut(info: &GamutInfo) -> Option<ColorGamut> {
for standard in [ColorGamut::Bt709, ColorGamut::DisplayP3, ColorGamut::Bt2100] {
let reference = GamutInfo::from_standard(standard);
if chromaticity_matches(info.red, reference.red)
&& chromaticity_matches(info.green, reference.green)
&& chromaticity_matches(info.blue, reference.blue)
&& chromaticity_matches(info.white, reference.white)
{
return Some(standard);
}
}
None
}
fn chromaticity_matches(lhs: Chromaticity, rhs: Chromaticity) -> bool {
(lhs.x - rhs.x).abs() <= GAMUT_MATCH_EPSILON && (lhs.y - rhs.y).abs() <= GAMUT_MATCH_EPSILON
}
impl Matrix3 {
fn apply(self, value: Xyz) -> Xyz {
Xyz {
x: self.0[0][0] * value.x + self.0[0][1] * value.y + self.0[0][2] * value.z,
y: self.0[1][0] * value.x + self.0[1][1] * value.y + self.0[1][2] * value.z,
z: self.0[2][0] * value.x + self.0[2][1] * value.y + self.0[2][2] * value.z,
}
}
fn inverse(self) -> Option<Self> {
let m = self.0;
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
if det.abs() <= f64::EPSILON {
return None;
}
let inv_det = 1.0 / det;
Some(Self([
[
(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
(m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
(m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
],
[
(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
(m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
(m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
],
[
(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
(m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
(m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
],
]))
}
}
#[cfg(test)]
mod tests {
use super::{display_p3, gamut_info_from_profile};
use ultrahdr_core::ColorGamut;
#[test]
fn display_p3_profile_classifies_correctly() {
let gamut = gamut_info_from_profile(display_p3()).expect("display-p3 gamut info");
assert_eq!(gamut.standard, Some(ColorGamut::DisplayP3));
}
#[test]
fn invalid_profile_returns_none() {
assert!(gamut_info_from_profile(b"not-an-icc-profile").is_none());
}
}