1use serde::{Deserialize, Serialize};
2
3#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[serde(deny_unknown_fields)]
7pub struct ColorMetadata {
8 pub primaries: ColorPrimaries,
10 pub transfer: TransferFunction,
12 pub matrix: ColorMatrix,
14 pub range: ColorRange,
16 pub dynamic_range: DynamicRange,
18}
19
20impl ColorMetadata {
21 pub const EXPORT: Self = Self {
27 primaries: ColorPrimaries::Bt709,
28 transfer: TransferFunction::Bt709,
29 matrix: ColorMatrix::Bt709,
30 range: ColorRange::Limited,
31 dynamic_range: DynamicRange::Sdr,
32 };
33
34 pub const UNDECLARED: Self = Self::EXPORT;
37
38 #[must_use]
41 pub const fn is_hdr(self) -> bool {
42 matches!(self.dynamic_range, DynamicRange::Hdr)
43 || matches!(self.transfer, TransferFunction::Pq | TransferFunction::Hlg)
44 }
45
46 #[must_use]
49 pub const fn is_consistent(self) -> bool {
50 matches!(self.dynamic_range, DynamicRange::Hdr)
51 == matches!(self.transfer, TransferFunction::Pq | TransferFunction::Hlg)
52 }
53}
54
55impl Default for ColorMetadata {
56 fn default() -> Self {
58 Self::UNDECLARED
59 }
60}
61
62#[derive(Copy, Clone, Debug, Eq, PartialEq)]
64pub struct YuvEncoding {
65 pub color: ColorMetadata,
67 pub chroma: crate::ChromaLocation,
69}
70
71impl YuvEncoding {
72 #[must_use]
74 pub const fn new(color: ColorMetadata, chroma: crate::ChromaLocation) -> Self {
75 Self { color, chroma }
76 }
77
78 #[must_use]
81 pub const fn export(codec: crate::VideoCodec) -> Self {
82 Self::new(ColorMetadata::EXPORT, codec.chroma_location())
83 }
84}
85
86#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89#[serde(rename_all = "snake_case")]
90pub enum ColorPrimaries {
91 Bt601,
93 Bt709,
95 Bt2020,
97}
98
99#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
102#[serde(rename_all = "snake_case")]
103pub enum TransferFunction {
104 Bt709,
106 Srgb,
108 Pq,
110 Hlg,
112}
113
114#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[serde(rename_all = "snake_case")]
118pub enum ColorMatrix {
119 Bt601,
121 Bt709,
123 Bt2020Ncl,
125 Rgb,
127}
128
129impl ColorMatrix {
130 #[must_use]
135 pub const fn luma_weights(self) -> Option<(f32, f32)> {
136 match self {
137 Self::Bt601 => Some((0.299, 0.114)),
138 Self::Bt709 => Some((0.2126, 0.0722)),
139 Self::Bt2020Ncl => Some((0.2627, 0.0593)),
140 Self::Rgb => None,
141 }
142 }
143}
144
145#[derive(Copy, Clone, Debug, PartialEq)]
151pub struct YcbcrLevels {
152 pub maximum: f32,
154 pub luma_black: f32,
156 pub luma_span: f32,
158 pub chroma_center: f32,
160 pub chroma_span: f32,
162}
163
164impl ColorRange {
165 #[must_use]
170 #[allow(clippy::cast_precision_loss)] pub fn levels(self, bit_depth: u8) -> YcbcrLevels {
172 let depth = u32::from(bit_depth.clamp(8, 16));
173 let maximum = ((1_u32 << depth) - 1) as f32;
174 let scale = (1_u32 << (depth - 8)) as f32;
175 match self {
176 Self::Limited => YcbcrLevels {
177 maximum,
178 luma_black: 16.0 * scale,
179 luma_span: 219.0 * scale,
180 chroma_center: 128.0 * scale,
181 chroma_span: 224.0 * scale,
182 },
183 Self::Full => YcbcrLevels {
184 maximum,
185 luma_black: 0.0,
186 luma_span: maximum,
187 chroma_center: (1_u32 << (depth - 1)) as f32,
188 chroma_span: maximum,
189 },
190 }
191 }
192}
193
194#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
197#[serde(rename_all = "snake_case")]
198pub enum ColorRange {
199 Limited,
201 Full,
203}
204
205#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
207#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
208#[serde(rename_all = "snake_case")]
209pub enum DynamicRange {
210 Sdr,
212 Hdr,
214}
215
216#[cfg(test)]
217#[allow(clippy::float_cmp)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn levels_scale_the_limited_range_with_the_bit_depth() {
224 let eight = ColorRange::Limited.levels(8);
225 assert_eq!(
226 [
227 eight.maximum,
228 eight.luma_black,
229 eight.luma_span,
230 eight.chroma_span
231 ],
232 [255.0, 16.0, 219.0, 224.0]
233 );
234 let ten = ColorRange::Limited.levels(10);
235 assert_eq!(
236 [
237 ten.maximum,
238 ten.luma_black,
239 ten.luma_span,
240 ten.chroma_center
241 ],
242 [1023.0, 64.0, 876.0, 512.0]
243 );
244 }
245
246 #[test]
247 fn full_range_centres_chroma_on_the_middle_code_not_on_one_half() {
248 let eight = ColorRange::Full.levels(8);
249 assert_eq!(eight.chroma_center, 128.0);
250 assert!(eight.chroma_center / eight.maximum > 0.5);
251 assert_eq!(ColorRange::Full.levels(10).chroma_center, 512.0);
252 }
253
254 #[test]
255 fn the_matrices_weigh_luma_as_their_standards_do() {
256 for matrix in [
257 ColorMatrix::Bt601,
258 ColorMatrix::Bt709,
259 ColorMatrix::Bt2020Ncl,
260 ] {
261 let (kr, kb) = matrix.luma_weights().expect("YCbCr");
262 assert!(kr > 0.0 && kb > 0.0 && kr + kb < 1.0);
263 }
264 assert_eq!(ColorMatrix::Rgb.luma_weights(), None);
265 assert_eq!(ColorMetadata::default(), ColorMetadata::EXPORT);
266 }
267}