1use core::fmt;
12use serde::{Deserialize, Serialize};
13
14const V4L2_COLORSPACE_SMPTE170M: u32 = 1;
16const V4L2_COLORSPACE_REC709: u32 = 3;
17const V4L2_COLORSPACE_470_SYSTEM_M: u32 = 5;
18const V4L2_COLORSPACE_470_SYSTEM_BG: u32 = 6;
19const V4L2_COLORSPACE_JPEG: u32 = 7;
20const V4L2_COLORSPACE_SRGB: u32 = 8;
21const V4L2_COLORSPACE_BT2020: u32 = 10;
22const V4L2_XFER_FUNC_709: u32 = 1;
23const V4L2_XFER_FUNC_SRGB: u32 = 2;
24const V4L2_XFER_FUNC_NONE: u32 = 5;
25const V4L2_XFER_FUNC_SMPTE2084: u32 = 7;
26const V4L2_YCBCR_ENC_DEFAULT: u32 = 0;
27const V4L2_YCBCR_ENC_601: u32 = 1;
28const V4L2_YCBCR_ENC_709: u32 = 2;
29const V4L2_YCBCR_ENC_BT2020: u32 = 6;
30const V4L2_QUANTIZATION_DEFAULT: u32 = 0;
31const V4L2_QUANTIZATION_FULL_RANGE: u32 = 1;
32const V4L2_QUANTIZATION_LIM_RANGE: u32 = 2;
33
34#[non_exhaustive]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum ColorSpace {
38 Bt709,
39 Bt2020,
40 Srgb,
41 Smpte170m,
42}
43
44#[non_exhaustive]
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub enum ColorTransfer {
56 Bt709,
57 Srgb,
58 Pq,
59 Hlg,
63 Linear,
64}
65
66#[non_exhaustive]
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub enum ColorEncoding {
70 Bt601,
71 Bt709,
72 Bt2020,
73}
74
75#[non_exhaustive]
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum ColorRange {
79 Full,
80 Limited,
81}
82
83impl ColorSpace {
84 pub fn as_str(self) -> &'static str {
86 match self {
87 Self::Bt709 => "bt709",
88 Self::Bt2020 => "bt2020",
89 Self::Srgb => "srgb",
90 Self::Smpte170m => "smpte170m",
91 }
92 }
93
94 pub fn from_v4l2(v: u32) -> Option<Self> {
99 match v {
100 V4L2_COLORSPACE_SMPTE170M => Some(Self::Smpte170m),
101 V4L2_COLORSPACE_REC709 => Some(Self::Bt709),
102 V4L2_COLORSPACE_470_SYSTEM_M | V4L2_COLORSPACE_470_SYSTEM_BG => Some(Self::Smpte170m),
104 V4L2_COLORSPACE_JPEG | V4L2_COLORSPACE_SRGB => Some(Self::Srgb),
105 V4L2_COLORSPACE_BT2020 => Some(Self::Bt2020),
106 _ => None,
107 }
108 }
109}
110
111impl ColorTransfer {
112 pub fn as_str(self) -> &'static str {
114 match self {
115 Self::Bt709 => "bt709",
116 Self::Srgb => "srgb",
117 Self::Pq => "pq",
118 Self::Hlg => "hlg",
119 Self::Linear => "linear",
120 }
121 }
122
123 pub fn from_v4l2(v: u32) -> Option<Self> {
132 match v {
133 V4L2_XFER_FUNC_709 => Some(Self::Bt709),
134 V4L2_XFER_FUNC_SRGB => Some(Self::Srgb),
135 V4L2_XFER_FUNC_NONE => Some(Self::Linear),
136 V4L2_XFER_FUNC_SMPTE2084 => Some(Self::Pq),
137 _ => None,
138 }
139 }
140}
141
142impl ColorEncoding {
143 pub fn as_str(self) -> &'static str {
145 match self {
146 Self::Bt601 => "bt601",
147 Self::Bt709 => "bt709",
148 Self::Bt2020 => "bt2020",
149 }
150 }
151
152 pub fn from_v4l2(v: u32) -> Option<Self> {
157 match v {
158 V4L2_YCBCR_ENC_601 => Some(Self::Bt601),
159 V4L2_YCBCR_ENC_709 => Some(Self::Bt709),
160 V4L2_YCBCR_ENC_BT2020 => Some(Self::Bt2020),
161 _ => None,
162 }
163 }
164}
165
166impl ColorRange {
167 pub fn as_str(self) -> &'static str {
169 match self {
170 Self::Full => "full",
171 Self::Limited => "limited",
172 }
173 }
174
175 pub fn from_v4l2(v: u32) -> Option<Self> {
180 match v {
181 V4L2_QUANTIZATION_FULL_RANGE => Some(Self::Full),
182 V4L2_QUANTIZATION_LIM_RANGE => Some(Self::Limited),
183 _ => None,
184 }
185 }
186}
187
188macro_rules! display_via_as_str {
189 ($($t:ty),*) => {$(
190 impl fmt::Display for $t {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 f.write_str(self.as_str())
193 }
194 }
195 )*};
196}
197display_via_as_str!(ColorSpace, ColorTransfer, ColorEncoding, ColorRange);
198
199#[derive(Debug, Clone, Copy, PartialEq)]
206pub struct MatrixWeights {
207 pub kr: f64,
208 pub kb: f64,
209}
210
211impl MatrixWeights {
212 pub fn kg(self) -> f64 {
214 1.0 - self.kr - self.kb
215 }
216}
217
218impl ColorEncoding {
219 pub fn luma_weights(self) -> MatrixWeights {
222 match self {
223 Self::Bt601 => MatrixWeights {
224 kr: 0.299,
225 kb: 0.114,
226 },
227 Self::Bt709 => MatrixWeights {
228 kr: 0.2126,
229 kb: 0.0722,
230 },
231 Self::Bt2020 => MatrixWeights {
232 kr: 0.2627,
233 kb: 0.0593,
234 },
235 }
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq)]
243pub struct RangeScaling {
244 pub y_offset: f64,
246 pub y_swing: f64,
248 pub c_swing: f64,
250}
251
252impl ColorRange {
253 pub fn scaling(self) -> RangeScaling {
257 match self {
258 Self::Full => RangeScaling {
259 y_offset: 0.0,
260 y_swing: 255.0,
261 c_swing: 255.0,
262 },
263 Self::Limited => RangeScaling {
264 y_offset: 16.0,
265 y_swing: 219.0,
266 c_swing: 224.0,
267 },
268 }
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
276pub struct Colorimetry {
277 pub space: Option<ColorSpace>,
278 pub transfer: Option<ColorTransfer>,
279 pub encoding: Option<ColorEncoding>,
280 pub range: Option<ColorRange>,
281}
282
283impl Colorimetry {
284 pub fn jfif() -> Self {
287 Self {
288 space: Some(ColorSpace::Srgb),
289 transfer: Some(ColorTransfer::Srgb),
290 encoding: Some(ColorEncoding::Bt601),
291 range: Some(ColorRange::Full),
292 }
293 }
294
295 pub fn from_v4l2(colorspace: u32, xfer: u32, ycbcr_enc: u32, quant: u32) -> Self {
307 let encoding = ColorEncoding::from_v4l2(ycbcr_enc).or_else(|| {
308 if ycbcr_enc == V4L2_YCBCR_ENC_DEFAULT {
309 Self::default_encoding_for_colorspace(colorspace)
310 } else {
311 None
312 }
313 });
314 let range = ColorRange::from_v4l2(quant).or_else(|| {
315 if quant == V4L2_QUANTIZATION_DEFAULT {
316 Self::default_range_for_colorspace(colorspace)
317 } else {
318 None
319 }
320 });
321 Self {
322 space: ColorSpace::from_v4l2(colorspace),
323 transfer: ColorTransfer::from_v4l2(xfer),
324 encoding,
325 range,
326 }
327 }
328
329 fn default_encoding_for_colorspace(colorspace: u32) -> Option<ColorEncoding> {
332 match colorspace {
333 V4L2_COLORSPACE_REC709 => Some(ColorEncoding::Bt709),
334 V4L2_COLORSPACE_BT2020 => Some(ColorEncoding::Bt2020),
335 V4L2_COLORSPACE_SMPTE170M
336 | V4L2_COLORSPACE_470_SYSTEM_M
337 | V4L2_COLORSPACE_470_SYSTEM_BG
338 | V4L2_COLORSPACE_JPEG
339 | V4L2_COLORSPACE_SRGB => Some(ColorEncoding::Bt601),
340 _ => None,
341 }
342 }
343
344 fn default_range_for_colorspace(colorspace: u32) -> Option<ColorRange> {
349 match colorspace {
350 V4L2_COLORSPACE_JPEG => Some(ColorRange::Full),
351 V4L2_COLORSPACE_SMPTE170M
352 | V4L2_COLORSPACE_REC709
353 | V4L2_COLORSPACE_BT2020
354 | V4L2_COLORSPACE_470_SYSTEM_M
355 | V4L2_COLORSPACE_470_SYSTEM_BG
356 | V4L2_COLORSPACE_SRGB => Some(ColorRange::Limited),
357 _ => None,
358 }
359 }
360
361 pub fn with_space(mut self, s: ColorSpace) -> Self {
363 self.space = Some(s);
364 self
365 }
366
367 pub fn with_transfer(mut self, t: ColorTransfer) -> Self {
369 self.transfer = Some(t);
370 self
371 }
372
373 pub fn with_encoding(mut self, e: ColorEncoding) -> Self {
375 self.encoding = Some(e);
376 self
377 }
378
379 pub fn with_range(mut self, r: ColorRange) -> Self {
381 self.range = Some(r);
382 self
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn from_v4l2_known_and_default() {
392 assert_eq!(ColorEncoding::from_v4l2(1), Some(ColorEncoding::Bt601));
393 assert_eq!(ColorEncoding::from_v4l2(2), Some(ColorEncoding::Bt709));
394 assert_eq!(ColorEncoding::from_v4l2(6), Some(ColorEncoding::Bt2020));
395 assert_eq!(ColorEncoding::from_v4l2(0), None); assert_eq!(ColorEncoding::from_v4l2(3), None); assert_eq!(ColorRange::from_v4l2(1), Some(ColorRange::Full));
398 assert_eq!(ColorRange::from_v4l2(2), Some(ColorRange::Limited));
399 assert_eq!(ColorSpace::from_v4l2(7), Some(ColorSpace::Srgb)); assert_eq!(ColorTransfer::from_v4l2(5), Some(ColorTransfer::Linear)); assert_eq!(ColorSpace::from_v4l2(1), Some(ColorSpace::Smpte170m)); assert_eq!(ColorSpace::from_v4l2(3), Some(ColorSpace::Bt709)); assert_eq!(ColorSpace::from_v4l2(10), Some(ColorSpace::Bt2020)); assert_eq!(ColorSpace::from_v4l2(5), Some(ColorSpace::Smpte170m)); assert_eq!(ColorSpace::from_v4l2(6), Some(ColorSpace::Smpte170m)); assert_eq!(ColorTransfer::from_v4l2(1), Some(ColorTransfer::Bt709)); assert_eq!(ColorTransfer::from_v4l2(2), Some(ColorTransfer::Srgb)); assert_eq!(ColorTransfer::from_v4l2(7), Some(ColorTransfer::Pq)); assert_eq!(ColorTransfer::from_v4l2(3), None); assert_eq!(ColorTransfer::from_v4l2(6), None); for v in 0u32..=10 {
418 assert_ne!(ColorTransfer::from_v4l2(v), Some(ColorTransfer::Hlg));
419 }
420 }
421
422 #[test]
423 fn jfif_is_bt601_full_srgb() {
424 let c = Colorimetry::jfif();
425 assert_eq!(c.space, Some(ColorSpace::Srgb));
426 assert_eq!(c.transfer, Some(ColorTransfer::Srgb));
427 assert_eq!(c.encoding, Some(ColorEncoding::Bt601));
428 assert_eq!(c.range, Some(ColorRange::Full));
429 }
430
431 #[test]
432 fn from_v4l2_struct_maps_all_axes_and_unknown_to_none() {
433 let c = Colorimetry::from_v4l2(3, 1, 2, 1); assert_eq!(c.space, Some(ColorSpace::Bt709));
435 assert_eq!(c.transfer, Some(ColorTransfer::Bt709));
436 assert_eq!(c.encoding, Some(ColorEncoding::Bt709));
437 assert_eq!(c.range, Some(ColorRange::Full));
438 let d = Colorimetry::from_v4l2(0, 0, 0, 0); assert_eq!(d, Colorimetry::default()); }
441
442 #[test]
443 fn from_v4l2_default_enc_quant_derive_from_colorspace() {
444 let jpeg = Colorimetry::from_v4l2(7, 0, 0, 0);
448 assert_eq!(jpeg.encoding, Some(ColorEncoding::Bt601));
449 assert_eq!(jpeg.range, Some(ColorRange::Full));
450
451 let rec709 = Colorimetry::from_v4l2(3, 0, 0, 0);
453 assert_eq!(rec709.encoding, Some(ColorEncoding::Bt709));
454 assert_eq!(rec709.range, Some(ColorRange::Limited));
455
456 let explicit = Colorimetry::from_v4l2(7, 0, 2, 2); assert_eq!(explicit.encoding, Some(ColorEncoding::Bt709));
459 assert_eq!(explicit.range, Some(ColorRange::Limited));
460
461 let unknown_enc = Colorimetry::from_v4l2(7, 0, 99, 0);
463 assert_eq!(unknown_enc.encoding, None);
464 assert_eq!(unknown_enc.range, Some(ColorRange::Full)); }
466
467 #[test]
468 fn luma_weights_are_the_canonical_bt_constants() {
469 assert_eq!(
470 ColorEncoding::Bt601.luma_weights(),
471 MatrixWeights {
472 kr: 0.299,
473 kb: 0.114
474 }
475 );
476 assert_eq!(
477 ColorEncoding::Bt709.luma_weights(),
478 MatrixWeights {
479 kr: 0.2126,
480 kb: 0.0722
481 }
482 );
483 assert_eq!(
484 ColorEncoding::Bt2020.luma_weights(),
485 MatrixWeights {
486 kr: 0.2627,
487 kb: 0.0593
488 }
489 );
490 assert!((ColorEncoding::Bt709.luma_weights().kg() - 0.7152).abs() < 1e-9);
492 }
493
494 #[test]
495 fn range_scaling_is_full_or_studio_swing() {
496 let full = ColorRange::Full.scaling();
497 assert_eq!(
498 full,
499 RangeScaling {
500 y_offset: 0.0,
501 y_swing: 255.0,
502 c_swing: 255.0
503 }
504 );
505 let limited = ColorRange::Limited.scaling();
506 assert_eq!(
507 limited,
508 RangeScaling {
509 y_offset: 16.0,
510 y_swing: 219.0,
511 c_swing: 224.0
512 }
513 );
514 }
515
516 #[test]
517 fn as_str_matches_schema() {
518 assert_eq!(ColorEncoding::Bt601.as_str(), "bt601");
519 assert_eq!(ColorRange::Full.as_str(), "full");
520 assert_eq!(ColorSpace::Smpte170m.as_str(), "smpte170m");
521 assert_eq!(ColorTransfer::Pq.as_str(), "pq");
522 assert_eq!(ColorTransfer::Hlg.as_str(), "hlg");
523 }
524}