use drmkit_display::color::{Chromaticity, ColorimetryInfo};
use drmkit_display::{Colorspace, ConnectorCapabilities, HdrSourceMetadata, TransferFunction};
use crate::display::DisplayParams;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
#[non_exhaustive]
pub enum ColorPrimaries {
#[default]
Bt709,
AdobeRgb,
DciP3,
Bt2020,
}
impl ColorPrimaries {
#[must_use]
pub const fn is_wider_than(self, other: Self) -> bool {
(self as u8) > (other as u8)
}
#[must_use]
pub const fn colorimetry(self) -> ColorimetryInfo {
let (red, green, blue) = match self {
Self::Bt709 => ((0.640, 0.330), (0.300, 0.600), (0.150, 0.060)),
Self::Bt2020 => ((0.708, 0.292), (0.170, 0.797), (0.131, 0.046)),
Self::DciP3 => ((0.680, 0.320), (0.265, 0.690), (0.150, 0.060)),
Self::AdobeRgb => ((0.640, 0.330), (0.210, 0.710), (0.150, 0.060)),
};
ColorimetryInfo {
red: Chromaticity { x: red.0, y: red.1 },
green: Chromaticity {
x: green.0,
y: green.1,
},
blue: Chromaticity {
x: blue.0,
y: blue.1,
},
white: Chromaticity {
x: 0.3127,
y: 0.3290,
},
has_primaries: true,
has_default_white: true,
}
}
#[must_use]
pub const fn colorspace(self) -> Colorspace {
match self {
Self::Bt709 => Colorspace::Default,
Self::Bt2020 => Colorspace::Bt2020Rgb,
Self::DciP3 => Colorspace::DciP3RgbD65,
Self::AdobeRgb => Colorspace::OpRgb,
}
}
}
#[deprecated(since = "0.1.2", note = "renamed to `OutputSignaling`")]
pub type OutputSignalling = OutputSignaling;
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct OutputSignaling {
pub colorspace: Option<Colorspace>,
pub hdr_metadata: Option<HdrSourceMetadata>,
pub hdr_downgraded: bool,
}
#[must_use]
pub fn widest_gamut<'a>(
layers: impl IntoIterator<Item = &'a DisplayParams>,
) -> Option<ColorPrimaries> {
layers
.into_iter()
.filter_map(|layer| layer.color_primaries)
.max()
}
const fn is_hdr(eotf: TransferFunction) -> bool {
matches!(
eotf,
TransferFunction::SmpteSt2084Pq | TransferFunction::Bt2100Hlg
)
}
#[must_use]
pub fn derive_output_signaling(
layers: &[DisplayParams],
capabilities: Option<&ConnectorCapabilities>,
) -> OutputSignaling {
let gamut = widest_gamut(layers);
let eotf = layers
.iter()
.filter_map(|layer| layer.source_eotf)
.filter(|eotf| is_hdr(*eotf))
.min_by_key(|eotf| match eotf {
TransferFunction::SmpteSt2084Pq => 0_u8,
_ => 1,
});
let mut signaling = OutputSignaling {
colorspace: gamut.map(ColorPrimaries::colorspace),
hdr_metadata: eotf.map(|eotf| HdrSourceMetadata {
eotf,
display_primaries: gamut.unwrap_or(ColorPrimaries::Bt2020).colorimetry(),
max_display_mastering_luminance: 0,
min_display_mastering_luminance: 0,
max_content_light_level: 0,
max_frame_average_light_level: 0,
}),
hdr_downgraded: false,
};
if signaling.hdr_metadata.is_some() && capabilities.is_some_and(|caps| !caps.can_signal_hdr()) {
signaling.hdr_metadata = None;
signaling.hdr_downgraded = true;
}
signaling
}
#[deprecated(since = "0.1.2", note = "renamed to `derive_output_signaling`")]
#[must_use]
pub fn derive_output_signalling(
layers: &[DisplayParams],
capabilities: Option<&ConnectorCapabilities>,
) -> OutputSignaling {
derive_output_signaling(layers, capabilities)
}
#[cfg(test)]
mod tests {
use super::{ColorPrimaries, derive_output_signaling, widest_gamut};
use crate::display::DisplayParams;
use drmkit_display::{Colorspace, ConnectorCapabilities, MaxBpc, TransferFunction};
fn plain() -> DisplayParams {
DisplayParams::default()
}
fn layer(primaries: Option<ColorPrimaries>, eotf: Option<TransferFunction>) -> DisplayParams {
DisplayParams {
color_primaries: primaries,
source_eotf: eotf,
..DisplayParams::default()
}
}
fn hdr_capable() -> ConnectorCapabilities {
ConnectorCapabilities::new(1)
.with_hdr_output_metadata(true)
.with_max_bpc(MaxBpc {
min: 8,
max: 10,
current: 10,
})
}
#[test]
fn bt709_is_canonical_srgb() {
let c = ColorPrimaries::Bt709.colorimetry();
assert!((c.red.x - 0.640).abs() < 1e-6 && (c.red.y - 0.330).abs() < 1e-6);
assert!((c.green.x - 0.300).abs() < 1e-6 && (c.green.y - 0.600).abs() < 1e-6);
assert!((c.blue.x - 0.150).abs() < 1e-6 && (c.blue.y - 0.060).abs() < 1e-6);
assert!((c.white.x - 0.3127).abs() < 1e-6 && (c.white.y - 0.3290).abs() < 1e-6);
assert!(c.has_primaries && c.has_default_white);
}
#[test]
fn bt2020_matches_rec2100() {
let c = ColorPrimaries::Bt2020.colorimetry();
assert!((c.red.x - 0.708).abs() < 1e-6);
assert!((c.green.y - 0.797).abs() < 1e-6);
assert!((c.blue.x - 0.131).abs() < 1e-6);
}
#[test]
fn dci_p3_d65_matches_smpte_eg432() {
let c = ColorPrimaries::DciP3.colorimetry();
assert!((c.red.x - 0.680).abs() < 1e-6);
assert!((c.green.x - 0.265).abs() < 1e-6);
assert!((c.white.x - 0.3127).abs() < 1e-6);
}
#[test]
fn adobe_rgb_differs_from_bt709_only_in_green() {
let adobe = ColorPrimaries::AdobeRgb.colorimetry();
let bt709 = ColorPrimaries::Bt709.colorimetry();
assert_eq!(adobe.red, bt709.red);
assert_eq!(adobe.blue, bt709.blue);
assert_ne!(adobe.green, bt709.green);
assert!((adobe.green.y - 0.710).abs() < 1e-6);
}
#[test]
fn no_layers_name_no_gamut() {
assert_eq!(widest_gamut(&[] as &[DisplayParams]), None);
assert_eq!(widest_gamut(&[plain(), plain()]), None);
}
#[test]
fn one_layer_passes_its_own_through() {
let layers = [layer(Some(ColorPrimaries::DciP3), None)];
assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::DciP3));
}
#[test]
fn the_ranking_runs_bt2020_over_p3_over_adobe_over_bt709() {
for (wider, narrower) in [
(ColorPrimaries::Bt2020, ColorPrimaries::DciP3),
(ColorPrimaries::DciP3, ColorPrimaries::AdobeRgb),
(ColorPrimaries::AdobeRgb, ColorPrimaries::Bt709),
] {
assert!(wider.is_wider_than(narrower), "{wider:?} over {narrower:?}");
assert_eq!(
widest_gamut(&[layer(Some(narrower), None), layer(Some(wider), None)]),
Some(wider)
);
assert_eq!(
widest_gamut(&[layer(Some(wider), None), layer(Some(narrower), None)]),
Some(wider)
);
}
}
#[test]
fn a_mixed_set_selects_the_widest() {
let layers = [
layer(Some(ColorPrimaries::Bt709), None),
plain(),
layer(Some(ColorPrimaries::Bt2020), None),
layer(Some(ColorPrimaries::AdobeRgb), None),
];
assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::Bt2020));
}
#[test]
fn layers_that_say_nothing_are_passed_over_not_counted_as_bt709() {
let layers = [plain(), layer(Some(ColorPrimaries::DciP3), None), plain()];
assert_eq!(widest_gamut(&layers), Some(ColorPrimaries::DciP3));
}
#[test]
fn bt709_maps_to_default_rather_than_bt709_ycc() {
assert_eq!(ColorPrimaries::Bt709.colorspace(), Colorspace::Default);
}
#[test]
fn the_other_three_map_to_their_own_entries() {
assert_eq!(ColorPrimaries::Bt2020.colorspace(), Colorspace::Bt2020Rgb);
assert_eq!(ColorPrimaries::DciP3.colorspace(), Colorspace::DciP3RgbD65);
assert_eq!(ColorPrimaries::AdobeRgb.colorspace(), Colorspace::OpRgb);
}
#[test]
fn no_layers_signal_nothing() {
let signaling = derive_output_signaling(&[], None);
assert_eq!(signaling.colorspace, None);
assert_eq!(signaling.hdr_metadata, None);
assert!(!signaling.hdr_downgraded);
}
#[test]
fn an_all_sdr_frame_that_names_nothing_leaves_the_connector_alone() {
let signaling = derive_output_signaling(&[plain(), plain()], None);
assert_eq!(signaling.colorspace, None);
assert_eq!(signaling.hdr_metadata, None);
}
#[test]
fn an_sdr_bt709_layer_asks_for_default_and_no_hdr() {
let layers = [layer(Some(ColorPrimaries::Bt709), None)];
let signaling = derive_output_signaling(&layers, None);
assert_eq!(signaling.colorspace, Some(Colorspace::Default));
assert_eq!(signaling.hdr_metadata, None);
}
#[test]
fn a_bt2020_pq_layer_produces_hdr_signaling() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
)];
let signaling = derive_output_signaling(&layers, None);
assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
let hdr = signaling.hdr_metadata.expect("hdr");
assert_eq!(hdr.eotf, TransferFunction::SmpteSt2084Pq);
assert_eq!(
hdr.display_primaries,
ColorPrimaries::Bt2020.colorimetry(),
"the mastering primaries come from the widest-gamut layer"
);
assert_eq!(hdr.max_display_mastering_luminance, 0);
assert_eq!(hdr.max_content_light_level, 0);
}
#[test]
fn one_hdr_layer_among_sdr_ones_carries_the_frame() {
let layers = [
layer(Some(ColorPrimaries::Bt709), None),
layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
),
];
let signaling = derive_output_signaling(&layers, None);
assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
assert!(signaling.hdr_metadata.is_some());
}
#[test]
fn pq_wins_when_both_hdr_curves_are_present() {
for order in [[0, 1], [1, 0]] {
let both = [
layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
),
layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::Bt2100Hlg),
),
];
let layers = [both[order[0]], both[order[1]]];
let hdr = derive_output_signaling(&layers, None)
.hdr_metadata
.expect("hdr");
assert_eq!(hdr.eotf, TransferFunction::SmpteSt2084Pq);
}
}
#[test]
fn an_hlg_only_frame_signals_hlg() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::Bt2100Hlg),
)];
let hdr = derive_output_signaling(&layers, None)
.hdr_metadata
.expect("hdr");
assert_eq!(hdr.eotf, TransferFunction::Bt2100Hlg);
}
#[test]
fn an_hdr_layer_that_names_no_gamut_masters_in_bt2020() {
let layers = [layer(None, Some(TransferFunction::SmpteSt2084Pq))];
let signaling = derive_output_signaling(&layers, None);
assert_eq!(signaling.colorspace, None, "no layer named a gamut");
let hdr = signaling.hdr_metadata.expect("hdr");
assert_eq!(hdr.display_primaries, ColorPrimaries::Bt2020.colorimetry());
}
#[test]
fn traditional_gamma_hdr_is_not_hdr_signaling() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::TraditionalGammaHdr),
)];
let signaling = derive_output_signaling(&layers, None);
assert_eq!(signaling.hdr_metadata, None);
assert!(!signaling.hdr_downgraded, "nothing was dropped");
assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
}
#[test]
fn a_capable_connector_keeps_the_metadata() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
)];
let signaling = derive_output_signaling(&layers, Some(&hdr_capable()));
assert!(signaling.hdr_metadata.is_some());
assert!(!signaling.hdr_downgraded);
}
#[test]
fn a_connector_without_the_blob_downgrades() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
)];
let no_blob = ConnectorCapabilities::new(1).with_max_bpc(MaxBpc {
min: 8,
max: 12,
current: 12,
});
let signaling = derive_output_signaling(&layers, Some(&no_blob));
assert_eq!(signaling.hdr_metadata, None);
assert!(signaling.hdr_downgraded);
assert_eq!(signaling.colorspace, Some(Colorspace::Bt2020Rgb));
}
#[test]
fn a_connector_capped_at_eight_bits_downgrades() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
)];
let eight_bit = ConnectorCapabilities::new(1)
.with_hdr_output_metadata(true)
.with_max_bpc(MaxBpc {
min: 6,
max: 8,
current: 8,
});
let signaling = derive_output_signaling(&layers, Some(&eight_bit));
assert_eq!(signaling.hdr_metadata, None);
assert!(signaling.hdr_downgraded);
}
#[test]
fn ten_bits_is_enough() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
)];
assert!(
derive_output_signaling(&layers, Some(&hdr_capable()))
.hdr_metadata
.is_some()
);
}
#[test]
fn an_sdr_frame_is_never_downgraded() {
let layers = [layer(Some(ColorPrimaries::Bt709), None)];
let eight_bit = ConnectorCapabilities::new(1).with_max_bpc(MaxBpc {
min: 6,
max: 8,
current: 8,
});
let signaling = derive_output_signaling(&layers, Some(&eight_bit));
assert!(!signaling.hdr_downgraded);
assert_eq!(signaling.colorspace, Some(Colorspace::Default));
}
#[test]
fn without_capabilities_the_constraint_is_not_checked() {
let layers = [layer(
Some(ColorPrimaries::Bt2020),
Some(TransferFunction::SmpteSt2084Pq),
)];
let signaling = derive_output_signaling(&layers, None);
assert!(signaling.hdr_metadata.is_some());
assert!(!signaling.hdr_downgraded);
}
#[test]
fn the_defaults_name_nothing() {
let params = DisplayParams::default();
assert_eq!(params.color_primaries, None);
assert_eq!(params.source_eotf, None);
}
}