oxideav_core/format.rs
1//! Media-type and sample/pixel format enumerations.
2//!
3//! Audio channel ordering follows SMPTE 2036-2 / ITU-R BS.775 conventions
4//! for surround layouts; per-channel positions are named with the
5//! WAVEFORMATEXTENSIBLE "front-left, front-right, …" vocabulary.
6
7/// Broad category of a stream's payload.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum MediaType {
10 /// Audio samples.
11 Audio,
12 /// Video pictures.
13 Video,
14 /// Timed-text / bitmap subtitle cues.
15 Subtitle,
16 /// Opaque non-media payload (timecodes, klv, chapters, …).
17 Data,
18 /// Category not (yet) determined.
19 Unknown,
20}
21
22/// A single speaker position within a multi-channel audio layout.
23///
24/// Names follow the WAVEFORMATEXTENSIBLE / SMPTE convention.
25/// `Side*` and `Back*` are kept distinct (mirroring 7.1's
26/// L/R + Ls/Rs + Lb/Rb separation) so codecs that surface the
27/// distinction don't collapse it. `Lr`/`Rr` (rear / back-rear) are aliases
28/// for `BackLeft`/`BackRight` in this taxonomy — the rear pair sits behind
29/// the listener on the room's centreline-extension, the side pair is at
30/// roughly ±90° from front. The enum is `#[non_exhaustive]` so additional
31/// positions (height channels for Atmos / Auro-3D, etc.) can be added
32/// without breaking downstream match arms.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum ChannelPosition {
36 /// Front-left (L). 30° left of centre in BS.775 listening geometry.
37 FrontLeft,
38 /// Front-right (R). 30° right of centre.
39 FrontRight,
40 /// Front-centre (C). Direct centre, 0°.
41 FrontCenter,
42 /// Low-frequency effects (LFE). Sub-bass, no positional meaning.
43 LowFrequency,
44 /// Back-left (Lb / Lr). Behind the listener, ±150° in 7.1.
45 BackLeft,
46 /// Back-right (Rb / Rr). Behind the listener, mirror of `BackLeft`.
47 BackRight,
48 /// Front left-of-centre (Lc). Used in cinema 7.1 SDDS layouts.
49 FrontLeftOfCenter,
50 /// Front right-of-centre (Rc). Mirror of `FrontLeftOfCenter`.
51 FrontRightOfCenter,
52 /// Back-centre (Cs). Single rear channel for 6.1 / BS.775 4.0.
53 BackCenter,
54 /// Side-left (Ls). ±90° on the listener's left in 5.1 / 7.1.
55 SideLeft,
56 /// Side-right (Rs). Mirror of `SideLeft`.
57 SideRight,
58 /// Top front-left. Atmos / Auro-3D height layer (placeholder).
59 TopFrontLeft,
60 /// Top front-right. Atmos / Auro-3D height layer (placeholder).
61 TopFrontRight,
62 /// Top back-left. Atmos / Auro-3D ceiling layer (placeholder).
63 TopBackLeft,
64 /// Top back-right. Atmos / Auro-3D ceiling layer (placeholder).
65 TopBackRight,
66}
67
68/// Audio channel layout — names a fixed ordered tuple of speaker
69/// positions, OR carries a discrete fallback count when the layout is
70/// unknown / non-standard.
71///
72/// Channel orderings are taken from ITU-R BS.775 (5.1 / 7.1 surround
73/// reference) and SMPTE ST 2036-2 (audio channel ordering for UHDTV).
74/// For 5.1 the canonical order this crate adopts is
75/// `L, R, C, LFE, Ls, Rs` (the WAVEFORMATEXTENSIBLE / Vorbis / Opus
76/// convention). 7.1 extends that with `Lb, Rb` (back-rear pair).
77///
78/// The `Stereo` variant covers both regular two-channel stereo and the
79/// AC-3 / AC-4 matrix-encoded downmix carriers `Lo/Ro` ("two of",
80/// downmix-compatible) and `Lt/Rt` ("matrix-encoded for Pro Logic
81/// extraction"); the dedicated [`LoRo`](ChannelLayout::LoRo) /
82/// [`LtRt`](ChannelLayout::LtRt) variants surface the distinction
83/// explicitly when a downstream filter or muxer needs it.
84///
85/// `DiscreteN(n)` is the catch-all for "we know there are `n` channels
86/// but no recognised layout" — used when a codec produces an unusual
87/// channel count (>8) or when the container failed to surface a layout
88/// flag. It is the only variant whose `position()` returns `None`.
89///
90/// Marked `#[non_exhaustive]` so additional standard layouts (Atmos
91/// 7.1.4, Auro-3D 9.1, …) can be added without breaking match-exhaustive
92/// downstream consumers.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub enum ChannelLayout {
96 /// Mono (1ch): C.
97 Mono,
98 /// Stereo (2ch): L, R.
99 Stereo,
100 /// 2.1 (3ch): L, R, LFE.
101 Stereo21,
102 /// 3.0 surround (3ch): L, R, C.
103 Surround30,
104 /// Quadraphonic (4ch): L, R, Ls, Rs — no centre, side surrounds.
105 Quad,
106 /// 4.0 surround per BS.775 (4ch): L, R, C, Cs — centre + back surround.
107 Surround40,
108 /// 4.1 surround (5ch): L, R, C, Cs, LFE.
109 Surround41,
110 /// 5.0 surround (5ch): L, R, C, Ls, Rs.
111 Surround50,
112 /// 5.1 surround (6ch): L, R, C, LFE, Ls, Rs.
113 Surround51,
114 /// 6.0 surround (6ch): L, R, C, Cs, Ls, Rs.
115 Surround60,
116 /// 6.1 surround (7ch): L, R, C, LFE, Cs, Ls, Rs.
117 Surround61,
118 /// 7.0 surround (7ch): L, R, C, Ls, Rs, Lb, Rb.
119 Surround70,
120 /// 7.1 surround (8ch): L, R, C, LFE, Ls, Rs, Lb, Rb.
121 Surround71,
122 /// AC-3 / AC-4 Lo/Ro stereo downmix (2ch). Two-channel mix preserving
123 /// downmix-compatibility coefficients; not matrix-encoded.
124 LoRo,
125 /// AC-3 / AC-4 Lt/Rt stereo downmix (2ch). Two-channel matrix-encoded
126 /// downmix carrying surround information for Dolby Pro Logic decoding.
127 LtRt,
128 /// Discrete fallback: `n` channels with no recognised layout. Used for
129 /// unusual / >8ch / unknown layouts surfaced by exotic codecs or
130 /// containers that drop layout flags.
131 DiscreteN(u16),
132}
133
134impl ChannelLayout {
135 /// Number of channels in this layout.
136 pub fn channel_count(&self) -> u16 {
137 match self {
138 Self::Mono => 1,
139 Self::Stereo | Self::LoRo | Self::LtRt => 2,
140 Self::Stereo21 | Self::Surround30 => 3,
141 Self::Quad | Self::Surround40 => 4,
142 Self::Surround41 | Self::Surround50 => 5,
143 Self::Surround51 | Self::Surround60 => 6,
144 Self::Surround61 | Self::Surround70 => 7,
145 Self::Surround71 => 8,
146 Self::DiscreteN(n) => *n,
147 }
148 }
149
150 /// Speaker positions in canonical order. Returns an empty slice for
151 /// `DiscreteN` since the layout is unknown — call [`positions_owned`]
152 /// to get a `Vec` if you need to enumerate slots regardless of
153 /// known/unknown status.
154 ///
155 /// [`positions_owned`]: Self::positions_owned
156 pub fn positions(&self) -> &'static [ChannelPosition] {
157 use ChannelPosition::*;
158 match self {
159 Self::Mono => &[FrontCenter],
160 Self::Stereo | Self::LoRo | Self::LtRt => &[FrontLeft, FrontRight],
161 Self::Stereo21 => &[FrontLeft, FrontRight, LowFrequency],
162 Self::Surround30 => &[FrontLeft, FrontRight, FrontCenter],
163 Self::Quad => &[FrontLeft, FrontRight, SideLeft, SideRight],
164 Self::Surround40 => &[FrontLeft, FrontRight, FrontCenter, BackCenter],
165 Self::Surround41 => &[FrontLeft, FrontRight, FrontCenter, BackCenter, LowFrequency],
166 Self::Surround50 => &[FrontLeft, FrontRight, FrontCenter, SideLeft, SideRight],
167 Self::Surround51 => &[
168 FrontLeft,
169 FrontRight,
170 FrontCenter,
171 LowFrequency,
172 SideLeft,
173 SideRight,
174 ],
175 Self::Surround60 => &[
176 FrontLeft,
177 FrontRight,
178 FrontCenter,
179 BackCenter,
180 SideLeft,
181 SideRight,
182 ],
183 Self::Surround61 => &[
184 FrontLeft,
185 FrontRight,
186 FrontCenter,
187 LowFrequency,
188 BackCenter,
189 SideLeft,
190 SideRight,
191 ],
192 Self::Surround70 => &[
193 FrontLeft,
194 FrontRight,
195 FrontCenter,
196 SideLeft,
197 SideRight,
198 BackLeft,
199 BackRight,
200 ],
201 Self::Surround71 => &[
202 FrontLeft,
203 FrontRight,
204 FrontCenter,
205 LowFrequency,
206 SideLeft,
207 SideRight,
208 BackLeft,
209 BackRight,
210 ],
211 Self::DiscreteN(_) => &[],
212 }
213 }
214
215 /// Owned position list. For known layouts this clones [`positions`];
216 /// for `DiscreteN(n)` it returns an empty `Vec` (positions remain
217 /// unknown). Provided so callers that just want "give me positions
218 /// for any layout" don't have to special-case the discrete arm.
219 ///
220 /// [`positions`]: Self::positions
221 pub fn positions_owned(&self) -> Vec<ChannelPosition> {
222 self.positions().to_vec()
223 }
224
225 /// Speaker position at slot `idx` in canonical order, or `None` for
226 /// out-of-range slots and for `DiscreteN` (where the layout is
227 /// unknown).
228 pub fn position(&self, idx: usize) -> Option<ChannelPosition> {
229 self.positions().get(idx).copied()
230 }
231
232 /// True when this layout carries a low-frequency-effects (LFE) channel.
233 pub fn has_lfe(&self) -> bool {
234 self.positions()
235 .iter()
236 .any(|p| matches!(p, ChannelPosition::LowFrequency))
237 }
238
239 /// True when this layout carries surround information (more than two
240 /// channels OR an LFE). `Stereo` / `Mono` return false; `LoRo` /
241 /// `LtRt` are 2-channel downmixes and also return false even though
242 /// they encode surround content (that's the whole point of a
243 /// downmix).
244 pub fn is_surround(&self) -> bool {
245 self.channel_count() > 2 || self.has_lfe()
246 }
247
248 /// Back-compat bridge: infer a layout from a bare channel count.
249 ///
250 /// This mapping is what lets codecs that haven't been updated to set
251 /// a layout explicitly continue to work: they keep producing a count
252 /// and we infer the most-common layout for that count. The choices
253 /// follow industry defaults — 5.1 wins for 6ch (more common than
254 /// 6.0), 7.1 wins for 8ch, and so on.
255 ///
256 /// | count | layout |
257 /// |-------|--------------|
258 /// | 1 | `Mono` |
259 /// | 2 | `Stereo` |
260 /// | 3 | `Surround30` |
261 /// | 4 | `Quad` |
262 /// | 5 | `Surround50` |
263 /// | 6 | `Surround51` |
264 /// | 7 | `Surround61` |
265 /// | 8 | `Surround71` |
266 /// | other | `DiscreteN` |
267 pub fn from_count(n: u16) -> ChannelLayout {
268 match n {
269 1 => Self::Mono,
270 2 => Self::Stereo,
271 3 => Self::Surround30,
272 4 => Self::Quad,
273 5 => Self::Surround50,
274 6 => Self::Surround51,
275 7 => Self::Surround61,
276 8 => Self::Surround71,
277 other => Self::DiscreteN(other),
278 }
279 }
280}
281
282impl std::fmt::Display for ChannelLayout {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 let s = match self {
285 Self::Mono => "mono",
286 Self::Stereo => "stereo",
287 Self::Stereo21 => "2.1",
288 Self::Surround30 => "3.0",
289 Self::Quad => "quad",
290 Self::Surround40 => "4.0",
291 Self::Surround41 => "4.1",
292 Self::Surround50 => "5.0",
293 Self::Surround51 => "5.1",
294 Self::Surround60 => "6.0",
295 Self::Surround61 => "6.1",
296 Self::Surround70 => "7.0",
297 Self::Surround71 => "7.1",
298 Self::LoRo => "loro",
299 Self::LtRt => "ltrt",
300 Self::DiscreteN(n) => return write!(f, "discrete{n}"),
301 };
302 f.write_str(s)
303 }
304}
305
306/// Error returned by the [`ChannelLayout`] `FromStr` impl when the input
307/// doesn't match any recognised layout name.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct ParseChannelLayoutError(pub String);
310
311impl std::fmt::Display for ParseChannelLayoutError {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 write!(f, "unrecognised channel layout: {:?}", self.0)
314 }
315}
316
317impl std::error::Error for ParseChannelLayoutError {}
318
319impl std::str::FromStr for ChannelLayout {
320 type Err = ParseChannelLayoutError;
321
322 fn from_str(s: &str) -> Result<Self, Self::Err> {
323 let lower = s.trim().to_ascii_lowercase();
324 let layout = match lower.as_str() {
325 "mono" | "1.0" => Self::Mono,
326 "stereo" | "2.0" => Self::Stereo,
327 "2.1" => Self::Stereo21,
328 "3.0" | "surround3" | "surround30" => Self::Surround30,
329 "quad" => Self::Quad,
330 "4.0" | "surround4" | "surround40" => Self::Surround40,
331 "4.1" | "surround41" => Self::Surround41,
332 "5.0" | "surround5" | "surround50" => Self::Surround50,
333 "5.1" | "surround51" => Self::Surround51,
334 "6.0" | "surround6" | "surround60" => Self::Surround60,
335 "6.1" | "surround61" => Self::Surround61,
336 "7.0" | "surround7" | "surround70" => Self::Surround70,
337 "7.1" | "surround71" => Self::Surround71,
338 "loro" | "lo/ro" => Self::LoRo,
339 "ltrt" | "lt/rt" => Self::LtRt,
340 other => {
341 if let Some(rest) = other.strip_prefix("discrete") {
342 if let Ok(n) = rest.parse::<u16>() {
343 return Ok(Self::DiscreteN(n));
344 }
345 }
346 return Err(ParseChannelLayoutError(s.to_owned()));
347 }
348 };
349 Ok(layout)
350 }
351}
352
353/// Audio sample format.
354///
355/// Variants carry **stable explicit discriminants** — the integer value
356/// of `SampleFormat::S16 as u8` is part of the public ABI. Add new
357/// variants only at the end with a fresh number; never reorder, renumber,
358/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
359/// downstream `match` statements; pinned discriminants additionally let
360/// the format round-trip through any byte-stable serialization
361/// (config files, capability blobs, IPC) without losing meaning across
362/// crate versions.
363#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
364#[non_exhaustive]
365#[repr(u8)]
366pub enum SampleFormat {
367 /// Unsigned 8-bit, interleaved.
368 U8 = 0,
369 /// Signed 8-bit, interleaved. Native format of Amiga 8SVX and MOD samples.
370 S8 = 1,
371 /// Signed 16-bit little-endian, interleaved.
372 S16 = 2,
373 /// Signed 24-bit packed (3 bytes/sample) little-endian, interleaved.
374 S24 = 3,
375 /// Signed 32-bit little-endian, interleaved.
376 S32 = 4,
377 /// 32-bit IEEE float, interleaved.
378 F32 = 5,
379 /// 64-bit IEEE float, interleaved.
380 F64 = 6,
381 /// Unsigned 8-bit, planar (one plane per channel).
382 U8P = 7,
383 /// Signed 16-bit little-endian, planar (one plane per channel).
384 S16P = 8,
385 /// Signed 32-bit little-endian, planar (one plane per channel).
386 S32P = 9,
387 /// 32-bit IEEE float, planar (one plane per channel).
388 F32P = 10,
389 /// 64-bit IEEE float, planar (one plane per channel).
390 F64P = 11,
391}
392
393impl SampleFormat {
394 /// `true` for the planar (one-plane-per-channel) variants.
395 pub fn is_planar(&self) -> bool {
396 matches!(
397 self,
398 Self::U8P | Self::S16P | Self::S32P | Self::F32P | Self::F64P
399 )
400 }
401
402 /// Bytes per sample *per channel*.
403 pub fn bytes_per_sample(&self) -> usize {
404 match self {
405 Self::U8 | Self::U8P | Self::S8 => 1,
406 Self::S16 | Self::S16P => 2,
407 Self::S24 => 3,
408 Self::S32 | Self::S32P | Self::F32 | Self::F32P => 4,
409 Self::F64 | Self::F64P => 8,
410 }
411 }
412
413 /// `true` for the IEEE-float variants (32- or 64-bit, either layout).
414 pub fn is_float(&self) -> bool {
415 matches!(self, Self::F32 | Self::F64 | Self::F32P | Self::F64P)
416 }
417
418 /// Number of `Vec<u8>` planes an [`AudioFrame`](crate::AudioFrame)
419 /// of this format carries for `channels` channels: planar formats
420 /// use one plane per channel, interleaved formats use one plane
421 /// total.
422 pub fn plane_count(&self, channels: u16) -> usize {
423 if self.is_planar() {
424 channels as usize
425 } else {
426 1
427 }
428 }
429}
430
431/// Video pixel format.
432///
433/// Variants carry **stable explicit discriminants** — the integer value
434/// of `PixelFormat::Yuv420P as u16` is part of the public ABI. Add new
435/// variants only at the end with a fresh number; never reorder, renumber,
436/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
437/// downstream `match` statements; pinned discriminants additionally let
438/// the format round-trip through any byte-stable serialization
439/// (config files, capability blobs, IPC, on-disk caches) without losing
440/// meaning across crate versions, and prevent inserts in the middle of
441/// the enum from shifting every later variant's number (which
442/// cargo-semver-checks rightly flags as a breaking change).
443///
444/// The first six variants (`Yuv420P` through `Gray8`) are the original
445/// formats produced by the early codec crates. Everything beyond that
446/// is additional surface handled by `oxideav-pixfmt` and the still-image
447/// codecs (PNG, GIF, still-JPEG).
448#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
449#[non_exhaustive]
450#[repr(u16)]
451pub enum PixelFormat {
452 /// 8-bit YUV 4:2:0, planar (Y, U, V).
453 Yuv420P = 0,
454 /// 8-bit YUV 4:2:2, planar.
455 Yuv422P = 1,
456 /// 8-bit YUV 4:4:4, planar.
457 Yuv444P = 2,
458 /// Packed 8-bit RGB, 3 bytes/pixel.
459 Rgb24 = 3,
460 /// Packed 8-bit RGBA, 4 bytes/pixel.
461 Rgba = 4,
462 /// Packed 8-bit grayscale.
463 Gray8 = 5,
464
465 // --- Palette ---
466 /// 8-bit palette indices — companion palette carried out of band.
467 Pal8 = 6,
468
469 // --- Packed RGB/BGR swizzles ---
470 /// Packed 8-bit BGR, 3 bytes/pixel.
471 Bgr24 = 7,
472 /// Packed 8-bit BGRA, 4 bytes/pixel.
473 Bgra = 8,
474 /// Packed 8-bit ARGB, 4 bytes/pixel (alpha first).
475 Argb = 9,
476 /// Packed 8-bit ABGR, 4 bytes/pixel.
477 Abgr = 10,
478
479 // --- Deeper packed RGB ---
480 /// Packed 16-bit-per-channel RGB, little-endian, 6 bytes/pixel.
481 Rgb48Le = 11,
482 /// Packed 16-bit-per-channel RGBA, little-endian, 8 bytes/pixel.
483 Rgba64Le = 12,
484
485 // --- Grayscale deeper / partial bit depths ---
486 /// 16-bit little-endian grayscale.
487 Gray16Le = 13,
488 /// 10-bit grayscale in a 16-bit little-endian word.
489 Gray10Le = 14,
490 /// 12-bit grayscale in a 16-bit little-endian word.
491 Gray12Le = 15,
492
493 // --- Higher-precision YUV ---
494 /// 10-bit YUV 4:2:0 planar, little-endian 16-bit storage.
495 Yuv420P10Le = 16,
496 /// 10-bit YUV 4:2:2 planar, little-endian 16-bit storage.
497 Yuv422P10Le = 17,
498 /// 10-bit YUV 4:4:4 planar, little-endian 16-bit storage.
499 Yuv444P10Le = 18,
500 /// 12-bit YUV 4:2:0 planar, little-endian 16-bit storage.
501 Yuv420P12Le = 19,
502 /// 12-bit YUV 4:2:2 planar, little-endian 16-bit storage.
503 Yuv422P12Le = 20,
504 /// 12-bit YUV 4:4:4 planar, little-endian 16-bit storage.
505 Yuv444P12Le = 21,
506
507 // --- Full-range ("J") YUV ---
508 //
509 // Legacy labels: the range is part of the format name. Every other
510 // format leaves the range to the stream's / frame's colour signal
511 // (`CodecParameters::color_signal`, `VideoFrame::color_signal`);
512 // see `implied_color_range`.
513 /// JPEG/full-range YUV 4:2:0 planar. Implies
514 /// [`ColorRange::Full`](crate::ColorRange::Full).
515 YuvJ420P = 22,
516 /// JPEG/full-range YUV 4:2:2 planar. Implies
517 /// [`ColorRange::Full`](crate::ColorRange::Full).
518 YuvJ422P = 23,
519 /// JPEG/full-range YUV 4:4:4 planar. Implies
520 /// [`ColorRange::Full`](crate::ColorRange::Full).
521 YuvJ444P = 24,
522
523 // --- Semi-planar YUV ---
524 /// YUV 4:2:0, planar Y + interleaved UV (NV12).
525 Nv12 = 25,
526 /// YUV 4:2:0, planar Y + interleaved VU (NV21).
527 Nv21 = 26,
528
529 // --- Gray + alpha / YUV + alpha ---
530 /// Packed grayscale + alpha, 2 bytes/pixel (Y, A).
531 Ya8 = 27,
532 /// Yuv420P with an additional full-resolution alpha plane.
533 Yuva420P = 28,
534
535 // --- Mono (1 bit per pixel) ---
536 /// 1 bit per pixel, packed MSB-first, 0 = black.
537 MonoBlack = 29,
538 /// 1 bit per pixel, packed MSB-first, 0 = white.
539 MonoWhite = 30,
540
541 // --- Interleaved YUV 4:2:2 ---
542 /// Packed 4:2:2, byte order Y0 U0 Y1 V0.
543 Yuyv422 = 31,
544 /// Packed 4:2:2, byte order U0 Y0 V0 Y1.
545 Uyvy422 = 32,
546
547 // --- Print / prepress ---
548 /// Packed 8-bit CMYK, 4 bytes/pixel in byte order C, M, Y, K.
549 /// "Regular" convention: C=0 means no cyan ink (white), C=255 means
550 /// full cyan. Used by JPEG 4-component scans from non-Adobe encoders
551 /// and by many print-side image toolchains. Adobe Photoshop's
552 /// inverted CMYK (where 0 = full ink) is the separate
553 /// [`CmykInverted`](Self::CmykInverted) variant.
554 Cmyk = 33,
555
556 // --- Wide-horizontal subsampled YUV ---
557 /// 8-bit YUV 4:1:1, planar (Y, U, V). Luma at full resolution; chroma
558 /// horizontally subsampled by 4 (each chroma sample covers a 4×1
559 /// luma block), no vertical subsampling. Native sampling of
560 /// NTSC DV-25 and a legal JPEG sampling layout (luma H=4, V=1;
561 /// chroma H=V=1) emitted by some real-world JPEG corpora.
562 Yuv411P = 34,
563
564 // --- Planar GBR / GBRA (RGB stored as planes in G,B,R order) ---
565 //
566 // High-bit-depth GBR(A) layouts used by MagicYUV, JPEG 2000, OpenEXR,
567 // TIFF and similar workflows that need lossless RGB at 10/12/14 bits
568 // per channel. Planes are ordered G, B, R (and A for the `Gbrap*`
569 // variants) — and
570 // each sample is stored as a 16-bit little-endian word with the
571 // top bits zero. The native 8-bit ([`Gbrp8`](Self::Gbrp8)) and
572 // full-width 16-bit ([`Gbrp16Le`](Self::Gbrp16Le) /
573 // [`Gbrap16Le`](Self::Gbrap16Le)) companions arrived later and
574 // therefore live at fresh appended discriminants (52-54), per the
575 // append-only rule.
576 /// 10-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
577 /// G, B, R; each sample uses the low 10 bits of a 16-bit word.
578 Gbrp10Le = 35,
579 /// 10-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
580 /// ordered G, B, R, A; each sample uses the low 10 bits of a 16-bit
581 /// word.
582 Gbrap10Le = 36,
583 /// 12-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
584 /// G, B, R; each sample uses the low 12 bits of a 16-bit word.
585 Gbrp12Le = 37,
586 /// 12-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
587 /// ordered G, B, R, A; each sample uses the low 12 bits of a 16-bit
588 /// word.
589 Gbrap12Le = 38,
590 /// 14-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
591 /// G, B, R; each sample uses the low 14 bits of a 16-bit word.
592 Gbrp14Le = 39,
593 /// 14-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
594 /// ordered G, B, R, A; each sample uses the low 14 bits of a 16-bit
595 /// word.
596 Gbrap14Le = 40,
597
598 // --- 16-bit YUV planar ---
599 //
600 // Full-width companions to the 10/12-bit planar YUV variants above:
601 // same three-plane layout and little-endian 16-bit words, but ALL 16
602 // bits of every word are significant (there are no zero top bits and
603 // no separate "valid bits" count — full-scale is 65535). Needed by
604 // wavelet codecs whose signal-range presets go to 16 bits per
605 // component (SMPTE VC-2 / Dirac video-format presets 7 and 8).
606 /// 16-bit YUV 4:2:0 planar, little-endian 16-bit storage. All 16
607 /// bits of each sample word are significant.
608 Yuv420P16Le = 41,
609 /// 16-bit YUV 4:2:2 planar, little-endian 16-bit storage. All 16
610 /// bits of each sample word are significant.
611 Yuv422P16Le = 42,
612 /// 16-bit YUV 4:4:4 planar, little-endian 16-bit storage. All 16
613 /// bits of each sample word are significant.
614 Yuv444P16Le = 43,
615
616 // --- 8-bit YUV + alpha at the remaining chroma samplings ---
617 //
618 // Companions to `Yuva420P`: the alpha plane is always full
619 // resolution (one 8-bit sample per pixel, never chroma-subsampled),
620 // appended after the V plane as plane index 3. Intermediate/mezzanine
621 // codecs carry alpha at 4:2:2 and 4:4:4 samplings.
622 /// Yuv422P with an additional full-resolution alpha plane.
623 Yuva422P = 44,
624 /// Yuv444P with an additional full-resolution alpha plane.
625 Yuva444P = 45,
626
627 // --- Deep YUV + alpha (10/12/16-bit words with full-resolution A) ---
628 //
629 // Alpha-carrying companions to the 10/12/16-bit planar YUV variants
630 // above, completing the Yuva family for mezzanine codecs that carry
631 // deep colour together with an alpha channel. Same conventions as
632 // the 8-bit `Yuva*` trio: 4 planes ordered Y, U, V, A with the
633 // alpha plane always at full resolution (one sample per pixel,
634 // never chroma-subsampled) as plane index 3. Every sample — alpha
635 // included — is stored as a little-endian 16-bit word; for the
636 // 10/12-bit variants each sample uses the low bits of the word with
637 // the top bits zero, and for the 16-bit variants all 16 bits of
638 // every word are significant (full-scale is 65535), matching
639 // `Yuv420P16Le`/`Yuv422P16Le`/`Yuv444P16Le`.
640 /// 10-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
641 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
642 /// the low 10 bits of a 16-bit word.
643 Yuva422P10Le = 46,
644 /// 12-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
645 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
646 /// the low 12 bits of a 16-bit word.
647 Yuva422P12Le = 47,
648 /// 10-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
649 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
650 /// the low 10 bits of a 16-bit word.
651 Yuva444P10Le = 48,
652 /// 12-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
653 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
654 /// the low 12 bits of a 16-bit word.
655 Yuva444P12Le = 49,
656 /// 16-bit YUV 4:2:2 planar + full-resolution alpha, little-endian
657 /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
658 /// each sample word are significant.
659 Yuva422P16Le = 50,
660 /// 16-bit YUV 4:4:4 planar + full-resolution alpha, little-endian
661 /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
662 /// each sample word are significant.
663 Yuva444P16Le = 51,
664
665 // --- Native 8-bit and full-width 16-bit planar GBR(A) ---
666 //
667 // Companions to the 10/12/14-bit `Gbrp*`/`Gbrap*` family above,
668 // closing the planar-RGB depth ladder at both ends for lossless
669 // RGB codecs whose native coding space is per-plane G, B, R.
670 // Plane order is identical to the rest of the family: G, B, R
671 // (and A as plane index 3 for `Gbrap16Le`, always at full
672 // resolution — RGB has no chroma subsampling). `Gbrp8` stores one
673 // byte per sample with all 8 bits significant; the 16-bit variants
674 // store little-endian 16-bit words with ALL 16 bits significant
675 // (full-scale is 65535, matching the `Yuv*P16Le` convention — no
676 // zero top bits, no separate valid-bits count). Odd in-between
677 // depths on these storage formats (e.g. 9- or 15-bit RGB) are
678 // expressed via the per-plane significant-bits side-channel on
679 // `VideoFrame`, not by new enum variants.
680 /// 8-bit planar GBR. 3 planes ordered G, B, R; one byte per
681 /// sample, all 8 bits significant.
682 Gbrp8 = 52,
683 /// 16-bit planar GBR, little-endian 16-bit storage. 3 planes
684 /// ordered G, B, R; all 16 bits of each sample word are
685 /// significant.
686 Gbrp16Le = 53,
687 /// 16-bit planar GBR + alpha, little-endian 16-bit storage. 4
688 /// planes ordered G, B, R, A; all 16 bits of each sample word are
689 /// significant.
690 Gbrap16Le = 54,
691
692 // --- Deep YUV + alpha at 4:2:0 ---
693 //
694 // Completes the deep Yuva family begun by the 4:2:2/4:4:4 variants
695 // above (46-51) at the remaining chroma sampling. Same conventions:
696 // 4 planes ordered Y, U, V, A with the alpha plane always at full
697 // resolution (one sample per pixel, never chroma-subsampled) as
698 // plane index 3. Every sample — alpha included — is stored as a
699 // little-endian 16-bit word; the 10/12-bit variants keep values in
700 // the low bits of the word with the top bits zero, and the 16-bit
701 // variant has all 16 bits of every word significant (full-scale is
702 // 65535), matching `Yuv420P16Le`.
703 /// 10-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
704 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
705 /// the low 10 bits of a 16-bit word.
706 Yuva420P10Le = 55,
707 /// 12-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
708 /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses
709 /// the low 12 bits of a 16-bit word.
710 Yuva420P12Le = 56,
711 /// 16-bit YUV 4:2:0 planar + full-resolution alpha, little-endian
712 /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of
713 /// each sample word are significant.
714 Yuva420P16Le = 57,
715
716 // --- 8-bit planar GBR + alpha ---
717 //
718 // Alpha-carrying companion to `Gbrp8`, filling the last hole in
719 // the planar GBR(A) family: with this variant every depth on the
720 // ladder (8/10/12/14/16) exists in both alpha-less and
721 // alpha-carrying form. Same conventions as the rest of the family:
722 // planes ordered G, B, R, A with the alpha plane at full
723 // resolution (RGB has no chroma subsampling) as plane index 3,
724 // one byte per sample, all 8 bits significant. Lossless RGB codecs
725 // whose native coding space is per-plane G, B, R carry 8-bit RGBA
726 // in exactly this shape.
727 /// 8-bit planar GBR + alpha. 4 planes ordered G, B, R, A; one
728 /// byte per sample, all 8 bits significant.
729 Gbrap8 = 58,
730
731 // --- Deep gray + alpha ---
732 //
733 // 16-bit companion to `Ya8`, ending the gray+alpha ladder at the
734 // same depth the plain gray ladder already reaches (`Gray16Le`).
735 // Still-image wire formats carry 16-bit greyscale-with-alpha
736 // natively (PNG colour type 4 at bit depth 16); without this
737 // variant that content must detour through `Rgba64Le`, tripling
738 // the gray payload and losing the "single luminance component"
739 // semantics. Same packed shape as `Ya8` — interleaved Y then A —
740 // with each sample widened to a little-endian 16-bit word, all 16
741 // bits significant (full-scale is 65535, the `Gray16Le`
742 // convention). In-between gray+alpha depths stay the job of the
743 // per-plane significant-bits side-channel.
744 /// Packed 16-bit grayscale + alpha, little-endian, 4 bytes/pixel
745 /// (Y, A). All 16 bits of each sample word are significant.
746 Ya16Le = 59,
747
748 // --- Print / prepress, inverted-ink convention ---
749 //
750 // The companion `Cmyk` (33) reserved this name when it was added:
751 // Adobe-authored 4-component scans store ink coverage inverted on
752 // the wire (0 = full ink, 255 = no ink), and decoders that want to
753 // hand the wire values through losslessly need a format that says
754 // so rather than silently re-using the regular-convention `Cmyk`.
755 /// Packed 8-bit inverted CMYK, 4 bytes/pixel in byte order C, M,
756 /// Y, K. Inverted-ink convention: C=0 means full cyan ink, C=255
757 /// means no cyan (white) — the complement of [`Cmyk`](Self::Cmyk).
758 CmykInverted = 60,
759
760 // --- 4:4:0 planar YUV (full-width, half-height chroma) ---
761 //
762 // Vertical-only chroma subsampling: each chroma plane keeps the
763 // full luma width but carries half the rows — subsampling shifts
764 // ssx = 0, ssy = 1, the transpose of 4:2:2's half-width,
765 // full-height geometry. A legal JPEG sampling combination (luma
766 // H=1, V=2) seen in real-world corpora, and a coded
767 // chroma-sampling mode of video bitstreams whose sampling flags
768 // allow horizontal and vertical decimation to be chosen
769 // independently. The depth ladder mirrors the other planar YUV
770 // samplings: 8-bit bytes, then 10/12-bit values in the low bits
771 // of little-endian 16-bit words, then full-width 16-bit words
772 // with every bit significant (full-scale is 65535).
773 /// 8-bit YUV 4:4:0, planar (Y, U, V). Chroma at full width, half
774 /// height (ssx = 0, ssy = 1).
775 Yuv440P = 61,
776 /// 10-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each
777 /// sample uses the low 10 bits of a 16-bit word.
778 Yuv440P10Le = 62,
779 /// 12-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each
780 /// sample uses the low 12 bits of a 16-bit word.
781 Yuv440P12Le = 63,
782 /// 16-bit YUV 4:4:0 planar, little-endian 16-bit storage. All 16
783 /// bits of each sample word are significant.
784 Yuv440P16Le = 64,
785
786 // --- Scene-referred 32-bit float (linear-light HDR) ---
787 //
788 // IEEE 754 binary32 components stored as little-endian 32-bit
789 // words, one word per sample. Unlike every integer format above
790 // there is no integer full-scale: samples are scene-referred
791 // linear light where 1.0 is the nominal diffuse-white anchor and
792 // values outside [0, 1] are legal (speculars above white,
793 // negative out-of-gamut excursions). Needed by HDR image wire
794 // formats whose native component type is floating point. The
795 // packed trio mirrors `Gray8`/`Rgb24`/`Rgba` component orders at
796 // float width; the planar pair extends the planar GBR(A) family
797 // beyond the integer depth ladder, with the usual G, B, R (+ A)
798 // plane order and the alpha plane at full resolution as plane
799 // index 3.
800 /// Packed 32-bit float grayscale, little-endian, 4 bytes/pixel.
801 /// Scene-referred linear light.
802 GrayF32Le = 65,
803 /// Packed 32-bit float RGB, little-endian, 12 bytes/pixel in
804 /// component order R, G, B. Scene-referred linear light.
805 RgbF32Le = 66,
806 /// Packed 32-bit float RGBA, little-endian, 16 bytes/pixel in
807 /// component order R, G, B, A. Scene-referred linear light;
808 /// alpha is straight (non-premultiplied), nominal range [0, 1].
809 RgbaF32Le = 67,
810 /// 32-bit float planar GBR, little-endian. 3 planes ordered G, B,
811 /// R; one 4-byte word per sample. Scene-referred linear light.
812 GbrpF32Le = 68,
813 /// 32-bit float planar GBR + alpha, little-endian. 4 planes
814 /// ordered G, B, R, A; one 4-byte word per sample; the alpha
815 /// plane is at full resolution as plane index 3, straight
816 /// (non-premultiplied), nominal range [0, 1].
817 GbrapF32Le = 69,
818}
819
820impl PixelFormat {
821 /// True if this format stores its components in separate planes.
822 pub fn is_planar(&self) -> bool {
823 matches!(
824 self,
825 Self::Yuv420P
826 | Self::Yuv422P
827 | Self::Yuv444P
828 | Self::Yuv411P
829 | Self::Yuv420P10Le
830 | Self::Yuv422P10Le
831 | Self::Yuv444P10Le
832 | Self::Yuv420P12Le
833 | Self::Yuv422P12Le
834 | Self::Yuv444P12Le
835 | Self::Yuv420P16Le
836 | Self::Yuv422P16Le
837 | Self::Yuv444P16Le
838 | Self::Yuv440P
839 | Self::Yuv440P10Le
840 | Self::Yuv440P12Le
841 | Self::Yuv440P16Le
842 | Self::YuvJ420P
843 | Self::YuvJ422P
844 | Self::YuvJ444P
845 | Self::Nv12
846 | Self::Nv21
847 | Self::Yuva420P
848 | Self::Yuva422P
849 | Self::Yuva444P
850 | Self::Yuva422P10Le
851 | Self::Yuva422P12Le
852 | Self::Yuva444P10Le
853 | Self::Yuva444P12Le
854 | Self::Yuva422P16Le
855 | Self::Yuva444P16Le
856 | Self::Yuva420P10Le
857 | Self::Yuva420P12Le
858 | Self::Yuva420P16Le
859 | Self::Gbrp8
860 | Self::Gbrap8
861 | Self::Gbrp10Le
862 | Self::Gbrap10Le
863 | Self::Gbrp12Le
864 | Self::Gbrap12Le
865 | Self::Gbrp14Le
866 | Self::Gbrap14Le
867 | Self::Gbrp16Le
868 | Self::Gbrap16Le
869 | Self::GbrpF32Le
870 | Self::GbrapF32Le
871 )
872 }
873
874 /// The sample range this format's *label* commits to, if any.
875 ///
876 /// Only the three legacy full-range labels (`YuvJ420P`, `YuvJ422P`,
877 /// `YuvJ444P`) carry a range in their name and return
878 /// `Some(Full)`. Every other format — limited-range-by-convention
879 /// `Yuv*`, every >8-bit and alpha-bearing Y′CbCr surface, RGB and
880 /// grey — returns `None`: their range is a property of the signal,
881 /// not of the storage layout, and lives in
882 /// [`ColorSignal::range`](crate::ColorSignal::range) on the stream
883 /// ([`CodecParameters::color_signal`](crate::CodecParameters::color_signal))
884 /// or frame ([`VideoFrame::color_signal`](crate::VideoFrame::color_signal)).
885 /// Use
886 /// [`CodecParameters::resolved_color_range`](crate::CodecParameters::resolved_color_range)
887 /// for the combined answer (explicit signal first, then this label).
888 pub fn implied_color_range(&self) -> Option<crate::signal::ColorRange> {
889 match self {
890 Self::YuvJ420P | Self::YuvJ422P | Self::YuvJ444P => {
891 Some(crate::signal::ColorRange::Full)
892 }
893 _ => None,
894 }
895 }
896
897 /// True if the format is a palette index format (`Pal8`).
898 pub fn is_palette(&self) -> bool {
899 matches!(self, Self::Pal8)
900 }
901
902 /// True if this format carries an alpha channel.
903 pub fn has_alpha(&self) -> bool {
904 matches!(
905 self,
906 Self::Rgba
907 | Self::Bgra
908 | Self::Argb
909 | Self::Abgr
910 | Self::Rgba64Le
911 | Self::Ya8
912 | Self::Ya16Le
913 | Self::Yuva420P
914 | Self::Yuva422P
915 | Self::Yuva444P
916 | Self::Yuva422P10Le
917 | Self::Yuva422P12Le
918 | Self::Yuva444P10Le
919 | Self::Yuva444P12Le
920 | Self::Yuva422P16Le
921 | Self::Yuva444P16Le
922 | Self::Yuva420P10Le
923 | Self::Yuva420P12Le
924 | Self::Yuva420P16Le
925 | Self::Gbrap8
926 | Self::Gbrap10Le
927 | Self::Gbrap12Le
928 | Self::Gbrap14Le
929 | Self::Gbrap16Le
930 | Self::RgbaF32Le
931 | Self::GbrapF32Le
932 )
933 }
934
935 /// True for the 32-bit IEEE-float variants, packed or planar.
936 /// Float formats are scene-referred: samples carry linear light
937 /// with no integer full-scale — 1.0 is the nominal diffuse-white
938 /// anchor and values outside [0, 1] are legal.
939 pub fn is_float(&self) -> bool {
940 matches!(
941 self,
942 Self::GrayF32Le | Self::RgbF32Le | Self::RgbaF32Le | Self::GbrpF32Le | Self::GbrapF32Le
943 )
944 }
945
946 /// Number of planes in the stored layout. Packed and palette formats
947 /// return 1; NV12/NV21 return 2; planar YUV without alpha and the
948 /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4.
949 pub fn plane_count(&self) -> usize {
950 match self {
951 Self::Nv12 | Self::Nv21 => 2,
952 Self::Yuv420P
953 | Self::Yuv422P
954 | Self::Yuv444P
955 | Self::Yuv411P
956 | Self::Yuv420P10Le
957 | Self::Yuv422P10Le
958 | Self::Yuv444P10Le
959 | Self::Yuv420P12Le
960 | Self::Yuv422P12Le
961 | Self::Yuv444P12Le
962 | Self::Yuv420P16Le
963 | Self::Yuv422P16Le
964 | Self::Yuv444P16Le
965 | Self::Yuv440P
966 | Self::Yuv440P10Le
967 | Self::Yuv440P12Le
968 | Self::Yuv440P16Le
969 | Self::YuvJ420P
970 | Self::YuvJ422P
971 | Self::YuvJ444P
972 | Self::Gbrp8
973 | Self::Gbrp10Le
974 | Self::Gbrp12Le
975 | Self::Gbrp14Le
976 | Self::Gbrp16Le
977 | Self::GbrpF32Le => 3,
978 Self::Yuva420P
979 | Self::Yuva422P
980 | Self::Yuva444P
981 | Self::Yuva422P10Le
982 | Self::Yuva422P12Le
983 | Self::Yuva444P10Le
984 | Self::Yuva444P12Le
985 | Self::Yuva422P16Le
986 | Self::Yuva444P16Le
987 | Self::Yuva420P10Le
988 | Self::Yuva420P12Le
989 | Self::Yuva420P16Le
990 | Self::Gbrap8
991 | Self::Gbrap10Le
992 | Self::Gbrap12Le
993 | Self::Gbrap14Le
994 | Self::Gbrap16Le
995 | Self::GbrapF32Le => 4,
996 _ => 1,
997 }
998 }
999
1000 /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
1001 /// for chroma-subsampled YUV — intended for worst-case preallocation
1002 /// rather than wire-accurate accounting.
1003 pub fn bits_per_pixel_approx(&self) -> u32 {
1004 match self {
1005 Self::MonoBlack | Self::MonoWhite => 1,
1006 Self::Gray8 | Self::Pal8 => 8,
1007 Self::Ya8 => 16,
1008 // 16-bit gray + alpha: two LE 16-bit words per pixel, all
1009 // bits significant — packed bits equal storage bits.
1010 Self::Ya16Le => 32,
1011 Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
1012 Self::Rgb24 | Self::Bgr24 => 24,
1013 Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
1014 Self::Rgb48Le => 48,
1015 Self::Rgba64Le => 64,
1016 Self::Yuyv422 | Self::Uyvy422 => 16,
1017 Self::Cmyk | Self::CmykInverted => 32,
1018 // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
1019 // 10/12-bit variants double the byte count but we report the
1020 // packed-bits-per-pixel estimate for a uniform heuristic.
1021 Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
1022 // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
1023 // full res + 2 chroma planes each subsampled by 4).
1024 Self::Yuv411P => 12,
1025 Self::Yuv422P | Self::YuvJ422P => 16,
1026 // 4:4:0 packs the same 2 samples/pixel as 4:2:2 (Y at full
1027 // res + 2 chroma planes at half height, full width).
1028 Self::Yuv440P => 16,
1029 Self::Yuv444P | Self::YuvJ444P => 24,
1030 Self::Yuv420P10Le | Self::Yuv420P12Le | Self::Yuv420P16Le => 24,
1031 Self::Yuv422P10Le | Self::Yuv422P12Le | Self::Yuv422P16Le => 32,
1032 // Deep 4:4:0 matches deep 4:2:2 — 2 sample words per pixel.
1033 Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => 32,
1034 Self::Yuv444P10Le | Self::Yuv444P12Le | Self::Yuv444P16Le => 48,
1035 Self::Yuva420P => 20,
1036 // 4:2:2 + full-res alpha: 8 (Y) + 4 (U) + 4 (V) + 8 (A).
1037 Self::Yuva422P => 24,
1038 // 4:4:4 + full-res alpha: four full-resolution 8-bit planes.
1039 Self::Yuva444P => 32,
1040 // Deep 4:2:2 + full-res alpha in 16-bit words: the estimator
1041 // reports the 16-bit-word cost like the alpha-less deep YUV
1042 // arms above — 3 sample words per pixel (Y + U/2 + V/2 + A).
1043 Self::Yuva422P10Le | Self::Yuva422P12Le | Self::Yuva422P16Le => 48,
1044 // Deep 4:4:4 + full-res alpha: 4 sample words per pixel.
1045 Self::Yuva444P10Le | Self::Yuva444P12Le | Self::Yuva444P16Le => 64,
1046 // Deep 4:2:0 + full-res alpha in 16-bit words: 16-bit-word
1047 // storage cost of the alpha-less 4:2:0 arms above (24) plus
1048 // one full-resolution 16-bit alpha word per pixel.
1049 Self::Yuva420P10Le | Self::Yuva420P12Le | Self::Yuva420P16Le => 40,
1050 // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we
1051 // report the packed bits-per-pixel density (samples × bits)
1052 // rather than the 16-bit storage cost, matching how the
1053 // 10/12-bit YUV variants are reported above.
1054 Self::Gbrp10Le => 30,
1055 Self::Gbrap10Le => 40,
1056 Self::Gbrp12Le => 36,
1057 Self::Gbrap12Le => 48,
1058 Self::Gbrp14Le => 42,
1059 Self::Gbrap14Le => 56,
1060 // Native 8-bit GBR: three bytes per pixel, like Rgb24 but
1061 // planar. 16-bit GBR(A): packed bits == storage bits (every
1062 // bit of each 16-bit word is significant), so the density
1063 // and storage numbers coincide.
1064 Self::Gbrp8 => 24,
1065 Self::Gbrp16Le => 48,
1066 Self::Gbrap16Le => 64,
1067 // 8-bit GBR + alpha: four bytes per pixel, like Rgba but
1068 // planar.
1069 Self::Gbrap8 => 32,
1070 // 32-bit float family: every sample is a full binary32
1071 // word, so packed bits equal storage bits (32 per sample;
1072 // no chroma subsampling anywhere in the family).
1073 Self::GrayF32Le => 32,
1074 Self::RgbF32Le | Self::GbrpF32Le => 96,
1075 Self::RgbaF32Le | Self::GbrapF32Le => 128,
1076 }
1077 }
1078
1079 /// Log2 chroma-subsampling shifts `(ssx, ssy)` relative to the
1080 /// luma grid, for formats that carry chroma on a subsampled (or
1081 /// potentially subsampled) grid. The chroma sample grid is the
1082 /// luma grid right-shifted by `ssx` horizontally and `ssy`
1083 /// vertically, with ceiling division for odd luma sizes (see
1084 /// [`plane_dimensions`](Self::plane_dimensions)).
1085 ///
1086 /// | sampling | `(ssx, ssy)` | chroma geometry |
1087 /// |----------|--------------|-----------------|
1088 /// | 4:2:0 | `(1, 1)` | half width, half height |
1089 /// | 4:2:2 | `(1, 0)` | half width, full height |
1090 /// | 4:4:4 | `(0, 0)` | full resolution |
1091 /// | 4:1:1 | `(2, 0)` | quarter width, full height |
1092 /// | 4:4:0 | `(0, 1)` | full width, half height |
1093 ///
1094 /// Returns `None` for formats without a distinct chroma grid
1095 /// (grayscale, RGB/GBR in any layout, palette, mono, CMYK).
1096 /// Packed 4:2:2 (`Yuyv422`/`Uyvy422`) and semi-planar 4:2:0
1097 /// (`Nv12`/`Nv21`) report their sampling even though the chroma
1098 /// samples don't live in standalone planes.
1099 ///
1100 /// ```
1101 /// use oxideav_core::PixelFormat;
1102 /// // 4:4:0: full-width, half-height chroma.
1103 /// assert_eq!(PixelFormat::Yuv440P.chroma_subsampling(), Some((0, 1)));
1104 /// // 4:2:0: subsampled on both axes.
1105 /// assert_eq!(PixelFormat::Yuv420P.chroma_subsampling(), Some((1, 1)));
1106 /// // RGB has no chroma grid.
1107 /// assert_eq!(PixelFormat::Rgba.chroma_subsampling(), None);
1108 /// ```
1109 pub fn chroma_subsampling(&self) -> Option<(u32, u32)> {
1110 match self {
1111 // 4:2:0 — half width, half height.
1112 Self::Yuv420P
1113 | Self::YuvJ420P
1114 | Self::Yuv420P10Le
1115 | Self::Yuv420P12Le
1116 | Self::Yuv420P16Le
1117 | Self::Nv12
1118 | Self::Nv21
1119 | Self::Yuva420P
1120 | Self::Yuva420P10Le
1121 | Self::Yuva420P12Le
1122 | Self::Yuva420P16Le => Some((1, 1)),
1123 // 4:2:2 — half width, full height (packed 4:2:2 included).
1124 Self::Yuv422P
1125 | Self::YuvJ422P
1126 | Self::Yuv422P10Le
1127 | Self::Yuv422P12Le
1128 | Self::Yuv422P16Le
1129 | Self::Yuva422P
1130 | Self::Yuva422P10Le
1131 | Self::Yuva422P12Le
1132 | Self::Yuva422P16Le
1133 | Self::Yuyv422
1134 | Self::Uyvy422 => Some((1, 0)),
1135 // 4:4:4 — chroma at full resolution.
1136 Self::Yuv444P
1137 | Self::YuvJ444P
1138 | Self::Yuv444P10Le
1139 | Self::Yuv444P12Le
1140 | Self::Yuv444P16Le
1141 | Self::Yuva444P
1142 | Self::Yuva444P10Le
1143 | Self::Yuva444P12Le
1144 | Self::Yuva444P16Le => Some((0, 0)),
1145 // 4:1:1 — quarter width, full height.
1146 Self::Yuv411P => Some((2, 0)),
1147 // 4:4:0 — full width, half height.
1148 Self::Yuv440P | Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => {
1149 Some((0, 1))
1150 }
1151 // Everything else has no distinct chroma grid.
1152 _ => None,
1153 }
1154 }
1155
1156 /// Sample-grid dimensions of plane `plane` for a `width` ×
1157 /// `height` picture, with ceiling division on subsampled axes so
1158 /// odd luma sizes still cover every pixel.
1159 ///
1160 /// Conventions:
1161 /// - Plane 0 (luma / the packed plane) is always `(width, height)`.
1162 /// - Chroma planes (indices 1 and 2 of planar YUV, index 1 of the
1163 /// semi-planar formats) are the luma grid right-shifted by the
1164 /// [`chroma_subsampling`](Self::chroma_subsampling) factors.
1165 /// Semi-planar chroma dimensions are in chroma *positions* —
1166 /// each position stores two interleaved samples, which
1167 /// [`plane_row_bytes`](Self::plane_row_bytes) accounts for.
1168 /// - Alpha planes (index 3) and all planar-RGB planes are at full
1169 /// resolution.
1170 /// - Packed, palette, and bit-packed mono formats report pixel
1171 /// dimensions for their single plane; per-row byte cost comes
1172 /// from [`plane_row_bytes`](Self::plane_row_bytes).
1173 ///
1174 /// Returns `None` when `plane >= plane_count()`.
1175 ///
1176 /// ```
1177 /// use oxideav_core::PixelFormat;
1178 /// // 4:4:0 chroma: full width, half height (odd height rounds up).
1179 /// assert_eq!(
1180 /// PixelFormat::Yuv440P.plane_dimensions(1, 640, 481),
1181 /// Some((640, 241))
1182 /// );
1183 /// // Alpha plane of a deep YUVA format stays at full resolution.
1184 /// assert_eq!(
1185 /// PixelFormat::Yuva420P10Le.plane_dimensions(3, 7, 5),
1186 /// Some((7, 5))
1187 /// );
1188 /// assert_eq!(PixelFormat::Rgb24.plane_dimensions(1, 8, 8), None);
1189 /// ```
1190 pub fn plane_dimensions(&self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> {
1191 if plane >= self.plane_count() {
1192 return None;
1193 }
1194 match (self.chroma_subsampling(), plane) {
1195 (Some((ssx, ssy)), 1 | 2) => {
1196 Some((width.div_ceil(1 << ssx), height.div_ceil(1 << ssy)))
1197 }
1198 _ => Some((width, height)),
1199 }
1200 }
1201
1202 /// Tightly-packed byte count of one row of plane `plane` for a
1203 /// picture `width` pixels wide — no stride padding or alignment.
1204 /// Real codecs frequently over-allocate rows for alignment; this
1205 /// is the minimum a row occupies.
1206 ///
1207 /// Returns `None` when `plane >= plane_count()` or the byte count
1208 /// overflows `usize`.
1209 pub fn plane_row_bytes(&self, plane: usize, width: u32) -> Option<usize> {
1210 let (pw, _) = self.plane_dimensions(plane, width, 1)?;
1211 let pw = pw as usize;
1212 let bytes_per_position: usize = match self {
1213 // Bit-packed mono: 8 pixels per byte, ragged tail byte.
1214 Self::MonoBlack | Self::MonoWhite => return Some(pw.div_ceil(8)),
1215 // Packed 4:2:2 macropixels: 4 bytes per 2 pixels; an odd
1216 // trailing pixel still occupies a full macropixel.
1217 Self::Yuyv422 | Self::Uyvy422 => return pw.div_ceil(2).checked_mul(4),
1218 // One byte per sample position.
1219 Self::Gray8
1220 | Self::Pal8
1221 | Self::Yuv420P
1222 | Self::Yuv422P
1223 | Self::Yuv444P
1224 | Self::Yuv411P
1225 | Self::Yuv440P
1226 | Self::YuvJ420P
1227 | Self::YuvJ422P
1228 | Self::YuvJ444P
1229 | Self::Yuva420P
1230 | Self::Yuva422P
1231 | Self::Yuva444P
1232 | Self::Gbrp8
1233 | Self::Gbrap8 => 1,
1234 // Semi-planar: one byte per luma sample on plane 0, an
1235 // interleaved two-sample pair per chroma position on
1236 // plane 1.
1237 Self::Nv12 | Self::Nv21 => {
1238 if plane == 0 {
1239 1
1240 } else {
1241 2
1242 }
1243 }
1244 // Little-endian 16-bit words (10/12/14/16-bit storage).
1245 Self::Gray10Le
1246 | Self::Gray12Le
1247 | Self::Gray16Le
1248 | Self::Yuv420P10Le
1249 | Self::Yuv422P10Le
1250 | Self::Yuv444P10Le
1251 | Self::Yuv420P12Le
1252 | Self::Yuv422P12Le
1253 | Self::Yuv444P12Le
1254 | Self::Yuv420P16Le
1255 | Self::Yuv422P16Le
1256 | Self::Yuv444P16Le
1257 | Self::Yuv440P10Le
1258 | Self::Yuv440P12Le
1259 | Self::Yuv440P16Le
1260 | Self::Yuva422P10Le
1261 | Self::Yuva422P12Le
1262 | Self::Yuva444P10Le
1263 | Self::Yuva444P12Le
1264 | Self::Yuva422P16Le
1265 | Self::Yuva444P16Le
1266 | Self::Yuva420P10Le
1267 | Self::Yuva420P12Le
1268 | Self::Yuva420P16Le
1269 | Self::Gbrp10Le
1270 | Self::Gbrap10Le
1271 | Self::Gbrp12Le
1272 | Self::Gbrap12Le
1273 | Self::Gbrp14Le
1274 | Self::Gbrap14Le
1275 | Self::Gbrp16Le
1276 | Self::Gbrap16Le => 2,
1277 // Packed multi-component: whole-pixel byte cost.
1278 Self::Ya8 => 2,
1279 Self::Rgb24 | Self::Bgr24 => 3,
1280 Self::Rgba
1281 | Self::Bgra
1282 | Self::Argb
1283 | Self::Abgr
1284 | Self::Cmyk
1285 | Self::CmykInverted
1286 | Self::Ya16Le => 4,
1287 Self::Rgb48Le => 6,
1288 Self::Rgba64Le => 8,
1289 // 32-bit float: one binary32 word per sample (packed
1290 // grayscale and the planar GBR(A) planes), or the
1291 // whole-pixel cost for packed multi-component float.
1292 Self::GrayF32Le | Self::GbrpF32Le | Self::GbrapF32Le => 4,
1293 Self::RgbF32Le => 12,
1294 Self::RgbaF32Le => 16,
1295 };
1296 pw.checked_mul(bytes_per_position)
1297 }
1298
1299 /// Tightly-packed byte size of plane `plane` for a `width` ×
1300 /// `height` picture:
1301 /// [`plane_row_bytes`](Self::plane_row_bytes) × the plane's row
1302 /// count from [`plane_dimensions`](Self::plane_dimensions).
1303 ///
1304 /// Returns `None` when `plane >= plane_count()` or the size
1305 /// overflows `usize`.
1306 pub fn plane_size_bytes(&self, plane: usize, width: u32, height: u32) -> Option<usize> {
1307 let (_, ph) = self.plane_dimensions(plane, width, height)?;
1308 self.plane_row_bytes(plane, width)?.checked_mul(ph as usize)
1309 }
1310
1311 /// Tightly-packed byte size of a whole `width` × `height` frame in
1312 /// this format — the sum of
1313 /// [`plane_size_bytes`](Self::plane_size_bytes) over every plane,
1314 /// with no stride padding or inter-plane alignment. Out-of-band
1315 /// side data (the `Pal8` palette table, significant-bits records)
1316 /// is not included.
1317 ///
1318 /// Returns `None` on `usize` overflow.
1319 ///
1320 /// ```
1321 /// use oxideav_core::PixelFormat;
1322 /// // 4:2:0 at 4×4: 16 luma + 4 + 4 chroma bytes.
1323 /// assert_eq!(PixelFormat::Yuv420P.frame_size_bytes(4, 4), Some(24));
1324 /// // 4:4:0 at 6×5: 30 luma + 2 × (6 × 3) chroma bytes.
1325 /// assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
1326 /// // Packed float RGBA: 16 bytes per pixel.
1327 /// assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
1328 /// ```
1329 pub fn frame_size_bytes(&self, width: u32, height: u32) -> Option<usize> {
1330 let mut total = 0usize;
1331 for plane in 0..self.plane_count() {
1332 total = total.checked_add(self.plane_size_bytes(plane, width, height)?)?;
1333 }
1334 Some(total)
1335 }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340 use super::*;
1341
1342 /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
1343 /// stability commitment — the integer value of each variant is part of
1344 /// the public ABI. Any reorder, renumber, or removal will fail this test
1345 /// and the change MUST be a major version bump (or a fresh variant
1346 /// appended at a new number, leaving the existing ones untouched).
1347 #[test]
1348 fn implied_color_range_is_full_only_for_the_j_labels() {
1349 use crate::signal::ColorRange;
1350 for f in [
1351 PixelFormat::YuvJ420P,
1352 PixelFormat::YuvJ422P,
1353 PixelFormat::YuvJ444P,
1354 ] {
1355 assert_eq!(f.implied_color_range(), Some(ColorRange::Full), "{f:?}");
1356 }
1357 for f in [
1358 PixelFormat::Yuv420P,
1359 PixelFormat::Yuv444P,
1360 PixelFormat::Yuv420P10Le,
1361 PixelFormat::Yuv444P12Le,
1362 PixelFormat::Rgb24,
1363 PixelFormat::Rgba,
1364 PixelFormat::Gray8,
1365 PixelFormat::Gray16Le,
1366 PixelFormat::Nv12,
1367 PixelFormat::Pal8,
1368 ] {
1369 assert_eq!(f.implied_color_range(), None, "{f:?}");
1370 }
1371 }
1372
1373 #[test]
1374 fn pixel_format_discriminants_pinned() {
1375 assert_eq!(PixelFormat::Yuv420P as u16, 0);
1376 assert_eq!(PixelFormat::Yuv422P as u16, 1);
1377 assert_eq!(PixelFormat::Yuv444P as u16, 2);
1378 assert_eq!(PixelFormat::Rgb24 as u16, 3);
1379 assert_eq!(PixelFormat::Rgba as u16, 4);
1380 assert_eq!(PixelFormat::Gray8 as u16, 5);
1381 assert_eq!(PixelFormat::Pal8 as u16, 6);
1382 assert_eq!(PixelFormat::Bgr24 as u16, 7);
1383 assert_eq!(PixelFormat::Bgra as u16, 8);
1384 assert_eq!(PixelFormat::Argb as u16, 9);
1385 assert_eq!(PixelFormat::Abgr as u16, 10);
1386 assert_eq!(PixelFormat::Rgb48Le as u16, 11);
1387 assert_eq!(PixelFormat::Rgba64Le as u16, 12);
1388 assert_eq!(PixelFormat::Gray16Le as u16, 13);
1389 assert_eq!(PixelFormat::Gray10Le as u16, 14);
1390 assert_eq!(PixelFormat::Gray12Le as u16, 15);
1391 assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
1392 assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
1393 assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
1394 assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
1395 assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
1396 assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
1397 assert_eq!(PixelFormat::YuvJ420P as u16, 22);
1398 assert_eq!(PixelFormat::YuvJ422P as u16, 23);
1399 assert_eq!(PixelFormat::YuvJ444P as u16, 24);
1400 assert_eq!(PixelFormat::Nv12 as u16, 25);
1401 assert_eq!(PixelFormat::Nv21 as u16, 26);
1402 assert_eq!(PixelFormat::Ya8 as u16, 27);
1403 assert_eq!(PixelFormat::Yuva420P as u16, 28);
1404 assert_eq!(PixelFormat::MonoBlack as u16, 29);
1405 assert_eq!(PixelFormat::MonoWhite as u16, 30);
1406 assert_eq!(PixelFormat::Yuyv422 as u16, 31);
1407 assert_eq!(PixelFormat::Uyvy422 as u16, 32);
1408 assert_eq!(PixelFormat::Cmyk as u16, 33);
1409 assert_eq!(PixelFormat::Yuv411P as u16, 34);
1410 assert_eq!(PixelFormat::Gbrp10Le as u16, 35);
1411 assert_eq!(PixelFormat::Gbrap10Le as u16, 36);
1412 assert_eq!(PixelFormat::Gbrp12Le as u16, 37);
1413 assert_eq!(PixelFormat::Gbrap12Le as u16, 38);
1414 assert_eq!(PixelFormat::Gbrp14Le as u16, 39);
1415 assert_eq!(PixelFormat::Gbrap14Le as u16, 40);
1416 assert_eq!(PixelFormat::Yuv420P16Le as u16, 41);
1417 assert_eq!(PixelFormat::Yuv422P16Le as u16, 42);
1418 assert_eq!(PixelFormat::Yuv444P16Le as u16, 43);
1419 assert_eq!(PixelFormat::Yuva422P as u16, 44);
1420 assert_eq!(PixelFormat::Yuva444P as u16, 45);
1421 assert_eq!(PixelFormat::Yuva422P10Le as u16, 46);
1422 assert_eq!(PixelFormat::Yuva422P12Le as u16, 47);
1423 assert_eq!(PixelFormat::Yuva444P10Le as u16, 48);
1424 assert_eq!(PixelFormat::Yuva444P12Le as u16, 49);
1425 assert_eq!(PixelFormat::Yuva422P16Le as u16, 50);
1426 assert_eq!(PixelFormat::Yuva444P16Le as u16, 51);
1427 assert_eq!(PixelFormat::Gbrp8 as u16, 52);
1428 assert_eq!(PixelFormat::Gbrp16Le as u16, 53);
1429 assert_eq!(PixelFormat::Gbrap16Le as u16, 54);
1430 assert_eq!(PixelFormat::Yuva420P10Le as u16, 55);
1431 assert_eq!(PixelFormat::Yuva420P12Le as u16, 56);
1432 assert_eq!(PixelFormat::Yuva420P16Le as u16, 57);
1433 assert_eq!(PixelFormat::Gbrap8 as u16, 58);
1434 assert_eq!(PixelFormat::Ya16Le as u16, 59);
1435 assert_eq!(PixelFormat::CmykInverted as u16, 60);
1436 assert_eq!(PixelFormat::Yuv440P as u16, 61);
1437 assert_eq!(PixelFormat::Yuv440P10Le as u16, 62);
1438 assert_eq!(PixelFormat::Yuv440P12Le as u16, 63);
1439 assert_eq!(PixelFormat::Yuv440P16Le as u16, 64);
1440 assert_eq!(PixelFormat::GrayF32Le as u16, 65);
1441 assert_eq!(PixelFormat::RgbF32Le as u16, 66);
1442 assert_eq!(PixelFormat::RgbaF32Le as u16, 67);
1443 assert_eq!(PixelFormat::GbrpF32Le as u16, 68);
1444 assert_eq!(PixelFormat::GbrapF32Le as u16, 69);
1445 }
1446
1447 #[test]
1448 fn sample_format_discriminants_pinned() {
1449 assert_eq!(SampleFormat::U8 as u8, 0);
1450 assert_eq!(SampleFormat::S8 as u8, 1);
1451 assert_eq!(SampleFormat::S16 as u8, 2);
1452 assert_eq!(SampleFormat::S24 as u8, 3);
1453 assert_eq!(SampleFormat::S32 as u8, 4);
1454 assert_eq!(SampleFormat::F32 as u8, 5);
1455 assert_eq!(SampleFormat::F64 as u8, 6);
1456 assert_eq!(SampleFormat::U8P as u8, 7);
1457 assert_eq!(SampleFormat::S16P as u8, 8);
1458 assert_eq!(SampleFormat::S32P as u8, 9);
1459 assert_eq!(SampleFormat::F32P as u8, 10);
1460 assert_eq!(SampleFormat::F64P as u8, 11);
1461 }
1462
1463 #[test]
1464 fn high_bit_yuv_planar_metadata() {
1465 // 10-bit reference variants are planar with three planes.
1466 assert!(PixelFormat::Yuv420P10Le.is_planar());
1467 assert!(PixelFormat::Yuv422P10Le.is_planar());
1468 assert!(PixelFormat::Yuv444P10Le.is_planar());
1469
1470 // 12-bit variants must follow the same shape.
1471 assert!(PixelFormat::Yuv420P12Le.is_planar());
1472 assert!(PixelFormat::Yuv422P12Le.is_planar());
1473 assert!(PixelFormat::Yuv444P12Le.is_planar());
1474
1475 assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
1476 assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
1477 assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
1478
1479 // 16-bit variants must follow the same shape.
1480 for fmt in [
1481 PixelFormat::Yuv420P16Le,
1482 PixelFormat::Yuv422P16Le,
1483 PixelFormat::Yuv444P16Le,
1484 ] {
1485 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1486 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1487 }
1488
1489 // None of the high-bit YUV variants carry alpha or palette.
1490 assert!(!PixelFormat::Yuv422P12Le.has_alpha());
1491 assert!(!PixelFormat::Yuv444P12Le.has_alpha());
1492 assert!(!PixelFormat::Yuv422P12Le.is_palette());
1493 assert!(!PixelFormat::Yuv444P12Le.is_palette());
1494 assert!(!PixelFormat::Yuv420P16Le.has_alpha());
1495 assert!(!PixelFormat::Yuv422P16Le.has_alpha());
1496 assert!(!PixelFormat::Yuv444P16Le.has_alpha());
1497 assert!(!PixelFormat::Yuv420P16Le.is_palette());
1498 assert!(!PixelFormat::Yuv422P16Le.is_palette());
1499 assert!(!PixelFormat::Yuv444P16Le.is_palette());
1500 }
1501
1502 #[test]
1503 fn channel_layout_round_trip_count_for_known_layouts() {
1504 // For every `n` that `from_count` maps to a named layout, the
1505 // resulting layout's `channel_count()` must equal `n` again.
1506 for n in 1..=8u16 {
1507 let layout = ChannelLayout::from_count(n);
1508 assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
1509 // None of these defaults should fall through to DiscreteN.
1510 assert!(
1511 !matches!(layout, ChannelLayout::DiscreteN(_)),
1512 "from_count({n}) unexpectedly produced DiscreteN"
1513 );
1514 }
1515 }
1516
1517 #[test]
1518 fn channel_layout_from_count_default_table() {
1519 // The exact mapping documented on `from_count` — pin it so
1520 // future refactors don't silently change the inferred layout.
1521 assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
1522 assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
1523 assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
1524 assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
1525 assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
1526 assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
1527 assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
1528 assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
1529 }
1530
1531 #[test]
1532 fn channel_layout_unknown_count_falls_through_to_discrete() {
1533 assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
1534 assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
1535 assert_eq!(
1536 ChannelLayout::from_count(64).channel_count(),
1537 64,
1538 "DiscreteN must report the count it was constructed with"
1539 );
1540 }
1541
1542 #[test]
1543 fn channel_layout_position_lookup() {
1544 assert_eq!(
1545 ChannelLayout::Stereo.position(0),
1546 Some(ChannelPosition::FrontLeft)
1547 );
1548 assert_eq!(
1549 ChannelLayout::Stereo.position(1),
1550 Some(ChannelPosition::FrontRight)
1551 );
1552 assert_eq!(ChannelLayout::Stereo.position(2), None);
1553
1554 // 5.1 canonical: L, R, C, LFE, Ls, Rs.
1555 let s51 = ChannelLayout::Surround51;
1556 assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
1557 assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
1558 assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
1559 assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
1560 assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
1561 assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
1562 assert_eq!(s51.position(6), None);
1563
1564 // DiscreteN never reveals a position.
1565 assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
1566 }
1567
1568 #[test]
1569 fn channel_layout_lfe_and_surround_predicates() {
1570 assert!(ChannelLayout::Surround51.has_lfe());
1571 assert!(ChannelLayout::Surround71.has_lfe());
1572 assert!(ChannelLayout::Stereo21.has_lfe());
1573 assert!(!ChannelLayout::Quad.has_lfe());
1574 assert!(!ChannelLayout::Surround50.has_lfe());
1575 assert!(!ChannelLayout::Stereo.has_lfe());
1576
1577 assert!(!ChannelLayout::Mono.is_surround());
1578 assert!(!ChannelLayout::Stereo.is_surround());
1579 // Downmix carriers are still 2ch / no-LFE → not "surround" by
1580 // the layout-shape definition; the surround info lives in the
1581 // sample matrix itself.
1582 assert!(!ChannelLayout::LoRo.is_surround());
1583 assert!(!ChannelLayout::LtRt.is_surround());
1584 assert!(ChannelLayout::Stereo21.is_surround());
1585 assert!(ChannelLayout::Surround51.is_surround());
1586 assert!(ChannelLayout::Surround71.is_surround());
1587 }
1588
1589 #[test]
1590 fn channel_layout_display_and_fromstr_round_trip() {
1591 use std::str::FromStr;
1592 let cases = [
1593 ChannelLayout::Mono,
1594 ChannelLayout::Stereo,
1595 ChannelLayout::Stereo21,
1596 ChannelLayout::Surround30,
1597 ChannelLayout::Quad,
1598 ChannelLayout::Surround40,
1599 ChannelLayout::Surround41,
1600 ChannelLayout::Surround50,
1601 ChannelLayout::Surround51,
1602 ChannelLayout::Surround60,
1603 ChannelLayout::Surround61,
1604 ChannelLayout::Surround70,
1605 ChannelLayout::Surround71,
1606 ChannelLayout::LoRo,
1607 ChannelLayout::LtRt,
1608 ChannelLayout::DiscreteN(13),
1609 ];
1610 for layout in cases {
1611 let s = layout.to_string();
1612 let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
1613 assert_eq!(parsed, layout, "round-trip failed via {s:?}");
1614 }
1615 }
1616
1617 #[test]
1618 fn channel_layout_fromstr_accepts_aliases_and_case() {
1619 use std::str::FromStr;
1620 assert_eq!(
1621 ChannelLayout::from_str("STEREO").unwrap(),
1622 ChannelLayout::Stereo
1623 );
1624 assert_eq!(
1625 ChannelLayout::from_str("2.0").unwrap(),
1626 ChannelLayout::Stereo
1627 );
1628 assert_eq!(
1629 ChannelLayout::from_str("5.1").unwrap(),
1630 ChannelLayout::Surround51
1631 );
1632 assert_eq!(
1633 ChannelLayout::from_str("Lo/Ro").unwrap(),
1634 ChannelLayout::LoRo
1635 );
1636 assert_eq!(
1637 ChannelLayout::from_str("lt/rt").unwrap(),
1638 ChannelLayout::LtRt
1639 );
1640 assert!(ChannelLayout::from_str("absurd_layout").is_err());
1641 }
1642
1643 #[test]
1644 fn channel_layout_positions_owned_matches_static_slice() {
1645 for layout in [
1646 ChannelLayout::Mono,
1647 ChannelLayout::Surround51,
1648 ChannelLayout::Surround71,
1649 ] {
1650 assert_eq!(layout.positions_owned(), layout.positions());
1651 }
1652 // DiscreteN returns an empty owned vec — positions are unknown.
1653 assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
1654 }
1655
1656 #[test]
1657 fn sample_format_plane_count_interleaved_is_one() {
1658 // Interleaved formats always pack into a single plane, regardless
1659 // of channel count.
1660 for ch in [1u16, 2, 6, 8, 64, 0] {
1661 assert_eq!(SampleFormat::S16.plane_count(ch), 1);
1662 assert_eq!(SampleFormat::F32.plane_count(ch), 1);
1663 assert_eq!(SampleFormat::U8.plane_count(ch), 1);
1664 assert_eq!(SampleFormat::S24.plane_count(ch), 1);
1665 }
1666 }
1667
1668 #[test]
1669 fn sample_format_plane_count_planar_matches_channels() {
1670 // Planar formats use one plane per channel.
1671 assert_eq!(SampleFormat::S16P.plane_count(1), 1);
1672 assert_eq!(SampleFormat::S16P.plane_count(2), 2);
1673 assert_eq!(SampleFormat::F32P.plane_count(6), 6);
1674 assert_eq!(SampleFormat::F64P.plane_count(8), 8);
1675
1676 // Edge case: zero channels in a planar format yields zero planes.
1677 assert_eq!(SampleFormat::S32P.plane_count(0), 0);
1678 }
1679
1680 #[test]
1681 fn high_bit_yuv_bits_per_pixel_approx() {
1682 // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
1683 // packed-bits estimator (the approximation reports samples-per-pixel
1684 // density, not the 16-bit storage width).
1685 assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
1686 assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
1687 assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
1688 assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
1689 assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
1690
1691 // 16-bit: packed bits == storage bits (every bit of the 16-bit
1692 // word is significant), so the estimator lands on the same
1693 // numbers as the 10/12-bit siblings.
1694 assert_eq!(PixelFormat::Yuv420P16Le.bits_per_pixel_approx(), 24);
1695 assert_eq!(PixelFormat::Yuv422P16Le.bits_per_pixel_approx(), 32);
1696 assert_eq!(PixelFormat::Yuv444P16Le.bits_per_pixel_approx(), 48);
1697 }
1698
1699 #[test]
1700 fn yuva_planar_metadata() {
1701 // All three alpha-carrying planar YUV samplings share one shape:
1702 // planar, 4 planes (Y, U, V, full-resolution A), alpha set, not
1703 // a palette format.
1704 for fmt in [
1705 PixelFormat::Yuva420P,
1706 PixelFormat::Yuva422P,
1707 PixelFormat::Yuva444P,
1708 ] {
1709 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1710 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1711 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1712 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1713 }
1714
1715 // Packed-bits estimator: the alpha plane adds a full 8 bits per
1716 // pixel on top of the alpha-less sampling's density.
1717 assert_eq!(
1718 PixelFormat::Yuva420P.bits_per_pixel_approx(),
1719 PixelFormat::Yuv420P.bits_per_pixel_approx() + 8
1720 );
1721 assert_eq!(
1722 PixelFormat::Yuva422P.bits_per_pixel_approx(),
1723 PixelFormat::Yuv422P.bits_per_pixel_approx() + 8
1724 );
1725 assert_eq!(
1726 PixelFormat::Yuva444P.bits_per_pixel_approx(),
1727 PixelFormat::Yuv444P.bits_per_pixel_approx() + 8
1728 );
1729 assert_eq!(PixelFormat::Yuva422P.bits_per_pixel_approx(), 24);
1730 assert_eq!(PixelFormat::Yuva444P.bits_per_pixel_approx(), 32);
1731 }
1732
1733 #[test]
1734 fn deep_yuva_planar_metadata() {
1735 // All six deep alpha-carrying variants share one shape: planar,
1736 // 4 planes (Y, U, V, full-resolution A), alpha set, no palette.
1737 for fmt in [
1738 PixelFormat::Yuva422P10Le,
1739 PixelFormat::Yuva422P12Le,
1740 PixelFormat::Yuva444P10Le,
1741 PixelFormat::Yuva444P12Le,
1742 PixelFormat::Yuva422P16Le,
1743 PixelFormat::Yuva444P16Le,
1744 ] {
1745 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1746 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1747 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1748 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1749 }
1750 }
1751
1752 #[test]
1753 fn deep_yuva_bits_per_pixel_approx() {
1754 // Estimator reports 16-bit-word storage cost, matching the
1755 // alpha-less deep YUV trio: the full-resolution alpha word adds
1756 // 16 on top of the alpha-less sampling's number.
1757 for fmt in [
1758 PixelFormat::Yuva422P10Le,
1759 PixelFormat::Yuva422P12Le,
1760 PixelFormat::Yuva422P16Le,
1761 ] {
1762 assert_eq!(fmt.bits_per_pixel_approx(), 48, "{fmt:?}");
1763 }
1764 for fmt in [
1765 PixelFormat::Yuva444P10Le,
1766 PixelFormat::Yuva444P12Le,
1767 PixelFormat::Yuva444P16Le,
1768 ] {
1769 assert_eq!(fmt.bits_per_pixel_approx(), 64, "{fmt:?}");
1770 }
1771 assert_eq!(
1772 PixelFormat::Yuva422P16Le.bits_per_pixel_approx(),
1773 PixelFormat::Yuv422P16Le.bits_per_pixel_approx() + 16
1774 );
1775 assert_eq!(
1776 PixelFormat::Yuva444P16Le.bits_per_pixel_approx(),
1777 PixelFormat::Yuv444P16Le.bits_per_pixel_approx() + 16
1778 );
1779 assert_eq!(
1780 PixelFormat::Yuva422P10Le.bits_per_pixel_approx(),
1781 PixelFormat::Yuv422P10Le.bits_per_pixel_approx() + 16
1782 );
1783 assert_eq!(
1784 PixelFormat::Yuva444P12Le.bits_per_pixel_approx(),
1785 PixelFormat::Yuv444P12Le.bits_per_pixel_approx() + 16
1786 );
1787 }
1788
1789 #[test]
1790 fn high_bit_gbr_planar_metadata() {
1791 // All six new variants are planar with the right plane count.
1792 for fmt in [
1793 PixelFormat::Gbrp10Le,
1794 PixelFormat::Gbrp12Le,
1795 PixelFormat::Gbrp14Le,
1796 ] {
1797 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1798 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1799 assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1800 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1801 }
1802 for fmt in [
1803 PixelFormat::Gbrap10Le,
1804 PixelFormat::Gbrap12Le,
1805 PixelFormat::Gbrap14Le,
1806 ] {
1807 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1808 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1809 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1810 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1811 }
1812 }
1813
1814 #[test]
1815 fn high_bit_gbr_bits_per_pixel_approx() {
1816 // Packed bits-per-pixel = samples × bits (consistent with how
1817 // the 10/12-bit YUV variants are reported above).
1818 assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30);
1819 assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40);
1820 assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36);
1821 assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48);
1822 assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42);
1823 assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56);
1824 }
1825
1826 #[test]
1827 fn high_bit_gbr_constructible_and_distinct() {
1828 // Round-trip the discriminant through `as u16` and back via the
1829 // pinning test's reverse mapping — every variant must be unique.
1830 let all = [
1831 PixelFormat::Gbrp10Le,
1832 PixelFormat::Gbrap10Le,
1833 PixelFormat::Gbrp12Le,
1834 PixelFormat::Gbrap12Le,
1835 PixelFormat::Gbrp14Le,
1836 PixelFormat::Gbrap14Le,
1837 PixelFormat::Gbrp8,
1838 PixelFormat::Gbrap8,
1839 PixelFormat::Gbrp16Le,
1840 PixelFormat::Gbrap16Le,
1841 ];
1842 let mut seen = std::collections::HashSet::new();
1843 for fmt in all {
1844 assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
1845 }
1846 }
1847
1848 #[test]
1849 fn gbr_depth_ladder_ends_metadata() {
1850 // Gbrp8 and the 16-bit pair share the family shape: planar,
1851 // G/B/R plane order (3 planes), alpha only on Gbrap16Le, never
1852 // palette.
1853 for fmt in [PixelFormat::Gbrp8, PixelFormat::Gbrp16Le] {
1854 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1855 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
1856 assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
1857 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1858 }
1859 assert!(PixelFormat::Gbrap16Le.is_planar());
1860 assert_eq!(PixelFormat::Gbrap16Le.plane_count(), 4);
1861 assert!(PixelFormat::Gbrap16Le.has_alpha());
1862 assert!(!PixelFormat::Gbrap16Le.is_palette());
1863 }
1864
1865 #[test]
1866 fn gbr_depth_ladder_ends_bits_per_pixel_approx() {
1867 // Gbrp8 matches the packed 8-bit RGB density (planar layout
1868 // doesn't change bits-per-pixel), and the 16-bit pair matches
1869 // the packed 16-bit RGB(A) densities — for 16-bit words packed
1870 // bits equal storage bits.
1871 assert_eq!(
1872 PixelFormat::Gbrp8.bits_per_pixel_approx(),
1873 PixelFormat::Rgb24.bits_per_pixel_approx()
1874 );
1875 assert_eq!(
1876 PixelFormat::Gbrp16Le.bits_per_pixel_approx(),
1877 PixelFormat::Rgb48Le.bits_per_pixel_approx()
1878 );
1879 assert_eq!(
1880 PixelFormat::Gbrap16Le.bits_per_pixel_approx(),
1881 PixelFormat::Rgba64Le.bits_per_pixel_approx()
1882 );
1883 assert_eq!(PixelFormat::Gbrp8.bits_per_pixel_approx(), 24);
1884 assert_eq!(PixelFormat::Gbrp16Le.bits_per_pixel_approx(), 48);
1885 assert_eq!(PixelFormat::Gbrap16Le.bits_per_pixel_approx(), 64);
1886 }
1887
1888 #[test]
1889 fn gbrap8_metadata() {
1890 // Gbrap8 completes the GBR(A) family: every depth on the
1891 // 8/10/12/14/16 ladder now has both an alpha-less and an
1892 // alpha-carrying variant. Shape matches the rest of the
1893 // alpha-carrying family: planar, 4 planes (G, B, R,
1894 // full-resolution A), alpha set, never palette.
1895 let fmt = PixelFormat::Gbrap8;
1896 assert!(fmt.is_planar());
1897 assert_eq!(fmt.plane_count(), 4);
1898 assert!(fmt.has_alpha());
1899 assert!(!fmt.is_palette());
1900 }
1901
1902 #[test]
1903 fn gbrap8_bits_per_pixel_approx() {
1904 // Four bytes per pixel: the packed Rgba density (planar layout
1905 // doesn't change bits-per-pixel), i.e. the alpha plane adds a
1906 // full 8 bits on top of Gbrp8.
1907 assert_eq!(PixelFormat::Gbrap8.bits_per_pixel_approx(), 32);
1908 assert_eq!(
1909 PixelFormat::Gbrap8.bits_per_pixel_approx(),
1910 PixelFormat::Rgba.bits_per_pixel_approx()
1911 );
1912 assert_eq!(
1913 PixelFormat::Gbrap8.bits_per_pixel_approx(),
1914 PixelFormat::Gbrp8.bits_per_pixel_approx() + 8
1915 );
1916 }
1917
1918 #[test]
1919 fn gbr_family_alpha_ladder_complete() {
1920 // Every GBR depth has an alpha companion with exactly one more
1921 // plane and the same planarity — the asymmetry Gbrap8 closed.
1922 let pairs = [
1923 (PixelFormat::Gbrp8, PixelFormat::Gbrap8),
1924 (PixelFormat::Gbrp10Le, PixelFormat::Gbrap10Le),
1925 (PixelFormat::Gbrp12Le, PixelFormat::Gbrap12Le),
1926 (PixelFormat::Gbrp14Le, PixelFormat::Gbrap14Le),
1927 (PixelFormat::Gbrp16Le, PixelFormat::Gbrap16Le),
1928 ];
1929 for (gbr, gbra) in pairs {
1930 assert!(gbr.is_planar() && gbra.is_planar());
1931 assert_eq!(gbr.plane_count(), 3, "{gbr:?}");
1932 assert_eq!(gbra.plane_count(), 4, "{gbra:?}");
1933 assert!(!gbr.has_alpha(), "{gbr:?}");
1934 assert!(gbra.has_alpha(), "{gbra:?}");
1935 }
1936 }
1937
1938 #[test]
1939 fn ya16le_metadata() {
1940 // Same packed shape as Ya8 (interleaved Y, A in one plane),
1941 // widened to 16-bit LE words: not planar, single plane, alpha
1942 // set, never palette. Density is exactly double Ya8's and
1943 // matches half of Rgba64Le (two components instead of four).
1944 let fmt = PixelFormat::Ya16Le;
1945 assert!(!fmt.is_planar());
1946 assert_eq!(fmt.plane_count(), 1);
1947 assert!(fmt.has_alpha());
1948 assert!(!fmt.is_palette());
1949 assert_eq!(fmt.bits_per_pixel_approx(), 32);
1950 assert_eq!(
1951 fmt.bits_per_pixel_approx(),
1952 PixelFormat::Ya8.bits_per_pixel_approx() * 2
1953 );
1954 assert_eq!(
1955 fmt.bits_per_pixel_approx(),
1956 PixelFormat::Rgba64Le.bits_per_pixel_approx() / 2
1957 );
1958 // The alpha word adds a full 16 bits on top of Gray16Le.
1959 assert_eq!(
1960 fmt.bits_per_pixel_approx(),
1961 PixelFormat::Gray16Le.bits_per_pixel_approx() + 16
1962 );
1963 }
1964
1965 #[test]
1966 fn cmyk_inverted_metadata() {
1967 // The inverted-ink convention changes sample semantics, not
1968 // layout: CmykInverted must be metadata-identical to Cmyk on
1969 // every shape predicate.
1970 let (reg, inv) = (PixelFormat::Cmyk, PixelFormat::CmykInverted);
1971 for fmt in [reg, inv] {
1972 assert!(!fmt.is_planar(), "{fmt:?}");
1973 assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
1974 assert!(!fmt.has_alpha(), "{fmt:?}");
1975 assert!(!fmt.is_palette(), "{fmt:?}");
1976 }
1977 assert_eq!(reg.bits_per_pixel_approx(), inv.bits_per_pixel_approx());
1978 assert_eq!(inv.bits_per_pixel_approx(), 32);
1979 // They remain distinct formats on the wire-stable axis.
1980 assert_ne!(reg as u16, inv as u16);
1981 }
1982
1983 #[test]
1984 fn deep_yuva420_planar_metadata() {
1985 // The 4:2:0 completions share the deep-Yuva shape: planar, 4
1986 // planes (Y, U, V, full-resolution A), alpha set, no palette.
1987 for fmt in [
1988 PixelFormat::Yuva420P10Le,
1989 PixelFormat::Yuva420P12Le,
1990 PixelFormat::Yuva420P16Le,
1991 ] {
1992 assert!(fmt.is_planar(), "{fmt:?} must be planar");
1993 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1994 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1995 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1996 }
1997 }
1998
1999 #[test]
2000 fn deep_yuva420_bits_per_pixel_approx() {
2001 // Same estimator convention as the 4:2:2/4:4:4 deep Yuva arms:
2002 // 16-bit-word storage cost, with the full-resolution alpha word
2003 // adding 16 on top of the alpha-less sampling's number.
2004 for fmt in [
2005 PixelFormat::Yuva420P10Le,
2006 PixelFormat::Yuva420P12Le,
2007 PixelFormat::Yuva420P16Le,
2008 ] {
2009 assert_eq!(fmt.bits_per_pixel_approx(), 40, "{fmt:?}");
2010 }
2011 assert_eq!(
2012 PixelFormat::Yuva420P10Le.bits_per_pixel_approx(),
2013 PixelFormat::Yuv420P10Le.bits_per_pixel_approx() + 16
2014 );
2015 assert_eq!(
2016 PixelFormat::Yuva420P12Le.bits_per_pixel_approx(),
2017 PixelFormat::Yuv420P12Le.bits_per_pixel_approx() + 16
2018 );
2019 assert_eq!(
2020 PixelFormat::Yuva420P16Le.bits_per_pixel_approx(),
2021 PixelFormat::Yuv420P16Le.bits_per_pixel_approx() + 16
2022 );
2023 }
2024
2025 /// Every `PixelFormat` variant, in discriminant order. Extend this
2026 /// list whenever a variant is appended — the consistency tests
2027 /// below sweep it.
2028 const ALL_PIXEL_FORMATS: [PixelFormat; 70] = [
2029 PixelFormat::Yuv420P,
2030 PixelFormat::Yuv422P,
2031 PixelFormat::Yuv444P,
2032 PixelFormat::Rgb24,
2033 PixelFormat::Rgba,
2034 PixelFormat::Gray8,
2035 PixelFormat::Pal8,
2036 PixelFormat::Bgr24,
2037 PixelFormat::Bgra,
2038 PixelFormat::Argb,
2039 PixelFormat::Abgr,
2040 PixelFormat::Rgb48Le,
2041 PixelFormat::Rgba64Le,
2042 PixelFormat::Gray16Le,
2043 PixelFormat::Gray10Le,
2044 PixelFormat::Gray12Le,
2045 PixelFormat::Yuv420P10Le,
2046 PixelFormat::Yuv422P10Le,
2047 PixelFormat::Yuv444P10Le,
2048 PixelFormat::Yuv420P12Le,
2049 PixelFormat::Yuv422P12Le,
2050 PixelFormat::Yuv444P12Le,
2051 PixelFormat::YuvJ420P,
2052 PixelFormat::YuvJ422P,
2053 PixelFormat::YuvJ444P,
2054 PixelFormat::Nv12,
2055 PixelFormat::Nv21,
2056 PixelFormat::Ya8,
2057 PixelFormat::Yuva420P,
2058 PixelFormat::MonoBlack,
2059 PixelFormat::MonoWhite,
2060 PixelFormat::Yuyv422,
2061 PixelFormat::Uyvy422,
2062 PixelFormat::Cmyk,
2063 PixelFormat::Yuv411P,
2064 PixelFormat::Gbrp10Le,
2065 PixelFormat::Gbrap10Le,
2066 PixelFormat::Gbrp12Le,
2067 PixelFormat::Gbrap12Le,
2068 PixelFormat::Gbrp14Le,
2069 PixelFormat::Gbrap14Le,
2070 PixelFormat::Yuv420P16Le,
2071 PixelFormat::Yuv422P16Le,
2072 PixelFormat::Yuv444P16Le,
2073 PixelFormat::Yuva422P,
2074 PixelFormat::Yuva444P,
2075 PixelFormat::Yuva422P10Le,
2076 PixelFormat::Yuva422P12Le,
2077 PixelFormat::Yuva444P10Le,
2078 PixelFormat::Yuva444P12Le,
2079 PixelFormat::Yuva422P16Le,
2080 PixelFormat::Yuva444P16Le,
2081 PixelFormat::Gbrp8,
2082 PixelFormat::Gbrp16Le,
2083 PixelFormat::Gbrap16Le,
2084 PixelFormat::Yuva420P10Le,
2085 PixelFormat::Yuva420P12Le,
2086 PixelFormat::Yuva420P16Le,
2087 PixelFormat::Gbrap8,
2088 PixelFormat::Ya16Le,
2089 PixelFormat::CmykInverted,
2090 PixelFormat::Yuv440P,
2091 PixelFormat::Yuv440P10Le,
2092 PixelFormat::Yuv440P12Le,
2093 PixelFormat::Yuv440P16Le,
2094 PixelFormat::GrayF32Le,
2095 PixelFormat::RgbF32Le,
2096 PixelFormat::RgbaF32Le,
2097 PixelFormat::GbrpF32Le,
2098 PixelFormat::GbrapF32Le,
2099 ];
2100
2101 #[test]
2102 fn all_pixel_formats_list_is_complete_and_distinct() {
2103 // The list is discriminant-ordered and dense: 0..70 with no
2104 // gaps and no duplicates. A newly appended variant that isn't
2105 // added to the list will break the length or density check.
2106 let mut seen = std::collections::HashSet::new();
2107 for fmt in ALL_PIXEL_FORMATS {
2108 assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
2109 }
2110 for d in 0..ALL_PIXEL_FORMATS.len() as u16 {
2111 assert!(seen.contains(&d), "discriminant {d} missing from list");
2112 }
2113 }
2114
2115 #[test]
2116 fn yuv440_family_metadata() {
2117 // The whole 4:4:0 ladder shares one shape: planar, 3 planes,
2118 // no alpha, no palette, full-width half-height chroma.
2119 for fmt in [
2120 PixelFormat::Yuv440P,
2121 PixelFormat::Yuv440P10Le,
2122 PixelFormat::Yuv440P12Le,
2123 PixelFormat::Yuv440P16Le,
2124 ] {
2125 assert!(fmt.is_planar(), "{fmt:?} must be planar");
2126 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
2127 assert!(!fmt.has_alpha(), "{fmt:?} must not carry alpha");
2128 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
2129 assert!(!fmt.is_float(), "{fmt:?} must not be float");
2130 assert_eq!(
2131 fmt.chroma_subsampling(),
2132 Some((0, 1)),
2133 "{fmt:?} must be full-width, half-height chroma"
2134 );
2135 }
2136 }
2137
2138 #[test]
2139 fn yuv440_bits_per_pixel_approx() {
2140 // 4:4:0 packs the same samples-per-pixel as 4:2:2 at every
2141 // depth (2 samples/pixel), so the estimator numbers coincide.
2142 assert_eq!(
2143 PixelFormat::Yuv440P.bits_per_pixel_approx(),
2144 PixelFormat::Yuv422P.bits_per_pixel_approx()
2145 );
2146 assert_eq!(PixelFormat::Yuv440P.bits_per_pixel_approx(), 16);
2147 for (f440, f422) in [
2148 (PixelFormat::Yuv440P10Le, PixelFormat::Yuv422P10Le),
2149 (PixelFormat::Yuv440P12Le, PixelFormat::Yuv422P12Le),
2150 (PixelFormat::Yuv440P16Le, PixelFormat::Yuv422P16Le),
2151 ] {
2152 assert_eq!(
2153 f440.bits_per_pixel_approx(),
2154 f422.bits_per_pixel_approx(),
2155 "{f440:?}"
2156 );
2157 assert_eq!(f440.bits_per_pixel_approx(), 32, "{f440:?}");
2158 }
2159 }
2160
2161 #[test]
2162 fn yuv440_plane_geometry() {
2163 // Even sizes: chroma keeps the width, halves the height.
2164 assert_eq!(
2165 PixelFormat::Yuv440P.plane_dimensions(0, 640, 480),
2166 Some((640, 480))
2167 );
2168 assert_eq!(
2169 PixelFormat::Yuv440P.plane_dimensions(1, 640, 480),
2170 Some((640, 240))
2171 );
2172 assert_eq!(
2173 PixelFormat::Yuv440P.plane_dimensions(2, 640, 480),
2174 Some((640, 240))
2175 );
2176 // Odd height rounds up; odd width is untouched (ssx = 0).
2177 for fmt in [
2178 PixelFormat::Yuv440P,
2179 PixelFormat::Yuv440P10Le,
2180 PixelFormat::Yuv440P12Le,
2181 PixelFormat::Yuv440P16Le,
2182 ] {
2183 assert_eq!(fmt.plane_dimensions(0, 7, 5), Some((7, 5)), "{fmt:?}");
2184 assert_eq!(fmt.plane_dimensions(1, 7, 5), Some((7, 3)), "{fmt:?}");
2185 assert_eq!(fmt.plane_dimensions(2, 7, 5), Some((7, 3)), "{fmt:?}");
2186 assert_eq!(fmt.plane_dimensions(3, 7, 5), None, "{fmt:?}");
2187 }
2188 // Degenerate 1-row picture: the chroma plane still has a row.
2189 assert_eq!(PixelFormat::Yuv440P.plane_dimensions(1, 3, 1), Some((3, 1)));
2190 }
2191
2192 #[test]
2193 fn yuv440_sizing_round_trips() {
2194 // 6×5 8-bit: luma 6×5 = 30, each chroma 6×ceil(5/2) = 18.
2195 assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(0, 6, 5), Some(30));
2196 assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(1, 6, 5), Some(18));
2197 assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(2, 6, 5), Some(18));
2198 assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66));
2199 // 7×5: 35 + 21 + 21.
2200 assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(7, 5), Some(77));
2201 // Deep variants store 16-bit words: exactly double at every
2202 // depth (row bytes = width × 2 regardless of valid bits).
2203 for fmt in [
2204 PixelFormat::Yuv440P10Le,
2205 PixelFormat::Yuv440P12Le,
2206 PixelFormat::Yuv440P16Le,
2207 ] {
2208 assert_eq!(fmt.plane_row_bytes(0, 7), Some(14), "{fmt:?}");
2209 assert_eq!(fmt.plane_row_bytes(1, 7), Some(14), "{fmt:?}");
2210 assert_eq!(fmt.frame_size_bytes(7, 5), Some(154), "{fmt:?}");
2211 }
2212 }
2213
2214 #[test]
2215 fn float_family_metadata() {
2216 // Packed trio: single plane, not planar.
2217 for fmt in [
2218 PixelFormat::GrayF32Le,
2219 PixelFormat::RgbF32Le,
2220 PixelFormat::RgbaF32Le,
2221 ] {
2222 assert!(!fmt.is_planar(), "{fmt:?}");
2223 assert_eq!(fmt.plane_count(), 1, "{fmt:?}");
2224 }
2225 // Planar pair: GBR(A) shape.
2226 assert!(PixelFormat::GbrpF32Le.is_planar());
2227 assert_eq!(PixelFormat::GbrpF32Le.plane_count(), 3);
2228 assert!(PixelFormat::GbrapF32Le.is_planar());
2229 assert_eq!(PixelFormat::GbrapF32Le.plane_count(), 4);
2230 // Alpha only on the RGBA/GBRA members.
2231 assert!(!PixelFormat::GrayF32Le.has_alpha());
2232 assert!(!PixelFormat::RgbF32Le.has_alpha());
2233 assert!(PixelFormat::RgbaF32Le.has_alpha());
2234 assert!(!PixelFormat::GbrpF32Le.has_alpha());
2235 assert!(PixelFormat::GbrapF32Le.has_alpha());
2236 // The whole family is float, non-palette, and has no chroma
2237 // grid.
2238 for fmt in [
2239 PixelFormat::GrayF32Le,
2240 PixelFormat::RgbF32Le,
2241 PixelFormat::RgbaF32Le,
2242 PixelFormat::GbrpF32Le,
2243 PixelFormat::GbrapF32Le,
2244 ] {
2245 assert!(fmt.is_float(), "{fmt:?} must be float");
2246 assert!(!fmt.is_palette(), "{fmt:?}");
2247 assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2248 }
2249 }
2250
2251 #[test]
2252 fn is_float_false_for_integer_formats() {
2253 for fmt in ALL_PIXEL_FORMATS {
2254 let expect = matches!(
2255 fmt,
2256 PixelFormat::GrayF32Le
2257 | PixelFormat::RgbF32Le
2258 | PixelFormat::RgbaF32Le
2259 | PixelFormat::GbrpF32Le
2260 | PixelFormat::GbrapF32Le
2261 );
2262 assert_eq!(fmt.is_float(), expect, "{fmt:?}");
2263 }
2264 }
2265
2266 #[test]
2267 fn float_family_bits_per_pixel_and_sizing() {
2268 // Packed bits equal storage bits: every sample is a full
2269 // binary32 word.
2270 assert_eq!(PixelFormat::GrayF32Le.bits_per_pixel_approx(), 32);
2271 assert_eq!(PixelFormat::RgbF32Le.bits_per_pixel_approx(), 96);
2272 assert_eq!(PixelFormat::RgbaF32Le.bits_per_pixel_approx(), 128);
2273 assert_eq!(PixelFormat::GbrpF32Le.bits_per_pixel_approx(), 96);
2274 assert_eq!(PixelFormat::GbrapF32Le.bits_per_pixel_approx(), 128);
2275 // Packed row/frame sizes.
2276 assert_eq!(PixelFormat::GrayF32Le.plane_row_bytes(0, 3), Some(12));
2277 assert_eq!(PixelFormat::GrayF32Le.frame_size_bytes(5, 3), Some(60));
2278 assert_eq!(PixelFormat::RgbF32Le.plane_row_bytes(0, 7), Some(84));
2279 assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144));
2280 // Planar float: 4 bytes per sample on every plane; the packed
2281 // and planar layouts of the same component set cost the same.
2282 assert_eq!(PixelFormat::GbrpF32Le.plane_row_bytes(1, 7), Some(28));
2283 assert_eq!(
2284 PixelFormat::GbrpF32Le.frame_size_bytes(7, 5),
2285 PixelFormat::RgbF32Le.frame_size_bytes(7, 5)
2286 );
2287 assert_eq!(
2288 PixelFormat::GbrapF32Le.frame_size_bytes(7, 5),
2289 PixelFormat::RgbaF32Le.frame_size_bytes(7, 5)
2290 );
2291 // All planes of planar float GBR(A) are full resolution.
2292 for plane in 0..4 {
2293 assert_eq!(
2294 PixelFormat::GbrapF32Le.plane_dimensions(plane, 7, 5),
2295 Some((7, 5))
2296 );
2297 }
2298 }
2299
2300 #[test]
2301 fn chroma_subsampling_table() {
2302 use PixelFormat::*;
2303 // One representative per sampling class plus the full new
2304 // family; the wildcard class returns None.
2305 assert_eq!(Yuv420P.chroma_subsampling(), Some((1, 1)));
2306 assert_eq!(Nv12.chroma_subsampling(), Some((1, 1)));
2307 assert_eq!(Yuva420P16Le.chroma_subsampling(), Some((1, 1)));
2308 assert_eq!(Yuv422P.chroma_subsampling(), Some((1, 0)));
2309 assert_eq!(Yuyv422.chroma_subsampling(), Some((1, 0)));
2310 assert_eq!(Uyvy422.chroma_subsampling(), Some((1, 0)));
2311 assert_eq!(Yuv444P.chroma_subsampling(), Some((0, 0)));
2312 assert_eq!(Yuva444P12Le.chroma_subsampling(), Some((0, 0)));
2313 assert_eq!(Yuv411P.chroma_subsampling(), Some((2, 0)));
2314 assert_eq!(Yuv440P.chroma_subsampling(), Some((0, 1)));
2315 assert_eq!(Yuv440P16Le.chroma_subsampling(), Some((0, 1)));
2316 for fmt in [
2317 Gray8,
2318 Gray16Le,
2319 Ya8,
2320 Ya16Le,
2321 Pal8,
2322 MonoBlack,
2323 MonoWhite,
2324 Rgb24,
2325 Rgba,
2326 Rgb48Le,
2327 Rgba64Le,
2328 Cmyk,
2329 CmykInverted,
2330 Gbrp8,
2331 Gbrap16Le,
2332 GrayF32Le,
2333 RgbaF32Le,
2334 GbrapF32Le,
2335 ] {
2336 assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}");
2337 }
2338 }
2339
2340 #[test]
2341 fn plane_dimensions_odd_sizes_across_samplings() {
2342 use PixelFormat::*;
2343 // 4:2:0 — both axes ceil-halved.
2344 assert_eq!(Yuv420P.plane_dimensions(1, 7, 5), Some((4, 3)));
2345 // 4:2:2 — width ceil-halved, height untouched.
2346 assert_eq!(Yuv422P.plane_dimensions(2, 7, 5), Some((4, 5)));
2347 // 4:1:1 — width ceil-quartered.
2348 assert_eq!(Yuv411P.plane_dimensions(1, 7, 5), Some((2, 5)));
2349 assert_eq!(Yuv411P.plane_dimensions(1, 9, 5), Some((3, 5)));
2350 // 4:4:4 — untouched.
2351 assert_eq!(Yuv444P.plane_dimensions(1, 7, 5), Some((7, 5)));
2352 // Semi-planar chroma positions.
2353 assert_eq!(Nv12.plane_dimensions(1, 7, 5), Some((4, 3)));
2354 assert_eq!(Nv21.plane_dimensions(1, 7, 5), Some((4, 3)));
2355 // Alpha planes are never subsampled.
2356 assert_eq!(Yuva420P.plane_dimensions(3, 7, 5), Some((7, 5)));
2357 assert_eq!(Yuva422P16Le.plane_dimensions(3, 7, 5), Some((7, 5)));
2358 // Planar RGB planes are never subsampled.
2359 for plane in 0..3 {
2360 assert_eq!(Gbrp12Le.plane_dimensions(plane, 7, 5), Some((7, 5)));
2361 }
2362 // Out-of-range planes.
2363 assert_eq!(Rgb24.plane_dimensions(1, 8, 8), None);
2364 assert_eq!(Yuv420P.plane_dimensions(3, 8, 8), None);
2365 assert_eq!(Yuva420P.plane_dimensions(4, 8, 8), None);
2366 // Zero-sized pictures collapse every plane to zero.
2367 assert_eq!(Yuv440P.plane_dimensions(1, 0, 0), Some((0, 0)));
2368 }
2369
2370 #[test]
2371 fn plane_row_bytes_conventions() {
2372 use PixelFormat::*;
2373 // Bit-packed mono: ceil(width / 8) with a ragged tail byte.
2374 assert_eq!(MonoBlack.plane_row_bytes(0, 13), Some(2));
2375 assert_eq!(MonoWhite.plane_row_bytes(0, 16), Some(2));
2376 assert_eq!(MonoBlack.plane_row_bytes(0, 17), Some(3));
2377 // Packed 4:2:2: 4-byte macropixels, odd width rounds up.
2378 assert_eq!(Yuyv422.plane_row_bytes(0, 6), Some(12));
2379 assert_eq!(Uyvy422.plane_row_bytes(0, 7), Some(16));
2380 // Semi-planar chroma: 2 bytes per position.
2381 assert_eq!(Nv12.plane_row_bytes(0, 7), Some(7));
2382 assert_eq!(Nv12.plane_row_bytes(1, 7), Some(8));
2383 // Deep planar planes: 2 bytes per sample regardless of the
2384 // number of valid bits in the word.
2385 assert_eq!(Yuv420P10Le.plane_row_bytes(1, 7), Some(8));
2386 assert_eq!(Gbrap14Le.plane_row_bytes(3, 5), Some(10));
2387 // Packed pixel costs.
2388 assert_eq!(Rgb24.plane_row_bytes(0, 5), Some(15));
2389 assert_eq!(Rgb48Le.plane_row_bytes(0, 2), Some(12));
2390 assert_eq!(Rgba64Le.plane_row_bytes(0, 2), Some(16));
2391 assert_eq!(Ya16Le.plane_row_bytes(0, 3), Some(12));
2392 assert_eq!(Cmyk.plane_row_bytes(0, 3), Some(12));
2393 // Out-of-range plane.
2394 assert_eq!(Gray8.plane_row_bytes(1, 8), None);
2395 }
2396
2397 #[test]
2398 fn frame_size_examples() {
2399 use PixelFormat::*;
2400 assert_eq!(Yuv420P.frame_size_bytes(4, 4), Some(24));
2401 assert_eq!(Nv12.frame_size_bytes(7, 5), Some(59)); // 35 + 4×3×2
2402 assert_eq!(Yuyv422.frame_size_bytes(7, 2), Some(32));
2403 assert_eq!(MonoBlack.frame_size_bytes(13, 3), Some(6));
2404 assert_eq!(Pal8.frame_size_bytes(5, 4), Some(20));
2405 assert_eq!(Ya16Le.frame_size_bytes(3, 3), Some(36));
2406 assert_eq!(Yuva444P16Le.frame_size_bytes(3, 3), Some(72));
2407 }
2408
2409 #[test]
2410 fn frame_size_is_sum_of_planes_for_every_format() {
2411 for fmt in ALL_PIXEL_FORMATS {
2412 for (w, h) in [(0, 0), (1, 1), (2, 2), (7, 5), (16, 16), (13, 1), (1, 13)] {
2413 let total = fmt
2414 .frame_size_bytes(w, h)
2415 .unwrap_or_else(|| panic!("{fmt:?} {w}x{h} must size"));
2416 let sum: usize = (0..fmt.plane_count())
2417 .map(|p| fmt.plane_size_bytes(p, w, h).unwrap())
2418 .sum();
2419 assert_eq!(total, sum, "{fmt:?} {w}x{h}");
2420 // Plane 0 is always the full pixel grid.
2421 assert_eq!(fmt.plane_dimensions(0, w, h), Some((w, h)), "{fmt:?}");
2422 // The plane table ends exactly at plane_count.
2423 assert_eq!(fmt.plane_dimensions(fmt.plane_count(), w, h), None);
2424 // Tightly-packed storage can never be smaller than the
2425 // packed-bits density estimate.
2426 let storage_bits = total as u128 * 8;
2427 let density_bits = w as u128 * h as u128 * fmt.bits_per_pixel_approx() as u128;
2428 assert!(
2429 storage_bits >= density_bits,
2430 "{fmt:?} {w}x{h}: storage {storage_bits} < density {density_bits}"
2431 );
2432 }
2433 }
2434 }
2435
2436 #[test]
2437 fn sizing_overflow_returns_none() {
2438 assert_eq!(
2439 PixelFormat::Rgba64Le.frame_size_bytes(u32::MAX, u32::MAX),
2440 None
2441 );
2442 assert_eq!(
2443 PixelFormat::RgbaF32Le.frame_size_bytes(u32::MAX, u32::MAX),
2444 None
2445 );
2446 assert_eq!(
2447 PixelFormat::Yuv440P16Le.plane_size_bytes(0, u32::MAX, u32::MAX),
2448 None
2449 );
2450 }
2451}