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    // --- Packed 16-bit RGB: three sub-byte components in one word ---
820    //
821    // One 16-bit word per pixel. The name lists the components from
822    // the word's most significant bits downward and the suffix is the
823    // byte order of the word in memory (`Le`: least significant byte
824    // first, `Be`: most significant byte first):
825    //
826    // | layout   | bit 15 | 14..10 | 15..11 | 10..5 | 9..5 | 4..0 |
827    // |----------|--------|--------|--------|-------|------|------|
828    // | `Rgb555` | unused | R      |        |       | G    | B    |
829    // | `Bgr555` | unused | B      |        |       | G    | R    |
830    // | `Rgb565` |        |        | R      | G     |      | B    |
831    // | `Bgr565` |        |        | B      | G     |      | R    |
832    //
833    // Component values are 5-bit (full-scale 31) or, for the middle
834    // component of 5-6-5, 6-bit (full-scale 63) integers; widening to
835    // 8 bits is the consumer's conversion (bit replication or
836    // scaling), not a property of the layout. The unused top bit of a
837    // 5-5-5 word is **not** an alpha channel: writers emit 0 and
838    // readers ignore it. A file format that uses that bit as a one-bit
839    // attribute carries the fact on its own side (its header), and a
840    // decoder that honours it emits an alpha-bearing layout instead.
841    // These are the native pixels of 16-bit bitmap files, 15/16-bit
842    // TGA files and many embedded framebuffers; `plane_row_bytes` is
843    // `2 × width`, `bits_per_pixel_approx` the 16-bit storage cost.
844    /// Packed 5-5-5 RGB in one little-endian 16-bit word: R in bits
845    /// 14..10, G in 9..5, B in 4..0; bit 15 unused (written as 0).
846    Rgb555Le = 70,
847    /// Packed 5-5-5 RGB in one big-endian 16-bit word: R in bits
848    /// 14..10, G in 9..5, B in 4..0; bit 15 unused (written as 0).
849    Rgb555Be = 71,
850    /// Packed 5-5-5 BGR in one little-endian 16-bit word: B in bits
851    /// 14..10, G in 9..5, R in 4..0; bit 15 unused (written as 0).
852    Bgr555Le = 72,
853    /// Packed 5-5-5 BGR in one big-endian 16-bit word: B in bits
854    /// 14..10, G in 9..5, R in 4..0; bit 15 unused (written as 0).
855    Bgr555Be = 73,
856    /// Packed 5-6-5 RGB in one little-endian 16-bit word: R in bits
857    /// 15..11, G in 10..5, B in 4..0.
858    Rgb565Le = 74,
859    /// Packed 5-6-5 RGB in one big-endian 16-bit word: R in bits
860    /// 15..11, G in 10..5, B in 4..0.
861    Rgb565Be = 75,
862    /// Packed 5-6-5 BGR in one little-endian 16-bit word: B in bits
863    /// 15..11, G in 10..5, R in 4..0.
864    Bgr565Le = 76,
865    /// Packed 5-6-5 BGR in one big-endian 16-bit word: B in bits
866    /// 15..11, G in 10..5, R in 4..0.
867    Bgr565Be = 77,
868
869    // --- Padded 32-bit RGB: three 8-bit components + one unused byte ---
870    //
871    // Four bytes per pixel in the byte order the name spells, where
872    // `X` is a padding byte: it is **not** alpha, readers ignore its
873    // value and writers emit 0. Layout tables elsewhere spell these
874    // `Rgb0` / `Bgr0` / `0Rgb` / `0Bgr`. The padding keeps every pixel
875    // 4-byte aligned, which is why framebuffers, 32-bit bitmap files
876    // with zero attribute bits and GPU upload paths use them; the
877    // alpha-bearing siblings at the same byte positions are `Rgba` /
878    // `Bgra` / `Argb` / `Abgr`. `has_alpha` is false; converting to an
879    // alpha layout sets alpha opaque, converting from one drops it.
880    /// Packed 8-bit RGB + one padding byte, 4 bytes/pixel in byte
881    /// order R, G, B, X.
882    Rgbx = 78,
883    /// Packed 8-bit BGR + one padding byte, 4 bytes/pixel in byte
884    /// order B, G, R, X.
885    Bgrx = 79,
886    /// One padding byte + packed 8-bit RGB, 4 bytes/pixel in byte
887    /// order X, R, G, B.
888    Xrgb = 80,
889    /// One padding byte + packed 8-bit BGR, 4 bytes/pixel in byte
890    /// order X, B, G, R.
891    Xbgr = 81,
892}
893
894impl PixelFormat {
895    /// True if this format stores its components in separate planes.
896    pub fn is_planar(&self) -> bool {
897        matches!(
898            self,
899            Self::Yuv420P
900                | Self::Yuv422P
901                | Self::Yuv444P
902                | Self::Yuv411P
903                | Self::Yuv420P10Le
904                | Self::Yuv422P10Le
905                | Self::Yuv444P10Le
906                | Self::Yuv420P12Le
907                | Self::Yuv422P12Le
908                | Self::Yuv444P12Le
909                | Self::Yuv420P16Le
910                | Self::Yuv422P16Le
911                | Self::Yuv444P16Le
912                | Self::Yuv440P
913                | Self::Yuv440P10Le
914                | Self::Yuv440P12Le
915                | Self::Yuv440P16Le
916                | Self::YuvJ420P
917                | Self::YuvJ422P
918                | Self::YuvJ444P
919                | Self::Nv12
920                | Self::Nv21
921                | Self::Yuva420P
922                | Self::Yuva422P
923                | Self::Yuva444P
924                | Self::Yuva422P10Le
925                | Self::Yuva422P12Le
926                | Self::Yuva444P10Le
927                | Self::Yuva444P12Le
928                | Self::Yuva422P16Le
929                | Self::Yuva444P16Le
930                | Self::Yuva420P10Le
931                | Self::Yuva420P12Le
932                | Self::Yuva420P16Le
933                | Self::Gbrp8
934                | Self::Gbrap8
935                | Self::Gbrp10Le
936                | Self::Gbrap10Le
937                | Self::Gbrp12Le
938                | Self::Gbrap12Le
939                | Self::Gbrp14Le
940                | Self::Gbrap14Le
941                | Self::Gbrp16Le
942                | Self::Gbrap16Le
943                | Self::GbrpF32Le
944                | Self::GbrapF32Le
945        )
946    }
947
948    /// The sample range this format's *label* commits to, if any.
949    ///
950    /// Only the three legacy full-range labels (`YuvJ420P`, `YuvJ422P`,
951    /// `YuvJ444P`) carry a range in their name and return
952    /// `Some(Full)`. Every other format — limited-range-by-convention
953    /// `Yuv*`, every >8-bit and alpha-bearing Y′CbCr surface, RGB and
954    /// grey — returns `None`: their range is a property of the signal,
955    /// not of the storage layout, and lives in
956    /// [`ColorSignal::range`](crate::ColorSignal::range) on the stream
957    /// ([`CodecParameters::color_signal`](crate::CodecParameters::color_signal))
958    /// or frame ([`VideoFrame::color_signal`](crate::VideoFrame::color_signal)).
959    /// Use
960    /// [`CodecParameters::resolved_color_range`](crate::CodecParameters::resolved_color_range)
961    /// for the combined answer (explicit signal first, then this label).
962    pub fn implied_color_range(&self) -> Option<crate::signal::ColorRange> {
963        match self {
964            Self::YuvJ420P | Self::YuvJ422P | Self::YuvJ444P => {
965                Some(crate::signal::ColorRange::Full)
966            }
967            _ => None,
968        }
969    }
970
971    /// True if the format is a palette index format (`Pal8`).
972    pub fn is_palette(&self) -> bool {
973        matches!(self, Self::Pal8)
974    }
975
976    /// True if this format carries an alpha channel.
977    pub fn has_alpha(&self) -> bool {
978        matches!(
979            self,
980            Self::Rgba
981                | Self::Bgra
982                | Self::Argb
983                | Self::Abgr
984                | Self::Rgba64Le
985                | Self::Ya8
986                | Self::Ya16Le
987                | Self::Yuva420P
988                | Self::Yuva422P
989                | Self::Yuva444P
990                | Self::Yuva422P10Le
991                | Self::Yuva422P12Le
992                | Self::Yuva444P10Le
993                | Self::Yuva444P12Le
994                | Self::Yuva422P16Le
995                | Self::Yuva444P16Le
996                | Self::Yuva420P10Le
997                | Self::Yuva420P12Le
998                | Self::Yuva420P16Le
999                | Self::Gbrap8
1000                | Self::Gbrap10Le
1001                | Self::Gbrap12Le
1002                | Self::Gbrap14Le
1003                | Self::Gbrap16Le
1004                | Self::RgbaF32Le
1005                | Self::GbrapF32Le
1006        )
1007    }
1008
1009    /// True for the 32-bit IEEE-float variants, packed or planar.
1010    /// Float formats are scene-referred: samples carry linear light
1011    /// with no integer full-scale — 1.0 is the nominal diffuse-white
1012    /// anchor and values outside [0, 1] are legal.
1013    pub fn is_float(&self) -> bool {
1014        matches!(
1015            self,
1016            Self::GrayF32Le | Self::RgbF32Le | Self::RgbaF32Le | Self::GbrpF32Le | Self::GbrapF32Le
1017        )
1018    }
1019
1020    /// True for the single-plane RGB layouts: every pixel's R, G and
1021    /// B components (plus alpha or padding, if any) are interleaved in
1022    /// one plane — `Rgb24` / `Bgr24`, the 32-bit `Rgba` / `Bgra` /
1023    /// `Argb` / `Abgr` and padded `Rgbx` / `Bgrx` / `Xrgb` / `Xbgr`,
1024    /// the 16-bit `Rgb555*` / `Bgr555*` / `Rgb565*` / `Bgr565*` words,
1025    /// `Rgb48Le` / `Rgba64Le` and the packed float `RgbF32Le` /
1026    /// `RgbaF32Le`. Planar GBR(A) (see [`is_rgb`](Self::is_rgb)),
1027    /// grey, YUV in any layout, palette, mono and CMYK are not.
1028    ///
1029    /// ```
1030    /// use oxideav_core::PixelFormat;
1031    /// assert!(PixelFormat::Rgb565Le.is_packed_rgb());
1032    /// assert!(PixelFormat::Xrgb.is_packed_rgb());
1033    /// assert!(!PixelFormat::Gbrp8.is_packed_rgb()); // planar
1034    /// assert!(!PixelFormat::Pal8.is_packed_rgb()); // indices, not components
1035    /// ```
1036    pub fn is_packed_rgb(&self) -> bool {
1037        matches!(
1038            self,
1039            Self::Rgb24
1040                | Self::Bgr24
1041                | Self::Rgba
1042                | Self::Bgra
1043                | Self::Argb
1044                | Self::Abgr
1045                | Self::Rgbx
1046                | Self::Bgrx
1047                | Self::Xrgb
1048                | Self::Xbgr
1049                | Self::Rgb555Le
1050                | Self::Rgb555Be
1051                | Self::Bgr555Le
1052                | Self::Bgr555Be
1053                | Self::Rgb565Le
1054                | Self::Rgb565Be
1055                | Self::Bgr565Le
1056                | Self::Bgr565Be
1057                | Self::Rgb48Le
1058                | Self::Rgba64Le
1059                | Self::RgbF32Le
1060                | Self::RgbaF32Le
1061        )
1062    }
1063
1064    /// True for every layout whose components are R, G and B (with or
1065    /// without alpha / padding), packed or planar: everything
1066    /// [`is_packed_rgb`](Self::is_packed_rgb) accepts plus the planar
1067    /// `Gbrp*` / `Gbrap*` family at every depth including float. Grey,
1068    /// YUV, palette (indices into an RGB table are not RGB samples),
1069    /// mono and CMYK are not.
1070    pub fn is_rgb(&self) -> bool {
1071        self.is_packed_rgb()
1072            || matches!(
1073                self,
1074                Self::Gbrp8
1075                    | Self::Gbrap8
1076                    | Self::Gbrp10Le
1077                    | Self::Gbrap10Le
1078                    | Self::Gbrp12Le
1079                    | Self::Gbrap12Le
1080                    | Self::Gbrp14Le
1081                    | Self::Gbrap14Le
1082                    | Self::Gbrp16Le
1083                    | Self::Gbrap16Le
1084                    | Self::GbrpF32Le
1085                    | Self::GbrapF32Le
1086            )
1087    }
1088
1089    /// Number of planes in the stored layout. Packed and palette formats
1090    /// return 1; NV12/NV21 return 2; planar YUV without alpha and the
1091    /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4.
1092    pub fn plane_count(&self) -> usize {
1093        match self {
1094            Self::Nv12 | Self::Nv21 => 2,
1095            Self::Yuv420P
1096            | Self::Yuv422P
1097            | Self::Yuv444P
1098            | Self::Yuv411P
1099            | Self::Yuv420P10Le
1100            | Self::Yuv422P10Le
1101            | Self::Yuv444P10Le
1102            | Self::Yuv420P12Le
1103            | Self::Yuv422P12Le
1104            | Self::Yuv444P12Le
1105            | Self::Yuv420P16Le
1106            | Self::Yuv422P16Le
1107            | Self::Yuv444P16Le
1108            | Self::Yuv440P
1109            | Self::Yuv440P10Le
1110            | Self::Yuv440P12Le
1111            | Self::Yuv440P16Le
1112            | Self::YuvJ420P
1113            | Self::YuvJ422P
1114            | Self::YuvJ444P
1115            | Self::Gbrp8
1116            | Self::Gbrp10Le
1117            | Self::Gbrp12Le
1118            | Self::Gbrp14Le
1119            | Self::Gbrp16Le
1120            | Self::GbrpF32Le => 3,
1121            Self::Yuva420P
1122            | Self::Yuva422P
1123            | Self::Yuva444P
1124            | Self::Yuva422P10Le
1125            | Self::Yuva422P12Le
1126            | Self::Yuva444P10Le
1127            | Self::Yuva444P12Le
1128            | Self::Yuva422P16Le
1129            | Self::Yuva444P16Le
1130            | Self::Yuva420P10Le
1131            | Self::Yuva420P12Le
1132            | Self::Yuva420P16Le
1133            | Self::Gbrap8
1134            | Self::Gbrap10Le
1135            | Self::Gbrap12Le
1136            | Self::Gbrap14Le
1137            | Self::Gbrap16Le
1138            | Self::GbrapF32Le => 4,
1139            _ => 1,
1140        }
1141    }
1142
1143    /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
1144    /// for chroma-subsampled YUV — intended for worst-case preallocation
1145    /// rather than wire-accurate accounting.
1146    pub fn bits_per_pixel_approx(&self) -> u32 {
1147        match self {
1148            Self::MonoBlack | Self::MonoWhite => 1,
1149            Self::Gray8 | Self::Pal8 => 8,
1150            Self::Ya8 => 16,
1151            // 16-bit gray + alpha: two LE 16-bit words per pixel, all
1152            // bits significant — packed bits equal storage bits.
1153            Self::Ya16Le => 32,
1154            Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
1155            Self::Rgb24 | Self::Bgr24 => 24,
1156            Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
1157            // Padded 32-bit RGB: the unused byte is storage the pixel
1158            // occupies, so the whole-pixel cost is reported like the
1159            // alpha-bearing quads above.
1160            Self::Rgbx | Self::Bgrx | Self::Xrgb | Self::Xbgr => 32,
1161            // Packed 16-bit RGB: one 16-bit word per pixel (the unused
1162            // top bit of 5-5-5 is still storage).
1163            Self::Rgb555Le
1164            | Self::Rgb555Be
1165            | Self::Bgr555Le
1166            | Self::Bgr555Be
1167            | Self::Rgb565Le
1168            | Self::Rgb565Be
1169            | Self::Bgr565Le
1170            | Self::Bgr565Be => 16,
1171            Self::Rgb48Le => 48,
1172            Self::Rgba64Le => 64,
1173            Self::Yuyv422 | Self::Uyvy422 => 16,
1174            Self::Cmyk | Self::CmykInverted => 32,
1175            // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
1176            // 10/12-bit variants double the byte count but we report the
1177            // packed-bits-per-pixel estimate for a uniform heuristic.
1178            Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
1179            // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
1180            // full res + 2 chroma planes each subsampled by 4).
1181            Self::Yuv411P => 12,
1182            Self::Yuv422P | Self::YuvJ422P => 16,
1183            // 4:4:0 packs the same 2 samples/pixel as 4:2:2 (Y at full
1184            // res + 2 chroma planes at half height, full width).
1185            Self::Yuv440P => 16,
1186            Self::Yuv444P | Self::YuvJ444P => 24,
1187            Self::Yuv420P10Le | Self::Yuv420P12Le | Self::Yuv420P16Le => 24,
1188            Self::Yuv422P10Le | Self::Yuv422P12Le | Self::Yuv422P16Le => 32,
1189            // Deep 4:4:0 matches deep 4:2:2 — 2 sample words per pixel.
1190            Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => 32,
1191            Self::Yuv444P10Le | Self::Yuv444P12Le | Self::Yuv444P16Le => 48,
1192            Self::Yuva420P => 20,
1193            // 4:2:2 + full-res alpha: 8 (Y) + 4 (U) + 4 (V) + 8 (A).
1194            Self::Yuva422P => 24,
1195            // 4:4:4 + full-res alpha: four full-resolution 8-bit planes.
1196            Self::Yuva444P => 32,
1197            // Deep 4:2:2 + full-res alpha in 16-bit words: the estimator
1198            // reports the 16-bit-word cost like the alpha-less deep YUV
1199            // arms above — 3 sample words per pixel (Y + U/2 + V/2 + A).
1200            Self::Yuva422P10Le | Self::Yuva422P12Le | Self::Yuva422P16Le => 48,
1201            // Deep 4:4:4 + full-res alpha: 4 sample words per pixel.
1202            Self::Yuva444P10Le | Self::Yuva444P12Le | Self::Yuva444P16Le => 64,
1203            // Deep 4:2:0 + full-res alpha in 16-bit words: 16-bit-word
1204            // storage cost of the alpha-less 4:2:0 arms above (24) plus
1205            // one full-resolution 16-bit alpha word per pixel.
1206            Self::Yuva420P10Le | Self::Yuva420P12Le | Self::Yuva420P16Le => 40,
1207            // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we
1208            // report the packed bits-per-pixel density (samples × bits)
1209            // rather than the 16-bit storage cost, matching how the
1210            // 10/12-bit YUV variants are reported above.
1211            Self::Gbrp10Le => 30,
1212            Self::Gbrap10Le => 40,
1213            Self::Gbrp12Le => 36,
1214            Self::Gbrap12Le => 48,
1215            Self::Gbrp14Le => 42,
1216            Self::Gbrap14Le => 56,
1217            // Native 8-bit GBR: three bytes per pixel, like Rgb24 but
1218            // planar. 16-bit GBR(A): packed bits == storage bits (every
1219            // bit of each 16-bit word is significant), so the density
1220            // and storage numbers coincide.
1221            Self::Gbrp8 => 24,
1222            Self::Gbrp16Le => 48,
1223            Self::Gbrap16Le => 64,
1224            // 8-bit GBR + alpha: four bytes per pixel, like Rgba but
1225            // planar.
1226            Self::Gbrap8 => 32,
1227            // 32-bit float family: every sample is a full binary32
1228            // word, so packed bits equal storage bits (32 per sample;
1229            // no chroma subsampling anywhere in the family).
1230            Self::GrayF32Le => 32,
1231            Self::RgbF32Le | Self::GbrpF32Le => 96,
1232            Self::RgbaF32Le | Self::GbrapF32Le => 128,
1233        }
1234    }
1235
1236    /// Log2 chroma-subsampling shifts `(ssx, ssy)` relative to the
1237    /// luma grid, for formats that carry chroma on a subsampled (or
1238    /// potentially subsampled) grid. The chroma sample grid is the
1239    /// luma grid right-shifted by `ssx` horizontally and `ssy`
1240    /// vertically, with ceiling division for odd luma sizes (see
1241    /// [`plane_dimensions`](Self::plane_dimensions)).
1242    ///
1243    /// | sampling | `(ssx, ssy)` | chroma geometry |
1244    /// |----------|--------------|-----------------|
1245    /// | 4:2:0    | `(1, 1)`     | half width, half height |
1246    /// | 4:2:2    | `(1, 0)`     | half width, full height |
1247    /// | 4:4:4    | `(0, 0)`     | full resolution |
1248    /// | 4:1:1    | `(2, 0)`     | quarter width, full height |
1249    /// | 4:4:0    | `(0, 1)`     | full width, half height |
1250    ///
1251    /// Returns `None` for formats without a distinct chroma grid
1252    /// (grayscale, RGB/GBR in any layout, palette, mono, CMYK).
1253    /// Packed 4:2:2 (`Yuyv422`/`Uyvy422`) and semi-planar 4:2:0
1254    /// (`Nv12`/`Nv21`) report their sampling even though the chroma
1255    /// samples don't live in standalone planes.
1256    ///
1257    /// ```
1258    /// use oxideav_core::PixelFormat;
1259    /// // 4:4:0: full-width, half-height chroma.
1260    /// assert_eq!(PixelFormat::Yuv440P.chroma_subsampling(), Some((0, 1)));
1261    /// // 4:2:0: subsampled on both axes.
1262    /// assert_eq!(PixelFormat::Yuv420P.chroma_subsampling(), Some((1, 1)));
1263    /// // RGB has no chroma grid.
1264    /// assert_eq!(PixelFormat::Rgba.chroma_subsampling(), None);
1265    /// ```
1266    pub fn chroma_subsampling(&self) -> Option<(u32, u32)> {
1267        match self {
1268            // 4:2:0 — half width, half height.
1269            Self::Yuv420P
1270            | Self::YuvJ420P
1271            | Self::Yuv420P10Le
1272            | Self::Yuv420P12Le
1273            | Self::Yuv420P16Le
1274            | Self::Nv12
1275            | Self::Nv21
1276            | Self::Yuva420P
1277            | Self::Yuva420P10Le
1278            | Self::Yuva420P12Le
1279            | Self::Yuva420P16Le => Some((1, 1)),
1280            // 4:2:2 — half width, full height (packed 4:2:2 included).
1281            Self::Yuv422P
1282            | Self::YuvJ422P
1283            | Self::Yuv422P10Le
1284            | Self::Yuv422P12Le
1285            | Self::Yuv422P16Le
1286            | Self::Yuva422P
1287            | Self::Yuva422P10Le
1288            | Self::Yuva422P12Le
1289            | Self::Yuva422P16Le
1290            | Self::Yuyv422
1291            | Self::Uyvy422 => Some((1, 0)),
1292            // 4:4:4 — chroma at full resolution.
1293            Self::Yuv444P
1294            | Self::YuvJ444P
1295            | Self::Yuv444P10Le
1296            | Self::Yuv444P12Le
1297            | Self::Yuv444P16Le
1298            | Self::Yuva444P
1299            | Self::Yuva444P10Le
1300            | Self::Yuva444P12Le
1301            | Self::Yuva444P16Le => Some((0, 0)),
1302            // 4:1:1 — quarter width, full height.
1303            Self::Yuv411P => Some((2, 0)),
1304            // 4:4:0 — full width, half height.
1305            Self::Yuv440P | Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => {
1306                Some((0, 1))
1307            }
1308            // Everything else has no distinct chroma grid.
1309            _ => None,
1310        }
1311    }
1312
1313    /// Sample-grid dimensions of plane `plane` for a `width` ×
1314    /// `height` picture, with ceiling division on subsampled axes so
1315    /// odd luma sizes still cover every pixel.
1316    ///
1317    /// Conventions:
1318    /// - Plane 0 (luma / the packed plane) is always `(width, height)`.
1319    /// - Chroma planes (indices 1 and 2 of planar YUV, index 1 of the
1320    ///   semi-planar formats) are the luma grid right-shifted by the
1321    ///   [`chroma_subsampling`](Self::chroma_subsampling) factors.
1322    ///   Semi-planar chroma dimensions are in chroma *positions* —
1323    ///   each position stores two interleaved samples, which
1324    ///   [`plane_row_bytes`](Self::plane_row_bytes) accounts for.
1325    /// - Alpha planes (index 3) and all planar-RGB planes are at full
1326    ///   resolution.
1327    /// - Packed, palette, and bit-packed mono formats report pixel
1328    ///   dimensions for their single plane; per-row byte cost comes
1329    ///   from [`plane_row_bytes`](Self::plane_row_bytes).
1330    ///
1331    /// Returns `None` when `plane >= plane_count()`.
1332    ///
1333    /// ```
1334    /// use oxideav_core::PixelFormat;
1335    /// // 4:4:0 chroma: full width, half height (odd height rounds up).
1336    /// assert_eq!(
1337    ///     PixelFormat::Yuv440P.plane_dimensions(1, 640, 481),
1338    ///     Some((640, 241))
1339    /// );
1340    /// // Alpha plane of a deep YUVA format stays at full resolution.
1341    /// assert_eq!(
1342    ///     PixelFormat::Yuva420P10Le.plane_dimensions(3, 7, 5),
1343    ///     Some((7, 5))
1344    /// );
1345    /// assert_eq!(PixelFormat::Rgb24.plane_dimensions(1, 8, 8), None);
1346    /// ```
1347    pub fn plane_dimensions(&self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> {
1348        if plane >= self.plane_count() {
1349            return None;
1350        }
1351        match (self.chroma_subsampling(), plane) {
1352            (Some((ssx, ssy)), 1 | 2) => {
1353                Some((width.div_ceil(1 << ssx), height.div_ceil(1 << ssy)))
1354            }
1355            _ => Some((width, height)),
1356        }
1357    }
1358
1359    /// Tightly-packed byte count of one row of plane `plane` for a
1360    /// picture `width` pixels wide — no stride padding or alignment.
1361    /// Real codecs frequently over-allocate rows for alignment; this
1362    /// is the minimum a row occupies.
1363    ///
1364    /// Returns `None` when `plane >= plane_count()` or the byte count
1365    /// overflows `usize`.
1366    pub fn plane_row_bytes(&self, plane: usize, width: u32) -> Option<usize> {
1367        let (pw, _) = self.plane_dimensions(plane, width, 1)?;
1368        let pw = pw as usize;
1369        let bytes_per_position: usize = match self {
1370            // Bit-packed mono: 8 pixels per byte, ragged tail byte.
1371            Self::MonoBlack | Self::MonoWhite => return Some(pw.div_ceil(8)),
1372            // Packed 4:2:2 macropixels: 4 bytes per 2 pixels; an odd
1373            // trailing pixel still occupies a full macropixel.
1374            Self::Yuyv422 | Self::Uyvy422 => return pw.div_ceil(2).checked_mul(4),
1375            // One byte per sample position.
1376            Self::Gray8
1377            | Self::Pal8
1378            | Self::Yuv420P
1379            | Self::Yuv422P
1380            | Self::Yuv444P
1381            | Self::Yuv411P
1382            | Self::Yuv440P
1383            | Self::YuvJ420P
1384            | Self::YuvJ422P
1385            | Self::YuvJ444P
1386            | Self::Yuva420P
1387            | Self::Yuva422P
1388            | Self::Yuva444P
1389            | Self::Gbrp8
1390            | Self::Gbrap8 => 1,
1391            // Semi-planar: one byte per luma sample on plane 0, an
1392            // interleaved two-sample pair per chroma position on
1393            // plane 1.
1394            Self::Nv12 | Self::Nv21 => {
1395                if plane == 0 {
1396                    1
1397                } else {
1398                    2
1399                }
1400            }
1401            // Little-endian 16-bit words (10/12/14/16-bit storage).
1402            Self::Gray10Le
1403            | Self::Gray12Le
1404            | Self::Gray16Le
1405            | Self::Yuv420P10Le
1406            | Self::Yuv422P10Le
1407            | Self::Yuv444P10Le
1408            | Self::Yuv420P12Le
1409            | Self::Yuv422P12Le
1410            | Self::Yuv444P12Le
1411            | Self::Yuv420P16Le
1412            | Self::Yuv422P16Le
1413            | Self::Yuv444P16Le
1414            | Self::Yuv440P10Le
1415            | Self::Yuv440P12Le
1416            | Self::Yuv440P16Le
1417            | Self::Yuva422P10Le
1418            | Self::Yuva422P12Le
1419            | Self::Yuva444P10Le
1420            | Self::Yuva444P12Le
1421            | Self::Yuva422P16Le
1422            | Self::Yuva444P16Le
1423            | Self::Yuva420P10Le
1424            | Self::Yuva420P12Le
1425            | Self::Yuva420P16Le
1426            | Self::Gbrp10Le
1427            | Self::Gbrap10Le
1428            | Self::Gbrp12Le
1429            | Self::Gbrap12Le
1430            | Self::Gbrp14Le
1431            | Self::Gbrap14Le
1432            | Self::Gbrp16Le
1433            | Self::Gbrap16Le => 2,
1434            // Packed multi-component: whole-pixel byte cost.
1435            Self::Ya8
1436            | Self::Rgb555Le
1437            | Self::Rgb555Be
1438            | Self::Bgr555Le
1439            | Self::Bgr555Be
1440            | Self::Rgb565Le
1441            | Self::Rgb565Be
1442            | Self::Bgr565Le
1443            | Self::Bgr565Be => 2,
1444            Self::Rgb24 | Self::Bgr24 => 3,
1445            Self::Rgba
1446            | Self::Bgra
1447            | Self::Argb
1448            | Self::Abgr
1449            | Self::Rgbx
1450            | Self::Bgrx
1451            | Self::Xrgb
1452            | Self::Xbgr
1453            | Self::Cmyk
1454            | Self::CmykInverted
1455            | Self::Ya16Le => 4,
1456            Self::Rgb48Le => 6,
1457            Self::Rgba64Le => 8,
1458            // 32-bit float: one binary32 word per sample (packed
1459            // grayscale and the planar GBR(A) planes), or the
1460            // whole-pixel cost for packed multi-component float.
1461            Self::GrayF32Le | Self::GbrpF32Le | Self::GbrapF32Le => 4,
1462            Self::RgbF32Le => 12,
1463            Self::RgbaF32Le => 16,
1464        };
1465        pw.checked_mul(bytes_per_position)
1466    }
1467
1468    /// Tightly-packed byte size of plane `plane` for a `width` ×
1469    /// `height` picture:
1470    /// [`plane_row_bytes`](Self::plane_row_bytes) × the plane's row
1471    /// count from [`plane_dimensions`](Self::plane_dimensions).
1472    ///
1473    /// Returns `None` when `plane >= plane_count()` or the size
1474    /// overflows `usize`.
1475    pub fn plane_size_bytes(&self, plane: usize, width: u32, height: u32) -> Option<usize> {
1476        let (_, ph) = self.plane_dimensions(plane, width, height)?;
1477        self.plane_row_bytes(plane, width)?.checked_mul(ph as usize)
1478    }
1479
1480    /// Tightly-packed byte size of a whole `width` × `height` frame in
1481    /// this format — the sum of
1482    /// [`plane_size_bytes`](Self::plane_size_bytes) over every plane,
1483    /// with no stride padding or inter-plane alignment. Out-of-band
1484    /// side data (the `Pal8` palette table, significant-bits records)
1485    /// is not included.
1486    ///
1487    /// Returns `None` on `usize` overflow.
1488    ///
1489    /// ```
1490    /// use oxideav_core::PixelFormat;
1491    /// // 4:2:0 at 4×4: 16 luma + 4 + 4 chroma bytes.
1492    /// assert_eq!(PixelFormat::Yuv420P.frame_size_bytes(4, 4), Some(24));
1493    /// // 4:4:0 at 6×5: 30 luma + 2 × (6 × 3) chroma bytes.
1494    /// assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
1495    /// // Packed float RGBA: 16 bytes per pixel.
1496    /// assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
1497    /// ```
1498    pub fn frame_size_bytes(&self, width: u32, height: u32) -> Option<usize> {
1499        let mut total = 0usize;
1500        for plane in 0..self.plane_count() {
1501            total = total.checked_add(self.plane_size_bytes(plane, width, height)?)?;
1502        }
1503        Some(total)
1504    }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509    use super::*;
1510
1511    /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
1512    /// stability commitment — the integer value of each variant is part of
1513    /// the public ABI. Any reorder, renumber, or removal will fail this test
1514    /// and the change MUST be a major version bump (or a fresh variant
1515    /// appended at a new number, leaving the existing ones untouched).
1516    #[test]
1517    fn implied_color_range_is_full_only_for_the_j_labels() {
1518        use crate::signal::ColorRange;
1519        for f in [
1520            PixelFormat::YuvJ420P,
1521            PixelFormat::YuvJ422P,
1522            PixelFormat::YuvJ444P,
1523        ] {
1524            assert_eq!(f.implied_color_range(), Some(ColorRange::Full), "{f:?}");
1525        }
1526        for f in [
1527            PixelFormat::Yuv420P,
1528            PixelFormat::Yuv444P,
1529            PixelFormat::Yuv420P10Le,
1530            PixelFormat::Yuv444P12Le,
1531            PixelFormat::Rgb24,
1532            PixelFormat::Rgba,
1533            PixelFormat::Gray8,
1534            PixelFormat::Gray16Le,
1535            PixelFormat::Nv12,
1536            PixelFormat::Pal8,
1537        ] {
1538            assert_eq!(f.implied_color_range(), None, "{f:?}");
1539        }
1540    }
1541
1542    #[test]
1543    fn pixel_format_discriminants_pinned() {
1544        assert_eq!(PixelFormat::Yuv420P as u16, 0);
1545        assert_eq!(PixelFormat::Yuv422P as u16, 1);
1546        assert_eq!(PixelFormat::Yuv444P as u16, 2);
1547        assert_eq!(PixelFormat::Rgb24 as u16, 3);
1548        assert_eq!(PixelFormat::Rgba as u16, 4);
1549        assert_eq!(PixelFormat::Gray8 as u16, 5);
1550        assert_eq!(PixelFormat::Pal8 as u16, 6);
1551        assert_eq!(PixelFormat::Bgr24 as u16, 7);
1552        assert_eq!(PixelFormat::Bgra as u16, 8);
1553        assert_eq!(PixelFormat::Argb as u16, 9);
1554        assert_eq!(PixelFormat::Abgr as u16, 10);
1555        assert_eq!(PixelFormat::Rgb48Le as u16, 11);
1556        assert_eq!(PixelFormat::Rgba64Le as u16, 12);
1557        assert_eq!(PixelFormat::Gray16Le as u16, 13);
1558        assert_eq!(PixelFormat::Gray10Le as u16, 14);
1559        assert_eq!(PixelFormat::Gray12Le as u16, 15);
1560        assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
1561        assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
1562        assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
1563        assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
1564        assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
1565        assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
1566        assert_eq!(PixelFormat::YuvJ420P as u16, 22);
1567        assert_eq!(PixelFormat::YuvJ422P as u16, 23);
1568        assert_eq!(PixelFormat::YuvJ444P as u16, 24);
1569        assert_eq!(PixelFormat::Nv12 as u16, 25);
1570        assert_eq!(PixelFormat::Nv21 as u16, 26);
1571        assert_eq!(PixelFormat::Ya8 as u16, 27);
1572        assert_eq!(PixelFormat::Yuva420P as u16, 28);
1573        assert_eq!(PixelFormat::MonoBlack as u16, 29);
1574        assert_eq!(PixelFormat::MonoWhite as u16, 30);
1575        assert_eq!(PixelFormat::Yuyv422 as u16, 31);
1576        assert_eq!(PixelFormat::Uyvy422 as u16, 32);
1577        assert_eq!(PixelFormat::Cmyk as u16, 33);
1578        assert_eq!(PixelFormat::Yuv411P as u16, 34);
1579        assert_eq!(PixelFormat::Gbrp10Le as u16, 35);
1580        assert_eq!(PixelFormat::Gbrap10Le as u16, 36);
1581        assert_eq!(PixelFormat::Gbrp12Le as u16, 37);
1582        assert_eq!(PixelFormat::Gbrap12Le as u16, 38);
1583        assert_eq!(PixelFormat::Gbrp14Le as u16, 39);
1584        assert_eq!(PixelFormat::Gbrap14Le as u16, 40);
1585        assert_eq!(PixelFormat::Yuv420P16Le as u16, 41);
1586        assert_eq!(PixelFormat::Yuv422P16Le as u16, 42);
1587        assert_eq!(PixelFormat::Yuv444P16Le as u16, 43);
1588        assert_eq!(PixelFormat::Yuva422P as u16, 44);
1589        assert_eq!(PixelFormat::Yuva444P as u16, 45);
1590        assert_eq!(PixelFormat::Yuva422P10Le as u16, 46);
1591        assert_eq!(PixelFormat::Yuva422P12Le as u16, 47);
1592        assert_eq!(PixelFormat::Yuva444P10Le as u16, 48);
1593        assert_eq!(PixelFormat::Yuva444P12Le as u16, 49);
1594        assert_eq!(PixelFormat::Yuva422P16Le as u16, 50);
1595        assert_eq!(PixelFormat::Yuva444P16Le as u16, 51);
1596        assert_eq!(PixelFormat::Gbrp8 as u16, 52);
1597        assert_eq!(PixelFormat::Gbrp16Le as u16, 53);
1598        assert_eq!(PixelFormat::Gbrap16Le as u16, 54);
1599        assert_eq!(PixelFormat::Yuva420P10Le as u16, 55);
1600        assert_eq!(PixelFormat::Yuva420P12Le as u16, 56);
1601        assert_eq!(PixelFormat::Yuva420P16Le as u16, 57);
1602        assert_eq!(PixelFormat::Gbrap8 as u16, 58);
1603        assert_eq!(PixelFormat::Ya16Le as u16, 59);
1604        assert_eq!(PixelFormat::CmykInverted as u16, 60);
1605        assert_eq!(PixelFormat::Yuv440P as u16, 61);
1606        assert_eq!(PixelFormat::Yuv440P10Le as u16, 62);
1607        assert_eq!(PixelFormat::Yuv440P12Le as u16, 63);
1608        assert_eq!(PixelFormat::Yuv440P16Le as u16, 64);
1609        assert_eq!(PixelFormat::GrayF32Le as u16, 65);
1610        assert_eq!(PixelFormat::RgbF32Le as u16, 66);
1611        assert_eq!(PixelFormat::RgbaF32Le as u16, 67);
1612        assert_eq!(PixelFormat::GbrpF32Le as u16, 68);
1613        assert_eq!(PixelFormat::GbrapF32Le as u16, 69);
1614        assert_eq!(PixelFormat::Rgb555Le as u16, 70);
1615        assert_eq!(PixelFormat::Rgb555Be as u16, 71);
1616        assert_eq!(PixelFormat::Bgr555Le as u16, 72);
1617        assert_eq!(PixelFormat::Bgr555Be as u16, 73);
1618        assert_eq!(PixelFormat::Rgb565Le as u16, 74);
1619        assert_eq!(PixelFormat::Rgb565Be as u16, 75);
1620        assert_eq!(PixelFormat::Bgr565Le as u16, 76);
1621        assert_eq!(PixelFormat::Bgr565Be as u16, 77);
1622        assert_eq!(PixelFormat::Rgbx as u16, 78);
1623        assert_eq!(PixelFormat::Bgrx as u16, 79);
1624        assert_eq!(PixelFormat::Xrgb as u16, 80);
1625        assert_eq!(PixelFormat::Xbgr as u16, 81);
1626    }
1627
1628    #[test]
1629    fn sample_format_discriminants_pinned() {
1630        assert_eq!(SampleFormat::U8 as u8, 0);
1631        assert_eq!(SampleFormat::S8 as u8, 1);
1632        assert_eq!(SampleFormat::S16 as u8, 2);
1633        assert_eq!(SampleFormat::S24 as u8, 3);
1634        assert_eq!(SampleFormat::S32 as u8, 4);
1635        assert_eq!(SampleFormat::F32 as u8, 5);
1636        assert_eq!(SampleFormat::F64 as u8, 6);
1637        assert_eq!(SampleFormat::U8P as u8, 7);
1638        assert_eq!(SampleFormat::S16P as u8, 8);
1639        assert_eq!(SampleFormat::S32P as u8, 9);
1640        assert_eq!(SampleFormat::F32P as u8, 10);
1641        assert_eq!(SampleFormat::F64P as u8, 11);
1642    }
1643
1644    #[test]
1645    fn high_bit_yuv_planar_metadata() {
1646        // 10-bit reference variants are planar with three planes.
1647        assert!(PixelFormat::Yuv420P10Le.is_planar());
1648        assert!(PixelFormat::Yuv422P10Le.is_planar());
1649        assert!(PixelFormat::Yuv444P10Le.is_planar());
1650
1651        // 12-bit variants must follow the same shape.
1652        assert!(PixelFormat::Yuv420P12Le.is_planar());
1653        assert!(PixelFormat::Yuv422P12Le.is_planar());
1654        assert!(PixelFormat::Yuv444P12Le.is_planar());
1655
1656        assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
1657        assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
1658        assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
1659
1660        // 16-bit variants must follow the same shape.
1661        for fmt in [
1662            PixelFormat::Yuv420P16Le,
1663            PixelFormat::Yuv422P16Le,
1664            PixelFormat::Yuv444P16Le,
1665        ] {
1666            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1667            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1668        }
1669
1670        // None of the high-bit YUV variants carry alpha or palette.
1671        assert!(!PixelFormat::Yuv422P12Le.has_alpha());
1672        assert!(!PixelFormat::Yuv444P12Le.has_alpha());
1673        assert!(!PixelFormat::Yuv422P12Le.is_palette());
1674        assert!(!PixelFormat::Yuv444P12Le.is_palette());
1675        assert!(!PixelFormat::Yuv420P16Le.has_alpha());
1676        assert!(!PixelFormat::Yuv422P16Le.has_alpha());
1677        assert!(!PixelFormat::Yuv444P16Le.has_alpha());
1678        assert!(!PixelFormat::Yuv420P16Le.is_palette());
1679        assert!(!PixelFormat::Yuv422P16Le.is_palette());
1680        assert!(!PixelFormat::Yuv444P16Le.is_palette());
1681    }
1682
1683    #[test]
1684    fn channel_layout_round_trip_count_for_known_layouts() {
1685        // For every `n` that `from_count` maps to a named layout, the
1686        // resulting layout's `channel_count()` must equal `n` again.
1687        for n in 1..=8u16 {
1688            let layout = ChannelLayout::from_count(n);
1689            assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
1690            // None of these defaults should fall through to DiscreteN.
1691            assert!(
1692                !matches!(layout, ChannelLayout::DiscreteN(_)),
1693                "from_count({n}) unexpectedly produced DiscreteN"
1694            );
1695        }
1696    }
1697
1698    #[test]
1699    fn channel_layout_from_count_default_table() {
1700        // The exact mapping documented on `from_count` — pin it so
1701        // future refactors don't silently change the inferred layout.
1702        assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
1703        assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
1704        assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
1705        assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
1706        assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
1707        assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
1708        assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
1709        assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
1710    }
1711
1712    #[test]
1713    fn channel_layout_unknown_count_falls_through_to_discrete() {
1714        assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
1715        assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
1716        assert_eq!(
1717            ChannelLayout::from_count(64).channel_count(),
1718            64,
1719            "DiscreteN must report the count it was constructed with"
1720        );
1721    }
1722
1723    #[test]
1724    fn channel_layout_position_lookup() {
1725        assert_eq!(
1726            ChannelLayout::Stereo.position(0),
1727            Some(ChannelPosition::FrontLeft)
1728        );
1729        assert_eq!(
1730            ChannelLayout::Stereo.position(1),
1731            Some(ChannelPosition::FrontRight)
1732        );
1733        assert_eq!(ChannelLayout::Stereo.position(2), None);
1734
1735        // 5.1 canonical: L, R, C, LFE, Ls, Rs.
1736        let s51 = ChannelLayout::Surround51;
1737        assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
1738        assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
1739        assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
1740        assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
1741        assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
1742        assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
1743        assert_eq!(s51.position(6), None);
1744
1745        // DiscreteN never reveals a position.
1746        assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
1747    }
1748
1749    #[test]
1750    fn channel_layout_lfe_and_surround_predicates() {
1751        assert!(ChannelLayout::Surround51.has_lfe());
1752        assert!(ChannelLayout::Surround71.has_lfe());
1753        assert!(ChannelLayout::Stereo21.has_lfe());
1754        assert!(!ChannelLayout::Quad.has_lfe());
1755        assert!(!ChannelLayout::Surround50.has_lfe());
1756        assert!(!ChannelLayout::Stereo.has_lfe());
1757
1758        assert!(!ChannelLayout::Mono.is_surround());
1759        assert!(!ChannelLayout::Stereo.is_surround());
1760        // Downmix carriers are still 2ch / no-LFE → not "surround" by
1761        // the layout-shape definition; the surround info lives in the
1762        // sample matrix itself.
1763        assert!(!ChannelLayout::LoRo.is_surround());
1764        assert!(!ChannelLayout::LtRt.is_surround());
1765        assert!(ChannelLayout::Stereo21.is_surround());
1766        assert!(ChannelLayout::Surround51.is_surround());
1767        assert!(ChannelLayout::Surround71.is_surround());
1768    }
1769
1770    #[test]
1771    fn channel_layout_display_and_fromstr_round_trip() {
1772        use std::str::FromStr;
1773        let cases = [
1774            ChannelLayout::Mono,
1775            ChannelLayout::Stereo,
1776            ChannelLayout::Stereo21,
1777            ChannelLayout::Surround30,
1778            ChannelLayout::Quad,
1779            ChannelLayout::Surround40,
1780            ChannelLayout::Surround41,
1781            ChannelLayout::Surround50,
1782            ChannelLayout::Surround51,
1783            ChannelLayout::Surround60,
1784            ChannelLayout::Surround61,
1785            ChannelLayout::Surround70,
1786            ChannelLayout::Surround71,
1787            ChannelLayout::LoRo,
1788            ChannelLayout::LtRt,
1789            ChannelLayout::DiscreteN(13),
1790        ];
1791        for layout in cases {
1792            let s = layout.to_string();
1793            let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
1794            assert_eq!(parsed, layout, "round-trip failed via {s:?}");
1795        }
1796    }
1797
1798    #[test]
1799    fn channel_layout_fromstr_accepts_aliases_and_case() {
1800        use std::str::FromStr;
1801        assert_eq!(
1802            ChannelLayout::from_str("STEREO").unwrap(),
1803            ChannelLayout::Stereo
1804        );
1805        assert_eq!(
1806            ChannelLayout::from_str("2.0").unwrap(),
1807            ChannelLayout::Stereo
1808        );
1809        assert_eq!(
1810            ChannelLayout::from_str("5.1").unwrap(),
1811            ChannelLayout::Surround51
1812        );
1813        assert_eq!(
1814            ChannelLayout::from_str("Lo/Ro").unwrap(),
1815            ChannelLayout::LoRo
1816        );
1817        assert_eq!(
1818            ChannelLayout::from_str("lt/rt").unwrap(),
1819            ChannelLayout::LtRt
1820        );
1821        assert!(ChannelLayout::from_str("absurd_layout").is_err());
1822    }
1823
1824    #[test]
1825    fn channel_layout_positions_owned_matches_static_slice() {
1826        for layout in [
1827            ChannelLayout::Mono,
1828            ChannelLayout::Surround51,
1829            ChannelLayout::Surround71,
1830        ] {
1831            assert_eq!(layout.positions_owned(), layout.positions());
1832        }
1833        // DiscreteN returns an empty owned vec — positions are unknown.
1834        assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
1835    }
1836
1837    #[test]
1838    fn sample_format_plane_count_interleaved_is_one() {
1839        // Interleaved formats always pack into a single plane, regardless
1840        // of channel count.
1841        for ch in [1u16, 2, 6, 8, 64, 0] {
1842            assert_eq!(SampleFormat::S16.plane_count(ch), 1);
1843            assert_eq!(SampleFormat::F32.plane_count(ch), 1);
1844            assert_eq!(SampleFormat::U8.plane_count(ch), 1);
1845            assert_eq!(SampleFormat::S24.plane_count(ch), 1);
1846        }
1847    }
1848
1849    #[test]
1850    fn sample_format_plane_count_planar_matches_channels() {
1851        // Planar formats use one plane per channel.
1852        assert_eq!(SampleFormat::S16P.plane_count(1), 1);
1853        assert_eq!(SampleFormat::S16P.plane_count(2), 2);
1854        assert_eq!(SampleFormat::F32P.plane_count(6), 6);
1855        assert_eq!(SampleFormat::F64P.plane_count(8), 8);
1856
1857        // Edge case: zero channels in a planar format yields zero planes.
1858        assert_eq!(SampleFormat::S32P.plane_count(0), 0);
1859    }
1860
1861    #[test]
1862    fn high_bit_yuv_bits_per_pixel_approx() {
1863        // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
1864        // packed-bits estimator (the approximation reports samples-per-pixel
1865        // density, not the 16-bit storage width).
1866        assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
1867        assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
1868        assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
1869        assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
1870        assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
1871
1872        // 16-bit: packed bits == storage bits (every bit of the 16-bit
1873        // word is significant), so the estimator lands on the same
1874        // numbers as the 10/12-bit siblings.
1875        assert_eq!(PixelFormat::Yuv420P16Le.bits_per_pixel_approx(), 24);
1876        assert_eq!(PixelFormat::Yuv422P16Le.bits_per_pixel_approx(), 32);
1877        assert_eq!(PixelFormat::Yuv444P16Le.bits_per_pixel_approx(), 48);
1878    }
1879
1880    #[test]
1881    fn yuva_planar_metadata() {
1882        // All three alpha-carrying planar YUV samplings share one shape:
1883        // planar, 4 planes (Y, U, V, full-resolution A), alpha set, not
1884        // a palette format.
1885        for fmt in [
1886            PixelFormat::Yuva420P,
1887            PixelFormat::Yuva422P,
1888            PixelFormat::Yuva444P,
1889        ] {
1890            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1891            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1892            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1893            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1894        }
1895
1896        // Packed-bits estimator: the alpha plane adds a full 8 bits per
1897        // pixel on top of the alpha-less sampling's density.
1898        assert_eq!(
1899            PixelFormat::Yuva420P.bits_per_pixel_approx(),
1900            PixelFormat::Yuv420P.bits_per_pixel_approx() + 8
1901        );
1902        assert_eq!(
1903            PixelFormat::Yuva422P.bits_per_pixel_approx(),
1904            PixelFormat::Yuv422P.bits_per_pixel_approx() + 8
1905        );
1906        assert_eq!(
1907            PixelFormat::Yuva444P.bits_per_pixel_approx(),
1908            PixelFormat::Yuv444P.bits_per_pixel_approx() + 8
1909        );
1910        assert_eq!(PixelFormat::Yuva422P.bits_per_pixel_approx(), 24);
1911        assert_eq!(PixelFormat::Yuva444P.bits_per_pixel_approx(), 32);
1912    }
1913
1914    #[test]
1915    fn deep_yuva_planar_metadata() {
1916        // All six deep alpha-carrying variants share one shape: planar,
1917        // 4 planes (Y, U, V, full-resolution A), alpha set, no palette.
1918        for fmt in [
1919            PixelFormat::Yuva422P10Le,
1920            PixelFormat::Yuva422P12Le,
1921            PixelFormat::Yuva444P10Le,
1922            PixelFormat::Yuva444P12Le,
1923            PixelFormat::Yuva422P16Le,
1924            PixelFormat::Yuva444P16Le,
1925        ] {
1926            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1927            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1928            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1929            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1930        }
1931    }
1932
1933    #[test]
1934    fn deep_yuva_bits_per_pixel_approx() {
1935        // Estimator reports 16-bit-word storage cost, matching the
1936        // alpha-less deep YUV trio: the full-resolution alpha word adds
1937        // 16 on top of the alpha-less sampling's number.
1938        for fmt in [
1939            PixelFormat::Yuva422P10Le,
1940            PixelFormat::Yuva422P12Le,
1941            PixelFormat::Yuva422P16Le,
1942        ] {
1943            assert_eq!(fmt.bits_per_pixel_approx(), 48, "{fmt:?}");
1944        }
1945        for fmt in [
1946            PixelFormat::Yuva444P10Le,
1947            PixelFormat::Yuva444P12Le,
1948            PixelFormat::Yuva444P16Le,
1949        ] {
1950            assert_eq!(fmt.bits_per_pixel_approx(), 64, "{fmt:?}");
1951        }
1952        assert_eq!(
1953            PixelFormat::Yuva422P16Le.bits_per_pixel_approx(),
1954            PixelFormat::Yuv422P16Le.bits_per_pixel_approx() + 16
1955        );
1956        assert_eq!(
1957            PixelFormat::Yuva444P16Le.bits_per_pixel_approx(),
1958            PixelFormat::Yuv444P16Le.bits_per_pixel_approx() + 16
1959        );
1960        assert_eq!(
1961            PixelFormat::Yuva422P10Le.bits_per_pixel_approx(),
1962            PixelFormat::Yuv422P10Le.bits_per_pixel_approx() + 16
1963        );
1964        assert_eq!(
1965            PixelFormat::Yuva444P12Le.bits_per_pixel_approx(),
1966            PixelFormat::Yuv444P12Le.bits_per_pixel_approx() + 16
1967        );
1968    }
1969
1970    #[test]
1971    fn high_bit_gbr_planar_metadata() {
1972        // All six new variants are planar with the right plane count.
1973        for fmt in [
1974            PixelFormat::Gbrp10Le,
1975            PixelFormat::Gbrp12Le,
1976            PixelFormat::Gbrp14Le,
1977        ] {
1978            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1979            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1980            assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1981            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1982        }
1983        for fmt in [
1984            PixelFormat::Gbrap10Le,
1985            PixelFormat::Gbrap12Le,
1986            PixelFormat::Gbrap14Le,
1987        ] {
1988            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1989            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1990            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1991            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1992        }
1993    }
1994
1995    #[test]
1996    fn high_bit_gbr_bits_per_pixel_approx() {
1997        // Packed bits-per-pixel = samples × bits (consistent with how
1998        // the 10/12-bit YUV variants are reported above).
1999        assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30);
2000        assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40);
2001        assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36);
2002        assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48);
2003        assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42);
2004        assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56);
2005    }
2006
2007    #[test]
2008    fn high_bit_gbr_constructible_and_distinct() {
2009        // Round-trip the discriminant through `as u16` and back via the
2010        // pinning test's reverse mapping — every variant must be unique.
2011        let all = [
2012            PixelFormat::Gbrp10Le,
2013            PixelFormat::Gbrap10Le,
2014            PixelFormat::Gbrp12Le,
2015            PixelFormat::Gbrap12Le,
2016            PixelFormat::Gbrp14Le,
2017            PixelFormat::Gbrap14Le,
2018            PixelFormat::Gbrp8,
2019            PixelFormat::Gbrap8,
2020            PixelFormat::Gbrp16Le,
2021            PixelFormat::Gbrap16Le,
2022        ];
2023        let mut seen = std::collections::HashSet::new();
2024        for fmt in all {
2025            assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
2026        }
2027    }
2028
2029    #[test]
2030    fn gbr_depth_ladder_ends_metadata() {
2031        // Gbrp8 and the 16-bit pair share the family shape: planar,
2032        // G/B/R plane order (3 planes), alpha only on Gbrap16Le, never
2033        // palette.
2034        for fmt in [PixelFormat::Gbrp8, PixelFormat::Gbrp16Le] {
2035            assert!(fmt.is_planar(), "{fmt:?} must be planar");
2036            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
2037            assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
2038            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
2039        }
2040        assert!(PixelFormat::Gbrap16Le.is_planar());
2041        assert_eq!(PixelFormat::Gbrap16Le.plane_count(), 4);
2042        assert!(PixelFormat::Gbrap16Le.has_alpha());
2043        assert!(!PixelFormat::Gbrap16Le.is_palette());
2044    }
2045
2046    #[test]
2047    fn gbr_depth_ladder_ends_bits_per_pixel_approx() {
2048        // Gbrp8 matches the packed 8-bit RGB density (planar layout
2049        // doesn't change bits-per-pixel), and the 16-bit pair matches
2050        // the packed 16-bit RGB(A) densities — for 16-bit words packed
2051        // bits equal storage bits.
2052        assert_eq!(
2053            PixelFormat::Gbrp8.bits_per_pixel_approx(),
2054            PixelFormat::Rgb24.bits_per_pixel_approx()
2055        );
2056        assert_eq!(
2057            PixelFormat::Gbrp16Le.bits_per_pixel_approx(),
2058            PixelFormat::Rgb48Le.bits_per_pixel_approx()
2059        );
2060        assert_eq!(
2061            PixelFormat::Gbrap16Le.bits_per_pixel_approx(),
2062            PixelFormat::Rgba64Le.bits_per_pixel_approx()
2063        );
2064        assert_eq!(PixelFormat::Gbrp8.bits_per_pixel_approx(), 24);
2065        assert_eq!(PixelFormat::Gbrp16Le.bits_per_pixel_approx(), 48);
2066        assert_eq!(PixelFormat::Gbrap16Le.bits_per_pixel_approx(), 64);
2067    }
2068
2069    #[test]
2070    fn gbrap8_metadata() {
2071        // Gbrap8 completes the GBR(A) family: every depth on the
2072        // 8/10/12/14/16 ladder now has both an alpha-less and an
2073        // alpha-carrying variant. Shape matches the rest of the
2074        // alpha-carrying family: planar, 4 planes (G, B, R,
2075        // full-resolution A), alpha set, never palette.
2076        let fmt = PixelFormat::Gbrap8;
2077        assert!(fmt.is_planar());
2078        assert_eq!(fmt.plane_count(), 4);
2079        assert!(fmt.has_alpha());
2080        assert!(!fmt.is_palette());
2081    }
2082
2083    #[test]
2084    fn gbrap8_bits_per_pixel_approx() {
2085        // Four bytes per pixel: the packed Rgba density (planar layout
2086        // doesn't change bits-per-pixel), i.e. the alpha plane adds a
2087        // full 8 bits on top of Gbrp8.
2088        assert_eq!(PixelFormat::Gbrap8.bits_per_pixel_approx(), 32);
2089        assert_eq!(
2090            PixelFormat::Gbrap8.bits_per_pixel_approx(),
2091            PixelFormat::Rgba.bits_per_pixel_approx()
2092        );
2093        assert_eq!(
2094            PixelFormat::Gbrap8.bits_per_pixel_approx(),
2095            PixelFormat::Gbrp8.bits_per_pixel_approx() + 8
2096        );
2097    }
2098
2099    #[test]
2100    fn gbr_family_alpha_ladder_complete() {
2101        // Every GBR depth has an alpha companion with exactly one more
2102        // plane and the same planarity — the asymmetry Gbrap8 closed.
2103        let pairs = [
2104            (PixelFormat::Gbrp8, PixelFormat::Gbrap8),
2105            (PixelFormat::Gbrp10Le, PixelFormat::Gbrap10Le),
2106            (PixelFormat::Gbrp12Le, PixelFormat::Gbrap12Le),
2107            (PixelFormat::Gbrp14Le, PixelFormat::Gbrap14Le),
2108            (PixelFormat::Gbrp16Le, PixelFormat::Gbrap16Le),
2109        ];
2110        for (gbr, gbra) in pairs {
2111            assert!(gbr.is_planar() && gbra.is_planar());
2112            assert_eq!(gbr.plane_count(), 3, "{gbr:?}");
2113            assert_eq!(gbra.plane_count(), 4, "{gbra:?}");
2114            assert!(!gbr.has_alpha(), "{gbr:?}");
2115            assert!(gbra.has_alpha(), "{gbra:?}");
2116        }
2117    }
2118
2119    #[test]
2120    fn ya16le_metadata() {
2121        // Same packed shape as Ya8 (interleaved Y, A in one plane),
2122        // widened to 16-bit LE words: not planar, single plane, alpha
2123        // set, never palette. Density is exactly double Ya8's and
2124        // matches half of Rgba64Le (two components instead of four).
2125        let fmt = PixelFormat::Ya16Le;
2126        assert!(!fmt.is_planar());
2127        assert_eq!(fmt.plane_count(), 1);
2128        assert!(fmt.has_alpha());
2129        assert!(!fmt.is_palette());
2130        assert_eq!(fmt.bits_per_pixel_approx(), 32);
2131        assert_eq!(
2132            fmt.bits_per_pixel_approx(),
2133            PixelFormat::Ya8.bits_per_pixel_approx() * 2
2134        );
2135        assert_eq!(
2136            fmt.bits_per_pixel_approx(),
2137            PixelFormat::Rgba64Le.bits_per_pixel_approx() / 2
2138        );
2139        // The alpha word adds a full 16 bits on top of Gray16Le.
2140        assert_eq!(
2141            fmt.bits_per_pixel_approx(),
2142            PixelFormat::Gray16Le.bits_per_pixel_approx() + 16
2143        );
2144    }
2145
2146    #[test]
2147    fn cmyk_inverted_metadata() {
2148        // The inverted-ink convention changes sample semantics, not
2149        // layout: CmykInverted must be metadata-identical to Cmyk on
2150        // every shape predicate.
2151        let (reg, inv) = (PixelFormat::Cmyk, PixelFormat::CmykInverted);
2152        for fmt in [reg, inv] {
2153            assert!(!fmt.is_planar(), "{fmt:?}");
2154            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2155            assert!(!fmt.has_alpha(), "{fmt:?}");
2156            assert!(!fmt.is_palette(), "{fmt:?}");
2157        }
2158        assert_eq!(reg.bits_per_pixel_approx(), inv.bits_per_pixel_approx());
2159        assert_eq!(inv.bits_per_pixel_approx(), 32);
2160        // They remain distinct formats on the wire-stable axis.
2161        assert_ne!(reg as u16, inv as u16);
2162    }
2163
2164    #[test]
2165    fn deep_yuva420_planar_metadata() {
2166        // The 4:2:0 completions share the deep-Yuva shape: planar, 4
2167        // planes (Y, U, V, full-resolution A), alpha set, no palette.
2168        for fmt in [
2169            PixelFormat::Yuva420P10Le,
2170            PixelFormat::Yuva420P12Le,
2171            PixelFormat::Yuva420P16Le,
2172        ] {
2173            assert!(fmt.is_planar(), "{fmt:?} must be planar");
2174            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
2175            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
2176            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
2177        }
2178    }
2179
2180    #[test]
2181    fn deep_yuva420_bits_per_pixel_approx() {
2182        // Same estimator convention as the 4:2:2/4:4:4 deep Yuva arms:
2183        // 16-bit-word storage cost, with the full-resolution alpha word
2184        // adding 16 on top of the alpha-less sampling's number.
2185        for fmt in [
2186            PixelFormat::Yuva420P10Le,
2187            PixelFormat::Yuva420P12Le,
2188            PixelFormat::Yuva420P16Le,
2189        ] {
2190            assert_eq!(fmt.bits_per_pixel_approx(), 40, "{fmt:?}");
2191        }
2192        assert_eq!(
2193            PixelFormat::Yuva420P10Le.bits_per_pixel_approx(),
2194            PixelFormat::Yuv420P10Le.bits_per_pixel_approx() + 16
2195        );
2196        assert_eq!(
2197            PixelFormat::Yuva420P12Le.bits_per_pixel_approx(),
2198            PixelFormat::Yuv420P12Le.bits_per_pixel_approx() + 16
2199        );
2200        assert_eq!(
2201            PixelFormat::Yuva420P16Le.bits_per_pixel_approx(),
2202            PixelFormat::Yuv420P16Le.bits_per_pixel_approx() + 16
2203        );
2204    }
2205
2206    /// Every `PixelFormat` variant, in discriminant order. Extend this
2207    /// list whenever a variant is appended — the consistency tests
2208    /// below sweep it.
2209    const ALL_PIXEL_FORMATS: [PixelFormat; 82] = [
2210        PixelFormat::Yuv420P,
2211        PixelFormat::Yuv422P,
2212        PixelFormat::Yuv444P,
2213        PixelFormat::Rgb24,
2214        PixelFormat::Rgba,
2215        PixelFormat::Gray8,
2216        PixelFormat::Pal8,
2217        PixelFormat::Bgr24,
2218        PixelFormat::Bgra,
2219        PixelFormat::Argb,
2220        PixelFormat::Abgr,
2221        PixelFormat::Rgb48Le,
2222        PixelFormat::Rgba64Le,
2223        PixelFormat::Gray16Le,
2224        PixelFormat::Gray10Le,
2225        PixelFormat::Gray12Le,
2226        PixelFormat::Yuv420P10Le,
2227        PixelFormat::Yuv422P10Le,
2228        PixelFormat::Yuv444P10Le,
2229        PixelFormat::Yuv420P12Le,
2230        PixelFormat::Yuv422P12Le,
2231        PixelFormat::Yuv444P12Le,
2232        PixelFormat::YuvJ420P,
2233        PixelFormat::YuvJ422P,
2234        PixelFormat::YuvJ444P,
2235        PixelFormat::Nv12,
2236        PixelFormat::Nv21,
2237        PixelFormat::Ya8,
2238        PixelFormat::Yuva420P,
2239        PixelFormat::MonoBlack,
2240        PixelFormat::MonoWhite,
2241        PixelFormat::Yuyv422,
2242        PixelFormat::Uyvy422,
2243        PixelFormat::Cmyk,
2244        PixelFormat::Yuv411P,
2245        PixelFormat::Gbrp10Le,
2246        PixelFormat::Gbrap10Le,
2247        PixelFormat::Gbrp12Le,
2248        PixelFormat::Gbrap12Le,
2249        PixelFormat::Gbrp14Le,
2250        PixelFormat::Gbrap14Le,
2251        PixelFormat::Yuv420P16Le,
2252        PixelFormat::Yuv422P16Le,
2253        PixelFormat::Yuv444P16Le,
2254        PixelFormat::Yuva422P,
2255        PixelFormat::Yuva444P,
2256        PixelFormat::Yuva422P10Le,
2257        PixelFormat::Yuva422P12Le,
2258        PixelFormat::Yuva444P10Le,
2259        PixelFormat::Yuva444P12Le,
2260        PixelFormat::Yuva422P16Le,
2261        PixelFormat::Yuva444P16Le,
2262        PixelFormat::Gbrp8,
2263        PixelFormat::Gbrp16Le,
2264        PixelFormat::Gbrap16Le,
2265        PixelFormat::Yuva420P10Le,
2266        PixelFormat::Yuva420P12Le,
2267        PixelFormat::Yuva420P16Le,
2268        PixelFormat::Gbrap8,
2269        PixelFormat::Ya16Le,
2270        PixelFormat::CmykInverted,
2271        PixelFormat::Yuv440P,
2272        PixelFormat::Yuv440P10Le,
2273        PixelFormat::Yuv440P12Le,
2274        PixelFormat::Yuv440P16Le,
2275        PixelFormat::GrayF32Le,
2276        PixelFormat::RgbF32Le,
2277        PixelFormat::RgbaF32Le,
2278        PixelFormat::GbrpF32Le,
2279        PixelFormat::GbrapF32Le,
2280        PixelFormat::Rgb555Le,
2281        PixelFormat::Rgb555Be,
2282        PixelFormat::Bgr555Le,
2283        PixelFormat::Bgr555Be,
2284        PixelFormat::Rgb565Le,
2285        PixelFormat::Rgb565Be,
2286        PixelFormat::Bgr565Le,
2287        PixelFormat::Bgr565Be,
2288        PixelFormat::Rgbx,
2289        PixelFormat::Bgrx,
2290        PixelFormat::Xrgb,
2291        PixelFormat::Xbgr,
2292    ];
2293
2294    #[test]
2295    fn all_pixel_formats_list_is_complete_and_distinct() {
2296        // The list is discriminant-ordered and dense: 0..82 with no
2297        // gaps and no duplicates. A newly appended variant that isn't
2298        // added to the list will break the length or density check.
2299        let mut seen = std::collections::HashSet::new();
2300        for fmt in ALL_PIXEL_FORMATS {
2301            assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
2302        }
2303        for d in 0..ALL_PIXEL_FORMATS.len() as u16 {
2304            assert!(seen.contains(&d), "discriminant {d} missing from list");
2305        }
2306    }
2307
2308    #[test]
2309    fn yuv440_family_metadata() {
2310        // The whole 4:4:0 ladder shares one shape: planar, 3 planes,
2311        // no alpha, no palette, full-width half-height chroma.
2312        for fmt in [
2313            PixelFormat::Yuv440P,
2314            PixelFormat::Yuv440P10Le,
2315            PixelFormat::Yuv440P12Le,
2316            PixelFormat::Yuv440P16Le,
2317        ] {
2318            assert!(fmt.is_planar(), "{fmt:?} must be planar");
2319            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
2320            assert!(!fmt.has_alpha(), "{fmt:?} must not carry alpha");
2321            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
2322            assert!(!fmt.is_float(), "{fmt:?} must not be float");
2323            assert_eq!(
2324                fmt.chroma_subsampling(),
2325                Some((0, 1)),
2326                "{fmt:?} must be full-width, half-height chroma"
2327            );
2328        }
2329    }
2330
2331    #[test]
2332    fn yuv440_bits_per_pixel_approx() {
2333        // 4:4:0 packs the same samples-per-pixel as 4:2:2 at every
2334        // depth (2 samples/pixel), so the estimator numbers coincide.
2335        assert_eq!(
2336            PixelFormat::Yuv440P.bits_per_pixel_approx(),
2337            PixelFormat::Yuv422P.bits_per_pixel_approx()
2338        );
2339        assert_eq!(PixelFormat::Yuv440P.bits_per_pixel_approx(), 16);
2340        for (f440, f422) in [
2341            (PixelFormat::Yuv440P10Le, PixelFormat::Yuv422P10Le),
2342            (PixelFormat::Yuv440P12Le, PixelFormat::Yuv422P12Le),
2343            (PixelFormat::Yuv440P16Le, PixelFormat::Yuv422P16Le),
2344        ] {
2345            assert_eq!(
2346                f440.bits_per_pixel_approx(),
2347                f422.bits_per_pixel_approx(),
2348                "{f440:?}"
2349            );
2350            assert_eq!(f440.bits_per_pixel_approx(), 32, "{f440:?}");
2351        }
2352    }
2353
2354    #[test]
2355    fn yuv440_plane_geometry() {
2356        // Even sizes: chroma keeps the width, halves the height.
2357        assert_eq!(
2358            PixelFormat::Yuv440P.plane_dimensions(0, 640, 480),
2359            Some((640, 480))
2360        );
2361        assert_eq!(
2362            PixelFormat::Yuv440P.plane_dimensions(1, 640, 480),
2363            Some((640, 240))
2364        );
2365        assert_eq!(
2366            PixelFormat::Yuv440P.plane_dimensions(2, 640, 480),
2367            Some((640, 240))
2368        );
2369        // Odd height rounds up; odd width is untouched (ssx = 0).
2370        for fmt in [
2371            PixelFormat::Yuv440P,
2372            PixelFormat::Yuv440P10Le,
2373            PixelFormat::Yuv440P12Le,
2374            PixelFormat::Yuv440P16Le,
2375        ] {
2376            assert_eq!(fmt.plane_dimensions(0, 7, 5), Some((7, 5)), "{fmt:?}");
2377            assert_eq!(fmt.plane_dimensions(1, 7, 5), Some((7, 3)), "{fmt:?}");
2378            assert_eq!(fmt.plane_dimensions(2, 7, 5), Some((7, 3)), "{fmt:?}");
2379            assert_eq!(fmt.plane_dimensions(3, 7, 5), None, "{fmt:?}");
2380        }
2381        // Degenerate 1-row picture: the chroma plane still has a row.
2382        assert_eq!(PixelFormat::Yuv440P.plane_dimensions(1, 3, 1), Some((3, 1)));
2383    }
2384
2385    #[test]
2386    fn yuv440_sizing_round_trips() {
2387        // 6×5 8-bit: luma 6×5 = 30, each chroma 6×ceil(5/2) = 18.
2388        assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(0, 6, 5), Some(30));
2389        assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(1, 6, 5), Some(18));
2390        assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(2, 6, 5), Some(18));
2391        assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
2392        // 7×5: 35 + 21 + 21.
2393        assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(7, 5), Some(77));
2394        // Deep variants store 16-bit words: exactly double at every
2395        // depth (row bytes = width × 2 regardless of valid bits).
2396        for fmt in [
2397            PixelFormat::Yuv440P10Le,
2398            PixelFormat::Yuv440P12Le,
2399            PixelFormat::Yuv440P16Le,
2400        ] {
2401            assert_eq!(fmt.plane_row_bytes(0, 7), Some(14), "{fmt:?}");
2402            assert_eq!(fmt.plane_row_bytes(1, 7), Some(14), "{fmt:?}");
2403            assert_eq!(fmt.frame_size_bytes(7, 5), Some(154), "{fmt:?}");
2404        }
2405    }
2406
2407    #[test]
2408    fn float_family_metadata() {
2409        // Packed trio: single plane, not planar.
2410        for fmt in [
2411            PixelFormat::GrayF32Le,
2412            PixelFormat::RgbF32Le,
2413            PixelFormat::RgbaF32Le,
2414        ] {
2415            assert!(!fmt.is_planar(), "{fmt:?}");
2416            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2417        }
2418        // Planar pair: GBR(A) shape.
2419        assert!(PixelFormat::GbrpF32Le.is_planar());
2420        assert_eq!(PixelFormat::GbrpF32Le.plane_count(), 3);
2421        assert!(PixelFormat::GbrapF32Le.is_planar());
2422        assert_eq!(PixelFormat::GbrapF32Le.plane_count(), 4);
2423        // Alpha only on the RGBA/GBRA members.
2424        assert!(!PixelFormat::GrayF32Le.has_alpha());
2425        assert!(!PixelFormat::RgbF32Le.has_alpha());
2426        assert!(PixelFormat::RgbaF32Le.has_alpha());
2427        assert!(!PixelFormat::GbrpF32Le.has_alpha());
2428        assert!(PixelFormat::GbrapF32Le.has_alpha());
2429        // The whole family is float, non-palette, and has no chroma
2430        // grid.
2431        for fmt in [
2432            PixelFormat::GrayF32Le,
2433            PixelFormat::RgbF32Le,
2434            PixelFormat::RgbaF32Le,
2435            PixelFormat::GbrpF32Le,
2436            PixelFormat::GbrapF32Le,
2437        ] {
2438            assert!(fmt.is_float(), "{fmt:?} must be float");
2439            assert!(!fmt.is_palette(), "{fmt:?}");
2440            assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2441        }
2442    }
2443
2444    #[test]
2445    fn is_float_false_for_integer_formats() {
2446        for fmt in ALL_PIXEL_FORMATS {
2447            let expect = matches!(
2448                fmt,
2449                PixelFormat::GrayF32Le
2450                    | PixelFormat::RgbF32Le
2451                    | PixelFormat::RgbaF32Le
2452                    | PixelFormat::GbrpF32Le
2453                    | PixelFormat::GbrapF32Le
2454            );
2455            assert_eq!(fmt.is_float(), expect, "{fmt:?}");
2456        }
2457    }
2458
2459    #[test]
2460    fn float_family_bits_per_pixel_and_sizing() {
2461        // Packed bits equal storage bits: every sample is a full
2462        // binary32 word.
2463        assert_eq!(PixelFormat::GrayF32Le.bits_per_pixel_approx(), 32);
2464        assert_eq!(PixelFormat::RgbF32Le.bits_per_pixel_approx(), 96);
2465        assert_eq!(PixelFormat::RgbaF32Le.bits_per_pixel_approx(), 128);
2466        assert_eq!(PixelFormat::GbrpF32Le.bits_per_pixel_approx(), 96);
2467        assert_eq!(PixelFormat::GbrapF32Le.bits_per_pixel_approx(), 128);
2468        // Packed row/frame sizes.
2469        assert_eq!(PixelFormat::GrayF32Le.plane_row_bytes(0, 3), Some(12));
2470        assert_eq!(PixelFormat::GrayF32Le.frame_size_bytes(5, 3), Some(60));
2471        assert_eq!(PixelFormat::RgbF32Le.plane_row_bytes(0, 7), Some(84));
2472        assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
2473        // Planar float: 4 bytes per sample on every plane; the packed
2474        // and planar layouts of the same component set cost the same.
2475        assert_eq!(PixelFormat::GbrpF32Le.plane_row_bytes(1, 7), Some(28));
2476        assert_eq!(
2477            PixelFormat::GbrpF32Le.frame_size_bytes(7, 5),
2478            PixelFormat::RgbF32Le.frame_size_bytes(7, 5)
2479        );
2480        assert_eq!(
2481            PixelFormat::GbrapF32Le.frame_size_bytes(7, 5),
2482            PixelFormat::RgbaF32Le.frame_size_bytes(7, 5)
2483        );
2484        // All planes of planar float GBR(A) are full resolution.
2485        for plane in 0..4 {
2486            assert_eq!(
2487                PixelFormat::GbrapF32Le.plane_dimensions(plane, 7, 5),
2488                Some((7, 5))
2489            );
2490        }
2491    }
2492
2493    #[test]
2494    fn chroma_subsampling_table() {
2495        use PixelFormat::*;
2496        // One representative per sampling class plus the full new
2497        // family; the wildcard class returns None.
2498        assert_eq!(Yuv420P.chroma_subsampling(), Some((1, 1)));
2499        assert_eq!(Nv12.chroma_subsampling(), Some((1, 1)));
2500        assert_eq!(Yuva420P16Le.chroma_subsampling(), Some((1, 1)));
2501        assert_eq!(Yuv422P.chroma_subsampling(), Some((1, 0)));
2502        assert_eq!(Yuyv422.chroma_subsampling(), Some((1, 0)));
2503        assert_eq!(Uyvy422.chroma_subsampling(), Some((1, 0)));
2504        assert_eq!(Yuv444P.chroma_subsampling(), Some((0, 0)));
2505        assert_eq!(Yuva444P12Le.chroma_subsampling(), Some((0, 0)));
2506        assert_eq!(Yuv411P.chroma_subsampling(), Some((2, 0)));
2507        assert_eq!(Yuv440P.chroma_subsampling(), Some((0, 1)));
2508        assert_eq!(Yuv440P16Le.chroma_subsampling(), Some((0, 1)));
2509        for fmt in [
2510            Gray8,
2511            Gray16Le,
2512            Ya8,
2513            Ya16Le,
2514            Pal8,
2515            MonoBlack,
2516            MonoWhite,
2517            Rgb24,
2518            Rgba,
2519            Rgb48Le,
2520            Rgba64Le,
2521            Cmyk,
2522            CmykInverted,
2523            Gbrp8,
2524            Gbrap16Le,
2525            GrayF32Le,
2526            RgbaF32Le,
2527            GbrapF32Le,
2528        ] {
2529            assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2530        }
2531    }
2532
2533    #[test]
2534    fn plane_dimensions_odd_sizes_across_samplings() {
2535        use PixelFormat::*;
2536        // 4:2:0 — both axes ceil-halved.
2537        assert_eq!(Yuv420P.plane_dimensions(1, 7, 5), Some((4, 3)));
2538        // 4:2:2 — width ceil-halved, height untouched.
2539        assert_eq!(Yuv422P.plane_dimensions(2, 7, 5), Some((4, 5)));
2540        // 4:1:1 — width ceil-quartered.
2541        assert_eq!(Yuv411P.plane_dimensions(1, 7, 5), Some((2, 5)));
2542        assert_eq!(Yuv411P.plane_dimensions(1, 9, 5), Some((3, 5)));
2543        // 4:4:4 — untouched.
2544        assert_eq!(Yuv444P.plane_dimensions(1, 7, 5), Some((7, 5)));
2545        // Semi-planar chroma positions.
2546        assert_eq!(Nv12.plane_dimensions(1, 7, 5), Some((4, 3)));
2547        assert_eq!(Nv21.plane_dimensions(1, 7, 5), Some((4, 3)));
2548        // Alpha planes are never subsampled.
2549        assert_eq!(Yuva420P.plane_dimensions(3, 7, 5), Some((7, 5)));
2550        assert_eq!(Yuva422P16Le.plane_dimensions(3, 7, 5), Some((7, 5)));
2551        // Planar RGB planes are never subsampled.
2552        for plane in 0..3 {
2553            assert_eq!(Gbrp12Le.plane_dimensions(plane, 7, 5), Some((7, 5)));
2554        }
2555        // Out-of-range planes.
2556        assert_eq!(Rgb24.plane_dimensions(1, 8, 8), None);
2557        assert_eq!(Yuv420P.plane_dimensions(3, 8, 8), None);
2558        assert_eq!(Yuva420P.plane_dimensions(4, 8, 8), None);
2559        // Zero-sized pictures collapse every plane to zero.
2560        assert_eq!(Yuv440P.plane_dimensions(1, 0, 0), Some((0, 0)));
2561    }
2562
2563    #[test]
2564    fn plane_row_bytes_conventions() {
2565        use PixelFormat::*;
2566        // Bit-packed mono: ceil(width / 8) with a ragged tail byte.
2567        assert_eq!(MonoBlack.plane_row_bytes(0, 13), Some(2));
2568        assert_eq!(MonoWhite.plane_row_bytes(0, 16), Some(2));
2569        assert_eq!(MonoBlack.plane_row_bytes(0, 17), Some(3));
2570        // Packed 4:2:2: 4-byte macropixels, odd width rounds up.
2571        assert_eq!(Yuyv422.plane_row_bytes(0, 6), Some(12));
2572        assert_eq!(Uyvy422.plane_row_bytes(0, 7), Some(16));
2573        // Semi-planar chroma: 2 bytes per position.
2574        assert_eq!(Nv12.plane_row_bytes(0, 7), Some(7));
2575        assert_eq!(Nv12.plane_row_bytes(1, 7), Some(8));
2576        // Deep planar planes: 2 bytes per sample regardless of the
2577        // number of valid bits in the word.
2578        assert_eq!(Yuv420P10Le.plane_row_bytes(1, 7), Some(8));
2579        assert_eq!(Gbrap14Le.plane_row_bytes(3, 5), Some(10));
2580        // Packed pixel costs.
2581        assert_eq!(Rgb24.plane_row_bytes(0, 5), Some(15));
2582        assert_eq!(Rgb48Le.plane_row_bytes(0, 2), Some(12));
2583        assert_eq!(Rgba64Le.plane_row_bytes(0, 2), Some(16));
2584        assert_eq!(Ya16Le.plane_row_bytes(0, 3), Some(12));
2585        assert_eq!(Cmyk.plane_row_bytes(0, 3), Some(12));
2586        // Out-of-range plane.
2587        assert_eq!(Gray8.plane_row_bytes(1, 8), None);
2588    }
2589
2590    #[test]
2591    fn frame_size_examples() {
2592        use PixelFormat::*;
2593        assert_eq!(Yuv420P.frame_size_bytes(4, 4), Some(24));
2594        assert_eq!(Nv12.frame_size_bytes(7, 5), Some(59)); // 35 + 4×3×2
2595        assert_eq!(Yuyv422.frame_size_bytes(7, 2), Some(32));
2596        assert_eq!(MonoBlack.frame_size_bytes(13, 3), Some(6));
2597        assert_eq!(Pal8.frame_size_bytes(5, 4), Some(20));
2598        assert_eq!(Ya16Le.frame_size_bytes(3, 3), Some(36));
2599        assert_eq!(Yuva444P16Le.frame_size_bytes(3, 3), Some(72));
2600    }
2601
2602    #[test]
2603    fn frame_size_is_sum_of_planes_for_every_format() {
2604        for fmt in ALL_PIXEL_FORMATS {
2605            for (w, h) in [(0, 0), (1, 1), (2, 2), (7, 5), (16, 16), (13, 1), (1, 13)] {
2606                let total = fmt
2607                    .frame_size_bytes(w, h)
2608                    .unwrap_or_else(|| panic!("{fmt:?} {w}x{h} must size"));
2609                let sum: usize = (0..fmt.plane_count())
2610                    .map(|p| fmt.plane_size_bytes(p, w, h).unwrap())
2611                    .sum();
2612                assert_eq!(total, sum, "{fmt:?} {w}x{h}");
2613                // Plane 0 is always the full pixel grid.
2614                assert_eq!(fmt.plane_dimensions(0, w, h), Some((w, h)), "{fmt:?}");
2615                // The plane table ends exactly at plane_count.
2616                assert_eq!(fmt.plane_dimensions(fmt.plane_count(), w, h), None);
2617                // Tightly-packed storage can never be smaller than the
2618                // packed-bits density estimate.
2619                let storage_bits = total as u128 * 8;
2620                let density_bits = w as u128 * h as u128 * fmt.bits_per_pixel_approx() as u128;
2621                assert!(
2622                    storage_bits >= density_bits,
2623                    "{fmt:?} {w}x{h}: storage {storage_bits} < density {density_bits}"
2624                );
2625            }
2626        }
2627    }
2628
2629    #[test]
2630    fn packed_16bit_rgb_family_metadata() {
2631        // One 16-bit word per pixel, single plane, no alpha (the unused
2632        // 5-5-5 top bit is padding), no chroma grid, not planar / float
2633        // / palette; storage cost 16 bits, rows 2 × width.
2634        for fmt in [
2635            PixelFormat::Rgb555Le,
2636            PixelFormat::Rgb555Be,
2637            PixelFormat::Bgr555Le,
2638            PixelFormat::Bgr555Be,
2639            PixelFormat::Rgb565Le,
2640            PixelFormat::Rgb565Be,
2641            PixelFormat::Bgr565Le,
2642            PixelFormat::Bgr565Be,
2643        ] {
2644            assert!(fmt.is_packed_rgb(), "{fmt:?}");
2645            assert!(fmt.is_rgb(), "{fmt:?}");
2646            assert!(!fmt.is_planar(), "{fmt:?}");
2647            assert!(!fmt.has_alpha(), "{fmt:?}");
2648            assert!(!fmt.is_float(), "{fmt:?}");
2649            assert!(!fmt.is_palette(), "{fmt:?}");
2650            assert_eq!(fmt.implied_color_range(), None, "{fmt:?}");
2651            assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2652            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2653            assert_eq!(fmt.bits_per_pixel_approx(), 16, "{fmt:?}");
2654            assert_eq!(fmt.plane_row_bytes(0, 7), Some(14), "{fmt:?}");
2655            assert_eq!(fmt.plane_dimensions(0, 7, 5), Some((7, 5)), "{fmt:?}");
2656            assert_eq!(fmt.plane_dimensions(1, 7, 5), None, "{fmt:?}");
2657            assert_eq!(fmt.frame_size_bytes(7, 5), Some(70), "{fmt:?}");
2658        }
2659    }
2660
2661    #[test]
2662    fn padded_32bit_rgb_family_metadata() {
2663        // Four bytes per pixel with one padding byte: the alpha-less
2664        // twins of Rgba / Bgra / Argb / Abgr at the same byte cost.
2665        for (fmt, alpha_twin) in [
2666            (PixelFormat::Rgbx, PixelFormat::Rgba),
2667            (PixelFormat::Bgrx, PixelFormat::Bgra),
2668            (PixelFormat::Xrgb, PixelFormat::Argb),
2669            (PixelFormat::Xbgr, PixelFormat::Abgr),
2670        ] {
2671            assert!(fmt.is_packed_rgb(), "{fmt:?}");
2672            assert!(fmt.is_rgb(), "{fmt:?}");
2673            assert!(!fmt.is_planar(), "{fmt:?}");
2674            assert!(!fmt.has_alpha(), "{fmt:?}");
2675            assert!(alpha_twin.has_alpha(), "{alpha_twin:?}");
2676            assert!(!fmt.is_float(), "{fmt:?}");
2677            assert!(!fmt.is_palette(), "{fmt:?}");
2678            assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2679            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2680            assert_eq!(
2681                fmt.bits_per_pixel_approx(),
2682                alpha_twin.bits_per_pixel_approx(),
2683                "{fmt:?}"
2684            );
2685            assert_eq!(fmt.plane_row_bytes(0, 7), Some(28), "{fmt:?}");
2686            assert_eq!(
2687                fmt.frame_size_bytes(7, 5),
2688                alpha_twin.frame_size_bytes(7, 5),
2689                "{fmt:?}"
2690            );
2691        }
2692    }
2693
2694    #[test]
2695    fn is_packed_rgb_and_is_rgb_partition_every_format() {
2696        for fmt in ALL_PIXEL_FORMATS {
2697            // Packed RGB: exactly one plane, never planar, and is_rgb
2698            // follows.
2699            if fmt.is_packed_rgb() {
2700                assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2701                assert!(!fmt.is_planar(), "{fmt:?}");
2702                assert!(fmt.is_rgb(), "{fmt:?}");
2703            }
2704            // Planar RGB is exactly the GBR(A) family.
2705            let planar_rgb = fmt.is_rgb() && !fmt.is_packed_rgb();
2706            assert_eq!(planar_rgb, format!("{fmt:?}").starts_with("Gbr"), "{fmt:?}");
2707            // RGB never has a chroma grid; the RGB predicates never
2708            // claim grey, YUV, palette, mono or CMYK.
2709            if fmt.is_rgb() {
2710                assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2711                assert!(!fmt.is_palette(), "{fmt:?}");
2712                let name = format!("{fmt:?}");
2713                assert!(
2714                    !(name.starts_with("Gray")
2715                        || name.starts_with("Yuv")
2716                        || name.starts_with("Nv")
2717                        || name.starts_with("Mono")
2718                        || name.starts_with("Cmyk")
2719                        || name.starts_with("Ya")
2720                        || name.starts_with("Pal")
2721                        || name.starts_with("Uyvy")),
2722                    "{fmt:?}"
2723                );
2724            }
2725        }
2726        // The explicit counts, so a mislabelled new variant is caught
2727        // by number and not only by shape.
2728        let packed = ALL_PIXEL_FORMATS
2729            .into_iter()
2730            .filter(PixelFormat::is_packed_rgb)
2731            .count();
2732        assert_eq!(packed, 22);
2733        let rgb = ALL_PIXEL_FORMATS
2734            .into_iter()
2735            .filter(PixelFormat::is_rgb)
2736            .count();
2737        assert_eq!(rgb, 22 + 12);
2738    }
2739
2740    #[test]
2741    fn sizing_overflow_returns_none() {
2742        assert_eq!(
2743            PixelFormat::Rgba64Le.frame_size_bytes(u32::MAX, u32::MAX),
2744            None
2745        );
2746        assert_eq!(
2747            PixelFormat::RgbaF32Le.frame_size_bytes(u32::MAX, u32::MAX),
2748            None
2749        );
2750        assert_eq!(
2751            PixelFormat::Yuv440P16Le.plane_size_bytes(0, u32::MAX, u32::MAX),
2752            None
2753        );
2754    }
2755}