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}