use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::detect::SourceEncodingDetails;
use crate::gainmap::GainMapPresence;
use crate::metadata::Metadata;
use crate::{ImageFormat, Orientation};
use zenpixels::{ColorAuthority, ColorPrimaries, TransferFunction};
pub use zenpixels::Cicp;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImageSequence {
#[default]
Single,
Animation {
frame_count: Option<u32>,
loop_count: Option<u32>,
random_access: bool,
},
Multi {
image_count: Option<u32>,
random_access: bool,
},
}
impl ImageSequence {
pub fn count(&self) -> Option<u32> {
match self {
Self::Single => Some(1),
Self::Animation { frame_count, .. } => *frame_count,
Self::Multi { image_count, .. } => *image_count,
}
}
pub fn random_access(&self) -> bool {
match self {
Self::Single => true,
Self::Animation { random_access, .. } => *random_access,
Self::Multi { random_access, .. } => *random_access,
}
}
pub fn is_animation(&self) -> bool {
matches!(self, Self::Animation { .. })
}
pub fn is_multi(&self) -> bool {
matches!(self, Self::Multi { .. })
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Supplements {
pub pyramid: bool,
pub gain_map: bool,
pub depth_map: bool,
pub segmentation_mattes: bool,
pub auxiliary: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Resolution {
pub x: f64,
pub y: f64,
pub unit: ResolutionUnit,
}
impl Resolution {
pub fn dpi(&self) -> (f64, f64) {
match self.unit {
ResolutionUnit::Inch => (self.x, self.y),
ResolutionUnit::Centimeter => (self.x * 2.54, self.y * 2.54),
ResolutionUnit::Meter => (self.x * 0.0254, self.y * 0.0254),
ResolutionUnit::Unknown => (self.x, self.y),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ResolutionUnit {
Inch,
Centimeter,
Meter,
#[default]
Unknown,
}
pub use zenpixels::{ContentLightLevel, MasteringDisplay};
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct SourceColor {
pub cicp: Option<Cicp>,
pub icc_profile: Option<Arc<[u8]>>,
pub color_authority: ColorAuthority,
pub bit_depth: Option<u8>,
pub channel_count: Option<u8>,
pub content_light_level: Option<ContentLightLevel>,
pub mastering_display: Option<MasteringDisplay>,
}
impl SourceColor {
pub fn with_cicp(mut self, cicp: Cicp) -> Self {
self.cicp = Some(cicp);
self
}
pub fn with_icc_profile(mut self, icc: impl Into<Arc<[u8]>>) -> Self {
self.icc_profile = Some(icc.into());
self
}
pub fn with_bit_depth(mut self, bit_depth: u8) -> Self {
self.bit_depth = Some(bit_depth);
self
}
pub fn with_channel_count(mut self, channel_count: u8) -> Self {
self.channel_count = Some(channel_count);
self
}
pub fn with_content_light_level(mut self, clli: ContentLightLevel) -> Self {
self.content_light_level = Some(clli);
self
}
pub fn with_mastering_display(mut self, mdcv: MasteringDisplay) -> Self {
self.mastering_display = Some(mdcv);
self
}
pub fn with_color_authority(mut self, authority: ColorAuthority) -> Self {
self.color_authority = authority;
self
}
pub fn has_hdr_transfer(&self) -> bool {
if let Some(c) = self.cicp
&& matches!(c.transfer_characteristics, 16 | 18)
{
return true;
}
if let Some(ref icc) = self.icc_profile
&& let Some(c) = zenpixels::icc::extract_cicp(icc)
{
return matches!(c.transfer_characteristics, 16 | 18);
}
false
}
pub fn transfer_function(&self) -> TransferFunction {
self.cicp
.and_then(|c| TransferFunction::from_cicp(c.transfer_characteristics))
.unwrap_or(TransferFunction::Unknown)
}
pub fn color_primaries(&self) -> ColorPrimaries {
self.cicp
.map(|c| c.color_primaries_enum())
.unwrap_or(ColorPrimaries::Bt709)
}
pub fn to_color_context(&self) -> zenpixels::ColorContext {
match self.color_authority {
ColorAuthority::Cicp if self.cicp.is_some() => {
zenpixels::ColorContext::from_cicp(self.cicp.unwrap())
}
ColorAuthority::Icc if self.icc_profile.is_some() => {
zenpixels::ColorContext::from_icc(self.icc_profile.clone().unwrap())
}
_ => {
#[allow(deprecated)]
if let (Some(icc), Some(cicp)) = (&self.icc_profile, self.cicp) {
zenpixels::ColorContext::from_icc_and_cicp(icc.clone(), cicp)
} else if let Some(icc) = &self.icc_profile {
zenpixels::ColorContext::from_icc(icc.clone())
} else if let Some(cicp) = self.cicp {
zenpixels::ColorContext::from_cicp(cicp)
} else {
zenpixels::ColorContext {
icc: None,
cicp: None,
}
}
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct EmbeddedMetadata {
pub exif: Option<Arc<[u8]>>,
pub xmp: Option<Arc<[u8]>>,
}
impl EmbeddedMetadata {
pub fn with_exif(mut self, exif: impl Into<Arc<[u8]>>) -> Self {
self.exif = Some(exif.into());
self
}
pub fn with_xmp(mut self, xmp: impl Into<Arc<[u8]>>) -> Self {
self.xmp = Some(xmp.into());
self
}
pub fn is_empty(&self) -> bool {
self.exif.is_none() && self.xmp.is_none()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ImageInfo {
pub width: u32,
pub height: u32,
pub format: ImageFormat,
pub has_alpha: bool,
pub is_progressive: bool,
pub sequence: ImageSequence,
pub supplements: Supplements,
pub gain_map: GainMapPresence,
pub orientation: Orientation,
pub resolution: Option<Resolution>,
pub source_color: SourceColor,
pub embedded_metadata: EmbeddedMetadata,
pub source_encoding: Option<Arc<dyn SourceEncodingDetails>>,
pub warnings: Vec<alloc::string::String>,
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(core::mem::size_of::<ImageInfo>() == 248);
impl ImageInfo {
pub fn new(width: u32, height: u32, format: ImageFormat) -> Self {
Self {
width,
height,
format,
has_alpha: false,
is_progressive: false,
sequence: ImageSequence::Single,
supplements: Supplements::default(),
gain_map: GainMapPresence::default(),
orientation: Orientation::Identity,
resolution: None,
source_color: SourceColor::default(),
embedded_metadata: EmbeddedMetadata::default(),
source_encoding: None,
warnings: Vec::new(),
}
}
pub fn with_alpha(mut self, has_alpha: bool) -> Self {
self.has_alpha = has_alpha;
self
}
pub fn with_progressive(mut self, progressive: bool) -> Self {
self.is_progressive = progressive;
self
}
pub fn with_sequence(mut self, sequence: ImageSequence) -> Self {
self.sequence = sequence;
self
}
pub fn with_supplements(mut self, supplements: Supplements) -> Self {
self.supplements = supplements;
self
}
pub fn with_gain_map(mut self, gain_map: GainMapPresence) -> Self {
self.gain_map = gain_map;
self
}
pub fn with_resolution(mut self, resolution: Resolution) -> Self {
self.resolution = Some(resolution);
self
}
pub fn is_animation(&self) -> bool {
self.sequence.is_animation()
}
pub fn is_multi_image(&self) -> bool {
self.sequence.is_multi()
}
pub fn has_additional_images(&self) -> bool {
!matches!(self.sequence, ImageSequence::Single)
}
pub fn frame_count(&self) -> Option<u32> {
self.sequence.count()
}
pub fn with_bit_depth(mut self, bit_depth: u8) -> Self {
self.source_color.bit_depth = Some(bit_depth);
self
}
pub fn with_channel_count(mut self, channel_count: u8) -> Self {
self.source_color.channel_count = Some(channel_count);
self
}
pub fn with_cicp(mut self, cicp: Cicp) -> Self {
self.source_color.cicp = Some(cicp);
self
}
pub fn with_content_light_level(mut self, clli: ContentLightLevel) -> Self {
self.source_color.content_light_level = Some(clli);
self
}
pub fn with_mastering_display(mut self, mdcv: MasteringDisplay) -> Self {
self.source_color.mastering_display = Some(mdcv);
self
}
pub fn with_icc_profile(mut self, icc: impl Into<Arc<[u8]>>) -> Self {
self.source_color.icc_profile = Some(icc.into());
self
}
pub fn with_color_authority(mut self, authority: ColorAuthority) -> Self {
self.source_color.color_authority = authority;
self
}
pub fn with_exif(mut self, exif: impl Into<Arc<[u8]>>) -> Self {
self.embedded_metadata.exif = Some(exif.into());
self
}
pub fn with_xmp(mut self, xmp: impl Into<Arc<[u8]>>) -> Self {
self.embedded_metadata.xmp = Some(xmp.into());
self
}
pub fn with_orientation(mut self, orientation: Orientation) -> Self {
self.orientation = orientation;
self
}
pub fn with_source_color(mut self, source_color: SourceColor) -> Self {
self.source_color = source_color;
self
}
pub fn with_embedded_metadata(mut self, embedded_metadata: EmbeddedMetadata) -> Self {
self.embedded_metadata = embedded_metadata;
self
}
pub fn with_source_encoding_details<T: SourceEncodingDetails + 'static>(
mut self,
details: T,
) -> Self {
self.source_encoding = Some(Arc::new(details));
self
}
pub fn source_encoding_details(&self) -> Option<&dyn SourceEncodingDetails> {
self.source_encoding.as_deref()
}
pub fn with_warning(mut self, msg: alloc::string::String) -> Self {
self.warnings.push(msg);
self
}
pub fn with_warnings(mut self, msgs: Vec<alloc::string::String>) -> Self {
self.warnings = msgs;
self
}
pub fn warnings(&self) -> &[alloc::string::String] {
&self.warnings
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn display_width(&self) -> u32 {
if self.orientation.swaps_axes() {
self.height
} else {
self.width
}
}
pub fn display_height(&self) -> u32 {
if self.orientation.swaps_axes() {
self.width
} else {
self.height
}
}
pub fn transfer_function(&self) -> TransferFunction {
self.source_color.transfer_function()
}
pub fn color_primaries(&self) -> ColorPrimaries {
self.source_color.color_primaries()
}
pub fn metadata(&self) -> Metadata {
Metadata::from(self)
}
}
impl core::fmt::Debug for ImageInfo {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut s = f.debug_struct("ImageInfo");
s.field("width", &self.width)
.field("height", &self.height)
.field("format", &self.format)
.field("has_alpha", &self.has_alpha)
.field("is_progressive", &self.is_progressive)
.field("sequence", &self.sequence)
.field("supplements", &self.supplements)
.field("gain_map", &self.gain_map)
.field("orientation", &self.orientation)
.field("source_color", &self.source_color)
.field("embedded_metadata", &self.embedded_metadata);
if self.source_encoding.is_some() {
s.field("source_encoding", &"Some(...)");
}
if !self.warnings.is_empty() {
s.field("warnings", &self.warnings);
}
s.finish()
}
}
impl PartialEq for ImageInfo {
fn eq(&self, other: &Self) -> bool {
self.width == other.width
&& self.height == other.height
&& self.format == other.format
&& self.has_alpha == other.has_alpha
&& self.is_progressive == other.is_progressive
&& self.sequence == other.sequence
&& self.supplements == other.supplements
&& self.gain_map == other.gain_map
&& self.orientation == other.orientation
&& self.source_color == other.source_color
&& self.resolution == other.resolution
&& self.embedded_metadata == other.embedded_metadata
&& self.warnings == other.warnings
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_dimensions_normal() {
let info = ImageInfo::new(100, 200, ImageFormat::Jpeg);
assert_eq!(info.display_width(), 100);
assert_eq!(info.display_height(), 200);
}
#[test]
fn display_dimensions_rotated() {
let info =
ImageInfo::new(100, 200, ImageFormat::Jpeg).with_orientation(Orientation::Rotate90);
assert_eq!(info.display_width(), 200);
assert_eq!(info.display_height(), 100);
}
#[test]
fn display_dimensions_rotate180() {
let info =
ImageInfo::new(100, 200, ImageFormat::Jpeg).with_orientation(Orientation::Rotate180);
assert_eq!(info.display_width(), 100);
assert_eq!(info.display_height(), 200);
}
#[test]
fn display_dimensions_all_orientations() {
let info = ImageInfo::new(100, 200, ImageFormat::Jpeg);
for orient in [
Orientation::Identity,
Orientation::FlipH,
Orientation::Rotate180,
Orientation::FlipV,
] {
let i = info.clone().with_orientation(orient);
assert_eq!((i.display_width(), i.display_height()), (100, 200));
}
for orient in [
Orientation::Transpose,
Orientation::Rotate90,
Orientation::Transverse,
Orientation::Rotate270,
] {
let i = info.clone().with_orientation(orient);
assert_eq!((i.display_width(), i.display_height()), (200, 100));
}
}
#[test]
fn image_info_builder() {
let info = ImageInfo::new(10, 20, ImageFormat::Png)
.with_alpha(true)
.with_sequence(ImageSequence::Animation {
frame_count: Some(5),
loop_count: None,
random_access: false,
})
.with_icc_profile(alloc::vec![1, 2])
.with_exif(alloc::vec![3, 4])
.with_xmp(alloc::vec![5, 6]);
assert!(info.has_alpha);
assert!(info.is_animation());
assert_eq!(info.frame_count(), Some(5));
assert_eq!(
info.source_color.icc_profile.as_deref(),
Some([1, 2].as_slice())
);
assert_eq!(
info.embedded_metadata.exif.as_deref(),
Some([3, 4].as_slice())
);
assert_eq!(
info.embedded_metadata.xmp.as_deref(),
Some([5, 6].as_slice())
);
}
#[test]
fn image_info_eq() {
let a = ImageInfo::new(10, 20, ImageFormat::Png).with_alpha(true);
let b = ImageInfo::new(10, 20, ImageFormat::Png).with_alpha(true);
assert_eq!(a, b);
let c = ImageInfo::new(10, 20, ImageFormat::Jpeg).with_alpha(true);
assert_ne!(a, c);
}
#[test]
fn cicp_constants() {
assert_eq!(Cicp::SRGB.color_primaries, 1);
assert_eq!(Cicp::SRGB.transfer_characteristics, 13);
assert_eq!(Cicp::BT2100_PQ.transfer_characteristics, 16);
assert_eq!(Cicp::BT2100_HLG.transfer_characteristics, 18);
const { assert!(Cicp::SRGB.full_range) };
}
#[test]
fn image_info_bit_depth_channels() {
let info = ImageInfo::new(100, 100, ImageFormat::Avif)
.with_bit_depth(10)
.with_channel_count(4)
.with_alpha(true);
assert_eq!(info.source_color.bit_depth, Some(10));
assert_eq!(info.source_color.channel_count, Some(4));
}
#[test]
fn image_info_hdr_metadata() {
let clli = ContentLightLevel::new(4000, 1000);
let mdcv = MasteringDisplay::new(
[[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]],
[0.3127, 0.3290],
4000.0,
0.005,
);
let info = ImageInfo::new(3840, 2160, ImageFormat::Avif)
.with_cicp(Cicp::BT2100_PQ)
.with_content_light_level(clli)
.with_mastering_display(mdcv);
assert_eq!(info.source_color.cicp, Some(Cicp::BT2100_PQ));
assert_eq!(
info.source_color
.content_light_level
.unwrap()
.max_content_light_level,
4000
);
assert_eq!(
info.source_color.mastering_display.unwrap().max_luminance,
4000.0
);
}
#[test]
fn transfer_function_from_cicp() {
use TransferFunction;
let info = ImageInfo::new(100, 100, ImageFormat::Avif).with_cicp(Cicp::SRGB);
assert_eq!(info.transfer_function(), TransferFunction::Srgb);
let info = ImageInfo::new(100, 100, ImageFormat::Avif).with_cicp(Cicp::BT2100_PQ);
assert_eq!(info.transfer_function(), TransferFunction::Pq);
let info = ImageInfo::new(100, 100, ImageFormat::Avif).with_cicp(Cicp::BT2100_HLG);
assert_eq!(info.transfer_function(), TransferFunction::Hlg);
}
#[test]
fn transfer_function_without_cicp() {
use TransferFunction;
let info = ImageInfo::new(100, 100, ImageFormat::Jpeg);
assert_eq!(info.transfer_function(), TransferFunction::Unknown);
}
#[test]
fn transfer_function_unrecognized_cicp() {
use TransferFunction;
let info = ImageInfo::new(100, 100, ImageFormat::Avif).with_cicp(Cicp::new(1, 99, 0, true));
assert_eq!(info.transfer_function(), TransferFunction::Unknown);
}
#[test]
fn cicp_display_srgb() {
let s = alloc::format!("{}", Cicp::SRGB);
assert_eq!(s, "BT.709/sRGB / sRGB / Identity/RGB (full range)");
}
#[test]
fn cicp_display_bt2100_pq() {
let s = alloc::format!("{}", Cicp::BT2100_PQ);
assert_eq!(s, "BT.2020 / PQ (HDR) / BT.2020 NCL (full range)");
}
#[test]
fn cicp_display_limited_range() {
let cicp = Cicp::new(1, 1, 1, false);
let s = alloc::format!("{}", cicp);
assert_eq!(s, "BT.709/sRGB / BT.709 / BT.709 (limited range)");
}
#[test]
fn cicp_name_helpers() {
assert_eq!(Cicp::color_primaries_name(1), "BT.709/sRGB");
assert_eq!(Cicp::color_primaries_name(12), "Display P3");
assert_eq!(Cicp::color_primaries_name(255), "Unknown");
assert_eq!(Cicp::transfer_characteristics_name(13), "sRGB");
assert_eq!(Cicp::transfer_characteristics_name(16), "PQ (HDR)");
assert_eq!(Cicp::transfer_characteristics_name(18), "HLG (HDR)");
assert_eq!(Cicp::matrix_coefficients_name(0), "Identity/RGB");
assert_eq!(Cicp::matrix_coefficients_name(6), "BT.601");
assert_eq!(Cicp::matrix_coefficients_name(9), "BT.2020 NCL");
}
use crate::icc::tests::build_icc_with_cicp;
fn build_icc_no_cicp() -> 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"desc");
data[136..140].copy_from_slice(&144u32.to_be_bytes());
data[140..144].copy_from_slice(&12u32.to_be_bytes());
data
}
#[test]
fn source_color_default_is_icc_authority() {
let sc = SourceColor::default();
assert_eq!(sc.color_authority, ColorAuthority::Icc);
assert!(sc.cicp.is_none());
assert!(sc.icc_profile.is_none());
}
#[test]
fn source_color_with_color_authority() {
let sc = SourceColor::default().with_color_authority(ColorAuthority::Cicp);
assert_eq!(sc.color_authority, ColorAuthority::Cicp);
}
#[test]
fn image_info_with_color_authority() {
let info =
ImageInfo::new(1, 1, ImageFormat::Png).with_color_authority(ColorAuthority::Cicp);
assert_eq!(info.source_color.color_authority, ColorAuthority::Cicp);
}
#[test]
fn has_hdr_transfer_cicp_pq() {
let sc = SourceColor::default().with_cicp(Cicp::BT2100_PQ);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_hlg() {
let sc = SourceColor::default().with_cicp(Cicp::BT2100_HLG);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_srgb_is_false() {
let sc = SourceColor::default().with_cicp(Cicp::SRGB);
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_p3_is_false() {
let sc = SourceColor::default().with_cicp(Cicp::DISPLAY_P3);
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_bt709_is_false() {
let sc = SourceColor::default().with_cicp(Cicp::new(1, 1, 0, true));
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_linear_is_false() {
let sc = SourceColor::default().with_cicp(Cicp::new(1, 8, 0, true));
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_icc_pq_tag() {
let icc = build_icc_with_cicp(9, 16, 0, true);
let sc = SourceColor::default().with_icc_profile(icc);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_icc_hlg_tag() {
let icc = build_icc_with_cicp(9, 18, 0, false);
let sc = SourceColor::default().with_icc_profile(icc);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_icc_srgb_tag_is_false() {
let icc = build_icc_with_cicp(1, 13, 0, true);
let sc = SourceColor::default().with_icc_profile(icc);
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_icc_no_cicp_tag_is_false() {
let icc = build_icc_no_cicp();
let sc = SourceColor::default().with_icc_profile(icc);
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_wins_over_icc() {
let icc = build_icc_with_cicp(1, 13, 0, true);
let sc = SourceColor::default()
.with_cicp(Cicp::BT2100_PQ)
.with_icc_profile(icc);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_sdr_but_icc_hdr_still_detects() {
let icc = build_icc_with_cicp(9, 16, 0, true);
let sc = SourceColor::default()
.with_cicp(Cicp::SRGB)
.with_icc_profile(icc);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_cicp_pq_short_circuits_before_icc() {
let sc = SourceColor::default()
.with_cicp(Cicp::BT2100_PQ)
.with_icc_profile(alloc::vec![0xFF; 10]);
assert!(sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_no_metadata_is_false() {
let sc = SourceColor::default();
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_empty_icc_is_false() {
let sc = SourceColor::default().with_icc_profile(alloc::vec![]);
assert!(!sc.has_hdr_transfer());
}
#[test]
fn has_hdr_transfer_garbage_icc_is_false() {
let sc = SourceColor::default().with_icc_profile(alloc::vec![0xFF; 200]);
assert!(!sc.has_hdr_transfer());
}
#[test]
fn source_color_transfer_function() {
let sc = SourceColor::default().with_cicp(Cicp::SRGB);
assert_eq!(sc.transfer_function(), TransferFunction::Srgb);
let sc = SourceColor::default().with_cicp(Cicp::BT2100_PQ);
assert_eq!(sc.transfer_function(), TransferFunction::Pq);
let sc = SourceColor::default().with_cicp(Cicp::BT2100_HLG);
assert_eq!(sc.transfer_function(), TransferFunction::Hlg);
let sc = SourceColor::default();
assert_eq!(sc.transfer_function(), TransferFunction::Unknown);
}
#[test]
fn source_color_primaries() {
let sc = SourceColor::default().with_cicp(Cicp::SRGB);
assert_eq!(sc.color_primaries(), ColorPrimaries::Bt709);
let sc = SourceColor::default().with_cicp(Cicp::DISPLAY_P3);
assert_eq!(sc.color_primaries(), ColorPrimaries::DisplayP3);
let sc = SourceColor::default();
assert_eq!(sc.color_primaries(), ColorPrimaries::Bt709);
}
#[test]
fn source_color_with_icc_accepts_vec() {
let sc = SourceColor::default().with_icc_profile(alloc::vec![1, 2, 3]);
assert_eq!(sc.icc_profile.as_deref(), Some(&[1, 2, 3][..]));
}
#[test]
fn source_color_with_icc_accepts_arc() {
let arc: Arc<[u8]> = Arc::from(&[4, 5, 6][..]);
let sc = SourceColor::default().with_icc_profile(arc.clone());
assert_eq!(sc.icc_profile, Some(arc));
}
#[test]
fn source_color_hdr_metadata_fields() {
let clli = ContentLightLevel::new(1000, 400);
let mdcv = MasteringDisplay::new(
[[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]],
[0.3127, 0.3290],
1000.0,
0.005,
);
let sc = SourceColor::default()
.with_content_light_level(clli)
.with_mastering_display(mdcv);
assert_eq!(
sc.content_light_level.unwrap().max_content_light_level,
1000
);
assert!(sc.mastering_display.is_some());
}
#[test]
fn source_color_bit_depth_channel_count() {
let sc = SourceColor::default()
.with_bit_depth(10)
.with_channel_count(4);
assert_eq!(sc.bit_depth, Some(10));
assert_eq!(sc.channel_count, Some(4));
}
#[test]
fn spec_jpeg_icc_only() {
let icc = alloc::vec![0u8; 128]; let sc = SourceColor::default()
.with_icc_profile(icc)
.with_color_authority(ColorAuthority::Icc);
assert_eq!(sc.color_authority, ColorAuthority::Icc);
assert!(sc.icc_profile.is_some());
assert!(sc.cicp.is_none());
}
#[test]
fn spec_avif_icc_colr_box() {
let icc = alloc::vec![0u8; 128];
let sc = SourceColor::default()
.with_icc_profile(icc)
.with_cicp(Cicp::BT2100_PQ)
.with_color_authority(ColorAuthority::Icc);
assert_eq!(sc.color_authority, ColorAuthority::Icc);
assert!(sc.icc_profile.is_some());
assert!(sc.cicp.is_some()); }
#[test]
fn spec_avif_nclx_only() {
let sc = SourceColor::default()
.with_cicp(Cicp::BT2100_PQ)
.with_color_authority(ColorAuthority::Cicp);
assert_eq!(sc.color_authority, ColorAuthority::Cicp);
assert!(sc.cicp.is_some());
assert!(sc.icc_profile.is_none());
}
#[test]
fn spec_png_cicp_chunk() {
let icc = alloc::vec![0u8; 128]; let sc = SourceColor::default()
.with_cicp(Cicp::SRGB)
.with_icc_profile(icc)
.with_color_authority(ColorAuthority::Cicp);
assert_eq!(sc.color_authority, ColorAuthority::Cicp);
}
#[test]
fn spec_png_iccp_only() {
let icc = alloc::vec![0u8; 128];
let sc = SourceColor::default()
.with_icc_profile(icc)
.with_color_authority(ColorAuthority::Icc);
assert_eq!(sc.color_authority, ColorAuthority::Icc);
assert!(sc.cicp.is_none());
}
#[test]
fn spec_jxl_enum_encoding() {
let sc = SourceColor::default()
.with_cicp(Cicp::SRGB)
.with_color_authority(ColorAuthority::Cicp);
assert_eq!(sc.color_authority, ColorAuthority::Cicp);
}
#[test]
fn spec_jxl_embedded_icc() {
let icc = alloc::vec![0u8; 128];
let sc = SourceColor::default()
.with_icc_profile(icc)
.with_color_authority(ColorAuthority::Icc);
assert_eq!(sc.color_authority, ColorAuthority::Icc);
}
#[test]
fn mismatch_icc_authority_no_icc_profile() {
let sc = SourceColor::default()
.with_cicp(Cicp::BT2100_PQ)
.with_color_authority(ColorAuthority::Icc);
assert!(sc.has_hdr_transfer());
assert!(sc.icc_profile.is_none()); }
#[test]
fn mismatch_cicp_authority_no_cicp() {
let icc_pq = build_icc_with_cicp(9, 16, 0, true);
let sc = SourceColor::default()
.with_icc_profile(icc_pq)
.with_color_authority(ColorAuthority::Cicp);
assert!(sc.has_hdr_transfer());
assert!(sc.cicp.is_none()); }
#[test]
fn to_color_context_cicp_authoritative_drops_icc() {
let sc = SourceColor::default()
.with_icc_profile(alloc::vec![1, 2, 3])
.with_cicp(Cicp::DISPLAY_P3)
.with_color_authority(ColorAuthority::Cicp);
let ctx = sc.to_color_context();
assert!(
ctx.icc.is_none(),
"ICC should be dropped when CICP is authoritative"
);
assert_eq!(ctx.cicp, Some(Cicp::DISPLAY_P3));
}
#[test]
fn to_color_context_icc_authoritative_drops_cicp() {
let icc = alloc::vec![10, 20, 30];
let sc = SourceColor::default()
.with_icc_profile(icc.clone())
.with_cicp(Cicp::SRGB)
.with_color_authority(ColorAuthority::Icc);
let ctx = sc.to_color_context();
assert!(
ctx.cicp.is_none(),
"CICP should be dropped when ICC is authoritative"
);
assert_eq!(ctx.icc.as_deref(), Some(icc.as_slice()));
}
#[test]
fn to_color_context_cicp_authoritative_no_cicp_keeps_icc_fallback() {
let icc = alloc::vec![10, 20, 30];
let sc = SourceColor::default()
.with_icc_profile(icc.clone())
.with_color_authority(ColorAuthority::Cicp);
let ctx = sc.to_color_context();
assert_eq!(ctx.icc.as_deref(), Some(icc.as_slice()));
assert!(ctx.cicp.is_none());
}
#[test]
fn to_color_context_icc_authoritative_no_icc_keeps_cicp_fallback() {
let sc = SourceColor::default()
.with_cicp(Cicp::BT2100_PQ)
.with_color_authority(ColorAuthority::Icc);
let ctx = sc.to_color_context();
assert!(ctx.icc.is_none());
assert_eq!(ctx.cicp, Some(Cicp::BT2100_PQ));
}
#[test]
fn to_color_context_neither_field_present() {
let sc = SourceColor::default();
let ctx = sc.to_color_context();
assert!(ctx.icc.is_none());
assert!(ctx.cicp.is_none());
}
#[test]
fn to_color_context_as_profile_source_returns_authoritative() {
let sc = SourceColor::default()
.with_icc_profile(alloc::vec![1, 2, 3])
.with_cicp(Cicp::SRGB)
.with_color_authority(ColorAuthority::Cicp);
let ctx = sc.to_color_context();
let src = ctx.as_profile_source().unwrap();
assert!(
matches!(src, zenpixels::ColorProfileSource::Cicp(_)),
"CICP authoritative should produce Cicp source, got {src:?}"
);
let icc = alloc::vec![10, 20, 30];
let sc = SourceColor::default()
.with_icc_profile(icc)
.with_cicp(Cicp::SRGB)
.with_color_authority(ColorAuthority::Icc);
let ctx = sc.to_color_context();
let src = ctx.as_profile_source().unwrap();
assert!(
matches!(src, zenpixels::ColorProfileSource::Icc(_)),
"ICC authoritative should produce Icc source, got {src:?}"
);
}
}