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    /// Parses an `FFmpeg` colour string, as `-vf drawbox=color=...` and friends
998    /// accept it.
999    ///
1000    /// Recognised forms:
1001    ///
1002    /// * `#RRGGBB` / `#RRGGBBAA` and `0xRRGGBB` / `0xRRGGBBAA`
1003    /// * a colour name, matched case-insensitively (`"green"`, `"DarkOrchid"`)
1004    /// * either of the above with an `@alpha` suffix, where alpha is a float in
1005    ///   `0.0..=1.0` or a two-digit `0xAA`
1006    ///
1007    /// Returns `None` for anything else, including `FFmpeg`'s `random` — it has no
1008    /// fixed value, so there is nothing to return.
1009    ///
1010    /// Alpha defaults to opaque when the form does not carry one.
1011    #[must_use]
1012    pub fn parse_ffmpeg(s: &str) -> Option<Self> {
1013        let s = s.trim();
1014        // `FFmpeg` splits the alpha suffix off first, so `0xRRGGBB@0.5` is as valid
1015        // as `red@0.5`; an `@` on an 8-digit hex overrides that hex's alpha byte.
1016        let (body, alpha) = match s.split_once('@') {
1017            Some((body, alpha)) => (body, Some(alpha)),
1018            None => (s, None),
1019        };
1020
1021        let mut color = parse_hex_color(body).or_else(|| lookup_color_name(body))?;
1022        if let Some(alpha) = alpha {
1023            color.a = parse_alpha(alpha)?;
1024        }
1025        Some(color)
1026    }
1027
1028    /// Every colour name [`parse_ffmpeg`](Self::parse_ffmpeg) accepts, with its
1029    /// value, in ascending name order.
1030    ///
1031    /// Lets a host offer the same palette `FFmpeg` understands (a colour picker,
1032    /// an autocomplete), and it is what
1033    /// `crates/ff-filter/tests/color_parse_reference_tests.rs` walks to re-check
1034    /// every row against the linked `FFmpeg`.
1035    #[must_use]
1036    pub fn ffmpeg_color_names() -> impl ExactSizeIterator<Item = (&'static str, Self)> {
1037        FFMPEG_COLOR_NAMES
1038            .iter()
1039            .map(|&(name, [r, g, b])| (name, Self::rgb(r, g, b)))
1040    }
1041}
1042
1043/// Parses `#RRGGBB[AA]` / `0xRRGGBB[AA]`, or `None` for any other shape.
1044fn parse_hex_color(s: &str) -> Option<Color> {
1045    // `0x` lower-case only: `av_parse_color` compares the prefix case-sensitively,
1046    // so accepting `0X` here would map a colour on the GPU that the CPU filter path
1047    // cannot build — measured, and the only form the two disagreed on.
1048    let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix('#'))?;
1049    if hex.len() != 6 && hex.len() != 8 {
1050        return None;
1051    }
1052    let byte = |i: usize| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok();
1053    Some(Color {
1054        r: byte(0)?,
1055        g: byte(2)?,
1056        b: byte(4)?,
1057        a: if hex.len() == 8 { byte(6)? } else { 255 },
1058    })
1059}
1060
1061/// Parses the `@alpha` suffix: a two-digit `0xAA`, or a float in `0.0..=1.0`.
1062#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1063fn parse_alpha(s: &str) -> Option<u8> {
1064    if let Some(hex) = s.strip_prefix("0x") {
1065        // Exactly two digits, which is what the documentation above promises;
1066        // `from_str_radix` alone would also take `@0x8`.
1067        if hex.len() != 2 {
1068            return None;
1069        }
1070        return u8::from_str_radix(hex, 16).ok();
1071    }
1072    let f: f32 = s.parse().ok()?;
1073    if !(0.0..=1.0).contains(&f) {
1074        return None;
1075    }
1076    // Rounding, not truncation: 1.0 must reach 255 and 0.5 must not land a byte low.
1077    Some((f * 255.0).round() as u8)
1078}
1079
1080/// Looks `name` up in [`FFMPEG_COLOR_NAMES`], case-insensitively.
1081fn lookup_color_name(name: &str) -> Option<Color> {
1082    let lower = name.to_ascii_lowercase();
1083    let idx = FFMPEG_COLOR_NAMES
1084        .binary_search_by(|(n, _)| (*n).cmp(lower.as_str()))
1085        .ok()?;
1086    let [r, g, b] = FFMPEG_COLOR_NAMES[idx].1;
1087    Some(Color::rgb(r, g, b))
1088}
1089
1090/// The colour names the linked `FFmpeg` accepts, lower-cased and sorted so the
1091/// lookup can binary-search. Mirrors `libavutil/parseutils.c`'s `color_table`.
1092///
1093/// These values are **not** the CSS list from memory: each was read out of
1094/// `FFmpeg` itself by rendering `color=c=<name>` and sampling the pixel, which is
1095/// how the surprises got caught — `green` is `0x008000` (the HTML value, not X11's
1096/// `0x00FF00`), and `FFmpeg` carries `lightgrey` but no `lightgray` while every
1097/// other grey is spelled `gray`. `ff-format` has no `FFmpeg` to ask at runtime, so
1098/// `crates/ff-filter/tests/color_parse_reference_tests.rs` re-checks every row
1099/// against it (RK-005: verify tokens against the real thing, not against docs).
1100const FFMPEG_COLOR_NAMES: &[(&str, [u8; 3])] = &[
1101    ("aliceblue", [0xF0, 0xF8, 0xFF]),
1102    ("antiquewhite", [0xFA, 0xEB, 0xD7]),
1103    ("aqua", [0x00, 0xFF, 0xFF]),
1104    ("aquamarine", [0x7F, 0xFF, 0xD4]),
1105    ("azure", [0xF0, 0xFF, 0xFF]),
1106    ("beige", [0xF5, 0xF5, 0xDC]),
1107    ("bisque", [0xFF, 0xE4, 0xC4]),
1108    ("black", [0x00, 0x00, 0x00]),
1109    ("blanchedalmond", [0xFF, 0xEB, 0xCD]),
1110    ("blue", [0x00, 0x00, 0xFF]),
1111    ("blueviolet", [0x8A, 0x2B, 0xE2]),
1112    ("brown", [0xA5, 0x2A, 0x2A]),
1113    ("burlywood", [0xDE, 0xB8, 0x87]),
1114    ("cadetblue", [0x5F, 0x9E, 0xA0]),
1115    ("chartreuse", [0x7F, 0xFF, 0x00]),
1116    ("chocolate", [0xD2, 0x69, 0x1E]),
1117    ("coral", [0xFF, 0x7F, 0x50]),
1118    ("cornflowerblue", [0x64, 0x95, 0xED]),
1119    ("cornsilk", [0xFF, 0xF8, 0xDC]),
1120    ("crimson", [0xDC, 0x14, 0x3C]),
1121    ("cyan", [0x00, 0xFF, 0xFF]),
1122    ("darkblue", [0x00, 0x00, 0x8B]),
1123    ("darkcyan", [0x00, 0x8B, 0x8B]),
1124    ("darkgoldenrod", [0xB8, 0x86, 0x0B]),
1125    ("darkgray", [0xA9, 0xA9, 0xA9]),
1126    ("darkgreen", [0x00, 0x64, 0x00]),
1127    ("darkkhaki", [0xBD, 0xB7, 0x6B]),
1128    ("darkmagenta", [0x8B, 0x00, 0x8B]),
1129    ("darkolivegreen", [0x55, 0x6B, 0x2F]),
1130    ("darkorange", [0xFF, 0x8C, 0x00]),
1131    ("darkorchid", [0x99, 0x32, 0xCC]),
1132    ("darkred", [0x8B, 0x00, 0x00]),
1133    ("darksalmon", [0xE9, 0x96, 0x7A]),
1134    ("darkseagreen", [0x8F, 0xBC, 0x8F]),
1135    ("darkslateblue", [0x48, 0x3D, 0x8B]),
1136    ("darkslategray", [0x2F, 0x4F, 0x4F]),
1137    ("darkturquoise", [0x00, 0xCE, 0xD1]),
1138    ("darkviolet", [0x94, 0x00, 0xD3]),
1139    ("deeppink", [0xFF, 0x14, 0x93]),
1140    ("deepskyblue", [0x00, 0xBF, 0xFF]),
1141    ("dimgray", [0x69, 0x69, 0x69]),
1142    ("dodgerblue", [0x1E, 0x90, 0xFF]),
1143    ("firebrick", [0xB2, 0x22, 0x22]),
1144    ("floralwhite", [0xFF, 0xFA, 0xF0]),
1145    ("forestgreen", [0x22, 0x8B, 0x22]),
1146    ("fuchsia", [0xFF, 0x00, 0xFF]),
1147    ("gainsboro", [0xDC, 0xDC, 0xDC]),
1148    ("ghostwhite", [0xF8, 0xF8, 0xFF]),
1149    ("gold", [0xFF, 0xD7, 0x00]),
1150    ("goldenrod", [0xDA, 0xA5, 0x20]),
1151    ("gray", [0x80, 0x80, 0x80]),
1152    ("green", [0x00, 0x80, 0x00]),
1153    ("greenyellow", [0xAD, 0xFF, 0x2F]),
1154    ("honeydew", [0xF0, 0xFF, 0xF0]),
1155    ("hotpink", [0xFF, 0x69, 0xB4]),
1156    ("indianred", [0xCD, 0x5C, 0x5C]),
1157    ("indigo", [0x4B, 0x00, 0x82]),
1158    ("ivory", [0xFF, 0xFF, 0xF0]),
1159    ("khaki", [0xF0, 0xE6, 0x8C]),
1160    ("lavender", [0xE6, 0xE6, 0xFA]),
1161    ("lavenderblush", [0xFF, 0xF0, 0xF5]),
1162    ("lawngreen", [0x7C, 0xFC, 0x00]),
1163    ("lemonchiffon", [0xFF, 0xFA, 0xCD]),
1164    ("lightblue", [0xAD, 0xD8, 0xE6]),
1165    ("lightcoral", [0xF0, 0x80, 0x80]),
1166    ("lightcyan", [0xE0, 0xFF, 0xFF]),
1167    ("lightgoldenrodyellow", [0xFA, 0xFA, 0xD2]),
1168    ("lightgreen", [0x90, 0xEE, 0x90]),
1169    ("lightgrey", [0xD3, 0xD3, 0xD3]),
1170    ("lightpink", [0xFF, 0xB6, 0xC1]),
1171    ("lightsalmon", [0xFF, 0xA0, 0x7A]),
1172    ("lightseagreen", [0x20, 0xB2, 0xAA]),
1173    ("lightskyblue", [0x87, 0xCE, 0xFA]),
1174    ("lightslategray", [0x77, 0x88, 0x99]),
1175    ("lightsteelblue", [0xB0, 0xC4, 0xDE]),
1176    ("lightyellow", [0xFF, 0xFF, 0xE0]),
1177    ("lime", [0x00, 0xFF, 0x00]),
1178    ("limegreen", [0x32, 0xCD, 0x32]),
1179    ("linen", [0xFA, 0xF0, 0xE6]),
1180    ("magenta", [0xFF, 0x00, 0xFF]),
1181    ("maroon", [0x80, 0x00, 0x00]),
1182    ("mediumaquamarine", [0x66, 0xCD, 0xAA]),
1183    ("mediumblue", [0x00, 0x00, 0xCD]),
1184    ("mediumorchid", [0xBA, 0x55, 0xD3]),
1185    ("mediumpurple", [0x93, 0x70, 0xD8]),
1186    ("mediumseagreen", [0x3C, 0xB3, 0x71]),
1187    ("mediumslateblue", [0x7B, 0x68, 0xEE]),
1188    ("mediumspringgreen", [0x00, 0xFA, 0x9A]),
1189    ("mediumturquoise", [0x48, 0xD1, 0xCC]),
1190    ("mediumvioletred", [0xC7, 0x15, 0x85]),
1191    ("midnightblue", [0x19, 0x19, 0x70]),
1192    ("mintcream", [0xF5, 0xFF, 0xFA]),
1193    ("mistyrose", [0xFF, 0xE4, 0xE1]),
1194    ("moccasin", [0xFF, 0xE4, 0xB5]),
1195    ("navajowhite", [0xFF, 0xDE, 0xAD]),
1196    ("navy", [0x00, 0x00, 0x80]),
1197    ("oldlace", [0xFD, 0xF5, 0xE6]),
1198    ("olive", [0x80, 0x80, 0x00]),
1199    ("olivedrab", [0x6B, 0x8E, 0x23]),
1200    ("orange", [0xFF, 0xA5, 0x00]),
1201    ("orangered", [0xFF, 0x45, 0x00]),
1202    ("orchid", [0xDA, 0x70, 0xD6]),
1203    ("palegoldenrod", [0xEE, 0xE8, 0xAA]),
1204    ("palegreen", [0x98, 0xFB, 0x98]),
1205    ("paleturquoise", [0xAF, 0xEE, 0xEE]),
1206    ("palevioletred", [0xD8, 0x70, 0x93]),
1207    ("papayawhip", [0xFF, 0xEF, 0xD5]),
1208    ("peachpuff", [0xFF, 0xDA, 0xB9]),
1209    ("peru", [0xCD, 0x85, 0x3F]),
1210    ("pink", [0xFF, 0xC0, 0xCB]),
1211    ("plum", [0xDD, 0xA0, 0xDD]),
1212    ("powderblue", [0xB0, 0xE0, 0xE6]),
1213    ("purple", [0x80, 0x00, 0x80]),
1214    ("red", [0xFF, 0x00, 0x00]),
1215    ("rosybrown", [0xBC, 0x8F, 0x8F]),
1216    ("royalblue", [0x41, 0x69, 0xE1]),
1217    ("saddlebrown", [0x8B, 0x45, 0x13]),
1218    ("salmon", [0xFA, 0x80, 0x72]),
1219    ("sandybrown", [0xF4, 0xA4, 0x60]),
1220    ("seagreen", [0x2E, 0x8B, 0x57]),
1221    ("seashell", [0xFF, 0xF5, 0xEE]),
1222    ("sienna", [0xA0, 0x52, 0x2D]),
1223    ("silver", [0xC0, 0xC0, 0xC0]),
1224    ("skyblue", [0x87, 0xCE, 0xEB]),
1225    ("slateblue", [0x6A, 0x5A, 0xCD]),
1226    ("slategray", [0x70, 0x80, 0x90]),
1227    ("snow", [0xFF, 0xFA, 0xFA]),
1228    ("springgreen", [0x00, 0xFF, 0x7F]),
1229    ("steelblue", [0x46, 0x82, 0xB4]),
1230    ("tan", [0xD2, 0xB4, 0x8C]),
1231    ("teal", [0x00, 0x80, 0x80]),
1232    ("thistle", [0xD8, 0xBF, 0xD8]),
1233    ("tomato", [0xFF, 0x63, 0x47]),
1234    ("turquoise", [0x40, 0xE0, 0xD0]),
1235    ("violet", [0xEE, 0x82, 0xEE]),
1236    ("wheat", [0xF5, 0xDE, 0xB3]),
1237    ("white", [0xFF, 0xFF, 0xFF]),
1238    ("whitesmoke", [0xF5, 0xF5, 0xF5]),
1239    ("yellow", [0xFF, 0xFF, 0x00]),
1240    ("yellowgreen", [0x9A, 0xCD, 0x32]),
1241];
1242
1243#[cfg(test)]
1244mod color_value_tests {
1245    use super::Color;
1246
1247    // parse_ffmpeg
1248
1249    #[test]
1250    fn parse_ffmpeg_should_read_hex_with_and_without_alpha() {
1251        assert_eq!(
1252            Color::parse_ffmpeg("0x123456"),
1253            Some(Color::rgb(18, 52, 86))
1254        );
1255        assert_eq!(Color::parse_ffmpeg("#123456"), Some(Color::rgb(18, 52, 86)));
1256        assert_eq!(
1257            Color::parse_ffmpeg("0x12345680"),
1258            Some(Color::rgba(18, 52, 86, 128))
1259        );
1260    }
1261
1262    #[test]
1263    fn parse_ffmpeg_should_read_a_name_case_insensitively() {
1264        // `green` is the HTML value, not X11's `0x00FF00` — the distinction the
1265        // table was read out of FFmpeg to get right.
1266        let green = Some(Color::rgb(0, 128, 0));
1267        assert_eq!(Color::parse_ffmpeg("green"), green);
1268        assert_eq!(Color::parse_ffmpeg("Green"), green);
1269        assert_eq!(Color::parse_ffmpeg("GREEN"), green);
1270        assert_eq!(Color::parse_ffmpeg("lime"), Some(Color::rgb(0, 255, 0)));
1271        assert_eq!(
1272            Color::parse_ffmpeg("DarkOrchid"),
1273            Some(Color::rgb(0x99, 0x32, 0xCC))
1274        );
1275    }
1276
1277    #[test]
1278    fn parse_ffmpeg_should_read_an_alpha_suffix_in_both_spellings() {
1279        assert_eq!(
1280            Color::parse_ffmpeg("black@0x80"),
1281            Some(Color::rgba(0, 0, 0, 128))
1282        );
1283        assert_eq!(
1284            Color::parse_ffmpeg("black@1"),
1285            Some(Color::rgba(0, 0, 0, 255))
1286        );
1287        assert_eq!(
1288            Color::parse_ffmpeg("black@0"),
1289            Some(Color::rgba(0, 0, 0, 0))
1290        );
1291        // The suffix overrides a hex form's own alpha byte, as FFmpeg's parser does.
1292        assert_eq!(
1293            Color::parse_ffmpeg("0x11223344@1.0"),
1294            Some(Color::rgba(0x11, 0x22, 0x33, 255))
1295        );
1296    }
1297
1298    #[test]
1299    fn parse_ffmpeg_should_reject_unparseable_forms() {
1300        // `random` has no fixed value, so there is nothing to return.
1301        assert_eq!(Color::parse_ffmpeg("random"), None);
1302        assert_eq!(Color::parse_ffmpeg("no_such_colour"), None);
1303        // FFmpeg has `lightgrey` but no `lightgray`; the table says so, and this
1304        // pins that it is the table talking and not a fuzzy match.
1305        assert!(Color::parse_ffmpeg("lightgrey").is_some());
1306        assert_eq!(Color::parse_ffmpeg("lightgray"), None);
1307        assert_eq!(Color::parse_ffmpeg("0x1234"), None);
1308        // FFmpeg's prefix comparison is case-sensitive, so `0X` is not a colour.
1309        // Accepting it would map on the GPU what the CPU filter path rejects.
1310        assert_eq!(Color::parse_ffmpeg("0X123456"), None);
1311        // The alpha suffix's hex form is two digits, as the doc says.
1312        assert_eq!(Color::parse_ffmpeg("black@0x8"), None);
1313        assert_eq!(Color::parse_ffmpeg("black@0x080"), None);
1314        assert_eq!(Color::parse_ffmpeg("0xGGGGGG"), None);
1315        assert_eq!(Color::parse_ffmpeg("black@1.5"), None);
1316        assert_eq!(Color::parse_ffmpeg(""), None);
1317    }
1318
1319    #[test]
1320    fn ffmpeg_color_names_should_be_sorted_and_lower_case() {
1321        // `lookup_color_name` binary-searches, so an unsorted or mixed-case row
1322        // would make some names silently unfindable rather than fail loudly.
1323        for pair in super::FFMPEG_COLOR_NAMES.windows(2) {
1324            assert!(
1325                pair[0].0 < pair[1].0,
1326                "table must be sorted and unique: {:?} then {:?}",
1327                pair[0].0,
1328                pair[1].0
1329            );
1330        }
1331        for (name, _) in super::FFMPEG_COLOR_NAMES {
1332            assert_eq!(
1333                *name,
1334                name.to_ascii_lowercase(),
1335                "table names must be lower-case: {name:?}"
1336            );
1337        }
1338    }
1339
1340    #[test]
1341    fn rgb_should_be_opaque() {
1342        let c = Color::rgb(10, 20, 30);
1343        assert_eq!(c, Color::rgba(10, 20, 30, 255));
1344        assert_eq!(c.a, 255);
1345    }
1346
1347    #[test]
1348    fn consts_should_have_expected_channels() {
1349        assert_eq!(Color::WHITE, Color::rgba(255, 255, 255, 255));
1350        assert_eq!(Color::BLACK, Color::rgba(0, 0, 0, 255));
1351        assert_eq!(Color::TRANSPARENT.a, 0);
1352    }
1353}