Skip to main content

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}