#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ColorRange {
#[default]
Unspecified,
Limited,
Full,
}
impl ColorRange {
pub fn from_full_range_flag(full: bool) -> Self {
if full {
Self::Full
} else {
Self::Limited
}
}
pub fn full_range_flag(self) -> Option<bool> {
match self {
Self::Limited => Some(false),
Self::Full => Some(true),
_ => None,
}
}
pub fn is_unspecified(self) -> bool {
!matches!(self, Self::Limited | Self::Full)
}
fn to_byte(self) -> u8 {
match self {
Self::Limited => 1,
Self::Full => 2,
_ => 0,
}
}
fn from_byte(b: u8) -> Self {
match b {
1 => Self::Limited,
2 => Self::Full,
_ => Self::Unspecified,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ColorPrimaries(pub u8);
impl ColorPrimaries {
pub const BT709: Self = Self(1);
pub const UNSPECIFIED: Self = Self(2);
pub const BT470_SYSTEM_M: Self = Self(4);
pub const BT470_SYSTEM_BG: Self = Self(5);
pub const BT601_525: Self = Self(6);
pub const SMPTE_ST240: Self = Self(7);
pub const GENERIC_FILM: Self = Self(8);
pub const BT2020: Self = Self(9);
pub const SMPTE_ST428: Self = Self(10);
pub const SMPTE_RP431: Self = Self(11);
pub const SMPTE_EG432: Self = Self(12);
pub const fn new(code_point: u8) -> Self {
Self(code_point)
}
pub const fn code_point(self) -> u8 {
self.0
}
pub const fn is_unspecified(self) -> bool {
self.0 == 2
}
pub fn name(self) -> Option<&'static str> {
Some(match self.0 {
1 => "bt709",
2 => "unspecified",
4 => "bt470m",
5 => "bt470bg",
6 => "bt601-525",
7 => "smpte240m",
8 => "film",
9 => "bt2020",
10 => "smpte428",
11 => "smpte431",
12 => "smpte432",
22 => "cp22",
_ => return None,
})
}
}
impl Default for ColorPrimaries {
fn default() -> Self {
Self::UNSPECIFIED
}
}
impl From<u8> for ColorPrimaries {
fn from(code_point: u8) -> Self {
Self(code_point)
}
}
impl std::fmt::Display for ColorPrimaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.name() {
Some(n) => f.write_str(n),
None => write!(f, "reserved({})", self.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TransferCharacteristics(pub u8);
impl TransferCharacteristics {
pub const BT709: Self = Self(1);
pub const UNSPECIFIED: Self = Self(2);
pub const GAMMA22: Self = Self(4);
pub const GAMMA28: Self = Self(5);
pub const BT601: Self = Self(6);
pub const SMPTE_ST240: Self = Self(7);
pub const LINEAR: Self = Self(8);
pub const LOG100: Self = Self(9);
pub const LOG100_SQRT10: Self = Self(10);
pub const IEC61966_2_4: Self = Self(11);
pub const BT1361_EXTENDED: Self = Self(12);
pub const IEC61966_2_1: Self = Self(13);
pub const BT2020_10BIT: Self = Self(14);
pub const BT2020_12BIT: Self = Self(15);
pub const SMPTE_ST2084: Self = Self(16);
pub const SMPTE_ST428: Self = Self(17);
pub const ARIB_STD_B67: Self = Self(18);
pub const fn new(code_point: u8) -> Self {
Self(code_point)
}
pub const fn code_point(self) -> u8 {
self.0
}
pub const fn is_unspecified(self) -> bool {
self.0 == 2
}
pub fn name(self) -> Option<&'static str> {
Some(match self.0 {
1 => "bt709",
2 => "unspecified",
4 => "gamma22",
5 => "gamma28",
6 => "bt601",
7 => "smpte240m",
8 => "linear",
9 => "log100",
10 => "log100-sqrt10",
11 => "iec61966-2-4",
12 => "bt1361e",
13 => "iec61966-2-1",
14 => "bt2020-10",
15 => "bt2020-12",
16 => "pq",
17 => "smpte428",
18 => "hlg",
_ => return None,
})
}
}
impl Default for TransferCharacteristics {
fn default() -> Self {
Self::UNSPECIFIED
}
}
impl From<u8> for TransferCharacteristics {
fn from(code_point: u8) -> Self {
Self(code_point)
}
}
impl std::fmt::Display for TransferCharacteristics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.name() {
Some(n) => f.write_str(n),
None => write!(f, "reserved({})", self.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MatrixCoefficients(pub u8);
impl MatrixCoefficients {
pub const IDENTITY: Self = Self(0);
pub const BT709: Self = Self(1);
pub const UNSPECIFIED: Self = Self(2);
pub const FCC: Self = Self(4);
pub const BT470_SYSTEM_BG: Self = Self(5);
pub const BT601_525: Self = Self(6);
pub const SMPTE_ST240: Self = Self(7);
pub const YCGCO: Self = Self(8);
pub const BT2020_NCL: Self = Self(9);
pub const BT2020_CL: Self = Self(10);
pub const SMPTE_ST2085: Self = Self(11);
pub const CHROMATICITY_DERIVED_NCL: Self = Self(12);
pub const CHROMATICITY_DERIVED_CL: Self = Self(13);
pub const ICTCP: Self = Self(14);
pub const IPT_C2: Self = Self(15);
pub const YCGCO_RE: Self = Self(16);
pub const YCGCO_RO: Self = Self(17);
pub const fn new(code_point: u8) -> Self {
Self(code_point)
}
pub const fn code_point(self) -> u8 {
self.0
}
pub const fn is_unspecified(self) -> bool {
self.0 == 2
}
pub const fn is_rgb_like(self) -> bool {
matches!(self.0, 0 | 8 | 16 | 17)
}
pub fn name(self) -> Option<&'static str> {
Some(match self.0 {
0 => "identity",
1 => "bt709",
2 => "unspecified",
4 => "fcc",
5 => "bt470bg",
6 => "bt601-525",
7 => "smpte240m",
8 => "ycgco",
9 => "bt2020ncl",
10 => "bt2020cl",
11 => "smpte2085",
12 => "chroma-derived-ncl",
13 => "chroma-derived-cl",
14 => "ictcp",
15 => "ipt-c2",
16 => "ycgco-re",
17 => "ycgco-ro",
_ => return None,
})
}
}
impl Default for MatrixCoefficients {
fn default() -> Self {
Self::UNSPECIFIED
}
}
impl From<u8> for MatrixCoefficients {
fn from(code_point: u8) -> Self {
Self(code_point)
}
}
impl std::fmt::Display for MatrixCoefficients {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.name() {
Some(n) => f.write_str(n),
None => write!(f, "reserved({})", self.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub struct ColorSignal {
pub range: ColorRange,
pub primaries: ColorPrimaries,
pub transfer: TransferCharacteristics,
pub matrix: MatrixCoefficients,
}
impl ColorSignal {
pub const WIRE_LEN: usize = 4;
pub const fn new(
range: ColorRange,
primaries: ColorPrimaries,
transfer: TransferCharacteristics,
matrix: MatrixCoefficients,
) -> Self {
Self {
range,
primaries,
transfer,
matrix,
}
}
pub const fn from_code_points(
primaries: u8,
transfer: u8,
matrix: u8,
full_range: bool,
) -> Self {
Self {
range: if full_range {
ColorRange::Full
} else {
ColorRange::Limited
},
primaries: ColorPrimaries(primaries),
transfer: TransferCharacteristics(transfer),
matrix: MatrixCoefficients(matrix),
}
}
pub const fn unspecified() -> Self {
Self {
range: ColorRange::Unspecified,
primaries: ColorPrimaries::UNSPECIFIED,
transfer: TransferCharacteristics::UNSPECIFIED,
matrix: MatrixCoefficients::UNSPECIFIED,
}
}
pub const fn srgb() -> Self {
Self::new(
ColorRange::Full,
ColorPrimaries::BT709,
TransferCharacteristics::IEC61966_2_1,
MatrixCoefficients::IDENTITY,
)
}
pub const fn bt709_limited() -> Self {
Self::new(
ColorRange::Limited,
ColorPrimaries::BT709,
TransferCharacteristics::BT709,
MatrixCoefficients::BT709,
)
}
pub const fn with_range(mut self, range: ColorRange) -> Self {
self.range = range;
self
}
pub const fn with_primaries(mut self, primaries: ColorPrimaries) -> Self {
self.primaries = primaries;
self
}
pub const fn with_transfer(mut self, transfer: TransferCharacteristics) -> Self {
self.transfer = transfer;
self
}
pub const fn with_matrix(mut self, matrix: MatrixCoefficients) -> Self {
self.matrix = matrix;
self
}
pub fn is_unspecified(&self) -> bool {
self.range.is_unspecified()
&& self.primaries.is_unspecified()
&& self.transfer.is_unspecified()
&& self.matrix.is_unspecified()
}
pub fn or(self, fallback: Self) -> Self {
Self {
range: if self.range.is_unspecified() {
fallback.range
} else {
self.range
},
primaries: if self.primaries.is_unspecified() {
fallback.primaries
} else {
self.primaries
},
transfer: if self.transfer.is_unspecified() {
fallback.transfer
} else {
self.transfer
},
matrix: if self.matrix.is_unspecified() {
fallback.matrix
} else {
self.matrix
},
}
}
pub fn to_bytes(&self) -> [u8; Self::WIRE_LEN] {
[
self.range.to_byte(),
self.primaries.0,
self.transfer.0,
self.matrix.0,
]
}
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let b = bytes.get(..Self::WIRE_LEN)?;
Some(Self {
range: ColorRange::from_byte(b[0]),
primaries: ColorPrimaries(b[1]),
transfer: TransferCharacteristics(b[2]),
matrix: MatrixCoefficients(b[3]),
})
}
}
impl std::fmt::Display for ColorSignal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let range = match self.range {
ColorRange::Limited => "limited",
ColorRange::Full => "full",
_ => "unspecified-range",
};
write!(
f,
"{}/{}/{} {}",
self.primaries, self.transfer, self.matrix, range
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_fully_unspecified_and_matches_h273_code_point_2() {
let s = ColorSignal::default();
assert!(s.is_unspecified());
assert_eq!(s, ColorSignal::unspecified());
assert_eq!(s.range, ColorRange::Unspecified);
assert_eq!(s.primaries.code_point(), 2);
assert_eq!(s.transfer.code_point(), 2);
assert_eq!(s.matrix.code_point(), 2);
assert_eq!(ColorPrimaries::default(), ColorPrimaries::UNSPECIFIED);
assert_eq!(
TransferCharacteristics::default(),
TransferCharacteristics::UNSPECIFIED
);
assert_eq!(
MatrixCoefficients::default(),
MatrixCoefficients::UNSPECIFIED
);
}
#[test]
fn named_constants_carry_the_h273_code_points() {
assert_eq!(ColorPrimaries::BT709.0, 1);
assert_eq!(ColorPrimaries::BT470_SYSTEM_M.0, 4);
assert_eq!(ColorPrimaries::BT470_SYSTEM_BG.0, 5);
assert_eq!(ColorPrimaries::BT601_525.0, 6);
assert_eq!(ColorPrimaries::SMPTE_ST240.0, 7);
assert_eq!(ColorPrimaries::GENERIC_FILM.0, 8);
assert_eq!(ColorPrimaries::BT2020.0, 9);
assert_eq!(ColorPrimaries::SMPTE_ST428.0, 10);
assert_eq!(ColorPrimaries::SMPTE_RP431.0, 11);
assert_eq!(ColorPrimaries::SMPTE_EG432.0, 12);
assert_eq!(TransferCharacteristics::BT709.0, 1);
assert_eq!(TransferCharacteristics::GAMMA22.0, 4);
assert_eq!(TransferCharacteristics::GAMMA28.0, 5);
assert_eq!(TransferCharacteristics::BT601.0, 6);
assert_eq!(TransferCharacteristics::SMPTE_ST240.0, 7);
assert_eq!(TransferCharacteristics::LINEAR.0, 8);
assert_eq!(TransferCharacteristics::LOG100.0, 9);
assert_eq!(TransferCharacteristics::LOG100_SQRT10.0, 10);
assert_eq!(TransferCharacteristics::IEC61966_2_4.0, 11);
assert_eq!(TransferCharacteristics::BT1361_EXTENDED.0, 12);
assert_eq!(TransferCharacteristics::IEC61966_2_1.0, 13);
assert_eq!(TransferCharacteristics::BT2020_10BIT.0, 14);
assert_eq!(TransferCharacteristics::BT2020_12BIT.0, 15);
assert_eq!(TransferCharacteristics::SMPTE_ST2084.0, 16);
assert_eq!(TransferCharacteristics::SMPTE_ST428.0, 17);
assert_eq!(TransferCharacteristics::ARIB_STD_B67.0, 18);
assert_eq!(MatrixCoefficients::IDENTITY.0, 0);
assert_eq!(MatrixCoefficients::BT709.0, 1);
assert_eq!(MatrixCoefficients::FCC.0, 4);
assert_eq!(MatrixCoefficients::BT470_SYSTEM_BG.0, 5);
assert_eq!(MatrixCoefficients::BT601_525.0, 6);
assert_eq!(MatrixCoefficients::SMPTE_ST240.0, 7);
assert_eq!(MatrixCoefficients::YCGCO.0, 8);
assert_eq!(MatrixCoefficients::BT2020_NCL.0, 9);
assert_eq!(MatrixCoefficients::BT2020_CL.0, 10);
assert_eq!(MatrixCoefficients::SMPTE_ST2085.0, 11);
assert_eq!(MatrixCoefficients::CHROMATICITY_DERIVED_NCL.0, 12);
assert_eq!(MatrixCoefficients::CHROMATICITY_DERIVED_CL.0, 13);
assert_eq!(MatrixCoefficients::ICTCP.0, 14);
assert_eq!(MatrixCoefficients::IPT_C2.0, 15);
assert_eq!(MatrixCoefficients::YCGCO_RE.0, 16);
assert_eq!(MatrixCoefficients::YCGCO_RO.0, 17);
}
#[test]
fn reserved_code_points_pass_through_and_have_no_name() {
for cp in [0u8, 3, 13, 21, 23, 200, 255] {
let p = ColorPrimaries::new(cp);
assert_eq!(p.code_point(), cp);
assert_eq!(p.name(), None);
assert_eq!(p.to_string(), format!("reserved({cp})"));
}
assert_eq!(TransferCharacteristics::new(0).name(), None);
assert_eq!(TransferCharacteristics::new(19).name(), None);
assert_eq!(MatrixCoefficients::new(3).name(), None);
assert_eq!(MatrixCoefficients::new(18).name(), None);
for cp in [1u8, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 22] {
assert!(ColorPrimaries::new(cp).name().is_some(), "cp {cp}");
}
for cp in 1u8..=18 {
if cp != 3 {
assert!(TransferCharacteristics::new(cp).name().is_some(), "tc {cp}");
}
}
for cp in 0u8..=17 {
if cp != 3 {
assert!(MatrixCoefficients::new(cp).name().is_some(), "mc {cp}");
}
}
}
#[test]
fn range_flag_round_trip() {
assert_eq!(ColorRange::from_full_range_flag(true), ColorRange::Full);
assert_eq!(ColorRange::from_full_range_flag(false), ColorRange::Limited);
assert_eq!(ColorRange::Full.full_range_flag(), Some(true));
assert_eq!(ColorRange::Limited.full_range_flag(), Some(false));
assert_eq!(ColorRange::Unspecified.full_range_flag(), None);
assert!(ColorRange::Unspecified.is_unspecified());
assert!(!ColorRange::Full.is_unspecified());
assert!(!ColorRange::Limited.is_unspecified());
}
#[test]
fn from_code_points_and_builders() {
let s = ColorSignal::from_code_points(9, 16, 9, false);
assert_eq!(s.primaries, ColorPrimaries::BT2020);
assert_eq!(s.transfer, TransferCharacteristics::SMPTE_ST2084);
assert_eq!(s.matrix, MatrixCoefficients::BT2020_NCL);
assert_eq!(s.range, ColorRange::Limited);
assert!(!s.is_unspecified());
let t = ColorSignal::unspecified()
.with_range(ColorRange::Full)
.with_primaries(ColorPrimaries::BT709)
.with_transfer(TransferCharacteristics::IEC61966_2_1)
.with_matrix(MatrixCoefficients::IDENTITY);
assert_eq!(t, ColorSignal::srgb());
assert_eq!(
ColorSignal::bt709_limited(),
ColorSignal::from_code_points(1, 1, 1, false)
);
}
#[test]
fn or_fills_only_unspecified_fields() {
let frame = ColorSignal::unspecified().with_range(ColorRange::Full);
let stream = ColorSignal::bt709_limited();
let merged = frame.or(stream);
assert_eq!(merged.range, ColorRange::Full);
assert_eq!(merged.primaries, ColorPrimaries::BT709);
assert_eq!(merged.transfer, TransferCharacteristics::BT709);
assert_eq!(merged.matrix, MatrixCoefficients::BT709);
assert_eq!(ColorSignal::srgb().or(stream), ColorSignal::srgb());
assert!(ColorSignal::default()
.or(ColorSignal::default())
.is_unspecified());
}
#[test]
fn wire_bytes_round_trip_and_tolerate_extra_or_unknown() {
let s = ColorSignal::from_code_points(12, 13, 0, true);
let bytes = s.to_bytes();
assert_eq!(bytes, [2, 12, 13, 0]);
assert_eq!(ColorSignal::from_bytes(&bytes), Some(s));
assert_eq!(ColorSignal::default().to_bytes(), [0, 2, 2, 2]);
assert_eq!(ColorSignal::bt709_limited().to_bytes()[0], 1);
assert_eq!(
ColorSignal::from_bytes(&[1, 1, 1, 1, 0xAA, 0xBB]),
Some(ColorSignal::bt709_limited())
);
assert_eq!(ColorSignal::from_bytes(&[1, 1, 1]), None);
assert_eq!(ColorSignal::from_bytes(&[]), None);
assert_eq!(
ColorSignal::from_bytes(&[7, 1, 1, 1]).unwrap().range,
ColorRange::Unspecified
);
}
#[test]
fn display_is_compact() {
assert_eq!(
ColorSignal::bt709_limited().to_string(),
"bt709/bt709/bt709 limited"
);
assert_eq!(
ColorSignal::srgb().to_string(),
"bt709/iec61966-2-1/identity full"
);
assert_eq!(
ColorSignal::default().to_string(),
"unspecified/unspecified/unspecified unspecified-range"
);
assert_eq!(
ColorSignal::from_code_points(9, 18, 200, false).to_string(),
"bt2020/hlg/reserved(200) limited"
);
}
#[test]
fn rgb_like_matrices() {
assert!(MatrixCoefficients::IDENTITY.is_rgb_like());
assert!(MatrixCoefficients::YCGCO.is_rgb_like());
assert!(MatrixCoefficients::YCGCO_RE.is_rgb_like());
assert!(MatrixCoefficients::YCGCO_RO.is_rgb_like());
assert!(!MatrixCoefficients::BT709.is_rgb_like());
assert!(!MatrixCoefficients::UNSPECIFIED.is_rgb_like());
}
#[test]
fn code_point_newtypes_convert_from_u8_and_clone() {
let p: ColorPrimaries = 9u8.into();
assert_eq!(p, ColorPrimaries::BT2020);
let t: TransferCharacteristics = 16u8.into();
assert_eq!(t, TransferCharacteristics::SMPTE_ST2084);
let m: MatrixCoefficients = 9u8.into();
assert_eq!(m, MatrixCoefficients::BT2020_NCL);
let s = ColorSignal::new(ColorRange::Limited, p, t, m);
#[allow(clippy::clone_on_copy)]
let c = s.clone();
assert_eq!(c, s);
}
}