use alloc::borrow::Cow;
use crate::{Cicp, ColorPrimaries, TransferFunction};
pub const DISPLAY_P3_V4: &[u8] = include_bytes!("profiles/DisplayP3Compat-v4.icc");
pub const DISPLAY_P3_V2: &[u8] = include_bytes!("profiles/DisplayP3Compat-v2-magic.icc");
pub const ADOBE_RGB: &[u8] = include_bytes!("profiles/AdobeCompat-v2.icc");
#[deprecated(
since = "0.2.4",
note = "renamed to ADOBE_RGB (now v2 pure-gamma form)"
)]
pub const ADOBE_RGB_V4: &[u8] = ADOBE_RGB;
#[deprecated(
since = "0.2.4",
note = "ProPhoto removed — TRC too fragmented to pick a canonical form"
)]
pub const PROPHOTO_V4: &[u8] = &[];
pub const REC2020_V4: &[u8] = include_bytes!("profiles/Rec2020Compat-v4.icc");
#[deprecated(
since = "0.2.12",
note = "transfer-blind (can mis-tag an HDR gamut with an SDR TRC); use \
synthesize_icc_for_cicp for transfer-aware synthesis, or embed a \
bundled const (DISPLAY_P3_V4 / REC2020_V4 / ADOBE_RGB) directly"
)]
#[inline]
pub const fn icc_profile_for_primaries(primaries: ColorPrimaries) -> Option<&'static [u8]> {
match primaries {
ColorPrimaries::DisplayP3 => Some(DISPLAY_P3_V4),
ColorPrimaries::Bt2020 => Some(REC2020_V4),
ColorPrimaries::AdobeRgb => Some(ADOBE_RGB),
ColorPrimaries::Bt709 | ColorPrimaries::Unknown | _ => None,
}
}
#[inline]
pub const fn display_p3_icc(prefer_v2: bool) -> &'static [u8] {
if prefer_v2 {
DISPLAY_P3_V2
} else {
DISPLAY_P3_V4
}
}
#[inline]
pub(crate) const fn bundled_icc_profile(
primaries: ColorPrimaries,
transfer: TransferFunction,
) -> Option<&'static [u8]> {
match (primaries, transfer) {
(ColorPrimaries::DisplayP3, TransferFunction::Srgb)
| (ColorPrimaries::DisplayP3, TransferFunction::Bt709) => Some(DISPLAY_P3_V4),
(ColorPrimaries::Bt2020, TransferFunction::Bt709)
| (ColorPrimaries::Bt2020, TransferFunction::Srgb) => Some(REC2020_V4),
(ColorPrimaries::AdobeRgb, TransferFunction::Gamma22) => Some(ADOBE_RGB),
_ => None,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SynthesizedIcc {
Profile(Cow<'static, [u8]>),
NotNeeded,
NeedsCms,
CmsUnsupported,
}
pub fn synthesize_icc_for_cicp(cicp: Cicp) -> SynthesizedIcc {
if matches!(cicp.color_primaries, 1 | 2) && matches!(cicp.transfer_characteristics, 1 | 2 | 13)
{
return SynthesizedIcc::NotNeeded;
}
let primaries = cicp.color_primaries_enum();
let transfer = cicp.transfer_function_enum();
if let Some(bytes) = bundled_icc_profile(primaries, transfer) {
return SynthesizedIcc::Profile(Cow::Borrowed(bytes));
}
#[cfg(feature = "cms-moxcms")]
{
match crate::cms_moxcms::icc_bytes_for_cicp(&cicp) {
Some(bytes) => SynthesizedIcc::Profile(Cow::Owned(bytes)),
None => SynthesizedIcc::CmsUnsupported,
}
}
#[cfg(not(feature = "cms-moxcms"))]
{
SynthesizedIcc::NeedsCms
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v4_profiles_valid_structure() {
let profiles: &[(&[u8], &str)] = &[
(DISPLAY_P3_V4, "Display P3 v4"),
(REC2020_V4, "Rec. 2020 v4"),
];
for (profile, name) in profiles {
assert_eq!(profile.len(), 480, "{name}: expected 480 bytes");
assert_eq!(
&profile[36..40],
b"acsp",
"{name}: missing ICC 'acsp' signature at offset 36"
);
assert_eq!(
&profile[12..16],
b"mntr",
"{name}: expected 'mntr' (monitor) profile class at offset 12"
);
}
}
#[test]
fn adobe_rgb_profile_valid_structure() {
assert_eq!(
ADOBE_RGB.len(),
374,
"Adobe RGB: expected 374 bytes (ICC v2)"
);
assert_eq!(
&ADOBE_RGB[36..40],
b"acsp",
"Adobe RGB: missing ICC 'acsp' signature at offset 36"
);
assert_eq!(
&ADOBE_RGB[12..16],
b"mntr",
"Adobe RGB: expected 'mntr' (monitor) profile class at offset 12"
);
let tag_count = u32::from_be_bytes([
ADOBE_RGB[128],
ADOBE_RGB[129],
ADOBE_RGB[130],
ADOBE_RGB[131],
]) as usize;
let mut found_pure_gamma_trc = false;
for i in 0..tag_count {
let b = 132 + i * 12;
if &ADOBE_RGB[b..b + 4] == b"rTRC" {
let off = u32::from_be_bytes([
ADOBE_RGB[b + 4],
ADOBE_RGB[b + 5],
ADOBE_RGB[b + 6],
ADOBE_RGB[b + 7],
]) as usize;
assert_eq!(
&ADOBE_RGB[off..off + 4],
b"curv",
"Adobe RGB: rTRC must be curveType (pure gamma)"
);
let count = u32::from_be_bytes([
ADOBE_RGB[off + 8],
ADOBE_RGB[off + 9],
ADOBE_RGB[off + 10],
ADOBE_RGB[off + 11],
]);
assert_eq!(
count, 1,
"Adobe RGB: curveType count must be 1 (pure gamma, no toe)"
);
found_pure_gamma_trc = true;
break;
}
}
assert!(found_pure_gamma_trc, "Adobe RGB: rTRC tag not found");
}
#[test]
fn v2_profile_valid_structure() {
assert_eq!(
DISPLAY_P3_V2.len(),
736,
"Display P3 v2: expected 736 bytes"
);
assert_eq!(
&DISPLAY_P3_V2[36..40],
b"acsp",
"Display P3 v2: missing ICC 'acsp' signature at offset 36"
);
assert_eq!(
&DISPLAY_P3_V2[12..16],
b"mntr",
"Display P3 v2: expected 'mntr' (monitor) profile class at offset 12"
);
}
#[test]
fn display_p3_icc_selector() {
assert_eq!(display_p3_icc(false).len(), 480); assert_eq!(display_p3_icc(true).len(), 736); }
#[test]
#[allow(deprecated)] fn icc_profile_for_primaries_mapping() {
assert_eq!(
icc_profile_for_primaries(ColorPrimaries::DisplayP3),
Some(DISPLAY_P3_V4)
);
assert_eq!(
icc_profile_for_primaries(ColorPrimaries::Bt2020),
Some(REC2020_V4)
);
assert_eq!(
icc_profile_for_primaries(ColorPrimaries::AdobeRgb),
Some(ADOBE_RGB)
);
assert!(icc_profile_for_primaries(ColorPrimaries::Bt709).is_none());
assert!(icc_profile_for_primaries(ColorPrimaries::Unknown).is_none());
}
#[test]
fn bundled_icc_profile_hits_combinations() {
assert_eq!(
bundled_icc_profile(ColorPrimaries::DisplayP3, TransferFunction::Srgb),
Some(DISPLAY_P3_V4)
);
assert_eq!(
bundled_icc_profile(ColorPrimaries::DisplayP3, TransferFunction::Bt709),
Some(DISPLAY_P3_V4)
);
assert_eq!(
bundled_icc_profile(ColorPrimaries::Bt2020, TransferFunction::Bt709),
Some(REC2020_V4)
);
assert_eq!(
bundled_icc_profile(ColorPrimaries::Bt2020, TransferFunction::Srgb),
Some(REC2020_V4)
);
assert_eq!(
bundled_icc_profile(ColorPrimaries::AdobeRgb, TransferFunction::Gamma22),
Some(ADOBE_RGB)
);
}
#[test]
fn bundled_icc_profile_rejects_hdr_transfers() {
assert!(bundled_icc_profile(ColorPrimaries::Bt2020, TransferFunction::Pq).is_none());
assert!(bundled_icc_profile(ColorPrimaries::Bt2020, TransferFunction::Hlg).is_none());
assert!(bundled_icc_profile(ColorPrimaries::DisplayP3, TransferFunction::Pq).is_none());
assert!(bundled_icc_profile(ColorPrimaries::DisplayP3, TransferFunction::Hlg).is_none());
assert!(bundled_icc_profile(ColorPrimaries::Bt2020, TransferFunction::Linear).is_none());
assert!(bundled_icc_profile(ColorPrimaries::DisplayP3, TransferFunction::Linear).is_none());
}
#[test]
fn bundled_icc_profile_rejects_mismatched_trc() {
assert!(bundled_icc_profile(ColorPrimaries::AdobeRgb, TransferFunction::Srgb).is_none());
assert!(bundled_icc_profile(ColorPrimaries::AdobeRgb, TransferFunction::Bt709).is_none());
assert!(
bundled_icc_profile(ColorPrimaries::DisplayP3, TransferFunction::Gamma22).is_none()
);
assert!(bundled_icc_profile(ColorPrimaries::Bt2020, TransferFunction::Gamma22).is_none());
}
#[test]
fn bundled_icc_profile_bt709_returns_none() {
assert!(bundled_icc_profile(ColorPrimaries::Bt709, TransferFunction::Srgb).is_none());
assert!(bundled_icc_profile(ColorPrimaries::Bt709, TransferFunction::Bt709).is_none());
assert!(bundled_icc_profile(ColorPrimaries::Unknown, TransferFunction::Unknown).is_none());
}
#[test]
fn synthesize_icc_for_cicp_srgb_is_not_needed() {
assert_eq!(
synthesize_icc_for_cicp(Cicp::SRGB),
SynthesizedIcc::NotNeeded
);
}
#[test]
fn synthesize_icc_for_cicp_display_p3_is_bundled_borrowed() {
match synthesize_icc_for_cicp(Cicp::DISPLAY_P3) {
SynthesizedIcc::Profile(Cow::Borrowed(bytes)) => assert_eq!(bytes, DISPLAY_P3_V4),
other => panic!("expected bundled borrowed Display P3 profile, got {other:?}"),
}
}
#[test]
fn synthesize_icc_for_cicp_hdr_pq_needs_cms_or_generates() {
let bt2020_pq = Cicp::new(9, 16, 9, false); let got = synthesize_icc_for_cicp(bt2020_pq);
#[cfg(feature = "cms-moxcms")]
match got {
SynthesizedIcc::Profile(Cow::Owned(bytes)) => assert!(!bytes.is_empty()),
other => panic!("expected CMS-generated owned PQ profile, got {other:?}"),
}
#[cfg(not(feature = "cms-moxcms"))]
assert_eq!(got, SynthesizedIcc::NeedsCms);
}
#[test]
fn synthesize_icc_for_cicp_reserved_transfer_is_unsupported_not_mistagged() {
let bt2020_reserved_trc = Cicp::new(9, 0, 9, false);
let got = synthesize_icc_for_cicp(bt2020_reserved_trc);
#[cfg(feature = "cms-moxcms")]
assert_eq!(got, SynthesizedIcc::CmsUnsupported);
#[cfg(not(feature = "cms-moxcms"))]
assert_eq!(got, SynthesizedIcc::NeedsCms);
}
#[test]
fn synthesize_icc_for_cicp_real_gamut_not_assumed_srgb() {
let bt601_g22 = Cicp::new(6, 4, 0, false);
let got = synthesize_icc_for_cicp(bt601_g22);
assert_ne!(
got,
SynthesizedIcc::NotNeeded,
"a real non-sRGB gamut must never be assumed to be the sRGB default"
);
#[cfg(feature = "cms-moxcms")]
assert!(matches!(got, SynthesizedIcc::Profile(_)));
#[cfg(not(feature = "cms-moxcms"))]
assert_eq!(got, SynthesizedIcc::NeedsCms);
}
}