Skip to main content

gamut_webp/vp8/
header.rs

1//! VP8 key-frame frame header (RFC 6386 §9, §19.1–§19.2): the uncompressed 10-byte chunk (frame tag,
2//! start code, dimensions) plus the boolean-coded header fields (color space, loop filter, partition
3//! count, quantizer indices, coefficient-probability updates).
4//!
5//! gamut codes key frames only (`key_frame` bit = 0). The header carries the `update_segmentation()`
6//! record (per-segment quantizer/filter adjustments + the segment-id tree probs), the loop-filter
7//! parameters, the partition count, the quantizer indices, and the coefficient-probability-update
8//! record — all parsed by the decoder so it tracks the working [`CoeffProbs`]. Per-macroblock
9//! loop-filter adjustments (`mb_lf_adjustments`) are the one body still rejected. Tracked in
10//! `../STATUS.md` section H.
11
12use gamut_core::{Error, Result};
13
14use super::bool_coder::{BoolDecoder, BoolEncoder};
15use super::tokens::{self, CoeffProbs, DEFAULT_COEFF_PROBS};
16
17/// The 3-byte start code that follows the frame tag in a VP8 key frame (RFC 6386 §9.1).
18pub const VP8_KEYFRAME_START_CODE: [u8; 3] = [0x9d, 0x01, 0x2a];
19
20/// Length in bytes of a key-frame's uncompressed data chunk (RFC 6386 §9.1).
21pub const UNCOMPRESSED_CHUNK_LEN: usize = 10;
22
23/// Per-segment adjustment state (RFC 6386 §9.3, §10). Still images usually leave this disabled.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub struct Segmentation {
26    /// Whether segmentation is enabled for the frame.
27    pub enabled: bool,
28    /// Whether the per-macroblock segment map is (re)transmitted this frame.
29    pub update_map: bool,
30    /// Feature-data mode: `true` = absolute values, `false` = deltas from the frame base
31    /// (`segment_feature_mode`).
32    pub abs_delta: bool,
33    /// Per-segment quantizer adjustment (absolute or delta, per [`abs_delta`](Self::abs_delta)).
34    pub quantizer: [i8; 4],
35    /// Per-segment loop-filter-level adjustment.
36    pub filter_strength: [i8; 4],
37    /// Branch probabilities for the segment-id tree (default 255 each).
38    pub tree_probs: [u8; 3],
39}
40
41/// Loop-filter header parameters (RFC 6386 §9.4).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub struct LoopFilterParams {
44    /// `true` selects the simple filter; `false` selects the normal filter.
45    pub simple: bool,
46    /// Base filter level (`0..=63`); 0 disables the loop filter.
47    pub level: u8,
48    /// Sharpness level (`0..=7`).
49    pub sharpness: u8,
50}
51
52/// Dequantization indices (RFC 6386 §9.6): a base AC index plus a signed delta per plane/coefficient.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub struct QuantIndices {
55    /// Base quantizer index (the Y1 AC index, `0..=127`).
56    pub y_ac: u8,
57    /// Y1 DC index delta.
58    pub y_dc_delta: i8,
59    /// Y2 (WHT) DC index delta.
60    pub y2_dc_delta: i8,
61    /// Y2 (WHT) AC index delta.
62    pub y2_ac_delta: i8,
63    /// Chroma DC index delta.
64    pub uv_dc_delta: i8,
65    /// Chroma AC index delta.
66    pub uv_ac_delta: i8,
67}
68
69/// A VP8 key-frame header (RFC 6386 §9). Intra/key-frame fields only — gamut codes no inter-frame
70/// state.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Vp8FrameHeader {
73    /// Frame width in pixels (the 14-bit field of the uncompressed chunk).
74    pub width: u16,
75    /// Frame height in pixels (the 14-bit field of the uncompressed chunk).
76    pub height: u16,
77    /// Horizontal upscaling hint (2 bits; 0 = none).
78    pub horizontal_scale: u8,
79    /// Vertical upscaling hint (2 bits; 0 = none).
80    pub vertical_scale: u8,
81    /// Bitstream version (3 bits; selects loop-filter / reconstruction variants).
82    pub version: u8,
83    /// Color space (0 = YUV per BT.601; 1 is reserved).
84    pub color_space: u8,
85    /// Whether pixel clamping is required (the `clamping_type` flag).
86    pub clamp_required: bool,
87    /// Segmentation state (§9.3).
88    pub segmentation: Segmentation,
89    /// Loop-filter header (§9.4).
90    pub loop_filter: LoopFilterParams,
91    /// Number of DCT-coefficient token partitions (1, 2, 4, or 8) (§9.5).
92    pub token_partitions: u8,
93    /// Dequantization indices (§9.6).
94    pub quant: QuantIndices,
95    /// Whether token-probability updates persist past this frame (§9.11).
96    pub refresh_entropy_probs: bool,
97    /// Whether macroblocks may signal that they carry no non-zero coefficients (§9.10).
98    pub mb_no_skip_coeff: bool,
99    /// Probability that a macroblock is *not* skipped (only meaningful if `mb_no_skip_coeff`) (§9.10).
100    pub prob_skip_false: u8,
101}
102
103/// The parsed uncompressed data chunk (RFC 6386 §9.1, §19.1).
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct UncompressedChunk {
106    /// Whether this is a key frame (the frame-tag bit is `0` for key frames).
107    pub is_key_frame: bool,
108    /// Bitstream version (3 bits).
109    pub version: u8,
110    /// Whether the frame is meant to be displayed.
111    pub show_frame: bool,
112    /// Size in bytes of the first (control) partition, excluding this chunk.
113    pub first_partition_size: u32,
114    /// Frame width in pixels (14 bits).
115    pub width: u16,
116    /// Frame height in pixels (14 bits).
117    pub height: u16,
118    /// Horizontal upscaling hint (2 bits).
119    pub horizontal_scale: u8,
120    /// Vertical upscaling hint (2 bits).
121    pub vertical_scale: u8,
122}
123
124/// `log2` of a token-partition count `{1, 2, 4, 8}`.
125fn log2_partitions(count: u8) -> u32 {
126    debug_assert!(
127        matches!(count, 1 | 2 | 4 | 8),
128        "token partition count must be 1, 2, 4, or 8"
129    );
130    u32::from(count).trailing_zeros()
131}
132
133/// Writes the 10-byte uncompressed data chunk for a key frame (RFC 6386 §19.1) to `out`:
134/// the frame tag (with `first_partition_size` and `show_frame = 1`), the start code, and the
135/// little-endian width/height + scale codes.
136pub fn write_uncompressed_chunk(
137    header: &Vp8FrameHeader,
138    first_partition_size: u32,
139    out: &mut Vec<u8>,
140) {
141    // key_frame bit (bit 0) = 0; version in bits 1-3; show_frame = 1 in bit 4; size in bits 5-23.
142    let tag = (u32::from(header.version) << 1) | (1 << 4) | (first_partition_size << 5);
143    out.push((tag & 0xff) as u8);
144    out.push(((tag >> 8) & 0xff) as u8);
145    out.push(((tag >> 16) & 0xff) as u8);
146    out.extend_from_slice(&VP8_KEYFRAME_START_CODE);
147    let h = u32::from(header.width) | (u32::from(header.horizontal_scale) << 14);
148    out.push((h & 0xff) as u8);
149    out.push(((h >> 8) & 0xff) as u8);
150    let v = u32::from(header.height) | (u32::from(header.vertical_scale) << 14);
151    out.push((v & 0xff) as u8);
152    out.push(((v >> 8) & 0xff) as u8);
153}
154
155/// Parses the uncompressed data chunk (RFC 6386 §19.1).
156///
157/// # Errors
158///
159/// Returns [`Error::InvalidInput`] if the data is too short or the key-frame start code is wrong, or
160/// [`Error::Unsupported`] for an inter frame (gamut codes key frames only).
161pub fn read_uncompressed_chunk(data: &[u8]) -> Result<UncompressedChunk> {
162    if data.len() < 3 {
163        return Err(Error::InvalidInput("VP8: truncated frame tag"));
164    }
165    let tag = u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16);
166    let is_key_frame = (tag & 1) == 0;
167    if !is_key_frame {
168        return Err(Error::Unsupported(
169            "VP8: only intra key frames are supported",
170        ));
171    }
172    if data.len() < UNCOMPRESSED_CHUNK_LEN {
173        return Err(Error::InvalidInput("VP8: truncated key-frame header"));
174    }
175    if data[3..6] != VP8_KEYFRAME_START_CODE {
176        return Err(Error::InvalidInput("VP8: bad key-frame start code"));
177    }
178    let hsc = u32::from(data[6]) | (u32::from(data[7]) << 8);
179    let vsc = u32::from(data[8]) | (u32::from(data[9]) << 8);
180    Ok(UncompressedChunk {
181        is_key_frame,
182        version: ((tag >> 1) & 0x7) as u8,
183        show_frame: (tag >> 4) & 1 != 0,
184        first_partition_size: (tag >> 5) & 0x7_FFFF,
185        width: (hsc & 0x3FFF) as u16,
186        horizontal_scale: (hsc >> 14) as u8,
187        height: (vsc & 0x3FFF) as u16,
188        vertical_scale: (vsc >> 14) as u8,
189    })
190}
191
192/// Writes a signed quantizer-index delta as `present` flag + magnitude `L(4)` + sign (RFC 6386 §19.2).
193fn write_delta(enc: &mut BoolEncoder, delta: i8) {
194    if delta == 0 {
195        enc.put_flag(false);
196    } else {
197        enc.put_flag(true);
198        enc.put_literal(u32::from(delta.unsigned_abs()), 4);
199        enc.put_flag(delta < 0);
200    }
201}
202
203/// Reads a signed quantizer-index delta (RFC 6386 §19.2).
204fn read_delta(dec: &mut BoolDecoder) -> i8 {
205    if dec.get_flag() {
206        let magnitude = dec.get_literal(4) as i8;
207        if dec.get_flag() {
208            -magnitude
209        } else {
210            magnitude
211        }
212    } else {
213        0
214    }
215}
216
217/// Writes `quant_indices()` (RFC 6386 §19.2): the base AC index then the five per-plane deltas.
218fn write_quant_indices(enc: &mut BoolEncoder, quant: &QuantIndices) {
219    enc.put_literal(u32::from(quant.y_ac), 7);
220    write_delta(enc, quant.y_dc_delta);
221    write_delta(enc, quant.y2_dc_delta);
222    write_delta(enc, quant.y2_ac_delta);
223    write_delta(enc, quant.uv_dc_delta);
224    write_delta(enc, quant.uv_ac_delta);
225}
226
227/// Reads `quant_indices()` (RFC 6386 §19.2).
228fn read_quant_indices(dec: &mut BoolDecoder) -> QuantIndices {
229    QuantIndices {
230        y_ac: dec.get_literal(7) as u8,
231        y_dc_delta: read_delta(dec),
232        y2_dc_delta: read_delta(dec),
233        y2_ac_delta: read_delta(dec),
234        uv_dc_delta: read_delta(dec),
235        uv_ac_delta: read_delta(dec),
236    }
237}
238
239/// Writes the `update_segmentation()` record (RFC 6386 §19.2): the map-update flag, optional feature
240/// data (per-segment quantizer and loop-filter adjustments), and optional segment-id tree probs.
241fn write_update_segmentation(enc: &mut BoolEncoder, seg: &Segmentation) {
242    enc.put_flag(seg.update_map);
243    let update_data = seg.abs_delta || seg.quantizer != [0; 4] || seg.filter_strength != [0; 4];
244    enc.put_flag(update_data);
245    if update_data {
246        enc.put_flag(seg.abs_delta); // segment_feature_mode
247        for q in seg.quantizer {
248            write_segment_feature(enc, q, 7);
249        }
250        for f in seg.filter_strength {
251            write_segment_feature(enc, f, 6);
252        }
253    }
254    if seg.update_map {
255        for p in seg.tree_probs {
256            if p == 255 {
257                enc.put_flag(false); // segment_prob_update: keep the default 255
258            } else {
259                enc.put_flag(true);
260                enc.put_literal(u32::from(p), 8);
261            }
262        }
263    }
264}
265
266/// Writes one signed segment-feature value as `present` flag + magnitude + sign (RFC 6386 §19.2).
267fn write_segment_feature(enc: &mut BoolEncoder, value: i8, bits: u32) {
268    if value == 0 {
269        enc.put_flag(false);
270    } else {
271        enc.put_flag(true);
272        enc.put_literal(u32::from(value.unsigned_abs()), bits);
273        enc.put_flag(value < 0);
274    }
275}
276
277/// Reads the `update_segmentation()` record, mirroring [`write_update_segmentation`].
278fn read_update_segmentation(dec: &mut BoolDecoder) -> Segmentation {
279    let update_map = dec.get_flag();
280    let mut seg = Segmentation {
281        enabled: true,
282        update_map,
283        tree_probs: [255; 3],
284        ..Segmentation::default()
285    };
286    if dec.get_flag() {
287        seg.abs_delta = dec.get_flag();
288        for q in &mut seg.quantizer {
289            *q = read_segment_feature(dec, 7);
290        }
291        for f in &mut seg.filter_strength {
292            *f = read_segment_feature(dec, 6);
293        }
294    }
295    if update_map {
296        for p in &mut seg.tree_probs {
297            if dec.get_flag() {
298                *p = dec.get_literal(8) as u8;
299            }
300        }
301    }
302    seg
303}
304
305/// Reads one signed segment-feature value, mirroring [`write_segment_feature`].
306fn read_segment_feature(dec: &mut BoolDecoder, bits: u32) -> i8 {
307    if dec.get_flag() {
308        let mag = dec.get_literal(bits) as i8;
309        if dec.get_flag() { -mag } else { mag }
310    } else {
311        0
312    }
313}
314
315/// Writes the boolean-coded key-frame header (RFC 6386 §19.2) into the first (control) partition's
316/// encoder `enc`, leaving it open for the per-macroblock records that follow. Loop-filter adjustments
317/// stay disabled (P14 territory); segmentation and coefficient-probability updates are emitted as
318/// configured.
319pub fn write_frame_header(enc: &mut BoolEncoder, header: &Vp8FrameHeader) {
320    enc.put_literal(u32::from(header.color_space), 1);
321    enc.put_flag(!header.clamp_required); // clamping_type: 1 = no clamp needed
322    enc.put_flag(header.segmentation.enabled);
323    if header.segmentation.enabled {
324        write_update_segmentation(enc, &header.segmentation);
325    }
326    enc.put_flag(header.loop_filter.simple); // filter_type
327    enc.put_literal(u32::from(header.loop_filter.level), 6);
328    enc.put_literal(u32::from(header.loop_filter.sharpness), 3);
329    enc.put_flag(false); // loop_filter_adj_enable (body in P11)
330    enc.put_literal(log2_partitions(header.token_partitions), 2);
331    write_quant_indices(enc, &header.quant);
332    enc.put_flag(header.refresh_entropy_probs);
333    tokens::write_coeff_prob_updates(enc, &DEFAULT_COEFF_PROBS, &DEFAULT_COEFF_PROBS);
334    enc.put_flag(header.mb_no_skip_coeff);
335    if header.mb_no_skip_coeff {
336        enc.put_literal(u32::from(header.prob_skip_false), 8);
337    }
338}
339
340/// Reads the boolean-coded key-frame header (RFC 6386 §19.2) from the control-partition decoder `dec`,
341/// returning the header and the working coefficient-probability table after any updates.
342///
343/// # Errors
344///
345/// Returns [`Error::Unsupported`] for per-macroblock loop-filter adjustments (those land with the
346/// remaining header features).
347pub fn read_frame_header(
348    chunk: &UncompressedChunk,
349    dec: &mut BoolDecoder,
350) -> Result<(Vp8FrameHeader, CoeffProbs)> {
351    let color_space = dec.get_literal(1) as u8;
352    let clamp_required = !dec.get_flag();
353    let segmentation = if dec.get_flag() {
354        read_update_segmentation(dec)
355    } else {
356        Segmentation::default()
357    };
358    let loop_filter = LoopFilterParams {
359        simple: dec.get_flag(),
360        level: dec.get_literal(6) as u8,
361        sharpness: dec.get_literal(3) as u8,
362    };
363    if dec.get_flag() {
364        return Err(Error::Unsupported(
365            "VP8: loop-filter adjustments not yet supported",
366        ));
367    }
368    let token_partitions = 1u8 << dec.get_literal(2);
369    let quant = read_quant_indices(dec);
370    let refresh_entropy_probs = dec.get_flag();
371    let mut coeff_probs = DEFAULT_COEFF_PROBS;
372    tokens::read_coeff_prob_updates(dec, &mut coeff_probs);
373    let mb_no_skip_coeff = dec.get_flag();
374    let prob_skip_false = if mb_no_skip_coeff {
375        dec.get_literal(8) as u8
376    } else {
377        0
378    };
379    let header = Vp8FrameHeader {
380        width: chunk.width,
381        height: chunk.height,
382        horizontal_scale: chunk.horizontal_scale,
383        vertical_scale: chunk.vertical_scale,
384        version: chunk.version,
385        color_space,
386        clamp_required,
387        segmentation,
388        loop_filter,
389        token_partitions,
390        quant,
391        refresh_entropy_probs,
392        mb_no_skip_coeff,
393        prob_skip_false,
394    };
395    Ok((header, coeff_probs))
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    fn sample_header() -> Vp8FrameHeader {
403        Vp8FrameHeader {
404            width: 176,
405            height: 144,
406            horizontal_scale: 0,
407            vertical_scale: 0,
408            version: 0,
409            color_space: 0,
410            clamp_required: true,
411            segmentation: Segmentation::default(),
412            loop_filter: LoopFilterParams {
413                simple: false,
414                level: 0,
415                sharpness: 0,
416            },
417            token_partitions: 1,
418            quant: QuantIndices::default(),
419            refresh_entropy_probs: true,
420            mb_no_skip_coeff: false,
421            prob_skip_false: 0,
422        }
423    }
424
425    /// Encodes a header to a complete (header-only) VP8 bitstream and decodes it back.
426    fn roundtrip(header: &Vp8FrameHeader) {
427        let mut enc = BoolEncoder::new();
428        write_frame_header(&mut enc, header);
429        let part0 = enc.finish();
430        let mut stream = Vec::new();
431        write_uncompressed_chunk(header, part0.len() as u32, &mut stream);
432        stream.extend_from_slice(&part0);
433
434        let chunk = read_uncompressed_chunk(&stream).expect("chunk");
435        assert!(chunk.is_key_frame);
436        assert_eq!((chunk.width, chunk.height), (header.width, header.height));
437        assert_eq!(chunk.first_partition_size as usize, part0.len());
438
439        let end = UNCOMPRESSED_CHUNK_LEN + chunk.first_partition_size as usize;
440        let mut dec = BoolDecoder::new(&stream[UNCOMPRESSED_CHUNK_LEN..end]);
441        let (decoded, probs) = read_frame_header(&chunk, &mut dec).expect("header");
442        assert_eq!(&decoded, header);
443        assert_eq!(
444            probs, DEFAULT_COEFF_PROBS,
445            "minimal header carries no prob updates"
446        );
447    }
448
449    #[test]
450    fn minimal_header_round_trips() {
451        roundtrip(&sample_header());
452    }
453
454    #[test]
455    fn dimensions_and_scale_round_trip() {
456        for (w, h, hs, vs) in [
457            (1u16, 1u16, 0u8, 0u8),
458            (16, 16, 0, 0),
459            (16383, 1, 3, 0),
460            (17, 9, 1, 2),
461        ] {
462            let mut header = sample_header();
463            header.width = w;
464            header.height = h;
465            header.horizontal_scale = hs;
466            header.vertical_scale = vs;
467            roundtrip(&header);
468        }
469    }
470
471    #[test]
472    fn quant_filter_and_flags_round_trip() {
473        let mut header = sample_header();
474        header.quant = QuantIndices {
475            y_ac: 100,
476            y_dc_delta: 7,
477            y2_dc_delta: -8,
478            y2_ac_delta: 15,
479            uv_dc_delta: -1,
480            uv_ac_delta: 0,
481        };
482        header.loop_filter = LoopFilterParams {
483            simple: true,
484            level: 47,
485            sharpness: 5,
486        };
487        header.color_space = 1;
488        header.clamp_required = false;
489        header.refresh_entropy_probs = false;
490        header.version = 3;
491        roundtrip(&header);
492    }
493
494    #[test]
495    fn skip_probability_round_trips() {
496        let mut header = sample_header();
497        header.mb_no_skip_coeff = true;
498        header.prob_skip_false = 210;
499        roundtrip(&header);
500    }
501
502    #[test]
503    fn partition_counts_round_trip() {
504        for count in [1u8, 2, 4, 8] {
505            let mut header = sample_header();
506            header.token_partitions = count;
507            roundtrip(&header);
508        }
509    }
510
511    #[test]
512    fn rejects_inter_frame_and_bad_start_code() {
513        // Inter frame: frame-tag bit 0 set.
514        assert!(matches!(
515            read_uncompressed_chunk(&[0x01, 0, 0, 0x9d, 0x01, 0x2a, 0, 0, 0, 0]),
516            Err(Error::Unsupported(_))
517        ));
518        // Key frame with a corrupted start code.
519        assert!(matches!(
520            read_uncompressed_chunk(&[0x00, 0, 0, 0x9d, 0x01, 0x2b, 16, 0, 16, 0]),
521            Err(Error::InvalidInput(_))
522        ));
523        // Truncated.
524        assert!(read_uncompressed_chunk(&[0x00, 0, 0]).is_err());
525    }
526
527    #[test]
528    fn rejects_unsupported_lf_adjust() {
529        let chunk = UncompressedChunk {
530            is_key_frame: true,
531            version: 0,
532            show_frame: true,
533            first_partition_size: 0,
534            width: 16,
535            height: 16,
536            horizontal_scale: 0,
537            vertical_scale: 0,
538        };
539        // color_space, clamping, segmentation=0, filter_type, level(6), sharpness(3), lf_adj = 1.
540        let mut lf = BoolEncoder::new();
541        lf.put_literal(0, 1);
542        lf.put_flag(true);
543        lf.put_flag(false);
544        lf.put_flag(false);
545        lf.put_literal(0, 6);
546        lf.put_literal(0, 3);
547        lf.put_flag(true);
548        let bytes = lf.finish();
549        assert!(matches!(
550            read_frame_header(&chunk, &mut BoolDecoder::new(&bytes)),
551            Err(Error::Unsupported(_))
552        ));
553    }
554
555    #[test]
556    fn segmentation_round_trips() {
557        let mut header = sample_header();
558        header.segmentation = Segmentation {
559            enabled: true,
560            update_map: true,
561            abs_delta: false,
562            quantizer: [-8, -2, 5, 12],
563            filter_strength: [0; 4],
564            tree_probs: [120, 200, 64],
565        };
566        let chunk = UncompressedChunk {
567            is_key_frame: true,
568            version: 0,
569            show_frame: true,
570            first_partition_size: 0,
571            width: header.width,
572            height: header.height,
573            horizontal_scale: 0,
574            vertical_scale: 0,
575        };
576        let mut enc = BoolEncoder::new();
577        write_frame_header(&mut enc, &header);
578        let bytes = enc.finish();
579        let (decoded, _) = read_frame_header(&chunk, &mut BoolDecoder::new(&bytes)).unwrap();
580        assert_eq!(decoded.segmentation, header.segmentation);
581    }
582}