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.470BG / SMPTE-170M**: SD content (576i PAL / 480i NTSC)
31/// - **BT.2020 NCL / CL**: UHD/HDR content (4K, 8K)
32/// - **RGB**: Identity matrix for RGB/GBR content
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
34#[non_exhaustive]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum ColorSpace {
37    /// ITU-R BT.709 — HD television matrix (most common for HD video)
38    #[default]
39    Bt709,
40    /// ITU-R BT.470BG — BT.601 625-line (PAL/SECAM SD) matrix
41    Bt470bg,
42    /// SMPTE 170M — BT.601 525-line (NTSC SD) matrix
43    Smpte170m,
44    /// ITU-R BT.2020 non-constant luminance — UHD/HDR matrix
45    Bt2020Ncl,
46    /// ITU-R BT.2020 constant luminance — UHD/HDR matrix
47    Bt2020Cl,
48    /// Identity / RGB (GBR planar) — no YUV matrix
49    Rgb,
50    /// FCC — legacy NTSC 1953 matrix
51    Fcc,
52    /// SMPTE 240M — legacy HD matrix
53    Smpte240m,
54    /// `YCgCo` — reversible `YCgCo` matrix
55    Ycgco,
56    /// Color space matrix is not specified or unknown
57    Unknown,
58}
59
60impl ColorSpace {
61    /// Returns the name of the color space as a human-readable string.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use ff_format::color::ColorSpace;
67    ///
68    /// assert_eq!(ColorSpace::Bt709.name(), "bt709");
69    /// assert_eq!(ColorSpace::Bt2020Ncl.name(), "bt2020ncl");
70    /// ```
71    #[must_use]
72    pub const fn name(&self) -> &'static str {
73        match self {
74            Self::Bt709 => "bt709",
75            Self::Bt470bg => "bt470bg",
76            Self::Smpte170m => "smpte170m",
77            Self::Bt2020Ncl => "bt2020ncl",
78            Self::Bt2020Cl => "bt2020cl",
79            Self::Rgb => "rgb",
80            Self::Fcc => "fcc",
81            Self::Smpte240m => "smpte240m",
82            Self::Ycgco => "ycgco",
83            Self::Unknown => "unknown",
84        }
85    }
86
87    /// Returns `true` if this is the HD matrix (BT.709).
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use ff_format::color::ColorSpace;
93    ///
94    /// assert!(ColorSpace::Bt709.is_hd());
95    /// assert!(!ColorSpace::Smpte170m.is_hd());
96    /// ```
97    #[must_use]
98    pub const fn is_hd(&self) -> bool {
99        matches!(self, Self::Bt709)
100    }
101
102    /// Returns `true` if this is an SD matrix (BT.601: BT.470BG or SMPTE-170M).
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use ff_format::color::ColorSpace;
108    ///
109    /// assert!(ColorSpace::Smpte170m.is_sd());
110    /// assert!(!ColorSpace::Bt709.is_sd());
111    /// ```
112    #[must_use]
113    pub const fn is_sd(&self) -> bool {
114        matches!(self, Self::Bt470bg | Self::Smpte170m)
115    }
116
117    /// Returns `true` if this is a UHD/HDR matrix (BT.2020 NCL or CL).
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use ff_format::color::ColorSpace;
123    ///
124    /// assert!(ColorSpace::Bt2020Ncl.is_uhd());
125    /// assert!(!ColorSpace::Bt709.is_uhd());
126    /// ```
127    #[must_use]
128    pub const fn is_uhd(&self) -> bool {
129        matches!(self, Self::Bt2020Ncl | Self::Bt2020Cl)
130    }
131
132    /// Returns `true` if the color space is unknown.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use ff_format::color::ColorSpace;
138    ///
139    /// assert!(ColorSpace::Unknown.is_unknown());
140    /// assert!(!ColorSpace::Bt709.is_unknown());
141    /// ```
142    #[must_use]
143    pub const fn is_unknown(&self) -> bool {
144        matches!(self, Self::Unknown)
145    }
146}
147
148impl fmt::Display for ColorSpace {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "{}", self.name())
151    }
152}
153
154/// Color range defining the valid range of color values.
155///
156/// Video typically uses "limited" range where black is at level 16 and white
157/// at level 235 (for 8-bit). Computer graphics typically use "full" range
158/// where black is 0 and white is 255.
159///
160/// # Common Usage
161///
162/// - **Limited**: Broadcast video, Blu-ray, streaming services
163/// - **Full**: Computer graphics, screenshots, game capture
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
165#[non_exhaustive]
166#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167pub enum ColorRange {
168    /// Limited/TV range (16-235 for Y, 16-240 for UV in 8-bit)
169    #[default]
170    Limited,
171    /// Full/PC range (0-255 for all components in 8-bit)
172    Full,
173    /// Color range is not specified or unknown
174    Unknown,
175}
176
177impl ColorRange {
178    /// Returns the name of the color range as a human-readable string.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use ff_format::color::ColorRange;
184    ///
185    /// assert_eq!(ColorRange::Limited.name(), "limited");
186    /// assert_eq!(ColorRange::Full.name(), "full");
187    /// ```
188    #[must_use]
189    pub const fn name(&self) -> &'static str {
190        match self {
191            Self::Limited => "limited",
192            Self::Full => "full",
193            Self::Unknown => "unknown",
194        }
195    }
196
197    /// Returns `true` if this is full (PC) range.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use ff_format::color::ColorRange;
203    ///
204    /// assert!(ColorRange::Full.is_full());
205    /// assert!(!ColorRange::Limited.is_full());
206    /// ```
207    #[must_use]
208    pub const fn is_full(&self) -> bool {
209        matches!(self, Self::Full)
210    }
211
212    /// Returns `true` if this is limited (TV) range.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use ff_format::color::ColorRange;
218    ///
219    /// assert!(ColorRange::Limited.is_limited());
220    /// assert!(!ColorRange::Full.is_limited());
221    /// ```
222    #[must_use]
223    pub const fn is_limited(&self) -> bool {
224        matches!(self, Self::Limited)
225    }
226
227    /// Returns `true` if the color range is unknown.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use ff_format::color::ColorRange;
233    ///
234    /// assert!(ColorRange::Unknown.is_unknown());
235    /// assert!(!ColorRange::Limited.is_unknown());
236    /// ```
237    #[must_use]
238    pub const fn is_unknown(&self) -> bool {
239        matches!(self, Self::Unknown)
240    }
241
242    /// Returns the minimum value for luma (Y) in 8-bit.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use ff_format::color::ColorRange;
248    ///
249    /// assert_eq!(ColorRange::Limited.luma_min_8bit(), 16);
250    /// assert_eq!(ColorRange::Full.luma_min_8bit(), 0);
251    /// ```
252    #[must_use]
253    pub const fn luma_min_8bit(&self) -> u8 {
254        match self {
255            Self::Limited => 16,
256            Self::Full | Self::Unknown => 0,
257        }
258    }
259
260    /// Returns the maximum value for luma (Y) in 8-bit.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use ff_format::color::ColorRange;
266    ///
267    /// assert_eq!(ColorRange::Limited.luma_max_8bit(), 235);
268    /// assert_eq!(ColorRange::Full.luma_max_8bit(), 255);
269    /// ```
270    #[must_use]
271    pub const fn luma_max_8bit(&self) -> u8 {
272        match self {
273            Self::Limited => 235,
274            Self::Full | Self::Unknown => 255,
275        }
276    }
277}
278
279impl fmt::Display for ColorRange {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        write!(f, "{}", self.name())
282    }
283}
284
285/// Color primaries defining the color gamut (the range of colors that can be represented).
286///
287/// Different standards define different primary colors (red, green, blue points)
288/// which determine the overall range of colors that can be displayed.
289///
290/// # Common Usage
291///
292/// - **BT.709**: HD content, same as sRGB primaries
293/// - **BT.470BG / SMPTE-170M**: SD content (PAL/SECAM / NTSC)
294/// - **DCI-P3 / Display P3**: digital cinema and wide-gamut displays
295/// - **BT.2020**: Wide color gamut for UHD/HDR
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
297#[non_exhaustive]
298#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
299pub enum ColorPrimaries {
300    /// ITU-R BT.709 primaries (same as sRGB, most common)
301    #[default]
302    Bt709,
303    /// ITU-R BT.470BG primaries (PAL/SECAM SD video)
304    Bt470bg,
305    /// SMPTE 170M primaries (NTSC SD video)
306    Smpte170m,
307    /// SMPTE 240M primaries (legacy HD)
308    Smpte240m,
309    /// Generic film primaries (Illuminant C)
310    Film,
311    /// DCI-P3 primaries (SMPTE RP 431-2, digital cinema)
312    DciP3,
313    /// Display P3 primaries (SMPTE EG 432-1, wide-gamut displays)
314    DisplayP3,
315    /// ITU-R BT.2020 primaries (wide color gamut for UHD/HDR)
316    Bt2020,
317    /// Color primaries are not specified or unknown
318    Unknown,
319}
320
321impl ColorPrimaries {
322    /// Returns the name of the color primaries as a human-readable string.
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// use ff_format::color::ColorPrimaries;
328    ///
329    /// assert_eq!(ColorPrimaries::Bt709.name(), "bt709");
330    /// assert_eq!(ColorPrimaries::Bt2020.name(), "bt2020");
331    /// ```
332    #[must_use]
333    pub const fn name(&self) -> &'static str {
334        match self {
335            Self::Bt709 => "bt709",
336            Self::Bt470bg => "bt470bg",
337            Self::Smpte170m => "smpte170m",
338            Self::Smpte240m => "smpte240m",
339            Self::Film => "film",
340            Self::DciP3 => "dci-p3",
341            Self::DisplayP3 => "display-p3",
342            Self::Bt2020 => "bt2020",
343            Self::Unknown => "unknown",
344        }
345    }
346
347    /// Returns `true` if this uses a wide color gamut (BT.2020, DCI-P3, or Display P3).
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use ff_format::color::ColorPrimaries;
353    ///
354    /// assert!(ColorPrimaries::Bt2020.is_wide_gamut());
355    /// assert!(ColorPrimaries::DciP3.is_wide_gamut());
356    /// assert!(!ColorPrimaries::Bt709.is_wide_gamut());
357    /// ```
358    #[must_use]
359    pub const fn is_wide_gamut(&self) -> bool {
360        matches!(self, Self::Bt2020 | Self::DciP3 | Self::DisplayP3)
361    }
362
363    /// Returns `true` if the color primaries are unknown.
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use ff_format::color::ColorPrimaries;
369    ///
370    /// assert!(ColorPrimaries::Unknown.is_unknown());
371    /// assert!(!ColorPrimaries::Bt709.is_unknown());
372    /// ```
373    #[must_use]
374    pub const fn is_unknown(&self) -> bool {
375        matches!(self, Self::Unknown)
376    }
377}
378
379impl fmt::Display for ColorPrimaries {
380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381        write!(f, "{}", self.name())
382    }
383}
384
385/// Color transfer characteristic (opto-electronic transfer function).
386///
387/// The transfer characteristic defines how scene luminance maps to the signal
388/// level stored in the video bitstream. Different HDR and SDR standards use
389/// different curves.
390///
391/// # Common Usage
392///
393/// - **`Bt709`**: Standard SDR video (HD television)
394/// - **`Gamma22`** / **`Gamma28`**: Pure power-law gamma 2.2 / 2.8 (legacy SDR)
395/// - **`Smpte170m`** / **`Smpte240m`**: SD / legacy-HD transfer characteristics
396/// - **`Srgb`**: sRGB / IEC 61966-2-1 (computer graphics, web)
397/// - **`Pq`**: HDR10 and Dolby Vision (SMPTE ST 2084 / Perceptual Quantizer)
398/// - **`Hlg`**: Hybrid Log-Gamma — broadcast-compatible HDR (ARIB STD-B67)
399/// - **`Bt2020_10`** / **`Bt2020_12`**: BT.2020 SDR at 10/12-bit depth
400/// - **`Linear`**: Linear light, no gamma applied
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
402#[non_exhaustive]
403#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404pub enum ColorTransfer {
405    /// ITU-R BT.709 transfer characteristic (standard SDR)
406    #[default]
407    Bt709,
408    /// Pure power-law gamma 2.2 (assumed display gamma)
409    Gamma22,
410    /// Pure power-law gamma 2.8 (BT.470 System B/G)
411    Gamma28,
412    /// SMPTE 170M transfer characteristic (SD)
413    Smpte170m,
414    /// SMPTE 240M transfer characteristic (legacy HD)
415    Smpte240m,
416    /// Linear light transfer (no gamma)
417    Linear,
418    /// sRGB / IEC 61966-2-1 transfer characteristic
419    Srgb,
420    /// ITU-R BT.2020 for 10-bit content
421    Bt2020_10,
422    /// ITU-R BT.2020 for 12-bit content
423    Bt2020_12,
424    /// Perceptual Quantizer / SMPTE ST 2084 — HDR10
425    Pq,
426    /// Hybrid Log-Gamma (ARIB STD-B67) — broadcast HDR
427    Hlg,
428    /// Transfer characteristic is not specified or unknown
429    Unknown,
430}
431
432impl ColorTransfer {
433    /// Returns the name of the color transfer characteristic as a string.
434    ///
435    /// # Examples
436    ///
437    /// ```
438    /// use ff_format::color::ColorTransfer;
439    ///
440    /// assert_eq!(ColorTransfer::Bt709.name(), "bt709");
441    /// assert_eq!(ColorTransfer::Hlg.name(), "hlg");
442    /// assert_eq!(ColorTransfer::Pq.name(), "pq");
443    /// ```
444    #[must_use]
445    pub const fn name(&self) -> &'static str {
446        match self {
447            Self::Bt709 => "bt709",
448            Self::Gamma22 => "gamma22",
449            Self::Gamma28 => "gamma28",
450            Self::Smpte170m => "smpte170m",
451            Self::Smpte240m => "smpte240m",
452            Self::Linear => "linear",
453            Self::Srgb => "srgb",
454            Self::Bt2020_10 => "bt2020-10",
455            Self::Bt2020_12 => "bt2020-12",
456            Self::Pq => "pq",
457            Self::Hlg => "hlg",
458            Self::Unknown => "unknown",
459        }
460    }
461
462    /// Returns `true` if this is an HDR transfer characteristic (`Pq` or `Hlg`).
463    ///
464    /// # Examples
465    ///
466    /// ```
467    /// use ff_format::color::ColorTransfer;
468    ///
469    /// assert!(ColorTransfer::Pq.is_hdr());
470    /// assert!(ColorTransfer::Hlg.is_hdr());
471    /// assert!(!ColorTransfer::Bt709.is_hdr());
472    /// ```
473    #[must_use]
474    pub const fn is_hdr(&self) -> bool {
475        matches!(self, Self::Pq | Self::Hlg)
476    }
477
478    /// Returns `true` if this is Hybrid Log-Gamma (HLG).
479    ///
480    /// # Examples
481    ///
482    /// ```
483    /// use ff_format::color::ColorTransfer;
484    ///
485    /// assert!(ColorTransfer::Hlg.is_hlg());
486    /// assert!(!ColorTransfer::Pq.is_hlg());
487    /// ```
488    #[must_use]
489    pub const fn is_hlg(&self) -> bool {
490        matches!(self, Self::Hlg)
491    }
492
493    /// Returns `true` if this is Perceptual Quantizer / SMPTE ST 2084 (PQ).
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use ff_format::color::ColorTransfer;
499    ///
500    /// assert!(ColorTransfer::Pq.is_pq());
501    /// assert!(!ColorTransfer::Hlg.is_pq());
502    /// ```
503    #[must_use]
504    pub const fn is_pq(&self) -> bool {
505        matches!(self, Self::Pq)
506    }
507
508    /// Returns `true` if the transfer characteristic is unknown.
509    ///
510    /// # Examples
511    ///
512    /// ```
513    /// use ff_format::color::ColorTransfer;
514    ///
515    /// assert!(ColorTransfer::Unknown.is_unknown());
516    /// assert!(!ColorTransfer::Bt709.is_unknown());
517    /// ```
518    #[must_use]
519    pub const fn is_unknown(&self) -> bool {
520        matches!(self, Self::Unknown)
521    }
522}
523
524impl fmt::Display for ColorTransfer {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        write!(f, "{}", self.name())
527    }
528}
529
530/// Alpha modes.
531///
532/// The alpha mode defines how the alpha channel should be handled when
533/// converting video frames.
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
535#[non_exhaustive]
536pub enum AlphaMode {
537    /// Unassociated alpha.
538    #[default]
539    Straight,
540    /// Associated alpha.
541    Premultiplied,
542    /// Alpha mode is not specified or unknown
543    Unknown,
544}
545
546impl AlphaMode {
547    /// Returns the name of the alpha mode as a string.
548    ///
549    /// # Examples
550    ///
551    /// ```
552    /// use ff_format::color::AlphaMode;
553    ///
554    /// assert_eq!(AlphaMode::Straight.name(), "straight");
555    /// assert_eq!(AlphaMode::Premultiplied.name(), "premultiplied");
556    /// ```
557    #[must_use]
558    pub const fn name(&self) -> &'static str {
559        match self {
560            Self::Straight => "straight",
561            Self::Premultiplied => "premultiplied",
562            Self::Unknown => "unknown",
563        }
564    }
565
566    /// Returns `true` if this is a straight alpha mode.
567    ///
568    /// # Examples
569    ///
570    /// ```
571    /// use ff_format::color::AlphaMode;
572    ///
573    /// assert!(AlphaMode::Straight.is_straight());
574    /// assert!(!AlphaMode::Premultiplied.is_straight());
575    /// ```
576    #[must_use]
577    pub const fn is_straight(&self) -> bool {
578        matches!(self, Self::Straight)
579    }
580
581    /// Returns `true` if this is a premultiplied alpha mode.
582    ///
583    /// # Examples
584    ///
585    /// ```
586    /// use ff_format::color::AlphaMode;
587    ///
588    /// assert!(AlphaMode::Premultiplied.is_premultiplied());
589    /// assert!(!AlphaMode::Straight.is_premultiplied());
590    /// ```
591    #[must_use]
592    pub const fn is_premultiplied(&self) -> bool {
593        matches!(self, Self::Premultiplied)
594    }
595
596    /// Returns `true` if the alpha mode is unknown.
597    ///
598    /// # Examples
599    ///
600    /// ```
601    /// use ff_format::color::AlphaMode;
602    ///
603    /// assert!(AlphaMode::Unknown.is_unknown());
604    /// assert!(!AlphaMode::Straight.is_unknown());
605    /// ```
606    #[must_use]
607    pub const fn is_unknown(&self) -> bool {
608        matches!(self, Self::Unknown)
609    }
610}
611
612impl fmt::Display for AlphaMode {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        write!(f, "{}", self.name())
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621
622    mod color_space_tests {
623        use super::*;
624
625        #[test]
626        fn test_names() {
627            assert_eq!(ColorSpace::Bt709.name(), "bt709");
628            assert_eq!(ColorSpace::Bt470bg.name(), "bt470bg");
629            assert_eq!(ColorSpace::Smpte170m.name(), "smpte170m");
630            assert_eq!(ColorSpace::Bt2020Ncl.name(), "bt2020ncl");
631            assert_eq!(ColorSpace::Bt2020Cl.name(), "bt2020cl");
632            assert_eq!(ColorSpace::Rgb.name(), "rgb");
633            assert_eq!(ColorSpace::Fcc.name(), "fcc");
634            assert_eq!(ColorSpace::Smpte240m.name(), "smpte240m");
635            assert_eq!(ColorSpace::Ycgco.name(), "ycgco");
636            assert_eq!(ColorSpace::Unknown.name(), "unknown");
637        }
638
639        #[test]
640        fn test_display() {
641            assert_eq!(format!("{}", ColorSpace::Bt709), "bt709");
642            assert_eq!(format!("{}", ColorSpace::Bt2020Ncl), "bt2020ncl");
643        }
644
645        #[test]
646        fn test_default() {
647            assert_eq!(ColorSpace::default(), ColorSpace::Bt709);
648        }
649
650        #[test]
651        fn test_is_hd_sd_uhd() {
652            assert!(ColorSpace::Bt709.is_hd());
653            assert!(!ColorSpace::Bt709.is_sd());
654            assert!(!ColorSpace::Bt709.is_uhd());
655
656            assert!(!ColorSpace::Smpte170m.is_hd());
657            assert!(ColorSpace::Smpte170m.is_sd());
658            assert!(ColorSpace::Bt470bg.is_sd());
659            assert!(!ColorSpace::Smpte170m.is_uhd());
660
661            assert!(!ColorSpace::Bt2020Ncl.is_hd());
662            assert!(!ColorSpace::Bt2020Ncl.is_sd());
663            assert!(ColorSpace::Bt2020Ncl.is_uhd());
664            assert!(ColorSpace::Bt2020Cl.is_uhd());
665        }
666
667        #[test]
668        fn test_is_unknown() {
669            assert!(ColorSpace::Unknown.is_unknown());
670            assert!(!ColorSpace::Bt709.is_unknown());
671        }
672
673        #[test]
674        fn test_debug() {
675            assert_eq!(format!("{:?}", ColorSpace::Bt709), "Bt709");
676            assert_eq!(format!("{:?}", ColorSpace::Rgb), "Rgb");
677        }
678
679        #[test]
680        fn test_equality_and_hash() {
681            use std::collections::HashSet;
682
683            assert_eq!(ColorSpace::Bt709, ColorSpace::Bt709);
684            assert_ne!(ColorSpace::Bt709, ColorSpace::Smpte170m);
685
686            let mut set = HashSet::new();
687            set.insert(ColorSpace::Bt709);
688            set.insert(ColorSpace::Smpte170m);
689            assert!(set.contains(&ColorSpace::Bt709));
690            assert!(!set.contains(&ColorSpace::Bt2020Ncl));
691        }
692
693        #[test]
694        fn test_copy() {
695            let space = ColorSpace::Bt709;
696            let copied = space;
697            assert_eq!(space, copied);
698        }
699    }
700
701    mod color_range_tests {
702        use super::*;
703
704        #[test]
705        fn test_names() {
706            assert_eq!(ColorRange::Limited.name(), "limited");
707            assert_eq!(ColorRange::Full.name(), "full");
708            assert_eq!(ColorRange::Unknown.name(), "unknown");
709        }
710
711        #[test]
712        fn test_display() {
713            assert_eq!(format!("{}", ColorRange::Limited), "limited");
714            assert_eq!(format!("{}", ColorRange::Full), "full");
715        }
716
717        #[test]
718        fn test_default() {
719            assert_eq!(ColorRange::default(), ColorRange::Limited);
720        }
721
722        #[test]
723        fn test_is_full_limited() {
724            assert!(ColorRange::Full.is_full());
725            assert!(!ColorRange::Full.is_limited());
726
727            assert!(!ColorRange::Limited.is_full());
728            assert!(ColorRange::Limited.is_limited());
729        }
730
731        #[test]
732        fn test_is_unknown() {
733            assert!(ColorRange::Unknown.is_unknown());
734            assert!(!ColorRange::Limited.is_unknown());
735        }
736
737        #[test]
738        fn test_luma_values() {
739            assert_eq!(ColorRange::Limited.luma_min_8bit(), 16);
740            assert_eq!(ColorRange::Limited.luma_max_8bit(), 235);
741
742            assert_eq!(ColorRange::Full.luma_min_8bit(), 0);
743            assert_eq!(ColorRange::Full.luma_max_8bit(), 255);
744
745            assert_eq!(ColorRange::Unknown.luma_min_8bit(), 0);
746            assert_eq!(ColorRange::Unknown.luma_max_8bit(), 255);
747        }
748
749        #[test]
750        fn test_equality_and_hash() {
751            use std::collections::HashSet;
752
753            assert_eq!(ColorRange::Limited, ColorRange::Limited);
754            assert_ne!(ColorRange::Limited, ColorRange::Full);
755
756            let mut set = HashSet::new();
757            set.insert(ColorRange::Limited);
758            set.insert(ColorRange::Full);
759            assert!(set.contains(&ColorRange::Limited));
760            assert!(!set.contains(&ColorRange::Unknown));
761        }
762    }
763
764    mod color_primaries_tests {
765        use super::*;
766
767        #[test]
768        fn test_names() {
769            assert_eq!(ColorPrimaries::Bt709.name(), "bt709");
770            assert_eq!(ColorPrimaries::Bt470bg.name(), "bt470bg");
771            assert_eq!(ColorPrimaries::Smpte170m.name(), "smpte170m");
772            assert_eq!(ColorPrimaries::Smpte240m.name(), "smpte240m");
773            assert_eq!(ColorPrimaries::Film.name(), "film");
774            assert_eq!(ColorPrimaries::DciP3.name(), "dci-p3");
775            assert_eq!(ColorPrimaries::DisplayP3.name(), "display-p3");
776            assert_eq!(ColorPrimaries::Bt2020.name(), "bt2020");
777            assert_eq!(ColorPrimaries::Unknown.name(), "unknown");
778        }
779
780        #[test]
781        fn test_display() {
782            assert_eq!(format!("{}", ColorPrimaries::Bt709), "bt709");
783            assert_eq!(format!("{}", ColorPrimaries::Bt2020), "bt2020");
784        }
785
786        #[test]
787        fn test_default() {
788            assert_eq!(ColorPrimaries::default(), ColorPrimaries::Bt709);
789        }
790
791        #[test]
792        fn test_is_wide_gamut() {
793            assert!(ColorPrimaries::Bt2020.is_wide_gamut());
794            assert!(ColorPrimaries::DciP3.is_wide_gamut());
795            assert!(ColorPrimaries::DisplayP3.is_wide_gamut());
796            assert!(!ColorPrimaries::Bt709.is_wide_gamut());
797            assert!(!ColorPrimaries::Smpte170m.is_wide_gamut());
798        }
799
800        #[test]
801        fn test_is_unknown() {
802            assert!(ColorPrimaries::Unknown.is_unknown());
803            assert!(!ColorPrimaries::Bt709.is_unknown());
804        }
805
806        #[test]
807        fn test_equality_and_hash() {
808            use std::collections::HashSet;
809
810            assert_eq!(ColorPrimaries::Bt709, ColorPrimaries::Bt709);
811            assert_ne!(ColorPrimaries::Bt709, ColorPrimaries::Bt2020);
812
813            let mut set = HashSet::new();
814            set.insert(ColorPrimaries::Bt709);
815            set.insert(ColorPrimaries::Bt2020);
816            assert!(set.contains(&ColorPrimaries::Bt709));
817            assert!(!set.contains(&ColorPrimaries::Smpte170m));
818        }
819    }
820
821    mod color_transfer_tests {
822        use super::*;
823
824        #[test]
825        fn test_names() {
826            assert_eq!(ColorTransfer::Bt709.name(), "bt709");
827            assert_eq!(ColorTransfer::Gamma22.name(), "gamma22");
828            assert_eq!(ColorTransfer::Gamma28.name(), "gamma28");
829            assert_eq!(ColorTransfer::Smpte170m.name(), "smpte170m");
830            assert_eq!(ColorTransfer::Smpte240m.name(), "smpte240m");
831            assert_eq!(ColorTransfer::Linear.name(), "linear");
832            assert_eq!(ColorTransfer::Srgb.name(), "srgb");
833            assert_eq!(ColorTransfer::Bt2020_10.name(), "bt2020-10");
834            assert_eq!(ColorTransfer::Bt2020_12.name(), "bt2020-12");
835            assert_eq!(ColorTransfer::Pq.name(), "pq");
836            assert_eq!(ColorTransfer::Hlg.name(), "hlg");
837            assert_eq!(ColorTransfer::Unknown.name(), "unknown");
838        }
839
840        #[test]
841        fn test_display() {
842            assert_eq!(format!("{}", ColorTransfer::Hlg), "hlg");
843            assert_eq!(format!("{}", ColorTransfer::Pq), "pq");
844            assert_eq!(format!("{}", ColorTransfer::Bt709), "bt709");
845        }
846
847        #[test]
848        fn test_default() {
849            assert_eq!(ColorTransfer::default(), ColorTransfer::Bt709);
850        }
851
852        #[test]
853        fn hlg_is_hdr_should_return_true() {
854            assert!(ColorTransfer::Hlg.is_hdr());
855            assert!(ColorTransfer::Hlg.is_hlg());
856            assert!(!ColorTransfer::Hlg.is_pq());
857        }
858
859        #[test]
860        fn pq_is_hdr_should_return_true() {
861            assert!(ColorTransfer::Pq.is_hdr());
862            assert!(ColorTransfer::Pq.is_pq());
863            assert!(!ColorTransfer::Pq.is_hlg());
864        }
865
866        #[test]
867        fn sdr_transfers_are_not_hdr() {
868            assert!(!ColorTransfer::Bt709.is_hdr());
869            assert!(!ColorTransfer::Gamma22.is_hdr());
870            assert!(!ColorTransfer::Gamma28.is_hdr());
871            assert!(!ColorTransfer::Smpte170m.is_hdr());
872            assert!(!ColorTransfer::Smpte240m.is_hdr());
873            assert!(!ColorTransfer::Srgb.is_hdr());
874            assert!(!ColorTransfer::Bt2020_10.is_hdr());
875            assert!(!ColorTransfer::Bt2020_12.is_hdr());
876            assert!(!ColorTransfer::Linear.is_hdr());
877        }
878
879        #[test]
880        fn is_unknown_should_only_match_unknown() {
881            assert!(ColorTransfer::Unknown.is_unknown());
882            assert!(!ColorTransfer::Bt709.is_unknown());
883            assert!(!ColorTransfer::Hlg.is_unknown());
884        }
885
886        #[test]
887        fn test_equality_and_hash() {
888            use std::collections::HashSet;
889
890            assert_eq!(ColorTransfer::Hlg, ColorTransfer::Hlg);
891            assert_ne!(ColorTransfer::Hlg, ColorTransfer::Pq);
892
893            let mut set = HashSet::new();
894            set.insert(ColorTransfer::Hlg);
895            set.insert(ColorTransfer::Pq);
896            assert!(set.contains(&ColorTransfer::Hlg));
897            assert!(!set.contains(&ColorTransfer::Bt709));
898        }
899    }
900
901    mod alpha_mode_tests {
902        use super::*;
903
904        #[test]
905        fn test_names() {
906            assert_eq!(AlphaMode::Straight.name(), "straight");
907            assert_eq!(AlphaMode::Premultiplied.name(), "premultiplied");
908            assert_eq!(AlphaMode::Unknown.name(), "unknown");
909        }
910
911        #[test]
912        fn test_display() {
913            assert_eq!(format!("{}", AlphaMode::Straight), "straight");
914            assert_eq!(format!("{}", AlphaMode::Premultiplied), "premultiplied");
915            assert_eq!(format!("{}", AlphaMode::Unknown), "unknown");
916        }
917
918        #[test]
919        fn test_default() {
920            assert_eq!(AlphaMode::default(), AlphaMode::Straight);
921        }
922
923        #[test]
924        fn is_straight_should_only_match_straight() {
925            assert!(AlphaMode::Straight.is_straight());
926            assert!(!AlphaMode::Premultiplied.is_straight());
927            assert!(!AlphaMode::Unknown.is_straight());
928        }
929
930        #[test]
931        fn is_premultiplied_should_only_match_premultiplied() {
932            assert!(AlphaMode::Premultiplied.is_premultiplied());
933            assert!(!AlphaMode::Straight.is_premultiplied());
934            assert!(!AlphaMode::Unknown.is_premultiplied());
935        }
936
937        #[test]
938        fn is_unknown_should_only_match_unknown() {
939            assert!(AlphaMode::Unknown.is_unknown());
940            assert!(!AlphaMode::Straight.is_unknown());
941            assert!(!AlphaMode::Premultiplied.is_unknown());
942        }
943
944        #[test]
945        fn test_equality_and_hash() {
946            use std::collections::HashSet;
947
948            assert_eq!(AlphaMode::Straight, AlphaMode::Straight);
949            assert_ne!(AlphaMode::Premultiplied, AlphaMode::Straight);
950
951            let mut set = HashSet::new();
952            set.insert(AlphaMode::Straight);
953            assert!(set.contains(&AlphaMode::Straight));
954            assert!(!set.contains(&AlphaMode::Premultiplied));
955        }
956    }
957}
958
959/// An 8-bit-per-channel RGBA color value.
960///
961/// A plain color value (fill / text / box color), independent of any colorimetry
962/// metadata ([`ColorSpace`] et al.). `a` is the alpha channel: `255` = opaque,
963/// `0` = fully transparent.
964#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
965#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
966pub struct Color {
967    /// Red channel (0–255).
968    pub r: u8,
969    /// Green channel (0–255).
970    pub g: u8,
971    /// Blue channel (0–255).
972    pub b: u8,
973    /// Alpha channel (0 = transparent, 255 = opaque).
974    pub a: u8,
975}
976
977impl Color {
978    /// Opaque white.
979    pub const WHITE: Self = Self::rgb(255, 255, 255);
980    /// Opaque black.
981    pub const BLACK: Self = Self::rgb(0, 0, 0);
982    /// Fully transparent (black with zero alpha).
983    pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
984
985    /// Creates an opaque color (`a = 255`).
986    #[must_use]
987    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
988        Self { r, g, b, a: 255 }
989    }
990
991    /// Creates a color with an explicit alpha channel.
992    #[must_use]
993    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
994        Self { r, g, b, a }
995    }
996}
997
998#[cfg(test)]
999mod color_value_tests {
1000    use super::Color;
1001
1002    #[test]
1003    fn rgb_should_be_opaque() {
1004        let c = Color::rgb(10, 20, 30);
1005        assert_eq!(c, Color::rgba(10, 20, 30, 255));
1006        assert_eq!(c.a, 255);
1007    }
1008
1009    #[test]
1010    fn consts_should_have_expected_channels() {
1011        assert_eq!(Color::WHITE, Color::rgba(255, 255, 255, 255));
1012        assert_eq!(Color::BLACK, Color::rgba(0, 0, 0, 255));
1013        assert_eq!(Color::TRANSPARENT.a, 0);
1014    }
1015}