use alloc::borrow::Cow;
use crate::{Cicp, ColorPrimaries, TransferFunction};
#[cfg(feature = "icc-db")]
mod cicp_bundle;
#[cfg(feature = "icc-db")]
#[rustfmt::skip]
mod cicp_bundle_index;
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 = "icc-db")]
{
if let Some(bytes) = cicp_bundle::bundled_profile_for_cicp(
cicp.color_primaries,
cicp.transfer_characteristics,
) {
return SynthesizedIcc::Profile(bytes);
}
SynthesizedIcc::CmsUnsupported
}
#[cfg(not(feature = "icc-db"))]
{
SynthesizedIcc::NeedsCms
}
}
pub fn synthesize_gray_icc_for_cicp(cicp: Cicp) -> SynthesizedIcc {
if matches!(cicp.color_primaries, 1 | 2) && matches!(cicp.transfer_characteristics, 1 | 2 | 13)
{
return SynthesizedIcc::NotNeeded;
}
#[cfg(feature = "icc-db")]
{
if let Some(bytes) = cicp_bundle::bundled_gray_profile_for_cicp(
cicp.color_primaries,
cicp.transfer_characteristics,
) {
return SynthesizedIcc::Profile(bytes);
}
SynthesizedIcc::CmsUnsupported
}
#[cfg(not(feature = "icc-db"))]
{
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_is_bundled_in_every_build() {
let bt2020_pq = Cicp::new(9, 16, 9, false); match synthesize_icc_for_cicp(bt2020_pq) {
SynthesizedIcc::Profile(bytes) => {
assert!(!bytes.is_empty());
assert_eq!(&bytes[36..40], b"acsp", "must be a valid ICC profile");
}
other => panic!("expected a bundled PQ profile, got {other:?}"),
}
}
#[test]
fn synthesize_icc_for_cicp_reserved_transfer_is_unsupported_not_mistagged() {
let bt2020_reserved_trc = Cicp::new(9, 0, 9, false);
assert_eq!(
synthesize_icc_for_cicp(bt2020_reserved_trc),
SynthesizedIcc::CmsUnsupported
);
}
#[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"
);
assert!(
matches!(got, SynthesizedIcc::Profile(_)),
"an assigned H.273 grid point must resolve from the blob in every build, got {got:?}"
);
}
#[test]
fn synthesize_icc_for_cicp_full_grid_coverage_without_cms() {
const ASSIGNED_PRIMARIES: &[u8] = &[1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 22];
const ASSIGNED_TRANSFERS: &[u8] =
&[1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
let mut profiles = 0usize;
let mut not_needed = 0usize;
for &p in ASSIGNED_PRIMARIES {
for &t in ASSIGNED_TRANSFERS {
let got = synthesize_icc_for_cicp(Cicp::new(p, t, 0, true));
match got {
SynthesizedIcc::Profile(bytes) => {
assert!(
matches!(bytes, Cow::Borrowed(_)),
"({p}, {t}) was served by the CMS (Cow::Owned) — must come \
from the blob/const (Cow::Borrowed) so coverage holds without a CMS"
);
assert_eq!(&bytes[36..40], b"acsp", "({p}, {t}) produced non-ICC bytes");
profiles += 1;
}
SynthesizedIcc::NotNeeded => {
assert!(
p == 1 && matches!(t, 1 | 13),
"({p}, {t}) unexpectedly NotNeeded"
);
not_needed += 1;
}
other => panic!("({p}, {t}) gave {other:?} — expected full blob coverage"),
}
}
}
assert_eq!(profiles, 174, "expected 174 profile-yielding combos");
assert_eq!(
not_needed, 2,
"expected exactly 2 sRGB-default NotNeeded combos"
);
}
}