oxideav_core/frame.rs
1//! Uncompressed audio and video frames.
2
3use crate::blob::{decode_blobs, encode_blobs, BlobKind, MetadataBlob};
4use crate::layer::LayerIdentity;
5use crate::signal::ColorSignal;
6use crate::subtitle::SubtitleCue;
7use crate::vector::VectorFrame;
8
9/// A decoded chunk of uncompressed data: either audio samples, a video
10/// picture, or (for subtitle streams) a single styled cue.
11///
12/// Marked `#[non_exhaustive]` — consumers that match on variants must
13/// include a wildcard arm. This lets the crate add new frame kinds (data
14/// tracks, hap rops, …) without breaking downstream code.
15#[derive(Clone, Debug)]
16#[non_exhaustive]
17pub enum Frame {
18 /// Uncompressed audio samples.
19 Audio(AudioFrame),
20 /// One uncompressed video picture.
21 Video(VideoFrame),
22 /// A single subtitle cue. Timing is carried inside the cue itself
23 /// (`start_us`/`end_us`) so it's independent of container time bases,
24 /// but the enclosing pipeline/muxer can still rescale via `pts` at
25 /// the packet layer.
26 Subtitle(SubtitleCue),
27 /// A resolution-independent vector-graphics frame. Produced by
28 /// vector-format decoders (`oxideav-svg`, the vector path of
29 /// `oxideav-pdf`) and consumed by vector renderers / writers.
30 /// See [`crate::vector`] for the full primitive set.
31 Vector(VectorFrame),
32}
33
34impl Frame {
35 /// Presentation timestamp of the frame in its stream's time base
36 /// (a subtitle cue reports its `start_us`); `None` if unknown.
37 pub fn pts(&self) -> Option<i64> {
38 match self {
39 Self::Audio(a) => a.pts,
40 Self::Video(v) => v.pts,
41 Self::Subtitle(s) => Some(s.start_us),
42 Self::Vector(v) => v.pts,
43 }
44 }
45}
46
47/// Uncompressed audio frame.
48///
49/// Stream-level properties (sample format, channel count, sample rate,
50/// time base) are NOT carried per-frame — read them from the stream's
51/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
52/// because real-time playback moves thousands per second per stream.
53///
54/// Sample layout is determined by the stream's `SampleFormat`:
55/// - Interleaved formats: `data` has one plane; samples are stored as
56/// `ch0 ch1 ... chN ch0 ch1 ... chN ...`.
57/// - Planar formats: `data` has one plane per channel.
58///
59/// Use [`SampleFormat::plane_count`](crate::SampleFormat::plane_count)
60/// with the stream's channel count to compute the expected `data.len()`.
61#[derive(Clone, Debug)]
62pub struct AudioFrame {
63 /// Number of samples *per channel* in this frame. Variable per-frame
64 /// for VBR codecs and on partial flushes.
65 pub samples: u32,
66 /// Presentation timestamp in the stream's time base; `None` if unknown.
67 pub pts: Option<i64>,
68 /// Raw sample bytes. Length matches `format.plane_count(channels)`
69 /// from the stream's `CodecParameters`.
70 pub data: Vec<Vec<u8>>,
71}
72
73/// Uncompressed video frame.
74///
75/// Stream-level properties (pixel format, width, height, time base) are
76/// NOT carried per-frame — read them from the stream's
77/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
78/// because real-time playback moves thousands per second per stream.
79///
80/// # Side-channels
81///
82/// `VideoFrame` (like [`VideoPlane`]) is a fully-public struct built by
83/// struct literal throughout the codec crates, so per-frame metadata
84/// cannot be added as new fields without breaking every constructor.
85/// Instead, optional metadata rides in-band as *side-channel* entries at
86/// the tail of `planes`: [`VideoPlane`] values whose shape is impossible
87/// for an image plane, which makes them unambiguous. Every record has
88/// non-empty `data` and a `stride` that no image plane can have —
89/// either `0` (an image plane's `data` is `stride × rows` long, so a
90/// zero stride forces empty data) or a value above `isize::MAX`
91/// (`stride × rows` with any non-zero row count would exceed what a
92/// `Vec` can hold). The whole `stride > isize::MAX` band is reserved
93/// for side-channel tags. Six record kinds exist, distinguished by
94/// their `stride` tag:
95///
96/// - **Palette** — `stride == 0`. Carries the color table for
97/// palette-indexed content
98/// ([`PixelFormat::Pal8`](crate::PixelFormat::Pal8)); see
99/// [`palette`](Self::palette) / [`set_palette`](Self::set_palette).
100/// - **Palette alpha** — `stride == usize::MAX - 3`. Carries one
101/// alpha byte per palette entry for tables with transparent or
102/// translucent entries (a GIF transparent index, a PNG `tRNS` chunk
103/// on a colour-type-3 image, TGA / BMP alpha palettes); entries the
104/// record does not cover are opaque. Only meaningful next to a
105/// palette record; see [`palette_alpha`](Self::palette_alpha) /
106/// [`set_palette_alpha`](Self::set_palette_alpha) and the combined
107/// [`palette_rgba`](Self::palette_rgba) /
108/// [`set_palette_rgba`](Self::set_palette_rgba).
109/// - **Per-plane significant bits** — `stride == usize::MAX`. Carries
110/// mixed per-plane bit depths (e.g. 12-bit luma with 10-bit chroma
111/// from a wavelet codec's custom signal range); see
112/// [`significant_bits`](Self::significant_bits) /
113/// [`set_significant_bits`](Self::set_significant_bits).
114/// - **Colour signal** — `stride == usize::MAX - 1`. Carries a
115/// [`ColorSignal`] (sample range + H.273 primaries / transfer /
116/// matrix) for producers whose signal is per-picture or that have no
117/// stream object to put it on; see
118/// [`color_signal`](Self::color_signal) /
119/// [`set_color_signal`](Self::set_color_signal).
120/// - **Layer identity** — `stride == usize::MAX - 2`. Carries a
121/// [`LayerIdentity`] (layer / view / access-unit) for frames of
122/// multi-layer or multi-view streams; see [`layer`](Self::layer) /
123/// [`set_layer`](Self::set_layer).
124/// - **Metadata blobs** — `stride == usize::MAX - 4`. Carries a list
125/// of [`MetadataBlob`]s (ICC profile, Exif, XMP, … — see
126/// [`crate::blob`]) for pictures whose metadata is per-picture: a
127/// multi-page TIFF's per-page Exif, HEIF burst items with their own
128/// profile. Wire form [`encode_blobs`] / [`decode_blobs`]; see
129/// [`blobs`](Self::blobs) / [`set_blobs`](Self::set_blobs). A blob
130/// that applies to the whole stream belongs on
131/// [`CodecParameters::blobs`](crate::CodecParameters::blobs) instead.
132///
133/// The records compose: a frame can carry any subset at once, in any
134/// order, within the trailing run of side-channel-shaped entries. The
135/// typed accessors find each record by its `stride` tag regardless of
136/// order, and [`image_planes`](Self::image_planes) /
137/// [`image_plane_count`](Self::image_plane_count) exclude the whole
138/// trailing run. Frames without any attached side-channel are
139/// byte-for-byte identical to what they always were.
140///
141/// Consumers that index `planes` directly (rather than through
142/// [`image_planes`](Self::image_planes)) see the records as extra
143/// trailing entries; producers attaching a record to every frame of a
144/// stream should therefore prefer the stream-level home
145/// ([`CodecParameters`](crate::CodecParameters)) for data that does not
146/// vary per picture, and attach per-frame records when the value is
147/// genuinely per-picture or no stream object exists.
148#[derive(Clone, Debug)]
149pub struct VideoFrame {
150 /// Presentation timestamp in the stream's time base; `None` if unknown.
151 pub pts: Option<i64>,
152 /// One entry per plane (e.g., 3 for Yuv420P). Each entry is `(stride, bytes)`.
153 ///
154 /// May additionally end with side-channel entries (palette, palette
155 /// alpha, per-plane significant bits, colour signal, layer identity,
156 /// metadata blobs — see the type-level docs). Code that
157 /// wants only pixel planes should iterate
158 /// [`image_planes`](Self::image_planes) instead of this field.
159 pub planes: Vec<VideoPlane>,
160}
161
162/// `stride` tag of the per-plane significant-bits side-channel record.
163/// (The palette record's tag is `0`; see the [`VideoFrame`] docs.)
164const SIGNIFICANT_BITS_STRIDE: usize = usize::MAX;
165
166/// `stride` tag of the colour-signal side-channel record.
167const COLOR_SIGNAL_STRIDE: usize = usize::MAX - 1;
168
169/// `stride` tag of the layer-identity side-channel record.
170const LAYER_IDENTITY_STRIDE: usize = usize::MAX - 2;
171
172/// `stride` tag of the palette-alpha side-channel record.
173const PALETTE_ALPHA_STRIDE: usize = usize::MAX - 3;
174
175/// `stride` tag of the metadata-blobs side-channel record.
176const BLOBS_STRIDE: usize = usize::MAX - 4;
177
178/// Smallest `stride` value that is impossible for an image plane with
179/// at least one row: `stride × rows` would exceed `isize::MAX`, the
180/// largest allocation a `Vec` can hold. Every non-zero side-channel
181/// tag lives at or above this value.
182const SIDE_CHANNEL_STRIDE_FLOOR: usize = isize::MAX as usize + 1;
183
184impl VideoFrame {
185 /// `true` when `plane` has a side-channel record shape: non-empty
186 /// data with an impossible-for-an-image-plane stride (`0`, or any
187 /// value above `isize::MAX`) as described in the type-level docs.
188 fn is_side_channel_entry(plane: &VideoPlane) -> bool {
189 (plane.stride == 0 || plane.stride >= SIDE_CHANNEL_STRIDE_FLOOR) && !plane.data.is_empty()
190 }
191
192 /// Index of the first entry of the trailing side-channel run — equal
193 /// to the number of image planes. Scans backwards from the tail
194 /// while entries have a side-channel shape.
195 fn side_channel_run_start(&self) -> usize {
196 let mut start = self.planes.len();
197 while start > 0 && Self::is_side_channel_entry(&self.planes[start - 1]) {
198 start -= 1;
199 }
200 start
201 }
202
203 /// Index in `planes` of the side-channel record tagged with
204 /// `stride_tag`, searching the trailing side-channel run only (the
205 /// last match wins if a malformed frame carries duplicates).
206 fn side_channel_index(&self, stride_tag: usize) -> Option<usize> {
207 let start = self.side_channel_run_start();
208 self.planes[start..]
209 .iter()
210 .rposition(|p| p.stride == stride_tag)
211 .map(|i| start + i)
212 }
213
214 /// Remove every record tagged `stride_tag` from the trailing
215 /// side-channel run, returning the data of the record the readers
216 /// would have reported (the last match — consistent with
217 /// [`side_channel_index`](Self::side_channel_index)).
218 fn remove_side_channel(&mut self, stride_tag: usize) -> Option<Vec<u8>> {
219 let reported = self
220 .side_channel_index(stride_tag)
221 .map(|i| self.planes.remove(i).data);
222 while let Some(i) = self.side_channel_index(stride_tag) {
223 self.planes.remove(i);
224 }
225 reported
226 }
227
228 /// The frame's attached palette, if any.
229 ///
230 /// Returns the raw bytes of the palette side-channel (see the
231 /// type-level docs): packed 3-byte RGB entries, entry `i` at bytes
232 /// `3*i .. 3*i + 3` in R, G, B order. A full
233 /// [`Pal8`](crate::PixelFormat::Pal8) table is 256 entries
234 /// (768 bytes), but producers may attach fewer when the source
235 /// image declares a shorter table; indices at or beyond
236 /// `len / 3` are undefined by this frame and up to the consumer's
237 /// missing-entry policy (typically black).
238 pub fn palette(&self) -> Option<&[u8]> {
239 self.side_channel_index(0)
240 .map(|i| self.planes[i].data.as_slice())
241 }
242
243 /// The RGB triplet for palette entry `index`, or `None` when no
244 /// palette is attached or the attached table is too short to cover
245 /// `index`. Sugar over [`palette`](Self::palette) for per-pixel
246 /// lookups.
247 pub fn palette_rgb(&self, index: u8) -> Option<[u8; 3]> {
248 let pal = self.palette()?;
249 let at = usize::from(index) * 3;
250 let entry = pal.get(at..at + 3)?;
251 Some([entry[0], entry[1], entry[2]])
252 }
253
254 /// Attach (or replace) the frame's palette side-channel.
255 ///
256 /// `rgb` is packed 3-byte RGB entries — see
257 /// [`palette`](Self::palette) for the exact layout; pass a length
258 /// that is a multiple of 3 (up to 768 bytes for a full 256-entry
259 /// [`Pal8`](crate::PixelFormat::Pal8) table). The bytes are stored
260 /// verbatim. An empty `rgb` removes any attached palette instead
261 /// (the sentinel requires non-empty data), leaving the frame
262 /// exactly as it was before any palette was attached.
263 pub fn set_palette(&mut self, rgb: Vec<u8>) {
264 self.remove_side_channel(0);
265 if !rgb.is_empty() {
266 self.planes.push(VideoPlane {
267 stride: 0,
268 data: rgb,
269 });
270 }
271 }
272
273 /// Builder-style counterpart to [`set_palette`](Self::set_palette)
274 /// for construction chains:
275 /// `VideoFrame { pts, planes }.with_palette(rgb)`.
276 pub fn with_palette(mut self, rgb: Vec<u8>) -> Self {
277 self.set_palette(rgb);
278 self
279 }
280
281 /// Detach and return the frame's palette side-channel, if any.
282 /// Afterwards the frame carries no palette (any other side-channel
283 /// record is left in place — including a palette-alpha record,
284 /// which then reads as `None` until a palette is attached again).
285 pub fn take_palette(&mut self) -> Option<Vec<u8>> {
286 self.remove_side_channel(0)
287 }
288
289 /// Number of entries the attached palette covers (`len / 3`), or
290 /// `0` without a palette.
291 fn palette_entry_count(&self) -> usize {
292 self.palette().map_or(0, |p| p.len() / 3)
293 }
294
295 /// The frame's attached palette-alpha record, if any.
296 ///
297 /// Returns the raw bytes of the palette-alpha side-channel (see the
298 /// type-level docs): byte `i` is the alpha of palette entry `i`
299 /// (`0` transparent, `255` opaque), in the same entry order as
300 /// [`palette`](Self::palette). The record may be shorter than the
301 /// palette — a GIF with one transparent index needs only
302 /// `index + 1` bytes — and every entry it does not cover is opaque.
303 ///
304 /// The record is only meaningful next to a palette: it reads as
305 /// `None` when no palette is attached, and a malformed record
306 /// **longer** than the palette's entry count also reads as `None`
307 /// (consumers fall back to opaque). Validation happens at read
308 /// time, so palette and alpha may be attached in either order.
309 pub fn palette_alpha(&self) -> Option<&[u8]> {
310 let i = self.side_channel_index(PALETTE_ALPHA_STRIDE)?;
311 let alpha = self.planes[i].data.as_slice();
312 let entries = self.palette_entry_count();
313 (entries > 0 && alpha.len() <= entries).then_some(alpha)
314 }
315
316 /// The RGBA quadruplet for palette entry `index`: the RGB triplet
317 /// from the palette record and the alpha from the palette-alpha
318 /// record, or `255` (opaque) when no alpha record is attached, it
319 /// does not cover `index`, or it is malformed. `None` when no
320 /// palette is attached or the palette is too short to cover
321 /// `index` — exactly when [`palette_rgb`](Self::palette_rgb) is
322 /// `None`. The one lookup a `Pal8` → RGBA expander needs.
323 pub fn palette_rgba(&self, index: u8) -> Option<[u8; 4]> {
324 let [r, g, b] = self.palette_rgb(index)?;
325 let a = self
326 .palette_alpha()
327 .and_then(|a| a.get(usize::from(index)).copied())
328 .unwrap_or(u8::MAX);
329 Some([r, g, b, a])
330 }
331
332 /// Attach (or replace) the frame's palette-alpha side-channel.
333 ///
334 /// `alpha` holds one byte per palette entry, in entry order — see
335 /// [`palette_alpha`](Self::palette_alpha) for the semantics (may be
336 /// shorter than the palette; uncovered entries are opaque). The
337 /// bytes are stored verbatim; the length is checked against the
338 /// palette when read, not here, so the two records may be attached
339 /// in either order. An empty `alpha` removes any attached record
340 /// instead (the sentinel requires non-empty data). Other
341 /// side-channel records — the palette included — are unaffected.
342 pub fn set_palette_alpha(&mut self, alpha: Vec<u8>) {
343 self.remove_side_channel(PALETTE_ALPHA_STRIDE);
344 if !alpha.is_empty() {
345 self.planes.push(VideoPlane {
346 stride: PALETTE_ALPHA_STRIDE,
347 data: alpha,
348 });
349 }
350 }
351
352 /// Builder-style counterpart to
353 /// [`set_palette_alpha`](Self::set_palette_alpha) for construction
354 /// chains:
355 /// `VideoFrame { pts, planes }.with_palette(rgb).with_palette_alpha(alpha)`.
356 pub fn with_palette_alpha(mut self, alpha: Vec<u8>) -> Self {
357 self.set_palette_alpha(alpha);
358 self
359 }
360
361 /// Detach and return the frame's palette-alpha side-channel, if
362 /// any — the bytes [`palette_alpha`](Self::palette_alpha) would
363 /// have reported, so a record that read as `None` (no palette, or
364 /// longer than the palette) is removed but returns `None`.
365 /// Afterwards the frame carries no palette alpha (the palette and
366 /// every other record are left in place).
367 pub fn take_palette_alpha(&mut self) -> Option<Vec<u8>> {
368 let valid = self.palette_alpha().is_some();
369 self.remove_side_channel(PALETTE_ALPHA_STRIDE)
370 .filter(|_| valid)
371 }
372
373 /// Attach (or replace) both palette records at once from RGBA
374 /// entries: entry `i` of `rgba` becomes bytes `3*i .. 3*i + 3` of
375 /// the palette record and byte `i` of the palette-alpha record.
376 /// The alpha record is written even when every entry is opaque
377 /// (stored verbatim, no trimming). An empty `rgba` removes both
378 /// records.
379 ///
380 /// ```
381 /// # use oxideav_core::{VideoFrame, VideoPlane};
382 /// let mut f = VideoFrame { pts: None, planes: vec![VideoPlane { stride: 1, data: vec![1] }] };
383 /// f.set_palette_rgba(&[[0, 0, 0, 0], [255, 255, 255, 255]]);
384 /// assert_eq!(f.palette_rgba(0), Some([0, 0, 0, 0]));
385 /// assert_eq!(f.palette_rgba(1), Some([255, 255, 255, 255]));
386 /// assert_eq!(f.palette_rgba(2), None);
387 /// assert_eq!(f.image_plane_count(), 1);
388 /// ```
389 pub fn set_palette_rgba(&mut self, rgba: &[[u8; 4]]) {
390 let rgb = rgba.iter().flat_map(|e| [e[0], e[1], e[2]]).collect();
391 let alpha = rgba.iter().map(|e| e[3]).collect();
392 self.set_palette(rgb);
393 self.set_palette_alpha(alpha);
394 }
395
396 /// Builder-style counterpart to
397 /// [`set_palette_rgba`](Self::set_palette_rgba).
398 pub fn with_palette_rgba(mut self, rgba: &[[u8; 4]]) -> Self {
399 self.set_palette_rgba(rgba);
400 self
401 }
402
403 /// The frame's attached per-plane significant-bits record, if any.
404 ///
405 /// Returns the raw bytes of the significant-bits side-channel (see
406 /// the type-level docs): byte `k` is the number of significant bits
407 /// in the samples of image plane `k`, in plane order. This lets a
408 /// producer express **mixed** per-plane depths that no single
409 /// [`PixelFormat`](crate::PixelFormat) variant can name — e.g. a
410 /// wavelet codec's custom signal range with 12-bit luma and 10-bit
411 /// chroma, stored on a `Yuv444P12Le` surface with an attached
412 /// record of `[12, 10, 10]`.
413 ///
414 /// # Semantics
415 ///
416 /// - Values are **LSB-anchored**: a plane with `b` significant bits
417 /// keeps its sample values in the low `b` bits of each storage
418 /// word, with the upper bits zero — the same convention as this
419 /// crate's partial-depth formats (`Gray10Le`, `Yuv420P10Le`,
420 /// `Gbrp12Le`, …, each documented as "uses the low N bits of a
421 /// 16-bit word"). Full-scale for `b` significant bits is
422 /// `(1 << b) - 1`.
423 /// - Each value must satisfy `1 ≤ b ≤ 8 × storage-word-bytes` of
424 /// the frame's pixel format (so at most 8 for byte-sized planes,
425 /// 16 for LE-16-bit-word planes). The record refines the storage
426 /// format's *significant* depth; it never changes the storage
427 /// word size or plane geometry.
428 /// - A record shorter than the image-plane count (or a missing
429 /// record) leaves the uncovered planes at the pixel format's own
430 /// documented depth. Bytes are stored verbatim; out-of-range
431 /// values are a producer bug and consumers may clamp or reject
432 /// them.
433 pub fn significant_bits(&self) -> Option<&[u8]> {
434 self.side_channel_index(SIGNIFICANT_BITS_STRIDE)
435 .map(|i| self.planes[i].data.as_slice())
436 }
437
438 /// The significant-bit count for image plane `plane`, or `None`
439 /// when no record is attached or the attached record is too short
440 /// to cover `plane` (fall back to the pixel format's own depth).
441 /// Sugar over [`significant_bits`](Self::significant_bits) for
442 /// per-plane lookups.
443 pub fn plane_significant_bits(&self, plane: usize) -> Option<u8> {
444 self.significant_bits()?.get(plane).copied()
445 }
446
447 /// Attach (or replace) the frame's per-plane significant-bits
448 /// side-channel.
449 ///
450 /// `bits` holds one byte per image plane, in plane order — see
451 /// [`significant_bits`](Self::significant_bits) for the exact
452 /// semantics (LSB-anchored values, `1 ≤ b ≤ storage word bits`).
453 /// The bytes are stored verbatim. An empty `bits` removes any
454 /// attached record instead (the sentinel requires non-empty data),
455 /// leaving the frame exactly as it was before any record was
456 /// attached. Any attached palette is unaffected.
457 pub fn set_significant_bits(&mut self, bits: Vec<u8>) {
458 self.remove_side_channel(SIGNIFICANT_BITS_STRIDE);
459 if !bits.is_empty() {
460 self.planes.push(VideoPlane {
461 stride: SIGNIFICANT_BITS_STRIDE,
462 data: bits,
463 });
464 }
465 }
466
467 /// Builder-style counterpart to
468 /// [`set_significant_bits`](Self::set_significant_bits) for
469 /// construction chains:
470 /// `VideoFrame { pts, planes }.with_significant_bits(bits)`.
471 pub fn with_significant_bits(mut self, bits: Vec<u8>) -> Self {
472 self.set_significant_bits(bits);
473 self
474 }
475
476 /// Detach and return the frame's per-plane significant-bits
477 /// side-channel, if any. Afterwards the frame carries no
478 /// significant-bits record (any attached palette is left in place).
479 pub fn take_significant_bits(&mut self) -> Option<Vec<u8>> {
480 self.remove_side_channel(SIGNIFICANT_BITS_STRIDE)
481 }
482
483 /// The frame's attached colour-signal description, if any.
484 ///
485 /// Decoded from the colour-signal side-channel record (see the
486 /// type-level docs; wire form in [`ColorSignal::to_bytes`]). A
487 /// per-frame description refines the stream-level
488 /// [`CodecParameters::color_signal`](crate::CodecParameters::color_signal):
489 /// consumers resolve `frame.color_signal().unwrap_or_default()
490 /// .or(params.color_signal)` and then apply their own policy to
491 /// whatever is still unspecified. A malformed (too short) record
492 /// reads as `None`.
493 pub fn color_signal(&self) -> Option<ColorSignal> {
494 self.side_channel_index(COLOR_SIGNAL_STRIDE)
495 .and_then(|i| ColorSignal::from_bytes(&self.planes[i].data))
496 }
497
498 /// Attach (or replace) the frame's colour-signal side-channel.
499 /// Other side-channel records are unaffected.
500 pub fn set_color_signal(&mut self, signal: ColorSignal) {
501 self.remove_side_channel(COLOR_SIGNAL_STRIDE);
502 self.planes.push(VideoPlane {
503 stride: COLOR_SIGNAL_STRIDE,
504 data: signal.to_bytes().to_vec(),
505 });
506 }
507
508 /// Builder-style counterpart to
509 /// [`set_color_signal`](Self::set_color_signal) for construction
510 /// chains: `VideoFrame { pts, planes }.with_color_signal(sig)`.
511 pub fn with_color_signal(mut self, signal: ColorSignal) -> Self {
512 self.set_color_signal(signal);
513 self
514 }
515
516 /// Detach and return the frame's colour-signal side-channel, if
517 /// any. Afterwards the frame carries no colour signal (other
518 /// records are left in place).
519 pub fn take_color_signal(&mut self) -> Option<ColorSignal> {
520 self.remove_side_channel(COLOR_SIGNAL_STRIDE)
521 .and_then(|d| ColorSignal::from_bytes(&d))
522 }
523
524 /// The frame's attached layer / view identity, if any.
525 ///
526 /// Decoded from the layer-identity side-channel record (see the
527 /// type-level docs; wire form in [`LayerIdentity::to_bytes`]).
528 /// Single-layer decoders attach nothing; consumers treat `None` as
529 /// "base layer, no view". A malformed (too short) record reads as
530 /// `None`.
531 pub fn layer(&self) -> Option<LayerIdentity> {
532 self.side_channel_index(LAYER_IDENTITY_STRIDE)
533 .and_then(|i| LayerIdentity::from_bytes(&self.planes[i].data))
534 }
535
536 /// The frame's layer identity, or the base layer when none is
537 /// attached. Sugar over [`layer`](Self::layer) for consumers that
538 /// handle single- and multi-layer streams alike.
539 pub fn layer_or_base(&self) -> LayerIdentity {
540 self.layer().unwrap_or_default()
541 }
542
543 /// Attach (or replace) the frame's layer-identity side-channel.
544 /// Other side-channel records are unaffected.
545 pub fn set_layer(&mut self, layer: LayerIdentity) {
546 self.remove_side_channel(LAYER_IDENTITY_STRIDE);
547 self.planes.push(VideoPlane {
548 stride: LAYER_IDENTITY_STRIDE,
549 data: layer.to_bytes().to_vec(),
550 });
551 }
552
553 /// Builder-style counterpart to [`set_layer`](Self::set_layer) for
554 /// construction chains:
555 /// `VideoFrame { pts, planes }.with_layer(LayerIdentity::new(1))`.
556 pub fn with_layer(mut self, layer: LayerIdentity) -> Self {
557 self.set_layer(layer);
558 self
559 }
560
561 /// Detach and return the frame's layer-identity side-channel, if
562 /// any. Afterwards the frame carries no layer identity (other
563 /// records are left in place).
564 pub fn take_layer(&mut self) -> Option<LayerIdentity> {
565 self.remove_side_channel(LAYER_IDENTITY_STRIDE)
566 .and_then(|d| LayerIdentity::from_bytes(&d))
567 }
568
569 /// The frame's attached metadata blobs, decoded from the blobs
570 /// side-channel record (see the type-level docs; wire form in
571 /// [`encode_blobs`]). Empty when no record is attached — and when
572 /// the record is malformed, so a consumer never sees half a list.
573 /// Decodes on every call: read it once per frame.
574 ///
575 /// A per-frame blob refines the stream-level
576 /// [`CodecParameters::blobs`](crate::CodecParameters::blobs) of
577 /// the same kind: resolve
578 /// `frame.blob(&k).or_else(|| params.blob(&k).cloned())`.
579 pub fn blobs(&self) -> Vec<MetadataBlob> {
580 self.side_channel_index(BLOBS_STRIDE)
581 .and_then(|i| decode_blobs(&self.planes[i].data))
582 .unwrap_or_default()
583 }
584
585 /// The first attached blob of `kind`, if any (see
586 /// [`blobs`](Self::blobs)).
587 pub fn blob(&self, kind: &BlobKind) -> Option<MetadataBlob> {
588 self.blobs().into_iter().find(|b| b.is(kind))
589 }
590
591 /// Attach (or replace) the frame's metadata-blobs side-channel
592 /// with `blobs`, in the given order. An empty list removes any
593 /// attached record instead (the sentinel requires non-empty data).
594 /// Other side-channel records are unaffected.
595 pub fn set_blobs(&mut self, blobs: Vec<MetadataBlob>) {
596 self.remove_side_channel(BLOBS_STRIDE);
597 if !blobs.is_empty() {
598 self.planes.push(VideoPlane {
599 stride: BLOBS_STRIDE,
600 data: encode_blobs(&blobs),
601 });
602 }
603 }
604
605 /// Append one blob to the frame's metadata-blobs record (creating
606 /// it when absent; a malformed existing record is replaced by the
607 /// single new blob). Blobs of the same kind are kept in insertion
608 /// order; nothing is replaced.
609 pub fn push_blob(&mut self, blob: MetadataBlob) {
610 let mut blobs = self.blobs();
611 blobs.push(blob);
612 self.set_blobs(blobs);
613 }
614
615 /// Builder-style counterpart to [`push_blob`](Self::push_blob) for
616 /// construction chains:
617 /// `VideoFrame { pts, planes }.with_blob(MetadataBlob::new(BlobKind::EXIF, exif))`.
618 pub fn with_blob(mut self, blob: MetadataBlob) -> Self {
619 self.push_blob(blob);
620 self
621 }
622
623 /// Builder-style counterpart to [`set_blobs`](Self::set_blobs).
624 pub fn with_blobs(mut self, blobs: Vec<MetadataBlob>) -> Self {
625 self.set_blobs(blobs);
626 self
627 }
628
629 /// Detach and return the frame's metadata blobs, if any (empty for
630 /// no record or a malformed one — which is removed all the same).
631 /// Afterwards the frame carries no blobs record (other records are
632 /// left in place).
633 pub fn take_blobs(&mut self) -> Vec<MetadataBlob> {
634 self.remove_side_channel(BLOBS_STRIDE)
635 .and_then(|d| decode_blobs(&d))
636 .unwrap_or_default()
637 }
638
639 /// The frame's image planes — `planes` with the trailing
640 /// side-channel entries (palette, palette alpha, significant bits,
641 /// colour signal, layer identity, metadata blobs) excluded.
642 /// Prefer this over indexing `planes` directly in code that
643 /// handles side-channel-capable frames.
644 pub fn image_planes(&self) -> &[VideoPlane] {
645 &self.planes[..self.image_plane_count()]
646 }
647
648 /// Number of image planes (excludes every side-channel entry).
649 /// Matches the stream pixel format's
650 /// [`plane_count`](crate::PixelFormat::plane_count) for well-formed
651 /// frames.
652 pub fn image_plane_count(&self) -> usize {
653 self.side_channel_run_start()
654 }
655}
656
657/// One plane of a [`VideoFrame`]: row-major sample bytes plus the
658/// stride between rows.
659///
660/// An entry with non-empty `data` and a `stride` of `0` or above
661/// `isize::MAX` is not an image plane: it is a side-channel record
662/// (palette, palette alpha, per-plane significant bits, colour signal,
663/// layer identity, metadata blobs) described on [`VideoFrame`] — only
664/// meaningful within the trailing
665/// run of `VideoFrame::planes`.
666#[derive(Clone, Debug)]
667pub struct VideoPlane {
668 /// Bytes per row in `data`.
669 pub stride: usize,
670 /// Raw plane bytes, `stride × rows` long (rows may carry padding
671 /// beyond the visible width).
672 pub data: Vec<u8>,
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 fn gray_frame() -> VideoFrame {
680 // 4×2 Gray8 image plane.
681 VideoFrame {
682 pts: Some(7),
683 planes: vec![VideoPlane {
684 stride: 4,
685 data: vec![0u8; 8],
686 }],
687 }
688 }
689
690 /// A full 256-entry table where entry i is (i, !i, i^0x55).
691 fn full_palette() -> Vec<u8> {
692 (0u16..256)
693 .flat_map(|i| {
694 let i = i as u8;
695 [i, !i, i ^ 0x55]
696 })
697 .collect()
698 }
699
700 #[test]
701 fn frame_without_palette_reports_none_and_full_image_planes() {
702 let f = gray_frame();
703 assert_eq!(f.palette(), None);
704 assert_eq!(f.palette_rgb(0), None);
705 assert_eq!(f.image_plane_count(), 1);
706 assert_eq!(f.image_planes().len(), 1);
707 assert_eq!(f.image_planes()[0].stride, 4);
708 }
709
710 #[test]
711 fn set_palette_round_trips_and_keeps_image_planes_intact() {
712 let mut f = gray_frame();
713 let pal = full_palette();
714 f.set_palette(pal.clone());
715
716 assert_eq!(f.palette(), Some(pal.as_slice()));
717 // Image-plane view is unchanged by the side-channel.
718 assert_eq!(f.image_plane_count(), 1);
719 assert_eq!(f.image_planes()[0].data.len(), 8);
720 // The raw field sees the sentinel entry at the tail.
721 assert_eq!(f.planes.len(), 2);
722 assert_eq!(f.planes[1].stride, 0);
723
724 // Entry lookup: entry i is (i, !i, i ^ 0x55) by construction.
725 assert_eq!(f.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
726 assert_eq!(f.palette_rgb(0xAB), Some([0xAB, 0x54, 0xFE]));
727 assert_eq!(f.palette_rgb(255), Some([0xFF, 0x00, 0xAA]));
728 }
729
730 #[test]
731 fn set_palette_replaces_existing_table() {
732 let mut f = gray_frame();
733 f.set_palette(vec![1, 2, 3]);
734 f.set_palette(vec![9, 8, 7, 6, 5, 4]);
735 // Replacement, not stacking: one image plane + one sentinel.
736 assert_eq!(f.planes.len(), 2);
737 assert_eq!(f.palette(), Some(&[9, 8, 7, 6, 5, 4][..]));
738 assert_eq!(f.palette_rgb(1), Some([6, 5, 4]));
739 }
740
741 #[test]
742 fn short_palette_covers_only_its_entries() {
743 let f = gray_frame().with_palette(vec![10, 20, 30, 40, 50, 60]);
744 assert_eq!(f.palette_rgb(0), Some([10, 20, 30]));
745 assert_eq!(f.palette_rgb(1), Some([40, 50, 60]));
746 // Beyond the table: undefined by the frame → None.
747 assert_eq!(f.palette_rgb(2), None);
748 assert_eq!(f.palette_rgb(255), None);
749 }
750
751 #[test]
752 fn empty_palette_clears_and_take_palette_detaches() {
753 let mut f = gray_frame();
754 f.set_palette(vec![1, 2, 3]);
755 assert!(f.palette().is_some());
756
757 // Empty input removes the side-channel entirely.
758 f.set_palette(Vec::new());
759 assert_eq!(f.palette(), None);
760 assert_eq!(f.planes.len(), 1);
761
762 // take_palette detaches and returns the bytes.
763 f.set_palette(vec![4, 5, 6]);
764 assert_eq!(f.take_palette(), Some(vec![4, 5, 6]));
765 assert_eq!(f.palette(), None);
766 assert_eq!(f.take_palette(), None);
767 assert_eq!(f.planes.len(), 1);
768 }
769
770 #[test]
771 fn zero_stride_empty_plane_is_not_mistaken_for_a_palette() {
772 // stride == 0 with EMPTY data is the degenerate (but
773 // contract-consistent) empty image plane, not the sentinel.
774 let f = VideoFrame {
775 pts: None,
776 planes: vec![
777 VideoPlane {
778 stride: 4,
779 data: vec![0u8; 8],
780 },
781 VideoPlane {
782 stride: 0,
783 data: Vec::new(),
784 },
785 ],
786 };
787 assert_eq!(f.palette(), None);
788 assert_eq!(f.image_plane_count(), 2);
789 }
790
791 #[test]
792 fn palette_on_frame_without_image_planes() {
793 // A palette can be attached before pixel planes exist (encoder
794 // scaffolding); the image-plane view is then empty.
795 let f = VideoFrame {
796 pts: None,
797 planes: Vec::new(),
798 }
799 .with_palette(vec![1, 2, 3]);
800 assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
801 assert_eq!(f.image_plane_count(), 0);
802 assert!(f.image_planes().is_empty());
803 }
804
805 #[test]
806 fn frame_without_significant_bits_reports_none() {
807 let f = gray_frame();
808 assert_eq!(f.significant_bits(), None);
809 assert_eq!(f.plane_significant_bits(0), None);
810 assert_eq!(f.image_plane_count(), 1);
811 }
812
813 #[test]
814 fn set_significant_bits_round_trips_and_keeps_image_planes_intact() {
815 // A 12-bit-luma / 10-bit-chroma mixed-depth frame (the VC-2
816 // custom-signal-range shape that motivated the record).
817 let mut f = VideoFrame {
818 pts: Some(3),
819 planes: vec![
820 VideoPlane {
821 stride: 8,
822 data: vec![0u8; 16],
823 },
824 VideoPlane {
825 stride: 8,
826 data: vec![0u8; 16],
827 },
828 VideoPlane {
829 stride: 8,
830 data: vec![0u8; 16],
831 },
832 ],
833 };
834 f.set_significant_bits(vec![12, 10, 10]);
835
836 assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
837 assert_eq!(f.plane_significant_bits(0), Some(12));
838 assert_eq!(f.plane_significant_bits(1), Some(10));
839 assert_eq!(f.plane_significant_bits(2), Some(10));
840 // Beyond the record: fall back to the format default → None.
841 assert_eq!(f.plane_significant_bits(3), None);
842
843 // Image-plane view is unchanged by the side-channel.
844 assert_eq!(f.image_plane_count(), 3);
845 assert_eq!(f.image_planes().len(), 3);
846 // The raw field sees the sentinel entry at the tail.
847 assert_eq!(f.planes.len(), 4);
848 assert_eq!(f.planes[3].stride, usize::MAX);
849 }
850
851 #[test]
852 fn set_significant_bits_replaces_and_empty_clears_and_take_detaches() {
853 let mut f = gray_frame();
854 f.set_significant_bits(vec![7]);
855 f.set_significant_bits(vec![6]);
856 // Replacement, not stacking.
857 assert_eq!(f.planes.len(), 2);
858 assert_eq!(f.significant_bits(), Some(&[6][..]));
859
860 // Empty input removes the side-channel entirely.
861 f.set_significant_bits(Vec::new());
862 assert_eq!(f.significant_bits(), None);
863 assert_eq!(f.planes.len(), 1);
864
865 // take_significant_bits detaches and returns the bytes.
866 f.set_significant_bits(vec![5]);
867 assert_eq!(f.take_significant_bits(), Some(vec![5]));
868 assert_eq!(f.significant_bits(), None);
869 assert_eq!(f.take_significant_bits(), None);
870 assert_eq!(f.planes.len(), 1);
871 }
872
873 #[test]
874 fn palette_and_significant_bits_compose_in_either_order() {
875 // Palette first, then depths.
876 let mut f = gray_frame()
877 .with_palette(vec![1, 2, 3])
878 .with_significant_bits(vec![8]);
879 assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
880 assert_eq!(f.significant_bits(), Some(&[8][..]));
881 assert_eq!(f.image_plane_count(), 1);
882 assert_eq!(f.planes.len(), 3);
883
884 // Replacing one record must not disturb the other, regardless
885 // of which currently sits at the tail.
886 f.set_palette(vec![9, 8, 7]);
887 assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
888 assert_eq!(f.significant_bits(), Some(&[8][..]));
889 f.set_significant_bits(vec![7]);
890 assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
891 assert_eq!(f.significant_bits(), Some(&[7][..]));
892 assert_eq!(f.image_plane_count(), 1);
893
894 // Depths first, then palette.
895 let g = gray_frame()
896 .with_significant_bits(vec![4])
897 .with_palette(full_palette());
898 assert_eq!(g.significant_bits(), Some(&[4][..]));
899 assert_eq!(g.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
900 assert_eq!(g.image_plane_count(), 1);
901
902 // Detaching one leaves the other attached.
903 let mut h = g;
904 assert_eq!(h.take_significant_bits(), Some(vec![4]));
905 assert_eq!(h.significant_bits(), None);
906 assert_eq!(h.palette().map(<[u8]>::len), Some(768));
907 assert_eq!(h.take_palette().map(|p| p.len()), Some(768));
908 assert_eq!(h.planes.len(), 1);
909 assert_eq!(h.image_plane_count(), 1);
910 }
911
912 #[test]
913 fn max_stride_empty_plane_is_not_mistaken_for_significant_bits() {
914 // stride == usize::MAX with EMPTY data is not the sentinel
915 // (mirroring the palette rule: sentinels require non-empty
916 // data). Degenerate, but must not be misread as a record.
917 let f = VideoFrame {
918 pts: None,
919 planes: vec![
920 VideoPlane {
921 stride: 4,
922 data: vec![0u8; 8],
923 },
924 VideoPlane {
925 stride: usize::MAX,
926 data: Vec::new(),
927 },
928 ],
929 };
930 assert_eq!(f.significant_bits(), None);
931 assert_eq!(f.image_plane_count(), 2);
932 }
933
934 #[test]
935 fn significant_bits_on_frame_without_image_planes() {
936 // Like the palette, the record can be attached before pixel
937 // planes exist (encoder scaffolding).
938 let f = VideoFrame {
939 pts: None,
940 planes: Vec::new(),
941 }
942 .with_significant_bits(vec![12, 10, 10]);
943 assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
944 assert_eq!(f.image_plane_count(), 0);
945 assert!(f.image_planes().is_empty());
946 }
947
948 #[test]
949 fn side_channels_survive_clone_and_frame_wrapping() {
950 let f = gray_frame()
951 .with_palette(vec![1, 2, 3])
952 .with_significant_bits(vec![6]);
953 let cloned = f.clone();
954 assert_eq!(cloned.palette(), f.palette());
955 assert_eq!(cloned.significant_bits(), f.significant_bits());
956
957 let wrapped = Frame::Video(cloned);
958 assert_eq!(wrapped.pts(), Some(7));
959 if let Frame::Video(v) = wrapped {
960 assert_eq!(v.palette(), Some(&[1, 2, 3][..]));
961 assert_eq!(v.significant_bits(), Some(&[6][..]));
962 } else {
963 unreachable!("wrapped as Video above");
964 }
965 }
966
967 #[test]
968 fn frame_without_color_signal_or_layer_reports_none() {
969 let f = gray_frame();
970 assert_eq!(f.color_signal(), None);
971 assert_eq!(f.layer(), None);
972 assert_eq!(f.layer_or_base(), LayerIdentity::base());
973 assert_eq!(f.image_plane_count(), 1);
974 assert_eq!(f.planes.len(), 1);
975 }
976
977 #[test]
978 fn set_color_signal_round_trips_and_keeps_image_planes_intact() {
979 let mut f = gray_frame();
980 let sig = ColorSignal::from_code_points(9, 16, 9, true);
981 f.set_color_signal(sig);
982 assert_eq!(f.color_signal(), Some(sig));
983 assert_eq!(f.image_plane_count(), 1);
984 assert_eq!(f.image_planes()[0].data.len(), 8);
985 assert_eq!(f.planes.len(), 2);
986 assert_eq!(f.planes[1].stride, usize::MAX - 1);
987 assert_eq!(f.planes[1].data, sig.to_bytes());
988
989 // Replacement, not stacking.
990 f.set_color_signal(ColorSignal::srgb());
991 assert_eq!(f.planes.len(), 2);
992 assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
993
994 // Detach.
995 assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
996 assert_eq!(f.color_signal(), None);
997 assert_eq!(f.take_color_signal(), None);
998 assert_eq!(f.planes.len(), 1);
999 }
1000
1001 #[test]
1002 fn set_layer_round_trips_and_keeps_image_planes_intact() {
1003 let mut f = gray_frame();
1004 let id = LayerIdentity::new(1).with_view_id(1).with_access_unit(9);
1005 f.set_layer(id);
1006 assert_eq!(f.layer(), Some(id));
1007 assert_eq!(f.layer_or_base(), id);
1008 assert_eq!(f.image_plane_count(), 1);
1009 assert_eq!(f.planes.len(), 2);
1010 assert_eq!(f.planes[1].stride, usize::MAX - 2);
1011
1012 f.set_layer(LayerIdentity::new(2));
1013 assert_eq!(f.planes.len(), 2);
1014 assert_eq!(f.layer(), Some(LayerIdentity::new(2)));
1015
1016 assert_eq!(f.take_layer(), Some(LayerIdentity::new(2)));
1017 assert_eq!(f.layer(), None);
1018 assert_eq!(f.take_layer(), None);
1019 assert_eq!(f.planes.len(), 1);
1020 }
1021
1022 #[test]
1023 fn all_four_side_channels_compose_in_any_order() {
1024 let sig = ColorSignal::bt709_limited();
1025 let id = LayerIdentity::new(1).with_view_id(1);
1026 let mut f = gray_frame()
1027 .with_layer(id)
1028 .with_palette(vec![1, 2, 3])
1029 .with_color_signal(sig)
1030 .with_significant_bits(vec![8]);
1031 assert_eq!(f.planes.len(), 5);
1032 assert_eq!(f.image_plane_count(), 1);
1033 assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
1034 assert_eq!(f.significant_bits(), Some(&[8][..]));
1035 assert_eq!(f.color_signal(), Some(sig));
1036 assert_eq!(f.layer(), Some(id));
1037
1038 // Replacing one record from the middle of the run leaves the
1039 // others in place.
1040 f.set_color_signal(ColorSignal::srgb());
1041 assert_eq!(f.planes.len(), 5);
1042 assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
1043 assert_eq!(f.significant_bits(), Some(&[8][..]));
1044 assert_eq!(f.layer(), Some(id));
1045 assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1046
1047 // Detaching in an arbitrary order.
1048 assert_eq!(f.take_layer(), Some(id));
1049 assert_eq!(f.take_palette(), Some(vec![1, 2, 3]));
1050 assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1051 assert_eq!(f.significant_bits(), Some(&[8][..]));
1052 assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1053 assert_eq!(f.take_significant_bits(), Some(vec![8]));
1054 assert_eq!(f.planes.len(), 1);
1055 assert_eq!(f.image_plane_count(), 1);
1056 }
1057
1058 #[test]
1059 fn huge_stride_empty_plane_is_not_a_side_channel() {
1060 // Any stride in the reserved band with EMPTY data stays an
1061 // (degenerate) image plane — sentinels require non-empty data.
1062 let f = VideoFrame {
1063 pts: None,
1064 planes: vec![
1065 VideoPlane {
1066 stride: 4,
1067 data: vec![0u8; 8],
1068 },
1069 VideoPlane {
1070 stride: usize::MAX - 1,
1071 data: Vec::new(),
1072 },
1073 VideoPlane {
1074 stride: usize::MAX - 2,
1075 data: Vec::new(),
1076 },
1077 ],
1078 };
1079 assert_eq!(f.color_signal(), None);
1080 assert_eq!(f.layer(), None);
1081 assert_eq!(f.image_plane_count(), 3);
1082 }
1083
1084 #[test]
1085 fn largest_real_stride_is_still_an_image_plane() {
1086 // isize::MAX is the largest stride a one-row plane can have;
1087 // it must not be classified as a side-channel tag.
1088 let f = VideoFrame {
1089 pts: None,
1090 planes: vec![VideoPlane {
1091 stride: isize::MAX as usize,
1092 data: vec![0u8; 1],
1093 }],
1094 };
1095 assert_eq!(f.image_plane_count(), 1);
1096 assert_eq!(f.color_signal(), None);
1097 assert_eq!(f.layer(), None);
1098 }
1099
1100 #[test]
1101 fn malformed_short_records_read_as_none() {
1102 let f = VideoFrame {
1103 pts: None,
1104 planes: vec![
1105 VideoPlane {
1106 stride: 4,
1107 data: vec![0u8; 8],
1108 },
1109 VideoPlane {
1110 stride: usize::MAX - 1,
1111 data: vec![1, 2],
1112 },
1113 VideoPlane {
1114 stride: usize::MAX - 2,
1115 data: vec![0; 5],
1116 },
1117 ],
1118 };
1119 // They are side-channel-shaped (excluded from the image planes)
1120 // but decode to nothing.
1121 assert_eq!(f.image_plane_count(), 1);
1122 assert_eq!(f.color_signal(), None);
1123 assert_eq!(f.layer(), None);
1124 }
1125
1126 #[test]
1127 fn color_signal_and_layer_survive_clone_and_frame_wrapping() {
1128 let sig = ColorSignal::from_code_points(1, 13, 0, true);
1129 let id = LayerIdentity::new(1).with_view_id(1).with_access_unit(3);
1130 let f = gray_frame().with_color_signal(sig).with_layer(id);
1131 let cloned = f.clone();
1132 assert_eq!(cloned.color_signal(), Some(sig));
1133 assert_eq!(cloned.layer(), Some(id));
1134 let wrapped = Frame::Video(cloned);
1135 assert_eq!(wrapped.pts(), Some(7));
1136 if let Frame::Video(v) = wrapped {
1137 assert_eq!(v.color_signal(), Some(sig));
1138 assert_eq!(v.layer(), Some(id));
1139 } else {
1140 unreachable!("wrapped as Video above");
1141 }
1142 }
1143
1144 #[test]
1145 fn frame_without_palette_alpha_reads_opaque() {
1146 // No alpha record: palette_alpha is None and every covered
1147 // entry is opaque through palette_rgba.
1148 let f = gray_frame().with_palette(vec![1, 2, 3, 4, 5, 6]);
1149 assert_eq!(f.palette_alpha(), None);
1150 assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 255]));
1151 assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1152 // Beyond the palette: None, exactly like palette_rgb.
1153 assert_eq!(f.palette_rgba(2), None);
1154 assert_eq!(f.image_plane_count(), 1);
1155 assert_eq!(f.planes.len(), 2);
1156
1157 // No palette at all: no RGBA either.
1158 let g = gray_frame();
1159 assert_eq!(g.palette_alpha(), None);
1160 assert_eq!(g.palette_rgba(0), None);
1161 }
1162
1163 #[test]
1164 fn set_palette_alpha_round_trips_and_keeps_image_planes_intact() {
1165 // A GIF-style table: entry 2 is the transparent index, the
1166 // record covers only entries 0..=2 and the rest stay opaque.
1167 let mut f = gray_frame().with_palette(full_palette());
1168 f.set_palette_alpha(vec![255, 255, 0]);
1169
1170 assert_eq!(f.palette_alpha(), Some(&[255, 255, 0][..]));
1171 assert_eq!(f.palette_rgba(0), Some([0x00, 0xFF, 0x55, 255]));
1172 assert_eq!(f.palette_rgba(2), Some([0x02, 0xFD, 0x57, 0]));
1173 // Uncovered entries are opaque.
1174 assert_eq!(f.palette_rgba(3), Some([0x03, 0xFC, 0x56, 255]));
1175 assert_eq!(f.palette_rgba(255), Some([0xFF, 0x00, 0xAA, 255]));
1176 // Image-plane view is unchanged; the raw field sees two records.
1177 assert_eq!(f.image_plane_count(), 1);
1178 assert_eq!(f.image_planes()[0].data.len(), 8);
1179 assert_eq!(f.planes.len(), 3);
1180 assert_eq!(f.planes[2].stride, usize::MAX - 3);
1181 // The RGB record is untouched by the alpha record.
1182 assert_eq!(f.palette().map(<[u8]>::len), Some(768));
1183
1184 // Replacement, not stacking.
1185 f.set_palette_alpha(vec![128]);
1186 assert_eq!(f.planes.len(), 3);
1187 assert_eq!(f.palette_alpha(), Some(&[128][..]));
1188 assert_eq!(f.palette_rgba(0), Some([0x00, 0xFF, 0x55, 128]));
1189
1190 // Empty input removes the record; the palette stays.
1191 f.set_palette_alpha(Vec::new());
1192 assert_eq!(f.palette_alpha(), None);
1193 assert_eq!(f.planes.len(), 2);
1194 assert_eq!(f.palette().map(<[u8]>::len), Some(768));
1195
1196 // take_palette_alpha detaches and returns the bytes.
1197 f.set_palette_alpha(vec![0, 255]);
1198 assert_eq!(f.take_palette_alpha(), Some(vec![0, 255]));
1199 assert_eq!(f.palette_alpha(), None);
1200 assert_eq!(f.take_palette_alpha(), None);
1201 assert_eq!(f.planes.len(), 2);
1202 }
1203
1204 #[test]
1205 fn palette_alpha_longer_than_palette_is_malformed_and_reads_none() {
1206 // Two-entry palette, three alpha bytes: malformed → None, and
1207 // palette_rgba falls back to opaque.
1208 let mut f = gray_frame()
1209 .with_palette(vec![1, 2, 3, 4, 5, 6])
1210 .with_palette_alpha(vec![0, 0, 0]);
1211 assert_eq!(f.palette_alpha(), None);
1212 assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 255]));
1213 assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1214 // The entry is still side-channel-shaped: excluded from the
1215 // image planes, present in the raw field.
1216 assert_eq!(f.image_plane_count(), 1);
1217 assert_eq!(f.planes.len(), 3);
1218 // Exactly as long as the palette is fine.
1219 f.set_palette_alpha(vec![0, 7]);
1220 assert_eq!(f.palette_alpha(), Some(&[0, 7][..]));
1221 assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 7]));
1222 // Growing the palette afterwards makes a previously malformed
1223 // record valid: validation is at read time.
1224 f.set_palette_alpha(vec![0, 0, 0]);
1225 assert_eq!(f.palette_alpha(), None);
1226 f.set_palette(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
1227 assert_eq!(f.palette_alpha(), Some(&[0, 0, 0][..]));
1228 // Taking a malformed record removes it but reports None.
1229 f.set_palette(vec![1, 2, 3]);
1230 assert_eq!(f.palette_alpha(), None);
1231 assert_eq!(f.take_palette_alpha(), None);
1232 assert_eq!(f.planes.len(), 2);
1233 assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
1234 }
1235
1236 #[test]
1237 fn palette_alpha_without_palette_reads_none_in_either_order() {
1238 // Alpha attached first (encoder scaffolding): meaningless until
1239 // the palette arrives, then readable.
1240 let mut f = gray_frame().with_palette_alpha(vec![0]);
1241 assert_eq!(f.palette_alpha(), None);
1242 assert_eq!(f.palette_rgba(0), None);
1243 assert_eq!(f.image_plane_count(), 1);
1244 assert_eq!(f.planes.len(), 2);
1245 f.set_palette(vec![9, 9, 9]);
1246 assert_eq!(f.palette_alpha(), Some(&[0][..]));
1247 assert_eq!(f.palette_rgba(0), Some([9, 9, 9, 0]));
1248
1249 // Detaching the palette orphans the alpha record: None again,
1250 // still side-channel-shaped, and set_palette brings it back.
1251 assert_eq!(f.take_palette(), Some(vec![9, 9, 9]));
1252 assert_eq!(f.palette_alpha(), None);
1253 assert_eq!(f.planes.len(), 2);
1254 assert_eq!(f.image_plane_count(), 1);
1255 f.set_palette(vec![1, 1, 1]);
1256 assert_eq!(f.palette_alpha(), Some(&[0][..]));
1257 }
1258
1259 #[test]
1260 fn set_palette_rgba_writes_both_records() {
1261 let table = [[10, 20, 30, 0], [40, 50, 60, 255], [70, 80, 90, 128]];
1262 let mut f = gray_frame().with_palette_rgba(&table);
1263 assert_eq!(f.palette(), Some(&[10, 20, 30, 40, 50, 60, 70, 80, 90][..]));
1264 assert_eq!(f.palette_alpha(), Some(&[0, 255, 128][..]));
1265 for (i, e) in table.iter().enumerate() {
1266 assert_eq!(f.palette_rgba(i as u8), Some(*e));
1267 }
1268 assert_eq!(f.palette_rgba(3), None);
1269 assert_eq!(f.image_plane_count(), 1);
1270 assert_eq!(f.planes.len(), 3);
1271
1272 // Replaces an existing pair.
1273 f.set_palette_rgba(&[[1, 2, 3, 4]]);
1274 assert_eq!(f.planes.len(), 3);
1275 assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 4]));
1276 assert_eq!(f.palette_rgba(1), None);
1277
1278 // Empty clears both.
1279 f.set_palette_rgba(&[]);
1280 assert_eq!(f.palette(), None);
1281 assert_eq!(f.palette_alpha(), None);
1282 assert_eq!(f.planes.len(), 1);
1283 }
1284
1285 #[test]
1286 fn all_five_side_channels_compose_in_any_order() {
1287 let sig = ColorSignal::bt709_limited();
1288 let id = LayerIdentity::new(1).with_view_id(1);
1289 // Alpha before palette, palette in the middle, every other
1290 // record around them.
1291 let mut f = gray_frame()
1292 .with_palette_alpha(vec![0])
1293 .with_layer(id)
1294 .with_palette(vec![1, 2, 3, 4, 5, 6])
1295 .with_color_signal(sig)
1296 .with_significant_bits(vec![8]);
1297 assert_eq!(f.planes.len(), 6);
1298 assert_eq!(f.image_plane_count(), 1);
1299 assert_eq!(f.palette(), Some(&[1, 2, 3, 4, 5, 6][..]));
1300 assert_eq!(f.palette_alpha(), Some(&[0][..]));
1301 assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 0]));
1302 assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1303 assert_eq!(f.significant_bits(), Some(&[8][..]));
1304 assert_eq!(f.color_signal(), Some(sig));
1305 assert_eq!(f.layer(), Some(id));
1306
1307 // Replacing records from the middle of the run leaves the
1308 // others in place.
1309 f.set_palette_alpha(vec![255, 0]);
1310 f.set_color_signal(ColorSignal::srgb());
1311 f.set_palette(vec![7, 8, 9, 10, 11, 12]);
1312 assert_eq!(f.planes.len(), 6);
1313 assert_eq!(f.palette_rgba(1), Some([10, 11, 12, 0]));
1314 assert_eq!(f.significant_bits(), Some(&[8][..]));
1315 assert_eq!(f.layer(), Some(id));
1316 assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1317
1318 // Detaching in an arbitrary order.
1319 assert_eq!(f.take_layer(), Some(id));
1320 assert_eq!(f.take_palette_alpha(), Some(vec![255, 0]));
1321 assert_eq!(f.palette_rgba(1), Some([10, 11, 12, 255]));
1322 assert_eq!(f.take_palette(), Some(vec![7, 8, 9, 10, 11, 12]));
1323 assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1324 assert_eq!(f.take_significant_bits(), Some(vec![8]));
1325 assert_eq!(f.planes.len(), 1);
1326 assert_eq!(f.image_plane_count(), 1);
1327
1328 // Reverse construction order lands on the same answers.
1329 let g = gray_frame()
1330 .with_significant_bits(vec![8])
1331 .with_color_signal(sig)
1332 .with_palette(vec![1, 2, 3])
1333 .with_layer(id)
1334 .with_palette_alpha(vec![9]);
1335 assert_eq!(g.planes.len(), 6);
1336 assert_eq!(g.image_plane_count(), 1);
1337 assert_eq!(g.palette_rgba(0), Some([1, 2, 3, 9]));
1338 assert_eq!(g.significant_bits(), Some(&[8][..]));
1339 assert_eq!(g.color_signal(), Some(sig));
1340 assert_eq!(g.layer(), Some(id));
1341 }
1342
1343 #[test]
1344 fn palette_alpha_tag_with_empty_data_is_not_a_record() {
1345 let f = VideoFrame {
1346 pts: None,
1347 planes: vec![
1348 VideoPlane {
1349 stride: 4,
1350 data: vec![0u8; 8],
1351 },
1352 VideoPlane {
1353 stride: usize::MAX - 3,
1354 data: Vec::new(),
1355 },
1356 ],
1357 };
1358 assert_eq!(f.palette_alpha(), None);
1359 assert_eq!(f.image_plane_count(), 2);
1360 }
1361
1362 #[test]
1363 fn palette_alpha_survives_clone_and_frame_wrapping() {
1364 let f = gray_frame().with_palette_rgba(&[[1, 2, 3, 0], [4, 5, 6, 255]]);
1365 let cloned = f.clone();
1366 assert_eq!(cloned.palette_alpha(), f.palette_alpha());
1367 let wrapped = Frame::Video(cloned);
1368 assert_eq!(wrapped.pts(), Some(7));
1369 if let Frame::Video(v) = wrapped {
1370 assert_eq!(v.palette_rgba(0), Some([1, 2, 3, 0]));
1371 assert_eq!(v.palette_rgba(1), Some([4, 5, 6, 255]));
1372 } else {
1373 unreachable!("wrapped as Video above");
1374 }
1375 }
1376
1377 #[test]
1378 fn frame_without_blobs_reads_empty() {
1379 let f = gray_frame();
1380 assert!(f.blobs().is_empty());
1381 assert_eq!(f.blob(&BlobKind::ICC), None);
1382 assert_eq!(f.image_plane_count(), 1);
1383 assert_eq!(f.planes.len(), 1);
1384 }
1385
1386 #[test]
1387 fn set_blobs_round_trips_and_keeps_image_planes_intact() {
1388 let icc = MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3, 4]);
1389 let exif = MetadataBlob::new(BlobKind::EXIF, b"II*\0".to_vec());
1390 let mut f = gray_frame();
1391 f.set_blobs(vec![icc.clone(), exif.clone()]);
1392
1393 assert_eq!(f.blobs(), vec![icc.clone(), exif.clone()]);
1394 assert_eq!(f.blob(&BlobKind::ICC), Some(icc.clone()));
1395 assert_eq!(f.blob(&BlobKind::EXIF), Some(exif.clone()));
1396 assert_eq!(f.blob(&BlobKind::XMP), None);
1397 // Image-plane view is unchanged; the raw field sees one record
1398 // holding the whole list, tagged usize::MAX - 4.
1399 assert_eq!(f.image_plane_count(), 1);
1400 assert_eq!(f.image_planes()[0].data.len(), 8);
1401 assert_eq!(f.planes.len(), 2);
1402 assert_eq!(f.planes[1].stride, usize::MAX - 4);
1403 assert_eq!(f.planes[1].data, encode_blobs(&[icc.clone(), exif.clone()]));
1404
1405 // Replacement, not stacking.
1406 f.set_blobs(vec![exif.clone()]);
1407 assert_eq!(f.planes.len(), 2);
1408 assert_eq!(f.blobs(), vec![exif.clone()]);
1409
1410 // Empty input removes the record entirely.
1411 f.set_blobs(Vec::new());
1412 assert!(f.blobs().is_empty());
1413 assert_eq!(f.planes.len(), 1);
1414
1415 // take_blobs detaches and returns the list.
1416 f.set_blobs(vec![icc.clone()]);
1417 assert_eq!(f.take_blobs(), vec![icc]);
1418 assert!(f.blobs().is_empty());
1419 assert!(f.take_blobs().is_empty());
1420 assert_eq!(f.planes.len(), 1);
1421 }
1422
1423 #[test]
1424 fn push_blob_appends_in_order_and_with_blob_chains() {
1425 let a = MetadataBlob::new(BlobKind::COVER_ART, vec![1]).with_mime("image/jpeg");
1426 let b = MetadataBlob::new(BlobKind::COVER_ART, vec![2]).with_mime("image/png");
1427 let c = MetadataBlob::new(BlobKind::custom("exr-attributes"), vec![3]);
1428 let mut f = gray_frame().with_blob(a.clone());
1429 f.push_blob(b.clone());
1430 f.push_blob(c.clone());
1431 // Same kind twice: both kept, first-of-kind lookup is stable.
1432 assert_eq!(f.blobs(), vec![a.clone(), b.clone(), c.clone()]);
1433 assert_eq!(f.blob(&BlobKind::COVER_ART), Some(a));
1434 assert_eq!(f.blob(&BlobKind::custom("exr-attributes")), Some(c));
1435 // Still one record.
1436 assert_eq!(f.planes.len(), 2);
1437 assert_eq!(f.image_plane_count(), 1);
1438 }
1439
1440 #[test]
1441 fn malformed_blobs_record_reads_empty_and_is_replaced_by_push() {
1442 let mut f = VideoFrame {
1443 pts: None,
1444 planes: vec![
1445 VideoPlane {
1446 stride: 4,
1447 data: vec![0u8; 8],
1448 },
1449 VideoPlane {
1450 stride: usize::MAX - 4,
1451 data: vec![9, 9, 9],
1452 },
1453 ],
1454 };
1455 // Side-channel-shaped (excluded from the image planes) but
1456 // decodes to nothing.
1457 assert_eq!(f.image_plane_count(), 1);
1458 assert!(f.blobs().is_empty());
1459 assert_eq!(f.blob(&BlobKind::ICC), None);
1460 // push replaces the unreadable record with the single new blob.
1461 let icc = MetadataBlob::new(BlobKind::ICC, vec![7]);
1462 f.push_blob(icc.clone());
1463 assert_eq!(f.planes.len(), 2);
1464 assert_eq!(f.blobs(), vec![icc]);
1465 // take on a malformed record removes it and reports empty.
1466 f.planes[1].data = vec![9, 9, 9];
1467 assert!(f.take_blobs().is_empty());
1468 assert_eq!(f.planes.len(), 1);
1469 }
1470
1471 #[test]
1472 fn blobs_tag_with_empty_data_is_not_a_record() {
1473 let f = VideoFrame {
1474 pts: None,
1475 planes: vec![
1476 VideoPlane {
1477 stride: 4,
1478 data: vec![0u8; 8],
1479 },
1480 VideoPlane {
1481 stride: usize::MAX - 4,
1482 data: Vec::new(),
1483 },
1484 ],
1485 };
1486 assert!(f.blobs().is_empty());
1487 assert_eq!(f.image_plane_count(), 2);
1488 }
1489
1490 #[test]
1491 fn all_six_side_channels_compose_in_any_order() {
1492 let sig = ColorSignal::bt709_limited();
1493 let id = LayerIdentity::new(1).with_view_id(1);
1494 let icc = MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3]);
1495 let xmp = MetadataBlob::new(BlobKind::XMP, b"<x/>".to_vec());
1496 let mut f = gray_frame()
1497 .with_blob(icc.clone())
1498 .with_palette_alpha(vec![0])
1499 .with_layer(id)
1500 .with_palette(vec![1, 2, 3, 4, 5, 6])
1501 .with_color_signal(sig)
1502 .with_significant_bits(vec![8]);
1503 assert_eq!(f.planes.len(), 7);
1504 assert_eq!(f.image_plane_count(), 1);
1505 assert_eq!(f.blobs(), vec![icc.clone()]);
1506 assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 0]));
1507 assert_eq!(f.significant_bits(), Some(&[8][..]));
1508 assert_eq!(f.color_signal(), Some(sig));
1509 assert_eq!(f.layer(), Some(id));
1510
1511 // Growing the blobs list from the middle of the run leaves the
1512 // others in place and keeps one record.
1513 f.push_blob(xmp.clone());
1514 f.set_color_signal(ColorSignal::srgb());
1515 assert_eq!(f.planes.len(), 7);
1516 assert_eq!(f.blobs(), vec![icc.clone(), xmp.clone()]);
1517 assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1518 assert_eq!(f.layer(), Some(id));
1519 assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1520
1521 // Detaching in an arbitrary order.
1522 assert_eq!(f.take_palette(), Some(vec![1, 2, 3, 4, 5, 6]));
1523 assert_eq!(f.blobs(), vec![icc.clone(), xmp.clone()]);
1524 assert_eq!(f.take_blobs(), vec![icc, xmp]);
1525 assert_eq!(f.take_layer(), Some(id));
1526 assert_eq!(f.take_palette_alpha(), None); // orphaned → None, removed
1527 assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1528 assert_eq!(f.take_significant_bits(), Some(vec![8]));
1529 assert_eq!(f.planes.len(), 1);
1530 assert_eq!(f.image_plane_count(), 1);
1531 }
1532
1533 #[test]
1534 fn blobs_survive_clone_and_frame_wrapping() {
1535 let exif = MetadataBlob::new(BlobKind::EXIF, b"MM\0*".to_vec());
1536 let f = gray_frame().with_blob(exif.clone());
1537 let cloned = f.clone();
1538 assert_eq!(cloned.blobs(), f.blobs());
1539 let wrapped = Frame::Video(cloned);
1540 assert_eq!(wrapped.pts(), Some(7));
1541 if let Frame::Video(v) = wrapped {
1542 assert_eq!(v.blob(&BlobKind::EXIF), Some(exif));
1543 } else {
1544 unreachable!("wrapped as Video above");
1545 }
1546 }
1547
1548 #[test]
1549 fn palette_survives_clone_and_frame_wrapping() {
1550 let f = gray_frame().with_palette(full_palette());
1551 let cloned = f.clone();
1552 assert_eq!(cloned.palette(), f.palette());
1553
1554 // Through the Frame enum, pts and palette both survive.
1555 let wrapped = Frame::Video(cloned);
1556 assert_eq!(wrapped.pts(), Some(7));
1557 if let Frame::Video(v) = wrapped {
1558 assert_eq!(v.palette().map(<[u8]>::len), Some(768));
1559 } else {
1560 unreachable!("wrapped as Video above");
1561 }
1562 }
1563}