use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ColorMetadata {
pub primaries: ColorPrimaries,
pub transfer: TransferFunction,
pub matrix: ColorMatrix,
pub range: ColorRange,
pub dynamic_range: DynamicRange,
}
impl ColorMetadata {
pub const EXPORT: Self = Self {
primaries: ColorPrimaries::Bt709,
transfer: TransferFunction::Bt709,
matrix: ColorMatrix::Bt709,
range: ColorRange::Limited,
dynamic_range: DynamicRange::Sdr,
};
pub const UNDECLARED: Self = Self::EXPORT;
#[must_use]
pub const fn is_hdr(self) -> bool {
matches!(self.dynamic_range, DynamicRange::Hdr)
|| matches!(self.transfer, TransferFunction::Pq | TransferFunction::Hlg)
}
#[must_use]
pub const fn is_consistent(self) -> bool {
matches!(self.dynamic_range, DynamicRange::Hdr)
== matches!(self.transfer, TransferFunction::Pq | TransferFunction::Hlg)
}
}
impl Default for ColorMetadata {
fn default() -> Self {
Self::UNDECLARED
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct YuvEncoding {
pub color: ColorMetadata,
pub chroma: crate::ChromaLocation,
}
impl YuvEncoding {
#[must_use]
pub const fn new(color: ColorMetadata, chroma: crate::ChromaLocation) -> Self {
Self { color, chroma }
}
#[must_use]
pub const fn export(codec: crate::VideoCodec) -> Self {
Self::new(ColorMetadata::EXPORT, codec.chroma_location())
}
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ColorPrimaries {
Bt601,
Bt709,
Bt2020,
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum TransferFunction {
Bt709,
Srgb,
Pq,
Hlg,
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ColorMatrix {
Bt601,
Bt709,
Bt2020Ncl,
Rgb,
}
impl ColorMatrix {
#[must_use]
pub const fn luma_weights(self) -> Option<(f32, f32)> {
match self {
Self::Bt601 => Some((0.299, 0.114)),
Self::Bt709 => Some((0.2126, 0.0722)),
Self::Bt2020Ncl => Some((0.2627, 0.0593)),
Self::Rgb => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct YcbcrLevels {
pub maximum: f32,
pub luma_black: f32,
pub luma_span: f32,
pub chroma_center: f32,
pub chroma_span: f32,
}
impl ColorRange {
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn levels(self, bit_depth: u8) -> YcbcrLevels {
let depth = u32::from(bit_depth.clamp(8, 16));
let maximum = ((1_u32 << depth) - 1) as f32;
let scale = (1_u32 << (depth - 8)) as f32;
match self {
Self::Limited => YcbcrLevels {
maximum,
luma_black: 16.0 * scale,
luma_span: 219.0 * scale,
chroma_center: 128.0 * scale,
chroma_span: 224.0 * scale,
},
Self::Full => YcbcrLevels {
maximum,
luma_black: 0.0,
luma_span: maximum,
chroma_center: (1_u32 << (depth - 1)) as f32,
chroma_span: maximum,
},
}
}
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ColorRange {
Limited,
Full,
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum DynamicRange {
Sdr,
Hdr,
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn levels_scale_the_limited_range_with_the_bit_depth() {
let eight = ColorRange::Limited.levels(8);
assert_eq!(
[
eight.maximum,
eight.luma_black,
eight.luma_span,
eight.chroma_span
],
[255.0, 16.0, 219.0, 224.0]
);
let ten = ColorRange::Limited.levels(10);
assert_eq!(
[
ten.maximum,
ten.luma_black,
ten.luma_span,
ten.chroma_center
],
[1023.0, 64.0, 876.0, 512.0]
);
}
#[test]
fn full_range_centres_chroma_on_the_middle_code_not_on_one_half() {
let eight = ColorRange::Full.levels(8);
assert_eq!(eight.chroma_center, 128.0);
assert!(eight.chroma_center / eight.maximum > 0.5);
assert_eq!(ColorRange::Full.levels(10).chroma_center, 512.0);
}
#[test]
fn the_matrices_weigh_luma_as_their_standards_do() {
for matrix in [
ColorMatrix::Bt601,
ColorMatrix::Bt709,
ColorMatrix::Bt2020Ncl,
] {
let (kr, kb) = matrix.luma_weights().expect("YCbCr");
assert!(kr > 0.0 && kb > 0.0 && kr + kb < 1.0);
}
assert_eq!(ColorMatrix::Rgb.luma_weights(), None);
assert_eq!(ColorMetadata::default(), ColorMetadata::EXPORT);
}
}