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 from_str_code(s: &str) -> Option<Self> {
91 match s {
92 "bt709" => Some(Self::Bt709),
93 "bt2020" => Some(Self::Bt2020),
94 "srgb" => Some(Self::Srgb),
95 "smpte170m" => Some(Self::Smpte170m),
96 _ => None,
97 }
98 }
99
100 pub fn as_str(self) -> &'static str {
102 match self {
103 Self::Bt709 => "bt709",
104 Self::Bt2020 => "bt2020",
105 Self::Srgb => "srgb",
106 Self::Smpte170m => "smpte170m",
107 }
108 }
109
110 pub fn from_v4l2(v: u32) -> Option<Self> {
115 match v {
116 V4L2_COLORSPACE_SMPTE170M => Some(Self::Smpte170m),
117 V4L2_COLORSPACE_REC709 => Some(Self::Bt709),
118 V4L2_COLORSPACE_470_SYSTEM_M | V4L2_COLORSPACE_470_SYSTEM_BG => Some(Self::Smpte170m),
120 V4L2_COLORSPACE_JPEG | V4L2_COLORSPACE_SRGB => Some(Self::Srgb),
121 V4L2_COLORSPACE_BT2020 => Some(Self::Bt2020),
122 _ => None,
123 }
124 }
125}
126
127impl ColorTransfer {
128 pub fn from_str_code(s: &str) -> Option<Self> {
135 match s {
136 "bt709" => Some(Self::Bt709),
137 "srgb" => Some(Self::Srgb),
138 "pq" => Some(Self::Pq),
139 "hlg" => Some(Self::Hlg),
140 "linear" => Some(Self::Linear),
141 _ => None,
142 }
143 }
144
145 pub fn as_str(self) -> &'static str {
147 match self {
148 Self::Bt709 => "bt709",
149 Self::Srgb => "srgb",
150 Self::Pq => "pq",
151 Self::Hlg => "hlg",
152 Self::Linear => "linear",
153 }
154 }
155
156 pub fn from_v4l2(v: u32) -> Option<Self> {
165 match v {
166 V4L2_XFER_FUNC_709 => Some(Self::Bt709),
167 V4L2_XFER_FUNC_SRGB => Some(Self::Srgb),
168 V4L2_XFER_FUNC_NONE => Some(Self::Linear),
169 V4L2_XFER_FUNC_SMPTE2084 => Some(Self::Pq),
170 _ => None,
171 }
172 }
173}
174
175impl ColorEncoding {
176 pub fn from_str_code(s: &str) -> Option<Self> {
183 match s {
184 "bt601" => Some(Self::Bt601),
185 "bt709" => Some(Self::Bt709),
186 "bt2020" => Some(Self::Bt2020),
187 _ => None,
188 }
189 }
190
191 pub fn as_str(self) -> &'static str {
193 match self {
194 Self::Bt601 => "bt601",
195 Self::Bt709 => "bt709",
196 Self::Bt2020 => "bt2020",
197 }
198 }
199
200 pub fn from_v4l2(v: u32) -> Option<Self> {
205 match v {
206 V4L2_YCBCR_ENC_601 => Some(Self::Bt601),
207 V4L2_YCBCR_ENC_709 => Some(Self::Bt709),
208 V4L2_YCBCR_ENC_BT2020 => Some(Self::Bt2020),
209 _ => None,
210 }
211 }
212}
213
214impl ColorRange {
215 pub fn from_str_code(s: &str) -> Option<Self> {
222 match s {
223 "full" => Some(Self::Full),
224 "limited" => Some(Self::Limited),
225 _ => None,
226 }
227 }
228
229 pub fn as_str(self) -> &'static str {
231 match self {
232 Self::Full => "full",
233 Self::Limited => "limited",
234 }
235 }
236
237 pub fn from_v4l2(v: u32) -> Option<Self> {
242 match v {
243 V4L2_QUANTIZATION_FULL_RANGE => Some(Self::Full),
244 V4L2_QUANTIZATION_LIM_RANGE => Some(Self::Limited),
245 _ => None,
246 }
247 }
248}
249
250macro_rules! display_via_as_str {
251 ($($t:ty),*) => {$(
252 impl fmt::Display for $t {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 f.write_str(self.as_str())
255 }
256 }
257 )*};
258}
259display_via_as_str!(ColorSpace, ColorTransfer, ColorEncoding, ColorRange);
260
261#[derive(Debug, Clone, Copy, PartialEq)]
268pub struct MatrixWeights {
269 pub kr: f64,
270 pub kb: f64,
271}
272
273impl MatrixWeights {
274 pub fn kg(self) -> f64 {
276 1.0 - self.kr - self.kb
277 }
278}
279
280impl ColorEncoding {
281 pub fn luma_weights(self) -> MatrixWeights {
284 match self {
285 Self::Bt601 => MatrixWeights {
286 kr: 0.299,
287 kb: 0.114,
288 },
289 Self::Bt709 => MatrixWeights {
290 kr: 0.2126,
291 kb: 0.0722,
292 },
293 Self::Bt2020 => MatrixWeights {
294 kr: 0.2627,
295 kb: 0.0593,
296 },
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq)]
305pub struct RangeScaling {
306 pub y_offset: f64,
308 pub y_swing: f64,
310 pub c_swing: f64,
312}
313
314impl ColorRange {
315 pub fn scaling(self) -> RangeScaling {
319 match self {
320 Self::Full => RangeScaling {
321 y_offset: 0.0,
322 y_swing: 255.0,
323 c_swing: 255.0,
324 },
325 Self::Limited => RangeScaling {
326 y_offset: 16.0,
327 y_swing: 219.0,
328 c_swing: 224.0,
329 },
330 }
331 }
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
338pub struct Colorimetry {
339 pub space: Option<ColorSpace>,
340 pub transfer: Option<ColorTransfer>,
341 pub encoding: Option<ColorEncoding>,
342 pub range: Option<ColorRange>,
343}
344
345impl Colorimetry {
346 pub fn jfif() -> Self {
349 Self {
350 space: Some(ColorSpace::Srgb),
351 transfer: Some(ColorTransfer::Srgb),
352 encoding: Some(ColorEncoding::Bt601),
353 range: Some(ColorRange::Full),
354 }
355 }
356
357 pub fn from_v4l2(
378 colorspace: u32,
379 xfer: u32,
380 ycbcr_enc: u32,
381 quant: u32,
382 is_rgb_or_hsv: bool,
383 ) -> Self {
384 let encoding = ColorEncoding::from_v4l2(ycbcr_enc).or_else(|| {
385 if ycbcr_enc == V4L2_YCBCR_ENC_DEFAULT {
386 Self::default_encoding_for_colorspace(colorspace)
387 } else {
388 None
389 }
390 });
391 let range = ColorRange::from_v4l2(quant).or_else(|| {
392 if quant == V4L2_QUANTIZATION_DEFAULT {
393 Self::default_range(is_rgb_or_hsv, colorspace)
394 } else {
395 None
396 }
397 });
398 Self {
399 space: ColorSpace::from_v4l2(colorspace),
400 transfer: ColorTransfer::from_v4l2(xfer),
401 encoding,
402 range,
403 }
404 }
405
406 fn default_encoding_for_colorspace(colorspace: u32) -> Option<ColorEncoding> {
411 match colorspace {
412 V4L2_COLORSPACE_REC709 => Some(ColorEncoding::Bt709),
413 V4L2_COLORSPACE_BT2020 => Some(ColorEncoding::Bt2020),
414 V4L2_COLORSPACE_SMPTE170M
415 | V4L2_COLORSPACE_470_SYSTEM_M
416 | V4L2_COLORSPACE_470_SYSTEM_BG
417 | V4L2_COLORSPACE_JPEG
418 | V4L2_COLORSPACE_SRGB => Some(ColorEncoding::Bt601),
419 _ => None,
420 }
421 }
422
423 fn default_range(is_rgb_or_hsv: bool, colorspace: u32) -> Option<ColorRange> {
429 if is_rgb_or_hsv {
430 return Some(ColorRange::Full);
431 }
432 match colorspace {
433 V4L2_COLORSPACE_JPEG => Some(ColorRange::Full),
434 V4L2_COLORSPACE_SMPTE170M
435 | V4L2_COLORSPACE_REC709
436 | V4L2_COLORSPACE_BT2020
437 | V4L2_COLORSPACE_470_SYSTEM_M
438 | V4L2_COLORSPACE_470_SYSTEM_BG
439 | V4L2_COLORSPACE_SRGB => Some(ColorRange::Limited),
440 _ => None,
441 }
442 }
443
444 pub fn with_space(mut self, s: ColorSpace) -> Self {
446 self.space = Some(s);
447 self
448 }
449
450 pub fn with_transfer(mut self, t: ColorTransfer) -> Self {
452 self.transfer = Some(t);
453 self
454 }
455
456 pub fn with_encoding(mut self, e: ColorEncoding) -> Self {
458 self.encoding = Some(e);
459 self
460 }
461
462 pub fn with_range(mut self, r: ColorRange) -> Self {
464 self.range = Some(r);
465 self
466 }
467
468 pub fn pack(&self) -> u32 {
481 let space: u32 = match self.space {
482 None => 0,
483 Some(ColorSpace::Bt709) => 1,
484 Some(ColorSpace::Bt2020) => 2,
485 Some(ColorSpace::Srgb) => 3,
486 Some(ColorSpace::Smpte170m) => 4,
487 };
488 let transfer: u32 = match self.transfer {
489 None => 0,
490 Some(ColorTransfer::Bt709) => 1,
491 Some(ColorTransfer::Srgb) => 2,
492 Some(ColorTransfer::Pq) => 3,
493 Some(ColorTransfer::Hlg) => 4,
494 Some(ColorTransfer::Linear) => 5,
495 };
496 let encoding: u32 = match self.encoding {
497 None => 0,
498 Some(ColorEncoding::Bt601) => 1,
499 Some(ColorEncoding::Bt709) => 2,
500 Some(ColorEncoding::Bt2020) => 3,
501 };
502 let range: u32 = match self.range {
503 None => 0,
504 Some(ColorRange::Full) => 1,
505 Some(ColorRange::Limited) => 2,
506 };
507 space | (transfer << 8) | (encoding << 16) | (range << 24)
508 }
509
510 pub fn unpack(bits: u32) -> Self {
519 let space = match bits & 0xFF {
520 1 => Some(ColorSpace::Bt709),
521 2 => Some(ColorSpace::Bt2020),
522 3 => Some(ColorSpace::Srgb),
523 4 => Some(ColorSpace::Smpte170m),
524 _ => None,
525 };
526 let transfer = match (bits >> 8) & 0xFF {
527 1 => Some(ColorTransfer::Bt709),
528 2 => Some(ColorTransfer::Srgb),
529 3 => Some(ColorTransfer::Pq),
530 4 => Some(ColorTransfer::Hlg),
531 5 => Some(ColorTransfer::Linear),
532 _ => None,
533 };
534 let encoding = match (bits >> 16) & 0xFF {
535 1 => Some(ColorEncoding::Bt601),
536 2 => Some(ColorEncoding::Bt709),
537 3 => Some(ColorEncoding::Bt2020),
538 _ => None,
539 };
540 let range = match (bits >> 24) & 0xFF {
541 1 => Some(ColorRange::Full),
542 2 => Some(ColorRange::Limited),
543 _ => None,
544 };
545 Colorimetry {
546 space,
547 transfer,
548 encoding,
549 range,
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn from_v4l2_known_and_default() {
560 assert_eq!(ColorEncoding::from_v4l2(1), Some(ColorEncoding::Bt601));
561 assert_eq!(ColorEncoding::from_v4l2(2), Some(ColorEncoding::Bt709));
562 assert_eq!(ColorEncoding::from_v4l2(6), Some(ColorEncoding::Bt2020));
563 assert_eq!(ColorEncoding::from_v4l2(0), None); assert_eq!(ColorEncoding::from_v4l2(3), None); assert_eq!(ColorRange::from_v4l2(1), Some(ColorRange::Full));
566 assert_eq!(ColorRange::from_v4l2(2), Some(ColorRange::Limited));
567 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 {
586 assert_ne!(ColorTransfer::from_v4l2(v), Some(ColorTransfer::Hlg));
587 }
588 }
589
590 #[test]
591 fn jfif_is_bt601_full_srgb() {
592 let c = Colorimetry::jfif();
593 assert_eq!(c.space, Some(ColorSpace::Srgb));
594 assert_eq!(c.transfer, Some(ColorTransfer::Srgb));
595 assert_eq!(c.encoding, Some(ColorEncoding::Bt601));
596 assert_eq!(c.range, Some(ColorRange::Full));
597 }
598
599 #[test]
600 fn from_v4l2_struct_maps_all_axes_and_unknown_to_none() {
601 let c = Colorimetry::from_v4l2(3, 1, 2, 1, false); assert_eq!(c.space, Some(ColorSpace::Bt709));
603 assert_eq!(c.transfer, Some(ColorTransfer::Bt709));
604 assert_eq!(c.encoding, Some(ColorEncoding::Bt709));
605 assert_eq!(c.range, Some(ColorRange::Full));
606 let d = Colorimetry::from_v4l2(0, 0, 0, 0, false); assert_eq!(d, Colorimetry::default()); }
609
610 #[test]
611 fn from_v4l2_default_enc_quant_derive_from_colorspace() {
612 let jpeg = Colorimetry::from_v4l2(7, 0, 0, 0, false);
616 assert_eq!(jpeg.encoding, Some(ColorEncoding::Bt601));
617 assert_eq!(jpeg.range, Some(ColorRange::Full));
618
619 let rec709 = Colorimetry::from_v4l2(3, 0, 0, 0, false);
621 assert_eq!(rec709.encoding, Some(ColorEncoding::Bt709));
622 assert_eq!(rec709.range, Some(ColorRange::Limited));
623
624 let explicit = Colorimetry::from_v4l2(7, 0, 2, 2, false); assert_eq!(explicit.encoding, Some(ColorEncoding::Bt709));
627 assert_eq!(explicit.range, Some(ColorRange::Limited));
628
629 let unknown_enc = Colorimetry::from_v4l2(7, 0, 99, 0, false);
631 assert_eq!(unknown_enc.encoding, None);
632 assert_eq!(unknown_enc.range, Some(ColorRange::Full)); }
634
635 #[test]
636 fn luma_weights_are_the_canonical_bt_constants() {
637 assert_eq!(
638 ColorEncoding::Bt601.luma_weights(),
639 MatrixWeights {
640 kr: 0.299,
641 kb: 0.114
642 }
643 );
644 assert_eq!(
645 ColorEncoding::Bt709.luma_weights(),
646 MatrixWeights {
647 kr: 0.2126,
648 kb: 0.0722
649 }
650 );
651 assert_eq!(
652 ColorEncoding::Bt2020.luma_weights(),
653 MatrixWeights {
654 kr: 0.2627,
655 kb: 0.0593
656 }
657 );
658 assert!((ColorEncoding::Bt709.luma_weights().kg() - 0.7152).abs() < 1e-9);
660 }
661
662 #[test]
663 fn range_scaling_is_full_or_studio_swing() {
664 let full = ColorRange::Full.scaling();
665 assert_eq!(
666 full,
667 RangeScaling {
668 y_offset: 0.0,
669 y_swing: 255.0,
670 c_swing: 255.0
671 }
672 );
673 let limited = ColorRange::Limited.scaling();
674 assert_eq!(
675 limited,
676 RangeScaling {
677 y_offset: 16.0,
678 y_swing: 219.0,
679 c_swing: 224.0
680 }
681 );
682 }
683
684 #[test]
685 fn as_str_and_from_str_code_are_inverses_for_every_variant() {
686 for v in [
689 ColorSpace::Bt709,
690 ColorSpace::Bt2020,
691 ColorSpace::Srgb,
692 ColorSpace::Smpte170m,
693 ] {
694 assert_eq!(ColorSpace::from_str_code(v.as_str()), Some(v), "{v:?}");
695 }
696 for v in [
697 ColorTransfer::Bt709,
698 ColorTransfer::Srgb,
699 ColorTransfer::Pq,
700 ColorTransfer::Hlg,
701 ColorTransfer::Linear,
702 ] {
703 assert_eq!(ColorTransfer::from_str_code(v.as_str()), Some(v), "{v:?}");
704 }
705 for v in [
706 ColorEncoding::Bt601,
707 ColorEncoding::Bt709,
708 ColorEncoding::Bt2020,
709 ] {
710 assert_eq!(ColorEncoding::from_str_code(v.as_str()), Some(v), "{v:?}");
711 }
712 for v in [ColorRange::Full, ColorRange::Limited] {
713 assert_eq!(ColorRange::from_str_code(v.as_str()), Some(v), "{v:?}");
714 }
715 }
716
717 #[test]
718 fn an_unknown_or_empty_label_is_unspecified_not_an_error() {
719 assert_eq!(ColorSpace::from_str_code(""), None);
720 assert_eq!(ColorSpace::from_str_code("not-a-colour-space"), None);
721 assert_eq!(
722 ColorRange::from_str_code("FULL"),
723 None,
724 "labels are case-sensitive"
725 );
726 }
727
728 #[test]
729 fn as_str_matches_schema() {
730 assert_eq!(ColorEncoding::Bt601.as_str(), "bt601");
731 assert_eq!(ColorRange::Full.as_str(), "full");
732 assert_eq!(ColorSpace::Smpte170m.as_str(), "smpte170m");
733 assert_eq!(ColorTransfer::Pq.as_str(), "pq");
734 assert_eq!(ColorTransfer::Hlg.as_str(), "hlg");
735 }
736
737 #[test]
738 fn pack_unpack_round_trips() {
739 assert_eq!(
744 Colorimetry::unpack(Colorimetry::default().pack()),
745 Colorimetry::default()
746 );
747 assert_eq!(Colorimetry::default().pack(), 0);
748
749 let jfif = Colorimetry::jfif();
750 assert_eq!(Colorimetry::unpack(jfif.pack()), jfif);
751
752 let full = Colorimetry {
753 space: Some(ColorSpace::Bt2020),
754 transfer: Some(ColorTransfer::Hlg),
755 encoding: Some(ColorEncoding::Bt2020),
756 range: Some(ColorRange::Limited),
757 };
758 assert_eq!(Colorimetry::unpack(full.pack()), full);
759 }
760
761 #[test]
762 fn unpack_maps_unrecognised_codes_to_none() {
763 let bits = 0xFFu32 | (0xFFu32 << 8) | (0xFFu32 << 16) | (0xFFu32 << 24);
767 assert_eq!(Colorimetry::unpack(bits), Colorimetry::default());
768 }
769
770 const SPACES: [(ColorSpace, u32); 4] = [
771 (ColorSpace::Bt709, 1),
772 (ColorSpace::Bt2020, 2),
773 (ColorSpace::Srgb, 3),
774 (ColorSpace::Smpte170m, 4),
775 ];
776 const TRANSFERS: [(ColorTransfer, u32); 5] = [
777 (ColorTransfer::Bt709, 1),
778 (ColorTransfer::Srgb, 2),
779 (ColorTransfer::Pq, 3),
780 (ColorTransfer::Hlg, 4),
781 (ColorTransfer::Linear, 5),
782 ];
783 const ENCODINGS: [(ColorEncoding, u32); 3] = [
784 (ColorEncoding::Bt601, 1),
785 (ColorEncoding::Bt709, 2),
786 (ColorEncoding::Bt2020, 3),
787 ];
788 const RANGES: [(ColorRange, u32); 2] = [(ColorRange::Full, 1), (ColorRange::Limited, 2)];
789
790 #[test]
791 fn pack_places_each_axis_code_in_its_own_byte() {
792 for (s, code) in SPACES {
794 let c = Colorimetry::default().with_space(s);
795 assert_eq!(c.pack(), code, "{s:?}");
796 assert_eq!(Colorimetry::unpack(code), c, "{s:?}");
797 }
798 for (t, code) in TRANSFERS {
799 let c = Colorimetry::default().with_transfer(t);
800 assert_eq!(c.pack(), code << 8, "{t:?}");
801 assert_eq!(Colorimetry::unpack(code << 8), c, "{t:?}");
802 }
803 for (e, code) in ENCODINGS {
804 let c = Colorimetry::default().with_encoding(e);
805 assert_eq!(c.pack(), code << 16, "{e:?}");
806 assert_eq!(Colorimetry::unpack(code << 16), c, "{e:?}");
807 }
808 for (r, code) in RANGES {
809 let c = Colorimetry::default().with_range(r);
810 assert_eq!(c.pack(), code << 24, "{r:?}");
811 assert_eq!(Colorimetry::unpack(code << 24), c, "{r:?}");
812 }
813 }
814
815 #[test]
816 fn unpack_decodes_a_fully_populated_word() {
817 assert_eq!(
818 Colorimetry::unpack(0x02_02_05_04),
819 Colorimetry {
820 space: Some(ColorSpace::Smpte170m),
821 transfer: Some(ColorTransfer::Linear),
822 encoding: Some(ColorEncoding::Bt709),
823 range: Some(ColorRange::Limited),
824 }
825 );
826 }
827
828 #[test]
829 fn every_axis_combination_round_trips_through_pack() {
830 for (s, _) in SPACES {
831 for (t, _) in TRANSFERS {
832 for (e, _) in ENCODINGS {
833 for (r, _) in RANGES {
834 let c = Colorimetry {
835 space: Some(s),
836 transfer: Some(t),
837 encoding: Some(e),
838 range: Some(r),
839 };
840 assert_eq!(Colorimetry::unpack(c.pack()), c);
841 }
842 }
843 }
844 }
845 }
846
847 #[test]
848 fn builders_set_exactly_their_own_axis() {
849 let c = Colorimetry::default()
850 .with_space(ColorSpace::Bt2020)
851 .with_transfer(ColorTransfer::Pq)
852 .with_encoding(ColorEncoding::Bt2020)
853 .with_range(ColorRange::Full);
854 assert_eq!(
855 c,
856 Colorimetry {
857 space: Some(ColorSpace::Bt2020),
858 transfer: Some(ColorTransfer::Pq),
859 encoding: Some(ColorEncoding::Bt2020),
860 range: Some(ColorRange::Full),
861 }
862 );
863 let base = Colorimetry::jfif();
865 assert_eq!(
866 base.with_space(ColorSpace::Bt709),
867 Colorimetry {
868 space: Some(ColorSpace::Bt709),
869 ..base
870 }
871 );
872 assert_eq!(
873 base.with_transfer(ColorTransfer::Hlg),
874 Colorimetry {
875 transfer: Some(ColorTransfer::Hlg),
876 ..base
877 }
878 );
879 assert_eq!(
880 base.with_encoding(ColorEncoding::Bt709),
881 Colorimetry {
882 encoding: Some(ColorEncoding::Bt709),
883 ..base
884 }
885 );
886 assert_eq!(
887 base.with_range(ColorRange::Limited),
888 Colorimetry {
889 range: Some(ColorRange::Limited),
890 ..base
891 }
892 );
893 }
894
895 #[test]
896 fn from_v4l2_derives_encoding_and_range_for_every_recognised_yuv_colorspace() {
897 use ColorEncoding as E;
898 use ColorRange as R;
899 let table = [
900 (V4L2_COLORSPACE_SMPTE170M, E::Bt601, R::Limited),
901 (V4L2_COLORSPACE_REC709, E::Bt709, R::Limited),
902 (V4L2_COLORSPACE_470_SYSTEM_M, E::Bt601, R::Limited),
903 (V4L2_COLORSPACE_470_SYSTEM_BG, E::Bt601, R::Limited),
904 (V4L2_COLORSPACE_JPEG, E::Bt601, R::Full),
905 (V4L2_COLORSPACE_SRGB, E::Bt601, R::Limited),
906 (V4L2_COLORSPACE_BT2020, E::Bt2020, R::Limited),
907 ];
908 for (colorspace, encoding, range) in table {
909 let c = Colorimetry::from_v4l2(colorspace, 0, 0, 0, false);
910 assert_eq!(c.encoding, Some(encoding), "colorspace {colorspace}");
911 assert_eq!(c.range, Some(range), "colorspace {colorspace}");
912 }
913 for colorspace in [0, 2, 4, 9, 11, 99] {
915 let c = Colorimetry::from_v4l2(colorspace, 0, 0, 0, false);
916 assert_eq!(
917 (c.encoding, c.range),
918 (None, None),
919 "colorspace {colorspace}"
920 );
921 }
922 }
923
924 const ALL_V4L2_COLORSPACES: [u32; 14] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99];
927
928 fn kernel_default_range(is_rgb_or_hsv: bool, colorspace: u32) -> ColorRange {
931 if is_rgb_or_hsv || colorspace == V4L2_COLORSPACE_JPEG {
932 ColorRange::Full
933 } else {
934 ColorRange::Limited
935 }
936 }
937
938 #[test]
939 fn from_v4l2_quantization_matches_the_kernel_for_every_colorspace_format_and_quant() {
940 for colorspace in ALL_V4L2_COLORSPACES {
941 let recognised = ColorSpace::from_v4l2(colorspace).is_some();
942 for is_rgb_or_hsv in [false, true] {
943 for quant in [
944 V4L2_QUANTIZATION_DEFAULT,
945 V4L2_QUANTIZATION_FULL_RANGE,
946 V4L2_QUANTIZATION_LIM_RANGE,
947 99,
948 ] {
949 let range =
950 Colorimetry::from_v4l2(colorspace, 0, 0, quant, is_rgb_or_hsv).range;
951 let expected = match quant {
952 V4L2_QUANTIZATION_FULL_RANGE => Some(ColorRange::Full),
953 V4L2_QUANTIZATION_LIM_RANGE => Some(ColorRange::Limited),
954 V4L2_QUANTIZATION_DEFAULT if is_rgb_or_hsv || recognised => {
955 Some(kernel_default_range(is_rgb_or_hsv, colorspace))
956 }
957 _ => None,
958 };
959 assert_eq!(
960 range, expected,
961 "colorspace {colorspace}, rgb/hsv {is_rgb_or_hsv}, quant {quant}"
962 );
963 }
964 }
965 }
966 }
967
968 #[test]
969 fn rgb_default_quantization_is_full_and_yuv_follows_the_colorspace() {
970 let range = |colorspace, is_rgb_or_hsv| {
971 Colorimetry::from_v4l2(colorspace, 0, 0, V4L2_QUANTIZATION_DEFAULT, is_rgb_or_hsv).range
972 };
973 assert_eq!(range(V4L2_COLORSPACE_SRGB, true), Some(ColorRange::Full));
974 assert_eq!(
975 range(V4L2_COLORSPACE_SRGB, false),
976 Some(ColorRange::Limited)
977 );
978 assert_eq!(range(V4L2_COLORSPACE_JPEG, false), Some(ColorRange::Full));
979 assert_eq!(range(0, true), Some(ColorRange::Full));
982 assert_eq!(range(V4L2_COLORSPACE_REC709, true), Some(ColorRange::Full));
983 }
984
985 #[test]
986 fn from_v4l2_encoding_matches_the_kernel_for_every_recognised_colorspace() {
987 for colorspace in ALL_V4L2_COLORSPACES {
991 for is_rgb_or_hsv in [false, true] {
992 let encoding = Colorimetry::from_v4l2(colorspace, 0, 0, 0, is_rgb_or_hsv).encoding;
993 let expected = ColorSpace::from_v4l2(colorspace).map(|_| match colorspace {
994 V4L2_COLORSPACE_REC709 => ColorEncoding::Bt709,
995 V4L2_COLORSPACE_BT2020 => ColorEncoding::Bt2020,
996 _ => ColorEncoding::Bt601,
997 });
998 assert_eq!(encoding, expected, "colorspace {colorspace}");
999 }
1000 }
1001 }
1002}