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