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    //
509    // Legacy labels: the range is part of the format name. Every other
510    // format leaves the range to the stream's / frame's colour signal
511    // (`CodecParameters::color_signal`, `VideoFrame::color_signal`);
512    // see `implied_color_range`.
513    /// JPEG/full-range YUV 4:2:0 planar. Implies
514    /// [`ColorRange::Full`](crate::ColorRange::Full).
515    YuvJ420P = 22,
516    /// JPEG/full-range YUV 4:2:2 planar. Implies
517    /// [`ColorRange::Full`](crate::ColorRange::Full).
518    YuvJ422P = 23,
519    /// JPEG/full-range YUV 4:4:4 planar. Implies
520    /// [`ColorRange::Full`](crate::ColorRange::Full).
521    YuvJ444P = 24,
522
523    // --- Semi-planar YUV ---
524    /// YUV 4:2:0, planar Y + interleaved UV (NV12).
525    Nv12 = 25,
526    /// YUV 4:2:0, planar Y + interleaved VU (NV21).
527    Nv21 = 26,
528
529    // --- Gray + alpha / YUV + alpha ---
530    /// Packed grayscale + alpha, 2 bytes/pixel (Y, A).
531    Ya8 = 27,
532    /// Yuv420P with an additional full-resolution alpha plane.
533    Yuva420P = 28,
534
535    // --- Mono (1 bit per pixel) ---
536    /// 1 bit per pixel, packed MSB-first, 0 = black.
537    MonoBlack = 29,
538    /// 1 bit per pixel, packed MSB-first, 0 = white.
539    MonoWhite = 30,
540
541    // --- Interleaved YUV 4:2:2 ---
542    /// Packed 4:2:2, byte order Y0 U0 Y1 V0.
543    Yuyv422 = 31,
544    /// Packed 4:2:2, byte order U0 Y0 V0 Y1.
545    Uyvy422 = 32,
546
547    // --- Print / prepress ---
548    /// Packed 8-bit CMYK, 4 bytes/pixel in byte order C, M, Y, K.
549    /// "Regular" convention: C=0 means no cyan ink (white), C=255 means
550    /// full cyan. Used by JPEG 4-component scans from non-Adobe encoders
551    /// and by many print-side image toolchains. Adobe Photoshop's
552    /// inverted CMYK (where 0 = full ink) is the separate
553    /// [`CmykInverted`](Self::CmykInverted) variant.
554    Cmyk = 33,
555
556    // --- Wide-horizontal subsampled YUV ---
557    /// 8-bit YUV 4:1:1, planar (Y, U, V). Luma at full resolution; chroma
558    /// horizontally subsampled by 4 (each chroma sample covers a 4×1
559    /// luma block), no vertical subsampling. Native sampling of
560    /// NTSC DV-25 and a legal JPEG sampling layout (luma H=4, V=1;
561    /// chroma H=V=1) emitted by some real-world JPEG corpora.
562    Yuv411P = 34,
563
564    // --- Planar GBR / GBRA (RGB stored as planes in G,B,R order) ---
565    //
566    // High-bit-depth GBR(A) layouts used by MagicYUV, JPEG 2000, OpenEXR,
567    // TIFF and similar workflows that need lossless RGB at 10/12/14 bits
568    // per channel. Planes are ordered G, B, R (and A for the `Gbrap*`
569    // variants) — and
570    // each sample is stored as a 16-bit little-endian word with the
571    // top bits zero. The native 8-bit ([`Gbrp8`](Self::Gbrp8)) and
572    // full-width 16-bit ([`Gbrp16Le`](Self::Gbrp16Le) /
573    // [`Gbrap16Le`](Self::Gbrap16Le)) companions arrived later and
574    // therefore live at fresh appended discriminants (52-54), per the
575    // append-only rule.
576    /// 10-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
577    /// G, B, R; each sample uses the low 10 bits of a 16-bit word.
578    Gbrp10Le = 35,
579    /// 10-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
580    /// ordered G, B, R, A; each sample uses the low 10 bits of a 16-bit
581    /// word.
582    Gbrap10Le = 36,
583    /// 12-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
584    /// G, B, R; each sample uses the low 12 bits of a 16-bit word.
585    Gbrp12Le = 37,
586    /// 12-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
587    /// ordered G, B, R, A; each sample uses the low 12 bits of a 16-bit
588    /// word.
589    Gbrap12Le = 38,
590    /// 14-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
591    /// G, B, R; each sample uses the low 14 bits of a 16-bit word.
592    Gbrp14Le = 39,
593    /// 14-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
594    /// ordered G, B, R, A; each sample uses the low 14 bits of a 16-bit
595    /// word.
596    Gbrap14Le = 40,
597
598    // --- 16-bit YUV planar ---
599    //
600    // Full-width companions to the 10/12-bit planar YUV variants above:
601    // same three-plane layout and little-endian 16-bit words, but ALL 16
602    // bits of every word are significant (there are no zero top bits and
603    // no separate "valid bits" count — full-scale is 65535). Needed by
604    // wavelet codecs whose signal-range presets go to 16 bits per
605    // component (SMPTE VC-2 / Dirac video-format presets 7 and 8).
606    /// 16-bit YUV 4:2:0 planar, little-endian 16-bit storage. All 16
607    /// bits of each sample word are significant.
608    Yuv420P16Le = 41,
609    /// 16-bit YUV 4:2:2 planar, little-endian 16-bit storage. All 16
610    /// bits of each sample word are significant.
611    Yuv422P16Le = 42,
612    /// 16-bit YUV 4:4:4 planar, little-endian 16-bit storage. All 16
613    /// bits of each sample word are significant.
614    Yuv444P16Le = 43,
615
616    // --- 8-bit YUV + alpha at the remaining chroma samplings ---
617    //
618    // Companions to `Yuva420P`: the alpha plane is always full
619    // resolution (one 8-bit sample per pixel, never chroma-subsampled),
620    // appended after the V plane as plane index 3. Intermediate/mezzanine
621    // codecs carry alpha at 4:2:2 and 4:4:4 samplings.
622    /// Yuv422P with an additional full-resolution alpha plane.
623    Yuva422P = 44,
624    /// Yuv444P with an additional full-resolution alpha plane.
625    Yuva444P = 45,
626
627    // --- Deep YUV + alpha (10/12/16-bit words with full-resolution A) ---
628    //
629    // Alpha-carrying companions to the 10/12/16-bit planar YUV variants
630    // above, completing the Yuva family for mezzanine codecs that carry
631    // deep colour together with an alpha channel. Same conventions as
632    // the 8-bit `Yuva*` trio: 4 planes ordered Y, U, V, A with the
633    // alpha plane always at full resolution (one sample per pixel,
634    // never chroma-subsampled) as plane index 3. Every sample — alpha
635    // included — is stored as a little-endian 16-bit word; for the
636    // 10/12-bit variants each sample uses the low bits of the word with
637    // the top bits zero, and for the 16-bit variants all 16 bits of
638    // every word are significant (full-scale is 65535), matching
639    // `Yuv420P16Le`/`Yuv422P16Le`/`Yuv444P16Le`.
640    /// 10-bit YUV 4:2:2 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    Yuva422P10Le = 46,
644    /// 12-bit YUV 4:2:2 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    Yuva422P12Le = 47,
648    /// 10-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
649    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
650    /// the low 10 bits of a 16-bit word.
651    Yuva444P10Le = 48,
652    /// 12-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
653    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
654    /// the low 12 bits of a 16-bit word.
655    Yuva444P12Le = 49,
656    /// 16-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
657    /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
658    /// each sample word are significant.
659    Yuva422P16Le = 50,
660    /// 16-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
661    /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
662    /// each sample word are significant.
663    Yuva444P16Le = 51,
664
665    // --- Native 8-bit and full-width 16-bit planar GBR(A) ---
666    //
667    // Companions to the 10/12/14-bit `Gbrp*`/`Gbrap*` family above,
668    // closing the planar-RGB depth ladder at both ends for lossless
669    // RGB codecs whose native coding space is per-plane G, B, R.
670    // Plane order is identical to the rest of the family: G, B, R
671    // (and A as plane index 3 for `Gbrap16Le`, always at full
672    // resolution — RGB has no chroma subsampling). `Gbrp8` stores one
673    // byte per sample with all 8 bits significant; the 16-bit variants
674    // store little-endian 16-bit words with ALL 16 bits significant
675    // (full-scale is 65535, matching the `Yuv*P16Le` convention — no
676    // zero top bits, no separate valid-bits count). Odd in-between
677    // depths on these storage formats (e.g. 9- or 15-bit RGB) are
678    // expressed via the per-plane significant-bits side-channel on
679    // `VideoFrame`, not by new enum variants.
680    /// 8-bit planar GBR. 3 planes ordered G, B, R; one byte per
681    /// sample, all 8 bits significant.
682    Gbrp8 = 52,
683    /// 16-bit planar GBR, little-endian 16-bit storage. 3 planes
684    /// ordered G, B, R; all 16 bits of each sample word are
685    /// significant.
686    Gbrp16Le = 53,
687    /// 16-bit planar GBR + alpha, little-endian 16-bit storage. 4
688    /// planes ordered G, B, R, A; all 16 bits of each sample word are
689    /// significant.
690    Gbrap16Le = 54,
691
692    // --- Deep YUV + alpha at 4:2:0 ---
693    //
694    // Completes the deep Yuva family begun by the 4:2:2/4:4:4 variants
695    // above (46-51) at the remaining chroma sampling. Same conventions:
696    // 4 planes ordered Y, U, V, A with the alpha plane always at full
697    // resolution (one sample per pixel, never chroma-subsampled) as
698    // plane index 3. Every sample — alpha included — is stored as a
699    // little-endian 16-bit word; the 10/12-bit variants keep values in
700    // the low bits of the word with the top bits zero, and the 16-bit
701    // variant has all 16 bits of every word significant (full-scale is
702    // 65535), matching `Yuv420P16Le`.
703    /// 10-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
704    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
705    /// the low 10 bits of a 16-bit word.
706    Yuva420P10Le = 55,
707    /// 12-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
708    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
709    /// the low 12 bits of a 16-bit word.
710    Yuva420P12Le = 56,
711    /// 16-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
712    /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
713    /// each sample word are significant.
714    Yuva420P16Le = 57,
715
716    // --- 8-bit planar GBR + alpha ---
717    //
718    // Alpha-carrying companion to `Gbrp8`, filling the last hole in
719    // the planar GBR(A) family: with this variant every depth on the
720    // ladder (8/10/12/14/16) exists in both alpha-less and
721    // alpha-carrying form. Same conventions as the rest of the family:
722    // planes ordered G, B, R, A with the alpha plane at full
723    // resolution (RGB has no chroma subsampling) as plane index 3,
724    // one byte per sample, all 8 bits significant. Lossless RGB codecs
725    // whose native coding space is per-plane G, B, R carry 8-bit RGBA
726    // in exactly this shape.
727    /// 8-bit planar GBR + alpha. 4 planes ordered G, B, R, A; one
728    /// byte per sample, all 8 bits significant.
729    Gbrap8 = 58,
730
731    // --- Deep gray + alpha ---
732    //
733    // 16-bit companion to `Ya8`, ending the gray+alpha ladder at the
734    // same depth the plain gray ladder already reaches (`Gray16Le`).
735    // Still-image wire formats carry 16-bit greyscale-with-alpha
736    // natively (PNG colour type 4 at bit depth 16); without this
737    // variant that content must detour through `Rgba64Le`, tripling
738    // the gray payload and losing the "single luminance component"
739    // semantics. Same packed shape as `Ya8` — interleaved Y then A —
740    // with each sample widened to a little-endian 16-bit word, all 16
741    // bits significant (full-scale is 65535, the `Gray16Le`
742    // convention). In-between gray+alpha depths stay the job of the
743    // per-plane significant-bits side-channel.
744    /// Packed 16-bit grayscale + alpha, little-endian, 4 bytes/pixel
745    /// (Y, A). All 16 bits of each sample word are significant.
746    Ya16Le = 59,
747
748    // --- Print / prepress, inverted-ink convention ---
749    //
750    // The companion `Cmyk` (33) reserved this name when it was added:
751    // Adobe-authored 4-component scans store ink coverage inverted on
752    // the wire (0 = full ink, 255 = no ink), and decoders that want to
753    // hand the wire values through losslessly need a format that says
754    // so rather than silently re-using the regular-convention `Cmyk`.
755    /// Packed 8-bit inverted CMYK, 4 bytes/pixel in byte order C, M,
756    /// Y, K. Inverted-ink convention: C=0 means full cyan ink, C=255
757    /// means no cyan (white) — the complement of [`Cmyk`](Self::Cmyk).
758    CmykInverted = 60,
759
760    // --- 4:4:0 planar YUV (full-width, half-height chroma) ---
761    //
762    // Vertical-only chroma subsampling: each chroma plane keeps the
763    // full luma width but carries half the rows — subsampling shifts
764    // ssx = 0, ssy = 1, the transpose of 4:2:2's half-width,
765    // full-height geometry. A legal JPEG sampling combination (luma
766    // H=1, V=2) seen in real-world corpora, and a coded
767    // chroma-sampling mode of video bitstreams whose sampling flags
768    // allow horizontal and vertical decimation to be chosen
769    // independently. The depth ladder mirrors the other planar YUV
770    // samplings: 8-bit bytes, then 10/12-bit values in the low bits
771    // of little-endian 16-bit words, then full-width 16-bit words
772    // with every bit significant (full-scale is 65535).
773    /// 8-bit YUV 4:4:0, planar (Y, U, V). Chroma at full width, half
774    /// height (ssx = 0, ssy = 1).
775    Yuv440P = 61,
776    /// 10-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each
777    /// sample uses the low 10 bits of a 16-bit word.
778    Yuv440P10Le = 62,
779    /// 12-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each
780    /// sample uses the low 12 bits of a 16-bit word.
781    Yuv440P12Le = 63,
782    /// 16-bit YUV 4:4:0 planar, little-endian 16-bit storage. All 16
783    /// bits of each sample word are significant.
784    Yuv440P16Le = 64,
785
786    // --- Scene-referred 32-bit float (linear-light HDR) ---
787    //
788    // IEEE 754 binary32 components stored as little-endian 32-bit
789    // words, one word per sample. Unlike every integer format above
790    // there is no integer full-scale: samples are scene-referred
791    // linear light where 1.0 is the nominal diffuse-white anchor and
792    // values outside [0, 1] are legal (speculars above white,
793    // negative out-of-gamut excursions). Needed by HDR image wire
794    // formats whose native component type is floating point. The
795    // packed trio mirrors `Gray8`/`Rgb24`/`Rgba` component orders at
796    // float width; the planar pair extends the planar GBR(A) family
797    // beyond the integer depth ladder, with the usual G, B, R (+ A)
798    // plane order and the alpha plane at full resolution as plane
799    // index 3.
800    /// Packed 32-bit float grayscale, little-endian, 4 bytes/pixel.
801    /// Scene-referred linear light.
802    GrayF32Le = 65,
803    /// Packed 32-bit float RGB, little-endian, 12 bytes/pixel in
804    /// component order R, G, B. Scene-referred linear light.
805    RgbF32Le = 66,
806    /// Packed 32-bit float RGBA, little-endian, 16 bytes/pixel in
807    /// component order R, G, B, A. Scene-referred linear light;
808    /// alpha is straight (non-premultiplied), nominal range [0, 1].
809    RgbaF32Le = 67,
810    /// 32-bit float planar GBR, little-endian. 3 planes ordered G, B,
811    /// R; one 4-byte word per sample. Scene-referred linear light.
812    GbrpF32Le = 68,
813    /// 32-bit float planar GBR + alpha, little-endian. 4 planes
814    /// ordered G, B, R, A; one 4-byte word per sample; the alpha
815    /// plane is at full resolution as plane index 3, straight
816    /// (non-premultiplied), nominal range [0, 1].
817    GbrapF32Le = 69,
818}
819
820impl PixelFormat {
821    /// True if this format stores its components in separate planes.
822    pub fn is_planar(&self) -> bool {
823        matches!(
824            self,
825            Self::Yuv420P
826                | Self::Yuv422P
827                | Self::Yuv444P
828                | Self::Yuv411P
829                | Self::Yuv420P10Le
830                | Self::Yuv422P10Le
831                | Self::Yuv444P10Le
832                | Self::Yuv420P12Le
833                | Self::Yuv422P12Le
834                | Self::Yuv444P12Le
835                | Self::Yuv420P16Le
836                | Self::Yuv422P16Le
837                | Self::Yuv444P16Le
838                | Self::Yuv440P
839                | Self::Yuv440P10Le
840                | Self::Yuv440P12Le
841                | Self::Yuv440P16Le
842                | Self::YuvJ420P
843                | Self::YuvJ422P
844                | Self::YuvJ444P
845                | Self::Nv12
846                | Self::Nv21
847                | Self::Yuva420P
848                | Self::Yuva422P
849                | Self::Yuva444P
850                | Self::Yuva422P10Le
851                | Self::Yuva422P12Le
852                | Self::Yuva444P10Le
853                | Self::Yuva444P12Le
854                | Self::Yuva422P16Le
855                | Self::Yuva444P16Le
856                | Self::Yuva420P10Le
857                | Self::Yuva420P12Le
858                | Self::Yuva420P16Le
859                | Self::Gbrp8
860                | Self::Gbrap8
861                | Self::Gbrp10Le
862                | Self::Gbrap10Le
863                | Self::Gbrp12Le
864                | Self::Gbrap12Le
865                | Self::Gbrp14Le
866                | Self::Gbrap14Le
867                | Self::Gbrp16Le
868                | Self::Gbrap16Le
869                | Self::GbrpF32Le
870                | Self::GbrapF32Le
871        )
872    }
873
874    /// The sample range this format's *label* commits to, if any.
875    ///
876    /// Only the three legacy full-range labels (`YuvJ420P`, `YuvJ422P`,
877    /// `YuvJ444P`) carry a range in their name and return
878    /// `Some(Full)`. Every other format — limited-range-by-convention
879    /// `Yuv*`, every >8-bit and alpha-bearing Y′CbCr surface, RGB and
880    /// grey — returns `None`: their range is a property of the signal,
881    /// not of the storage layout, and lives in
882    /// [`ColorSignal::range`](crate::ColorSignal::range) on the stream
883    /// ([`CodecParameters::color_signal`](crate::CodecParameters::color_signal))
884    /// or frame ([`VideoFrame::color_signal`](crate::VideoFrame::color_signal)).
885    /// Use
886    /// [`CodecParameters::resolved_color_range`](crate::CodecParameters::resolved_color_range)
887    /// for the combined answer (explicit signal first, then this label).
888    pub fn implied_color_range(&self) -> Option<crate::signal::ColorRange> {
889        match self {
890            Self::YuvJ420P | Self::YuvJ422P | Self::YuvJ444P => {
891                Some(crate::signal::ColorRange::Full)
892            }
893            _ => None,
894        }
895    }
896
897    /// True if the format is a palette index format (`Pal8`).
898    pub fn is_palette(&self) -> bool {
899        matches!(self, Self::Pal8)
900    }
901
902    /// True if this format carries an alpha channel.
903    pub fn has_alpha(&self) -> bool {
904        matches!(
905            self,
906            Self::Rgba
907                | Self::Bgra
908                | Self::Argb
909                | Self::Abgr
910                | Self::Rgba64Le
911                | Self::Ya8
912                | Self::Ya16Le
913                | Self::Yuva420P
914                | Self::Yuva422P
915                | Self::Yuva444P
916                | Self::Yuva422P10Le
917                | Self::Yuva422P12Le
918                | Self::Yuva444P10Le
919                | Self::Yuva444P12Le
920                | Self::Yuva422P16Le
921                | Self::Yuva444P16Le
922                | Self::Yuva420P10Le
923                | Self::Yuva420P12Le
924                | Self::Yuva420P16Le
925                | Self::Gbrap8
926                | Self::Gbrap10Le
927                | Self::Gbrap12Le
928                | Self::Gbrap14Le
929                | Self::Gbrap16Le
930                | Self::RgbaF32Le
931                | Self::GbrapF32Le
932        )
933    }
934
935    /// True for the 32-bit IEEE-float variants, packed or planar.
936    /// Float formats are scene-referred: samples carry linear light
937    /// with no integer full-scale — 1.0 is the nominal diffuse-white
938    /// anchor and values outside [0, 1] are legal.
939    pub fn is_float(&self) -> bool {
940        matches!(
941            self,
942            Self::GrayF32Le | Self::RgbF32Le | Self::RgbaF32Le | Self::GbrpF32Le | Self::GbrapF32Le
943        )
944    }
945
946    /// Number of planes in the stored layout. Packed and palette formats
947    /// return 1; NV12/NV21 return 2; planar YUV without alpha and the
948    /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4.
949    pub fn plane_count(&self) -> usize {
950        match self {
951            Self::Nv12 | Self::Nv21 => 2,
952            Self::Yuv420P
953            | Self::Yuv422P
954            | Self::Yuv444P
955            | Self::Yuv411P
956            | Self::Yuv420P10Le
957            | Self::Yuv422P10Le
958            | Self::Yuv444P10Le
959            | Self::Yuv420P12Le
960            | Self::Yuv422P12Le
961            | Self::Yuv444P12Le
962            | Self::Yuv420P16Le
963            | Self::Yuv422P16Le
964            | Self::Yuv444P16Le
965            | Self::Yuv440P
966            | Self::Yuv440P10Le
967            | Self::Yuv440P12Le
968            | Self::Yuv440P16Le
969            | Self::YuvJ420P
970            | Self::YuvJ422P
971            | Self::YuvJ444P
972            | Self::Gbrp8
973            | Self::Gbrp10Le
974            | Self::Gbrp12Le
975            | Self::Gbrp14Le
976            | Self::Gbrp16Le
977            | Self::GbrpF32Le => 3,
978            Self::Yuva420P
979            | Self::Yuva422P
980            | Self::Yuva444P
981            | Self::Yuva422P10Le
982            | Self::Yuva422P12Le
983            | Self::Yuva444P10Le
984            | Self::Yuva444P12Le
985            | Self::Yuva422P16Le
986            | Self::Yuva444P16Le
987            | Self::Yuva420P10Le
988            | Self::Yuva420P12Le
989            | Self::Yuva420P16Le
990            | Self::Gbrap8
991            | Self::Gbrap10Le
992            | Self::Gbrap12Le
993            | Self::Gbrap14Le
994            | Self::Gbrap16Le
995            | Self::GbrapF32Le => 4,
996            _ => 1,
997        }
998    }
999
1000    /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
1001    /// for chroma-subsampled YUV — intended for worst-case preallocation
1002    /// rather than wire-accurate accounting.
1003    pub fn bits_per_pixel_approx(&self) -> u32 {
1004        match self {
1005            Self::MonoBlack | Self::MonoWhite => 1,
1006            Self::Gray8 | Self::Pal8 => 8,
1007            Self::Ya8 => 16,
1008            // 16-bit gray + alpha: two LE 16-bit words per pixel, all
1009            // bits significant — packed bits equal storage bits.
1010            Self::Ya16Le => 32,
1011            Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
1012            Self::Rgb24 | Self::Bgr24 => 24,
1013            Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
1014            Self::Rgb48Le => 48,
1015            Self::Rgba64Le => 64,
1016            Self::Yuyv422 | Self::Uyvy422 => 16,
1017            Self::Cmyk | Self::CmykInverted => 32,
1018            // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
1019            // 10/12-bit variants double the byte count but we report the
1020            // packed-bits-per-pixel estimate for a uniform heuristic.
1021            Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
1022            // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
1023            // full res + 2 chroma planes each subsampled by 4).
1024            Self::Yuv411P => 12,
1025            Self::Yuv422P | Self::YuvJ422P => 16,
1026            // 4:4:0 packs the same 2 samples/pixel as 4:2:2 (Y at full
1027            // res + 2 chroma planes at half height, full width).
1028            Self::Yuv440P => 16,
1029            Self::Yuv444P | Self::YuvJ444P => 24,
1030            Self::Yuv420P10Le | Self::Yuv420P12Le | Self::Yuv420P16Le => 24,
1031            Self::Yuv422P10Le | Self::Yuv422P12Le | Self::Yuv422P16Le => 32,
1032            // Deep 4:4:0 matches deep 4:2:2 — 2 sample words per pixel.
1033            Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => 32,
1034            Self::Yuv444P10Le | Self::Yuv444P12Le | Self::Yuv444P16Le => 48,
1035            Self::Yuva420P => 20,
1036            // 4:2:2 + full-res alpha: 8 (Y) + 4 (U) + 4 (V) + 8 (A).
1037            Self::Yuva422P => 24,
1038            // 4:4:4 + full-res alpha: four full-resolution 8-bit planes.
1039            Self::Yuva444P => 32,
1040            // Deep 4:2:2 + full-res alpha in 16-bit words: the estimator
1041            // reports the 16-bit-word cost like the alpha-less deep YUV
1042            // arms above — 3 sample words per pixel (Y + U/2 + V/2 + A).
1043            Self::Yuva422P10Le | Self::Yuva422P12Le | Self::Yuva422P16Le => 48,
1044            // Deep 4:4:4 + full-res alpha: 4 sample words per pixel.
1045            Self::Yuva444P10Le | Self::Yuva444P12Le | Self::Yuva444P16Le => 64,
1046            // Deep 4:2:0 + full-res alpha in 16-bit words: 16-bit-word
1047            // storage cost of the alpha-less 4:2:0 arms above (24) plus
1048            // one full-resolution 16-bit alpha word per pixel.
1049            Self::Yuva420P10Le | Self::Yuva420P12Le | Self::Yuva420P16Le => 40,
1050            // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we
1051            // report the packed bits-per-pixel density (samples × bits)
1052            // rather than the 16-bit storage cost, matching how the
1053            // 10/12-bit YUV variants are reported above.
1054            Self::Gbrp10Le => 30,
1055            Self::Gbrap10Le => 40,
1056            Self::Gbrp12Le => 36,
1057            Self::Gbrap12Le => 48,
1058            Self::Gbrp14Le => 42,
1059            Self::Gbrap14Le => 56,
1060            // Native 8-bit GBR: three bytes per pixel, like Rgb24 but
1061            // planar. 16-bit GBR(A): packed bits == storage bits (every
1062            // bit of each 16-bit word is significant), so the density
1063            // and storage numbers coincide.
1064            Self::Gbrp8 => 24,
1065            Self::Gbrp16Le => 48,
1066            Self::Gbrap16Le => 64,
1067            // 8-bit GBR + alpha: four bytes per pixel, like Rgba but
1068            // planar.
1069            Self::Gbrap8 => 32,
1070            // 32-bit float family: every sample is a full binary32
1071            // word, so packed bits equal storage bits (32 per sample;
1072            // no chroma subsampling anywhere in the family).
1073            Self::GrayF32Le => 32,
1074            Self::RgbF32Le | Self::GbrpF32Le => 96,
1075            Self::RgbaF32Le | Self::GbrapF32Le => 128,
1076        }
1077    }
1078
1079    /// Log2 chroma-subsampling shifts `(ssx, ssy)` relative to the
1080    /// luma grid, for formats that carry chroma on a subsampled (or
1081    /// potentially subsampled) grid. The chroma sample grid is the
1082    /// luma grid right-shifted by `ssx` horizontally and `ssy`
1083    /// vertically, with ceiling division for odd luma sizes (see
1084    /// [`plane_dimensions`](Self::plane_dimensions)).
1085    ///
1086    /// | sampling | `(ssx, ssy)` | chroma geometry |
1087    /// |----------|--------------|-----------------|
1088    /// | 4:2:0    | `(1, 1)`     | half width, half height |
1089    /// | 4:2:2    | `(1, 0)`     | half width, full height |
1090    /// | 4:4:4    | `(0, 0)`     | full resolution |
1091    /// | 4:1:1    | `(2, 0)`     | quarter width, full height |
1092    /// | 4:4:0    | `(0, 1)`     | full width, half height |
1093    ///
1094    /// Returns `None` for formats without a distinct chroma grid
1095    /// (grayscale, RGB/GBR in any layout, palette, mono, CMYK).
1096    /// Packed 4:2:2 (`Yuyv422`/`Uyvy422`) and semi-planar 4:2:0
1097    /// (`Nv12`/`Nv21`) report their sampling even though the chroma
1098    /// samples don't live in standalone planes.
1099    ///
1100    /// ```
1101    /// use oxideav_core::PixelFormat;
1102    /// // 4:4:0: full-width, half-height chroma.
1103    /// assert_eq!(PixelFormat::Yuv440P.chroma_subsampling(), Some((0, 1)));
1104    /// // 4:2:0: subsampled on both axes.
1105    /// assert_eq!(PixelFormat::Yuv420P.chroma_subsampling(), Some((1, 1)));
1106    /// // RGB has no chroma grid.
1107    /// assert_eq!(PixelFormat::Rgba.chroma_subsampling(), None);
1108    /// ```
1109    pub fn chroma_subsampling(&self) -> Option<(u32, u32)> {
1110        match self {
1111            // 4:2:0 — half width, half height.
1112            Self::Yuv420P
1113            | Self::YuvJ420P
1114            | Self::Yuv420P10Le
1115            | Self::Yuv420P12Le
1116            | Self::Yuv420P16Le
1117            | Self::Nv12
1118            | Self::Nv21
1119            | Self::Yuva420P
1120            | Self::Yuva420P10Le
1121            | Self::Yuva420P12Le
1122            | Self::Yuva420P16Le => Some((1, 1)),
1123            // 4:2:2 — half width, full height (packed 4:2:2 included).
1124            Self::Yuv422P
1125            | Self::YuvJ422P
1126            | Self::Yuv422P10Le
1127            | Self::Yuv422P12Le
1128            | Self::Yuv422P16Le
1129            | Self::Yuva422P
1130            | Self::Yuva422P10Le
1131            | Self::Yuva422P12Le
1132            | Self::Yuva422P16Le
1133            | Self::Yuyv422
1134            | Self::Uyvy422 => Some((1, 0)),
1135            // 4:4:4 — chroma at full resolution.
1136            Self::Yuv444P
1137            | Self::YuvJ444P
1138            | Self::Yuv444P10Le
1139            | Self::Yuv444P12Le
1140            | Self::Yuv444P16Le
1141            | Self::Yuva444P
1142            | Self::Yuva444P10Le
1143            | Self::Yuva444P12Le
1144            | Self::Yuva444P16Le => Some((0, 0)),
1145            // 4:1:1 — quarter width, full height.
1146            Self::Yuv411P => Some((2, 0)),
1147            // 4:4:0 — full width, half height.
1148            Self::Yuv440P | Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => {
1149                Some((0, 1))
1150            }
1151            // Everything else has no distinct chroma grid.
1152            _ => None,
1153        }
1154    }
1155
1156    /// Sample-grid dimensions of plane `plane` for a `width` ×
1157    /// `height` picture, with ceiling division on subsampled axes so
1158    /// odd luma sizes still cover every pixel.
1159    ///
1160    /// Conventions:
1161    /// - Plane 0 (luma / the packed plane) is always `(width, height)`.
1162    /// - Chroma planes (indices 1 and 2 of planar YUV, index 1 of the
1163    ///   semi-planar formats) are the luma grid right-shifted by the
1164    ///   [`chroma_subsampling`](Self::chroma_subsampling) factors.
1165    ///   Semi-planar chroma dimensions are in chroma *positions* —
1166    ///   each position stores two interleaved samples, which
1167    ///   [`plane_row_bytes`](Self::plane_row_bytes) accounts for.
1168    /// - Alpha planes (index 3) and all planar-RGB planes are at full
1169    ///   resolution.
1170    /// - Packed, palette, and bit-packed mono formats report pixel
1171    ///   dimensions for their single plane; per-row byte cost comes
1172    ///   from [`plane_row_bytes`](Self::plane_row_bytes).
1173    ///
1174    /// Returns `None` when `plane >= plane_count()`.
1175    ///
1176    /// ```
1177    /// use oxideav_core::PixelFormat;
1178    /// // 4:4:0 chroma: full width, half height (odd height rounds up).
1179    /// assert_eq!(
1180    ///     PixelFormat::Yuv440P.plane_dimensions(1, 640, 481),
1181    ///     Some((640, 241))
1182    /// );
1183    /// // Alpha plane of a deep YUVA format stays at full resolution.
1184    /// assert_eq!(
1185    ///     PixelFormat::Yuva420P10Le.plane_dimensions(3, 7, 5),
1186    ///     Some((7, 5))
1187    /// );
1188    /// assert_eq!(PixelFormat::Rgb24.plane_dimensions(1, 8, 8), None);
1189    /// ```
1190    pub fn plane_dimensions(&self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> {
1191        if plane >= self.plane_count() {
1192            return None;
1193        }
1194        match (self.chroma_subsampling(), plane) {
1195            (Some((ssx, ssy)), 1 | 2) => {
1196                Some((width.div_ceil(1 << ssx), height.div_ceil(1 << ssy)))
1197            }
1198            _ => Some((width, height)),
1199        }
1200    }
1201
1202    /// Tightly-packed byte count of one row of plane `plane` for a
1203    /// picture `width` pixels wide — no stride padding or alignment.
1204    /// Real codecs frequently over-allocate rows for alignment; this
1205    /// is the minimum a row occupies.
1206    ///
1207    /// Returns `None` when `plane >= plane_count()` or the byte count
1208    /// overflows `usize`.
1209    pub fn plane_row_bytes(&self, plane: usize, width: u32) -> Option<usize> {
1210        let (pw, _) = self.plane_dimensions(plane, width, 1)?;
1211        let pw = pw as usize;
1212        let bytes_per_position: usize = match self {
1213            // Bit-packed mono: 8 pixels per byte, ragged tail byte.
1214            Self::MonoBlack | Self::MonoWhite => return Some(pw.div_ceil(8)),
1215            // Packed 4:2:2 macropixels: 4 bytes per 2 pixels; an odd
1216            // trailing pixel still occupies a full macropixel.
1217            Self::Yuyv422 | Self::Uyvy422 => return pw.div_ceil(2).checked_mul(4),
1218            // One byte per sample position.
1219            Self::Gray8
1220            | Self::Pal8
1221            | Self::Yuv420P
1222            | Self::Yuv422P
1223            | Self::Yuv444P
1224            | Self::Yuv411P
1225            | Self::Yuv440P
1226            | Self::YuvJ420P
1227            | Self::YuvJ422P
1228            | Self::YuvJ444P
1229            | Self::Yuva420P
1230            | Self::Yuva422P
1231            | Self::Yuva444P
1232            | Self::Gbrp8
1233            | Self::Gbrap8 => 1,
1234            // Semi-planar: one byte per luma sample on plane 0, an
1235            // interleaved two-sample pair per chroma position on
1236            // plane 1.
1237            Self::Nv12 | Self::Nv21 => {
1238                if plane == 0 {
1239                    1
1240                } else {
1241                    2
1242                }
1243            }
1244            // Little-endian 16-bit words (10/12/14/16-bit storage).
1245            Self::Gray10Le
1246            | Self::Gray12Le
1247            | Self::Gray16Le
1248            | Self::Yuv420P10Le
1249            | Self::Yuv422P10Le
1250            | Self::Yuv444P10Le
1251            | Self::Yuv420P12Le
1252            | Self::Yuv422P12Le
1253            | Self::Yuv444P12Le
1254            | Self::Yuv420P16Le
1255            | Self::Yuv422P16Le
1256            | Self::Yuv444P16Le
1257            | Self::Yuv440P10Le
1258            | Self::Yuv440P12Le
1259            | Self::Yuv440P16Le
1260            | Self::Yuva422P10Le
1261            | Self::Yuva422P12Le
1262            | Self::Yuva444P10Le
1263            | Self::Yuva444P12Le
1264            | Self::Yuva422P16Le
1265            | Self::Yuva444P16Le
1266            | Self::Yuva420P10Le
1267            | Self::Yuva420P12Le
1268            | Self::Yuva420P16Le
1269            | Self::Gbrp10Le
1270            | Self::Gbrap10Le
1271            | Self::Gbrp12Le
1272            | Self::Gbrap12Le
1273            | Self::Gbrp14Le
1274            | Self::Gbrap14Le
1275            | Self::Gbrp16Le
1276            | Self::Gbrap16Le => 2,
1277            // Packed multi-component: whole-pixel byte cost.
1278            Self::Ya8 => 2,
1279            Self::Rgb24 | Self::Bgr24 => 3,
1280            Self::Rgba
1281            | Self::Bgra
1282            | Self::Argb
1283            | Self::Abgr
1284            | Self::Cmyk
1285            | Self::CmykInverted
1286            | Self::Ya16Le => 4,
1287            Self::Rgb48Le => 6,
1288            Self::Rgba64Le => 8,
1289            // 32-bit float: one binary32 word per sample (packed
1290            // grayscale and the planar GBR(A) planes), or the
1291            // whole-pixel cost for packed multi-component float.
1292            Self::GrayF32Le | Self::GbrpF32Le | Self::GbrapF32Le => 4,
1293            Self::RgbF32Le => 12,
1294            Self::RgbaF32Le => 16,
1295        };
1296        pw.checked_mul(bytes_per_position)
1297    }
1298
1299    /// Tightly-packed byte size of plane `plane` for a `width` ×
1300    /// `height` picture:
1301    /// [`plane_row_bytes`](Self::plane_row_bytes) × the plane's row
1302    /// count from [`plane_dimensions`](Self::plane_dimensions).
1303    ///
1304    /// Returns `None` when `plane >= plane_count()` or the size
1305    /// overflows `usize`.
1306    pub fn plane_size_bytes(&self, plane: usize, width: u32, height: u32) -> Option<usize> {
1307        let (_, ph) = self.plane_dimensions(plane, width, height)?;
1308        self.plane_row_bytes(plane, width)?.checked_mul(ph as usize)
1309    }
1310
1311    /// Tightly-packed byte size of a whole `width` × `height` frame in
1312    /// this format — the sum of
1313    /// [`plane_size_bytes`](Self::plane_size_bytes) over every plane,
1314    /// with no stride padding or inter-plane alignment. Out-of-band
1315    /// side data (the `Pal8` palette table, significant-bits records)
1316    /// is not included.
1317    ///
1318    /// Returns `None` on `usize` overflow.
1319    ///
1320    /// ```
1321    /// use oxideav_core::PixelFormat;
1322    /// // 4:2:0 at 4×4: 16 luma + 4 + 4 chroma bytes.
1323    /// assert_eq!(PixelFormat::Yuv420P.frame_size_bytes(4, 4), Some(24));
1324    /// // 4:4:0 at 6×5: 30 luma + 2 × (6 × 3) chroma bytes.
1325    /// assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
1326    /// // Packed float RGBA: 16 bytes per pixel.
1327    /// assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
1328    /// ```
1329    pub fn frame_size_bytes(&self, width: u32, height: u32) -> Option<usize> {
1330        let mut total = 0usize;
1331        for plane in 0..self.plane_count() {
1332            total = total.checked_add(self.plane_size_bytes(plane, width, height)?)?;
1333        }
1334        Some(total)
1335    }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341
1342    /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
1343    /// stability commitment — the integer value of each variant is part of
1344    /// the public ABI. Any reorder, renumber, or removal will fail this test
1345    /// and the change MUST be a major version bump (or a fresh variant
1346    /// appended at a new number, leaving the existing ones untouched).
1347    #[test]
1348    fn implied_color_range_is_full_only_for_the_j_labels() {
1349        use crate::signal::ColorRange;
1350        for f in [
1351            PixelFormat::YuvJ420P,
1352            PixelFormat::YuvJ422P,
1353            PixelFormat::YuvJ444P,
1354        ] {
1355            assert_eq!(f.implied_color_range(), Some(ColorRange::Full), "{f:?}");
1356        }
1357        for f in [
1358            PixelFormat::Yuv420P,
1359            PixelFormat::Yuv444P,
1360            PixelFormat::Yuv420P10Le,
1361            PixelFormat::Yuv444P12Le,
1362            PixelFormat::Rgb24,
1363            PixelFormat::Rgba,
1364            PixelFormat::Gray8,
1365            PixelFormat::Gray16Le,
1366            PixelFormat::Nv12,
1367            PixelFormat::Pal8,
1368        ] {
1369            assert_eq!(f.implied_color_range(), None, "{f:?}");
1370        }
1371    }
1372
1373    #[test]
1374    fn pixel_format_discriminants_pinned() {
1375        assert_eq!(PixelFormat::Yuv420P as u16, 0);
1376        assert_eq!(PixelFormat::Yuv422P as u16, 1);
1377        assert_eq!(PixelFormat::Yuv444P as u16, 2);
1378        assert_eq!(PixelFormat::Rgb24 as u16, 3);
1379        assert_eq!(PixelFormat::Rgba as u16, 4);
1380        assert_eq!(PixelFormat::Gray8 as u16, 5);
1381        assert_eq!(PixelFormat::Pal8 as u16, 6);
1382        assert_eq!(PixelFormat::Bgr24 as u16, 7);
1383        assert_eq!(PixelFormat::Bgra as u16, 8);
1384        assert_eq!(PixelFormat::Argb as u16, 9);
1385        assert_eq!(PixelFormat::Abgr as u16, 10);
1386        assert_eq!(PixelFormat::Rgb48Le as u16, 11);
1387        assert_eq!(PixelFormat::Rgba64Le as u16, 12);
1388        assert_eq!(PixelFormat::Gray16Le as u16, 13);
1389        assert_eq!(PixelFormat::Gray10Le as u16, 14);
1390        assert_eq!(PixelFormat::Gray12Le as u16, 15);
1391        assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
1392        assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
1393        assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
1394        assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
1395        assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
1396        assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
1397        assert_eq!(PixelFormat::YuvJ420P as u16, 22);
1398        assert_eq!(PixelFormat::YuvJ422P as u16, 23);
1399        assert_eq!(PixelFormat::YuvJ444P as u16, 24);
1400        assert_eq!(PixelFormat::Nv12 as u16, 25);
1401        assert_eq!(PixelFormat::Nv21 as u16, 26);
1402        assert_eq!(PixelFormat::Ya8 as u16, 27);
1403        assert_eq!(PixelFormat::Yuva420P as u16, 28);
1404        assert_eq!(PixelFormat::MonoBlack as u16, 29);
1405        assert_eq!(PixelFormat::MonoWhite as u16, 30);
1406        assert_eq!(PixelFormat::Yuyv422 as u16, 31);
1407        assert_eq!(PixelFormat::Uyvy422 as u16, 32);
1408        assert_eq!(PixelFormat::Cmyk as u16, 33);
1409        assert_eq!(PixelFormat::Yuv411P as u16, 34);
1410        assert_eq!(PixelFormat::Gbrp10Le as u16, 35);
1411        assert_eq!(PixelFormat::Gbrap10Le as u16, 36);
1412        assert_eq!(PixelFormat::Gbrp12Le as u16, 37);
1413        assert_eq!(PixelFormat::Gbrap12Le as u16, 38);
1414        assert_eq!(PixelFormat::Gbrp14Le as u16, 39);
1415        assert_eq!(PixelFormat::Gbrap14Le as u16, 40);
1416        assert_eq!(PixelFormat::Yuv420P16Le as u16, 41);
1417        assert_eq!(PixelFormat::Yuv422P16Le as u16, 42);
1418        assert_eq!(PixelFormat::Yuv444P16Le as u16, 43);
1419        assert_eq!(PixelFormat::Yuva422P as u16, 44);
1420        assert_eq!(PixelFormat::Yuva444P as u16, 45);
1421        assert_eq!(PixelFormat::Yuva422P10Le as u16, 46);
1422        assert_eq!(PixelFormat::Yuva422P12Le as u16, 47);
1423        assert_eq!(PixelFormat::Yuva444P10Le as u16, 48);
1424        assert_eq!(PixelFormat::Yuva444P12Le as u16, 49);
1425        assert_eq!(PixelFormat::Yuva422P16Le as u16, 50);
1426        assert_eq!(PixelFormat::Yuva444P16Le as u16, 51);
1427        assert_eq!(PixelFormat::Gbrp8 as u16, 52);
1428        assert_eq!(PixelFormat::Gbrp16Le as u16, 53);
1429        assert_eq!(PixelFormat::Gbrap16Le as u16, 54);
1430        assert_eq!(PixelFormat::Yuva420P10Le as u16, 55);
1431        assert_eq!(PixelFormat::Yuva420P12Le as u16, 56);
1432        assert_eq!(PixelFormat::Yuva420P16Le as u16, 57);
1433        assert_eq!(PixelFormat::Gbrap8 as u16, 58);
1434        assert_eq!(PixelFormat::Ya16Le as u16, 59);
1435        assert_eq!(PixelFormat::CmykInverted as u16, 60);
1436        assert_eq!(PixelFormat::Yuv440P as u16, 61);
1437        assert_eq!(PixelFormat::Yuv440P10Le as u16, 62);
1438        assert_eq!(PixelFormat::Yuv440P12Le as u16, 63);
1439        assert_eq!(PixelFormat::Yuv440P16Le as u16, 64);
1440        assert_eq!(PixelFormat::GrayF32Le as u16, 65);
1441        assert_eq!(PixelFormat::RgbF32Le as u16, 66);
1442        assert_eq!(PixelFormat::RgbaF32Le as u16, 67);
1443        assert_eq!(PixelFormat::GbrpF32Le as u16, 68);
1444        assert_eq!(PixelFormat::GbrapF32Le as u16, 69);
1445    }
1446
1447    #[test]
1448    fn sample_format_discriminants_pinned() {
1449        assert_eq!(SampleFormat::U8 as u8, 0);
1450        assert_eq!(SampleFormat::S8 as u8, 1);
1451        assert_eq!(SampleFormat::S16 as u8, 2);
1452        assert_eq!(SampleFormat::S24 as u8, 3);
1453        assert_eq!(SampleFormat::S32 as u8, 4);
1454        assert_eq!(SampleFormat::F32 as u8, 5);
1455        assert_eq!(SampleFormat::F64 as u8, 6);
1456        assert_eq!(SampleFormat::U8P as u8, 7);
1457        assert_eq!(SampleFormat::S16P as u8, 8);
1458        assert_eq!(SampleFormat::S32P as u8, 9);
1459        assert_eq!(SampleFormat::F32P as u8, 10);
1460        assert_eq!(SampleFormat::F64P as u8, 11);
1461    }
1462
1463    #[test]
1464    fn high_bit_yuv_planar_metadata() {
1465        // 10-bit reference variants are planar with three planes.
1466        assert!(PixelFormat::Yuv420P10Le.is_planar());
1467        assert!(PixelFormat::Yuv422P10Le.is_planar());
1468        assert!(PixelFormat::Yuv444P10Le.is_planar());
1469
1470        // 12-bit variants must follow the same shape.
1471        assert!(PixelFormat::Yuv420P12Le.is_planar());
1472        assert!(PixelFormat::Yuv422P12Le.is_planar());
1473        assert!(PixelFormat::Yuv444P12Le.is_planar());
1474
1475        assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
1476        assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
1477        assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
1478
1479        // 16-bit variants must follow the same shape.
1480        for fmt in [
1481            PixelFormat::Yuv420P16Le,
1482            PixelFormat::Yuv422P16Le,
1483            PixelFormat::Yuv444P16Le,
1484        ] {
1485            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1486            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1487        }
1488
1489        // None of the high-bit YUV variants carry alpha or palette.
1490        assert!(!PixelFormat::Yuv422P12Le.has_alpha());
1491        assert!(!PixelFormat::Yuv444P12Le.has_alpha());
1492        assert!(!PixelFormat::Yuv422P12Le.is_palette());
1493        assert!(!PixelFormat::Yuv444P12Le.is_palette());
1494        assert!(!PixelFormat::Yuv420P16Le.has_alpha());
1495        assert!(!PixelFormat::Yuv422P16Le.has_alpha());
1496        assert!(!PixelFormat::Yuv444P16Le.has_alpha());
1497        assert!(!PixelFormat::Yuv420P16Le.is_palette());
1498        assert!(!PixelFormat::Yuv422P16Le.is_palette());
1499        assert!(!PixelFormat::Yuv444P16Le.is_palette());
1500    }
1501
1502    #[test]
1503    fn channel_layout_round_trip_count_for_known_layouts() {
1504        // For every `n` that `from_count` maps to a named layout, the
1505        // resulting layout's `channel_count()` must equal `n` again.
1506        for n in 1..=8u16 {
1507            let layout = ChannelLayout::from_count(n);
1508            assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
1509            // None of these defaults should fall through to DiscreteN.
1510            assert!(
1511                !matches!(layout, ChannelLayout::DiscreteN(_)),
1512                "from_count({n}) unexpectedly produced DiscreteN"
1513            );
1514        }
1515    }
1516
1517    #[test]
1518    fn channel_layout_from_count_default_table() {
1519        // The exact mapping documented on `from_count` — pin it so
1520        // future refactors don't silently change the inferred layout.
1521        assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
1522        assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
1523        assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
1524        assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
1525        assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
1526        assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
1527        assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
1528        assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
1529    }
1530
1531    #[test]
1532    fn channel_layout_unknown_count_falls_through_to_discrete() {
1533        assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
1534        assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
1535        assert_eq!(
1536            ChannelLayout::from_count(64).channel_count(),
1537            64,
1538            "DiscreteN must report the count it was constructed with"
1539        );
1540    }
1541
1542    #[test]
1543    fn channel_layout_position_lookup() {
1544        assert_eq!(
1545            ChannelLayout::Stereo.position(0),
1546            Some(ChannelPosition::FrontLeft)
1547        );
1548        assert_eq!(
1549            ChannelLayout::Stereo.position(1),
1550            Some(ChannelPosition::FrontRight)
1551        );
1552        assert_eq!(ChannelLayout::Stereo.position(2), None);
1553
1554        // 5.1 canonical: L, R, C, LFE, Ls, Rs.
1555        let s51 = ChannelLayout::Surround51;
1556        assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
1557        assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
1558        assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
1559        assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
1560        assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
1561        assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
1562        assert_eq!(s51.position(6), None);
1563
1564        // DiscreteN never reveals a position.
1565        assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
1566    }
1567
1568    #[test]
1569    fn channel_layout_lfe_and_surround_predicates() {
1570        assert!(ChannelLayout::Surround51.has_lfe());
1571        assert!(ChannelLayout::Surround71.has_lfe());
1572        assert!(ChannelLayout::Stereo21.has_lfe());
1573        assert!(!ChannelLayout::Quad.has_lfe());
1574        assert!(!ChannelLayout::Surround50.has_lfe());
1575        assert!(!ChannelLayout::Stereo.has_lfe());
1576
1577        assert!(!ChannelLayout::Mono.is_surround());
1578        assert!(!ChannelLayout::Stereo.is_surround());
1579        // Downmix carriers are still 2ch / no-LFE → not "surround" by
1580        // the layout-shape definition; the surround info lives in the
1581        // sample matrix itself.
1582        assert!(!ChannelLayout::LoRo.is_surround());
1583        assert!(!ChannelLayout::LtRt.is_surround());
1584        assert!(ChannelLayout::Stereo21.is_surround());
1585        assert!(ChannelLayout::Surround51.is_surround());
1586        assert!(ChannelLayout::Surround71.is_surround());
1587    }
1588
1589    #[test]
1590    fn channel_layout_display_and_fromstr_round_trip() {
1591        use std::str::FromStr;
1592        let cases = [
1593            ChannelLayout::Mono,
1594            ChannelLayout::Stereo,
1595            ChannelLayout::Stereo21,
1596            ChannelLayout::Surround30,
1597            ChannelLayout::Quad,
1598            ChannelLayout::Surround40,
1599            ChannelLayout::Surround41,
1600            ChannelLayout::Surround50,
1601            ChannelLayout::Surround51,
1602            ChannelLayout::Surround60,
1603            ChannelLayout::Surround61,
1604            ChannelLayout::Surround70,
1605            ChannelLayout::Surround71,
1606            ChannelLayout::LoRo,
1607            ChannelLayout::LtRt,
1608            ChannelLayout::DiscreteN(13),
1609        ];
1610        for layout in cases {
1611            let s = layout.to_string();
1612            let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
1613            assert_eq!(parsed, layout, "round-trip failed via {s:?}");
1614        }
1615    }
1616
1617    #[test]
1618    fn channel_layout_fromstr_accepts_aliases_and_case() {
1619        use std::str::FromStr;
1620        assert_eq!(
1621            ChannelLayout::from_str("STEREO").unwrap(),
1622            ChannelLayout::Stereo
1623        );
1624        assert_eq!(
1625            ChannelLayout::from_str("2.0").unwrap(),
1626            ChannelLayout::Stereo
1627        );
1628        assert_eq!(
1629            ChannelLayout::from_str("5.1").unwrap(),
1630            ChannelLayout::Surround51
1631        );
1632        assert_eq!(
1633            ChannelLayout::from_str("Lo/Ro").unwrap(),
1634            ChannelLayout::LoRo
1635        );
1636        assert_eq!(
1637            ChannelLayout::from_str("lt/rt").unwrap(),
1638            ChannelLayout::LtRt
1639        );
1640        assert!(ChannelLayout::from_str("absurd_layout").is_err());
1641    }
1642
1643    #[test]
1644    fn channel_layout_positions_owned_matches_static_slice() {
1645        for layout in [
1646            ChannelLayout::Mono,
1647            ChannelLayout::Surround51,
1648            ChannelLayout::Surround71,
1649        ] {
1650            assert_eq!(layout.positions_owned(), layout.positions());
1651        }
1652        // DiscreteN returns an empty owned vec — positions are unknown.
1653        assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
1654    }
1655
1656    #[test]
1657    fn sample_format_plane_count_interleaved_is_one() {
1658        // Interleaved formats always pack into a single plane, regardless
1659        // of channel count.
1660        for ch in [1u16, 2, 6, 8, 64, 0] {
1661            assert_eq!(SampleFormat::S16.plane_count(ch), 1);
1662            assert_eq!(SampleFormat::F32.plane_count(ch), 1);
1663            assert_eq!(SampleFormat::U8.plane_count(ch), 1);
1664            assert_eq!(SampleFormat::S24.plane_count(ch), 1);
1665        }
1666    }
1667
1668    #[test]
1669    fn sample_format_plane_count_planar_matches_channels() {
1670        // Planar formats use one plane per channel.
1671        assert_eq!(SampleFormat::S16P.plane_count(1), 1);
1672        assert_eq!(SampleFormat::S16P.plane_count(2), 2);
1673        assert_eq!(SampleFormat::F32P.plane_count(6), 6);
1674        assert_eq!(SampleFormat::F64P.plane_count(8), 8);
1675
1676        // Edge case: zero channels in a planar format yields zero planes.
1677        assert_eq!(SampleFormat::S32P.plane_count(0), 0);
1678    }
1679
1680    #[test]
1681    fn high_bit_yuv_bits_per_pixel_approx() {
1682        // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
1683        // packed-bits estimator (the approximation reports samples-per-pixel
1684        // density, not the 16-bit storage width).
1685        assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
1686        assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
1687        assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
1688        assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
1689        assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
1690
1691        // 16-bit: packed bits == storage bits (every bit of the 16-bit
1692        // word is significant), so the estimator lands on the same
1693        // numbers as the 10/12-bit siblings.
1694        assert_eq!(PixelFormat::Yuv420P16Le.bits_per_pixel_approx(), 24);
1695        assert_eq!(PixelFormat::Yuv422P16Le.bits_per_pixel_approx(), 32);
1696        assert_eq!(PixelFormat::Yuv444P16Le.bits_per_pixel_approx(), 48);
1697    }
1698
1699    #[test]
1700    fn yuva_planar_metadata() {
1701        // All three alpha-carrying planar YUV samplings share one shape:
1702        // planar, 4 planes (Y, U, V, full-resolution A), alpha set, not
1703        // a palette format.
1704        for fmt in [
1705            PixelFormat::Yuva420P,
1706            PixelFormat::Yuva422P,
1707            PixelFormat::Yuva444P,
1708        ] {
1709            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1710            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1711            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1712            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1713        }
1714
1715        // Packed-bits estimator: the alpha plane adds a full 8 bits per
1716        // pixel on top of the alpha-less sampling's density.
1717        assert_eq!(
1718            PixelFormat::Yuva420P.bits_per_pixel_approx(),
1719            PixelFormat::Yuv420P.bits_per_pixel_approx() + 8
1720        );
1721        assert_eq!(
1722            PixelFormat::Yuva422P.bits_per_pixel_approx(),
1723            PixelFormat::Yuv422P.bits_per_pixel_approx() + 8
1724        );
1725        assert_eq!(
1726            PixelFormat::Yuva444P.bits_per_pixel_approx(),
1727            PixelFormat::Yuv444P.bits_per_pixel_approx() + 8
1728        );
1729        assert_eq!(PixelFormat::Yuva422P.bits_per_pixel_approx(), 24);
1730        assert_eq!(PixelFormat::Yuva444P.bits_per_pixel_approx(), 32);
1731    }
1732
1733    #[test]
1734    fn deep_yuva_planar_metadata() {
1735        // All six deep alpha-carrying variants share one shape: planar,
1736        // 4 planes (Y, U, V, full-resolution A), alpha set, no palette.
1737        for fmt in [
1738            PixelFormat::Yuva422P10Le,
1739            PixelFormat::Yuva422P12Le,
1740            PixelFormat::Yuva444P10Le,
1741            PixelFormat::Yuva444P12Le,
1742            PixelFormat::Yuva422P16Le,
1743            PixelFormat::Yuva444P16Le,
1744        ] {
1745            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1746            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1747            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1748            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1749        }
1750    }
1751
1752    #[test]
1753    fn deep_yuva_bits_per_pixel_approx() {
1754        // Estimator reports 16-bit-word storage cost, matching the
1755        // alpha-less deep YUV trio: the full-resolution alpha word adds
1756        // 16 on top of the alpha-less sampling's number.
1757        for fmt in [
1758            PixelFormat::Yuva422P10Le,
1759            PixelFormat::Yuva422P12Le,
1760            PixelFormat::Yuva422P16Le,
1761        ] {
1762            assert_eq!(fmt.bits_per_pixel_approx(), 48, "{fmt:?}");
1763        }
1764        for fmt in [
1765            PixelFormat::Yuva444P10Le,
1766            PixelFormat::Yuva444P12Le,
1767            PixelFormat::Yuva444P16Le,
1768        ] {
1769            assert_eq!(fmt.bits_per_pixel_approx(), 64, "{fmt:?}");
1770        }
1771        assert_eq!(
1772            PixelFormat::Yuva422P16Le.bits_per_pixel_approx(),
1773            PixelFormat::Yuv422P16Le.bits_per_pixel_approx() + 16
1774        );
1775        assert_eq!(
1776            PixelFormat::Yuva444P16Le.bits_per_pixel_approx(),
1777            PixelFormat::Yuv444P16Le.bits_per_pixel_approx() + 16
1778        );
1779        assert_eq!(
1780            PixelFormat::Yuva422P10Le.bits_per_pixel_approx(),
1781            PixelFormat::Yuv422P10Le.bits_per_pixel_approx() + 16
1782        );
1783        assert_eq!(
1784            PixelFormat::Yuva444P12Le.bits_per_pixel_approx(),
1785            PixelFormat::Yuv444P12Le.bits_per_pixel_approx() + 16
1786        );
1787    }
1788
1789    #[test]
1790    fn high_bit_gbr_planar_metadata() {
1791        // All six new variants are planar with the right plane count.
1792        for fmt in [
1793            PixelFormat::Gbrp10Le,
1794            PixelFormat::Gbrp12Le,
1795            PixelFormat::Gbrp14Le,
1796        ] {
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        for fmt in [
1803            PixelFormat::Gbrap10Le,
1804            PixelFormat::Gbrap12Le,
1805            PixelFormat::Gbrap14Le,
1806        ] {
1807            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1808            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1809            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1810            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1811        }
1812    }
1813
1814    #[test]
1815    fn high_bit_gbr_bits_per_pixel_approx() {
1816        // Packed bits-per-pixel = samples × bits (consistent with how
1817        // the 10/12-bit YUV variants are reported above).
1818        assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30);
1819        assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40);
1820        assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36);
1821        assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48);
1822        assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42);
1823        assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56);
1824    }
1825
1826    #[test]
1827    fn high_bit_gbr_constructible_and_distinct() {
1828        // Round-trip the discriminant through `as u16` and back via the
1829        // pinning test's reverse mapping — every variant must be unique.
1830        let all = [
1831            PixelFormat::Gbrp10Le,
1832            PixelFormat::Gbrap10Le,
1833            PixelFormat::Gbrp12Le,
1834            PixelFormat::Gbrap12Le,
1835            PixelFormat::Gbrp14Le,
1836            PixelFormat::Gbrap14Le,
1837            PixelFormat::Gbrp8,
1838            PixelFormat::Gbrap8,
1839            PixelFormat::Gbrp16Le,
1840            PixelFormat::Gbrap16Le,
1841        ];
1842        let mut seen = std::collections::HashSet::new();
1843        for fmt in all {
1844            assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
1845        }
1846    }
1847
1848    #[test]
1849    fn gbr_depth_ladder_ends_metadata() {
1850        // Gbrp8 and the 16-bit pair share the family shape: planar,
1851        // G/B/R plane order (3 planes), alpha only on Gbrap16Le, never
1852        // palette.
1853        for fmt in [PixelFormat::Gbrp8, PixelFormat::Gbrp16Le] {
1854            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1855            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1856            assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1857            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1858        }
1859        assert!(PixelFormat::Gbrap16Le.is_planar());
1860        assert_eq!(PixelFormat::Gbrap16Le.plane_count(), 4);
1861        assert!(PixelFormat::Gbrap16Le.has_alpha());
1862        assert!(!PixelFormat::Gbrap16Le.is_palette());
1863    }
1864
1865    #[test]
1866    fn gbr_depth_ladder_ends_bits_per_pixel_approx() {
1867        // Gbrp8 matches the packed 8-bit RGB density (planar layout
1868        // doesn't change bits-per-pixel), and the 16-bit pair matches
1869        // the packed 16-bit RGB(A) densities — for 16-bit words packed
1870        // bits equal storage bits.
1871        assert_eq!(
1872            PixelFormat::Gbrp8.bits_per_pixel_approx(),
1873            PixelFormat::Rgb24.bits_per_pixel_approx()
1874        );
1875        assert_eq!(
1876            PixelFormat::Gbrp16Le.bits_per_pixel_approx(),
1877            PixelFormat::Rgb48Le.bits_per_pixel_approx()
1878        );
1879        assert_eq!(
1880            PixelFormat::Gbrap16Le.bits_per_pixel_approx(),
1881            PixelFormat::Rgba64Le.bits_per_pixel_approx()
1882        );
1883        assert_eq!(PixelFormat::Gbrp8.bits_per_pixel_approx(), 24);
1884        assert_eq!(PixelFormat::Gbrp16Le.bits_per_pixel_approx(), 48);
1885        assert_eq!(PixelFormat::Gbrap16Le.bits_per_pixel_approx(), 64);
1886    }
1887
1888    #[test]
1889    fn gbrap8_metadata() {
1890        // Gbrap8 completes the GBR(A) family: every depth on the
1891        // 8/10/12/14/16 ladder now has both an alpha-less and an
1892        // alpha-carrying variant. Shape matches the rest of the
1893        // alpha-carrying family: planar, 4 planes (G, B, R,
1894        // full-resolution A), alpha set, never palette.
1895        let fmt = PixelFormat::Gbrap8;
1896        assert!(fmt.is_planar());
1897        assert_eq!(fmt.plane_count(), 4);
1898        assert!(fmt.has_alpha());
1899        assert!(!fmt.is_palette());
1900    }
1901
1902    #[test]
1903    fn gbrap8_bits_per_pixel_approx() {
1904        // Four bytes per pixel: the packed Rgba density (planar layout
1905        // doesn't change bits-per-pixel), i.e. the alpha plane adds a
1906        // full 8 bits on top of Gbrp8.
1907        assert_eq!(PixelFormat::Gbrap8.bits_per_pixel_approx(), 32);
1908        assert_eq!(
1909            PixelFormat::Gbrap8.bits_per_pixel_approx(),
1910            PixelFormat::Rgba.bits_per_pixel_approx()
1911        );
1912        assert_eq!(
1913            PixelFormat::Gbrap8.bits_per_pixel_approx(),
1914            PixelFormat::Gbrp8.bits_per_pixel_approx() + 8
1915        );
1916    }
1917
1918    #[test]
1919    fn gbr_family_alpha_ladder_complete() {
1920        // Every GBR depth has an alpha companion with exactly one more
1921        // plane and the same planarity — the asymmetry Gbrap8 closed.
1922        let pairs = [
1923            (PixelFormat::Gbrp8, PixelFormat::Gbrap8),
1924            (PixelFormat::Gbrp10Le, PixelFormat::Gbrap10Le),
1925            (PixelFormat::Gbrp12Le, PixelFormat::Gbrap12Le),
1926            (PixelFormat::Gbrp14Le, PixelFormat::Gbrap14Le),
1927            (PixelFormat::Gbrp16Le, PixelFormat::Gbrap16Le),
1928        ];
1929        for (gbr, gbra) in pairs {
1930            assert!(gbr.is_planar() && gbra.is_planar());
1931            assert_eq!(gbr.plane_count(), 3, "{gbr:?}");
1932            assert_eq!(gbra.plane_count(), 4, "{gbra:?}");
1933            assert!(!gbr.has_alpha(), "{gbr:?}");
1934            assert!(gbra.has_alpha(), "{gbra:?}");
1935        }
1936    }
1937
1938    #[test]
1939    fn ya16le_metadata() {
1940        // Same packed shape as Ya8 (interleaved Y, A in one plane),
1941        // widened to 16-bit LE words: not planar, single plane, alpha
1942        // set, never palette. Density is exactly double Ya8's and
1943        // matches half of Rgba64Le (two components instead of four).
1944        let fmt = PixelFormat::Ya16Le;
1945        assert!(!fmt.is_planar());
1946        assert_eq!(fmt.plane_count(), 1);
1947        assert!(fmt.has_alpha());
1948        assert!(!fmt.is_palette());
1949        assert_eq!(fmt.bits_per_pixel_approx(), 32);
1950        assert_eq!(
1951            fmt.bits_per_pixel_approx(),
1952            PixelFormat::Ya8.bits_per_pixel_approx() * 2
1953        );
1954        assert_eq!(
1955            fmt.bits_per_pixel_approx(),
1956            PixelFormat::Rgba64Le.bits_per_pixel_approx() / 2
1957        );
1958        // The alpha word adds a full 16 bits on top of Gray16Le.
1959        assert_eq!(
1960            fmt.bits_per_pixel_approx(),
1961            PixelFormat::Gray16Le.bits_per_pixel_approx() + 16
1962        );
1963    }
1964
1965    #[test]
1966    fn cmyk_inverted_metadata() {
1967        // The inverted-ink convention changes sample semantics, not
1968        // layout: CmykInverted must be metadata-identical to Cmyk on
1969        // every shape predicate.
1970        let (reg, inv) = (PixelFormat::Cmyk, PixelFormat::CmykInverted);
1971        for fmt in [reg, inv] {
1972            assert!(!fmt.is_planar(), "{fmt:?}");
1973            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
1974            assert!(!fmt.has_alpha(), "{fmt:?}");
1975            assert!(!fmt.is_palette(), "{fmt:?}");
1976        }
1977        assert_eq!(reg.bits_per_pixel_approx(), inv.bits_per_pixel_approx());
1978        assert_eq!(inv.bits_per_pixel_approx(), 32);
1979        // They remain distinct formats on the wire-stable axis.
1980        assert_ne!(reg as u16, inv as u16);
1981    }
1982
1983    #[test]
1984    fn deep_yuva420_planar_metadata() {
1985        // The 4:2:0 completions share the deep-Yuva shape: planar, 4
1986        // planes (Y, U, V, full-resolution A), alpha set, no palette.
1987        for fmt in [
1988            PixelFormat::Yuva420P10Le,
1989            PixelFormat::Yuva420P12Le,
1990            PixelFormat::Yuva420P16Le,
1991        ] {
1992            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1993            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1994            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1995            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1996        }
1997    }
1998
1999    #[test]
2000    fn deep_yuva420_bits_per_pixel_approx() {
2001        // Same estimator convention as the 4:2:2/4:4:4 deep Yuva arms:
2002        // 16-bit-word storage cost, with the full-resolution alpha word
2003        // adding 16 on top of the alpha-less sampling's number.
2004        for fmt in [
2005            PixelFormat::Yuva420P10Le,
2006            PixelFormat::Yuva420P12Le,
2007            PixelFormat::Yuva420P16Le,
2008        ] {
2009            assert_eq!(fmt.bits_per_pixel_approx(), 40, "{fmt:?}");
2010        }
2011        assert_eq!(
2012            PixelFormat::Yuva420P10Le.bits_per_pixel_approx(),
2013            PixelFormat::Yuv420P10Le.bits_per_pixel_approx() + 16
2014        );
2015        assert_eq!(
2016            PixelFormat::Yuva420P12Le.bits_per_pixel_approx(),
2017            PixelFormat::Yuv420P12Le.bits_per_pixel_approx() + 16
2018        );
2019        assert_eq!(
2020            PixelFormat::Yuva420P16Le.bits_per_pixel_approx(),
2021            PixelFormat::Yuv420P16Le.bits_per_pixel_approx() + 16
2022        );
2023    }
2024
2025    /// Every `PixelFormat` variant, in discriminant order. Extend this
2026    /// list whenever a variant is appended — the consistency tests
2027    /// below sweep it.
2028    const ALL_PIXEL_FORMATS: [PixelFormat; 70] = [
2029        PixelFormat::Yuv420P,
2030        PixelFormat::Yuv422P,
2031        PixelFormat::Yuv444P,
2032        PixelFormat::Rgb24,
2033        PixelFormat::Rgba,
2034        PixelFormat::Gray8,
2035        PixelFormat::Pal8,
2036        PixelFormat::Bgr24,
2037        PixelFormat::Bgra,
2038        PixelFormat::Argb,
2039        PixelFormat::Abgr,
2040        PixelFormat::Rgb48Le,
2041        PixelFormat::Rgba64Le,
2042        PixelFormat::Gray16Le,
2043        PixelFormat::Gray10Le,
2044        PixelFormat::Gray12Le,
2045        PixelFormat::Yuv420P10Le,
2046        PixelFormat::Yuv422P10Le,
2047        PixelFormat::Yuv444P10Le,
2048        PixelFormat::Yuv420P12Le,
2049        PixelFormat::Yuv422P12Le,
2050        PixelFormat::Yuv444P12Le,
2051        PixelFormat::YuvJ420P,
2052        PixelFormat::YuvJ422P,
2053        PixelFormat::YuvJ444P,
2054        PixelFormat::Nv12,
2055        PixelFormat::Nv21,
2056        PixelFormat::Ya8,
2057        PixelFormat::Yuva420P,
2058        PixelFormat::MonoBlack,
2059        PixelFormat::MonoWhite,
2060        PixelFormat::Yuyv422,
2061        PixelFormat::Uyvy422,
2062        PixelFormat::Cmyk,
2063        PixelFormat::Yuv411P,
2064        PixelFormat::Gbrp10Le,
2065        PixelFormat::Gbrap10Le,
2066        PixelFormat::Gbrp12Le,
2067        PixelFormat::Gbrap12Le,
2068        PixelFormat::Gbrp14Le,
2069        PixelFormat::Gbrap14Le,
2070        PixelFormat::Yuv420P16Le,
2071        PixelFormat::Yuv422P16Le,
2072        PixelFormat::Yuv444P16Le,
2073        PixelFormat::Yuva422P,
2074        PixelFormat::Yuva444P,
2075        PixelFormat::Yuva422P10Le,
2076        PixelFormat::Yuva422P12Le,
2077        PixelFormat::Yuva444P10Le,
2078        PixelFormat::Yuva444P12Le,
2079        PixelFormat::Yuva422P16Le,
2080        PixelFormat::Yuva444P16Le,
2081        PixelFormat::Gbrp8,
2082        PixelFormat::Gbrp16Le,
2083        PixelFormat::Gbrap16Le,
2084        PixelFormat::Yuva420P10Le,
2085        PixelFormat::Yuva420P12Le,
2086        PixelFormat::Yuva420P16Le,
2087        PixelFormat::Gbrap8,
2088        PixelFormat::Ya16Le,
2089        PixelFormat::CmykInverted,
2090        PixelFormat::Yuv440P,
2091        PixelFormat::Yuv440P10Le,
2092        PixelFormat::Yuv440P12Le,
2093        PixelFormat::Yuv440P16Le,
2094        PixelFormat::GrayF32Le,
2095        PixelFormat::RgbF32Le,
2096        PixelFormat::RgbaF32Le,
2097        PixelFormat::GbrpF32Le,
2098        PixelFormat::GbrapF32Le,
2099    ];
2100
2101    #[test]
2102    fn all_pixel_formats_list_is_complete_and_distinct() {
2103        // The list is discriminant-ordered and dense: 0..70 with no
2104        // gaps and no duplicates. A newly appended variant that isn't
2105        // added to the list will break the length or density check.
2106        let mut seen = std::collections::HashSet::new();
2107        for fmt in ALL_PIXEL_FORMATS {
2108            assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
2109        }
2110        for d in 0..ALL_PIXEL_FORMATS.len() as u16 {
2111            assert!(seen.contains(&d), "discriminant {d} missing from list");
2112        }
2113    }
2114
2115    #[test]
2116    fn yuv440_family_metadata() {
2117        // The whole 4:4:0 ladder shares one shape: planar, 3 planes,
2118        // no alpha, no palette, full-width half-height chroma.
2119        for fmt in [
2120            PixelFormat::Yuv440P,
2121            PixelFormat::Yuv440P10Le,
2122            PixelFormat::Yuv440P12Le,
2123            PixelFormat::Yuv440P16Le,
2124        ] {
2125            assert!(fmt.is_planar(), "{fmt:?} must be planar");
2126            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
2127            assert!(!fmt.has_alpha(), "{fmt:?} must not carry alpha");
2128            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
2129            assert!(!fmt.is_float(), "{fmt:?} must not be float");
2130            assert_eq!(
2131                fmt.chroma_subsampling(),
2132                Some((0, 1)),
2133                "{fmt:?} must be full-width, half-height chroma"
2134            );
2135        }
2136    }
2137
2138    #[test]
2139    fn yuv440_bits_per_pixel_approx() {
2140        // 4:4:0 packs the same samples-per-pixel as 4:2:2 at every
2141        // depth (2 samples/pixel), so the estimator numbers coincide.
2142        assert_eq!(
2143            PixelFormat::Yuv440P.bits_per_pixel_approx(),
2144            PixelFormat::Yuv422P.bits_per_pixel_approx()
2145        );
2146        assert_eq!(PixelFormat::Yuv440P.bits_per_pixel_approx(), 16);
2147        for (f440, f422) in [
2148            (PixelFormat::Yuv440P10Le, PixelFormat::Yuv422P10Le),
2149            (PixelFormat::Yuv440P12Le, PixelFormat::Yuv422P12Le),
2150            (PixelFormat::Yuv440P16Le, PixelFormat::Yuv422P16Le),
2151        ] {
2152            assert_eq!(
2153                f440.bits_per_pixel_approx(),
2154                f422.bits_per_pixel_approx(),
2155                "{f440:?}"
2156            );
2157            assert_eq!(f440.bits_per_pixel_approx(), 32, "{f440:?}");
2158        }
2159    }
2160
2161    #[test]
2162    fn yuv440_plane_geometry() {
2163        // Even sizes: chroma keeps the width, halves the height.
2164        assert_eq!(
2165            PixelFormat::Yuv440P.plane_dimensions(0, 640, 480),
2166            Some((640, 480))
2167        );
2168        assert_eq!(
2169            PixelFormat::Yuv440P.plane_dimensions(1, 640, 480),
2170            Some((640, 240))
2171        );
2172        assert_eq!(
2173            PixelFormat::Yuv440P.plane_dimensions(2, 640, 480),
2174            Some((640, 240))
2175        );
2176        // Odd height rounds up; odd width is untouched (ssx = 0).
2177        for fmt in [
2178            PixelFormat::Yuv440P,
2179            PixelFormat::Yuv440P10Le,
2180            PixelFormat::Yuv440P12Le,
2181            PixelFormat::Yuv440P16Le,
2182        ] {
2183            assert_eq!(fmt.plane_dimensions(0, 7, 5), Some((7, 5)), "{fmt:?}");
2184            assert_eq!(fmt.plane_dimensions(1, 7, 5), Some((7, 3)), "{fmt:?}");
2185            assert_eq!(fmt.plane_dimensions(2, 7, 5), Some((7, 3)), "{fmt:?}");
2186            assert_eq!(fmt.plane_dimensions(3, 7, 5), None, "{fmt:?}");
2187        }
2188        // Degenerate 1-row picture: the chroma plane still has a row.
2189        assert_eq!(PixelFormat::Yuv440P.plane_dimensions(1, 3, 1), Some((3, 1)));
2190    }
2191
2192    #[test]
2193    fn yuv440_sizing_round_trips() {
2194        // 6×5 8-bit: luma 6×5 = 30, each chroma 6×ceil(5/2) = 18.
2195        assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(0, 6, 5), Some(30));
2196        assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(1, 6, 5), Some(18));
2197        assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(2, 6, 5), Some(18));
2198        assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
2199        // 7×5: 35 + 21 + 21.
2200        assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(7, 5), Some(77));
2201        // Deep variants store 16-bit words: exactly double at every
2202        // depth (row bytes = width × 2 regardless of valid bits).
2203        for fmt in [
2204            PixelFormat::Yuv440P10Le,
2205            PixelFormat::Yuv440P12Le,
2206            PixelFormat::Yuv440P16Le,
2207        ] {
2208            assert_eq!(fmt.plane_row_bytes(0, 7), Some(14), "{fmt:?}");
2209            assert_eq!(fmt.plane_row_bytes(1, 7), Some(14), "{fmt:?}");
2210            assert_eq!(fmt.frame_size_bytes(7, 5), Some(154), "{fmt:?}");
2211        }
2212    }
2213
2214    #[test]
2215    fn float_family_metadata() {
2216        // Packed trio: single plane, not planar.
2217        for fmt in [
2218            PixelFormat::GrayF32Le,
2219            PixelFormat::RgbF32Le,
2220            PixelFormat::RgbaF32Le,
2221        ] {
2222            assert!(!fmt.is_planar(), "{fmt:?}");
2223            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2224        }
2225        // Planar pair: GBR(A) shape.
2226        assert!(PixelFormat::GbrpF32Le.is_planar());
2227        assert_eq!(PixelFormat::GbrpF32Le.plane_count(), 3);
2228        assert!(PixelFormat::GbrapF32Le.is_planar());
2229        assert_eq!(PixelFormat::GbrapF32Le.plane_count(), 4);
2230        // Alpha only on the RGBA/GBRA members.
2231        assert!(!PixelFormat::GrayF32Le.has_alpha());
2232        assert!(!PixelFormat::RgbF32Le.has_alpha());
2233        assert!(PixelFormat::RgbaF32Le.has_alpha());
2234        assert!(!PixelFormat::GbrpF32Le.has_alpha());
2235        assert!(PixelFormat::GbrapF32Le.has_alpha());
2236        // The whole family is float, non-palette, and has no chroma
2237        // grid.
2238        for fmt in [
2239            PixelFormat::GrayF32Le,
2240            PixelFormat::RgbF32Le,
2241            PixelFormat::RgbaF32Le,
2242            PixelFormat::GbrpF32Le,
2243            PixelFormat::GbrapF32Le,
2244        ] {
2245            assert!(fmt.is_float(), "{fmt:?} must be float");
2246            assert!(!fmt.is_palette(), "{fmt:?}");
2247            assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2248        }
2249    }
2250
2251    #[test]
2252    fn is_float_false_for_integer_formats() {
2253        for fmt in ALL_PIXEL_FORMATS {
2254            let expect = matches!(
2255                fmt,
2256                PixelFormat::GrayF32Le
2257                    | PixelFormat::RgbF32Le
2258                    | PixelFormat::RgbaF32Le
2259                    | PixelFormat::GbrpF32Le
2260                    | PixelFormat::GbrapF32Le
2261            );
2262            assert_eq!(fmt.is_float(), expect, "{fmt:?}");
2263        }
2264    }
2265
2266    #[test]
2267    fn float_family_bits_per_pixel_and_sizing() {
2268        // Packed bits equal storage bits: every sample is a full
2269        // binary32 word.
2270        assert_eq!(PixelFormat::GrayF32Le.bits_per_pixel_approx(), 32);
2271        assert_eq!(PixelFormat::RgbF32Le.bits_per_pixel_approx(), 96);
2272        assert_eq!(PixelFormat::RgbaF32Le.bits_per_pixel_approx(), 128);
2273        assert_eq!(PixelFormat::GbrpF32Le.bits_per_pixel_approx(), 96);
2274        assert_eq!(PixelFormat::GbrapF32Le.bits_per_pixel_approx(), 128);
2275        // Packed row/frame sizes.
2276        assert_eq!(PixelFormat::GrayF32Le.plane_row_bytes(0, 3), Some(12));
2277        assert_eq!(PixelFormat::GrayF32Le.frame_size_bytes(5, 3), Some(60));
2278        assert_eq!(PixelFormat::RgbF32Le.plane_row_bytes(0, 7), Some(84));
2279        assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
2280        // Planar float: 4 bytes per sample on every plane; the packed
2281        // and planar layouts of the same component set cost the same.
2282        assert_eq!(PixelFormat::GbrpF32Le.plane_row_bytes(1, 7), Some(28));
2283        assert_eq!(
2284            PixelFormat::GbrpF32Le.frame_size_bytes(7, 5),
2285            PixelFormat::RgbF32Le.frame_size_bytes(7, 5)
2286        );
2287        assert_eq!(
2288            PixelFormat::GbrapF32Le.frame_size_bytes(7, 5),
2289            PixelFormat::RgbaF32Le.frame_size_bytes(7, 5)
2290        );
2291        // All planes of planar float GBR(A) are full resolution.
2292        for plane in 0..4 {
2293            assert_eq!(
2294                PixelFormat::GbrapF32Le.plane_dimensions(plane, 7, 5),
2295                Some((7, 5))
2296            );
2297        }
2298    }
2299
2300    #[test]
2301    fn chroma_subsampling_table() {
2302        use PixelFormat::*;
2303        // One representative per sampling class plus the full new
2304        // family; the wildcard class returns None.
2305        assert_eq!(Yuv420P.chroma_subsampling(), Some((1, 1)));
2306        assert_eq!(Nv12.chroma_subsampling(), Some((1, 1)));
2307        assert_eq!(Yuva420P16Le.chroma_subsampling(), Some((1, 1)));
2308        assert_eq!(Yuv422P.chroma_subsampling(), Some((1, 0)));
2309        assert_eq!(Yuyv422.chroma_subsampling(), Some((1, 0)));
2310        assert_eq!(Uyvy422.chroma_subsampling(), Some((1, 0)));
2311        assert_eq!(Yuv444P.chroma_subsampling(), Some((0, 0)));
2312        assert_eq!(Yuva444P12Le.chroma_subsampling(), Some((0, 0)));
2313        assert_eq!(Yuv411P.chroma_subsampling(), Some((2, 0)));
2314        assert_eq!(Yuv440P.chroma_subsampling(), Some((0, 1)));
2315        assert_eq!(Yuv440P16Le.chroma_subsampling(), Some((0, 1)));
2316        for fmt in [
2317            Gray8,
2318            Gray16Le,
2319            Ya8,
2320            Ya16Le,
2321            Pal8,
2322            MonoBlack,
2323            MonoWhite,
2324            Rgb24,
2325            Rgba,
2326            Rgb48Le,
2327            Rgba64Le,
2328            Cmyk,
2329            CmykInverted,
2330            Gbrp8,
2331            Gbrap16Le,
2332            GrayF32Le,
2333            RgbaF32Le,
2334            GbrapF32Le,
2335        ] {
2336            assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2337        }
2338    }
2339
2340    #[test]
2341    fn plane_dimensions_odd_sizes_across_samplings() {
2342        use PixelFormat::*;
2343        // 4:2:0 — both axes ceil-halved.
2344        assert_eq!(Yuv420P.plane_dimensions(1, 7, 5), Some((4, 3)));
2345        // 4:2:2 — width ceil-halved, height untouched.
2346        assert_eq!(Yuv422P.plane_dimensions(2, 7, 5), Some((4, 5)));
2347        // 4:1:1 — width ceil-quartered.
2348        assert_eq!(Yuv411P.plane_dimensions(1, 7, 5), Some((2, 5)));
2349        assert_eq!(Yuv411P.plane_dimensions(1, 9, 5), Some((3, 5)));
2350        // 4:4:4 — untouched.
2351        assert_eq!(Yuv444P.plane_dimensions(1, 7, 5), Some((7, 5)));
2352        // Semi-planar chroma positions.
2353        assert_eq!(Nv12.plane_dimensions(1, 7, 5), Some((4, 3)));
2354        assert_eq!(Nv21.plane_dimensions(1, 7, 5), Some((4, 3)));
2355        // Alpha planes are never subsampled.
2356        assert_eq!(Yuva420P.plane_dimensions(3, 7, 5), Some((7, 5)));
2357        assert_eq!(Yuva422P16Le.plane_dimensions(3, 7, 5), Some((7, 5)));
2358        // Planar RGB planes are never subsampled.
2359        for plane in 0..3 {
2360            assert_eq!(Gbrp12Le.plane_dimensions(plane, 7, 5), Some((7, 5)));
2361        }
2362        // Out-of-range planes.
2363        assert_eq!(Rgb24.plane_dimensions(1, 8, 8), None);
2364        assert_eq!(Yuv420P.plane_dimensions(3, 8, 8), None);
2365        assert_eq!(Yuva420P.plane_dimensions(4, 8, 8), None);
2366        // Zero-sized pictures collapse every plane to zero.
2367        assert_eq!(Yuv440P.plane_dimensions(1, 0, 0), Some((0, 0)));
2368    }
2369
2370    #[test]
2371    fn plane_row_bytes_conventions() {
2372        use PixelFormat::*;
2373        // Bit-packed mono: ceil(width / 8) with a ragged tail byte.
2374        assert_eq!(MonoBlack.plane_row_bytes(0, 13), Some(2));
2375        assert_eq!(MonoWhite.plane_row_bytes(0, 16), Some(2));
2376        assert_eq!(MonoBlack.plane_row_bytes(0, 17), Some(3));
2377        // Packed 4:2:2: 4-byte macropixels, odd width rounds up.
2378        assert_eq!(Yuyv422.plane_row_bytes(0, 6), Some(12));
2379        assert_eq!(Uyvy422.plane_row_bytes(0, 7), Some(16));
2380        // Semi-planar chroma: 2 bytes per position.
2381        assert_eq!(Nv12.plane_row_bytes(0, 7), Some(7));
2382        assert_eq!(Nv12.plane_row_bytes(1, 7), Some(8));
2383        // Deep planar planes: 2 bytes per sample regardless of the
2384        // number of valid bits in the word.
2385        assert_eq!(Yuv420P10Le.plane_row_bytes(1, 7), Some(8));
2386        assert_eq!(Gbrap14Le.plane_row_bytes(3, 5), Some(10));
2387        // Packed pixel costs.
2388        assert_eq!(Rgb24.plane_row_bytes(0, 5), Some(15));
2389        assert_eq!(Rgb48Le.plane_row_bytes(0, 2), Some(12));
2390        assert_eq!(Rgba64Le.plane_row_bytes(0, 2), Some(16));
2391        assert_eq!(Ya16Le.plane_row_bytes(0, 3), Some(12));
2392        assert_eq!(Cmyk.plane_row_bytes(0, 3), Some(12));
2393        // Out-of-range plane.
2394        assert_eq!(Gray8.plane_row_bytes(1, 8), None);
2395    }
2396
2397    #[test]
2398    fn frame_size_examples() {
2399        use PixelFormat::*;
2400        assert_eq!(Yuv420P.frame_size_bytes(4, 4), Some(24));
2401        assert_eq!(Nv12.frame_size_bytes(7, 5), Some(59)); // 35 + 4×3×2
2402        assert_eq!(Yuyv422.frame_size_bytes(7, 2), Some(32));
2403        assert_eq!(MonoBlack.frame_size_bytes(13, 3), Some(6));
2404        assert_eq!(Pal8.frame_size_bytes(5, 4), Some(20));
2405        assert_eq!(Ya16Le.frame_size_bytes(3, 3), Some(36));
2406        assert_eq!(Yuva444P16Le.frame_size_bytes(3, 3), Some(72));
2407    }
2408
2409    #[test]
2410    fn frame_size_is_sum_of_planes_for_every_format() {
2411        for fmt in ALL_PIXEL_FORMATS {
2412            for (w, h) in [(0, 0), (1, 1), (2, 2), (7, 5), (16, 16), (13, 1), (1, 13)] {
2413                let total = fmt
2414                    .frame_size_bytes(w, h)
2415                    .unwrap_or_else(|| panic!("{fmt:?} {w}x{h} must size"));
2416                let sum: usize = (0..fmt.plane_count())
2417                    .map(|p| fmt.plane_size_bytes(p, w, h).unwrap())
2418                    .sum();
2419                assert_eq!(total, sum, "{fmt:?} {w}x{h}");
2420                // Plane 0 is always the full pixel grid.
2421                assert_eq!(fmt.plane_dimensions(0, w, h), Some((w, h)), "{fmt:?}");
2422                // The plane table ends exactly at plane_count.
2423                assert_eq!(fmt.plane_dimensions(fmt.plane_count(), w, h), None);
2424                // Tightly-packed storage can never be smaller than the
2425                // packed-bits density estimate.
2426                let storage_bits = total as u128 * 8;
2427                let density_bits = w as u128 * h as u128 * fmt.bits_per_pixel_approx() as u128;
2428                assert!(
2429                    storage_bits >= density_bits,
2430                    "{fmt:?} {w}x{h}: storage {storage_bits} < density {density_bits}"
2431                );
2432            }
2433        }
2434    }
2435
2436    #[test]
2437    fn sizing_overflow_returns_none() {
2438        assert_eq!(
2439            PixelFormat::Rgba64Le.frame_size_bytes(u32::MAX, u32::MAX),
2440            None
2441        );
2442        assert_eq!(
2443            PixelFormat::RgbaF32Le.frame_size_bytes(u32::MAX, u32::MAX),
2444            None
2445        );
2446        assert_eq!(
2447            PixelFormat::Yuv440P16Le.plane_size_bytes(0, u32::MAX, u32::MAX),
2448            None
2449        );
2450    }
2451}