use crate::decode::SourceColor;
use zenpixels::{
Cicp, ColorPrimaries, ColorProfileSource, PixelDescriptor, PixelFormat, TransferFunction,
};
#[deprecated(
since = "0.1.17",
note = "use descriptor_for_decoded_pixels_v2 (drops placebo IccMatchTolerance)"
)]
#[allow(deprecated)]
pub fn descriptor_for_decoded_pixels(
format: PixelFormat,
source_color: &SourceColor,
corrected_to: Option<&Cicp>,
_tolerance: IccMatchTolerance,
) -> PixelDescriptor {
let corrected_src = corrected_to.map(|c| ColorProfileSource::Cicp(*c));
descriptor_for_decoded_pixels_v2(format, source_color, corrected_src.as_ref())
}
pub fn descriptor_for_decoded_pixels_v2(
format: PixelFormat,
source_color: &SourceColor,
corrected_to: Option<&ColorProfileSource<'_>>,
) -> PixelDescriptor {
let (primaries, transfer) = resolve_color(source_color, corrected_to);
PixelDescriptor::from_pixel_format(format)
.with_primaries(primaries)
.with_transfer(transfer)
}
pub fn resolve_color(
source_color: &SourceColor,
corrected_to: Option<&ColorProfileSource<'_>>,
) -> (ColorPrimaries, TransferFunction) {
if let Some(src) = corrected_to {
return resolve_profile_source(src);
}
if let Some(src) = source_color.to_color_context().as_profile_source() {
return resolve_profile_source(&src);
}
(ColorPrimaries::Bt709, TransferFunction::Srgb)
}
fn resolve_profile_source(src: &ColorProfileSource<'_>) -> (ColorPrimaries, TransferFunction) {
match src {
ColorProfileSource::Cicp(cicp) => (
ColorPrimaries::from_cicp(cicp.color_primaries).unwrap_or(ColorPrimaries::Unknown),
TransferFunction::from_cicp(cicp.transfer_characteristics)
.unwrap_or(TransferFunction::Unknown),
),
ColorProfileSource::Icc(icc) => match zenpixels::icc::identify_common(icc) {
Some(id) => (id.primaries, id.transfer),
None => (ColorPrimaries::Unknown, TransferFunction::Unknown),
},
ColorProfileSource::Named(named) => named.to_primaries_transfer(),
ColorProfileSource::PrimariesTransferPair {
primaries,
transfer,
} => (*primaries, *transfer),
_ => (ColorPrimaries::Unknown, TransferFunction::Unknown),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[must_use]
#[deprecated(
since = "0.1.16",
note = "zenpixels::icc::identify_common uses Intent tolerance; sub-Intent variants are placebo"
)]
pub enum IccMatchTolerance {
Exact = 1,
Precise = 3,
Approximate = 13,
Intent = 56,
}
#[deprecated(
since = "0.1.16",
note = "use zenpixels::icc::identify_common — returns richer IccIdentification with valid_use"
)]
#[allow(deprecated)]
pub fn identify_well_known_icc(
icc_bytes: &[u8],
_tolerance: IccMatchTolerance,
) -> Option<(ColorPrimaries, TransferFunction)> {
let id = zenpixels::icc::identify_common(icc_bytes)?;
Some((id.primaries, id.transfer))
}
#[deprecated(since = "0.1.16", note = "use zenpixels::icc::is_common_srgb")]
pub fn icc_profile_is_srgb(icc_bytes: &[u8]) -> bool {
zenpixels::icc::is_common_srgb(icc_bytes)
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use alloc::sync::Arc;
use zenpixels::{AlphaMode, Cicp, ColorPrimaries, SignalRange, TransferFunction};
#[test]
fn no_metadata_assumes_srgb() {
let sc = SourceColor::default();
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgb8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
}
#[test]
fn no_metadata_gray_assumes_srgb() {
let sc = SourceColor::default();
let desc =
descriptor_for_decoded_pixels(PixelFormat::Gray8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
assert_eq!(desc.pixel_format(), PixelFormat::Gray8);
}
#[test]
fn no_metadata_rgba_assumes_srgb() {
let sc = SourceColor::default();
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgba8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
assert_eq!(desc.pixel_format(), PixelFormat::Rgba8);
assert_eq!(desc.alpha(), Some(AlphaMode::Straight));
}
#[test]
fn no_metadata_f32_assumes_srgb() {
let sc = SourceColor::default();
let desc = descriptor_for_decoded_pixels(
PixelFormat::RgbF32,
&sc,
None,
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
assert_eq!(desc.pixel_format(), PixelFormat::RgbF32);
}
#[test]
fn cicp_srgb_sets_srgb_descriptor() {
let sc = SourceColor::default().with_cicp(Cicp::SRGB);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgb8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
}
#[test]
fn cicp_p3_sets_descriptor() {
let sc = SourceColor::default().with_cicp(Cicp::DISPLAY_P3);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgb8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
}
#[test]
fn cicp_pq_sets_descriptor() {
let sc = SourceColor::default().with_cicp(Cicp::BT2100_PQ);
let desc = descriptor_for_decoded_pixels(
PixelFormat::RgbaF32,
&sc,
None,
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Pq);
assert_eq!(desc.primaries, ColorPrimaries::Bt2020);
}
#[test]
fn cicp_hlg_sets_descriptor() {
let sc = SourceColor::default().with_cicp(Cicp::BT2100_HLG);
let desc = descriptor_for_decoded_pixels(
PixelFormat::RgbF32,
&sc,
None,
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Hlg);
assert_eq!(desc.primaries, ColorPrimaries::Bt2020);
}
#[test]
fn cicp_takes_precedence_over_icc_when_cicp_authoritative() {
let fake_icc: Arc<[u8]> = Arc::from(alloc::vec![0u8; 64].into_boxed_slice());
let sc = SourceColor::default()
.with_cicp(Cicp::DISPLAY_P3)
.with_icc_profile(fake_icc)
.with_color_authority(zenpixels::ColorAuthority::Cicp);
let desc = descriptor_for_decoded_pixels_v2(PixelFormat::Rgb8, &sc, None);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
}
#[test]
fn icc_takes_precedence_over_cicp_when_icc_authoritative() {
let fake_icc: Arc<[u8]> = Arc::from(alloc::vec![0u8; 64].into_boxed_slice());
let sc = SourceColor::default()
.with_cicp(Cicp::DISPLAY_P3)
.with_icc_profile(fake_icc)
.with_color_authority(zenpixels::ColorAuthority::Icc);
let desc = descriptor_for_decoded_pixels_v2(PixelFormat::Rgb8, &sc, None);
assert_eq!(desc.transfer, TransferFunction::Unknown);
assert_eq!(desc.primaries, ColorPrimaries::Unknown);
}
#[test]
fn cicp_preserves_pixel_format() {
let sc = SourceColor::default().with_cicp(Cicp::DISPLAY_P3);
for fmt in [
PixelFormat::Rgb8,
PixelFormat::Rgba8,
PixelFormat::Gray8,
PixelFormat::RgbF32,
PixelFormat::Bgra8,
] {
let desc = descriptor_for_decoded_pixels(fmt, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.pixel_format(), fmt, "format mismatch for {fmt:?}");
}
}
#[test]
fn unknown_icc_yields_unknown_descriptor() {
let fake_icc: Arc<[u8]> = Arc::from(alloc::vec![0u8; 64].into_boxed_slice());
let sc = SourceColor::default().with_icc_profile(fake_icc);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgb8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Unknown);
assert_eq!(desc.primaries, ColorPrimaries::Unknown);
}
#[test]
fn unknown_icc_preserves_format_and_alpha() {
let fake_icc: Arc<[u8]> = Arc::from(alloc::vec![99u8; 128].into_boxed_slice());
let sc = SourceColor::default().with_icc_profile(fake_icc);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgba8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.pixel_format(), PixelFormat::Rgba8);
assert_eq!(desc.alpha(), Some(AlphaMode::Straight));
assert_eq!(desc.signal_range, SignalRange::Full);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgb8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.pixel_format(), PixelFormat::Rgb8);
assert!(desc.alpha().is_none());
}
#[test]
fn unknown_icc_gray_preserves_format() {
let fake_icc: Arc<[u8]> = Arc::from(alloc::vec![42u8; 96].into_boxed_slice());
let sc = SourceColor::default().with_icc_profile(fake_icc);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Gray8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.pixel_format(), PixelFormat::Gray8);
assert_eq!(desc.transfer, TransferFunction::Unknown);
assert_eq!(desc.primaries, ColorPrimaries::Unknown);
}
#[test]
fn empty_icc_yields_unknown() {
let empty_icc: Arc<[u8]> = Arc::from(alloc::vec![].into_boxed_slice());
let sc = SourceColor::default().with_icc_profile(empty_icc);
let desc =
descriptor_for_decoded_pixels(PixelFormat::Rgb8, &sc, None, IccMatchTolerance::Intent);
assert_eq!(desc.transfer, TransferFunction::Unknown);
assert_eq!(desc.primaries, ColorPrimaries::Unknown);
}
#[test]
fn corrected_to_overrides_source_cicp() {
let sc = SourceColor::default().with_cicp(Cicp::DISPLAY_P3);
let desc = descriptor_for_decoded_pixels(
PixelFormat::Rgb8,
&sc,
Some(&Cicp::SRGB),
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
}
#[test]
fn corrected_to_overrides_unknown_icc() {
let fake_icc: Arc<[u8]> = Arc::from(alloc::vec![0u8; 64].into_boxed_slice());
let sc = SourceColor::default().with_icc_profile(fake_icc);
let desc = descriptor_for_decoded_pixels(
PixelFormat::Rgb8,
&sc,
Some(&Cicp::SRGB),
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
}
#[test]
fn corrected_to_overrides_no_metadata() {
let sc = SourceColor::default();
let desc = descriptor_for_decoded_pixels(
PixelFormat::Rgb8,
&sc,
Some(&Cicp::SRGB),
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::Bt709);
}
#[test]
fn corrected_to_p3_target() {
let sc = SourceColor::default().with_cicp(Cicp::SRGB);
let desc = descriptor_for_decoded_pixels(
PixelFormat::Rgb8,
&sc,
Some(&Cicp::DISPLAY_P3),
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, TransferFunction::Srgb);
assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
}
#[test]
fn corrected_to_preserves_format() {
let sc = SourceColor::default().with_cicp(Cicp::BT2100_PQ);
let desc = descriptor_for_decoded_pixels(
PixelFormat::Bgra8,
&sc,
Some(&Cicp::SRGB),
IccMatchTolerance::Intent,
);
assert_eq!(desc.pixel_format(), PixelFormat::Bgra8);
assert_eq!(desc.transfer, TransferFunction::Srgb);
}
#[test]
fn identify_rejects_empty() {
assert!(identify_well_known_icc(&[], IccMatchTolerance::Intent).is_none());
assert!(!icc_profile_is_srgb(&[]));
}
#[test]
fn identify_rejects_garbage() {
assert!(identify_well_known_icc(&[0u8; 100], IccMatchTolerance::Intent).is_none());
}
#[test]
fn identify_rejects_short() {
assert!(identify_well_known_icc(&[1, 2, 3, 4], IccMatchTolerance::Intent).is_none());
}
#[test]
fn icc_profile_is_srgb_compat() {
assert!(!icc_profile_is_srgb(&[0u8; 100]));
}
#[test]
fn tolerance_ordering() {
assert!(IccMatchTolerance::Exact < IccMatchTolerance::Precise);
assert!(IccMatchTolerance::Precise < IccMatchTolerance::Approximate);
assert!(IccMatchTolerance::Approximate < IccMatchTolerance::Intent);
}
fn sc_none() -> SourceColor {
SourceColor::default()
}
fn sc_cicp(c: Cicp) -> SourceColor {
SourceColor::default().with_cicp(c)
}
fn sc_icc(fill: u8, len: usize) -> SourceColor {
let icc: Arc<[u8]> = Arc::from(alloc::vec![fill; len].into_boxed_slice());
SourceColor::default().with_icc_profile(icc)
}
fn sc_cicp_icc(c: Cicp, fill: u8, len: usize) -> SourceColor {
let icc: Arc<[u8]> = Arc::from(alloc::vec![fill; len].into_boxed_slice());
SourceColor::default()
.with_cicp(c)
.with_icc_profile(icc)
.with_color_authority(zenpixels::ColorAuthority::Cicp)
}
use ColorPrimaries as CP;
use TransferFunction as TF;
type FormatScenario = (&'static str, PixelFormat, SourceColor, Option<Cicp>, TF, CP);
#[test]
fn format_scenarios() {
let cases: &[FormatScenario] = &[
(
"jpeg_no_icc",
PixelFormat::Rgb8,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
(
"jpeg_unknown_icc",
PixelFormat::Rgb8,
sc_icc(0xCA, 3144),
None,
TF::Unknown,
CP::Unknown,
),
(
"jpeg_corrected",
PixelFormat::Rgb8,
sc_icc(0xCA, 3144),
Some(Cicp::SRGB),
TF::Srgb,
CP::Bt709,
),
(
"png_cicp_p3",
PixelFormat::Rgba8,
sc_cicp(Cicp::DISPLAY_P3),
None,
TF::Srgb,
CP::DisplayP3,
),
(
"png_cicp_over_icc",
PixelFormat::Rgba8,
sc_cicp_icc(Cicp::DISPLAY_P3, 0, 100),
None,
TF::Srgb,
CP::DisplayP3,
),
(
"png_no_metadata",
PixelFormat::Rgba8,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
(
"png_hdr_pq",
PixelFormat::Rgba16,
sc_cicp(Cicp::BT2100_PQ),
None,
TF::Pq,
CP::Bt2020,
),
(
"webp_no_icc",
PixelFormat::Rgba8,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
(
"webp_unknown_icc",
PixelFormat::Rgba8,
sc_icc(0xA3, 480),
None,
TF::Unknown,
CP::Unknown,
),
(
"avif_srgb",
PixelFormat::Rgba8,
sc_cicp(Cicp::SRGB),
None,
TF::Srgb,
CP::Bt709,
),
(
"avif_hdr10",
PixelFormat::RgbaF32,
sc_cicp(Cicp::BT2100_PQ),
None,
TF::Pq,
CP::Bt2020,
),
(
"avif_hlg",
PixelFormat::RgbF32,
sc_cicp(Cicp::BT2100_HLG),
None,
TF::Hlg,
CP::Bt2020,
),
(
"avif_p3",
PixelFormat::Rgb8,
sc_cicp(Cicp::DISPLAY_P3),
None,
TF::Srgb,
CP::DisplayP3,
),
(
"jxl_srgb",
PixelFormat::Rgb8,
sc_cicp(Cicp::SRGB),
None,
TF::Srgb,
CP::Bt709,
),
(
"jxl_p3_pq",
PixelFormat::RgbaF32,
sc_cicp(Cicp::new(12, 16, 0, true)),
None,
TF::Pq,
CP::DisplayP3,
),
(
"heic_p3",
PixelFormat::Rgba8,
sc_cicp(Cicp::DISPLAY_P3),
None,
TF::Srgb,
CP::DisplayP3,
),
(
"heic_hdr10",
PixelFormat::RgbaF32,
sc_cicp(Cicp::BT2100_PQ),
None,
TF::Pq,
CP::Bt2020,
),
(
"gif_srgb",
PixelFormat::Rgba8,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
(
"bmp_srgb",
PixelFormat::Rgb8,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
(
"pnm_gray",
PixelFormat::Gray8,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
(
"tiff_unknown_icc",
PixelFormat::Rgb16,
sc_icc(0x54, 7261),
None,
TF::Unknown,
CP::Unknown,
),
(
"tiff_no_icc",
PixelFormat::Rgb16,
sc_none(),
None,
TF::Srgb,
CP::Bt709,
),
];
for &(name, fmt, ref sc, ref corrected, exp_tf, exp_cp) in cases {
let desc = descriptor_for_decoded_pixels(
fmt,
sc,
corrected.as_ref(),
IccMatchTolerance::Intent,
);
assert_eq!(desc.transfer, exp_tf, "{name}: transfer");
assert_eq!(desc.primaries, exp_cp, "{name}: primaries");
assert_eq!(desc.pixel_format(), fmt, "{name}: format");
}
}
#[test]
fn all_paths_produce_full_range() {
let cases: &[(SourceColor, Option<&Cicp>)] = &[
(SourceColor::default(), None),
(SourceColor::default().with_cicp(Cicp::SRGB), None),
(SourceColor::default().with_cicp(Cicp::DISPLAY_P3), None),
(SourceColor::default().with_cicp(Cicp::BT2100_PQ), None),
(SourceColor::default(), Some(&Cicp::SRGB)),
];
for (sc, corrected) in cases {
let desc = descriptor_for_decoded_pixels(
PixelFormat::Rgb8,
sc,
*corrected,
IccMatchTolerance::Intent,
);
assert_eq!(
desc.signal_range,
SignalRange::Full,
"non-full range for {sc:?} corrected={corrected:?}"
);
}
}
}