Skip to main content

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}