#![allow(clippy::match_same_arms)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Primaries {
pub red: (f64, f64),
pub green: (f64, f64),
pub blue: (f64, f64),
}
impl Primaries {
#[must_use]
pub const fn new(red: (f64, f64), green: (f64, f64), blue: (f64, f64)) -> Self {
Self { red, green, blue }
}
#[must_use]
pub const fn bt709() -> Self {
Self {
red: (0.64, 0.33),
green: (0.30, 0.60),
blue: (0.15, 0.06),
}
}
#[must_use]
pub const fn bt2020() -> Self {
Self {
red: (0.708, 0.292),
green: (0.170, 0.797),
blue: (0.131, 0.046),
}
}
#[must_use]
pub const fn dci_p3() -> Self {
Self {
red: (0.680, 0.320),
green: (0.265, 0.690),
blue: (0.150, 0.060),
}
}
#[must_use]
pub const fn display_p3() -> Self {
Self::dci_p3()
}
#[must_use]
pub const fn adobe_rgb() -> Self {
Self {
red: (0.64, 0.33),
green: (0.21, 0.71),
blue: (0.15, 0.06),
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
fn in_range(p: (f64, f64)) -> bool {
p.0 >= 0.0 && p.0 <= 1.0 && p.1 >= 0.0 && p.1 <= 1.0
}
in_range(self.red) && in_range(self.green) && in_range(self.blue)
}
#[must_use]
pub fn gamut_area(&self) -> f64 {
let (rx, ry) = self.red;
let (gx, gy) = self.green;
let (bx, by) = self.blue;
0.5 * ((rx * gy - gx * ry) + (gx * by - bx * gy) + (bx * ry - rx * by)).abs()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum WhitePoint {
#[default]
D65,
D50,
Dci,
Custom(f64, f64),
}
impl WhitePoint {
#[must_use]
pub const fn xy(&self) -> (f64, f64) {
match self {
Self::D65 => (0.3127, 0.3290),
Self::D50 => (0.3457, 0.3585),
Self::Dci => (0.314, 0.351),
Self::Custom(x, y) => (*x, *y),
}
}
#[must_use]
pub const fn cct(&self) -> u32 {
match self {
Self::D65 => 6500,
Self::D50 => 5000,
Self::Dci => 5900,
Self::Custom(_, _) => 6500, }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ColorPrimaries {
#[default]
BT709,
BT2020,
DciP3,
DisplayP3,
AdobeRgb,
Bt470M,
Bt470Bg,
Smpte170M,
Smpte240M,
Film,
Bt2100,
Custom,
}
impl ColorPrimaries {
#[must_use]
pub const fn primaries(&self) -> Primaries {
match self {
Self::BT709 => Primaries::bt709(),
Self::BT2020 | Self::Bt2100 => Primaries::bt2020(),
Self::DciP3 | Self::DisplayP3 => Primaries::dci_p3(),
Self::AdobeRgb => Primaries::adobe_rgb(),
Self::Bt470M => Primaries::new((0.67, 0.33), (0.21, 0.71), (0.14, 0.08)),
Self::Bt470Bg | Self::Smpte170M => {
Primaries::new((0.64, 0.33), (0.29, 0.60), (0.15, 0.06))
}
Self::Smpte240M => Primaries::new((0.63, 0.34), (0.31, 0.595), (0.155, 0.070)),
Self::Film => Primaries::new((0.681, 0.319), (0.243, 0.692), (0.145, 0.049)),
Self::Custom => Primaries::bt709(), }
}
#[must_use]
pub const fn white_point(&self) -> WhitePoint {
match self {
Self::BT709
| Self::BT2020
| Self::DisplayP3
| Self::AdobeRgb
| Self::Bt470Bg
| Self::Smpte170M
| Self::Smpte240M
| Self::Bt2100 => WhitePoint::D65,
Self::DciP3 => WhitePoint::Dci,
Self::Bt470M => WhitePoint::Custom(0.310, 0.316), Self::Film => WhitePoint::Custom(0.310, 0.316), Self::Custom => WhitePoint::D65, }
}
#[must_use]
pub const fn is_wide_gamut(&self) -> bool {
matches!(
self,
Self::BT2020 | Self::DciP3 | Self::DisplayP3 | Self::AdobeRgb | Self::Bt2100
)
}
#[must_use]
pub const fn name(&self) -> &str {
match self {
Self::BT709 => "BT.709 / Rec.709",
Self::BT2020 => "BT.2020 / Rec.2020",
Self::DciP3 => "DCI-P3",
Self::DisplayP3 => "Display P3",
Self::AdobeRgb => "Adobe RGB (1998)",
Self::Bt470M => "BT.470 System M",
Self::Bt470Bg => "BT.470 System B, G",
Self::Smpte170M => "SMPTE 170M",
Self::Smpte240M => "SMPTE 240M",
Self::Film => "Film",
Self::Bt2100 => "BT.2100",
Self::Custom => "Custom",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_primaries_creation() {
let primaries = Primaries::new((0.64, 0.33), (0.30, 0.60), (0.15, 0.06));
assert_eq!(primaries.red, (0.64, 0.33));
assert_eq!(primaries.green, (0.30, 0.60));
assert_eq!(primaries.blue, (0.15, 0.06));
}
#[test]
fn test_bt709_primaries() {
let primaries = Primaries::bt709();
assert_eq!(primaries.red, (0.64, 0.33));
assert_eq!(primaries.green, (0.30, 0.60));
assert_eq!(primaries.blue, (0.15, 0.06));
assert!(primaries.is_valid());
}
#[test]
fn test_bt2020_primaries() {
let primaries = Primaries::bt2020();
assert_eq!(primaries.red, (0.708, 0.292));
assert!(primaries.is_valid());
}
#[test]
fn test_primaries_validation() {
let valid = Primaries::bt709();
assert!(valid.is_valid());
let invalid = Primaries::new((1.5, 0.5), (0.3, 0.6), (0.15, 0.06));
assert!(!invalid.is_valid());
}
#[test]
fn test_gamut_area() {
let bt709 = Primaries::bt709();
let bt2020 = Primaries::bt2020();
let area_709 = bt709.gamut_area();
let area_2020 = bt2020.gamut_area();
assert!(area_709 > 0.0);
assert!(area_2020 > area_709);
}
#[test]
fn test_white_point_d65() {
let wp = WhitePoint::D65;
assert_eq!(wp.xy(), (0.3127, 0.3290));
assert_eq!(wp.cct(), 6500);
}
#[test]
fn test_white_point_d50() {
let wp = WhitePoint::D50;
assert_eq!(wp.xy(), (0.3457, 0.3585));
assert_eq!(wp.cct(), 5000);
}
#[test]
fn test_white_point_custom() {
let wp = WhitePoint::Custom(0.33, 0.33);
assert_eq!(wp.xy(), (0.33, 0.33));
}
#[test]
fn test_color_primaries_bt709() {
let cp = ColorPrimaries::BT709;
let primaries = cp.primaries();
assert_eq!(primaries.red, (0.64, 0.33));
assert_eq!(cp.white_point(), WhitePoint::D65);
assert!(!cp.is_wide_gamut());
}
#[test]
fn test_color_primaries_bt2020() {
let cp = ColorPrimaries::BT2020;
let primaries = cp.primaries();
assert_eq!(primaries.red, (0.708, 0.292));
assert!(cp.is_wide_gamut());
}
#[test]
fn test_color_primaries_dci_p3() {
let cp = ColorPrimaries::DciP3;
assert!(cp.is_wide_gamut());
assert_eq!(cp.white_point(), WhitePoint::Dci);
}
#[test]
fn test_color_primaries_display_p3() {
let cp = ColorPrimaries::DisplayP3;
assert!(cp.is_wide_gamut());
assert_eq!(cp.white_point(), WhitePoint::D65);
}
#[test]
fn test_color_primaries_names() {
assert_eq!(ColorPrimaries::BT709.name(), "BT.709 / Rec.709");
assert_eq!(ColorPrimaries::BT2020.name(), "BT.2020 / Rec.2020");
assert_eq!(ColorPrimaries::DciP3.name(), "DCI-P3");
}
}