1use crate::{ErrorCategory, ErrorCode, PixelFlowError, Result};
4
5#[repr(u8)]
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum ColorRange {
9 Full,
11 Limited,
13}
14
15impl ColorRange {
16 pub fn parse(value: &str) -> Result<Self> {
18 match value {
19 "full" => Ok(Self::Full),
20 "limited" => Ok(Self::Limited),
21 _ => Err(PixelFlowError::new(
22 ErrorCategory::Format,
23 ErrorCode::new("format.unsupported_range"),
24 format!("unsupported color range '{value}'"),
25 )),
26 }
27 }
28
29 #[must_use]
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 Self::Full => "full",
34 Self::Limited => "limited",
35 }
36 }
37}
38
39#[repr(u8)]
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum ColorMatrix {
43 Identity,
45 Bt709,
47 Bt470M,
49 Bt470Bg,
51 Smpte170M,
53 Smpte240M,
55 Ycgco,
57 Bt2020Ncl,
59 Bt2020Cl,
61 Smpte2085,
63 ChromaticityDerivedNcl,
65 ChromaticityDerivedCl,
67 Ictcp,
69}
70
71impl ColorMatrix {
72 pub fn parse(value: &str) -> Result<Self> {
74 match value {
75 "identity" => Ok(Self::Identity),
76 "bt709" => Ok(Self::Bt709),
77 "bt470m" => Ok(Self::Bt470M),
78 "bt470bg" => Ok(Self::Bt470Bg),
79 "smpte170m" => Ok(Self::Smpte170M),
80 "smpte240m" => Ok(Self::Smpte240M),
81 "ycgco" => Ok(Self::Ycgco),
82 "bt2020_ncl" => Ok(Self::Bt2020Ncl),
83 "bt2020_cl" => Ok(Self::Bt2020Cl),
84 "smpte2085" => Ok(Self::Smpte2085),
85 "chromaticity_derived_ncl" => Ok(Self::ChromaticityDerivedNcl),
86 "chromaticity_derived_cl" => Ok(Self::ChromaticityDerivedCl),
87 "ictcp" => Ok(Self::Ictcp),
88 _ => Err(color_metadata_error(
89 "format.unsupported_matrix",
90 format!("unsupported color matrix '{value}'"),
91 )),
92 }
93 }
94
95 #[must_use]
97 pub const fn as_str(self) -> &'static str {
98 match self {
99 Self::Identity => "identity",
100 Self::Bt709 => "bt709",
101 Self::Bt470M => "bt470m",
102 Self::Bt470Bg => "bt470bg",
103 Self::Smpte170M => "smpte170m",
104 Self::Smpte240M => "smpte240m",
105 Self::Ycgco => "ycgco",
106 Self::Bt2020Ncl => "bt2020_ncl",
107 Self::Bt2020Cl => "bt2020_cl",
108 Self::Smpte2085 => "smpte2085",
109 Self::ChromaticityDerivedNcl => "chromaticity_derived_ncl",
110 Self::ChromaticityDerivedCl => "chromaticity_derived_cl",
111 Self::Ictcp => "ictcp",
112 }
113 }
114
115 #[must_use]
117 pub const fn luma_coefficients(self) -> (f32, f32, f32) {
118 match self {
119 Self::Identity | Self::Ycgco | Self::Ictcp => (1.0, 1.0, 1.0),
120 Self::Bt709 => (0.2126, 0.7152, 0.0722),
121 Self::Bt470M => (0.30, 0.59, 0.11),
122 Self::Bt470Bg | Self::Smpte170M => (0.299, 0.587, 0.114),
123 Self::Smpte240M => (0.212, 0.701, 0.087),
124 Self::Bt2020Ncl | Self::Bt2020Cl | Self::Smpte2085 => (0.2627, 0.6780, 0.0593),
125 Self::ChromaticityDerivedNcl | Self::ChromaticityDerivedCl => (0.0, 0.0, 0.0),
126 }
127 }
128}
129
130#[repr(u8)]
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub enum ColorTransfer {
134 Bt1886,
136 Bt470M,
138 Bt470Bg,
140 Smpte170M,
142 Smpte240M,
144 Linear,
146 Log100,
148 Log316,
150 Xvycc,
152 Bt1361E,
154 Srgb,
156 Bt2020_10,
158 Bt2020_12,
160 Pq,
162 Smpte428,
164 Hlg,
166}
167
168impl ColorTransfer {
169 pub fn parse(value: &str) -> Result<Self> {
171 match value {
172 "bt1886" => Ok(Self::Bt1886),
173 "bt470m" => Ok(Self::Bt470M),
174 "bt470bg" => Ok(Self::Bt470Bg),
175 "smpte170m" => Ok(Self::Smpte170M),
176 "smpte240m" => Ok(Self::Smpte240M),
177 "linear" => Ok(Self::Linear),
178 "log100" => Ok(Self::Log100),
179 "log316" => Ok(Self::Log316),
180 "xvycc" => Ok(Self::Xvycc),
181 "bt1361e" => Ok(Self::Bt1361E),
182 "srgb" => Ok(Self::Srgb),
183 "bt2020_10" => Ok(Self::Bt2020_10),
184 "bt2020_12" => Ok(Self::Bt2020_12),
185 "pq" => Ok(Self::Pq),
186 "smpte428" => Ok(Self::Smpte428),
187 "hlg" => Ok(Self::Hlg),
188 _ => Err(color_metadata_error(
189 "format.unsupported_transfer",
190 format!("unsupported color transfer '{value}'"),
191 )),
192 }
193 }
194
195 #[must_use]
197 pub const fn as_str(self) -> &'static str {
198 match self {
199 Self::Bt1886 => "bt1886",
200 Self::Bt470M => "bt470m",
201 Self::Bt470Bg => "bt470bg",
202 Self::Smpte170M => "smpte170m",
203 Self::Smpte240M => "smpte240m",
204 Self::Linear => "linear",
205 Self::Log100 => "log100",
206 Self::Log316 => "log316",
207 Self::Xvycc => "xvycc",
208 Self::Bt1361E => "bt1361e",
209 Self::Srgb => "srgb",
210 Self::Bt2020_10 => "bt2020_10",
211 Self::Bt2020_12 => "bt2020_12",
212 Self::Pq => "pq",
213 Self::Smpte428 => "smpte428",
214 Self::Hlg => "hlg",
215 }
216 }
217}
218
219#[repr(u8)]
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum ColorPrimaries {
223 Bt709,
225 Bt470M,
227 Bt470Bg,
229 Smpte170M,
231 Smpte240M,
233 Film,
235 Bt2020,
237 Smpte428,
239 DciP3,
241 DisplayP3,
243}
244
245impl ColorPrimaries {
246 pub fn parse(value: &str) -> Result<Self> {
248 match value {
249 "bt709" => Ok(Self::Bt709),
250 "bt470m" => Ok(Self::Bt470M),
251 "bt470bg" => Ok(Self::Bt470Bg),
252 "smpte170m" => Ok(Self::Smpte170M),
253 "smpte240m" => Ok(Self::Smpte240M),
254 "film" => Ok(Self::Film),
255 "bt2020" => Ok(Self::Bt2020),
256 "smpte428" => Ok(Self::Smpte428),
257 "dci_p3" => Ok(Self::DciP3),
258 "display_p3" => Ok(Self::DisplayP3),
259 _ => Err(color_metadata_error(
260 "format.unsupported_primaries",
261 format!("unsupported color primaries '{value}'"),
262 )),
263 }
264 }
265
266 #[must_use]
268 pub const fn as_str(self) -> &'static str {
269 match self {
270 Self::Bt709 => "bt709",
271 Self::Bt470M => "bt470m",
272 Self::Bt470Bg => "bt470bg",
273 Self::Smpte170M => "smpte170m",
274 Self::Smpte240M => "smpte240m",
275 Self::Film => "film",
276 Self::Bt2020 => "bt2020",
277 Self::Smpte428 => "smpte428",
278 Self::DciP3 => "dci_p3",
279 Self::DisplayP3 => "display_p3",
280 }
281 }
282}
283
284#[repr(u8)]
286#[derive(Clone, Copy, Debug, Eq, PartialEq)]
287pub enum ChromaSiting {
288 Left,
290 Center,
292 TopLeft,
294 Top,
296 BottomLeft,
298 Bottom,
300}
301
302impl ChromaSiting {
303 pub fn parse(value: &str) -> Result<Self> {
305 match value {
306 "left" => Ok(Self::Left),
307 "center" => Ok(Self::Center),
308 "top_left" => Ok(Self::TopLeft),
309 "top" => Ok(Self::Top),
310 "bottom_left" => Ok(Self::BottomLeft),
311 "bottom" => Ok(Self::Bottom),
312 _ => Err(color_metadata_error(
313 "format.unsupported_chroma_siting",
314 format!("unsupported chroma siting '{value}'"),
315 )),
316 }
317 }
318
319 #[must_use]
321 pub const fn as_str(self) -> &'static str {
322 match self {
323 Self::Left => "left",
324 Self::Center => "center",
325 Self::TopLeft => "top_left",
326 Self::Top => "top",
327 Self::BottomLeft => "bottom_left",
328 Self::Bottom => "bottom",
329 }
330 }
331
332 #[must_use]
334 pub const fn offsets(self) -> (f32, f32) {
335 match self {
336 Self::Left => (0.0, 0.5),
337 Self::Center => (0.5, 0.5),
338 Self::TopLeft => (0.0, 0.0),
339 Self::Top => (0.5, 0.0),
340 Self::BottomLeft => (0.0, 1.0),
341 Self::Bottom => (0.5, 1.0),
342 }
343 }
344}
345
346fn color_metadata_error(code: &'static str, message: impl Into<String>) -> PixelFlowError {
347 PixelFlowError::new(ErrorCategory::Format, ErrorCode::new(code), message)
348}
349
350#[repr(u8)]
352#[derive(Clone, Copy, Debug, Eq, PartialEq)]
353pub enum SampleType {
354 U8,
356 U16,
358 F32,
360}
361
362impl SampleType {
363 #[must_use]
365 pub const fn bytes_per_sample(self) -> usize {
366 match self {
367 Self::U8 => 1,
368 Self::U16 => 2,
369 Self::F32 => 4,
370 }
371 }
372}
373
374#[repr(u8)]
376#[derive(Clone, Copy, Debug, Eq, PartialEq)]
377pub enum FormatFamily {
378 Gray,
380 Yuv,
382 PlanarRgb,
384}
385
386#[repr(u8)]
388#[derive(Clone, Copy, Debug, Eq, PartialEq)]
389pub enum ChromaSubsampling {
390 Cs444,
392 Cs422,
394 Cs420,
396}
397
398#[repr(u8)]
400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
401pub enum PlaneRole {
402 Gray,
404 Y,
406 U,
408 V,
410 R,
412 G,
414 B,
416}
417
418#[derive(Clone, Debug, Eq, PartialEq)]
420pub struct PlaneDescriptor {
421 pub role: PlaneRole,
423 pub width_divisor: usize,
425 pub height_divisor: usize,
427 pub sample_type: SampleType,
429}
430
431#[derive(Clone, Debug, Eq, PartialEq)]
433pub struct FormatDescriptor {
434 name: String,
435 family: FormatFamily,
436 subsampling: Option<ChromaSubsampling>,
437 bits_per_sample: u8,
438 sample_type: SampleType,
439 planes: Vec<PlaneDescriptor>,
440}
441
442impl FormatDescriptor {
443 #[must_use]
445 pub fn name(&self) -> &str {
446 &self.name
447 }
448
449 #[must_use]
451 pub const fn family(&self) -> FormatFamily {
452 self.family
453 }
454
455 #[must_use]
457 pub const fn subsampling(&self) -> Option<ChromaSubsampling> {
458 self.subsampling
459 }
460
461 #[must_use]
463 pub const fn bits_per_sample(&self) -> u8 {
464 self.bits_per_sample
465 }
466
467 #[must_use]
469 pub const fn sample_type(&self) -> SampleType {
470 self.sample_type
471 }
472
473 #[must_use]
475 pub fn planes(&self) -> &[PlaneDescriptor] {
476 &self.planes
477 }
478}
479
480pub fn format_with_bit_depth(format: &FormatDescriptor, bits: u8) -> Result<FormatDescriptor> {
482 let suffix = match bits {
483 8 => "8",
484 10 => "10",
485 12 => "12",
486 16 => "16",
487 32 => "f32",
488 _ => {
489 return Err(PixelFlowError::new(
490 ErrorCategory::Format,
491 ErrorCode::new("format.unsupported_bit_depth"),
492 format!("unsupported bit depth '{bits}'"),
493 ));
494 }
495 };
496
497 let alias = match (format.family(), format.subsampling()) {
498 (FormatFamily::Gray, None) => format!("gray{suffix}"),
499 (FormatFamily::Yuv, Some(ChromaSubsampling::Cs420)) => format!("yuv420p{suffix}"),
500 (FormatFamily::Yuv, Some(ChromaSubsampling::Cs422)) => format!("yuv422p{suffix}"),
501 (FormatFamily::Yuv, Some(ChromaSubsampling::Cs444)) => format!("yuv444p{suffix}"),
502 (FormatFamily::PlanarRgb, None) => format!("rgbp{suffix}"),
503 _ => {
504 return Err(PixelFlowError::new(
505 ErrorCategory::Format,
506 ErrorCode::new("format.unsupported_bit_depth"),
507 format!("cannot derive bit depth format from '{}'", format.name()),
508 ));
509 }
510 };
511
512 resolve_format_alias(&alias)
513}
514
515pub fn resolve_format_alias(alias: &str) -> Result<FormatDescriptor> {
517 let normalized = alias.to_ascii_lowercase();
518
519 if let Some(descriptor) = resolve_gray(&normalized) {
520 return Ok(descriptor);
521 }
522 if let Some(descriptor) = resolve_yuv(&normalized) {
523 return Ok(descriptor);
524 }
525 if let Some(descriptor) = resolve_planar_rgb(&normalized) {
526 return Ok(descriptor);
527 }
528
529 Err(PixelFlowError::new(
530 ErrorCategory::Format,
531 ErrorCode::new("format.unsupported_alias"),
532 format!("unsupported format alias '{alias}'"),
533 ))
534}
535
536fn resolve_gray(alias: &str) -> Option<FormatDescriptor> {
537 let suffix = alias.strip_prefix("gray")?;
538 let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
539 Some(FormatDescriptor {
540 name: format!("gray{canonical_suffix}"),
541 family: FormatFamily::Gray,
542 subsampling: None,
543 bits_per_sample: bits,
544 sample_type: sample,
545 planes: vec![PlaneDescriptor {
546 role: PlaneRole::Gray,
547 width_divisor: 1,
548 height_divisor: 1,
549 sample_type: sample,
550 }],
551 })
552}
553
554fn resolve_yuv(alias: &str) -> Option<FormatDescriptor> {
555 let (prefix, subsampling) = [
556 ("yuv420p", ChromaSubsampling::Cs420),
557 ("yuv422p", ChromaSubsampling::Cs422),
558 ("yuv444p", ChromaSubsampling::Cs444),
559 ]
560 .into_iter()
561 .find(|(prefix, _)| alias.starts_with(prefix))?;
562
563 let suffix = alias.strip_prefix(prefix)?;
564 let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
565 let (chroma_w_div, chroma_h_div) = match subsampling {
566 ChromaSubsampling::Cs444 => (1, 1),
567 ChromaSubsampling::Cs422 => (2, 1),
568 ChromaSubsampling::Cs420 => (2, 2),
569 };
570
571 Some(FormatDescriptor {
572 name: format!("{prefix}{canonical_suffix}"),
573 family: FormatFamily::Yuv,
574 subsampling: Some(subsampling),
575 bits_per_sample: bits,
576 sample_type: sample,
577 planes: vec![
578 PlaneDescriptor {
579 role: PlaneRole::Y,
580 width_divisor: 1,
581 height_divisor: 1,
582 sample_type: sample,
583 },
584 PlaneDescriptor {
585 role: PlaneRole::U,
586 width_divisor: chroma_w_div,
587 height_divisor: chroma_h_div,
588 sample_type: sample,
589 },
590 PlaneDescriptor {
591 role: PlaneRole::V,
592 width_divisor: chroma_w_div,
593 height_divisor: chroma_h_div,
594 sample_type: sample,
595 },
596 ],
597 })
598}
599
600fn resolve_planar_rgb(alias: &str) -> Option<FormatDescriptor> {
601 let suffix = alias
602 .strip_prefix("rgbp")
603 .or_else(|| alias.strip_prefix("gbrp"))?;
604 let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
605
606 Some(FormatDescriptor {
607 name: format!("rgbp{canonical_suffix}"),
608 family: FormatFamily::PlanarRgb,
609 subsampling: None,
610 bits_per_sample: bits,
611 sample_type: sample,
612 planes: vec![
613 PlaneDescriptor {
614 role: PlaneRole::R,
615 width_divisor: 1,
616 height_divisor: 1,
617 sample_type: sample,
618 },
619 PlaneDescriptor {
620 role: PlaneRole::G,
621 width_divisor: 1,
622 height_divisor: 1,
623 sample_type: sample,
624 },
625 PlaneDescriptor {
626 role: PlaneRole::B,
627 width_divisor: 1,
628 height_divisor: 1,
629 sample_type: sample,
630 },
631 ],
632 })
633}
634
635fn parse_sample_suffix(suffix: &str) -> Option<(u8, SampleType, &'static str)> {
636 match suffix {
637 "" | "8" => Some((8, SampleType::U8, "8")),
638 "10" => Some((10, SampleType::U16, "10")),
639 "12" => Some((12, SampleType::U16, "12")),
640 "16" => Some((16, SampleType::U16, "16")),
641 "f32" => Some((32, SampleType::F32, "f32")),
642 _ => None,
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 #![expect(clippy::indexing_slicing, reason = "allow in tests")]
649
650 use crate::{ErrorCategory, ErrorCode};
651
652 use super::{
653 ChromaSiting, ChromaSubsampling, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer,
654 FormatFamily, PlaneRole, SampleType, format_with_bit_depth, resolve_format_alias,
655 };
656
657 #[test]
658 fn color_metadata_parse_accepts_canonical_names() {
659 assert_eq!(
660 ColorRange::parse("full").expect("full range"),
661 ColorRange::Full
662 );
663 assert_eq!(
664 ColorMatrix::parse("identity").expect("identity matrix"),
665 ColorMatrix::Identity
666 );
667 assert_eq!(
668 ColorMatrix::parse("bt709").expect("bt709 matrix"),
669 ColorMatrix::Bt709
670 );
671 assert_eq!(
672 ColorMatrix::parse("bt470m").expect("bt470m matrix"),
673 ColorMatrix::Bt470M
674 );
675 assert_eq!(
676 ColorMatrix::parse("bt470bg").expect("bt470bg matrix"),
677 ColorMatrix::Bt470Bg
678 );
679 assert_eq!(
680 ColorMatrix::parse("smpte170m").expect("smpte170m matrix"),
681 ColorMatrix::Smpte170M
682 );
683 assert_eq!(
684 ColorMatrix::parse("smpte240m").expect("smpte240m matrix"),
685 ColorMatrix::Smpte240M
686 );
687 assert_eq!(
688 ColorMatrix::parse("ycgco").expect("ycgco matrix"),
689 ColorMatrix::Ycgco
690 );
691 assert_eq!(
692 ColorMatrix::parse("bt2020_ncl").expect("bt2020 matrix"),
693 ColorMatrix::Bt2020Ncl
694 );
695 assert_eq!(
696 ColorMatrix::parse("bt2020_cl").expect("bt2020 CL matrix"),
697 ColorMatrix::Bt2020Cl
698 );
699 assert_eq!(
700 ColorMatrix::parse("smpte2085").expect("smpte2085 matrix"),
701 ColorMatrix::Smpte2085
702 );
703 assert_eq!(
704 ColorMatrix::parse("chromaticity_derived_ncl").expect("chromaticity NCL matrix"),
705 ColorMatrix::ChromaticityDerivedNcl
706 );
707 assert_eq!(
708 ColorMatrix::parse("chromaticity_derived_cl").expect("chromaticity CL matrix"),
709 ColorMatrix::ChromaticityDerivedCl
710 );
711 assert_eq!(
712 ColorMatrix::parse("ictcp").expect("ICtCp matrix"),
713 ColorMatrix::Ictcp
714 );
715
716 assert_eq!(
717 ColorTransfer::parse("bt1886").expect("BT.1886 transfer"),
718 ColorTransfer::Bt1886
719 );
720 assert_eq!(
721 ColorTransfer::parse("bt470m").expect("BT.470M transfer"),
722 ColorTransfer::Bt470M
723 );
724 assert_eq!(
725 ColorTransfer::parse("bt470bg").expect("BT.470BG transfer"),
726 ColorTransfer::Bt470Bg
727 );
728 assert_eq!(
729 ColorTransfer::parse("smpte170m").expect("SMPTE 170M transfer"),
730 ColorTransfer::Smpte170M
731 );
732 assert_eq!(
733 ColorTransfer::parse("smpte240m").expect("SMPTE 240M transfer"),
734 ColorTransfer::Smpte240M
735 );
736 assert_eq!(
737 ColorTransfer::parse("linear").expect("linear transfer"),
738 ColorTransfer::Linear
739 );
740 assert_eq!(
741 ColorTransfer::parse("log100").expect("Log100 transfer"),
742 ColorTransfer::Log100
743 );
744 assert_eq!(
745 ColorTransfer::parse("log316").expect("Log316 transfer"),
746 ColorTransfer::Log316
747 );
748 assert_eq!(
749 ColorTransfer::parse("xvycc").expect("xvYCC transfer"),
750 ColorTransfer::Xvycc
751 );
752 assert_eq!(
753 ColorTransfer::parse("bt1361e").expect("BT.1361E transfer"),
754 ColorTransfer::Bt1361E
755 );
756 assert_eq!(
757 ColorTransfer::parse("srgb").expect("sRGB transfer"),
758 ColorTransfer::Srgb
759 );
760 assert_eq!(
761 ColorTransfer::parse("bt2020_10").expect("bt2020 transfer"),
762 ColorTransfer::Bt2020_10
763 );
764 assert_eq!(
765 ColorTransfer::parse("bt2020_12").expect("bt2020 transfer"),
766 ColorTransfer::Bt2020_12
767 );
768 assert_eq!(
769 ColorTransfer::parse("pq").expect("PQ transfer"),
770 ColorTransfer::Pq
771 );
772 assert_eq!(
773 ColorTransfer::parse("smpte428").expect("SMPTE 428 transfer"),
774 ColorTransfer::Smpte428
775 );
776 assert_eq!(
777 ColorTransfer::parse("hlg").expect("HLG transfer"),
778 ColorTransfer::Hlg
779 );
780
781 assert_eq!(
782 ColorPrimaries::parse("bt709").expect("BT.709 primaries"),
783 ColorPrimaries::Bt709
784 );
785 assert_eq!(
786 ColorPrimaries::parse("bt470m").expect("BT.470M primaries"),
787 ColorPrimaries::Bt470M
788 );
789 assert_eq!(
790 ColorPrimaries::parse("bt470bg").expect("BT.470BG primaries"),
791 ColorPrimaries::Bt470Bg
792 );
793 assert_eq!(
794 ColorPrimaries::parse("smpte170m").expect("SMPTE 170M primaries"),
795 ColorPrimaries::Smpte170M
796 );
797 assert_eq!(
798 ColorPrimaries::parse("smpte240m").expect("SMPTE 240M primaries"),
799 ColorPrimaries::Smpte240M
800 );
801 assert_eq!(
802 ColorPrimaries::parse("film").expect("film primaries"),
803 ColorPrimaries::Film
804 );
805 assert_eq!(
806 ColorPrimaries::parse("bt2020").expect("BT.2020 primaries"),
807 ColorPrimaries::Bt2020
808 );
809 assert_eq!(
810 ColorPrimaries::parse("smpte428").expect("SMPTE 428 primaries"),
811 ColorPrimaries::Smpte428
812 );
813 assert_eq!(
814 ColorPrimaries::parse("dci_p3").expect("DCI-P3 primaries"),
815 ColorPrimaries::DciP3
816 );
817 assert_eq!(
818 ColorPrimaries::parse("display_p3").expect("Display P3 primaries"),
819 ColorPrimaries::DisplayP3
820 );
821 assert_eq!(
822 ChromaSiting::parse("top_left").expect("top-left siting"),
823 ChromaSiting::TopLeft
824 );
825
826 assert_eq!(ColorMatrix::Ictcp.as_str(), "ictcp");
827 assert_eq!(ColorTransfer::Pq.as_str(), "pq");
828 assert_eq!(ColorPrimaries::DisplayP3.as_str(), "display_p3");
829 assert_eq!(ChromaSiting::Bottom.offsets(), (0.5, 1.0));
830 }
831
832 #[test]
833 fn color_metadata_parse_rejects_noncanonical_names() {
834 for (label, error) in [
835 (
836 "matrix",
837 ColorMatrix::parse("rec709").expect_err("alias should fail"),
838 ),
839 (
840 "transfer",
841 ColorTransfer::parse("bt.1886").expect_err("punctuated name should fail"),
842 ),
843 (
844 "primaries",
845 ColorPrimaries::parse("p3").expect_err("alias should fail"),
846 ),
847 (
848 "siting",
849 ChromaSiting::parse("mpeg2").expect_err("alias should fail"),
850 ),
851 ] {
852 assert_eq!(error.category(), ErrorCategory::Format, "{label}");
853 }
854 }
855
856 #[test]
857 fn color_metadata_matrix_luma_coefficients_are_stable() {
858 assert_eq!(
859 ColorMatrix::Bt709.luma_coefficients(),
860 (0.2126, 0.7152, 0.0722)
861 );
862 assert_eq!(ColorMatrix::Bt470M.luma_coefficients(), (0.30, 0.59, 0.11));
863 assert_eq!(
864 ColorMatrix::Bt470Bg.luma_coefficients(),
865 (0.299, 0.587, 0.114)
866 );
867 assert_eq!(
868 ColorMatrix::Smpte170M.luma_coefficients(),
869 (0.299, 0.587, 0.114)
870 );
871 assert_eq!(
872 ColorMatrix::Smpte240M.luma_coefficients(),
873 (0.212, 0.701, 0.087)
874 );
875 assert_eq!(
876 ColorMatrix::Bt2020Ncl.luma_coefficients(),
877 (0.2627, 0.6780, 0.0593)
878 );
879 }
880
881 #[test]
882 fn color_range_parse_accepts_official_names() {
883 assert_eq!(
884 ColorRange::parse("full").expect("full should parse"),
885 ColorRange::Full
886 );
887 assert_eq!(
888 ColorRange::parse("limited").expect("limited should parse"),
889 ColorRange::Limited
890 );
891 assert_eq!(ColorRange::Full.as_str(), "full");
892 assert_eq!(ColorRange::Limited.as_str(), "limited");
893 }
894
895 #[test]
896 fn color_range_parse_rejects_unknown_names() {
897 let error = ColorRange::parse("tv").expect_err("unknown range should fail");
898
899 assert_eq!(error.category(), ErrorCategory::Format);
900 assert_eq!(error.code(), ErrorCode::new("format.unsupported_range"));
901 }
902
903 #[test]
904 fn format_with_bit_depth_preserves_family_and_subsampling() {
905 let yuv = resolve_format_alias("yuv420p8").expect("alias should resolve");
906 let rgb = resolve_format_alias("rgbp16").expect("alias should resolve");
907 let gray = resolve_format_alias("grayf32").expect("alias should resolve");
908
909 assert_eq!(
910 format_with_bit_depth(&yuv, 10)
911 .expect("10-bit YUV should derive")
912 .name(),
913 "yuv420p10"
914 );
915 assert_eq!(
916 format_with_bit_depth(&rgb, 32)
917 .expect("f32 RGB should derive")
918 .name(),
919 "rgbpf32"
920 );
921 assert_eq!(
922 format_with_bit_depth(&gray, 8)
923 .expect("8-bit gray should derive")
924 .name(),
925 "gray8"
926 );
927 }
928
929 #[test]
930 fn format_with_bit_depth_rejects_unsupported_depth() {
931 let format = resolve_format_alias("gray8").expect("alias should resolve");
932 let error = format_with_bit_depth(&format, 14).expect_err("14-bit output should fail");
933
934 assert_eq!(error.category(), ErrorCategory::Format);
935 assert_eq!(error.code(), ErrorCode::new("format.unsupported_bit_depth"));
936 }
937
938 #[test]
939 fn resolves_yuv420p10_to_stable_descriptor() {
940 let descriptor = resolve_format_alias("yuv420p10").expect("alias should resolve");
941
942 assert_eq!(descriptor.name(), "yuv420p10");
943 assert_eq!(descriptor.family(), FormatFamily::Yuv);
944 assert_eq!(descriptor.subsampling(), Some(ChromaSubsampling::Cs420));
945 assert_eq!(descriptor.bits_per_sample(), 10);
946 assert_eq!(descriptor.sample_type(), SampleType::U16);
947 assert_eq!(descriptor.planes().len(), 3);
948 assert_eq!(descriptor.planes()[0].role, PlaneRole::Y);
949 assert_eq!(descriptor.planes()[1].role, PlaneRole::U);
950 assert_eq!(descriptor.planes()[1].width_divisor, 2);
951 assert_eq!(descriptor.planes()[1].height_divisor, 2);
952 }
953
954 #[test]
955 fn resolves_planar_rgb_aliases_to_same_descriptor() {
956 let rgb = resolve_format_alias("rgbp10").expect("alias should resolve");
957 let gbr = resolve_format_alias("gbrp10").expect("alias should resolve");
958
959 assert_eq!(rgb, gbr);
960 assert_eq!(rgb.family(), FormatFamily::PlanarRgb);
961 assert_eq!(rgb.planes()[0].role, PlaneRole::R);
962 assert_eq!(rgb.planes()[1].role, PlaneRole::G);
963 assert_eq!(rgb.planes()[2].role, PlaneRole::B);
964 }
965
966 #[test]
967 fn resolves_float_gray_descriptor() {
968 let descriptor = resolve_format_alias("grayf32").expect("alias should resolve");
969
970 assert_eq!(descriptor.name(), "grayf32");
971 assert_eq!(descriptor.family(), FormatFamily::Gray);
972 assert_eq!(descriptor.bits_per_sample(), 32);
973 assert_eq!(descriptor.sample_type(), SampleType::F32);
974 assert_eq!(descriptor.planes()[0].role, PlaneRole::Gray);
975 }
976
977 #[test]
978 fn unsupported_alias_returns_structured_format_error() {
979 let error = resolve_format_alias("yuv420p14").expect_err("unsupported alias should fail");
980
981 assert_eq!(error.category(), ErrorCategory::Format);
982 assert_eq!(error.code(), ErrorCode::new("format.unsupported_alias"));
983 assert!(error.message().contains("yuv420p14"));
984 }
985}