Skip to main content

otf_pixels_codec_avif/av1/
seq.rs

1//! The AV1 sequence header (spec §5.5).
2//!
3//! One sequence header governs every frame that follows it: the maximum frame
4//! size, which coding tools are enabled, the superblock size, and the colour
5//! configuration (bit depth, subsampling, range, matrix). In AVIF it arrives in
6//! the `av1C` box's configuration OBUs, and the frame header cannot be read
7//! without it.
8//!
9//! The whole header is parsed even though a still image leaves most of its
10//! inter-prediction switches off: the fields sit in a fixed order with no
11//! length prefix, so a field skipped is every field after it misread. What the
12//! still-picture restriction buys is not a shorter parse but the guarantee that
13//! the *values* land in their defaults — order hint off, force-integer-MV
14//! selected — which the frame header then relies on.
15
16use super::bits::BitReader;
17use otf_pixels_core::{PixelsError, Result};
18
19/// `SELECT_SCREEN_CONTENT_TOOLS` / `SELECT_INTEGER_MV` (§3): the sentinel that
20/// defers the choice to each frame header.
21const SELECT: u8 = 2;
22
23// Colour code points that select the sRGB "identity matrix" fast path (§5.5.2).
24const CP_BT_709: u8 = 1;
25const TC_SRGB: u8 = 13;
26const MC_IDENTITY: u8 = 0;
27const CP_UNSPECIFIED: u8 = 2;
28const TC_UNSPECIFIED: u8 = 2;
29const MC_UNSPECIFIED: u8 = 2;
30
31/// One operating point (§5.5.1). A still image has exactly one and decodes it.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct OperatingPoint {
34    /// `operating_point_idc` — the layer bitmask this point selects.
35    pub idc: u16,
36    /// `seq_level_idx` — the AV1 level.
37    pub seq_level_idx: u8,
38    /// `seq_tier` — Main (0) or High (1) tier.
39    pub seq_tier: u8,
40}
41
42/// The colour configuration (`color_config`, §5.5.2).
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct ColorConfig {
45    /// Sample bit depth: 8, 10 or 12.
46    pub bit_depth: u8,
47    /// Whether the stream carries only a luma plane.
48    pub mono_chrome: bool,
49    /// 1 for monochrome, 3 otherwise.
50    pub num_planes: u8,
51    /// CICP colour primaries.
52    pub color_primaries: u8,
53    /// CICP transfer characteristics.
54    pub transfer_characteristics: u8,
55    /// CICP matrix coefficients.
56    pub matrix_coefficients: u8,
57    /// Full-range (`true`) versus studio-range (`false`) samples.
58    pub color_range: bool,
59    /// Horizontal chroma subsampling (0 or 1).
60    pub subsampling_x: u8,
61    /// Vertical chroma subsampling (0 or 1).
62    pub subsampling_y: u8,
63    /// `chroma_sample_position` — siting of chroma against luma.
64    pub chroma_sample_position: u8,
65    /// Whether U and V carry independent delta-Q.
66    pub separate_uv_delta_q: bool,
67}
68
69/// A fully parsed sequence header.
70#[derive(Debug, Clone)]
71pub struct SequenceHeader {
72    /// `seq_profile` — 0, 1 or 2.
73    pub seq_profile: u8,
74    /// Whether the stream is flagged as a single still picture.
75    pub still_picture: bool,
76    /// Whether the compact still-picture header form was used.
77    pub reduced_still_picture_header: bool,
78    /// Every operating point in declaration order.
79    pub operating_points: Vec<OperatingPoint>,
80    /// `OperatingPointIdc` for the chosen point (point 0): the layer mask the
81    /// frame header uses to decide whether to read temporal/spatial IDs.
82    pub operating_point_idc: u16,
83    /// Bits used to code a frame width, `frame_width_bits_minus_1 + 1`.
84    pub frame_width_bits: u32,
85    /// Bits used to code a frame height, `frame_height_bits_minus_1 + 1`.
86    pub frame_height_bits: u32,
87    /// Maximum frame width in samples.
88    pub max_frame_width: u32,
89    /// Maximum frame height in samples.
90    pub max_frame_height: u32,
91    /// Whether frames carry explicit frame-id numbers.
92    pub frame_id_numbers_present: bool,
93    /// `delta_frame_id_length_minus_2 + 2` when ids are present.
94    pub delta_frame_id_length: u32,
95    /// `additional_frame_id_length_minus_1 + 1` when ids are present.
96    pub additional_frame_id_length: u32,
97    /// Whether superblocks are 128x128 (`true`) or 64x64 (`false`).
98    pub use_128x128_superblock: bool,
99    /// Whether filter-intra prediction is enabled.
100    pub enable_filter_intra: bool,
101    /// Whether the intra edge filter is enabled.
102    pub enable_intra_edge_filter: bool,
103    /// Inter-only tool switches, retained so the frame header parse stays
104    /// faithful even though a still image never exercises them.
105    pub enable_interintra_compound: bool,
106    /// Whether masked compound is enabled.
107    pub enable_masked_compound: bool,
108    /// Whether warped motion is enabled.
109    pub enable_warped_motion: bool,
110    /// Whether the dual interpolation filter is enabled.
111    pub enable_dual_filter: bool,
112    /// Whether order hints are coded.
113    pub enable_order_hint: bool,
114    /// Whether jnt_comp (distance-weighted compound) is enabled.
115    pub enable_jnt_comp: bool,
116    /// Whether reference-frame motion vectors are enabled.
117    pub enable_ref_frame_mvs: bool,
118    /// `seq_force_screen_content_tools`, possibly the `SELECT` sentinel.
119    pub seq_force_screen_content_tools: u8,
120    /// `seq_force_integer_mv`, possibly the `SELECT` sentinel.
121    pub seq_force_integer_mv: u8,
122    /// `OrderHintBits` — bits used to code an order hint, 0 when disabled.
123    pub order_hint_bits: u32,
124    /// Whether super-resolution is enabled.
125    pub enable_superres: bool,
126    /// Whether CDEF is enabled.
127    pub enable_cdef: bool,
128    /// Whether loop restoration is enabled.
129    pub enable_restoration: bool,
130    /// The colour configuration.
131    pub color: ColorConfig,
132    /// Whether film-grain parameters may appear in frame headers.
133    pub film_grain_params_present: bool,
134    /// Whether a decoder model was signalled (affects the frame header).
135    pub decoder_model_info_present: bool,
136    /// `buffer_delay_length_minus_1 + 1` from the decoder model.
137    pub buffer_delay_length: u32,
138    /// `buffer_removal_time_length_minus_1 + 1` from the decoder model.
139    pub buffer_removal_time_length: u32,
140    /// `frame_presentation_time_length_minus_1 + 1` from the decoder model.
141    pub frame_presentation_time_length: u32,
142    /// Whether pictures are equally spaced in time.
143    pub equal_picture_interval: bool,
144    /// Whether timing information was present.
145    pub timing_info_present: bool,
146}
147
148impl SequenceHeader {
149    /// Parse a sequence header from the start of an OBU payload.
150    pub fn parse(r: &mut BitReader<'_>) -> Result<Self> {
151        let seq_profile = r.f(3)? as u8;
152        if seq_profile > 2 {
153            return Err(PixelsError::malformed(
154                "avif",
155                "AV1 seq_profile above 2 is not a defined profile",
156            ));
157        }
158        let still_picture = r.flag()?;
159        let reduced_still_picture_header = r.flag()?;
160
161        let mut timing_info_present = false;
162        let mut decoder_model_info_present = false;
163        let mut buffer_delay_length = 0;
164        let mut buffer_removal_time_length = 0;
165        let mut frame_presentation_time_length = 0;
166        let mut equal_picture_interval = false;
167        let mut operating_points = Vec::new();
168
169        if reduced_still_picture_header {
170            let seq_level_idx = r.f(5)? as u8;
171            operating_points.push(OperatingPoint {
172                idc: 0,
173                seq_level_idx,
174                seq_tier: 0,
175            });
176        } else {
177            timing_info_present = r.flag()?;
178            if timing_info_present {
179                equal_picture_interval = parse_timing_info(r)?;
180                decoder_model_info_present = r.flag()?;
181                if decoder_model_info_present {
182                    let model = parse_decoder_model_info(r)?;
183                    buffer_delay_length = model.0;
184                    buffer_removal_time_length = model.1;
185                    frame_presentation_time_length = model.2;
186                }
187            }
188            let initial_display_delay_present = r.flag()?;
189            let operating_points_cnt = r.f(5)? + 1;
190            for _ in 0..operating_points_cnt {
191                let idc = r.f(12)? as u16;
192                let seq_level_idx = r.f(5)? as u8;
193                let seq_tier = if seq_level_idx > 7 { r.f(1)? as u8 } else { 0 };
194                if decoder_model_info_present {
195                    let present_for_op = r.flag()?;
196                    if present_for_op {
197                        // operating_parameters_info: two buffer delays + a flag.
198                        r.f(buffer_delay_length)?;
199                        r.f(buffer_delay_length)?;
200                        r.f(1)?;
201                    }
202                }
203                if initial_display_delay_present {
204                    let present_for_op = r.flag()?;
205                    if present_for_op {
206                        r.f(4)?;
207                    }
208                }
209                operating_points.push(OperatingPoint {
210                    idc,
211                    seq_level_idx,
212                    seq_tier,
213                });
214            }
215        }
216
217        // choose_operating_point defaults to point 0.
218        let operating_point_idc = operating_points.first().map_or(0, |op| op.idc);
219
220        let frame_width_bits = r.f(4)? + 1;
221        let frame_height_bits = r.f(4)? + 1;
222        let max_frame_width = r.f(frame_width_bits)? + 1;
223        let max_frame_height = r.f(frame_height_bits)? + 1;
224
225        let frame_id_numbers_present = if reduced_still_picture_header {
226            false
227        } else {
228            r.flag()?
229        };
230        let mut delta_frame_id_length = 0;
231        let mut additional_frame_id_length = 0;
232        if frame_id_numbers_present {
233            delta_frame_id_length = r.f(4)? + 2;
234            additional_frame_id_length = r.f(3)? + 1;
235        }
236
237        let use_128x128_superblock = r.flag()?;
238        let enable_filter_intra = r.flag()?;
239        let enable_intra_edge_filter = r.flag()?;
240
241        let mut enable_interintra_compound = false;
242        let mut enable_masked_compound = false;
243        let mut enable_warped_motion = false;
244        let mut enable_dual_filter = false;
245        let mut enable_order_hint = false;
246        let mut enable_jnt_comp = false;
247        let mut enable_ref_frame_mvs = false;
248        let mut seq_force_screen_content_tools = SELECT;
249        let mut seq_force_integer_mv = SELECT;
250        let mut order_hint_bits = 0;
251
252        if !reduced_still_picture_header {
253            enable_interintra_compound = r.flag()?;
254            enable_masked_compound = r.flag()?;
255            enable_warped_motion = r.flag()?;
256            enable_dual_filter = r.flag()?;
257            enable_order_hint = r.flag()?;
258            if enable_order_hint {
259                enable_jnt_comp = r.flag()?;
260                enable_ref_frame_mvs = r.flag()?;
261            }
262            let seq_choose_screen_content_tools = r.flag()?;
263            seq_force_screen_content_tools = if seq_choose_screen_content_tools {
264                SELECT
265            } else {
266                r.f(1)? as u8
267            };
268            if seq_force_screen_content_tools > 0 {
269                let seq_choose_integer_mv = r.flag()?;
270                seq_force_integer_mv = if seq_choose_integer_mv {
271                    SELECT
272                } else {
273                    r.f(1)? as u8
274                };
275            } else {
276                seq_force_integer_mv = SELECT;
277            }
278            if enable_order_hint {
279                order_hint_bits = r.f(3)? + 1;
280            }
281        }
282
283        let enable_superres = r.flag()?;
284        let enable_cdef = r.flag()?;
285        let enable_restoration = r.flag()?;
286        let color = parse_color_config(r, seq_profile)?;
287        let film_grain_params_present = r.flag()?;
288
289        Ok(Self {
290            seq_profile,
291            still_picture,
292            reduced_still_picture_header,
293            operating_points,
294            operating_point_idc,
295            frame_width_bits,
296            frame_height_bits,
297            max_frame_width,
298            max_frame_height,
299            frame_id_numbers_present,
300            delta_frame_id_length,
301            additional_frame_id_length,
302            use_128x128_superblock,
303            enable_filter_intra,
304            enable_intra_edge_filter,
305            enable_interintra_compound,
306            enable_masked_compound,
307            enable_warped_motion,
308            enable_dual_filter,
309            enable_order_hint,
310            enable_jnt_comp,
311            enable_ref_frame_mvs,
312            seq_force_screen_content_tools,
313            seq_force_integer_mv,
314            order_hint_bits,
315            enable_superres,
316            enable_cdef,
317            enable_restoration,
318            color,
319            film_grain_params_present,
320            decoder_model_info_present,
321            buffer_delay_length,
322            buffer_removal_time_length,
323            frame_presentation_time_length,
324            equal_picture_interval,
325            timing_info_present,
326        })
327    }
328}
329
330/// `timing_info` (§5.5.3). Returns `equal_picture_interval`.
331fn parse_timing_info(r: &mut BitReader<'_>) -> Result<bool> {
332    let _num_units_in_display_tick = r.f(32)?;
333    let _time_scale = r.f(32)?;
334    let equal_picture_interval = r.flag()?;
335    if equal_picture_interval {
336        let _num_ticks_per_picture_minus_1 = r.uvlc()?;
337    }
338    Ok(equal_picture_interval)
339}
340
341/// `decoder_model_info` (§5.5.4). Returns the three length fields the frame
342/// header needs to size its own delay and presentation-time reads.
343fn parse_decoder_model_info(r: &mut BitReader<'_>) -> Result<(u32, u32, u32)> {
344    let buffer_delay_length = r.f(5)? + 1;
345    let _num_units_in_decoding_tick = r.f(32)?;
346    let buffer_removal_time_length = r.f(5)? + 1;
347    let frame_presentation_time_length = r.f(5)? + 1;
348    Ok((
349        buffer_delay_length,
350        buffer_removal_time_length,
351        frame_presentation_time_length,
352    ))
353}
354
355/// `color_config` (§5.5.2).
356fn parse_color_config(r: &mut BitReader<'_>, seq_profile: u8) -> Result<ColorConfig> {
357    let high_bitdepth = r.flag()?;
358    let bit_depth = if seq_profile == 2 && high_bitdepth {
359        if r.flag()? { 12 } else { 10 }
360    } else if high_bitdepth {
361        10
362    } else {
363        8
364    };
365
366    let mono_chrome = if seq_profile == 1 { false } else { r.flag()? };
367    let num_planes = if mono_chrome { 1 } else { 3 };
368
369    let color_description_present = r.flag()?;
370    let (color_primaries, transfer_characteristics, matrix_coefficients) =
371        if color_description_present {
372            (r.f(8)? as u8, r.f(8)? as u8, r.f(8)? as u8)
373        } else {
374            (CP_UNSPECIFIED, TC_UNSPECIFIED, MC_UNSPECIFIED)
375        };
376
377    if mono_chrome {
378        let color_range = r.flag()?;
379        return Ok(ColorConfig {
380            bit_depth,
381            mono_chrome,
382            num_planes,
383            color_primaries,
384            transfer_characteristics,
385            matrix_coefficients,
386            color_range,
387            subsampling_x: 1,
388            subsampling_y: 1,
389            chroma_sample_position: 0,
390            separate_uv_delta_q: false,
391        });
392    }
393
394    let (color_range, subsampling_x, subsampling_y);
395    if color_primaries == CP_BT_709
396        && transfer_characteristics == TC_SRGB
397        && matrix_coefficients == MC_IDENTITY
398    {
399        // The sRGB fast path is implicitly full-range 4:4:4.
400        color_range = true;
401        subsampling_x = 0;
402        subsampling_y = 0;
403    } else {
404        color_range = r.flag()?;
405        match seq_profile {
406            0 => {
407                subsampling_x = 1;
408                subsampling_y = 1;
409            }
410            1 => {
411                subsampling_x = 0;
412                subsampling_y = 0;
413            }
414            _ => {
415                if bit_depth == 12 {
416                    subsampling_x = r.f(1)? as u8;
417                    subsampling_y = if subsampling_x != 0 { r.f(1)? as u8 } else { 0 };
418                } else {
419                    subsampling_x = 1;
420                    subsampling_y = 0;
421                }
422            }
423        }
424    }
425
426    let chroma_sample_position = if subsampling_x == 1 && subsampling_y == 1 {
427        r.f(2)? as u8
428    } else {
429        0
430    };
431    let separate_uv_delta_q = r.flag()?;
432
433    Ok(ColorConfig {
434        bit_depth,
435        mono_chrome,
436        num_planes,
437        color_primaries,
438        transfer_characteristics,
439        matrix_coefficients,
440        color_range,
441        subsampling_x,
442        subsampling_y,
443        chroma_sample_position,
444        separate_uv_delta_q,
445    })
446}
447
448#[cfg(test)]
449#[allow(
450    clippy::unwrap_used,
451    clippy::indexing_slicing,
452    clippy::panic,
453    reason = "tests operate on known-good values and assert shapes directly"
454)]
455mod tests {
456    use super::*;
457
458    /// A minimal reduced still-picture sequence header, assembled bit by bit so
459    /// the test reads as the syntax does. Returns the packed bytes.
460    struct SeqBuilder {
461        bits: Vec<u8>,
462    }
463    impl SeqBuilder {
464        fn new() -> Self {
465            Self { bits: Vec::new() }
466        }
467        fn put(&mut self, value: u32, n: u32) -> &mut Self {
468            for i in (0..n).rev() {
469                self.bits.push(((value >> i) & 1) as u8);
470            }
471            self
472        }
473        fn pack(&self) -> Vec<u8> {
474            let mut out = vec![0_u8; self.bits.len().div_ceil(8)];
475            for (i, &bit) in self.bits.iter().enumerate() {
476                if bit != 0 {
477                    out[i / 8] |= 1 << (7 - (i % 8));
478                }
479            }
480            out
481        }
482    }
483
484    /// Build the common reduced-still-picture header for an 8-bit 4:2:0 image
485    /// of the given size, with no colour description.
486    fn reduced_still(width: u32, height: u32) -> Vec<u8> {
487        let mut b = SeqBuilder::new();
488        b.put(0, 3); // seq_profile = 0
489        b.put(1, 1); // still_picture = 1
490        b.put(1, 1); // reduced_still_picture_header = 1
491        b.put(1, 5); // seq_level_idx[0]
492        b.put(15, 4); // frame_width_bits_minus_1 = 15 -> 16 bits
493        b.put(15, 4); // frame_height_bits_minus_1 = 15 -> 16 bits
494        b.put(width - 1, 16); // max_frame_width_minus_1
495        b.put(height - 1, 16); // max_frame_height_minus_1
496        b.put(0, 1); // use_128x128_superblock = 0
497        b.put(0, 1); // enable_filter_intra = 0
498        b.put(0, 1); // enable_intra_edge_filter = 0
499        b.put(0, 1); // enable_superres = 0
500        b.put(0, 1); // enable_cdef = 0
501        b.put(0, 1); // enable_restoration = 0
502        // color_config: high_bitdepth=0, mono_chrome=0,
503        // color_description_present=0, then (not sRGB path) color_range=0,
504        // profile 0 -> 4:2:0, chroma_sample_position(2), separate_uv_delta_q=0
505        b.put(0, 1); // high_bitdepth = 0 -> 8-bit
506        b.put(0, 1); // mono_chrome = 0
507        b.put(0, 1); // color_description_present = 0
508        b.put(0, 1); // color_range = 0
509        b.put(0, 2); // chroma_sample_position
510        b.put(0, 1); // separate_uv_delta_q = 0
511        b.put(0, 1); // film_grain_params_present = 0
512        b.pack()
513    }
514
515    #[test]
516    fn parses_a_reduced_still_picture_header() {
517        let bytes = reduced_still(320, 240);
518        let mut r = BitReader::new(&bytes);
519        let seq = SequenceHeader::parse(&mut r).unwrap();
520        assert_eq!(seq.seq_profile, 0);
521        assert!(seq.still_picture);
522        assert!(seq.reduced_still_picture_header);
523        assert_eq!(seq.max_frame_width, 320);
524        assert_eq!(seq.max_frame_height, 240);
525        assert_eq!(seq.color.bit_depth, 8);
526        assert!(!seq.color.mono_chrome);
527        assert_eq!(seq.color.subsampling_x, 1);
528        assert_eq!(seq.color.subsampling_y, 1);
529        assert_eq!(seq.operating_points.len(), 1);
530        // Reduced header forces the inter tools to their off/select defaults.
531        assert_eq!(seq.seq_force_screen_content_tools, SELECT);
532        assert_eq!(seq.seq_force_integer_mv, SELECT);
533        assert_eq!(seq.order_hint_bits, 0);
534        assert!(!seq.enable_order_hint);
535    }
536
537    #[test]
538    fn monochrome_forces_a_single_plane_and_subsampling() {
539        let mut b = SeqBuilder::new();
540        b.put(0, 3).put(1, 1).put(1, 1).put(1, 5);
541        b.put(7, 4).put(7, 4); // 8-bit dimension fields
542        b.put(99, 8).put(49, 8); // 100 x 50
543        b.put(0, 1).put(0, 1).put(0, 1); // sb / filter-intra / edge
544        b.put(0, 1).put(0, 1).put(0, 1); // superres / cdef / restoration
545        b.put(0, 1); // high_bitdepth
546        b.put(1, 1); // mono_chrome = 1
547        b.put(0, 1); // color_description_present = 0
548        b.put(0, 1); // color_range
549        b.put(0, 1); // film_grain_params_present
550        let bytes = b.pack();
551        let mut r = BitReader::new(&bytes);
552        let seq = SequenceHeader::parse(&mut r).unwrap();
553        assert!(seq.color.mono_chrome);
554        assert_eq!(seq.color.num_planes, 1);
555        assert_eq!(seq.color.subsampling_x, 1);
556        assert_eq!(seq.color.subsampling_y, 1);
557        assert_eq!(seq.max_frame_width, 100);
558        assert_eq!(seq.max_frame_height, 50);
559    }
560
561    #[test]
562    fn the_srgb_identity_path_is_full_range_444() {
563        let mut b = SeqBuilder::new();
564        b.put(1, 3); // seq_profile = 1 (4:4:4 capable)
565        b.put(1, 1).put(1, 1).put(1, 5);
566        b.put(7, 4).put(7, 4).put(63, 8).put(63, 8); // 64 x 64
567        b.put(0, 1).put(0, 1).put(0, 1);
568        b.put(0, 1).put(0, 1).put(0, 1);
569        b.put(0, 1); // high_bitdepth = 0
570        // seq_profile == 1 -> mono_chrome not read
571        b.put(1, 1); // color_description_present = 1
572        b.put(CP_BT_709 as u32, 8);
573        b.put(TC_SRGB as u32, 8);
574        b.put(MC_IDENTITY as u32, 8);
575        // sRGB identity path: color_range/subsampling not read;
576        // subsampling is 0,0 so chroma_sample_position not read.
577        b.put(0, 1); // separate_uv_delta_q
578        b.put(0, 1); // film_grain_params_present
579        let bytes = b.pack();
580        let mut r = BitReader::new(&bytes);
581        let seq = SequenceHeader::parse(&mut r).unwrap();
582        assert_eq!(seq.color.subsampling_x, 0);
583        assert_eq!(seq.color.subsampling_y, 0);
584        assert!(seq.color.color_range);
585        assert_eq!(seq.color.matrix_coefficients, MC_IDENTITY);
586    }
587
588    #[test]
589    fn a_profile_above_two_is_rejected() {
590        let bytes = [0xE0]; // seq_profile = 7
591        let mut r = BitReader::new(&bytes);
592        assert!(SequenceHeader::parse(&mut r).is_err());
593    }
594}