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}