Skip to main content

oxideav_core/
format.rs

1//! Media-type and sample/pixel format enumerations.
2//!
3//! Audio channel ordering follows SMPTE 2036-2 / ITU-R BS.775 conventions
4//! for surround layouts; per-channel positions are named with the
5//! WAVEFORMATEXTENSIBLE "front-left, front-right, …" vocabulary.
6
7/// Broad category of a stream's payload.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum MediaType {
10    /// Audio samples.
11    Audio,
12    /// Video pictures.
13    Video,
14    /// Timed-text / bitmap subtitle cues.
15    Subtitle,
16    /// Opaque non-media payload (timecodes, klv, chapters, …).
17    Data,
18    /// Category not (yet) determined.
19    Unknown,
20}
21
22/// A single speaker position within a multi-channel audio layout.
23///
24/// Names follow the WAVEFORMATEXTENSIBLE / SMPTE convention.
25/// `Side*` and `Back*` are kept distinct (mirroring 7.1's
26/// L/R + Ls/Rs + Lb/Rb separation) so codecs that surface the
27/// distinction don't collapse it. `Lr`/`Rr` (rear / back-rear) are aliases
28/// for `BackLeft`/`BackRight` in this taxonomy — the rear pair sits behind
29/// the listener on the room's centreline-extension, the side pair is at
30/// roughly ±90° from front. The enum is `#[non_exhaustive]` so additional
31/// positions (height channels for Atmos / Auro-3D, etc.) can be added
32/// without breaking downstream match arms.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum ChannelPosition {
36    /// Front-left (L). 30° left of centre in BS.775 listening geometry.
37    FrontLeft,
38    /// Front-right (R). 30° right of centre.
39    FrontRight,
40    /// Front-centre (C). Direct centre, 0°.
41    FrontCenter,
42    /// Low-frequency effects (LFE). Sub-bass, no positional meaning.
43    LowFrequency,
44    /// Back-left (Lb / Lr). Behind the listener, ±150° in 7.1.
45    BackLeft,
46    /// Back-right (Rb / Rr). Behind the listener, mirror of `BackLeft`.
47    BackRight,
48    /// Front left-of-centre (Lc). Used in cinema 7.1 SDDS layouts.
49    FrontLeftOfCenter,
50    /// Front right-of-centre (Rc). Mirror of `FrontLeftOfCenter`.
51    FrontRightOfCenter,
52    /// Back-centre (Cs). Single rear channel for 6.1 / BS.775 4.0.
53    BackCenter,
54    /// Side-left (Ls). ±90° on the listener's left in 5.1 / 7.1.
55    SideLeft,
56    /// Side-right (Rs). Mirror of `SideLeft`.
57    SideRight,
58    /// Top front-left. Atmos / Auro-3D height layer (placeholder).
59    TopFrontLeft,
60    /// Top front-right. Atmos / Auro-3D height layer (placeholder).
61    TopFrontRight,
62    /// Top back-left. Atmos / Auro-3D ceiling layer (placeholder).
63    TopBackLeft,
64    /// Top back-right. Atmos / Auro-3D ceiling layer (placeholder).
65    TopBackRight,
66}
67
68/// Audio channel layout — names a fixed ordered tuple of speaker
69/// positions, OR carries a discrete fallback count when the layout is
70/// unknown / non-standard.
71///
72/// Channel orderings are taken from ITU-R BS.775 (5.1 / 7.1 surround
73/// reference) and SMPTE ST 2036-2 (audio channel ordering for UHDTV).
74/// For 5.1 the canonical order this crate adopts is
75/// `L, R, C, LFE, Ls, Rs` (the WAVEFORMATEXTENSIBLE / Vorbis / Opus
76/// convention). 7.1 extends that with `Lb, Rb` (back-rear pair).
77///
78/// The `Stereo` variant covers both regular two-channel stereo and the
79/// AC-3 / AC-4 matrix-encoded downmix carriers `Lo/Ro` ("two of",
80/// downmix-compatible) and `Lt/Rt` ("matrix-encoded for Pro Logic
81/// extraction"); the dedicated [`LoRo`](ChannelLayout::LoRo) /
82/// [`LtRt`](ChannelLayout::LtRt) variants surface the distinction
83/// explicitly when a downstream filter or muxer needs it.
84///
85/// `DiscreteN(n)` is the catch-all for "we know there are `n` channels
86/// but no recognised layout" — used when a codec produces an unusual
87/// channel count (>8) or when the container failed to surface a layout
88/// flag. It is the only variant whose `position()` returns `None`.
89///
90/// Marked `#[non_exhaustive]` so additional standard layouts (Atmos
91/// 7.1.4, Auro-3D 9.1, …) can be added without breaking match-exhaustive
92/// downstream consumers.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub enum ChannelLayout {
96    /// Mono (1ch): C.
97    Mono,
98    /// Stereo (2ch): L, R.
99    Stereo,
100    /// 2.1 (3ch): L, R, LFE.
101    Stereo21,
102    /// 3.0 surround (3ch): L, R, C.
103    Surround30,
104    /// Quadraphonic (4ch): L, R, Ls, Rs — no centre, side surrounds.
105    Quad,
106    /// 4.0 surround per BS.775 (4ch): L, R, C, Cs — centre + back surround.
107    Surround40,
108    /// 4.1 surround (5ch): L, R, C, Cs, LFE.
109    Surround41,
110    /// 5.0 surround (5ch): L, R, C, Ls, Rs.
111    Surround50,
112    /// 5.1 surround (6ch): L, R, C, LFE, Ls, Rs.
113    Surround51,
114    /// 6.0 surround (6ch): L, R, C, Cs, Ls, Rs.
115    Surround60,
116    /// 6.1 surround (7ch): L, R, C, LFE, Cs, Ls, Rs.
117    Surround61,
118    /// 7.0 surround (7ch): L, R, C, Ls, Rs, Lb, Rb.
119    Surround70,
120    /// 7.1 surround (8ch): L, R, C, LFE, Ls, Rs, Lb, Rb.
121    Surround71,
122    /// AC-3 / AC-4 Lo/Ro stereo downmix (2ch). Two-channel mix preserving
123    /// downmix-compatibility coefficients; not matrix-encoded.
124    LoRo,
125    /// AC-3 / AC-4 Lt/Rt stereo downmix (2ch). Two-channel matrix-encoded
126    /// downmix carrying surround information for Dolby Pro Logic decoding.
127    LtRt,
128    /// Discrete fallback: `n` channels with no recognised layout. Used for
129    /// unusual / >8ch / unknown layouts surfaced by exotic codecs or
130    /// containers that drop layout flags.
131    DiscreteN(u16),
132}
133
134impl ChannelLayout {
135    /// Number of channels in this layout.
136    pub fn channel_count(&self) -> u16 {
137        match self {
138            Self::Mono => 1,
139            Self::Stereo | Self::LoRo | Self::LtRt => 2,
140            Self::Stereo21 | Self::Surround30 => 3,
141            Self::Quad | Self::Surround40 => 4,
142            Self::Surround41 | Self::Surround50 => 5,
143            Self::Surround51 | Self::Surround60 => 6,
144            Self::Surround61 | Self::Surround70 => 7,
145            Self::Surround71 => 8,
146            Self::DiscreteN(n) => *n,
147        }
148    }
149
150    /// Speaker positions in canonical order. Returns an empty slice for
151    /// `DiscreteN` since the layout is unknown — call [`positions_owned`]
152    /// to get a `Vec` if you need to enumerate slots regardless of
153    /// known/unknown status.
154    ///
155    /// [`positions_owned`]: Self::positions_owned
156    pub fn positions(&self) -> &'static [ChannelPosition] {
157        use ChannelPosition::*;
158        match self {
159            Self::Mono => &[FrontCenter],
160            Self::Stereo | Self::LoRo | Self::LtRt => &[FrontLeft, FrontRight],
161            Self::Stereo21 => &[FrontLeft, FrontRight, LowFrequency],
162            Self::Surround30 => &[FrontLeft, FrontRight, FrontCenter],
163            Self::Quad => &[FrontLeft, FrontRight, SideLeft, SideRight],
164            Self::Surround40 => &[FrontLeft, FrontRight, FrontCenter, BackCenter],
165            Self::Surround41 => &[FrontLeft, FrontRight, FrontCenter, BackCenter, LowFrequency],
166            Self::Surround50 => &[FrontLeft, FrontRight, FrontCenter, SideLeft, SideRight],
167            Self::Surround51 => &[
168                FrontLeft,
169                FrontRight,
170                FrontCenter,
171                LowFrequency,
172                SideLeft,
173                SideRight,
174            ],
175            Self::Surround60 => &[
176                FrontLeft,
177                FrontRight,
178                FrontCenter,
179                BackCenter,
180                SideLeft,
181                SideRight,
182            ],
183            Self::Surround61 => &[
184                FrontLeft,
185                FrontRight,
186                FrontCenter,
187                LowFrequency,
188                BackCenter,
189                SideLeft,
190                SideRight,
191            ],
192            Self::Surround70 => &[
193                FrontLeft,
194                FrontRight,
195                FrontCenter,
196                SideLeft,
197                SideRight,
198                BackLeft,
199                BackRight,
200            ],
201            Self::Surround71 => &[
202                FrontLeft,
203                FrontRight,
204                FrontCenter,
205                LowFrequency,
206                SideLeft,
207                SideRight,
208                BackLeft,
209                BackRight,
210            ],
211            Self::DiscreteN(_) => &[],
212        }
213    }
214
215    /// Owned position list. For known layouts this clones [`positions`];
216    /// for `DiscreteN(n)` it returns an empty `Vec` (positions remain
217    /// unknown). Provided so callers that just want "give me positions
218    /// for any layout" don't have to special-case the discrete arm.
219    ///
220    /// [`positions`]: Self::positions
221    pub fn positions_owned(&self) -> Vec<ChannelPosition> {
222        self.positions().to_vec()
223    }
224
225    /// Speaker position at slot `idx` in canonical order, or `None` for
226    /// out-of-range slots and for `DiscreteN` (where the layout is
227    /// unknown).
228    pub fn position(&self, idx: usize) -> Option<ChannelPosition> {
229        self.positions().get(idx).copied()
230    }
231
232    /// True when this layout carries a low-frequency-effects (LFE) channel.
233    pub fn has_lfe(&self) -> bool {
234        self.positions()
235            .iter()
236            .any(|p| matches!(p, ChannelPosition::LowFrequency))
237    }
238
239    /// True when this layout carries surround information (more than two
240    /// channels OR an LFE). `Stereo` / `Mono` return false; `LoRo` /
241    /// `LtRt` are 2-channel downmixes and also return false even though
242    /// they encode surround content (that's the whole point of a
243    /// downmix).
244    pub fn is_surround(&self) -> bool {
245        self.channel_count() > 2 || self.has_lfe()
246    }
247
248    /// Back-compat bridge: infer a layout from a bare channel count.
249    ///
250    /// This mapping is what lets codecs that haven't been updated to set
251    /// a layout explicitly continue to work: they keep producing a count
252    /// and we infer the most-common layout for that count. The choices
253    /// follow industry defaults — 5.1 wins for 6ch (more common than
254    /// 6.0), 7.1 wins for 8ch, and so on.
255    ///
256    /// | count | layout       |
257    /// |-------|--------------|
258    /// | 1     | `Mono`       |
259    /// | 2     | `Stereo`     |
260    /// | 3     | `Surround30` |
261    /// | 4     | `Quad`       |
262    /// | 5     | `Surround50` |
263    /// | 6     | `Surround51` |
264    /// | 7     | `Surround61` |
265    /// | 8     | `Surround71` |
266    /// | other | `DiscreteN`  |
267    pub fn from_count(n: u16) -> ChannelLayout {
268        match n {
269            1 => Self::Mono,
270            2 => Self::Stereo,
271            3 => Self::Surround30,
272            4 => Self::Quad,
273            5 => Self::Surround50,
274            6 => Self::Surround51,
275            7 => Self::Surround61,
276            8 => Self::Surround71,
277            other => Self::DiscreteN(other),
278        }
279    }
280}
281
282impl std::fmt::Display for ChannelLayout {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        let s = match self {
285            Self::Mono => "mono",
286            Self::Stereo => "stereo",
287            Self::Stereo21 => "2.1",
288            Self::Surround30 => "3.0",
289            Self::Quad => "quad",
290            Self::Surround40 => "4.0",
291            Self::Surround41 => "4.1",
292            Self::Surround50 => "5.0",
293            Self::Surround51 => "5.1",
294            Self::Surround60 => "6.0",
295            Self::Surround61 => "6.1",
296            Self::Surround70 => "7.0",
297            Self::Surround71 => "7.1",
298            Self::LoRo => "loro",
299            Self::LtRt => "ltrt",
300            Self::DiscreteN(n) => return write!(f, "discrete{n}"),
301        };
302        f.write_str(s)
303    }
304}
305
306/// Error returned by the [`ChannelLayout`] `FromStr` impl when the input
307/// doesn't match any recognised layout name.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct ParseChannelLayoutError(pub String);
310
311impl std::fmt::Display for ParseChannelLayoutError {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        write!(f, "unrecognised channel layout: {:?}", self.0)
314    }
315}
316
317impl std::error::Error for ParseChannelLayoutError {}
318
319impl std::str::FromStr for ChannelLayout {
320    type Err = ParseChannelLayoutError;
321
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        let lower = s.trim().to_ascii_lowercase();
324        let layout = match lower.as_str() {
325            "mono" | "1.0" => Self::Mono,
326            "stereo" | "2.0" => Self::Stereo,
327            "2.1" => Self::Stereo21,
328            "3.0" | "surround3" | "surround30" => Self::Surround30,
329            "quad" => Self::Quad,
330            "4.0" | "surround4" | "surround40" => Self::Surround40,
331            "4.1" | "surround41" => Self::Surround41,
332            "5.0" | "surround5" | "surround50" => Self::Surround50,
333            "5.1" | "surround51" => Self::Surround51,
334            "6.0" | "surround6" | "surround60" => Self::Surround60,
335            "6.1" | "surround61" => Self::Surround61,
336            "7.0" | "surround7" | "surround70" => Self::Surround70,
337            "7.1" | "surround71" => Self::Surround71,
338            "loro" | "lo/ro" => Self::LoRo,
339            "ltrt" | "lt/rt" => Self::LtRt,
340            other => {
341                if let Some(rest) = other.strip_prefix("discrete") {
342                    if let Ok(n) = rest.parse::<u16>() {
343                        return Ok(Self::DiscreteN(n));
344                    }
345                }
346                return Err(ParseChannelLayoutError(s.to_owned()));
347            }
348        };
349        Ok(layout)
350    }
351}
352
353/// Audio sample format.
354///
355/// Variants carry **stable explicit discriminants** — the integer value
356/// of `SampleFormat::S16 as u8` is part of the public ABI. Add new
357/// variants only at the end with a fresh number; never reorder, renumber,
358/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
359/// downstream `match` statements; pinned discriminants additionally let
360/// the format round-trip through any byte-stable serialization
361/// (config files, capability blobs, IPC) without losing meaning across
362/// crate versions.
363#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
364#[non_exhaustive]
365#[repr(u8)]
366pub enum SampleFormat {
367    /// Unsigned 8-bit, interleaved.
368    U8 = 0,
369    /// Signed 8-bit, interleaved. Native format of Amiga 8SVX and MOD samples.
370    S8 = 1,
371    /// Signed 16-bit little-endian, interleaved.
372    S16 = 2,
373    /// Signed 24-bit packed (3 bytes/sample) little-endian, interleaved.
374    S24 = 3,
375    /// Signed 32-bit little-endian, interleaved.
376    S32 = 4,
377    /// 32-bit IEEE float, interleaved.
378    F32 = 5,
379    /// 64-bit IEEE float, interleaved.
380    F64 = 6,
381    /// Unsigned 8-bit, planar (one plane per channel).
382    U8P = 7,
383    /// Signed 16-bit little-endian, planar (one plane per channel).
384    S16P = 8,
385    /// Signed 32-bit little-endian, planar (one plane per channel).
386    S32P = 9,
387    /// 32-bit IEEE float, planar (one plane per channel).
388    F32P = 10,
389    /// 64-bit IEEE float, planar (one plane per channel).
390    F64P = 11,
391}
392
393impl SampleFormat {
394    /// `true` for the planar (one-plane-per-channel) variants.
395    pub fn is_planar(&self) -> bool {
396        matches!(
397            self,
398            Self::U8P | Self::S16P | Self::S32P | Self::F32P | Self::F64P
399        )
400    }
401
402    /// Bytes per sample *per channel*.
403    pub fn bytes_per_sample(&self) -> usize {
404        match self {
405            Self::U8 | Self::U8P | Self::S8 => 1,
406            Self::S16 | Self::S16P => 2,
407            Self::S24 => 3,
408            Self::S32 | Self::S32P | Self::F32 | Self::F32P => 4,
409            Self::F64 | Self::F64P => 8,
410        }
411    }
412
413    /// `true` for the IEEE-float variants (32- or 64-bit, either layout).
414    pub fn is_float(&self) -> bool {
415        matches!(self, Self::F32 | Self::F64 | Self::F32P | Self::F64P)
416    }
417
418    /// Number of `Vec<u8>` planes an [`AudioFrame`](crate::AudioFrame)
419    /// of this format carries for `channels` channels: planar formats
420    /// use one plane per channel, interleaved formats use one plane
421    /// total.
422    pub fn plane_count(&self, channels: u16) -> usize {
423        if self.is_planar() {
424            channels as usize
425        } else {
426            1
427        }
428    }
429}
430
431/// Video pixel format.
432///
433/// Variants carry **stable explicit discriminants** — the integer value
434/// of `PixelFormat::Yuv420P as u16` is part of the public ABI. Add new
435/// variants only at the end with a fresh number; never reorder, renumber,
436/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
437/// downstream `match` statements; pinned discriminants additionally let
438/// the format round-trip through any byte-stable serialization
439/// (config files, capability blobs, IPC, on-disk caches) without losing
440/// meaning across crate versions, and prevent inserts in the middle of
441/// the enum from shifting every later variant's number (which
442/// cargo-semver-checks rightly flags as a breaking change).
443///
444/// The first six variants (`Yuv420P` through `Gray8`) are the original
445/// formats produced by the early codec crates. Everything beyond that
446/// is additional surface handled by `oxideav-pixfmt` and the still-image
447/// codecs (PNG, GIF, still-JPEG).
448#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
449#[non_exhaustive]
450#[repr(u16)]
451pub enum PixelFormat {
452    /// 8-bit YUV 4:2:0, planar (Y, U, V).
453    Yuv420P = 0,
454    /// 8-bit YUV 4:2:2, planar.
455    Yuv422P = 1,
456    /// 8-bit YUV 4:4:4, planar.
457    Yuv444P = 2,
458    /// Packed 8-bit RGB, 3 bytes/pixel.
459    Rgb24 = 3,
460    /// Packed 8-bit RGBA, 4 bytes/pixel.
461    Rgba = 4,
462    /// Packed 8-bit grayscale.
463    Gray8 = 5,
464
465    // --- Palette ---
466    /// 8-bit palette indices — companion palette carried out of band.
467    Pal8 = 6,
468
469    // --- Packed RGB/BGR swizzles ---
470    /// Packed 8-bit BGR, 3 bytes/pixel.
471    Bgr24 = 7,
472    /// Packed 8-bit BGRA, 4 bytes/pixel.
473    Bgra = 8,
474    /// Packed 8-bit ARGB, 4 bytes/pixel (alpha first).
475    Argb = 9,
476    /// Packed 8-bit ABGR, 4 bytes/pixel.
477    Abgr = 10,
478
479    // --- Deeper packed RGB ---
480    /// Packed 16-bit-per-channel RGB, little-endian, 6 bytes/pixel.
481    Rgb48Le = 11,
482    /// Packed 16-bit-per-channel RGBA, little-endian, 8 bytes/pixel.
483    Rgba64Le = 12,
484
485    // --- Grayscale deeper / partial bit depths ---
486    /// 16-bit little-endian grayscale.
487    Gray16Le = 13,
488    /// 10-bit grayscale in a 16-bit little-endian word.
489    Gray10Le = 14,
490    /// 12-bit grayscale in a 16-bit little-endian word.
491    Gray12Le = 15,
492
493    // --- Higher-precision YUV ---
494    /// 10-bit YUV 4:2:0 planar, little-endian 16-bit storage.
495    Yuv420P10Le = 16,
496    /// 10-bit YUV 4:2:2 planar, little-endian 16-bit storage.
497    Yuv422P10Le = 17,
498    /// 10-bit YUV 4:4:4 planar, little-endian 16-bit storage.
499    Yuv444P10Le = 18,
500    /// 12-bit YUV 4:2:0 planar, little-endian 16-bit storage.
501    Yuv420P12Le = 19,
502    /// 12-bit YUV 4:2:2 planar, little-endian 16-bit storage.
503    Yuv422P12Le = 20,
504    /// 12-bit YUV 4:4:4 planar, little-endian 16-bit storage.
505    Yuv444P12Le = 21,
506
507    // --- Full-range ("J") YUV ---
508    /// JPEG/full-range YUV 4:2:0 planar.
509    YuvJ420P = 22,
510    /// JPEG/full-range YUV 4:2:2 planar.
511    YuvJ422P = 23,
512    /// JPEG/full-range YUV 4:4:4 planar.
513    YuvJ444P = 24,
514
515    // --- Semi-planar YUV ---
516    /// YUV 4:2:0, planar Y + interleaved UV (NV12).
517    Nv12 = 25,
518    /// YUV 4:2:0, planar Y + interleaved VU (NV21).
519    Nv21 = 26,
520
521    // --- Gray + alpha / YUV + alpha ---
522    /// Packed grayscale + alpha, 2 bytes/pixel (Y, A).
523    Ya8 = 27,
524    /// Yuv420P with an additional full-resolution alpha plane.
525    Yuva420P = 28,
526
527    // --- Mono (1 bit per pixel) ---
528    /// 1 bit per pixel, packed MSB-first, 0 = black.
529    MonoBlack = 29,
530    /// 1 bit per pixel, packed MSB-first, 0 = white.
531    MonoWhite = 30,
532
533    // --- Interleaved YUV 4:2:2 ---
534    /// Packed 4:2:2, byte order Y0 U0 Y1 V0.
535    Yuyv422 = 31,
536    /// Packed 4:2:2, byte order U0 Y0 V0 Y1.
537    Uyvy422 = 32,
538
539    // --- Print / prepress ---
540    /// Packed 8-bit CMYK, 4 bytes/pixel in byte order C, M, Y, K.
541    /// "Regular" convention: C=0 means no cyan ink (white), C=255 means
542    /// full cyan. Used by JPEG 4-component scans from non-Adobe encoders
543    /// and by many print-side image toolchains. Adobe Photoshop's
544    /// inverted CMYK (where 0 = full ink) is the separate
545    /// [`CmykInverted`](Self::CmykInverted) variant.
546    Cmyk = 33,
547
548    // --- Wide-horizontal subsampled YUV ---
549    /// 8-bit YUV 4:1:1, planar (Y, U, V). Luma at full resolution; chroma
550    /// horizontally subsampled by 4 (each chroma sample covers a 4×1
551    /// luma block), no vertical subsampling. Native sampling of
552    /// NTSC DV-25 and a legal JPEG sampling layout (luma H=4, V=1;
553    /// chroma H=V=1) emitted by some real-world JPEG corpora.
554    Yuv411P = 34,
555
556    // --- Planar GBR / GBRA (RGB stored as planes in G,B,R order) ---
557    //
558    // High-bit-depth GBR(A) layouts used by MagicYUV, JPEG 2000, OpenEXR,
559    // TIFF and similar workflows that need lossless RGB at 10/12/14 bits
560    // per channel. Planes are ordered G, B, R (and A for the `Gbrap*`
561    // variants) — and
562    // each sample is stored as a 16-bit little-endian word with the
563    // top bits zero. The native 8-bit ([`Gbrp8`](Self::Gbrp8)) and
564    // full-width 16-bit ([`Gbrp16Le`](Self::Gbrp16Le) /
565    // [`Gbrap16Le`](Self::Gbrap16Le)) companions arrived later and
566    // therefore live at fresh appended discriminants (52-54), per the
567    // append-only rule.
568    /// 10-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
569    /// G, B, R; each sample uses the low 10 bits of a 16-bit word.
570    Gbrp10Le = 35,
571    /// 10-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
572    /// ordered G, B, R, A; each sample uses the low 10 bits of a 16-bit
573    /// word.
574    Gbrap10Le = 36,
575    /// 12-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
576    /// G, B, R; each sample uses the low 12 bits of a 16-bit word.
577    Gbrp12Le = 37,
578    /// 12-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
579    /// ordered G, B, R, A; each sample uses the low 12 bits of a 16-bit
580    /// word.
581    Gbrap12Le = 38,
582    /// 14-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
583    /// G, B, R; each sample uses the low 14 bits of a 16-bit word.
584    Gbrp14Le = 39,
585    /// 14-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
586    /// ordered G, B, R, A; each sample uses the low 14 bits of a 16-bit
587    /// word.
588    Gbrap14Le = 40,
589
590    // --- 16-bit YUV planar ---
591    //
592    // Full-width companions to the 10/12-bit planar YUV variants above:
593    // same three-plane layout and little-endian 16-bit words, but ALL 16
594    // bits of every word are significant (there are no zero top bits and
595    // no separate "valid bits" count — full-scale is 65535). Needed by
596    // wavelet codecs whose signal-range presets go to 16 bits per
597    // component (SMPTE VC-2 / Dirac video-format presets 7 and 8).
598    /// 16-bit YUV 4:2:0 planar, little-endian 16-bit storage. All 16
599    /// bits of each sample word are significant.
600    Yuv420P16Le = 41,
601    /// 16-bit YUV 4:2:2 planar, little-endian 16-bit storage. All 16
602    /// bits of each sample word are significant.
603    Yuv422P16Le = 42,
604    /// 16-bit YUV 4:4:4 planar, little-endian 16-bit storage. All 16
605    /// bits of each sample word are significant.
606    Yuv444P16Le = 43,
607
608    // --- 8-bit YUV + alpha at the remaining chroma samplings ---
609    //
610    // Companions to `Yuva420P`: the alpha plane is always full
611    // resolution (one 8-bit sample per pixel, never chroma-subsampled),
612    // appended after the V plane as plane index 3. Intermediate/mezzanine
613    // codecs carry alpha at 4:2:2 and 4:4:4 samplings.
614    /// Yuv422P with an additional full-resolution alpha plane.
615    Yuva422P = 44,
616    /// Yuv444P with an additional full-resolution alpha plane.
617    Yuva444P = 45,
618
619    // --- Deep YUV + alpha (10/12/16-bit words with full-resolution A) ---
620    //
621    // Alpha-carrying companions to the 10/12/16-bit planar YUV variants
622    // above, completing the Yuva family for mezzanine codecs that carry
623    // deep colour together with an alpha channel. Same conventions as
624    // the 8-bit `Yuva*` trio: 4 planes ordered Y, U, V, A with the
625    // alpha plane always at full resolution (one sample per pixel,
626    // never chroma-subsampled) as plane index 3. Every sample — alpha
627    // included — is stored as a little-endian 16-bit word; for the
628    // 10/12-bit variants each sample uses the low bits of the word with
629    // the top bits zero, and for the 16-bit variants all 16 bits of
630    // every word are significant (full-scale is 65535), matching
631    // `Yuv420P16Le`/`Yuv422P16Le`/`Yuv444P16Le`.
632    /// 10-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
633    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
634    /// the low 10 bits of a 16-bit word.
635    Yuva422P10Le = 46,
636    /// 12-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
637    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
638    /// the low 12 bits of a 16-bit word.
639    Yuva422P12Le = 47,
640    /// 10-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
641    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
642    /// the low 10 bits of a 16-bit word.
643    Yuva444P10Le = 48,
644    /// 12-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
645    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
646    /// the low 12 bits of a 16-bit word.
647    Yuva444P12Le = 49,
648    /// 16-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
649    /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
650    /// each sample word are significant.
651    Yuva422P16Le = 50,
652    /// 16-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
653    /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
654    /// each sample word are significant.
655    Yuva444P16Le = 51,
656
657    // --- Native 8-bit and full-width 16-bit planar GBR(A) ---
658    //
659    // Companions to the 10/12/14-bit `Gbrp*`/`Gbrap*` family above,
660    // closing the planar-RGB depth ladder at both ends for lossless
661    // RGB codecs whose native coding space is per-plane G, B, R.
662    // Plane order is identical to the rest of the family: G, B, R
663    // (and A as plane index 3 for `Gbrap16Le`, always at full
664    // resolution — RGB has no chroma subsampling). `Gbrp8` stores one
665    // byte per sample with all 8 bits significant; the 16-bit variants
666    // store little-endian 16-bit words with ALL 16 bits significant
667    // (full-scale is 65535, matching the `Yuv*P16Le` convention — no
668    // zero top bits, no separate valid-bits count). Odd in-between
669    // depths on these storage formats (e.g. 9- or 15-bit RGB) are
670    // expressed via the per-plane significant-bits side-channel on
671    // `VideoFrame`, not by new enum variants.
672    /// 8-bit planar GBR. 3 planes ordered G, B, R; one byte per
673    /// sample, all 8 bits significant.
674    Gbrp8 = 52,
675    /// 16-bit planar GBR, little-endian 16-bit storage. 3 planes
676    /// ordered G, B, R; all 16 bits of each sample word are
677    /// significant.
678    Gbrp16Le = 53,
679    /// 16-bit planar GBR + alpha, little-endian 16-bit storage. 4
680    /// planes ordered G, B, R, A; all 16 bits of each sample word are
681    /// significant.
682    Gbrap16Le = 54,
683
684    // --- Deep YUV + alpha at 4:2:0 ---
685    //
686    // Completes the deep Yuva family begun by the 4:2:2/4:4:4 variants
687    // above (46-51) at the remaining chroma sampling. Same conventions:
688    // 4 planes ordered Y, U, V, A with the alpha plane always at full
689    // resolution (one sample per pixel, never chroma-subsampled) as
690    // plane index 3. Every sample — alpha included — is stored as a
691    // little-endian 16-bit word; the 10/12-bit variants keep values in
692    // the low bits of the word with the top bits zero, and the 16-bit
693    // variant has all 16 bits of every word significant (full-scale is
694    // 65535), matching `Yuv420P16Le`.
695    /// 10-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
696    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
697    /// the low 10 bits of a 16-bit word.
698    Yuva420P10Le = 55,
699    /// 12-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
700    /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
701    /// the low 12 bits of a 16-bit word.
702    Yuva420P12Le = 56,
703    /// 16-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
704    /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
705    /// each sample word are significant.
706    Yuva420P16Le = 57,
707
708    // --- 8-bit planar GBR + alpha ---
709    //
710    // Alpha-carrying companion to `Gbrp8`, filling the last hole in
711    // the planar GBR(A) family: with this variant every depth on the
712    // ladder (8/10/12/14/16) exists in both alpha-less and
713    // alpha-carrying form. Same conventions as the rest of the family:
714    // planes ordered G, B, R, A with the alpha plane at full
715    // resolution (RGB has no chroma subsampling) as plane index 3,
716    // one byte per sample, all 8 bits significant. Lossless RGB codecs
717    // whose native coding space is per-plane G, B, R carry 8-bit RGBA
718    // in exactly this shape.
719    /// 8-bit planar GBR + alpha. 4 planes ordered G, B, R, A; one
720    /// byte per sample, all 8 bits significant.
721    Gbrap8 = 58,
722
723    // --- Deep gray + alpha ---
724    //
725    // 16-bit companion to `Ya8`, ending the gray+alpha ladder at the
726    // same depth the plain gray ladder already reaches (`Gray16Le`).
727    // Still-image wire formats carry 16-bit greyscale-with-alpha
728    // natively (PNG colour type 4 at bit depth 16); without this
729    // variant that content must detour through `Rgba64Le`, tripling
730    // the gray payload and losing the "single luminance component"
731    // semantics. Same packed shape as `Ya8` — interleaved Y then A —
732    // with each sample widened to a little-endian 16-bit word, all 16
733    // bits significant (full-scale is 65535, the `Gray16Le`
734    // convention). In-between gray+alpha depths stay the job of the
735    // per-plane significant-bits side-channel.
736    /// Packed 16-bit grayscale + alpha, little-endian, 4 bytes/pixel
737    /// (Y, A). All 16 bits of each sample word are significant.
738    Ya16Le = 59,
739
740    // --- Print / prepress, inverted-ink convention ---
741    //
742    // The companion `Cmyk` (33) reserved this name when it was added:
743    // Adobe-authored 4-component scans store ink coverage inverted on
744    // the wire (0 = full ink, 255 = no ink), and decoders that want to
745    // hand the wire values through losslessly need a format that says
746    // so rather than silently re-using the regular-convention `Cmyk`.
747    /// Packed 8-bit inverted CMYK, 4 bytes/pixel in byte order C, M,
748    /// Y, K. Inverted-ink convention: C=0 means full cyan ink, C=255
749    /// means no cyan (white) — the complement of [`Cmyk`](Self::Cmyk).
750    CmykInverted = 60,
751}
752
753impl PixelFormat {
754    /// True if this format stores its components in separate planes.
755    pub fn is_planar(&self) -> bool {
756        matches!(
757            self,
758            Self::Yuv420P
759                | Self::Yuv422P
760                | Self::Yuv444P
761                | Self::Yuv411P
762                | Self::Yuv420P10Le
763                | Self::Yuv422P10Le
764                | Self::Yuv444P10Le
765                | Self::Yuv420P12Le
766                | Self::Yuv422P12Le
767                | Self::Yuv444P12Le
768                | Self::Yuv420P16Le
769                | Self::Yuv422P16Le
770                | Self::Yuv444P16Le
771                | Self::YuvJ420P
772                | Self::YuvJ422P
773                | Self::YuvJ444P
774                | Self::Nv12
775                | Self::Nv21
776                | Self::Yuva420P
777                | Self::Yuva422P
778                | Self::Yuva444P
779                | Self::Yuva422P10Le
780                | Self::Yuva422P12Le
781                | Self::Yuva444P10Le
782                | Self::Yuva444P12Le
783                | Self::Yuva422P16Le
784                | Self::Yuva444P16Le
785                | Self::Yuva420P10Le
786                | Self::Yuva420P12Le
787                | Self::Yuva420P16Le
788                | Self::Gbrp8
789                | Self::Gbrap8
790                | Self::Gbrp10Le
791                | Self::Gbrap10Le
792                | Self::Gbrp12Le
793                | Self::Gbrap12Le
794                | Self::Gbrp14Le
795                | Self::Gbrap14Le
796                | Self::Gbrp16Le
797                | Self::Gbrap16Le
798        )
799    }
800
801    /// True if the format is a palette index format (`Pal8`).
802    pub fn is_palette(&self) -> bool {
803        matches!(self, Self::Pal8)
804    }
805
806    /// True if this format carries an alpha channel.
807    pub fn has_alpha(&self) -> bool {
808        matches!(
809            self,
810            Self::Rgba
811                | Self::Bgra
812                | Self::Argb
813                | Self::Abgr
814                | Self::Rgba64Le
815                | Self::Ya8
816                | Self::Ya16Le
817                | Self::Yuva420P
818                | Self::Yuva422P
819                | Self::Yuva444P
820                | Self::Yuva422P10Le
821                | Self::Yuva422P12Le
822                | Self::Yuva444P10Le
823                | Self::Yuva444P12Le
824                | Self::Yuva422P16Le
825                | Self::Yuva444P16Le
826                | Self::Yuva420P10Le
827                | Self::Yuva420P12Le
828                | Self::Yuva420P16Le
829                | Self::Gbrap8
830                | Self::Gbrap10Le
831                | Self::Gbrap12Le
832                | Self::Gbrap14Le
833                | Self::Gbrap16Le
834        )
835    }
836
837    /// Number of planes in the stored layout. Packed and palette formats
838    /// return 1; NV12/NV21 return 2; planar YUV without alpha and the
839    /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4.
840    pub fn plane_count(&self) -> usize {
841        match self {
842            Self::Nv12 | Self::Nv21 => 2,
843            Self::Yuv420P
844            | Self::Yuv422P
845            | Self::Yuv444P
846            | Self::Yuv411P
847            | Self::Yuv420P10Le
848            | Self::Yuv422P10Le
849            | Self::Yuv444P10Le
850            | Self::Yuv420P12Le
851            | Self::Yuv422P12Le
852            | Self::Yuv444P12Le
853            | Self::Yuv420P16Le
854            | Self::Yuv422P16Le
855            | Self::Yuv444P16Le
856            | Self::YuvJ420P
857            | Self::YuvJ422P
858            | Self::YuvJ444P
859            | Self::Gbrp8
860            | Self::Gbrp10Le
861            | Self::Gbrp12Le
862            | Self::Gbrp14Le
863            | Self::Gbrp16Le => 3,
864            Self::Yuva420P
865            | Self::Yuva422P
866            | Self::Yuva444P
867            | Self::Yuva422P10Le
868            | Self::Yuva422P12Le
869            | Self::Yuva444P10Le
870            | Self::Yuva444P12Le
871            | Self::Yuva422P16Le
872            | Self::Yuva444P16Le
873            | Self::Yuva420P10Le
874            | Self::Yuva420P12Le
875            | Self::Yuva420P16Le
876            | Self::Gbrap8
877            | Self::Gbrap10Le
878            | Self::Gbrap12Le
879            | Self::Gbrap14Le
880            | Self::Gbrap16Le => 4,
881            _ => 1,
882        }
883    }
884
885    /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
886    /// for chroma-subsampled YUV — intended for worst-case preallocation
887    /// rather than wire-accurate accounting.
888    pub fn bits_per_pixel_approx(&self) -> u32 {
889        match self {
890            Self::MonoBlack | Self::MonoWhite => 1,
891            Self::Gray8 | Self::Pal8 => 8,
892            Self::Ya8 => 16,
893            // 16-bit gray + alpha: two LE 16-bit words per pixel, all
894            // bits significant — packed bits equal storage bits.
895            Self::Ya16Le => 32,
896            Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
897            Self::Rgb24 | Self::Bgr24 => 24,
898            Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
899            Self::Rgb48Le => 48,
900            Self::Rgba64Le => 64,
901            Self::Yuyv422 | Self::Uyvy422 => 16,
902            Self::Cmyk | Self::CmykInverted => 32,
903            // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
904            // 10/12-bit variants double the byte count but we report the
905            // packed-bits-per-pixel estimate for a uniform heuristic.
906            Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
907            // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
908            // full res + 2 chroma planes each subsampled by 4).
909            Self::Yuv411P => 12,
910            Self::Yuv422P | Self::YuvJ422P => 16,
911            Self::Yuv444P | Self::YuvJ444P => 24,
912            Self::Yuv420P10Le | Self::Yuv420P12Le | Self::Yuv420P16Le => 24,
913            Self::Yuv422P10Le | Self::Yuv422P12Le | Self::Yuv422P16Le => 32,
914            Self::Yuv444P10Le | Self::Yuv444P12Le | Self::Yuv444P16Le => 48,
915            Self::Yuva420P => 20,
916            // 4:2:2 + full-res alpha: 8 (Y) + 4 (U) + 4 (V) + 8 (A).
917            Self::Yuva422P => 24,
918            // 4:4:4 + full-res alpha: four full-resolution 8-bit planes.
919            Self::Yuva444P => 32,
920            // Deep 4:2:2 + full-res alpha in 16-bit words: the estimator
921            // reports the 16-bit-word cost like the alpha-less deep YUV
922            // arms above — 3 sample words per pixel (Y + U/2 + V/2 + A).
923            Self::Yuva422P10Le | Self::Yuva422P12Le | Self::Yuva422P16Le => 48,
924            // Deep 4:4:4 + full-res alpha: 4 sample words per pixel.
925            Self::Yuva444P10Le | Self::Yuva444P12Le | Self::Yuva444P16Le => 64,
926            // Deep 4:2:0 + full-res alpha in 16-bit words: 16-bit-word
927            // storage cost of the alpha-less 4:2:0 arms above (24) plus
928            // one full-resolution 16-bit alpha word per pixel.
929            Self::Yuva420P10Le | Self::Yuva420P12Le | Self::Yuva420P16Le => 40,
930            // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we
931            // report the packed bits-per-pixel density (samples × bits)
932            // rather than the 16-bit storage cost, matching how the
933            // 10/12-bit YUV variants are reported above.
934            Self::Gbrp10Le => 30,
935            Self::Gbrap10Le => 40,
936            Self::Gbrp12Le => 36,
937            Self::Gbrap12Le => 48,
938            Self::Gbrp14Le => 42,
939            Self::Gbrap14Le => 56,
940            // Native 8-bit GBR: three bytes per pixel, like Rgb24 but
941            // planar. 16-bit GBR(A): packed bits == storage bits (every
942            // bit of each 16-bit word is significant), so the density
943            // and storage numbers coincide.
944            Self::Gbrp8 => 24,
945            Self::Gbrp16Le => 48,
946            Self::Gbrap16Le => 64,
947            // 8-bit GBR + alpha: four bytes per pixel, like Rgba but
948            // planar.
949            Self::Gbrap8 => 32,
950        }
951    }
952}
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957
958    /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
959    /// stability commitment — the integer value of each variant is part of
960    /// the public ABI. Any reorder, renumber, or removal will fail this test
961    /// and the change MUST be a major version bump (or a fresh variant
962    /// appended at a new number, leaving the existing ones untouched).
963    #[test]
964    fn pixel_format_discriminants_pinned() {
965        assert_eq!(PixelFormat::Yuv420P as u16, 0);
966        assert_eq!(PixelFormat::Yuv422P as u16, 1);
967        assert_eq!(PixelFormat::Yuv444P as u16, 2);
968        assert_eq!(PixelFormat::Rgb24 as u16, 3);
969        assert_eq!(PixelFormat::Rgba as u16, 4);
970        assert_eq!(PixelFormat::Gray8 as u16, 5);
971        assert_eq!(PixelFormat::Pal8 as u16, 6);
972        assert_eq!(PixelFormat::Bgr24 as u16, 7);
973        assert_eq!(PixelFormat::Bgra as u16, 8);
974        assert_eq!(PixelFormat::Argb as u16, 9);
975        assert_eq!(PixelFormat::Abgr as u16, 10);
976        assert_eq!(PixelFormat::Rgb48Le as u16, 11);
977        assert_eq!(PixelFormat::Rgba64Le as u16, 12);
978        assert_eq!(PixelFormat::Gray16Le as u16, 13);
979        assert_eq!(PixelFormat::Gray10Le as u16, 14);
980        assert_eq!(PixelFormat::Gray12Le as u16, 15);
981        assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
982        assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
983        assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
984        assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
985        assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
986        assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
987        assert_eq!(PixelFormat::YuvJ420P as u16, 22);
988        assert_eq!(PixelFormat::YuvJ422P as u16, 23);
989        assert_eq!(PixelFormat::YuvJ444P as u16, 24);
990        assert_eq!(PixelFormat::Nv12 as u16, 25);
991        assert_eq!(PixelFormat::Nv21 as u16, 26);
992        assert_eq!(PixelFormat::Ya8 as u16, 27);
993        assert_eq!(PixelFormat::Yuva420P as u16, 28);
994        assert_eq!(PixelFormat::MonoBlack as u16, 29);
995        assert_eq!(PixelFormat::MonoWhite as u16, 30);
996        assert_eq!(PixelFormat::Yuyv422 as u16, 31);
997        assert_eq!(PixelFormat::Uyvy422 as u16, 32);
998        assert_eq!(PixelFormat::Cmyk as u16, 33);
999        assert_eq!(PixelFormat::Yuv411P as u16, 34);
1000        assert_eq!(PixelFormat::Gbrp10Le as u16, 35);
1001        assert_eq!(PixelFormat::Gbrap10Le as u16, 36);
1002        assert_eq!(PixelFormat::Gbrp12Le as u16, 37);
1003        assert_eq!(PixelFormat::Gbrap12Le as u16, 38);
1004        assert_eq!(PixelFormat::Gbrp14Le as u16, 39);
1005        assert_eq!(PixelFormat::Gbrap14Le as u16, 40);
1006        assert_eq!(PixelFormat::Yuv420P16Le as u16, 41);
1007        assert_eq!(PixelFormat::Yuv422P16Le as u16, 42);
1008        assert_eq!(PixelFormat::Yuv444P16Le as u16, 43);
1009        assert_eq!(PixelFormat::Yuva422P as u16, 44);
1010        assert_eq!(PixelFormat::Yuva444P as u16, 45);
1011        assert_eq!(PixelFormat::Yuva422P10Le as u16, 46);
1012        assert_eq!(PixelFormat::Yuva422P12Le as u16, 47);
1013        assert_eq!(PixelFormat::Yuva444P10Le as u16, 48);
1014        assert_eq!(PixelFormat::Yuva444P12Le as u16, 49);
1015        assert_eq!(PixelFormat::Yuva422P16Le as u16, 50);
1016        assert_eq!(PixelFormat::Yuva444P16Le as u16, 51);
1017        assert_eq!(PixelFormat::Gbrp8 as u16, 52);
1018        assert_eq!(PixelFormat::Gbrp16Le as u16, 53);
1019        assert_eq!(PixelFormat::Gbrap16Le as u16, 54);
1020        assert_eq!(PixelFormat::Yuva420P10Le as u16, 55);
1021        assert_eq!(PixelFormat::Yuva420P12Le as u16, 56);
1022        assert_eq!(PixelFormat::Yuva420P16Le as u16, 57);
1023        assert_eq!(PixelFormat::Gbrap8 as u16, 58);
1024        assert_eq!(PixelFormat::Ya16Le as u16, 59);
1025        assert_eq!(PixelFormat::CmykInverted as u16, 60);
1026    }
1027
1028    #[test]
1029    fn sample_format_discriminants_pinned() {
1030        assert_eq!(SampleFormat::U8 as u8, 0);
1031        assert_eq!(SampleFormat::S8 as u8, 1);
1032        assert_eq!(SampleFormat::S16 as u8, 2);
1033        assert_eq!(SampleFormat::S24 as u8, 3);
1034        assert_eq!(SampleFormat::S32 as u8, 4);
1035        assert_eq!(SampleFormat::F32 as u8, 5);
1036        assert_eq!(SampleFormat::F64 as u8, 6);
1037        assert_eq!(SampleFormat::U8P as u8, 7);
1038        assert_eq!(SampleFormat::S16P as u8, 8);
1039        assert_eq!(SampleFormat::S32P as u8, 9);
1040        assert_eq!(SampleFormat::F32P as u8, 10);
1041        assert_eq!(SampleFormat::F64P as u8, 11);
1042    }
1043
1044    #[test]
1045    fn high_bit_yuv_planar_metadata() {
1046        // 10-bit reference variants are planar with three planes.
1047        assert!(PixelFormat::Yuv420P10Le.is_planar());
1048        assert!(PixelFormat::Yuv422P10Le.is_planar());
1049        assert!(PixelFormat::Yuv444P10Le.is_planar());
1050
1051        // 12-bit variants must follow the same shape.
1052        assert!(PixelFormat::Yuv420P12Le.is_planar());
1053        assert!(PixelFormat::Yuv422P12Le.is_planar());
1054        assert!(PixelFormat::Yuv444P12Le.is_planar());
1055
1056        assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
1057        assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
1058        assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
1059
1060        // 16-bit variants must follow the same shape.
1061        for fmt in [
1062            PixelFormat::Yuv420P16Le,
1063            PixelFormat::Yuv422P16Le,
1064            PixelFormat::Yuv444P16Le,
1065        ] {
1066            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1067            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1068        }
1069
1070        // None of the high-bit YUV variants carry alpha or palette.
1071        assert!(!PixelFormat::Yuv422P12Le.has_alpha());
1072        assert!(!PixelFormat::Yuv444P12Le.has_alpha());
1073        assert!(!PixelFormat::Yuv422P12Le.is_palette());
1074        assert!(!PixelFormat::Yuv444P12Le.is_palette());
1075        assert!(!PixelFormat::Yuv420P16Le.has_alpha());
1076        assert!(!PixelFormat::Yuv422P16Le.has_alpha());
1077        assert!(!PixelFormat::Yuv444P16Le.has_alpha());
1078        assert!(!PixelFormat::Yuv420P16Le.is_palette());
1079        assert!(!PixelFormat::Yuv422P16Le.is_palette());
1080        assert!(!PixelFormat::Yuv444P16Le.is_palette());
1081    }
1082
1083    #[test]
1084    fn channel_layout_round_trip_count_for_known_layouts() {
1085        // For every `n` that `from_count` maps to a named layout, the
1086        // resulting layout's `channel_count()` must equal `n` again.
1087        for n in 1..=8u16 {
1088            let layout = ChannelLayout::from_count(n);
1089            assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
1090            // None of these defaults should fall through to DiscreteN.
1091            assert!(
1092                !matches!(layout, ChannelLayout::DiscreteN(_)),
1093                "from_count({n}) unexpectedly produced DiscreteN"
1094            );
1095        }
1096    }
1097
1098    #[test]
1099    fn channel_layout_from_count_default_table() {
1100        // The exact mapping documented on `from_count` — pin it so
1101        // future refactors don't silently change the inferred layout.
1102        assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
1103        assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
1104        assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
1105        assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
1106        assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
1107        assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
1108        assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
1109        assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
1110    }
1111
1112    #[test]
1113    fn channel_layout_unknown_count_falls_through_to_discrete() {
1114        assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
1115        assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
1116        assert_eq!(
1117            ChannelLayout::from_count(64).channel_count(),
1118            64,
1119            "DiscreteN must report the count it was constructed with"
1120        );
1121    }
1122
1123    #[test]
1124    fn channel_layout_position_lookup() {
1125        assert_eq!(
1126            ChannelLayout::Stereo.position(0),
1127            Some(ChannelPosition::FrontLeft)
1128        );
1129        assert_eq!(
1130            ChannelLayout::Stereo.position(1),
1131            Some(ChannelPosition::FrontRight)
1132        );
1133        assert_eq!(ChannelLayout::Stereo.position(2), None);
1134
1135        // 5.1 canonical: L, R, C, LFE, Ls, Rs.
1136        let s51 = ChannelLayout::Surround51;
1137        assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
1138        assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
1139        assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
1140        assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
1141        assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
1142        assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
1143        assert_eq!(s51.position(6), None);
1144
1145        // DiscreteN never reveals a position.
1146        assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
1147    }
1148
1149    #[test]
1150    fn channel_layout_lfe_and_surround_predicates() {
1151        assert!(ChannelLayout::Surround51.has_lfe());
1152        assert!(ChannelLayout::Surround71.has_lfe());
1153        assert!(ChannelLayout::Stereo21.has_lfe());
1154        assert!(!ChannelLayout::Quad.has_lfe());
1155        assert!(!ChannelLayout::Surround50.has_lfe());
1156        assert!(!ChannelLayout::Stereo.has_lfe());
1157
1158        assert!(!ChannelLayout::Mono.is_surround());
1159        assert!(!ChannelLayout::Stereo.is_surround());
1160        // Downmix carriers are still 2ch / no-LFE → not "surround" by
1161        // the layout-shape definition; the surround info lives in the
1162        // sample matrix itself.
1163        assert!(!ChannelLayout::LoRo.is_surround());
1164        assert!(!ChannelLayout::LtRt.is_surround());
1165        assert!(ChannelLayout::Stereo21.is_surround());
1166        assert!(ChannelLayout::Surround51.is_surround());
1167        assert!(ChannelLayout::Surround71.is_surround());
1168    }
1169
1170    #[test]
1171    fn channel_layout_display_and_fromstr_round_trip() {
1172        use std::str::FromStr;
1173        let cases = [
1174            ChannelLayout::Mono,
1175            ChannelLayout::Stereo,
1176            ChannelLayout::Stereo21,
1177            ChannelLayout::Surround30,
1178            ChannelLayout::Quad,
1179            ChannelLayout::Surround40,
1180            ChannelLayout::Surround41,
1181            ChannelLayout::Surround50,
1182            ChannelLayout::Surround51,
1183            ChannelLayout::Surround60,
1184            ChannelLayout::Surround61,
1185            ChannelLayout::Surround70,
1186            ChannelLayout::Surround71,
1187            ChannelLayout::LoRo,
1188            ChannelLayout::LtRt,
1189            ChannelLayout::DiscreteN(13),
1190        ];
1191        for layout in cases {
1192            let s = layout.to_string();
1193            let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
1194            assert_eq!(parsed, layout, "round-trip failed via {s:?}");
1195        }
1196    }
1197
1198    #[test]
1199    fn channel_layout_fromstr_accepts_aliases_and_case() {
1200        use std::str::FromStr;
1201        assert_eq!(
1202            ChannelLayout::from_str("STEREO").unwrap(),
1203            ChannelLayout::Stereo
1204        );
1205        assert_eq!(
1206            ChannelLayout::from_str("2.0").unwrap(),
1207            ChannelLayout::Stereo
1208        );
1209        assert_eq!(
1210            ChannelLayout::from_str("5.1").unwrap(),
1211            ChannelLayout::Surround51
1212        );
1213        assert_eq!(
1214            ChannelLayout::from_str("Lo/Ro").unwrap(),
1215            ChannelLayout::LoRo
1216        );
1217        assert_eq!(
1218            ChannelLayout::from_str("lt/rt").unwrap(),
1219            ChannelLayout::LtRt
1220        );
1221        assert!(ChannelLayout::from_str("absurd_layout").is_err());
1222    }
1223
1224    #[test]
1225    fn channel_layout_positions_owned_matches_static_slice() {
1226        for layout in [
1227            ChannelLayout::Mono,
1228            ChannelLayout::Surround51,
1229            ChannelLayout::Surround71,
1230        ] {
1231            assert_eq!(layout.positions_owned(), layout.positions());
1232        }
1233        // DiscreteN returns an empty owned vec — positions are unknown.
1234        assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
1235    }
1236
1237    #[test]
1238    fn sample_format_plane_count_interleaved_is_one() {
1239        // Interleaved formats always pack into a single plane, regardless
1240        // of channel count.
1241        for ch in [1u16, 2, 6, 8, 64, 0] {
1242            assert_eq!(SampleFormat::S16.plane_count(ch), 1);
1243            assert_eq!(SampleFormat::F32.plane_count(ch), 1);
1244            assert_eq!(SampleFormat::U8.plane_count(ch), 1);
1245            assert_eq!(SampleFormat::S24.plane_count(ch), 1);
1246        }
1247    }
1248
1249    #[test]
1250    fn sample_format_plane_count_planar_matches_channels() {
1251        // Planar formats use one plane per channel.
1252        assert_eq!(SampleFormat::S16P.plane_count(1), 1);
1253        assert_eq!(SampleFormat::S16P.plane_count(2), 2);
1254        assert_eq!(SampleFormat::F32P.plane_count(6), 6);
1255        assert_eq!(SampleFormat::F64P.plane_count(8), 8);
1256
1257        // Edge case: zero channels in a planar format yields zero planes.
1258        assert_eq!(SampleFormat::S32P.plane_count(0), 0);
1259    }
1260
1261    #[test]
1262    fn high_bit_yuv_bits_per_pixel_approx() {
1263        // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
1264        // packed-bits estimator (the approximation reports samples-per-pixel
1265        // density, not the 16-bit storage width).
1266        assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
1267        assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
1268        assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
1269        assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
1270        assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
1271
1272        // 16-bit: packed bits == storage bits (every bit of the 16-bit
1273        // word is significant), so the estimator lands on the same
1274        // numbers as the 10/12-bit siblings.
1275        assert_eq!(PixelFormat::Yuv420P16Le.bits_per_pixel_approx(), 24);
1276        assert_eq!(PixelFormat::Yuv422P16Le.bits_per_pixel_approx(), 32);
1277        assert_eq!(PixelFormat::Yuv444P16Le.bits_per_pixel_approx(), 48);
1278    }
1279
1280    #[test]
1281    fn yuva_planar_metadata() {
1282        // All three alpha-carrying planar YUV samplings share one shape:
1283        // planar, 4 planes (Y, U, V, full-resolution A), alpha set, not
1284        // a palette format.
1285        for fmt in [
1286            PixelFormat::Yuva420P,
1287            PixelFormat::Yuva422P,
1288            PixelFormat::Yuva444P,
1289        ] {
1290            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1291            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1292            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1293            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1294        }
1295
1296        // Packed-bits estimator: the alpha plane adds a full 8 bits per
1297        // pixel on top of the alpha-less sampling's density.
1298        assert_eq!(
1299            PixelFormat::Yuva420P.bits_per_pixel_approx(),
1300            PixelFormat::Yuv420P.bits_per_pixel_approx() + 8
1301        );
1302        assert_eq!(
1303            PixelFormat::Yuva422P.bits_per_pixel_approx(),
1304            PixelFormat::Yuv422P.bits_per_pixel_approx() + 8
1305        );
1306        assert_eq!(
1307            PixelFormat::Yuva444P.bits_per_pixel_approx(),
1308            PixelFormat::Yuv444P.bits_per_pixel_approx() + 8
1309        );
1310        assert_eq!(PixelFormat::Yuva422P.bits_per_pixel_approx(), 24);
1311        assert_eq!(PixelFormat::Yuva444P.bits_per_pixel_approx(), 32);
1312    }
1313
1314    #[test]
1315    fn deep_yuva_planar_metadata() {
1316        // All six deep alpha-carrying variants share one shape: planar,
1317        // 4 planes (Y, U, V, full-resolution A), alpha set, no palette.
1318        for fmt in [
1319            PixelFormat::Yuva422P10Le,
1320            PixelFormat::Yuva422P12Le,
1321            PixelFormat::Yuva444P10Le,
1322            PixelFormat::Yuva444P12Le,
1323            PixelFormat::Yuva422P16Le,
1324            PixelFormat::Yuva444P16Le,
1325        ] {
1326            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1327            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1328            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1329            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1330        }
1331    }
1332
1333    #[test]
1334    fn deep_yuva_bits_per_pixel_approx() {
1335        // Estimator reports 16-bit-word storage cost, matching the
1336        // alpha-less deep YUV trio: the full-resolution alpha word adds
1337        // 16 on top of the alpha-less sampling's number.
1338        for fmt in [
1339            PixelFormat::Yuva422P10Le,
1340            PixelFormat::Yuva422P12Le,
1341            PixelFormat::Yuva422P16Le,
1342        ] {
1343            assert_eq!(fmt.bits_per_pixel_approx(), 48, "{fmt:?}");
1344        }
1345        for fmt in [
1346            PixelFormat::Yuva444P10Le,
1347            PixelFormat::Yuva444P12Le,
1348            PixelFormat::Yuva444P16Le,
1349        ] {
1350            assert_eq!(fmt.bits_per_pixel_approx(), 64, "{fmt:?}");
1351        }
1352        assert_eq!(
1353            PixelFormat::Yuva422P16Le.bits_per_pixel_approx(),
1354            PixelFormat::Yuv422P16Le.bits_per_pixel_approx() + 16
1355        );
1356        assert_eq!(
1357            PixelFormat::Yuva444P16Le.bits_per_pixel_approx(),
1358            PixelFormat::Yuv444P16Le.bits_per_pixel_approx() + 16
1359        );
1360        assert_eq!(
1361            PixelFormat::Yuva422P10Le.bits_per_pixel_approx(),
1362            PixelFormat::Yuv422P10Le.bits_per_pixel_approx() + 16
1363        );
1364        assert_eq!(
1365            PixelFormat::Yuva444P12Le.bits_per_pixel_approx(),
1366            PixelFormat::Yuv444P12Le.bits_per_pixel_approx() + 16
1367        );
1368    }
1369
1370    #[test]
1371    fn high_bit_gbr_planar_metadata() {
1372        // All six new variants are planar with the right plane count.
1373        for fmt in [
1374            PixelFormat::Gbrp10Le,
1375            PixelFormat::Gbrp12Le,
1376            PixelFormat::Gbrp14Le,
1377        ] {
1378            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1379            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1380            assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1381            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1382        }
1383        for fmt in [
1384            PixelFormat::Gbrap10Le,
1385            PixelFormat::Gbrap12Le,
1386            PixelFormat::Gbrap14Le,
1387        ] {
1388            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1389            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1390            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1391            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1392        }
1393    }
1394
1395    #[test]
1396    fn high_bit_gbr_bits_per_pixel_approx() {
1397        // Packed bits-per-pixel = samples × bits (consistent with how
1398        // the 10/12-bit YUV variants are reported above).
1399        assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30);
1400        assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40);
1401        assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36);
1402        assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48);
1403        assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42);
1404        assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56);
1405    }
1406
1407    #[test]
1408    fn high_bit_gbr_constructible_and_distinct() {
1409        // Round-trip the discriminant through `as u16` and back via the
1410        // pinning test's reverse mapping — every variant must be unique.
1411        let all = [
1412            PixelFormat::Gbrp10Le,
1413            PixelFormat::Gbrap10Le,
1414            PixelFormat::Gbrp12Le,
1415            PixelFormat::Gbrap12Le,
1416            PixelFormat::Gbrp14Le,
1417            PixelFormat::Gbrap14Le,
1418            PixelFormat::Gbrp8,
1419            PixelFormat::Gbrap8,
1420            PixelFormat::Gbrp16Le,
1421            PixelFormat::Gbrap16Le,
1422        ];
1423        let mut seen = std::collections::HashSet::new();
1424        for fmt in all {
1425            assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
1426        }
1427    }
1428
1429    #[test]
1430    fn gbr_depth_ladder_ends_metadata() {
1431        // Gbrp8 and the 16-bit pair share the family shape: planar,
1432        // G/B/R plane order (3 planes), alpha only on Gbrap16Le, never
1433        // palette.
1434        for fmt in [PixelFormat::Gbrp8, PixelFormat::Gbrp16Le] {
1435            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1436            assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1437            assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1438            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1439        }
1440        assert!(PixelFormat::Gbrap16Le.is_planar());
1441        assert_eq!(PixelFormat::Gbrap16Le.plane_count(), 4);
1442        assert!(PixelFormat::Gbrap16Le.has_alpha());
1443        assert!(!PixelFormat::Gbrap16Le.is_palette());
1444    }
1445
1446    #[test]
1447    fn gbr_depth_ladder_ends_bits_per_pixel_approx() {
1448        // Gbrp8 matches the packed 8-bit RGB density (planar layout
1449        // doesn't change bits-per-pixel), and the 16-bit pair matches
1450        // the packed 16-bit RGB(A) densities — for 16-bit words packed
1451        // bits equal storage bits.
1452        assert_eq!(
1453            PixelFormat::Gbrp8.bits_per_pixel_approx(),
1454            PixelFormat::Rgb24.bits_per_pixel_approx()
1455        );
1456        assert_eq!(
1457            PixelFormat::Gbrp16Le.bits_per_pixel_approx(),
1458            PixelFormat::Rgb48Le.bits_per_pixel_approx()
1459        );
1460        assert_eq!(
1461            PixelFormat::Gbrap16Le.bits_per_pixel_approx(),
1462            PixelFormat::Rgba64Le.bits_per_pixel_approx()
1463        );
1464        assert_eq!(PixelFormat::Gbrp8.bits_per_pixel_approx(), 24);
1465        assert_eq!(PixelFormat::Gbrp16Le.bits_per_pixel_approx(), 48);
1466        assert_eq!(PixelFormat::Gbrap16Le.bits_per_pixel_approx(), 64);
1467    }
1468
1469    #[test]
1470    fn gbrap8_metadata() {
1471        // Gbrap8 completes the GBR(A) family: every depth on the
1472        // 8/10/12/14/16 ladder now has both an alpha-less and an
1473        // alpha-carrying variant. Shape matches the rest of the
1474        // alpha-carrying family: planar, 4 planes (G, B, R,
1475        // full-resolution A), alpha set, never palette.
1476        let fmt = PixelFormat::Gbrap8;
1477        assert!(fmt.is_planar());
1478        assert_eq!(fmt.plane_count(), 4);
1479        assert!(fmt.has_alpha());
1480        assert!(!fmt.is_palette());
1481    }
1482
1483    #[test]
1484    fn gbrap8_bits_per_pixel_approx() {
1485        // Four bytes per pixel: the packed Rgba density (planar layout
1486        // doesn't change bits-per-pixel), i.e. the alpha plane adds a
1487        // full 8 bits on top of Gbrp8.
1488        assert_eq!(PixelFormat::Gbrap8.bits_per_pixel_approx(), 32);
1489        assert_eq!(
1490            PixelFormat::Gbrap8.bits_per_pixel_approx(),
1491            PixelFormat::Rgba.bits_per_pixel_approx()
1492        );
1493        assert_eq!(
1494            PixelFormat::Gbrap8.bits_per_pixel_approx(),
1495            PixelFormat::Gbrp8.bits_per_pixel_approx() + 8
1496        );
1497    }
1498
1499    #[test]
1500    fn gbr_family_alpha_ladder_complete() {
1501        // Every GBR depth has an alpha companion with exactly one more
1502        // plane and the same planarity — the asymmetry Gbrap8 closed.
1503        let pairs = [
1504            (PixelFormat::Gbrp8, PixelFormat::Gbrap8),
1505            (PixelFormat::Gbrp10Le, PixelFormat::Gbrap10Le),
1506            (PixelFormat::Gbrp12Le, PixelFormat::Gbrap12Le),
1507            (PixelFormat::Gbrp14Le, PixelFormat::Gbrap14Le),
1508            (PixelFormat::Gbrp16Le, PixelFormat::Gbrap16Le),
1509        ];
1510        for (gbr, gbra) in pairs {
1511            assert!(gbr.is_planar() && gbra.is_planar());
1512            assert_eq!(gbr.plane_count(), 3, "{gbr:?}");
1513            assert_eq!(gbra.plane_count(), 4, "{gbra:?}");
1514            assert!(!gbr.has_alpha(), "{gbr:?}");
1515            assert!(gbra.has_alpha(), "{gbra:?}");
1516        }
1517    }
1518
1519    #[test]
1520    fn ya16le_metadata() {
1521        // Same packed shape as Ya8 (interleaved Y, A in one plane),
1522        // widened to 16-bit LE words: not planar, single plane, alpha
1523        // set, never palette. Density is exactly double Ya8's and
1524        // matches half of Rgba64Le (two components instead of four).
1525        let fmt = PixelFormat::Ya16Le;
1526        assert!(!fmt.is_planar());
1527        assert_eq!(fmt.plane_count(), 1);
1528        assert!(fmt.has_alpha());
1529        assert!(!fmt.is_palette());
1530        assert_eq!(fmt.bits_per_pixel_approx(), 32);
1531        assert_eq!(
1532            fmt.bits_per_pixel_approx(),
1533            PixelFormat::Ya8.bits_per_pixel_approx() * 2
1534        );
1535        assert_eq!(
1536            fmt.bits_per_pixel_approx(),
1537            PixelFormat::Rgba64Le.bits_per_pixel_approx() / 2
1538        );
1539        // The alpha word adds a full 16 bits on top of Gray16Le.
1540        assert_eq!(
1541            fmt.bits_per_pixel_approx(),
1542            PixelFormat::Gray16Le.bits_per_pixel_approx() + 16
1543        );
1544    }
1545
1546    #[test]
1547    fn cmyk_inverted_metadata() {
1548        // The inverted-ink convention changes sample semantics, not
1549        // layout: CmykInverted must be metadata-identical to Cmyk on
1550        // every shape predicate.
1551        let (reg, inv) = (PixelFormat::Cmyk, PixelFormat::CmykInverted);
1552        for fmt in [reg, inv] {
1553            assert!(!fmt.is_planar(), "{fmt:?}");
1554            assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
1555            assert!(!fmt.has_alpha(), "{fmt:?}");
1556            assert!(!fmt.is_palette(), "{fmt:?}");
1557        }
1558        assert_eq!(reg.bits_per_pixel_approx(), inv.bits_per_pixel_approx());
1559        assert_eq!(inv.bits_per_pixel_approx(), 32);
1560        // They remain distinct formats on the wire-stable axis.
1561        assert_ne!(reg as u16, inv as u16);
1562    }
1563
1564    #[test]
1565    fn deep_yuva420_planar_metadata() {
1566        // The 4:2:0 completions share the deep-Yuva shape: planar, 4
1567        // planes (Y, U, V, full-resolution A), alpha set, no palette.
1568        for fmt in [
1569            PixelFormat::Yuva420P10Le,
1570            PixelFormat::Yuva420P12Le,
1571            PixelFormat::Yuva420P16Le,
1572        ] {
1573            assert!(fmt.is_planar(), "{fmt:?} must be planar");
1574            assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1575            assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1576            assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1577        }
1578    }
1579
1580    #[test]
1581    fn deep_yuva420_bits_per_pixel_approx() {
1582        // Same estimator convention as the 4:2:2/4:4:4 deep Yuva arms:
1583        // 16-bit-word storage cost, with the full-resolution alpha word
1584        // adding 16 on top of the alpha-less sampling's number.
1585        for fmt in [
1586            PixelFormat::Yuva420P10Le,
1587            PixelFormat::Yuva420P12Le,
1588            PixelFormat::Yuva420P16Le,
1589        ] {
1590            assert_eq!(fmt.bits_per_pixel_approx(), 40, "{fmt:?}");
1591        }
1592        assert_eq!(
1593            PixelFormat::Yuva420P10Le.bits_per_pixel_approx(),
1594            PixelFormat::Yuv420P10Le.bits_per_pixel_approx() + 16
1595        );
1596        assert_eq!(
1597            PixelFormat::Yuva420P12Le.bits_per_pixel_approx(),
1598            PixelFormat::Yuv420P12Le.bits_per_pixel_approx() + 16
1599        );
1600        assert_eq!(
1601            PixelFormat::Yuva420P16Le.bits_per_pixel_approx(),
1602            PixelFormat::Yuv420P16Le.bits_per_pixel_approx() + 16
1603        );
1604    }
1605}