Skip to main content

ff_format/
color.rs

1//! Color space and related type definitions.
2//!
3//! This module provides enums for color-related metadata commonly found
4//! in video streams, including color space, color range, and color primaries.
5//!
6//! # Examples
7//!
8//! ```
9//! use ff_format::color::{ColorSpace, ColorRange, ColorPrimaries};
10//!
11//! // HD video typically uses BT.709
12//! let space = ColorSpace::Bt709;
13//! let range = ColorRange::Limited;
14//! let primaries = ColorPrimaries::Bt709;
15//!
16//! assert!(space.is_hd());
17//! assert!(!range.is_full());
18//! ```
19
20use std::fmt;
21
22/// Color space (matrix coefficients) for YUV to RGB conversion.
23///
24/// The color space defines how YUV values are converted to RGB and vice versa.
25/// Different standards use different matrix coefficients for this conversion.
26///
27/// # Common Usage
28///
29/// - **BT.709**: HD content (720p, 1080p)
30/// - **BT.601**: SD content (480i, 576i)
31/// - **BT.2020**: UHD/HDR content (4K, 8K)
32/// - **sRGB**: Computer graphics, web content
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
34#[non_exhaustive]
35pub enum ColorSpace {
36    /// ITU-R BT.709 - HD television standard (most common for HD video)
37    #[default]
38    Bt709,
39    /// ITU-R BT.601 - SD television standard
40    Bt601,
41    /// ITU-R BT.2020 - UHD/HDR television standard
42    Bt2020,
43    /// sRGB color space - computer graphics and web
44    Srgb,
45    /// Color space is not specified or unknown
46    Unknown,
47}
48
49impl ColorSpace {
50    /// Returns the name of the color space as a human-readable string.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use ff_format::color::ColorSpace;
56    ///
57    /// assert_eq!(ColorSpace::Bt709.name(), "bt709");
58    /// assert_eq!(ColorSpace::Bt601.name(), "bt601");
59    /// ```
60    #[must_use]
61    pub const fn name(&self) -> &'static str {
62        match self {
63            Self::Bt709 => "bt709",
64            Self::Bt601 => "bt601",
65            Self::Bt2020 => "bt2020",
66            Self::Srgb => "srgb",
67            Self::Unknown => "unknown",
68        }
69    }
70
71    /// Returns `true` if this is an HD color space (BT.709).
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use ff_format::color::ColorSpace;
77    ///
78    /// assert!(ColorSpace::Bt709.is_hd());
79    /// assert!(!ColorSpace::Bt601.is_hd());
80    /// ```
81    #[must_use]
82    pub const fn is_hd(&self) -> bool {
83        matches!(self, Self::Bt709)
84    }
85
86    /// Returns `true` if this is an SD color space (BT.601).
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use ff_format::color::ColorSpace;
92    ///
93    /// assert!(ColorSpace::Bt601.is_sd());
94    /// assert!(!ColorSpace::Bt709.is_sd());
95    /// ```
96    #[must_use]
97    pub const fn is_sd(&self) -> bool {
98        matches!(self, Self::Bt601)
99    }
100
101    /// Returns `true` if this is a UHD/HDR color space (BT.2020).
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use ff_format::color::ColorSpace;
107    ///
108    /// assert!(ColorSpace::Bt2020.is_uhd());
109    /// assert!(!ColorSpace::Bt709.is_uhd());
110    /// ```
111    #[must_use]
112    pub const fn is_uhd(&self) -> bool {
113        matches!(self, Self::Bt2020)
114    }
115
116    /// Returns `true` if the color space is unknown.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use ff_format::color::ColorSpace;
122    ///
123    /// assert!(ColorSpace::Unknown.is_unknown());
124    /// assert!(!ColorSpace::Bt709.is_unknown());
125    /// ```
126    #[must_use]
127    pub const fn is_unknown(&self) -> bool {
128        matches!(self, Self::Unknown)
129    }
130}
131
132impl fmt::Display for ColorSpace {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}", self.name())
135    }
136}
137
138/// Color range defining the valid range of color values.
139///
140/// Video typically uses "limited" range where black is at level 16 and white
141/// at level 235 (for 8-bit). Computer graphics typically use "full" range
142/// where black is 0 and white is 255.
143///
144/// # Common Usage
145///
146/// - **Limited**: Broadcast video, Blu-ray, streaming services
147/// - **Full**: Computer graphics, screenshots, game capture
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
149#[non_exhaustive]
150pub enum ColorRange {
151    /// Limited/TV range (16-235 for Y, 16-240 for UV in 8-bit)
152    #[default]
153    Limited,
154    /// Full/PC range (0-255 for all components in 8-bit)
155    Full,
156    /// Color range is not specified or unknown
157    Unknown,
158}
159
160impl ColorRange {
161    /// Returns the name of the color range as a human-readable string.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use ff_format::color::ColorRange;
167    ///
168    /// assert_eq!(ColorRange::Limited.name(), "limited");
169    /// assert_eq!(ColorRange::Full.name(), "full");
170    /// ```
171    #[must_use]
172    pub const fn name(&self) -> &'static str {
173        match self {
174            Self::Limited => "limited",
175            Self::Full => "full",
176            Self::Unknown => "unknown",
177        }
178    }
179
180    /// Returns `true` if this is full (PC) range.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use ff_format::color::ColorRange;
186    ///
187    /// assert!(ColorRange::Full.is_full());
188    /// assert!(!ColorRange::Limited.is_full());
189    /// ```
190    #[must_use]
191    pub const fn is_full(&self) -> bool {
192        matches!(self, Self::Full)
193    }
194
195    /// Returns `true` if this is limited (TV) range.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use ff_format::color::ColorRange;
201    ///
202    /// assert!(ColorRange::Limited.is_limited());
203    /// assert!(!ColorRange::Full.is_limited());
204    /// ```
205    #[must_use]
206    pub const fn is_limited(&self) -> bool {
207        matches!(self, Self::Limited)
208    }
209
210    /// Returns `true` if the color range is unknown.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use ff_format::color::ColorRange;
216    ///
217    /// assert!(ColorRange::Unknown.is_unknown());
218    /// assert!(!ColorRange::Limited.is_unknown());
219    /// ```
220    #[must_use]
221    pub const fn is_unknown(&self) -> bool {
222        matches!(self, Self::Unknown)
223    }
224
225    /// Returns the minimum value for luma (Y) in 8-bit.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use ff_format::color::ColorRange;
231    ///
232    /// assert_eq!(ColorRange::Limited.luma_min_8bit(), 16);
233    /// assert_eq!(ColorRange::Full.luma_min_8bit(), 0);
234    /// ```
235    #[must_use]
236    pub const fn luma_min_8bit(&self) -> u8 {
237        match self {
238            Self::Limited => 16,
239            Self::Full | Self::Unknown => 0,
240        }
241    }
242
243    /// Returns the maximum value for luma (Y) in 8-bit.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use ff_format::color::ColorRange;
249    ///
250    /// assert_eq!(ColorRange::Limited.luma_max_8bit(), 235);
251    /// assert_eq!(ColorRange::Full.luma_max_8bit(), 255);
252    /// ```
253    #[must_use]
254    pub const fn luma_max_8bit(&self) -> u8 {
255        match self {
256            Self::Limited => 235,
257            Self::Full | Self::Unknown => 255,
258        }
259    }
260}
261
262impl fmt::Display for ColorRange {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "{}", self.name())
265    }
266}
267
268/// Color primaries defining the color gamut (the range of colors that can be represented).
269///
270/// Different standards define different primary colors (red, green, blue points)
271/// which determine the overall range of colors that can be displayed.
272///
273/// # Common Usage
274///
275/// - **BT.709**: HD content, same as sRGB primaries
276/// - **BT.601**: SD content (NTSC or PAL)
277/// - **BT.2020**: Wide color gamut for UHD/HDR
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
279#[non_exhaustive]
280pub enum ColorPrimaries {
281    /// ITU-R BT.709 primaries (same as sRGB, most common)
282    #[default]
283    Bt709,
284    /// ITU-R BT.601 primaries (SD video)
285    Bt601,
286    /// ITU-R BT.2020 primaries (wide color gamut for UHD/HDR)
287    Bt2020,
288    /// Color primaries are not specified or unknown
289    Unknown,
290}
291
292impl ColorPrimaries {
293    /// Returns the name of the color primaries as a human-readable string.
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use ff_format::color::ColorPrimaries;
299    ///
300    /// assert_eq!(ColorPrimaries::Bt709.name(), "bt709");
301    /// assert_eq!(ColorPrimaries::Bt2020.name(), "bt2020");
302    /// ```
303    #[must_use]
304    pub const fn name(&self) -> &'static str {
305        match self {
306            Self::Bt709 => "bt709",
307            Self::Bt601 => "bt601",
308            Self::Bt2020 => "bt2020",
309            Self::Unknown => "unknown",
310        }
311    }
312
313    /// Returns `true` if this uses wide color gamut (BT.2020).
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use ff_format::color::ColorPrimaries;
319    ///
320    /// assert!(ColorPrimaries::Bt2020.is_wide_gamut());
321    /// assert!(!ColorPrimaries::Bt709.is_wide_gamut());
322    /// ```
323    #[must_use]
324    pub const fn is_wide_gamut(&self) -> bool {
325        matches!(self, Self::Bt2020)
326    }
327
328    /// Returns `true` if the color primaries are unknown.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use ff_format::color::ColorPrimaries;
334    ///
335    /// assert!(ColorPrimaries::Unknown.is_unknown());
336    /// assert!(!ColorPrimaries::Bt709.is_unknown());
337    /// ```
338    #[must_use]
339    pub const fn is_unknown(&self) -> bool {
340        matches!(self, Self::Unknown)
341    }
342}
343
344impl fmt::Display for ColorPrimaries {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        write!(f, "{}", self.name())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    mod color_space_tests {
355        use super::*;
356
357        #[test]
358        fn test_names() {
359            assert_eq!(ColorSpace::Bt709.name(), "bt709");
360            assert_eq!(ColorSpace::Bt601.name(), "bt601");
361            assert_eq!(ColorSpace::Bt2020.name(), "bt2020");
362            assert_eq!(ColorSpace::Srgb.name(), "srgb");
363            assert_eq!(ColorSpace::Unknown.name(), "unknown");
364        }
365
366        #[test]
367        fn test_display() {
368            assert_eq!(format!("{}", ColorSpace::Bt709), "bt709");
369            assert_eq!(format!("{}", ColorSpace::Bt2020), "bt2020");
370        }
371
372        #[test]
373        fn test_default() {
374            assert_eq!(ColorSpace::default(), ColorSpace::Bt709);
375        }
376
377        #[test]
378        fn test_is_hd_sd_uhd() {
379            assert!(ColorSpace::Bt709.is_hd());
380            assert!(!ColorSpace::Bt709.is_sd());
381            assert!(!ColorSpace::Bt709.is_uhd());
382
383            assert!(!ColorSpace::Bt601.is_hd());
384            assert!(ColorSpace::Bt601.is_sd());
385            assert!(!ColorSpace::Bt601.is_uhd());
386
387            assert!(!ColorSpace::Bt2020.is_hd());
388            assert!(!ColorSpace::Bt2020.is_sd());
389            assert!(ColorSpace::Bt2020.is_uhd());
390        }
391
392        #[test]
393        fn test_is_unknown() {
394            assert!(ColorSpace::Unknown.is_unknown());
395            assert!(!ColorSpace::Bt709.is_unknown());
396        }
397
398        #[test]
399        fn test_debug() {
400            assert_eq!(format!("{:?}", ColorSpace::Bt709), "Bt709");
401            assert_eq!(format!("{:?}", ColorSpace::Srgb), "Srgb");
402        }
403
404        #[test]
405        fn test_equality_and_hash() {
406            use std::collections::HashSet;
407
408            assert_eq!(ColorSpace::Bt709, ColorSpace::Bt709);
409            assert_ne!(ColorSpace::Bt709, ColorSpace::Bt601);
410
411            let mut set = HashSet::new();
412            set.insert(ColorSpace::Bt709);
413            set.insert(ColorSpace::Bt601);
414            assert!(set.contains(&ColorSpace::Bt709));
415            assert!(!set.contains(&ColorSpace::Bt2020));
416        }
417
418        #[test]
419        fn test_copy() {
420            let space = ColorSpace::Bt709;
421            let copied = space;
422            assert_eq!(space, copied);
423        }
424    }
425
426    mod color_range_tests {
427        use super::*;
428
429        #[test]
430        fn test_names() {
431            assert_eq!(ColorRange::Limited.name(), "limited");
432            assert_eq!(ColorRange::Full.name(), "full");
433            assert_eq!(ColorRange::Unknown.name(), "unknown");
434        }
435
436        #[test]
437        fn test_display() {
438            assert_eq!(format!("{}", ColorRange::Limited), "limited");
439            assert_eq!(format!("{}", ColorRange::Full), "full");
440        }
441
442        #[test]
443        fn test_default() {
444            assert_eq!(ColorRange::default(), ColorRange::Limited);
445        }
446
447        #[test]
448        fn test_is_full_limited() {
449            assert!(ColorRange::Full.is_full());
450            assert!(!ColorRange::Full.is_limited());
451
452            assert!(!ColorRange::Limited.is_full());
453            assert!(ColorRange::Limited.is_limited());
454        }
455
456        #[test]
457        fn test_is_unknown() {
458            assert!(ColorRange::Unknown.is_unknown());
459            assert!(!ColorRange::Limited.is_unknown());
460        }
461
462        #[test]
463        fn test_luma_values() {
464            assert_eq!(ColorRange::Limited.luma_min_8bit(), 16);
465            assert_eq!(ColorRange::Limited.luma_max_8bit(), 235);
466
467            assert_eq!(ColorRange::Full.luma_min_8bit(), 0);
468            assert_eq!(ColorRange::Full.luma_max_8bit(), 255);
469
470            assert_eq!(ColorRange::Unknown.luma_min_8bit(), 0);
471            assert_eq!(ColorRange::Unknown.luma_max_8bit(), 255);
472        }
473
474        #[test]
475        fn test_equality_and_hash() {
476            use std::collections::HashSet;
477
478            assert_eq!(ColorRange::Limited, ColorRange::Limited);
479            assert_ne!(ColorRange::Limited, ColorRange::Full);
480
481            let mut set = HashSet::new();
482            set.insert(ColorRange::Limited);
483            set.insert(ColorRange::Full);
484            assert!(set.contains(&ColorRange::Limited));
485            assert!(!set.contains(&ColorRange::Unknown));
486        }
487    }
488
489    mod color_primaries_tests {
490        use super::*;
491
492        #[test]
493        fn test_names() {
494            assert_eq!(ColorPrimaries::Bt709.name(), "bt709");
495            assert_eq!(ColorPrimaries::Bt601.name(), "bt601");
496            assert_eq!(ColorPrimaries::Bt2020.name(), "bt2020");
497            assert_eq!(ColorPrimaries::Unknown.name(), "unknown");
498        }
499
500        #[test]
501        fn test_display() {
502            assert_eq!(format!("{}", ColorPrimaries::Bt709), "bt709");
503            assert_eq!(format!("{}", ColorPrimaries::Bt2020), "bt2020");
504        }
505
506        #[test]
507        fn test_default() {
508            assert_eq!(ColorPrimaries::default(), ColorPrimaries::Bt709);
509        }
510
511        #[test]
512        fn test_is_wide_gamut() {
513            assert!(ColorPrimaries::Bt2020.is_wide_gamut());
514            assert!(!ColorPrimaries::Bt709.is_wide_gamut());
515            assert!(!ColorPrimaries::Bt601.is_wide_gamut());
516        }
517
518        #[test]
519        fn test_is_unknown() {
520            assert!(ColorPrimaries::Unknown.is_unknown());
521            assert!(!ColorPrimaries::Bt709.is_unknown());
522        }
523
524        #[test]
525        fn test_equality_and_hash() {
526            use std::collections::HashSet;
527
528            assert_eq!(ColorPrimaries::Bt709, ColorPrimaries::Bt709);
529            assert_ne!(ColorPrimaries::Bt709, ColorPrimaries::Bt2020);
530
531            let mut set = HashSet::new();
532            set.insert(ColorPrimaries::Bt709);
533            set.insert(ColorPrimaries::Bt2020);
534            assert!(set.contains(&ColorPrimaries::Bt709));
535            assert!(!set.contains(&ColorPrimaries::Bt601));
536        }
537    }
538}