use zenpixels::icc;
use zenpixels::{Cicp, ColorAuthority, ColorModel};
use crate::capabilities::EncodeCapabilities;
use crate::info::SourceColor;
use crate::metadata::IccRetention;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ColorEmitPolicy {
Compatibility,
#[default]
Balanced,
Compact,
Verbatim,
Custom(ColorEmitFields),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum CicpEmission {
#[default]
WhereValidCarrier,
WhereverSupported,
Never,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ColorEmitFields {
pub icc: IccRetention,
pub cicp: CicpEmission,
}
impl Default for ColorEmitFields {
fn default() -> Self {
Self {
icc: IccRetention::DropIfCicpSafeSoleCarrier,
cicp: CicpEmission::WhereValidCarrier,
}
}
}
impl ColorEmitFields {
pub const fn new(icc: IccRetention, cicp: CicpEmission) -> Self {
Self { icc, cicp }
}
}
impl ColorEmitPolicy {
pub const fn fields(&self) -> ColorEmitFields {
match self {
Self::Compatibility => ColorEmitFields {
icc: IccRetention::Keep,
cicp: CicpEmission::WhereValidCarrier,
},
Self::Balanced => ColorEmitFields {
icc: IccRetention::DropIfCicpSafeSoleCarrier,
cicp: CicpEmission::WhereValidCarrier,
},
Self::Compact => ColorEmitFields {
icc: IccRetention::DropIfCicpRepresentable,
cicp: CicpEmission::WhereverSupported,
},
Self::Verbatim => ColorEmitFields {
icc: IccRetention::Keep,
cicp: CicpEmission::WhereValidCarrier,
},
Self::Custom(f) => *f,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum IccDisposition {
KeepSource,
SynthesizeFrom(Cicp),
Drop,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ColorEmitPlan {
pub cicp: Option<Cicp>,
pub icc: IccDisposition,
}
fn representable_cicp(src: &SourceColor) -> Option<Cicp> {
let from_icc = |icc_bytes: &[u8]| -> Option<Cicp> {
icc::extract_cicp(icc_bytes)
.or_else(|| icc::identify_common(icc_bytes).and_then(|id| id.to_cicp()))
};
if src.color_authority == ColorAuthority::Icc
&& let Some(icc_bytes) = src.icc_profile.as_deref()
{
return from_icc(icc_bytes);
}
if let Some(c) = src.cicp {
return Some(c);
}
from_icc(src.icc_profile.as_deref()?)
}
pub fn resolve_color_emit(
src: &SourceColor,
target: &EncodeCapabilities,
policy: ColorEmitPolicy,
) -> ColorEmitPlan {
let fields = policy.fields();
let src_has_icc = src.icc_profile.is_some();
let model = src
.icc_profile
.as_deref()
.and_then(icc::profile_color_space);
let is_gray = matches!(model, Some(ColorModel::Gray)) || src.channel_count == Some(1);
let is_cmyk = matches!(model, Some(ColorModel::Cmyk));
if is_gray || is_cmyk {
return ColorEmitPlan {
cicp: None,
icc: if src_has_icc {
IccDisposition::KeepSource
} else {
IccDisposition::Drop
},
};
}
let repr_cicp = representable_cicp(src);
let cicp_represents = repr_cicp.is_some();
let has_carrier = target.cicp();
let is_valid_carrier = target.cicp_is_valid_carrier();
let sole_safe = target.cicp_safe_sole_carrier();
let icc_is_srgb = src.icc_profile.as_deref().is_some_and(icc::is_common_srgb);
let emit_cicp = match policy {
ColorEmitPolicy::Verbatim => has_carrier && src.cicp.is_some(),
_ => match fields.cicp {
CicpEmission::Never => false,
CicpEmission::WhereValidCarrier => has_carrier && is_valid_carrier && cicp_represents,
CicpEmission::WhereverSupported => has_carrier && cicp_represents,
},
};
let cicp_out = if emit_cicp {
if policy == ColorEmitPolicy::Verbatim {
src.cicp
} else {
repr_cicp
}
} else {
None
};
let drop_by_rule = match fields.icc {
IccRetention::Drop => true,
IccRetention::Keep => false,
IccRetention::KeepNonSrgb => icc_is_srgb,
IccRetention::DropIfCicpRepresentable => emit_cicp && cicp_represents,
IccRetention::DropIfCicpSafeSoleCarrier => emit_cicp && sole_safe && cicp_represents,
};
let drop_icc = match policy {
ColorEmitPolicy::Balanced => drop_by_rule || (emit_cicp && icc_is_srgb),
_ => drop_by_rule,
};
let is_assumed_default = |c: &Cicp| {
matches!(c.color_primaries, 1 | 2) && matches!(c.transfer_characteristics, 1 | 2 | 13)
};
let synth_worthwhile = repr_cicp.is_some_and(|c| !is_assumed_default(&c));
let icc = if src_has_icc {
if drop_icc {
IccDisposition::Drop
} else {
IccDisposition::KeepSource
}
} else if !emit_cicp && synth_worthwhile && policy != ColorEmitPolicy::Verbatim {
IccDisposition::SynthesizeFrom(repr_cicp.expect("synth_worthwhile"))
} else if synth_worthwhile
&& (matches!(policy, ColorEmitPolicy::Compatibility)
|| (matches!(policy, ColorEmitPolicy::Balanced) && !sole_safe))
{
IccDisposition::SynthesizeFrom(repr_cicp.expect("synth_worthwhile"))
} else {
IccDisposition::Drop
};
ColorEmitPlan {
cicp: cicp_out,
icc,
}
}
#[cfg(test)]
mod tests {
use super::*;
use zenpixels::ColorAuthority;
fn caps_jxl() -> EncodeCapabilities {
EncodeCapabilities::new()
.with_icc(true)
.with_cicp(true)
.with_cicp_is_valid_carrier(true)
.with_cicp_safe_sole_carrier(true)
}
fn caps_avif() -> EncodeCapabilities {
EncodeCapabilities::new()
.with_icc(true)
.with_cicp(true)
.with_cicp_is_valid_carrier(true)
.with_cicp_safe_sole_carrier(true)
}
fn caps_jpeg() -> EncodeCapabilities {
EncodeCapabilities::new().with_icc(true)
}
fn caps_png() -> EncodeCapabilities {
EncodeCapabilities::new()
.with_icc(true)
.with_cicp(true)
.with_cicp_is_valid_carrier(true)
.with_cicp_safe_sole_carrier(false)
}
fn src_cicp(c: Cicp) -> SourceColor {
SourceColor::default()
.with_cicp(c)
.with_color_authority(ColorAuthority::Cicp)
.with_channel_count(3)
}
#[test]
fn jxl_balanced_strips_representable_icc() {
let src = SourceColor::default()
.with_cicp(Cicp::SRGB)
.with_icc_profile(alloc::vec![0u8; 132])
.with_color_authority(ColorAuthority::Cicp)
.with_channel_count(3);
let plan = resolve_color_emit(&src, &caps_jxl(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, Some(Cicp::SRGB));
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn authoritative_icc_not_dropped_for_roundtrip_cicp() {
let src = SourceColor::default()
.with_cicp(Cicp::SRGB)
.with_icc_profile(alloc::vec![0u8; 132]) .with_color_authority(ColorAuthority::Icc)
.with_channel_count(3);
let plan = resolve_color_emit(&src, &caps_jxl(), ColorEmitPolicy::Balanced);
assert_eq!(
plan.icc,
IccDisposition::KeepSource,
"authoritative ICC must survive"
);
assert_eq!(
plan.cicp, None,
"no CICP can represent the unidentifiable authoritative ICC"
);
}
#[test]
fn avif_balanced_drops_redundant_icc_now_sole_safe() {
let p3 = src_cicp(Cicp::DISPLAY_P3).with_icc_profile(alloc::vec![0u8; 132]);
let plan = resolve_color_emit(&p3, &caps_avif(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn jpeg_synthesizes_icc_from_cicp() {
let src = src_cicp(Cicp::DISPLAY_P3);
let plan = resolve_color_emit(&src, &caps_jpeg(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, None);
assert_eq!(plan.icc, IccDisposition::SynthesizeFrom(Cicp::DISPLAY_P3));
}
#[test]
fn compact_strips_icc_on_avif() {
let p3 = src_cicp(Cicp::DISPLAY_P3).with_icc_profile(alloc::vec![0u8; 132]);
let plan = resolve_color_emit(&p3, &caps_avif(), ColorEmitPolicy::Compact);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn compatibility_always_keeps_or_synthesizes_icc() {
let src = src_cicp(Cicp::DISPLAY_P3);
let plan = resolve_color_emit(&src, &caps_avif(), ColorEmitPolicy::Compatibility);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::SynthesizeFrom(Cicp::DISPLAY_P3));
}
#[test]
fn grayscale_keeps_icc_suppresses_cicp() {
let src = SourceColor::default()
.with_icc_profile(alloc::vec![0u8; 132])
.with_channel_count(1);
let plan = resolve_color_emit(&src, &caps_avif(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, None);
assert_eq!(plan.icc, IccDisposition::KeepSource);
}
#[test]
fn verbatim_passes_source_through() {
let src = src_cicp(Cicp::DISPLAY_P3).with_icc_profile(alloc::vec![0u8; 132]);
let plan = resolve_color_emit(&src, &caps_avif(), ColorEmitPolicy::Verbatim);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::KeepSource);
}
#[test]
fn default_policy_is_balanced() {
assert_eq!(ColorEmitPolicy::default(), ColorEmitPolicy::Balanced);
}
#[test]
fn png_emits_cicp_keeps_icc_under_balanced() {
let p3 = src_cicp(Cicp::DISPLAY_P3).with_icc_profile(alloc::vec![0u8; 132]);
let plan = resolve_color_emit(&p3, &caps_png(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::KeepSource);
}
#[test]
fn srgb_only_source_does_not_synthesize_redundant_icc() {
let src = src_cicp(Cicp::SRGB);
let plan = resolve_color_emit(&src, &caps_jpeg(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, None);
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn custom_policy_is_constructible() {
let policy = ColorEmitPolicy::Custom(ColorEmitFields::new(
IccRetention::Keep,
CicpEmission::Never,
));
let p3 = src_cicp(Cicp::DISPLAY_P3).with_icc_profile(alloc::vec![0u8; 132]);
let plan = resolve_color_emit(&p3, &caps_avif(), policy);
assert_eq!(plan.cicp, None); assert_eq!(plan.icc, IccDisposition::KeepSource); }
#[test]
fn png_balanced_synthesizes_icc_companion_for_cicp_only() {
let p3 = src_cicp(Cicp::DISPLAY_P3);
let plan = resolve_color_emit(&p3, &caps_png(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::SynthesizeFrom(Cicp::DISPLAY_P3));
}
#[test]
fn avif_balanced_cicp_only_needs_no_companion_now_sole_safe() {
let p3 = src_cicp(Cicp::DISPLAY_P3);
let plan = resolve_color_emit(&p3, &caps_avif(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn png_compact_stays_cicp_only_for_cicp_only() {
let p3 = src_cicp(Cicp::DISPLAY_P3);
let plan = resolve_color_emit(&p3, &caps_png(), ColorEmitPolicy::Compact);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn srgb_cicp_only_png_balanced_does_not_synthesize() {
let srgb = src_cicp(Cicp::SRGB);
let plan = resolve_color_emit(&srgb, &caps_png(), ColorEmitPolicy::Balanced);
assert_eq!(plan.icc, IccDisposition::Drop);
}
#[test]
fn jxl_balanced_cicp_only_needs_no_companion() {
let p3 = src_cicp(Cicp::DISPLAY_P3);
let plan = resolve_color_emit(&p3, &caps_jxl(), ColorEmitPolicy::Balanced);
assert_eq!(plan.cicp, Some(Cicp::DISPLAY_P3));
assert_eq!(plan.icc, IccDisposition::Drop);
}
}