oxideav_core/format.rs
1//! Media-type and sample/pixel format enumerations.
2//!
3//! Audio channel ordering follows SMPTE 2036-2 / ITU-R BS.775 conventions
4//! for surround layouts; per-channel positions are named with the
5//! WAVEFORMATEXTENSIBLE "front-left, front-right, …" vocabulary.
6
7/// Broad category of a stream's payload.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum MediaType {
10 /// Audio samples.
11 Audio,
12 /// Video pictures.
13 Video,
14 /// Timed-text / bitmap subtitle cues.
15 Subtitle,
16 /// Opaque non-media payload (timecodes, klv, chapters, …).
17 Data,
18 /// Category not (yet) determined.
19 Unknown,
20}
21
22/// A single speaker position within a multi-channel audio layout.
23///
24/// Names follow the WAVEFORMATEXTENSIBLE / SMPTE convention.
25/// `Side*` and `Back*` are kept distinct (mirroring 7.1's
26/// L/R + Ls/Rs + Lb/Rb separation) so codecs that surface the
27/// distinction don't collapse it. `Lr`/`Rr` (rear / back-rear) are aliases
28/// for `BackLeft`/`BackRight` in this taxonomy — the rear pair sits behind
29/// the listener on the room's centreline-extension, the side pair is at
30/// roughly ±90° from front. The enum is `#[non_exhaustive]` so additional
31/// positions (height channels for Atmos / Auro-3D, etc.) can be added
32/// without breaking downstream match arms.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum ChannelPosition {
36 /// Front-left (L). 30° left of centre in BS.775 listening geometry.
37 FrontLeft,
38 /// Front-right (R). 30° right of centre.
39 FrontRight,
40 /// Front-centre (C). Direct centre, 0°.
41 FrontCenter,
42 /// Low-frequency effects (LFE). Sub-bass, no positional meaning.
43 LowFrequency,
44 /// Back-left (Lb / Lr). Behind the listener, ±150° in 7.1.
45 BackLeft,
46 /// Back-right (Rb / Rr). Behind the listener, mirror of `BackLeft`.
47 BackRight,
48 /// Front left-of-centre (Lc). Used in cinema 7.1 SDDS layouts.
49 FrontLeftOfCenter,
50 /// Front right-of-centre (Rc). Mirror of `FrontLeftOfCenter`.
51 FrontRightOfCenter,
52 /// Back-centre (Cs). Single rear channel for 6.1 / BS.775 4.0.
53 BackCenter,
54 /// Side-left (Ls). ±90° on the listener's left in 5.1 / 7.1.
55 SideLeft,
56 /// Side-right (Rs). Mirror of `SideLeft`.
57 SideRight,
58 /// Top front-left. Atmos / Auro-3D height layer (placeholder).
59 TopFrontLeft,
60 /// Top front-right. Atmos / Auro-3D height layer (placeholder).
61 TopFrontRight,
62 /// Top back-left. Atmos / Auro-3D ceiling layer (placeholder).
63 TopBackLeft,
64 /// Top back-right. Atmos / Auro-3D ceiling layer (placeholder).
65 TopBackRight,
66}
67
68/// Audio channel layout — names a fixed ordered tuple of speaker
69/// positions, OR carries a discrete fallback count when the layout is
70/// unknown / non-standard.
71///
72/// Channel orderings are taken from ITU-R BS.775 (5.1 / 7.1 surround
73/// reference) and SMPTE ST 2036-2 (audio channel ordering for UHDTV).
74/// For 5.1 the canonical order this crate adopts is
75/// `L, R, C, LFE, Ls, Rs` (the WAVEFORMATEXTENSIBLE / Vorbis / Opus
76/// convention). 7.1 extends that with `Lb, Rb` (back-rear pair).
77///
78/// The `Stereo` variant covers both regular two-channel stereo and the
79/// AC-3 / AC-4 matrix-encoded downmix carriers `Lo/Ro` ("two of",
80/// downmix-compatible) and `Lt/Rt` ("matrix-encoded for Pro Logic
81/// extraction"); the dedicated [`LoRo`](ChannelLayout::LoRo) /
82/// [`LtRt`](ChannelLayout::LtRt) variants surface the distinction
83/// explicitly when a downstream filter or muxer needs it.
84///
85/// `DiscreteN(n)` is the catch-all for "we know there are `n` channels
86/// but no recognised layout" — used when a codec produces an unusual
87/// channel count (>8) or when the container failed to surface a layout
88/// flag. It is the only variant whose `position()` returns `None`.
89///
90/// Marked `#[non_exhaustive]` so additional standard layouts (Atmos
91/// 7.1.4, Auro-3D 9.1, …) can be added without breaking match-exhaustive
92/// downstream consumers.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub enum ChannelLayout {
96 /// Mono (1ch): C.
97 Mono,
98 /// Stereo (2ch): L, R.
99 Stereo,
100 /// 2.1 (3ch): L, R, LFE.
101 Stereo21,
102 /// 3.0 surround (3ch): L, R, C.
103 Surround30,
104 /// Quadraphonic (4ch): L, R, Ls, Rs — no centre, side surrounds.
105 Quad,
106 /// 4.0 surround per BS.775 (4ch): L, R, C, Cs — centre + back surround.
107 Surround40,
108 /// 4.1 surround (5ch): L, R, C, Cs, LFE.
109 Surround41,
110 /// 5.0 surround (5ch): L, R, C, Ls, Rs.
111 Surround50,
112 /// 5.1 surround (6ch): L, R, C, LFE, Ls, Rs.
113 Surround51,
114 /// 6.0 surround (6ch): L, R, C, Cs, Ls, Rs.
115 Surround60,
116 /// 6.1 surround (7ch): L, R, C, LFE, Cs, Ls, Rs.
117 Surround61,
118 /// 7.0 surround (7ch): L, R, C, Ls, Rs, Lb, Rb.
119 Surround70,
120 /// 7.1 surround (8ch): L, R, C, LFE, Ls, Rs, Lb, Rb.
121 Surround71,
122 /// AC-3 / AC-4 Lo/Ro stereo downmix (2ch). Two-channel mix preserving
123 /// downmix-compatibility coefficients; not matrix-encoded.
124 LoRo,
125 /// AC-3 / AC-4 Lt/Rt stereo downmix (2ch). Two-channel matrix-encoded
126 /// downmix carrying surround information for Dolby Pro Logic decoding.
127 LtRt,
128 /// Discrete fallback: `n` channels with no recognised layout. Used for
129 /// unusual / >8ch / unknown layouts surfaced by exotic codecs or
130 /// containers that drop layout flags.
131 DiscreteN(u16),
132}
133
134impl ChannelLayout {
135 /// Number of channels in this layout.
136 pub fn channel_count(&self) -> u16 {
137 match self {
138 Self::Mono => 1,
139 Self::Stereo | Self::LoRo | Self::LtRt => 2,
140 Self::Stereo21 | Self::Surround30 => 3,
141 Self::Quad | Self::Surround40 => 4,
142 Self::Surround41 | Self::Surround50 => 5,
143 Self::Surround51 | Self::Surround60 => 6,
144 Self::Surround61 | Self::Surround70 => 7,
145 Self::Surround71 => 8,
146 Self::DiscreteN(n) => *n,
147 }
148 }
149
150 /// Speaker positions in canonical order. Returns an empty slice for
151 /// `DiscreteN` since the layout is unknown — call [`positions_owned`]
152 /// to get a `Vec` if you need to enumerate slots regardless of
153 /// known/unknown status.
154 ///
155 /// [`positions_owned`]: Self::positions_owned
156 pub fn positions(&self) -> &'static [ChannelPosition] {
157 use ChannelPosition::*;
158 match self {
159 Self::Mono => &[FrontCenter],
160 Self::Stereo | Self::LoRo | Self::LtRt => &[FrontLeft, FrontRight],
161 Self::Stereo21 => &[FrontLeft, FrontRight, LowFrequency],
162 Self::Surround30 => &[FrontLeft, FrontRight, FrontCenter],
163 Self::Quad => &[FrontLeft, FrontRight, SideLeft, SideRight],
164 Self::Surround40 => &[FrontLeft, FrontRight, FrontCenter, BackCenter],
165 Self::Surround41 => &[FrontLeft, FrontRight, FrontCenter, BackCenter, LowFrequency],
166 Self::Surround50 => &[FrontLeft, FrontRight, FrontCenter, SideLeft, SideRight],
167 Self::Surround51 => &[
168 FrontLeft,
169 FrontRight,
170 FrontCenter,
171 LowFrequency,
172 SideLeft,
173 SideRight,
174 ],
175 Self::Surround60 => &[
176 FrontLeft,
177 FrontRight,
178 FrontCenter,
179 BackCenter,
180 SideLeft,
181 SideRight,
182 ],
183 Self::Surround61 => &[
184 FrontLeft,
185 FrontRight,
186 FrontCenter,
187 LowFrequency,
188 BackCenter,
189 SideLeft,
190 SideRight,
191 ],
192 Self::Surround70 => &[
193 FrontLeft,
194 FrontRight,
195 FrontCenter,
196 SideLeft,
197 SideRight,
198 BackLeft,
199 BackRight,
200 ],
201 Self::Surround71 => &[
202 FrontLeft,
203 FrontRight,
204 FrontCenter,
205 LowFrequency,
206 SideLeft,
207 SideRight,
208 BackLeft,
209 BackRight,
210 ],
211 Self::DiscreteN(_) => &[],
212 }
213 }
214
215 /// Owned position list. For known layouts this clones [`positions`];
216 /// for `DiscreteN(n)` it returns an empty `Vec` (positions remain
217 /// unknown). Provided so callers that just want "give me positions
218 /// for any layout" don't have to special-case the discrete arm.
219 ///
220 /// [`positions`]: Self::positions
221 pub fn positions_owned(&self) -> Vec<ChannelPosition> {
222 self.positions().to_vec()
223 }
224
225 /// Speaker position at slot `idx` in canonical order, or `None` for
226 /// out-of-range slots and for `DiscreteN` (where the layout is
227 /// unknown).
228 pub fn position(&self, idx: usize) -> Option<ChannelPosition> {
229 self.positions().get(idx).copied()
230 }
231
232 /// True when this layout carries a low-frequency-effects (LFE) channel.
233 pub fn has_lfe(&self) -> bool {
234 self.positions()
235 .iter()
236 .any(|p| matches!(p, ChannelPosition::LowFrequency))
237 }
238
239 /// True when this layout carries surround information (more than two
240 /// channels OR an LFE). `Stereo` / `Mono` return false; `LoRo` /
241 /// `LtRt` are 2-channel downmixes and also return false even though
242 /// they encode surround content (that's the whole point of a
243 /// downmix).
244 pub fn is_surround(&self) -> bool {
245 self.channel_count() > 2 || self.has_lfe()
246 }
247
248 /// Back-compat bridge: infer a layout from a bare channel count.
249 ///
250 /// This mapping is what lets codecs that haven't been updated to set
251 /// a layout explicitly continue to work: they keep producing a count
252 /// and we infer the most-common layout for that count. The choices
253 /// follow industry defaults — 5.1 wins for 6ch (more common than
254 /// 6.0), 7.1 wins for 8ch, and so on.
255 ///
256 /// | count | layout |
257 /// |-------|--------------|
258 /// | 1 | `Mono` |
259 /// | 2 | `Stereo` |
260 /// | 3 | `Surround30` |
261 /// | 4 | `Quad` |
262 /// | 5 | `Surround50` |
263 /// | 6 | `Surround51` |
264 /// | 7 | `Surround61` |
265 /// | 8 | `Surround71` |
266 /// | other | `DiscreteN` |
267 pub fn from_count(n: u16) -> ChannelLayout {
268 match n {
269 1 => Self::Mono,
270 2 => Self::Stereo,
271 3 => Self::Surround30,
272 4 => Self::Quad,
273 5 => Self::Surround50,
274 6 => Self::Surround51,
275 7 => Self::Surround61,
276 8 => Self::Surround71,
277 other => Self::DiscreteN(other),
278 }
279 }
280}
281
282impl std::fmt::Display for ChannelLayout {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 let s = match self {
285 Self::Mono => "mono",
286 Self::Stereo => "stereo",
287 Self::Stereo21 => "2.1",
288 Self::Surround30 => "3.0",
289 Self::Quad => "quad",
290 Self::Surround40 => "4.0",
291 Self::Surround41 => "4.1",
292 Self::Surround50 => "5.0",
293 Self::Surround51 => "5.1",
294 Self::Surround60 => "6.0",
295 Self::Surround61 => "6.1",
296 Self::Surround70 => "7.0",
297 Self::Surround71 => "7.1",
298 Self::LoRo => "loro",
299 Self::LtRt => "ltrt",
300 Self::DiscreteN(n) => return write!(f, "discrete{n}"),
301 };
302 f.write_str(s)
303 }
304}
305
306/// Error returned by the [`ChannelLayout`] `FromStr` impl when the input
307/// doesn't match any recognised layout name.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct ParseChannelLayoutError(pub String);
310
311impl std::fmt::Display for ParseChannelLayoutError {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 write!(f, "unrecognised channel layout: {:?}", self.0)
314 }
315}
316
317impl std::error::Error for ParseChannelLayoutError {}
318
319impl std::str::FromStr for ChannelLayout {
320 type Err = ParseChannelLayoutError;
321
322 fn from_str(s: &str) -> Result<Self, Self::Err> {
323 let lower = s.trim().to_ascii_lowercase();
324 let layout = match lower.as_str() {
325 "mono" | "1.0" => Self::Mono,
326 "stereo" | "2.0" => Self::Stereo,
327 "2.1" => Self::Stereo21,
328 "3.0" | "surround3" | "surround30" => Self::Surround30,
329 "quad" => Self::Quad,
330 "4.0" | "surround4" | "surround40" => Self::Surround40,
331 "4.1" | "surround41" => Self::Surround41,
332 "5.0" | "surround5" | "surround50" => Self::Surround50,
333 "5.1" | "surround51" => Self::Surround51,
334 "6.0" | "surround6" | "surround60" => Self::Surround60,
335 "6.1" | "surround61" => Self::Surround61,
336 "7.0" | "surround7" | "surround70" => Self::Surround70,
337 "7.1" | "surround71" => Self::Surround71,
338 "loro" | "lo/ro" => Self::LoRo,
339 "ltrt" | "lt/rt" => Self::LtRt,
340 other => {
341 if let Some(rest) = other.strip_prefix("discrete") {
342 if let Ok(n) = rest.parse::<u16>() {
343 return Ok(Self::DiscreteN(n));
344 }
345 }
346 return Err(ParseChannelLayoutError(s.to_owned()));
347 }
348 };
349 Ok(layout)
350 }
351}
352
353/// Audio sample format.
354///
355/// Variants carry **stable explicit discriminants** — the integer value
356/// of `SampleFormat::S16 as u8` is part of the public ABI. Add new
357/// variants only at the end with a fresh number; never reorder, renumber,
358/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
359/// downstream `match` statements; pinned discriminants additionally let
360/// the format round-trip through any byte-stable serialization
361/// (config files, capability blobs, IPC) without losing meaning across
362/// crate versions.
363#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
364#[non_exhaustive]
365#[repr(u8)]
366pub enum SampleFormat {
367 /// Unsigned 8-bit, interleaved.
368 U8 = 0,
369 /// Signed 8-bit, interleaved. Native format of Amiga 8SVX and MOD samples.
370 S8 = 1,
371 /// Signed 16-bit little-endian, interleaved.
372 S16 = 2,
373 /// Signed 24-bit packed (3 bytes/sample) little-endian, interleaved.
374 S24 = 3,
375 /// Signed 32-bit little-endian, interleaved.
376 S32 = 4,
377 /// 32-bit IEEE float, interleaved.
378 F32 = 5,
379 /// 64-bit IEEE float, interleaved.
380 F64 = 6,
381 /// Unsigned 8-bit, planar (one plane per channel).
382 U8P = 7,
383 /// Signed 16-bit little-endian, planar (one plane per channel).
384 S16P = 8,
385 /// Signed 32-bit little-endian, planar (one plane per channel).
386 S32P = 9,
387 /// 32-bit IEEE float, planar (one plane per channel).
388 F32P = 10,
389 /// 64-bit IEEE float, planar (one plane per channel).
390 F64P = 11,
391}
392
393impl SampleFormat {
394 /// `true` for the planar (one-plane-per-channel) variants.
395 pub fn is_planar(&self) -> bool {
396 matches!(
397 self,
398 Self::U8P | Self::S16P | Self::S32P | Self::F32P | Self::F64P
399 )
400 }
401
402 /// Bytes per sample *per channel*.
403 pub fn bytes_per_sample(&self) -> usize {
404 match self {
405 Self::U8 | Self::U8P | Self::S8 => 1,
406 Self::S16 | Self::S16P => 2,
407 Self::S24 => 3,
408 Self::S32 | Self::S32P | Self::F32 | Self::F32P => 4,
409 Self::F64 | Self::F64P => 8,
410 }
411 }
412
413 /// `true` for the IEEE-float variants (32- or 64-bit, either layout).
414 pub fn is_float(&self) -> bool {
415 matches!(self, Self::F32 | Self::F64 | Self::F32P | Self::F64P)
416 }
417
418 /// Number of `Vec<u8>` planes an [`AudioFrame`](crate::AudioFrame)
419 /// of this format carries for `channels` channels: planar formats
420 /// use one plane per channel, interleaved formats use one plane
421 /// total.
422 pub fn plane_count(&self, channels: u16) -> usize {
423 if self.is_planar() {
424 channels as usize
425 } else {
426 1
427 }
428 }
429}
430
431/// Video pixel format.
432///
433/// Variants carry **stable explicit discriminants** — the integer value
434/// of `PixelFormat::Yuv420P as u16` is part of the public ABI. Add new
435/// variants only at the end with a fresh number; never reorder, renumber,
436/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
437/// downstream `match` statements; pinned discriminants additionally let
438/// the format round-trip through any byte-stable serialization
439/// (config files, capability blobs, IPC, on-disk caches) without losing
440/// meaning across crate versions, and prevent inserts in the middle of
441/// the enum from shifting every later variant's number (which
442/// cargo-semver-checks rightly flags as a breaking change).
443///
444/// The first six variants (`Yuv420P` through `Gray8`) are the original
445/// formats produced by the early codec crates. Everything beyond that
446/// is additional surface handled by `oxideav-pixfmt` and the still-image
447/// codecs (PNG, GIF, still-JPEG).
448#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
449#[non_exhaustive]
450#[repr(u16)]
451pub enum PixelFormat {
452 /// 8-bit YUV 4:2:0, planar (Y, U, V).
453 Yuv420P = 0,
454 /// 8-bit YUV 4:2:2, planar.
455 Yuv422P = 1,
456 /// 8-bit YUV 4:4:4, planar.
457 Yuv444P = 2,
458 /// Packed 8-bit RGB, 3 bytes/pixel.
459 Rgb24 = 3,
460 /// Packed 8-bit RGBA, 4 bytes/pixel.
461 Rgba = 4,
462 /// Packed 8-bit grayscale.
463 Gray8 = 5,
464
465 // --- Palette ---
466 /// 8-bit palette indices — companion palette carried out of band.
467 Pal8 = 6,
468
469 // --- Packed RGB/BGR swizzles ---
470 /// Packed 8-bit BGR, 3 bytes/pixel.
471 Bgr24 = 7,
472 /// Packed 8-bit BGRA, 4 bytes/pixel.
473 Bgra = 8,
474 /// Packed 8-bit ARGB, 4 bytes/pixel (alpha first).
475 Argb = 9,
476 /// Packed 8-bit ABGR, 4 bytes/pixel.
477 Abgr = 10,
478
479 // --- Deeper packed RGB ---
480 /// Packed 16-bit-per-channel RGB, little-endian, 6 bytes/pixel.
481 Rgb48Le = 11,
482 /// Packed 16-bit-per-channel RGBA, little-endian, 8 bytes/pixel.
483 Rgba64Le = 12,
484
485 // --- Grayscale deeper / partial bit depths ---
486 /// 16-bit little-endian grayscale.
487 Gray16Le = 13,
488 /// 10-bit grayscale in a 16-bit little-endian word.
489 Gray10Le = 14,
490 /// 12-bit grayscale in a 16-bit little-endian word.
491 Gray12Le = 15,
492
493 // --- Higher-precision YUV ---
494 /// 10-bit YUV 4:2:0 planar, little-endian 16-bit storage.
495 Yuv420P10Le = 16,
496 /// 10-bit YUV 4:2:2 planar, little-endian 16-bit storage.
497 Yuv422P10Le = 17,
498 /// 10-bit YUV 4:4:4 planar, little-endian 16-bit storage.
499 Yuv444P10Le = 18,
500 /// 12-bit YUV 4:2:0 planar, little-endian 16-bit storage.
501 Yuv420P12Le = 19,
502 /// 12-bit YUV 4:2:2 planar, little-endian 16-bit storage.
503 Yuv422P12Le = 20,
504 /// 12-bit YUV 4:4:4 planar, little-endian 16-bit storage.
505 Yuv444P12Le = 21,
506
507 // --- Full-range ("J") YUV ---
508 /// JPEG/full-range YUV 4:2:0 planar.
509 YuvJ420P = 22,
510 /// JPEG/full-range YUV 4:2:2 planar.
511 YuvJ422P = 23,
512 /// JPEG/full-range YUV 4:4:4 planar.
513 YuvJ444P = 24,
514
515 // --- Semi-planar YUV ---
516 /// YUV 4:2:0, planar Y + interleaved UV (NV12).
517 Nv12 = 25,
518 /// YUV 4:2:0, planar Y + interleaved VU (NV21).
519 Nv21 = 26,
520
521 // --- Gray + alpha / YUV + alpha ---
522 /// Packed grayscale + alpha, 2 bytes/pixel (Y, A).
523 Ya8 = 27,
524 /// Yuv420P with an additional full-resolution alpha plane.
525 Yuva420P = 28,
526
527 // --- Mono (1 bit per pixel) ---
528 /// 1 bit per pixel, packed MSB-first, 0 = black.
529 MonoBlack = 29,
530 /// 1 bit per pixel, packed MSB-first, 0 = white.
531 MonoWhite = 30,
532
533 // --- Interleaved YUV 4:2:2 ---
534 /// Packed 4:2:2, byte order Y0 U0 Y1 V0.
535 Yuyv422 = 31,
536 /// Packed 4:2:2, byte order U0 Y0 V0 Y1.
537 Uyvy422 = 32,
538
539 // --- Print / prepress ---
540 /// Packed 8-bit CMYK, 4 bytes/pixel in byte order C, M, Y, K.
541 /// "Regular" convention: C=0 means no cyan ink (white), C=255 means
542 /// full cyan. Used by JPEG 4-component scans from non-Adobe encoders
543 /// and by many print-side image toolchains. Adobe Photoshop's
544 /// inverted CMYK (where 0 = full ink) is the separate
545 /// [`CmykInverted`](Self::CmykInverted) variant.
546 Cmyk = 33,
547
548 // --- Wide-horizontal subsampled YUV ---
549 /// 8-bit YUV 4:1:1, planar (Y, U, V). Luma at full resolution; chroma
550 /// horizontally subsampled by 4 (each chroma sample covers a 4×1
551 /// luma block), no vertical subsampling. Native sampling of
552 /// NTSC DV-25 and a legal JPEG sampling layout (luma H=4, V=1;
553 /// chroma H=V=1) emitted by some real-world JPEG corpora.
554 Yuv411P = 34,
555
556 // --- Planar GBR / GBRA (RGB stored as planes in G,B,R order) ---
557 //
558 // High-bit-depth GBR(A) layouts used by MagicYUV, JPEG 2000, OpenEXR,
559 // TIFF and similar workflows that need lossless RGB at 10/12/14 bits
560 // per channel. Planes are ordered G, B, R (and A for the `Gbrap*`
561 // variants) — and
562 // each sample is stored as a 16-bit little-endian word with the
563 // top bits zero. The native 8-bit ([`Gbrp8`](Self::Gbrp8)) and
564 // full-width 16-bit ([`Gbrp16Le`](Self::Gbrp16Le) /
565 // [`Gbrap16Le`](Self::Gbrap16Le)) companions arrived later and
566 // therefore live at fresh appended discriminants (52-54), per the
567 // append-only rule.
568 /// 10-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
569 /// G, B, R; each sample uses the low 10 bits of a 16-bit word.
570 Gbrp10Le = 35,
571 /// 10-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
572 /// ordered G, B, R, A; each sample uses the low 10 bits of a 16-bit
573 /// word.
574 Gbrap10Le = 36,
575 /// 12-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
576 /// G, B, R; each sample uses the low 12 bits of a 16-bit word.
577 Gbrp12Le = 37,
578 /// 12-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
579 /// ordered G, B, R, A; each sample uses the low 12 bits of a 16-bit
580 /// word.
581 Gbrap12Le = 38,
582 /// 14-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
583 /// G, B, R; each sample uses the low 14 bits of a 16-bit word.
584 Gbrp14Le = 39,
585 /// 14-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
586 /// ordered G, B, R, A; each sample uses the low 14 bits of a 16-bit
587 /// word.
588 Gbrap14Le = 40,
589
590 // --- 16-bit YUV planar ---
591 //
592 // Full-width companions to the 10/12-bit planar YUV variants above:
593 // same three-plane layout and little-endian 16-bit words, but ALL 16
594 // bits of every word are significant (there are no zero top bits and
595 // no separate "valid bits" count — full-scale is 65535). Needed by
596 // wavelet codecs whose signal-range presets go to 16 bits per
597 // component (SMPTE VC-2 / Dirac video-format presets 7 and 8).
598 /// 16-bit YUV 4:2:0 planar, little-endian 16-bit storage. All 16
599 /// bits of each sample word are significant.
600 Yuv420P16Le = 41,
601 /// 16-bit YUV 4:2:2 planar, little-endian 16-bit storage. All 16
602 /// bits of each sample word are significant.
603 Yuv422P16Le = 42,
604 /// 16-bit YUV 4:4:4 planar, little-endian 16-bit storage. All 16
605 /// bits of each sample word are significant.
606 Yuv444P16Le = 43,
607
608 // --- 8-bit YUV + alpha at the remaining chroma samplings ---
609 //
610 // Companions to `Yuva420P`: the alpha plane is always full
611 // resolution (one 8-bit sample per pixel, never chroma-subsampled),
612 // appended after the V plane as plane index 3. Intermediate/mezzanine
613 // codecs carry alpha at 4:2:2 and 4:4:4 samplings.
614 /// Yuv422P with an additional full-resolution alpha plane.
615 Yuva422P = 44,
616 /// Yuv444P with an additional full-resolution alpha plane.
617 Yuva444P = 45,
618
619 // --- Deep YUV + alpha (10/12/16-bit words with full-resolution A) ---
620 //
621 // Alpha-carrying companions to the 10/12/16-bit planar YUV variants
622 // above, completing the Yuva family for mezzanine codecs that carry
623 // deep colour together with an alpha channel. Same conventions as
624 // the 8-bit `Yuva*` trio: 4 planes ordered Y, U, V, A with the
625 // alpha plane always at full resolution (one sample per pixel,
626 // never chroma-subsampled) as plane index 3. Every sample — alpha
627 // included — is stored as a little-endian 16-bit word; for the
628 // 10/12-bit variants each sample uses the low bits of the word with
629 // the top bits zero, and for the 16-bit variants all 16 bits of
630 // every word are significant (full-scale is 65535), matching
631 // `Yuv420P16Le`/`Yuv422P16Le`/`Yuv444P16Le`.
632 /// 10-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
633 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
634 /// the low 10 bits of a 16-bit word.
635 Yuva422P10Le = 46,
636 /// 12-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
637 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
638 /// the low 12 bits of a 16-bit word.
639 Yuva422P12Le = 47,
640 /// 10-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
641 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
642 /// the low 10 bits of a 16-bit word.
643 Yuva444P10Le = 48,
644 /// 12-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
645 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
646 /// the low 12 bits of a 16-bit word.
647 Yuva444P12Le = 49,
648 /// 16-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
649 /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
650 /// each sample word are significant.
651 Yuva422P16Le = 50,
652 /// 16-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
653 /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
654 /// each sample word are significant.
655 Yuva444P16Le = 51,
656
657 // --- Native 8-bit and full-width 16-bit planar GBR(A) ---
658 //
659 // Companions to the 10/12/14-bit `Gbrp*`/`Gbrap*` family above,
660 // closing the planar-RGB depth ladder at both ends for lossless
661 // RGB codecs whose native coding space is per-plane G, B, R.
662 // Plane order is identical to the rest of the family: G, B, R
663 // (and A as plane index 3 for `Gbrap16Le`, always at full
664 // resolution — RGB has no chroma subsampling). `Gbrp8` stores one
665 // byte per sample with all 8 bits significant; the 16-bit variants
666 // store little-endian 16-bit words with ALL 16 bits significant
667 // (full-scale is 65535, matching the `Yuv*P16Le` convention — no
668 // zero top bits, no separate valid-bits count). Odd in-between
669 // depths on these storage formats (e.g. 9- or 15-bit RGB) are
670 // expressed via the per-plane significant-bits side-channel on
671 // `VideoFrame`, not by new enum variants.
672 /// 8-bit planar GBR. 3 planes ordered G, B, R; one byte per
673 /// sample, all 8 bits significant.
674 Gbrp8 = 52,
675 /// 16-bit planar GBR, little-endian 16-bit storage. 3 planes
676 /// ordered G, B, R; all 16 bits of each sample word are
677 /// significant.
678 Gbrp16Le = 53,
679 /// 16-bit planar GBR + alpha, little-endian 16-bit storage. 4
680 /// planes ordered G, B, R, A; all 16 bits of each sample word are
681 /// significant.
682 Gbrap16Le = 54,
683
684 // --- Deep YUV + alpha at 4:2:0 ---
685 //
686 // Completes the deep Yuva family begun by the 4:2:2/4:4:4 variants
687 // above (46-51) at the remaining chroma sampling. Same conventions:
688 // 4 planes ordered Y, U, V, A with the alpha plane always at full
689 // resolution (one sample per pixel, never chroma-subsampled) as
690 // plane index 3. Every sample — alpha included — is stored as a
691 // little-endian 16-bit word; the 10/12-bit variants keep values in
692 // the low bits of the word with the top bits zero, and the 16-bit
693 // variant has all 16 bits of every word significant (full-scale is
694 // 65535), matching `Yuv420P16Le`.
695 /// 10-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
696 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
697 /// the low 10 bits of a 16-bit word.
698 Yuva420P10Le = 55,
699 /// 12-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
700 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
701 /// the low 12 bits of a 16-bit word.
702 Yuva420P12Le = 56,
703 /// 16-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
704 /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
705 /// each sample word are significant.
706 Yuva420P16Le = 57,
707
708 // --- 8-bit planar GBR + alpha ---
709 //
710 // Alpha-carrying companion to `Gbrp8`, filling the last hole in
711 // the planar GBR(A) family: with this variant every depth on the
712 // ladder (8/10/12/14/16) exists in both alpha-less and
713 // alpha-carrying form. Same conventions as the rest of the family:
714 // planes ordered G, B, R, A with the alpha plane at full
715 // resolution (RGB has no chroma subsampling) as plane index 3,
716 // one byte per sample, all 8 bits significant. Lossless RGB codecs
717 // whose native coding space is per-plane G, B, R carry 8-bit RGBA
718 // in exactly this shape.
719 /// 8-bit planar GBR + alpha. 4 planes ordered G, B, R, A; one
720 /// byte per sample, all 8 bits significant.
721 Gbrap8 = 58,
722
723 // --- Deep gray + alpha ---
724 //
725 // 16-bit companion to `Ya8`, ending the gray+alpha ladder at the
726 // same depth the plain gray ladder already reaches (`Gray16Le`).
727 // Still-image wire formats carry 16-bit greyscale-with-alpha
728 // natively (PNG colour type 4 at bit depth 16); without this
729 // variant that content must detour through `Rgba64Le`, tripling
730 // the gray payload and losing the "single luminance component"
731 // semantics. Same packed shape as `Ya8` — interleaved Y then A —
732 // with each sample widened to a little-endian 16-bit word, all 16
733 // bits significant (full-scale is 65535, the `Gray16Le`
734 // convention). In-between gray+alpha depths stay the job of the
735 // per-plane significant-bits side-channel.
736 /// Packed 16-bit grayscale + alpha, little-endian, 4 bytes/pixel
737 /// (Y, A). All 16 bits of each sample word are significant.
738 Ya16Le = 59,
739
740 // --- Print / prepress, inverted-ink convention ---
741 //
742 // The companion `Cmyk` (33) reserved this name when it was added:
743 // Adobe-authored 4-component scans store ink coverage inverted on
744 // the wire (0 = full ink, 255 = no ink), and decoders that want to
745 // hand the wire values through losslessly need a format that says
746 // so rather than silently re-using the regular-convention `Cmyk`.
747 /// Packed 8-bit inverted CMYK, 4 bytes/pixel in byte order C, M,
748 /// Y, K. Inverted-ink convention: C=0 means full cyan ink, C=255
749 /// means no cyan (white) — the complement of [`Cmyk`](Self::Cmyk).
750 CmykInverted = 60,
751
752 // --- 4:4:0 planar YUV (full-width, half-height chroma) ---
753 //
754 // Vertical-only chroma subsampling: each chroma plane keeps the
755 // full luma width but carries half the rows — subsampling shifts
756 // ssx = 0, ssy = 1, the transpose of 4:2:2's half-width,
757 // full-height geometry. A legal JPEG sampling combination (luma
758 // H=1, V=2) seen in real-world corpora, and a coded
759 // chroma-sampling mode of video bitstreams whose sampling flags
760 // allow horizontal and vertical decimation to be chosen
761 // independently. The depth ladder mirrors the other planar YUV
762 // samplings: 8-bit bytes, then 10/12-bit values in the low bits
763 // of little-endian 16-bit words, then full-width 16-bit words
764 // with every bit significant (full-scale is 65535).
765 /// 8-bit YUV 4:4:0, planar (Y, U, V). Chroma at full width, half
766 /// height (ssx = 0, ssy = 1).
767 Yuv440P = 61,
768 /// 10-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each
769 /// sample uses the low 10 bits of a 16-bit word.
770 Yuv440P10Le = 62,
771 /// 12-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each
772 /// sample uses the low 12 bits of a 16-bit word.
773 Yuv440P12Le = 63,
774 /// 16-bit YUV 4:4:0 planar, little-endian 16-bit storage. All 16
775 /// bits of each sample word are significant.
776 Yuv440P16Le = 64,
777
778 // --- Scene-referred 32-bit float (linear-light HDR) ---
779 //
780 // IEEE 754 binary32 components stored as little-endian 32-bit
781 // words, one word per sample. Unlike every integer format above
782 // there is no integer full-scale: samples are scene-referred
783 // linear light where 1.0 is the nominal diffuse-white anchor and
784 // values outside [0, 1] are legal (speculars above white,
785 // negative out-of-gamut excursions). Needed by HDR image wire
786 // formats whose native component type is floating point. The
787 // packed trio mirrors `Gray8`/`Rgb24`/`Rgba` component orders at
788 // float width; the planar pair extends the planar GBR(A) family
789 // beyond the integer depth ladder, with the usual G, B, R (+ A)
790 // plane order and the alpha plane at full resolution as plane
791 // index 3.
792 /// Packed 32-bit float grayscale, little-endian, 4 bytes/pixel.
793 /// Scene-referred linear light.
794 GrayF32Le = 65,
795 /// Packed 32-bit float RGB, little-endian, 12 bytes/pixel in
796 /// component order R, G, B. Scene-referred linear light.
797 RgbF32Le = 66,
798 /// Packed 32-bit float RGBA, little-endian, 16 bytes/pixel in
799 /// component order R, G, B, A. Scene-referred linear light;
800 /// alpha is straight (non-premultiplied), nominal range [0, 1].
801 RgbaF32Le = 67,
802 /// 32-bit float planar GBR, little-endian. 3 planes ordered G, B,
803 /// R; one 4-byte word per sample. Scene-referred linear light.
804 GbrpF32Le = 68,
805 /// 32-bit float planar GBR + alpha, little-endian. 4 planes
806 /// ordered G, B, R, A; one 4-byte word per sample; the alpha
807 /// plane is at full resolution as plane index 3, straight
808 /// (non-premultiplied), nominal range [0, 1].
809 GbrapF32Le = 69,
810}
811
812impl PixelFormat {
813 /// True if this format stores its components in separate planes.
814 pub fn is_planar(&self) -> bool {
815 matches!(
816 self,
817 Self::Yuv420P
818 | Self::Yuv422P
819 | Self::Yuv444P
820 | Self::Yuv411P
821 | Self::Yuv420P10Le
822 | Self::Yuv422P10Le
823 | Self::Yuv444P10Le
824 | Self::Yuv420P12Le
825 | Self::Yuv422P12Le
826 | Self::Yuv444P12Le
827 | Self::Yuv420P16Le
828 | Self::Yuv422P16Le
829 | Self::Yuv444P16Le
830 | Self::Yuv440P
831 | Self::Yuv440P10Le
832 | Self::Yuv440P12Le
833 | Self::Yuv440P16Le
834 | Self::YuvJ420P
835 | Self::YuvJ422P
836 | Self::YuvJ444P
837 | Self::Nv12
838 | Self::Nv21
839 | Self::Yuva420P
840 | Self::Yuva422P
841 | Self::Yuva444P
842 | Self::Yuva422P10Le
843 | Self::Yuva422P12Le
844 | Self::Yuva444P10Le
845 | Self::Yuva444P12Le
846 | Self::Yuva422P16Le
847 | Self::Yuva444P16Le
848 | Self::Yuva420P10Le
849 | Self::Yuva420P12Le
850 | Self::Yuva420P16Le
851 | Self::Gbrp8
852 | Self::Gbrap8
853 | Self::Gbrp10Le
854 | Self::Gbrap10Le
855 | Self::Gbrp12Le
856 | Self::Gbrap12Le
857 | Self::Gbrp14Le
858 | Self::Gbrap14Le
859 | Self::Gbrp16Le
860 | Self::Gbrap16Le
861 | Self::GbrpF32Le
862 | Self::GbrapF32Le
863 )
864 }
865
866 /// True if the format is a palette index format (`Pal8`).
867 pub fn is_palette(&self) -> bool {
868 matches!(self, Self::Pal8)
869 }
870
871 /// True if this format carries an alpha channel.
872 pub fn has_alpha(&self) -> bool {
873 matches!(
874 self,
875 Self::Rgba
876 | Self::Bgra
877 | Self::Argb
878 | Self::Abgr
879 | Self::Rgba64Le
880 | Self::Ya8
881 | Self::Ya16Le
882 | Self::Yuva420P
883 | Self::Yuva422P
884 | Self::Yuva444P
885 | Self::Yuva422P10Le
886 | Self::Yuva422P12Le
887 | Self::Yuva444P10Le
888 | Self::Yuva444P12Le
889 | Self::Yuva422P16Le
890 | Self::Yuva444P16Le
891 | Self::Yuva420P10Le
892 | Self::Yuva420P12Le
893 | Self::Yuva420P16Le
894 | Self::Gbrap8
895 | Self::Gbrap10Le
896 | Self::Gbrap12Le
897 | Self::Gbrap14Le
898 | Self::Gbrap16Le
899 | Self::RgbaF32Le
900 | Self::GbrapF32Le
901 )
902 }
903
904 /// True for the 32-bit IEEE-float variants, packed or planar.
905 /// Float formats are scene-referred: samples carry linear light
906 /// with no integer full-scale — 1.0 is the nominal diffuse-white
907 /// anchor and values outside [0, 1] are legal.
908 pub fn is_float(&self) -> bool {
909 matches!(
910 self,
911 Self::GrayF32Le | Self::RgbF32Le | Self::RgbaF32Le | Self::GbrpF32Le | Self::GbrapF32Le
912 )
913 }
914
915 /// Number of planes in the stored layout. Packed and palette formats
916 /// return 1; NV12/NV21 return 2; planar YUV without alpha and the
917 /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4.
918 pub fn plane_count(&self) -> usize {
919 match self {
920 Self::Nv12 | Self::Nv21 => 2,
921 Self::Yuv420P
922 | Self::Yuv422P
923 | Self::Yuv444P
924 | Self::Yuv411P
925 | Self::Yuv420P10Le
926 | Self::Yuv422P10Le
927 | Self::Yuv444P10Le
928 | Self::Yuv420P12Le
929 | Self::Yuv422P12Le
930 | Self::Yuv444P12Le
931 | Self::Yuv420P16Le
932 | Self::Yuv422P16Le
933 | Self::Yuv444P16Le
934 | Self::Yuv440P
935 | Self::Yuv440P10Le
936 | Self::Yuv440P12Le
937 | Self::Yuv440P16Le
938 | Self::YuvJ420P
939 | Self::YuvJ422P
940 | Self::YuvJ444P
941 | Self::Gbrp8
942 | Self::Gbrp10Le
943 | Self::Gbrp12Le
944 | Self::Gbrp14Le
945 | Self::Gbrp16Le
946 | Self::GbrpF32Le => 3,
947 Self::Yuva420P
948 | Self::Yuva422P
949 | Self::Yuva444P
950 | Self::Yuva422P10Le
951 | Self::Yuva422P12Le
952 | Self::Yuva444P10Le
953 | Self::Yuva444P12Le
954 | Self::Yuva422P16Le
955 | Self::Yuva444P16Le
956 | Self::Yuva420P10Le
957 | Self::Yuva420P12Le
958 | Self::Yuva420P16Le
959 | Self::Gbrap8
960 | Self::Gbrap10Le
961 | Self::Gbrap12Le
962 | Self::Gbrap14Le
963 | Self::Gbrap16Le
964 | Self::GbrapF32Le => 4,
965 _ => 1,
966 }
967 }
968
969 /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
970 /// for chroma-subsampled YUV — intended for worst-case preallocation
971 /// rather than wire-accurate accounting.
972 pub fn bits_per_pixel_approx(&self) -> u32 {
973 match self {
974 Self::MonoBlack | Self::MonoWhite => 1,
975 Self::Gray8 | Self::Pal8 => 8,
976 Self::Ya8 => 16,
977 // 16-bit gray + alpha: two LE 16-bit words per pixel, all
978 // bits significant — packed bits equal storage bits.
979 Self::Ya16Le => 32,
980 Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
981 Self::Rgb24 | Self::Bgr24 => 24,
982 Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
983 Self::Rgb48Le => 48,
984 Self::Rgba64Le => 64,
985 Self::Yuyv422 | Self::Uyvy422 => 16,
986 Self::Cmyk | Self::CmykInverted => 32,
987 // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
988 // 10/12-bit variants double the byte count but we report the
989 // packed-bits-per-pixel estimate for a uniform heuristic.
990 Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
991 // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
992 // full res + 2 chroma planes each subsampled by 4).
993 Self::Yuv411P => 12,
994 Self::Yuv422P | Self::YuvJ422P => 16,
995 // 4:4:0 packs the same 2 samples/pixel as 4:2:2 (Y at full
996 // res + 2 chroma planes at half height, full width).
997 Self::Yuv440P => 16,
998 Self::Yuv444P | Self::YuvJ444P => 24,
999 Self::Yuv420P10Le | Self::Yuv420P12Le | Self::Yuv420P16Le => 24,
1000 Self::Yuv422P10Le | Self::Yuv422P12Le | Self::Yuv422P16Le => 32,
1001 // Deep 4:4:0 matches deep 4:2:2 — 2 sample words per pixel.
1002 Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => 32,
1003 Self::Yuv444P10Le | Self::Yuv444P12Le | Self::Yuv444P16Le => 48,
1004 Self::Yuva420P => 20,
1005 // 4:2:2 + full-res alpha: 8 (Y) + 4 (U) + 4 (V) + 8 (A).
1006 Self::Yuva422P => 24,
1007 // 4:4:4 + full-res alpha: four full-resolution 8-bit planes.
1008 Self::Yuva444P => 32,
1009 // Deep 4:2:2 + full-res alpha in 16-bit words: the estimator
1010 // reports the 16-bit-word cost like the alpha-less deep YUV
1011 // arms above — 3 sample words per pixel (Y + U/2 + V/2 + A).
1012 Self::Yuva422P10Le | Self::Yuva422P12Le | Self::Yuva422P16Le => 48,
1013 // Deep 4:4:4 + full-res alpha: 4 sample words per pixel.
1014 Self::Yuva444P10Le | Self::Yuva444P12Le | Self::Yuva444P16Le => 64,
1015 // Deep 4:2:0 + full-res alpha in 16-bit words: 16-bit-word
1016 // storage cost of the alpha-less 4:2:0 arms above (24) plus
1017 // one full-resolution 16-bit alpha word per pixel.
1018 Self::Yuva420P10Le | Self::Yuva420P12Le | Self::Yuva420P16Le => 40,
1019 // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we
1020 // report the packed bits-per-pixel density (samples × bits)
1021 // rather than the 16-bit storage cost, matching how the
1022 // 10/12-bit YUV variants are reported above.
1023 Self::Gbrp10Le => 30,
1024 Self::Gbrap10Le => 40,
1025 Self::Gbrp12Le => 36,
1026 Self::Gbrap12Le => 48,
1027 Self::Gbrp14Le => 42,
1028 Self::Gbrap14Le => 56,
1029 // Native 8-bit GBR: three bytes per pixel, like Rgb24 but
1030 // planar. 16-bit GBR(A): packed bits == storage bits (every
1031 // bit of each 16-bit word is significant), so the density
1032 // and storage numbers coincide.
1033 Self::Gbrp8 => 24,
1034 Self::Gbrp16Le => 48,
1035 Self::Gbrap16Le => 64,
1036 // 8-bit GBR + alpha: four bytes per pixel, like Rgba but
1037 // planar.
1038 Self::Gbrap8 => 32,
1039 // 32-bit float family: every sample is a full binary32
1040 // word, so packed bits equal storage bits (32 per sample;
1041 // no chroma subsampling anywhere in the family).
1042 Self::GrayF32Le => 32,
1043 Self::RgbF32Le | Self::GbrpF32Le => 96,
1044 Self::RgbaF32Le | Self::GbrapF32Le => 128,
1045 }
1046 }
1047
1048 /// Log2 chroma-subsampling shifts `(ssx, ssy)` relative to the
1049 /// luma grid, for formats that carry chroma on a subsampled (or
1050 /// potentially subsampled) grid. The chroma sample grid is the
1051 /// luma grid right-shifted by `ssx` horizontally and `ssy`
1052 /// vertically, with ceiling division for odd luma sizes (see
1053 /// [`plane_dimensions`](Self::plane_dimensions)).
1054 ///
1055 /// | sampling | `(ssx, ssy)` | chroma geometry |
1056 /// |----------|--------------|-----------------|
1057 /// | 4:2:0 | `(1, 1)` | half width, half height |
1058 /// | 4:2:2 | `(1, 0)` | half width, full height |
1059 /// | 4:4:4 | `(0, 0)` | full resolution |
1060 /// | 4:1:1 | `(2, 0)` | quarter width, full height |
1061 /// | 4:4:0 | `(0, 1)` | full width, half height |
1062 ///
1063 /// Returns `None` for formats without a distinct chroma grid
1064 /// (grayscale, RGB/GBR in any layout, palette, mono, CMYK).
1065 /// Packed 4:2:2 (`Yuyv422`/`Uyvy422`) and semi-planar 4:2:0
1066 /// (`Nv12`/`Nv21`) report their sampling even though the chroma
1067 /// samples don't live in standalone planes.
1068 ///
1069 /// ```
1070 /// use oxideav_core::PixelFormat;
1071 /// // 4:4:0: full-width, half-height chroma.
1072 /// assert_eq!(PixelFormat::Yuv440P.chroma_subsampling(), Some((0, 1)));
1073 /// // 4:2:0: subsampled on both axes.
1074 /// assert_eq!(PixelFormat::Yuv420P.chroma_subsampling(), Some((1, 1)));
1075 /// // RGB has no chroma grid.
1076 /// assert_eq!(PixelFormat::Rgba.chroma_subsampling(), None);
1077 /// ```
1078 pub fn chroma_subsampling(&self) -> Option<(u32, u32)> {
1079 match self {
1080 // 4:2:0 — half width, half height.
1081 Self::Yuv420P
1082 | Self::YuvJ420P
1083 | Self::Yuv420P10Le
1084 | Self::Yuv420P12Le
1085 | Self::Yuv420P16Le
1086 | Self::Nv12
1087 | Self::Nv21
1088 | Self::Yuva420P
1089 | Self::Yuva420P10Le
1090 | Self::Yuva420P12Le
1091 | Self::Yuva420P16Le => Some((1, 1)),
1092 // 4:2:2 — half width, full height (packed 4:2:2 included).
1093 Self::Yuv422P
1094 | Self::YuvJ422P
1095 | Self::Yuv422P10Le
1096 | Self::Yuv422P12Le
1097 | Self::Yuv422P16Le
1098 | Self::Yuva422P
1099 | Self::Yuva422P10Le
1100 | Self::Yuva422P12Le
1101 | Self::Yuva422P16Le
1102 | Self::Yuyv422
1103 | Self::Uyvy422 => Some((1, 0)),
1104 // 4:4:4 — chroma at full resolution.
1105 Self::Yuv444P
1106 | Self::YuvJ444P
1107 | Self::Yuv444P10Le
1108 | Self::Yuv444P12Le
1109 | Self::Yuv444P16Le
1110 | Self::Yuva444P
1111 | Self::Yuva444P10Le
1112 | Self::Yuva444P12Le
1113 | Self::Yuva444P16Le => Some((0, 0)),
1114 // 4:1:1 — quarter width, full height.
1115 Self::Yuv411P => Some((2, 0)),
1116 // 4:4:0 — full width, half height.
1117 Self::Yuv440P | Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => {
1118 Some((0, 1))
1119 }
1120 // Everything else has no distinct chroma grid.
1121 _ => None,
1122 }
1123 }
1124
1125 /// Sample-grid dimensions of plane `plane` for a `width` ×
1126 /// `height` picture, with ceiling division on subsampled axes so
1127 /// odd luma sizes still cover every pixel.
1128 ///
1129 /// Conventions:
1130 /// - Plane 0 (luma / the packed plane) is always `(width, height)`.
1131 /// - Chroma planes (indices 1 and 2 of planar YUV, index 1 of the
1132 /// semi-planar formats) are the luma grid right-shifted by the
1133 /// [`chroma_subsampling`](Self::chroma_subsampling) factors.
1134 /// Semi-planar chroma dimensions are in chroma *positions* —
1135 /// each position stores two interleaved samples, which
1136 /// [`plane_row_bytes`](Self::plane_row_bytes) accounts for.
1137 /// - Alpha planes (index 3) and all planar-RGB planes are at full
1138 /// resolution.
1139 /// - Packed, palette, and bit-packed mono formats report pixel
1140 /// dimensions for their single plane; per-row byte cost comes
1141 /// from [`plane_row_bytes`](Self::plane_row_bytes).
1142 ///
1143 /// Returns `None` when `plane >= plane_count()`.
1144 ///
1145 /// ```
1146 /// use oxideav_core::PixelFormat;
1147 /// // 4:4:0 chroma: full width, half height (odd height rounds up).
1148 /// assert_eq!(
1149 /// PixelFormat::Yuv440P.plane_dimensions(1, 640, 481),
1150 /// Some((640, 241))
1151 /// );
1152 /// // Alpha plane of a deep YUVA format stays at full resolution.
1153 /// assert_eq!(
1154 /// PixelFormat::Yuva420P10Le.plane_dimensions(3, 7, 5),
1155 /// Some((7, 5))
1156 /// );
1157 /// assert_eq!(PixelFormat::Rgb24.plane_dimensions(1, 8, 8), None);
1158 /// ```
1159 pub fn plane_dimensions(&self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> {
1160 if plane >= self.plane_count() {
1161 return None;
1162 }
1163 match (self.chroma_subsampling(), plane) {
1164 (Some((ssx, ssy)), 1 | 2) => {
1165 Some((width.div_ceil(1 << ssx), height.div_ceil(1 << ssy)))
1166 }
1167 _ => Some((width, height)),
1168 }
1169 }
1170
1171 /// Tightly-packed byte count of one row of plane `plane` for a
1172 /// picture `width` pixels wide — no stride padding or alignment.
1173 /// Real codecs frequently over-allocate rows for alignment; this
1174 /// is the minimum a row occupies.
1175 ///
1176 /// Returns `None` when `plane >= plane_count()` or the byte count
1177 /// overflows `usize`.
1178 pub fn plane_row_bytes(&self, plane: usize, width: u32) -> Option<usize> {
1179 let (pw, _) = self.plane_dimensions(plane, width, 1)?;
1180 let pw = pw as usize;
1181 let bytes_per_position: usize = match self {
1182 // Bit-packed mono: 8 pixels per byte, ragged tail byte.
1183 Self::MonoBlack | Self::MonoWhite => return Some(pw.div_ceil(8)),
1184 // Packed 4:2:2 macropixels: 4 bytes per 2 pixels; an odd
1185 // trailing pixel still occupies a full macropixel.
1186 Self::Yuyv422 | Self::Uyvy422 => return pw.div_ceil(2).checked_mul(4),
1187 // One byte per sample position.
1188 Self::Gray8
1189 | Self::Pal8
1190 | Self::Yuv420P
1191 | Self::Yuv422P
1192 | Self::Yuv444P
1193 | Self::Yuv411P
1194 | Self::Yuv440P
1195 | Self::YuvJ420P
1196 | Self::YuvJ422P
1197 | Self::YuvJ444P
1198 | Self::Yuva420P
1199 | Self::Yuva422P
1200 | Self::Yuva444P
1201 | Self::Gbrp8
1202 | Self::Gbrap8 => 1,
1203 // Semi-planar: one byte per luma sample on plane 0, an
1204 // interleaved two-sample pair per chroma position on
1205 // plane 1.
1206 Self::Nv12 | Self::Nv21 => {
1207 if plane == 0 {
1208 1
1209 } else {
1210 2
1211 }
1212 }
1213 // Little-endian 16-bit words (10/12/14/16-bit storage).
1214 Self::Gray10Le
1215 | Self::Gray12Le
1216 | Self::Gray16Le
1217 | Self::Yuv420P10Le
1218 | Self::Yuv422P10Le
1219 | Self::Yuv444P10Le
1220 | Self::Yuv420P12Le
1221 | Self::Yuv422P12Le
1222 | Self::Yuv444P12Le
1223 | Self::Yuv420P16Le
1224 | Self::Yuv422P16Le
1225 | Self::Yuv444P16Le
1226 | Self::Yuv440P10Le
1227 | Self::Yuv440P12Le
1228 | Self::Yuv440P16Le
1229 | Self::Yuva422P10Le
1230 | Self::Yuva422P12Le
1231 | Self::Yuva444P10Le
1232 | Self::Yuva444P12Le
1233 | Self::Yuva422P16Le
1234 | Self::Yuva444P16Le
1235 | Self::Yuva420P10Le
1236 | Self::Yuva420P12Le
1237 | Self::Yuva420P16Le
1238 | Self::Gbrp10Le
1239 | Self::Gbrap10Le
1240 | Self::Gbrp12Le
1241 | Self::Gbrap12Le
1242 | Self::Gbrp14Le
1243 | Self::Gbrap14Le
1244 | Self::Gbrp16Le
1245 | Self::Gbrap16Le => 2,
1246 // Packed multi-component: whole-pixel byte cost.
1247 Self::Ya8 => 2,
1248 Self::Rgb24 | Self::Bgr24 => 3,
1249 Self::Rgba
1250 | Self::Bgra
1251 | Self::Argb
1252 | Self::Abgr
1253 | Self::Cmyk
1254 | Self::CmykInverted
1255 | Self::Ya16Le => 4,
1256 Self::Rgb48Le => 6,
1257 Self::Rgba64Le => 8,
1258 // 32-bit float: one binary32 word per sample (packed
1259 // grayscale and the planar GBR(A) planes), or the
1260 // whole-pixel cost for packed multi-component float.
1261 Self::GrayF32Le | Self::GbrpF32Le | Self::GbrapF32Le => 4,
1262 Self::RgbF32Le => 12,
1263 Self::RgbaF32Le => 16,
1264 };
1265 pw.checked_mul(bytes_per_position)
1266 }
1267
1268 /// Tightly-packed byte size of plane `plane` for a `width` ×
1269 /// `height` picture:
1270 /// [`plane_row_bytes`](Self::plane_row_bytes) × the plane's row
1271 /// count from [`plane_dimensions`](Self::plane_dimensions).
1272 ///
1273 /// Returns `None` when `plane >= plane_count()` or the size
1274 /// overflows `usize`.
1275 pub fn plane_size_bytes(&self, plane: usize, width: u32, height: u32) -> Option<usize> {
1276 let (_, ph) = self.plane_dimensions(plane, width, height)?;
1277 self.plane_row_bytes(plane, width)?.checked_mul(ph as usize)
1278 }
1279
1280 /// Tightly-packed byte size of a whole `width` × `height` frame in
1281 /// this format — the sum of
1282 /// [`plane_size_bytes`](Self::plane_size_bytes) over every plane,
1283 /// with no stride padding or inter-plane alignment. Out-of-band
1284 /// side data (the `Pal8` palette table, significant-bits records)
1285 /// is not included.
1286 ///
1287 /// Returns `None` on `usize` overflow.
1288 ///
1289 /// ```
1290 /// use oxideav_core::PixelFormat;
1291 /// // 4:2:0 at 4×4: 16 luma + 4 + 4 chroma bytes.
1292 /// assert_eq!(PixelFormat::Yuv420P.frame_size_bytes(4, 4), Some(24));
1293 /// // 4:4:0 at 6×5: 30 luma + 2 × (6 × 3) chroma bytes.
1294 /// assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
1295 /// // Packed float RGBA: 16 bytes per pixel.
1296 /// assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
1297 /// ```
1298 pub fn frame_size_bytes(&self, width: u32, height: u32) -> Option<usize> {
1299 let mut total = 0usize;
1300 for plane in 0..self.plane_count() {
1301 total = total.checked_add(self.plane_size_bytes(plane, width, height)?)?;
1302 }
1303 Some(total)
1304 }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use super::*;
1310
1311 /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
1312 /// stability commitment — the integer value of each variant is part of
1313 /// the public ABI. Any reorder, renumber, or removal will fail this test
1314 /// and the change MUST be a major version bump (or a fresh variant
1315 /// appended at a new number, leaving the existing ones untouched).
1316 #[test]
1317 fn pixel_format_discriminants_pinned() {
1318 assert_eq!(PixelFormat::Yuv420P as u16, 0);
1319 assert_eq!(PixelFormat::Yuv422P as u16, 1);
1320 assert_eq!(PixelFormat::Yuv444P as u16, 2);
1321 assert_eq!(PixelFormat::Rgb24 as u16, 3);
1322 assert_eq!(PixelFormat::Rgba as u16, 4);
1323 assert_eq!(PixelFormat::Gray8 as u16, 5);
1324 assert_eq!(PixelFormat::Pal8 as u16, 6);
1325 assert_eq!(PixelFormat::Bgr24 as u16, 7);
1326 assert_eq!(PixelFormat::Bgra as u16, 8);
1327 assert_eq!(PixelFormat::Argb as u16, 9);
1328 assert_eq!(PixelFormat::Abgr as u16, 10);
1329 assert_eq!(PixelFormat::Rgb48Le as u16, 11);
1330 assert_eq!(PixelFormat::Rgba64Le as u16, 12);
1331 assert_eq!(PixelFormat::Gray16Le as u16, 13);
1332 assert_eq!(PixelFormat::Gray10Le as u16, 14);
1333 assert_eq!(PixelFormat::Gray12Le as u16, 15);
1334 assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
1335 assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
1336 assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
1337 assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
1338 assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
1339 assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
1340 assert_eq!(PixelFormat::YuvJ420P as u16, 22);
1341 assert_eq!(PixelFormat::YuvJ422P as u16, 23);
1342 assert_eq!(PixelFormat::YuvJ444P as u16, 24);
1343 assert_eq!(PixelFormat::Nv12 as u16, 25);
1344 assert_eq!(PixelFormat::Nv21 as u16, 26);
1345 assert_eq!(PixelFormat::Ya8 as u16, 27);
1346 assert_eq!(PixelFormat::Yuva420P as u16, 28);
1347 assert_eq!(PixelFormat::MonoBlack as u16, 29);
1348 assert_eq!(PixelFormat::MonoWhite as u16, 30);
1349 assert_eq!(PixelFormat::Yuyv422 as u16, 31);
1350 assert_eq!(PixelFormat::Uyvy422 as u16, 32);
1351 assert_eq!(PixelFormat::Cmyk as u16, 33);
1352 assert_eq!(PixelFormat::Yuv411P as u16, 34);
1353 assert_eq!(PixelFormat::Gbrp10Le as u16, 35);
1354 assert_eq!(PixelFormat::Gbrap10Le as u16, 36);
1355 assert_eq!(PixelFormat::Gbrp12Le as u16, 37);
1356 assert_eq!(PixelFormat::Gbrap12Le as u16, 38);
1357 assert_eq!(PixelFormat::Gbrp14Le as u16, 39);
1358 assert_eq!(PixelFormat::Gbrap14Le as u16, 40);
1359 assert_eq!(PixelFormat::Yuv420P16Le as u16, 41);
1360 assert_eq!(PixelFormat::Yuv422P16Le as u16, 42);
1361 assert_eq!(PixelFormat::Yuv444P16Le as u16, 43);
1362 assert_eq!(PixelFormat::Yuva422P as u16, 44);
1363 assert_eq!(PixelFormat::Yuva444P as u16, 45);
1364 assert_eq!(PixelFormat::Yuva422P10Le as u16, 46);
1365 assert_eq!(PixelFormat::Yuva422P12Le as u16, 47);
1366 assert_eq!(PixelFormat::Yuva444P10Le as u16, 48);
1367 assert_eq!(PixelFormat::Yuva444P12Le as u16, 49);
1368 assert_eq!(PixelFormat::Yuva422P16Le as u16, 50);
1369 assert_eq!(PixelFormat::Yuva444P16Le as u16, 51);
1370 assert_eq!(PixelFormat::Gbrp8 as u16, 52);
1371 assert_eq!(PixelFormat::Gbrp16Le as u16, 53);
1372 assert_eq!(PixelFormat::Gbrap16Le as u16, 54);
1373 assert_eq!(PixelFormat::Yuva420P10Le as u16, 55);
1374 assert_eq!(PixelFormat::Yuva420P12Le as u16, 56);
1375 assert_eq!(PixelFormat::Yuva420P16Le as u16, 57);
1376 assert_eq!(PixelFormat::Gbrap8 as u16, 58);
1377 assert_eq!(PixelFormat::Ya16Le as u16, 59);
1378 assert_eq!(PixelFormat::CmykInverted as u16, 60);
1379 assert_eq!(PixelFormat::Yuv440P as u16, 61);
1380 assert_eq!(PixelFormat::Yuv440P10Le as u16, 62);
1381 assert_eq!(PixelFormat::Yuv440P12Le as u16, 63);
1382 assert_eq!(PixelFormat::Yuv440P16Le as u16, 64);
1383 assert_eq!(PixelFormat::GrayF32Le as u16, 65);
1384 assert_eq!(PixelFormat::RgbF32Le as u16, 66);
1385 assert_eq!(PixelFormat::RgbaF32Le as u16, 67);
1386 assert_eq!(PixelFormat::GbrpF32Le as u16, 68);
1387 assert_eq!(PixelFormat::GbrapF32Le as u16, 69);
1388 }
1389
1390 #[test]
1391 fn sample_format_discriminants_pinned() {
1392 assert_eq!(SampleFormat::U8 as u8, 0);
1393 assert_eq!(SampleFormat::S8 as u8, 1);
1394 assert_eq!(SampleFormat::S16 as u8, 2);
1395 assert_eq!(SampleFormat::S24 as u8, 3);
1396 assert_eq!(SampleFormat::S32 as u8, 4);
1397 assert_eq!(SampleFormat::F32 as u8, 5);
1398 assert_eq!(SampleFormat::F64 as u8, 6);
1399 assert_eq!(SampleFormat::U8P as u8, 7);
1400 assert_eq!(SampleFormat::S16P as u8, 8);
1401 assert_eq!(SampleFormat::S32P as u8, 9);
1402 assert_eq!(SampleFormat::F32P as u8, 10);
1403 assert_eq!(SampleFormat::F64P as u8, 11);
1404 }
1405
1406 #[test]
1407 fn high_bit_yuv_planar_metadata() {
1408 // 10-bit reference variants are planar with three planes.
1409 assert!(PixelFormat::Yuv420P10Le.is_planar());
1410 assert!(PixelFormat::Yuv422P10Le.is_planar());
1411 assert!(PixelFormat::Yuv444P10Le.is_planar());
1412
1413 // 12-bit variants must follow the same shape.
1414 assert!(PixelFormat::Yuv420P12Le.is_planar());
1415 assert!(PixelFormat::Yuv422P12Le.is_planar());
1416 assert!(PixelFormat::Yuv444P12Le.is_planar());
1417
1418 assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
1419 assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
1420 assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
1421
1422 // 16-bit variants must follow the same shape.
1423 for fmt in [
1424 PixelFormat::Yuv420P16Le,
1425 PixelFormat::Yuv422P16Le,
1426 PixelFormat::Yuv444P16Le,
1427 ] {
1428 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1429 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1430 }
1431
1432 // None of the high-bit YUV variants carry alpha or palette.
1433 assert!(!PixelFormat::Yuv422P12Le.has_alpha());
1434 assert!(!PixelFormat::Yuv444P12Le.has_alpha());
1435 assert!(!PixelFormat::Yuv422P12Le.is_palette());
1436 assert!(!PixelFormat::Yuv444P12Le.is_palette());
1437 assert!(!PixelFormat::Yuv420P16Le.has_alpha());
1438 assert!(!PixelFormat::Yuv422P16Le.has_alpha());
1439 assert!(!PixelFormat::Yuv444P16Le.has_alpha());
1440 assert!(!PixelFormat::Yuv420P16Le.is_palette());
1441 assert!(!PixelFormat::Yuv422P16Le.is_palette());
1442 assert!(!PixelFormat::Yuv444P16Le.is_palette());
1443 }
1444
1445 #[test]
1446 fn channel_layout_round_trip_count_for_known_layouts() {
1447 // For every `n` that `from_count` maps to a named layout, the
1448 // resulting layout's `channel_count()` must equal `n` again.
1449 for n in 1..=8u16 {
1450 let layout = ChannelLayout::from_count(n);
1451 assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
1452 // None of these defaults should fall through to DiscreteN.
1453 assert!(
1454 !matches!(layout, ChannelLayout::DiscreteN(_)),
1455 "from_count({n}) unexpectedly produced DiscreteN"
1456 );
1457 }
1458 }
1459
1460 #[test]
1461 fn channel_layout_from_count_default_table() {
1462 // The exact mapping documented on `from_count` — pin it so
1463 // future refactors don't silently change the inferred layout.
1464 assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
1465 assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
1466 assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
1467 assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
1468 assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
1469 assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
1470 assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
1471 assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
1472 }
1473
1474 #[test]
1475 fn channel_layout_unknown_count_falls_through_to_discrete() {
1476 assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
1477 assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
1478 assert_eq!(
1479 ChannelLayout::from_count(64).channel_count(),
1480 64,
1481 "DiscreteN must report the count it was constructed with"
1482 );
1483 }
1484
1485 #[test]
1486 fn channel_layout_position_lookup() {
1487 assert_eq!(
1488 ChannelLayout::Stereo.position(0),
1489 Some(ChannelPosition::FrontLeft)
1490 );
1491 assert_eq!(
1492 ChannelLayout::Stereo.position(1),
1493 Some(ChannelPosition::FrontRight)
1494 );
1495 assert_eq!(ChannelLayout::Stereo.position(2), None);
1496
1497 // 5.1 canonical: L, R, C, LFE, Ls, Rs.
1498 let s51 = ChannelLayout::Surround51;
1499 assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
1500 assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
1501 assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
1502 assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
1503 assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
1504 assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
1505 assert_eq!(s51.position(6), None);
1506
1507 // DiscreteN never reveals a position.
1508 assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
1509 }
1510
1511 #[test]
1512 fn channel_layout_lfe_and_surround_predicates() {
1513 assert!(ChannelLayout::Surround51.has_lfe());
1514 assert!(ChannelLayout::Surround71.has_lfe());
1515 assert!(ChannelLayout::Stereo21.has_lfe());
1516 assert!(!ChannelLayout::Quad.has_lfe());
1517 assert!(!ChannelLayout::Surround50.has_lfe());
1518 assert!(!ChannelLayout::Stereo.has_lfe());
1519
1520 assert!(!ChannelLayout::Mono.is_surround());
1521 assert!(!ChannelLayout::Stereo.is_surround());
1522 // Downmix carriers are still 2ch / no-LFE → not "surround" by
1523 // the layout-shape definition; the surround info lives in the
1524 // sample matrix itself.
1525 assert!(!ChannelLayout::LoRo.is_surround());
1526 assert!(!ChannelLayout::LtRt.is_surround());
1527 assert!(ChannelLayout::Stereo21.is_surround());
1528 assert!(ChannelLayout::Surround51.is_surround());
1529 assert!(ChannelLayout::Surround71.is_surround());
1530 }
1531
1532 #[test]
1533 fn channel_layout_display_and_fromstr_round_trip() {
1534 use std::str::FromStr;
1535 let cases = [
1536 ChannelLayout::Mono,
1537 ChannelLayout::Stereo,
1538 ChannelLayout::Stereo21,
1539 ChannelLayout::Surround30,
1540 ChannelLayout::Quad,
1541 ChannelLayout::Surround40,
1542 ChannelLayout::Surround41,
1543 ChannelLayout::Surround50,
1544 ChannelLayout::Surround51,
1545 ChannelLayout::Surround60,
1546 ChannelLayout::Surround61,
1547 ChannelLayout::Surround70,
1548 ChannelLayout::Surround71,
1549 ChannelLayout::LoRo,
1550 ChannelLayout::LtRt,
1551 ChannelLayout::DiscreteN(13),
1552 ];
1553 for layout in cases {
1554 let s = layout.to_string();
1555 let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
1556 assert_eq!(parsed, layout, "round-trip failed via {s:?}");
1557 }
1558 }
1559
1560 #[test]
1561 fn channel_layout_fromstr_accepts_aliases_and_case() {
1562 use std::str::FromStr;
1563 assert_eq!(
1564 ChannelLayout::from_str("STEREO").unwrap(),
1565 ChannelLayout::Stereo
1566 );
1567 assert_eq!(
1568 ChannelLayout::from_str("2.0").unwrap(),
1569 ChannelLayout::Stereo
1570 );
1571 assert_eq!(
1572 ChannelLayout::from_str("5.1").unwrap(),
1573 ChannelLayout::Surround51
1574 );
1575 assert_eq!(
1576 ChannelLayout::from_str("Lo/Ro").unwrap(),
1577 ChannelLayout::LoRo
1578 );
1579 assert_eq!(
1580 ChannelLayout::from_str("lt/rt").unwrap(),
1581 ChannelLayout::LtRt
1582 );
1583 assert!(ChannelLayout::from_str("absurd_layout").is_err());
1584 }
1585
1586 #[test]
1587 fn channel_layout_positions_owned_matches_static_slice() {
1588 for layout in [
1589 ChannelLayout::Mono,
1590 ChannelLayout::Surround51,
1591 ChannelLayout::Surround71,
1592 ] {
1593 assert_eq!(layout.positions_owned(), layout.positions());
1594 }
1595 // DiscreteN returns an empty owned vec — positions are unknown.
1596 assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
1597 }
1598
1599 #[test]
1600 fn sample_format_plane_count_interleaved_is_one() {
1601 // Interleaved formats always pack into a single plane, regardless
1602 // of channel count.
1603 for ch in [1u16, 2, 6, 8, 64, 0] {
1604 assert_eq!(SampleFormat::S16.plane_count(ch), 1);
1605 assert_eq!(SampleFormat::F32.plane_count(ch), 1);
1606 assert_eq!(SampleFormat::U8.plane_count(ch), 1);
1607 assert_eq!(SampleFormat::S24.plane_count(ch), 1);
1608 }
1609 }
1610
1611 #[test]
1612 fn sample_format_plane_count_planar_matches_channels() {
1613 // Planar formats use one plane per channel.
1614 assert_eq!(SampleFormat::S16P.plane_count(1), 1);
1615 assert_eq!(SampleFormat::S16P.plane_count(2), 2);
1616 assert_eq!(SampleFormat::F32P.plane_count(6), 6);
1617 assert_eq!(SampleFormat::F64P.plane_count(8), 8);
1618
1619 // Edge case: zero channels in a planar format yields zero planes.
1620 assert_eq!(SampleFormat::S32P.plane_count(0), 0);
1621 }
1622
1623 #[test]
1624 fn high_bit_yuv_bits_per_pixel_approx() {
1625 // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
1626 // packed-bits estimator (the approximation reports samples-per-pixel
1627 // density, not the 16-bit storage width).
1628 assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
1629 assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
1630 assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
1631 assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
1632 assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
1633
1634 // 16-bit: packed bits == storage bits (every bit of the 16-bit
1635 // word is significant), so the estimator lands on the same
1636 // numbers as the 10/12-bit siblings.
1637 assert_eq!(PixelFormat::Yuv420P16Le.bits_per_pixel_approx(), 24);
1638 assert_eq!(PixelFormat::Yuv422P16Le.bits_per_pixel_approx(), 32);
1639 assert_eq!(PixelFormat::Yuv444P16Le.bits_per_pixel_approx(), 48);
1640 }
1641
1642 #[test]
1643 fn yuva_planar_metadata() {
1644 // All three alpha-carrying planar YUV samplings share one shape:
1645 // planar, 4 planes (Y, U, V, full-resolution A), alpha set, not
1646 // a palette format.
1647 for fmt in [
1648 PixelFormat::Yuva420P,
1649 PixelFormat::Yuva422P,
1650 PixelFormat::Yuva444P,
1651 ] {
1652 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1653 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1654 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1655 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1656 }
1657
1658 // Packed-bits estimator: the alpha plane adds a full 8 bits per
1659 // pixel on top of the alpha-less sampling's density.
1660 assert_eq!(
1661 PixelFormat::Yuva420P.bits_per_pixel_approx(),
1662 PixelFormat::Yuv420P.bits_per_pixel_approx() + 8
1663 );
1664 assert_eq!(
1665 PixelFormat::Yuva422P.bits_per_pixel_approx(),
1666 PixelFormat::Yuv422P.bits_per_pixel_approx() + 8
1667 );
1668 assert_eq!(
1669 PixelFormat::Yuva444P.bits_per_pixel_approx(),
1670 PixelFormat::Yuv444P.bits_per_pixel_approx() + 8
1671 );
1672 assert_eq!(PixelFormat::Yuva422P.bits_per_pixel_approx(), 24);
1673 assert_eq!(PixelFormat::Yuva444P.bits_per_pixel_approx(), 32);
1674 }
1675
1676 #[test]
1677 fn deep_yuva_planar_metadata() {
1678 // All six deep alpha-carrying variants share one shape: planar,
1679 // 4 planes (Y, U, V, full-resolution A), alpha set, no palette.
1680 for fmt in [
1681 PixelFormat::Yuva422P10Le,
1682 PixelFormat::Yuva422P12Le,
1683 PixelFormat::Yuva444P10Le,
1684 PixelFormat::Yuva444P12Le,
1685 PixelFormat::Yuva422P16Le,
1686 PixelFormat::Yuva444P16Le,
1687 ] {
1688 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1689 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1690 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1691 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1692 }
1693 }
1694
1695 #[test]
1696 fn deep_yuva_bits_per_pixel_approx() {
1697 // Estimator reports 16-bit-word storage cost, matching the
1698 // alpha-less deep YUV trio: the full-resolution alpha word adds
1699 // 16 on top of the alpha-less sampling's number.
1700 for fmt in [
1701 PixelFormat::Yuva422P10Le,
1702 PixelFormat::Yuva422P12Le,
1703 PixelFormat::Yuva422P16Le,
1704 ] {
1705 assert_eq!(fmt.bits_per_pixel_approx(), 48, "{fmt:?}");
1706 }
1707 for fmt in [
1708 PixelFormat::Yuva444P10Le,
1709 PixelFormat::Yuva444P12Le,
1710 PixelFormat::Yuva444P16Le,
1711 ] {
1712 assert_eq!(fmt.bits_per_pixel_approx(), 64, "{fmt:?}");
1713 }
1714 assert_eq!(
1715 PixelFormat::Yuva422P16Le.bits_per_pixel_approx(),
1716 PixelFormat::Yuv422P16Le.bits_per_pixel_approx() + 16
1717 );
1718 assert_eq!(
1719 PixelFormat::Yuva444P16Le.bits_per_pixel_approx(),
1720 PixelFormat::Yuv444P16Le.bits_per_pixel_approx() + 16
1721 );
1722 assert_eq!(
1723 PixelFormat::Yuva422P10Le.bits_per_pixel_approx(),
1724 PixelFormat::Yuv422P10Le.bits_per_pixel_approx() + 16
1725 );
1726 assert_eq!(
1727 PixelFormat::Yuva444P12Le.bits_per_pixel_approx(),
1728 PixelFormat::Yuv444P12Le.bits_per_pixel_approx() + 16
1729 );
1730 }
1731
1732 #[test]
1733 fn high_bit_gbr_planar_metadata() {
1734 // All six new variants are planar with the right plane count.
1735 for fmt in [
1736 PixelFormat::Gbrp10Le,
1737 PixelFormat::Gbrp12Le,
1738 PixelFormat::Gbrp14Le,
1739 ] {
1740 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1741 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1742 assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1743 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1744 }
1745 for fmt in [
1746 PixelFormat::Gbrap10Le,
1747 PixelFormat::Gbrap12Le,
1748 PixelFormat::Gbrap14Le,
1749 ] {
1750 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1751 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1752 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1753 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1754 }
1755 }
1756
1757 #[test]
1758 fn high_bit_gbr_bits_per_pixel_approx() {
1759 // Packed bits-per-pixel = samples × bits (consistent with how
1760 // the 10/12-bit YUV variants are reported above).
1761 assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30);
1762 assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40);
1763 assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36);
1764 assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48);
1765 assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42);
1766 assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56);
1767 }
1768
1769 #[test]
1770 fn high_bit_gbr_constructible_and_distinct() {
1771 // Round-trip the discriminant through `as u16` and back via the
1772 // pinning test's reverse mapping — every variant must be unique.
1773 let all = [
1774 PixelFormat::Gbrp10Le,
1775 PixelFormat::Gbrap10Le,
1776 PixelFormat::Gbrp12Le,
1777 PixelFormat::Gbrap12Le,
1778 PixelFormat::Gbrp14Le,
1779 PixelFormat::Gbrap14Le,
1780 PixelFormat::Gbrp8,
1781 PixelFormat::Gbrap8,
1782 PixelFormat::Gbrp16Le,
1783 PixelFormat::Gbrap16Le,
1784 ];
1785 let mut seen = std::collections::HashSet::new();
1786 for fmt in all {
1787 assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
1788 }
1789 }
1790
1791 #[test]
1792 fn gbr_depth_ladder_ends_metadata() {
1793 // Gbrp8 and the 16-bit pair share the family shape: planar,
1794 // G/B/R plane order (3 planes), alpha only on Gbrap16Le, never
1795 // palette.
1796 for fmt in [PixelFormat::Gbrp8, PixelFormat::Gbrp16Le] {
1797 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1798 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1799 assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1800 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1801 }
1802 assert!(PixelFormat::Gbrap16Le.is_planar());
1803 assert_eq!(PixelFormat::Gbrap16Le.plane_count(), 4);
1804 assert!(PixelFormat::Gbrap16Le.has_alpha());
1805 assert!(!PixelFormat::Gbrap16Le.is_palette());
1806 }
1807
1808 #[test]
1809 fn gbr_depth_ladder_ends_bits_per_pixel_approx() {
1810 // Gbrp8 matches the packed 8-bit RGB density (planar layout
1811 // doesn't change bits-per-pixel), and the 16-bit pair matches
1812 // the packed 16-bit RGB(A) densities — for 16-bit words packed
1813 // bits equal storage bits.
1814 assert_eq!(
1815 PixelFormat::Gbrp8.bits_per_pixel_approx(),
1816 PixelFormat::Rgb24.bits_per_pixel_approx()
1817 );
1818 assert_eq!(
1819 PixelFormat::Gbrp16Le.bits_per_pixel_approx(),
1820 PixelFormat::Rgb48Le.bits_per_pixel_approx()
1821 );
1822 assert_eq!(
1823 PixelFormat::Gbrap16Le.bits_per_pixel_approx(),
1824 PixelFormat::Rgba64Le.bits_per_pixel_approx()
1825 );
1826 assert_eq!(PixelFormat::Gbrp8.bits_per_pixel_approx(), 24);
1827 assert_eq!(PixelFormat::Gbrp16Le.bits_per_pixel_approx(), 48);
1828 assert_eq!(PixelFormat::Gbrap16Le.bits_per_pixel_approx(), 64);
1829 }
1830
1831 #[test]
1832 fn gbrap8_metadata() {
1833 // Gbrap8 completes the GBR(A) family: every depth on the
1834 // 8/10/12/14/16 ladder now has both an alpha-less and an
1835 // alpha-carrying variant. Shape matches the rest of the
1836 // alpha-carrying family: planar, 4 planes (G, B, R,
1837 // full-resolution A), alpha set, never palette.
1838 let fmt = PixelFormat::Gbrap8;
1839 assert!(fmt.is_planar());
1840 assert_eq!(fmt.plane_count(), 4);
1841 assert!(fmt.has_alpha());
1842 assert!(!fmt.is_palette());
1843 }
1844
1845 #[test]
1846 fn gbrap8_bits_per_pixel_approx() {
1847 // Four bytes per pixel: the packed Rgba density (planar layout
1848 // doesn't change bits-per-pixel), i.e. the alpha plane adds a
1849 // full 8 bits on top of Gbrp8.
1850 assert_eq!(PixelFormat::Gbrap8.bits_per_pixel_approx(), 32);
1851 assert_eq!(
1852 PixelFormat::Gbrap8.bits_per_pixel_approx(),
1853 PixelFormat::Rgba.bits_per_pixel_approx()
1854 );
1855 assert_eq!(
1856 PixelFormat::Gbrap8.bits_per_pixel_approx(),
1857 PixelFormat::Gbrp8.bits_per_pixel_approx() + 8
1858 );
1859 }
1860
1861 #[test]
1862 fn gbr_family_alpha_ladder_complete() {
1863 // Every GBR depth has an alpha companion with exactly one more
1864 // plane and the same planarity — the asymmetry Gbrap8 closed.
1865 let pairs = [
1866 (PixelFormat::Gbrp8, PixelFormat::Gbrap8),
1867 (PixelFormat::Gbrp10Le, PixelFormat::Gbrap10Le),
1868 (PixelFormat::Gbrp12Le, PixelFormat::Gbrap12Le),
1869 (PixelFormat::Gbrp14Le, PixelFormat::Gbrap14Le),
1870 (PixelFormat::Gbrp16Le, PixelFormat::Gbrap16Le),
1871 ];
1872 for (gbr, gbra) in pairs {
1873 assert!(gbr.is_planar() && gbra.is_planar());
1874 assert_eq!(gbr.plane_count(), 3, "{gbr:?}");
1875 assert_eq!(gbra.plane_count(), 4, "{gbra:?}");
1876 assert!(!gbr.has_alpha(), "{gbr:?}");
1877 assert!(gbra.has_alpha(), "{gbra:?}");
1878 }
1879 }
1880
1881 #[test]
1882 fn ya16le_metadata() {
1883 // Same packed shape as Ya8 (interleaved Y, A in one plane),
1884 // widened to 16-bit LE words: not planar, single plane, alpha
1885 // set, never palette. Density is exactly double Ya8's and
1886 // matches half of Rgba64Le (two components instead of four).
1887 let fmt = PixelFormat::Ya16Le;
1888 assert!(!fmt.is_planar());
1889 assert_eq!(fmt.plane_count(), 1);
1890 assert!(fmt.has_alpha());
1891 assert!(!fmt.is_palette());
1892 assert_eq!(fmt.bits_per_pixel_approx(), 32);
1893 assert_eq!(
1894 fmt.bits_per_pixel_approx(),
1895 PixelFormat::Ya8.bits_per_pixel_approx() * 2
1896 );
1897 assert_eq!(
1898 fmt.bits_per_pixel_approx(),
1899 PixelFormat::Rgba64Le.bits_per_pixel_approx() / 2
1900 );
1901 // The alpha word adds a full 16 bits on top of Gray16Le.
1902 assert_eq!(
1903 fmt.bits_per_pixel_approx(),
1904 PixelFormat::Gray16Le.bits_per_pixel_approx() + 16
1905 );
1906 }
1907
1908 #[test]
1909 fn cmyk_inverted_metadata() {
1910 // The inverted-ink convention changes sample semantics, not
1911 // layout: CmykInverted must be metadata-identical to Cmyk on
1912 // every shape predicate.
1913 let (reg, inv) = (PixelFormat::Cmyk, PixelFormat::CmykInverted);
1914 for fmt in [reg, inv] {
1915 assert!(!fmt.is_planar(), "{fmt:?}");
1916 assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
1917 assert!(!fmt.has_alpha(), "{fmt:?}");
1918 assert!(!fmt.is_palette(), "{fmt:?}");
1919 }
1920 assert_eq!(reg.bits_per_pixel_approx(), inv.bits_per_pixel_approx());
1921 assert_eq!(inv.bits_per_pixel_approx(), 32);
1922 // They remain distinct formats on the wire-stable axis.
1923 assert_ne!(reg as u16, inv as u16);
1924 }
1925
1926 #[test]
1927 fn deep_yuva420_planar_metadata() {
1928 // The 4:2:0 completions share the deep-Yuva shape: planar, 4
1929 // planes (Y, U, V, full-resolution A), alpha set, no palette.
1930 for fmt in [
1931 PixelFormat::Yuva420P10Le,
1932 PixelFormat::Yuva420P12Le,
1933 PixelFormat::Yuva420P16Le,
1934 ] {
1935 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1936 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1937 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1938 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1939 }
1940 }
1941
1942 #[test]
1943 fn deep_yuva420_bits_per_pixel_approx() {
1944 // Same estimator convention as the 4:2:2/4:4:4 deep Yuva arms:
1945 // 16-bit-word storage cost, with the full-resolution alpha word
1946 // adding 16 on top of the alpha-less sampling's number.
1947 for fmt in [
1948 PixelFormat::Yuva420P10Le,
1949 PixelFormat::Yuva420P12Le,
1950 PixelFormat::Yuva420P16Le,
1951 ] {
1952 assert_eq!(fmt.bits_per_pixel_approx(), 40, "{fmt:?}");
1953 }
1954 assert_eq!(
1955 PixelFormat::Yuva420P10Le.bits_per_pixel_approx(),
1956 PixelFormat::Yuv420P10Le.bits_per_pixel_approx() + 16
1957 );
1958 assert_eq!(
1959 PixelFormat::Yuva420P12Le.bits_per_pixel_approx(),
1960 PixelFormat::Yuv420P12Le.bits_per_pixel_approx() + 16
1961 );
1962 assert_eq!(
1963 PixelFormat::Yuva420P16Le.bits_per_pixel_approx(),
1964 PixelFormat::Yuv420P16Le.bits_per_pixel_approx() + 16
1965 );
1966 }
1967
1968 /// Every `PixelFormat` variant, in discriminant order. Extend this
1969 /// list whenever a variant is appended — the consistency tests
1970 /// below sweep it.
1971 const ALL_PIXEL_FORMATS: [PixelFormat; 70] = [
1972 PixelFormat::Yuv420P,
1973 PixelFormat::Yuv422P,
1974 PixelFormat::Yuv444P,
1975 PixelFormat::Rgb24,
1976 PixelFormat::Rgba,
1977 PixelFormat::Gray8,
1978 PixelFormat::Pal8,
1979 PixelFormat::Bgr24,
1980 PixelFormat::Bgra,
1981 PixelFormat::Argb,
1982 PixelFormat::Abgr,
1983 PixelFormat::Rgb48Le,
1984 PixelFormat::Rgba64Le,
1985 PixelFormat::Gray16Le,
1986 PixelFormat::Gray10Le,
1987 PixelFormat::Gray12Le,
1988 PixelFormat::Yuv420P10Le,
1989 PixelFormat::Yuv422P10Le,
1990 PixelFormat::Yuv444P10Le,
1991 PixelFormat::Yuv420P12Le,
1992 PixelFormat::Yuv422P12Le,
1993 PixelFormat::Yuv444P12Le,
1994 PixelFormat::YuvJ420P,
1995 PixelFormat::YuvJ422P,
1996 PixelFormat::YuvJ444P,
1997 PixelFormat::Nv12,
1998 PixelFormat::Nv21,
1999 PixelFormat::Ya8,
2000 PixelFormat::Yuva420P,
2001 PixelFormat::MonoBlack,
2002 PixelFormat::MonoWhite,
2003 PixelFormat::Yuyv422,
2004 PixelFormat::Uyvy422,
2005 PixelFormat::Cmyk,
2006 PixelFormat::Yuv411P,
2007 PixelFormat::Gbrp10Le,
2008 PixelFormat::Gbrap10Le,
2009 PixelFormat::Gbrp12Le,
2010 PixelFormat::Gbrap12Le,
2011 PixelFormat::Gbrp14Le,
2012 PixelFormat::Gbrap14Le,
2013 PixelFormat::Yuv420P16Le,
2014 PixelFormat::Yuv422P16Le,
2015 PixelFormat::Yuv444P16Le,
2016 PixelFormat::Yuva422P,
2017 PixelFormat::Yuva444P,
2018 PixelFormat::Yuva422P10Le,
2019 PixelFormat::Yuva422P12Le,
2020 PixelFormat::Yuva444P10Le,
2021 PixelFormat::Yuva444P12Le,
2022 PixelFormat::Yuva422P16Le,
2023 PixelFormat::Yuva444P16Le,
2024 PixelFormat::Gbrp8,
2025 PixelFormat::Gbrp16Le,
2026 PixelFormat::Gbrap16Le,
2027 PixelFormat::Yuva420P10Le,
2028 PixelFormat::Yuva420P12Le,
2029 PixelFormat::Yuva420P16Le,
2030 PixelFormat::Gbrap8,
2031 PixelFormat::Ya16Le,
2032 PixelFormat::CmykInverted,
2033 PixelFormat::Yuv440P,
2034 PixelFormat::Yuv440P10Le,
2035 PixelFormat::Yuv440P12Le,
2036 PixelFormat::Yuv440P16Le,
2037 PixelFormat::GrayF32Le,
2038 PixelFormat::RgbF32Le,
2039 PixelFormat::RgbaF32Le,
2040 PixelFormat::GbrpF32Le,
2041 PixelFormat::GbrapF32Le,
2042 ];
2043
2044 #[test]
2045 fn all_pixel_formats_list_is_complete_and_distinct() {
2046 // The list is discriminant-ordered and dense: 0..70 with no
2047 // gaps and no duplicates. A newly appended variant that isn't
2048 // added to the list will break the length or density check.
2049 let mut seen = std::collections::HashSet::new();
2050 for fmt in ALL_PIXEL_FORMATS {
2051 assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
2052 }
2053 for d in 0..ALL_PIXEL_FORMATS.len() as u16 {
2054 assert!(seen.contains(&d), "discriminant {d} missing from list");
2055 }
2056 }
2057
2058 #[test]
2059 fn yuv440_family_metadata() {
2060 // The whole 4:4:0 ladder shares one shape: planar, 3 planes,
2061 // no alpha, no palette, full-width half-height chroma.
2062 for fmt in [
2063 PixelFormat::Yuv440P,
2064 PixelFormat::Yuv440P10Le,
2065 PixelFormat::Yuv440P12Le,
2066 PixelFormat::Yuv440P16Le,
2067 ] {
2068 assert!(fmt.is_planar(), "{fmt:?} must be planar");
2069 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
2070 assert!(!fmt.has_alpha(), "{fmt:?} must not carry alpha");
2071 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
2072 assert!(!fmt.is_float(), "{fmt:?} must not be float");
2073 assert_eq!(
2074 fmt.chroma_subsampling(),
2075 Some((0, 1)),
2076 "{fmt:?} must be full-width, half-height chroma"
2077 );
2078 }
2079 }
2080
2081 #[test]
2082 fn yuv440_bits_per_pixel_approx() {
2083 // 4:4:0 packs the same samples-per-pixel as 4:2:2 at every
2084 // depth (2 samples/pixel), so the estimator numbers coincide.
2085 assert_eq!(
2086 PixelFormat::Yuv440P.bits_per_pixel_approx(),
2087 PixelFormat::Yuv422P.bits_per_pixel_approx()
2088 );
2089 assert_eq!(PixelFormat::Yuv440P.bits_per_pixel_approx(), 16);
2090 for (f440, f422) in [
2091 (PixelFormat::Yuv440P10Le, PixelFormat::Yuv422P10Le),
2092 (PixelFormat::Yuv440P12Le, PixelFormat::Yuv422P12Le),
2093 (PixelFormat::Yuv440P16Le, PixelFormat::Yuv422P16Le),
2094 ] {
2095 assert_eq!(
2096 f440.bits_per_pixel_approx(),
2097 f422.bits_per_pixel_approx(),
2098 "{f440:?}"
2099 );
2100 assert_eq!(f440.bits_per_pixel_approx(), 32, "{f440:?}");
2101 }
2102 }
2103
2104 #[test]
2105 fn yuv440_plane_geometry() {
2106 // Even sizes: chroma keeps the width, halves the height.
2107 assert_eq!(
2108 PixelFormat::Yuv440P.plane_dimensions(0, 640, 480),
2109 Some((640, 480))
2110 );
2111 assert_eq!(
2112 PixelFormat::Yuv440P.plane_dimensions(1, 640, 480),
2113 Some((640, 240))
2114 );
2115 assert_eq!(
2116 PixelFormat::Yuv440P.plane_dimensions(2, 640, 480),
2117 Some((640, 240))
2118 );
2119 // Odd height rounds up; odd width is untouched (ssx = 0).
2120 for fmt in [
2121 PixelFormat::Yuv440P,
2122 PixelFormat::Yuv440P10Le,
2123 PixelFormat::Yuv440P12Le,
2124 PixelFormat::Yuv440P16Le,
2125 ] {
2126 assert_eq!(fmt.plane_dimensions(0, 7, 5), Some((7, 5)), "{fmt:?}");
2127 assert_eq!(fmt.plane_dimensions(1, 7, 5), Some((7, 3)), "{fmt:?}");
2128 assert_eq!(fmt.plane_dimensions(2, 7, 5), Some((7, 3)), "{fmt:?}");
2129 assert_eq!(fmt.plane_dimensions(3, 7, 5), None, "{fmt:?}");
2130 }
2131 // Degenerate 1-row picture: the chroma plane still has a row.
2132 assert_eq!(PixelFormat::Yuv440P.plane_dimensions(1, 3, 1), Some((3, 1)));
2133 }
2134
2135 #[test]
2136 fn yuv440_sizing_round_trips() {
2137 // 6×5 8-bit: luma 6×5 = 30, each chroma 6×ceil(5/2) = 18.
2138 assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(0, 6, 5), Some(30));
2139 assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(1, 6, 5), Some(18));
2140 assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(2, 6, 5), Some(18));
2141 assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
2142 // 7×5: 35 + 21 + 21.
2143 assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(7, 5), Some(77));
2144 // Deep variants store 16-bit words: exactly double at every
2145 // depth (row bytes = width × 2 regardless of valid bits).
2146 for fmt in [
2147 PixelFormat::Yuv440P10Le,
2148 PixelFormat::Yuv440P12Le,
2149 PixelFormat::Yuv440P16Le,
2150 ] {
2151 assert_eq!(fmt.plane_row_bytes(0, 7), Some(14), "{fmt:?}");
2152 assert_eq!(fmt.plane_row_bytes(1, 7), Some(14), "{fmt:?}");
2153 assert_eq!(fmt.frame_size_bytes(7, 5), Some(154), "{fmt:?}");
2154 }
2155 }
2156
2157 #[test]
2158 fn float_family_metadata() {
2159 // Packed trio: single plane, not planar.
2160 for fmt in [
2161 PixelFormat::GrayF32Le,
2162 PixelFormat::RgbF32Le,
2163 PixelFormat::RgbaF32Le,
2164 ] {
2165 assert!(!fmt.is_planar(), "{fmt:?}");
2166 assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2167 }
2168 // Planar pair: GBR(A) shape.
2169 assert!(PixelFormat::GbrpF32Le.is_planar());
2170 assert_eq!(PixelFormat::GbrpF32Le.plane_count(), 3);
2171 assert!(PixelFormat::GbrapF32Le.is_planar());
2172 assert_eq!(PixelFormat::GbrapF32Le.plane_count(), 4);
2173 // Alpha only on the RGBA/GBRA members.
2174 assert!(!PixelFormat::GrayF32Le.has_alpha());
2175 assert!(!PixelFormat::RgbF32Le.has_alpha());
2176 assert!(PixelFormat::RgbaF32Le.has_alpha());
2177 assert!(!PixelFormat::GbrpF32Le.has_alpha());
2178 assert!(PixelFormat::GbrapF32Le.has_alpha());
2179 // The whole family is float, non-palette, and has no chroma
2180 // grid.
2181 for fmt in [
2182 PixelFormat::GrayF32Le,
2183 PixelFormat::RgbF32Le,
2184 PixelFormat::RgbaF32Le,
2185 PixelFormat::GbrpF32Le,
2186 PixelFormat::GbrapF32Le,
2187 ] {
2188 assert!(fmt.is_float(), "{fmt:?} must be float");
2189 assert!(!fmt.is_palette(), "{fmt:?}");
2190 assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2191 }
2192 }
2193
2194 #[test]
2195 fn is_float_false_for_integer_formats() {
2196 for fmt in ALL_PIXEL_FORMATS {
2197 let expect = matches!(
2198 fmt,
2199 PixelFormat::GrayF32Le
2200 | PixelFormat::RgbF32Le
2201 | PixelFormat::RgbaF32Le
2202 | PixelFormat::GbrpF32Le
2203 | PixelFormat::GbrapF32Le
2204 );
2205 assert_eq!(fmt.is_float(), expect, "{fmt:?}");
2206 }
2207 }
2208
2209 #[test]
2210 fn float_family_bits_per_pixel_and_sizing() {
2211 // Packed bits equal storage bits: every sample is a full
2212 // binary32 word.
2213 assert_eq!(PixelFormat::GrayF32Le.bits_per_pixel_approx(), 32);
2214 assert_eq!(PixelFormat::RgbF32Le.bits_per_pixel_approx(), 96);
2215 assert_eq!(PixelFormat::RgbaF32Le.bits_per_pixel_approx(), 128);
2216 assert_eq!(PixelFormat::GbrpF32Le.bits_per_pixel_approx(), 96);
2217 assert_eq!(PixelFormat::GbrapF32Le.bits_per_pixel_approx(), 128);
2218 // Packed row/frame sizes.
2219 assert_eq!(PixelFormat::GrayF32Le.plane_row_bytes(0, 3), Some(12));
2220 assert_eq!(PixelFormat::GrayF32Le.frame_size_bytes(5, 3), Some(60));
2221 assert_eq!(PixelFormat::RgbF32Le.plane_row_bytes(0, 7), Some(84));
2222 assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
2223 // Planar float: 4 bytes per sample on every plane; the packed
2224 // and planar layouts of the same component set cost the same.
2225 assert_eq!(PixelFormat::GbrpF32Le.plane_row_bytes(1, 7), Some(28));
2226 assert_eq!(
2227 PixelFormat::GbrpF32Le.frame_size_bytes(7, 5),
2228 PixelFormat::RgbF32Le.frame_size_bytes(7, 5)
2229 );
2230 assert_eq!(
2231 PixelFormat::GbrapF32Le.frame_size_bytes(7, 5),
2232 PixelFormat::RgbaF32Le.frame_size_bytes(7, 5)
2233 );
2234 // All planes of planar float GBR(A) are full resolution.
2235 for plane in 0..4 {
2236 assert_eq!(
2237 PixelFormat::GbrapF32Le.plane_dimensions(plane, 7, 5),
2238 Some((7, 5))
2239 );
2240 }
2241 }
2242
2243 #[test]
2244 fn chroma_subsampling_table() {
2245 use PixelFormat::*;
2246 // One representative per sampling class plus the full new
2247 // family; the wildcard class returns None.
2248 assert_eq!(Yuv420P.chroma_subsampling(), Some((1, 1)));
2249 assert_eq!(Nv12.chroma_subsampling(), Some((1, 1)));
2250 assert_eq!(Yuva420P16Le.chroma_subsampling(), Some((1, 1)));
2251 assert_eq!(Yuv422P.chroma_subsampling(), Some((1, 0)));
2252 assert_eq!(Yuyv422.chroma_subsampling(), Some((1, 0)));
2253 assert_eq!(Uyvy422.chroma_subsampling(), Some((1, 0)));
2254 assert_eq!(Yuv444P.chroma_subsampling(), Some((0, 0)));
2255 assert_eq!(Yuva444P12Le.chroma_subsampling(), Some((0, 0)));
2256 assert_eq!(Yuv411P.chroma_subsampling(), Some((2, 0)));
2257 assert_eq!(Yuv440P.chroma_subsampling(), Some((0, 1)));
2258 assert_eq!(Yuv440P16Le.chroma_subsampling(), Some((0, 1)));
2259 for fmt in [
2260 Gray8,
2261 Gray16Le,
2262 Ya8,
2263 Ya16Le,
2264 Pal8,
2265 MonoBlack,
2266 MonoWhite,
2267 Rgb24,
2268 Rgba,
2269 Rgb48Le,
2270 Rgba64Le,
2271 Cmyk,
2272 CmykInverted,
2273 Gbrp8,
2274 Gbrap16Le,
2275 GrayF32Le,
2276 RgbaF32Le,
2277 GbrapF32Le,
2278 ] {
2279 assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2280 }
2281 }
2282
2283 #[test]
2284 fn plane_dimensions_odd_sizes_across_samplings() {
2285 use PixelFormat::*;
2286 // 4:2:0 — both axes ceil-halved.
2287 assert_eq!(Yuv420P.plane_dimensions(1, 7, 5), Some((4, 3)));
2288 // 4:2:2 — width ceil-halved, height untouched.
2289 assert_eq!(Yuv422P.plane_dimensions(2, 7, 5), Some((4, 5)));
2290 // 4:1:1 — width ceil-quartered.
2291 assert_eq!(Yuv411P.plane_dimensions(1, 7, 5), Some((2, 5)));
2292 assert_eq!(Yuv411P.plane_dimensions(1, 9, 5), Some((3, 5)));
2293 // 4:4:4 — untouched.
2294 assert_eq!(Yuv444P.plane_dimensions(1, 7, 5), Some((7, 5)));
2295 // Semi-planar chroma positions.
2296 assert_eq!(Nv12.plane_dimensions(1, 7, 5), Some((4, 3)));
2297 assert_eq!(Nv21.plane_dimensions(1, 7, 5), Some((4, 3)));
2298 // Alpha planes are never subsampled.
2299 assert_eq!(Yuva420P.plane_dimensions(3, 7, 5), Some((7, 5)));
2300 assert_eq!(Yuva422P16Le.plane_dimensions(3, 7, 5), Some((7, 5)));
2301 // Planar RGB planes are never subsampled.
2302 for plane in 0..3 {
2303 assert_eq!(Gbrp12Le.plane_dimensions(plane, 7, 5), Some((7, 5)));
2304 }
2305 // Out-of-range planes.
2306 assert_eq!(Rgb24.plane_dimensions(1, 8, 8), None);
2307 assert_eq!(Yuv420P.plane_dimensions(3, 8, 8), None);
2308 assert_eq!(Yuva420P.plane_dimensions(4, 8, 8), None);
2309 // Zero-sized pictures collapse every plane to zero.
2310 assert_eq!(Yuv440P.plane_dimensions(1, 0, 0), Some((0, 0)));
2311 }
2312
2313 #[test]
2314 fn plane_row_bytes_conventions() {
2315 use PixelFormat::*;
2316 // Bit-packed mono: ceil(width / 8) with a ragged tail byte.
2317 assert_eq!(MonoBlack.plane_row_bytes(0, 13), Some(2));
2318 assert_eq!(MonoWhite.plane_row_bytes(0, 16), Some(2));
2319 assert_eq!(MonoBlack.plane_row_bytes(0, 17), Some(3));
2320 // Packed 4:2:2: 4-byte macropixels, odd width rounds up.
2321 assert_eq!(Yuyv422.plane_row_bytes(0, 6), Some(12));
2322 assert_eq!(Uyvy422.plane_row_bytes(0, 7), Some(16));
2323 // Semi-planar chroma: 2 bytes per position.
2324 assert_eq!(Nv12.plane_row_bytes(0, 7), Some(7));
2325 assert_eq!(Nv12.plane_row_bytes(1, 7), Some(8));
2326 // Deep planar planes: 2 bytes per sample regardless of the
2327 // number of valid bits in the word.
2328 assert_eq!(Yuv420P10Le.plane_row_bytes(1, 7), Some(8));
2329 assert_eq!(Gbrap14Le.plane_row_bytes(3, 5), Some(10));
2330 // Packed pixel costs.
2331 assert_eq!(Rgb24.plane_row_bytes(0, 5), Some(15));
2332 assert_eq!(Rgb48Le.plane_row_bytes(0, 2), Some(12));
2333 assert_eq!(Rgba64Le.plane_row_bytes(0, 2), Some(16));
2334 assert_eq!(Ya16Le.plane_row_bytes(0, 3), Some(12));
2335 assert_eq!(Cmyk.plane_row_bytes(0, 3), Some(12));
2336 // Out-of-range plane.
2337 assert_eq!(Gray8.plane_row_bytes(1, 8), None);
2338 }
2339
2340 #[test]
2341 fn frame_size_examples() {
2342 use PixelFormat::*;
2343 assert_eq!(Yuv420P.frame_size_bytes(4, 4), Some(24));
2344 assert_eq!(Nv12.frame_size_bytes(7, 5), Some(59)); // 35 + 4×3×2
2345 assert_eq!(Yuyv422.frame_size_bytes(7, 2), Some(32));
2346 assert_eq!(MonoBlack.frame_size_bytes(13, 3), Some(6));
2347 assert_eq!(Pal8.frame_size_bytes(5, 4), Some(20));
2348 assert_eq!(Ya16Le.frame_size_bytes(3, 3), Some(36));
2349 assert_eq!(Yuva444P16Le.frame_size_bytes(3, 3), Some(72));
2350 }
2351
2352 #[test]
2353 fn frame_size_is_sum_of_planes_for_every_format() {
2354 for fmt in ALL_PIXEL_FORMATS {
2355 for (w, h) in [(0, 0), (1, 1), (2, 2), (7, 5), (16, 16), (13, 1), (1, 13)] {
2356 let total = fmt
2357 .frame_size_bytes(w, h)
2358 .unwrap_or_else(|| panic!("{fmt:?} {w}x{h} must size"));
2359 let sum: usize = (0..fmt.plane_count())
2360 .map(|p| fmt.plane_size_bytes(p, w, h).unwrap())
2361 .sum();
2362 assert_eq!(total, sum, "{fmt:?} {w}x{h}");
2363 // Plane 0 is always the full pixel grid.
2364 assert_eq!(fmt.plane_dimensions(0, w, h), Some((w, h)), "{fmt:?}");
2365 // The plane table ends exactly at plane_count.
2366 assert_eq!(fmt.plane_dimensions(fmt.plane_count(), w, h), None);
2367 // Tightly-packed storage can never be smaller than the
2368 // packed-bits density estimate.
2369 let storage_bits = total as u128 * 8;
2370 let density_bits = w as u128 * h as u128 * fmt.bits_per_pixel_approx() as u128;
2371 assert!(
2372 storage_bits >= density_bits,
2373 "{fmt:?} {w}x{h}: storage {storage_bits} < density {density_bits}"
2374 );
2375 }
2376 }
2377 }
2378
2379 #[test]
2380 fn sizing_overflow_returns_none() {
2381 assert_eq!(
2382 PixelFormat::Rgba64Le.frame_size_bytes(u32::MAX, u32::MAX),
2383 None
2384 );
2385 assert_eq!(
2386 PixelFormat::RgbaF32Le.frame_size_bytes(u32::MAX, u32::MAX),
2387 None
2388 );
2389 assert_eq!(
2390 PixelFormat::Yuv440P16Le.plane_size_bytes(0, u32::MAX, u32::MAX),
2391 None
2392 );
2393 }
2394}