ff_format/pixel.rs
1//! Pixel format definitions for video processing.
2//!
3//! This module provides the [`PixelFormat`] enum which represents various
4//! pixel formats used in video processing. It supports both packed (RGB/BGRA)
5//! and planar (YUV) formats commonly used in video editing.
6//!
7//! # Examples
8//!
9//! ```
10//! use ff_format::PixelFormat;
11//!
12//! let format = PixelFormat::Yuv420p;
13//! assert!(format.is_planar());
14//! assert!(!format.is_packed());
15//! assert_eq!(format.num_planes(), 3);
16//!
17//! let rgba = PixelFormat::Rgba;
18//! assert!(rgba.has_alpha());
19//! assert_eq!(rgba.bits_per_pixel(), Some(32));
20//! ```
21
22use std::fmt;
23
24/// Pixel format for video frames.
25///
26/// This enum represents various pixel formats used in video processing.
27/// It is designed to cover the most common formats used in video editing
28/// while remaining extensible via the `Other` variant.
29///
30/// # Format Categories
31///
32/// - **Packed RGB**: Data stored contiguously (Rgb24, Rgba, Bgr24, Bgra)
33/// - **Planar YUV**: Separate planes for Y, U, V components (Yuv420p, Yuv422p, Yuv444p)
34/// - **Semi-planar**: Y plane + interleaved UV (Nv12, Nv21)
35/// - **High bit depth**: 10-bit formats for HDR content (Yuv420p10le, P010le)
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum PixelFormat {
39 // Packed RGB
40 /// 24-bit RGB (8:8:8) - 3 bytes per pixel
41 Rgb24,
42 /// 32-bit RGBA (8:8:8:8) - 4 bytes per pixel with alpha
43 Rgba,
44 /// 24-bit BGR (8:8:8) - 3 bytes per pixel, reversed channel order
45 Bgr24,
46 /// 32-bit BGRA (8:8:8:8) - 4 bytes per pixel with alpha, reversed channel order
47 Bgra,
48
49 // Planar YUV
50 /// YUV 4:2:0 planar - most common video format (H.264, etc.)
51 Yuv420p,
52 /// YUV 4:2:2 planar - higher chroma resolution
53 Yuv422p,
54 /// YUV 4:4:4 planar - full chroma resolution
55 Yuv444p,
56
57 // Semi-planar (NV12/NV21)
58 /// Y plane + interleaved UV - common in hardware decoders
59 Nv12,
60 /// Y plane + interleaved VU - Android camera format
61 Nv21,
62
63 // High bit depth
64 /// 10-bit YUV 4:2:0 planar - HDR content
65 Yuv420p10le,
66 /// 10-bit semi-planar NV12 - HDR hardware decoding
67 P010le,
68
69 // Grayscale
70 /// 8-bit grayscale
71 Gray8,
72
73 // Extensibility
74 /// Unknown or unsupported format with `FFmpeg`'s `AVPixelFormat` value
75 Other(u32),
76}
77
78impl PixelFormat {
79 /// Returns the format name as a human-readable string.
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use ff_format::PixelFormat;
85 ///
86 /// assert_eq!(PixelFormat::Yuv420p.name(), "yuv420p");
87 /// assert_eq!(PixelFormat::Rgba.name(), "rgba");
88 /// ```
89 #[must_use]
90 pub const fn name(&self) -> &'static str {
91 match self {
92 Self::Rgb24 => "rgb24",
93 Self::Rgba => "rgba",
94 Self::Bgr24 => "bgr24",
95 Self::Bgra => "bgra",
96 Self::Yuv420p => "yuv420p",
97 Self::Yuv422p => "yuv422p",
98 Self::Yuv444p => "yuv444p",
99 Self::Nv12 => "nv12",
100 Self::Nv21 => "nv21",
101 Self::Yuv420p10le => "yuv420p10le",
102 Self::P010le => "p010le",
103 Self::Gray8 => "gray8",
104 Self::Other(_) => "unknown",
105 }
106 }
107
108 /// Returns the number of planes for this format.
109 ///
110 /// - Packed formats (RGB, RGBA, etc.) have 1 plane
111 /// - Planar YUV formats have 3 planes (Y, U, V)
112 /// - Semi-planar formats (NV12, NV21) have 2 planes (Y, UV)
113 /// - Grayscale has 1 plane
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use ff_format::PixelFormat;
119 ///
120 /// assert_eq!(PixelFormat::Rgba.num_planes(), 1);
121 /// assert_eq!(PixelFormat::Yuv420p.num_planes(), 3);
122 /// assert_eq!(PixelFormat::Nv12.num_planes(), 2);
123 /// ```
124 #[must_use]
125 pub const fn num_planes(&self) -> usize {
126 match self {
127 // Planar YUV - Y, U, V planes
128 Self::Yuv420p | Self::Yuv422p | Self::Yuv444p | Self::Yuv420p10le => 3,
129 // Semi-planar - Y plane + interleaved UV plane
130 Self::Nv12 | Self::Nv21 | Self::P010le => 2,
131 // Packed formats and unknown - single plane
132 Self::Rgb24 | Self::Rgba | Self::Bgr24 | Self::Bgra | Self::Gray8 | Self::Other(_) => 1,
133 }
134 }
135
136 /// Alias for [`num_planes`](Self::num_planes) for API compatibility.
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use ff_format::PixelFormat;
142 ///
143 /// assert_eq!(PixelFormat::Yuv420p.plane_count(), 3);
144 /// ```
145 #[must_use]
146 #[inline]
147 pub const fn plane_count(&self) -> usize {
148 self.num_planes()
149 }
150
151 /// Returns `true` if this is a packed format (single plane with interleaved components).
152 ///
153 /// Packed formats store all color components contiguously in memory,
154 /// making them suitable for direct rendering but less efficient for
155 /// video compression.
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use ff_format::PixelFormat;
161 ///
162 /// assert!(PixelFormat::Rgba.is_packed());
163 /// assert!(!PixelFormat::Yuv420p.is_packed());
164 /// ```
165 #[must_use]
166 pub const fn is_packed(&self) -> bool {
167 matches!(
168 self,
169 Self::Rgb24 | Self::Rgba | Self::Bgr24 | Self::Bgra | Self::Gray8
170 )
171 }
172
173 /// Returns `true` if this is a planar format (separate planes for each component).
174 ///
175 /// Planar formats store each color component in a separate memory region,
176 /// which is more efficient for video codecs and some GPU operations.
177 ///
178 /// Note: Semi-planar formats (NV12, NV21, P010le) are considered planar
179 /// as they have multiple planes, even though UV is interleaved.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use ff_format::PixelFormat;
185 ///
186 /// assert!(PixelFormat::Yuv420p.is_planar());
187 /// assert!(PixelFormat::Nv12.is_planar()); // Semi-planar is also planar
188 /// assert!(!PixelFormat::Rgba.is_planar());
189 /// ```
190 #[must_use]
191 pub const fn is_planar(&self) -> bool {
192 !self.is_packed()
193 }
194
195 /// Returns `true` if this format has an alpha (transparency) channel.
196 ///
197 /// # Examples
198 ///
199 /// ```
200 /// use ff_format::PixelFormat;
201 ///
202 /// assert!(PixelFormat::Rgba.has_alpha());
203 /// assert!(PixelFormat::Bgra.has_alpha());
204 /// assert!(!PixelFormat::Rgb24.has_alpha());
205 /// assert!(!PixelFormat::Yuv420p.has_alpha());
206 /// ```
207 #[must_use]
208 pub const fn has_alpha(&self) -> bool {
209 matches!(self, Self::Rgba | Self::Bgra)
210 }
211
212 /// Returns `true` if this is an RGB-based format.
213 ///
214 /// # Examples
215 ///
216 /// ```
217 /// use ff_format::PixelFormat;
218 ///
219 /// assert!(PixelFormat::Rgb24.is_rgb());
220 /// assert!(PixelFormat::Rgba.is_rgb());
221 /// assert!(PixelFormat::Bgra.is_rgb()); // BGR is still RGB family
222 /// assert!(!PixelFormat::Yuv420p.is_rgb());
223 /// ```
224 #[must_use]
225 pub const fn is_rgb(&self) -> bool {
226 matches!(self, Self::Rgb24 | Self::Rgba | Self::Bgr24 | Self::Bgra)
227 }
228
229 /// Returns `true` if this is a YUV-based format.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use ff_format::PixelFormat;
235 ///
236 /// assert!(PixelFormat::Yuv420p.is_yuv());
237 /// assert!(PixelFormat::Nv12.is_yuv());
238 /// assert!(!PixelFormat::Rgba.is_yuv());
239 /// ```
240 #[must_use]
241 pub const fn is_yuv(&self) -> bool {
242 matches!(
243 self,
244 Self::Yuv420p
245 | Self::Yuv422p
246 | Self::Yuv444p
247 | Self::Nv12
248 | Self::Nv21
249 | Self::Yuv420p10le
250 | Self::P010le
251 )
252 }
253
254 /// Returns the bits per pixel for packed formats.
255 ///
256 /// For planar formats, this returns `None` because the concept of
257 /// "bits per pixel" doesn't apply directly - use [`bytes_per_pixel`](Self::bytes_per_pixel)
258 /// to get the average bytes per pixel instead.
259 ///
260 /// # Examples
261 ///
262 /// ```
263 /// use ff_format::PixelFormat;
264 ///
265 /// assert_eq!(PixelFormat::Rgb24.bits_per_pixel(), Some(24));
266 /// assert_eq!(PixelFormat::Rgba.bits_per_pixel(), Some(32));
267 /// assert_eq!(PixelFormat::Yuv420p.bits_per_pixel(), None);
268 /// ```
269 #[must_use]
270 pub const fn bits_per_pixel(&self) -> Option<usize> {
271 match self {
272 Self::Rgb24 | Self::Bgr24 => Some(24),
273 Self::Rgba | Self::Bgra => Some(32),
274 Self::Gray8 => Some(8),
275 // Planar formats don't have a simple bits-per-pixel value
276 _ => None,
277 }
278 }
279
280 /// Returns the average bytes per pixel.
281 ///
282 /// For packed formats, this is exact. For planar YUV formats, this
283 /// returns the average considering subsampling:
284 /// - YUV 4:2:0: 1.5 bytes/pixel (12 bits)
285 /// - YUV 4:2:2: 2 bytes/pixel (16 bits)
286 /// - YUV 4:4:4: 3 bytes/pixel (24 bits)
287 ///
288 /// Note: For formats with non-integer bytes per pixel (like `Yuv420p`),
289 /// this rounds up to the nearest byte.
290 ///
291 /// # Examples
292 ///
293 /// ```
294 /// use ff_format::PixelFormat;
295 ///
296 /// assert_eq!(PixelFormat::Rgba.bytes_per_pixel(), 4);
297 /// assert_eq!(PixelFormat::Rgb24.bytes_per_pixel(), 3);
298 /// assert_eq!(PixelFormat::Yuv420p.bytes_per_pixel(), 2); // Actually 1.5, rounded up
299 /// assert_eq!(PixelFormat::Yuv444p.bytes_per_pixel(), 3);
300 /// ```
301 #[must_use]
302 pub const fn bytes_per_pixel(&self) -> usize {
303 match self {
304 // Grayscale - 1 byte per pixel
305 Self::Gray8 => 1,
306
307 // YUV 4:2:0 (8-bit and 10-bit) and YUV 4:2:2 - average ~2 bytes per pixel
308 Self::Yuv420p
309 | Self::Nv12
310 | Self::Nv21
311 | Self::Yuv420p10le
312 | Self::P010le
313 | Self::Yuv422p => 2,
314
315 // RGB24/BGR24 and YUV 4:4:4 - 3 bytes per pixel
316 Self::Rgb24 | Self::Bgr24 | Self::Yuv444p => 3,
317
318 // RGBA/BGRA and unknown formats - 4 bytes per pixel
319 Self::Rgba | Self::Bgra | Self::Other(_) => 4,
320 }
321 }
322
323 /// Returns `true` if this is a high bit depth format (> 8 bits per component).
324 ///
325 /// # Examples
326 ///
327 /// ```
328 /// use ff_format::PixelFormat;
329 ///
330 /// assert!(PixelFormat::Yuv420p10le.is_high_bit_depth());
331 /// assert!(PixelFormat::P010le.is_high_bit_depth());
332 /// assert!(!PixelFormat::Yuv420p.is_high_bit_depth());
333 /// ```
334 #[must_use]
335 pub const fn is_high_bit_depth(&self) -> bool {
336 matches!(self, Self::Yuv420p10le | Self::P010le)
337 }
338
339 /// Returns the bit depth per component.
340 ///
341 /// Most formats use 8 bits per component, while high bit depth
342 /// formats use 10 bits.
343 ///
344 /// # Examples
345 ///
346 /// ```
347 /// use ff_format::PixelFormat;
348 ///
349 /// assert_eq!(PixelFormat::Rgba.bit_depth(), 8);
350 /// assert_eq!(PixelFormat::Yuv420p10le.bit_depth(), 10);
351 /// ```
352 #[must_use]
353 pub const fn bit_depth(&self) -> usize {
354 match self {
355 Self::Yuv420p10le | Self::P010le => 10,
356 // All other formats including unknown are 8-bit
357 _ => 8,
358 }
359 }
360}
361
362impl fmt::Display for PixelFormat {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 write!(f, "{}", self.name())
365 }
366}
367
368impl Default for PixelFormat {
369 /// Returns the default pixel format.
370 ///
371 /// The default is [`PixelFormat::Yuv420p`] as it's the most common
372 /// format used in video encoding.
373 fn default() -> Self {
374 Self::Yuv420p
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_format_names() {
384 assert_eq!(PixelFormat::Rgb24.name(), "rgb24");
385 assert_eq!(PixelFormat::Rgba.name(), "rgba");
386 assert_eq!(PixelFormat::Bgr24.name(), "bgr24");
387 assert_eq!(PixelFormat::Bgra.name(), "bgra");
388 assert_eq!(PixelFormat::Yuv420p.name(), "yuv420p");
389 assert_eq!(PixelFormat::Yuv422p.name(), "yuv422p");
390 assert_eq!(PixelFormat::Yuv444p.name(), "yuv444p");
391 assert_eq!(PixelFormat::Nv12.name(), "nv12");
392 assert_eq!(PixelFormat::Nv21.name(), "nv21");
393 assert_eq!(PixelFormat::Yuv420p10le.name(), "yuv420p10le");
394 assert_eq!(PixelFormat::P010le.name(), "p010le");
395 assert_eq!(PixelFormat::Gray8.name(), "gray8");
396 assert_eq!(PixelFormat::Other(999).name(), "unknown");
397 }
398
399 #[test]
400 fn test_plane_count() {
401 // Packed formats - 1 plane
402 assert_eq!(PixelFormat::Rgb24.num_planes(), 1);
403 assert_eq!(PixelFormat::Rgba.num_planes(), 1);
404 assert_eq!(PixelFormat::Bgr24.num_planes(), 1);
405 assert_eq!(PixelFormat::Bgra.num_planes(), 1);
406 assert_eq!(PixelFormat::Gray8.num_planes(), 1);
407
408 // Planar YUV - 3 planes
409 assert_eq!(PixelFormat::Yuv420p.num_planes(), 3);
410 assert_eq!(PixelFormat::Yuv422p.num_planes(), 3);
411 assert_eq!(PixelFormat::Yuv444p.num_planes(), 3);
412 assert_eq!(PixelFormat::Yuv420p10le.num_planes(), 3);
413
414 // Semi-planar - 2 planes
415 assert_eq!(PixelFormat::Nv12.num_planes(), 2);
416 assert_eq!(PixelFormat::Nv21.num_planes(), 2);
417 assert_eq!(PixelFormat::P010le.num_planes(), 2);
418
419 // plane_count is alias for num_planes
420 assert_eq!(PixelFormat::Yuv420p.plane_count(), 3);
421 }
422
423 #[test]
424 fn test_packed_vs_planar() {
425 // Packed formats
426 assert!(PixelFormat::Rgb24.is_packed());
427 assert!(PixelFormat::Rgba.is_packed());
428 assert!(PixelFormat::Bgr24.is_packed());
429 assert!(PixelFormat::Bgra.is_packed());
430 assert!(PixelFormat::Gray8.is_packed());
431 assert!(!PixelFormat::Rgb24.is_planar());
432
433 // Planar formats
434 assert!(PixelFormat::Yuv420p.is_planar());
435 assert!(PixelFormat::Yuv422p.is_planar());
436 assert!(PixelFormat::Yuv444p.is_planar());
437 assert!(PixelFormat::Nv12.is_planar());
438 assert!(PixelFormat::Nv21.is_planar());
439 assert!(!PixelFormat::Yuv420p.is_packed());
440 }
441
442 #[test]
443 fn test_has_alpha() {
444 assert!(PixelFormat::Rgba.has_alpha());
445 assert!(PixelFormat::Bgra.has_alpha());
446 assert!(!PixelFormat::Rgb24.has_alpha());
447 assert!(!PixelFormat::Bgr24.has_alpha());
448 assert!(!PixelFormat::Yuv420p.has_alpha());
449 assert!(!PixelFormat::Gray8.has_alpha());
450 }
451
452 #[test]
453 fn test_is_rgb() {
454 assert!(PixelFormat::Rgb24.is_rgb());
455 assert!(PixelFormat::Rgba.is_rgb());
456 assert!(PixelFormat::Bgr24.is_rgb());
457 assert!(PixelFormat::Bgra.is_rgb());
458 assert!(!PixelFormat::Yuv420p.is_rgb());
459 assert!(!PixelFormat::Nv12.is_rgb());
460 assert!(!PixelFormat::Gray8.is_rgb());
461 }
462
463 #[test]
464 fn test_is_yuv() {
465 assert!(PixelFormat::Yuv420p.is_yuv());
466 assert!(PixelFormat::Yuv422p.is_yuv());
467 assert!(PixelFormat::Yuv444p.is_yuv());
468 assert!(PixelFormat::Nv12.is_yuv());
469 assert!(PixelFormat::Nv21.is_yuv());
470 assert!(PixelFormat::Yuv420p10le.is_yuv());
471 assert!(PixelFormat::P010le.is_yuv());
472 assert!(!PixelFormat::Rgb24.is_yuv());
473 assert!(!PixelFormat::Rgba.is_yuv());
474 assert!(!PixelFormat::Gray8.is_yuv());
475 }
476
477 #[test]
478 fn test_bits_per_pixel() {
479 // Packed formats have defined bits per pixel
480 assert_eq!(PixelFormat::Rgb24.bits_per_pixel(), Some(24));
481 assert_eq!(PixelFormat::Bgr24.bits_per_pixel(), Some(24));
482 assert_eq!(PixelFormat::Rgba.bits_per_pixel(), Some(32));
483 assert_eq!(PixelFormat::Bgra.bits_per_pixel(), Some(32));
484 assert_eq!(PixelFormat::Gray8.bits_per_pixel(), Some(8));
485
486 // Planar formats don't have simple bits per pixel
487 assert_eq!(PixelFormat::Yuv420p.bits_per_pixel(), None);
488 assert_eq!(PixelFormat::Nv12.bits_per_pixel(), None);
489 }
490
491 #[test]
492 fn test_bytes_per_pixel() {
493 // Packed formats
494 assert_eq!(PixelFormat::Rgb24.bytes_per_pixel(), 3);
495 assert_eq!(PixelFormat::Bgr24.bytes_per_pixel(), 3);
496 assert_eq!(PixelFormat::Rgba.bytes_per_pixel(), 4);
497 assert_eq!(PixelFormat::Bgra.bytes_per_pixel(), 4);
498 assert_eq!(PixelFormat::Gray8.bytes_per_pixel(), 1);
499
500 // YUV 4:2:0 - 1.5 bytes average, rounded to 2
501 assert_eq!(PixelFormat::Yuv420p.bytes_per_pixel(), 2);
502 assert_eq!(PixelFormat::Nv12.bytes_per_pixel(), 2);
503 assert_eq!(PixelFormat::Nv21.bytes_per_pixel(), 2);
504
505 // YUV 4:2:2 - 2 bytes
506 assert_eq!(PixelFormat::Yuv422p.bytes_per_pixel(), 2);
507
508 // YUV 4:4:4 - 3 bytes
509 assert_eq!(PixelFormat::Yuv444p.bytes_per_pixel(), 3);
510
511 // High bit depth
512 assert_eq!(PixelFormat::Yuv420p10le.bytes_per_pixel(), 2);
513 assert_eq!(PixelFormat::P010le.bytes_per_pixel(), 2);
514 }
515
516 #[test]
517 fn test_high_bit_depth() {
518 assert!(PixelFormat::Yuv420p10le.is_high_bit_depth());
519 assert!(PixelFormat::P010le.is_high_bit_depth());
520 assert!(!PixelFormat::Yuv420p.is_high_bit_depth());
521 assert!(!PixelFormat::Rgba.is_high_bit_depth());
522 }
523
524 #[test]
525 fn test_bit_depth() {
526 assert_eq!(PixelFormat::Rgba.bit_depth(), 8);
527 assert_eq!(PixelFormat::Yuv420p.bit_depth(), 8);
528 assert_eq!(PixelFormat::Yuv420p10le.bit_depth(), 10);
529 assert_eq!(PixelFormat::P010le.bit_depth(), 10);
530 }
531
532 #[test]
533 fn test_display() {
534 assert_eq!(format!("{}", PixelFormat::Yuv420p), "yuv420p");
535 assert_eq!(format!("{}", PixelFormat::Rgba), "rgba");
536 assert_eq!(format!("{}", PixelFormat::Other(123)), "unknown");
537 }
538
539 #[test]
540 fn test_default() {
541 assert_eq!(PixelFormat::default(), PixelFormat::Yuv420p);
542 }
543
544 #[test]
545 fn test_debug() {
546 assert_eq!(format!("{:?}", PixelFormat::Rgba), "Rgba");
547 assert_eq!(format!("{:?}", PixelFormat::Yuv420p), "Yuv420p");
548 assert_eq!(format!("{:?}", PixelFormat::Other(42)), "Other(42)");
549 }
550
551 #[test]
552 fn test_equality_and_hash() {
553 use std::collections::HashSet;
554
555 assert_eq!(PixelFormat::Rgba, PixelFormat::Rgba);
556 assert_ne!(PixelFormat::Rgba, PixelFormat::Bgra);
557 assert_eq!(PixelFormat::Other(1), PixelFormat::Other(1));
558 assert_ne!(PixelFormat::Other(1), PixelFormat::Other(2));
559
560 // Test Hash implementation
561 let mut set = HashSet::new();
562 set.insert(PixelFormat::Rgba);
563 set.insert(PixelFormat::Yuv420p);
564 assert!(set.contains(&PixelFormat::Rgba));
565 assert!(!set.contains(&PixelFormat::Bgra));
566 }
567
568 #[test]
569 fn test_copy() {
570 let format = PixelFormat::Yuv420p;
571 let copied = format;
572 // Both original and copy are still usable (Copy semantics)
573 assert_eq!(format, copied);
574 assert_eq!(format.name(), copied.name());
575 }
576}