Skip to main content

zenavif_parse/
lib.rs

1#![deny(unsafe_code)]
2#![allow(clippy::missing_safety_doc)]
3//! Module for parsing ISO Base Media Format aka video/mp4 streams.
4//!
5//! This crate is written entirely in safe Rust code except for the C FFI bindings.
6
7// This Source Code Form is subject to the terms of the Mozilla Public
8// License, v. 2.0. If a copy of the MPL was not distributed with this
9// file, You can obtain one at https://mozilla.org/MPL/2.0/.
10
11use arrayvec::ArrayVec;
12use log::{debug, warn};
13
14use bitreader::BitReader;
15use byteorder::ReadBytesExt;
16use fallible_collections::{TryClone, TryReserveError};
17use std::borrow::Cow;
18use std::convert::{TryFrom, TryInto as _};
19
20use std::io::{Read, Take};
21use std::num::NonZeroU32;
22use std::ops::{Range, RangeFrom};
23
24mod obu;
25
26mod boxes;
27use crate::boxes::{BoxType, FourCC};
28
29/// This crate can be used from C.
30#[cfg(feature = "eager")]
31pub mod c_api;
32
33pub use enough::{Stop, StopReason, Unstoppable};
34
35// Arbitrary buffer size limit used for raw read_bufs on a box.
36// const BUF_SIZE_LIMIT: u64 = 10 * 1024 * 1024;
37
38/// A trait to indicate a type can be infallibly converted to `u64`.
39/// This should only be implemented for infallible conversions, so only unsigned types are valid.
40trait ToU64 {
41    fn to_u64(self) -> u64;
42}
43
44/// Statically verify that the platform `usize` can fit within a `u64`.
45/// If the size won't fit on the given platform, this will fail at compile time, but if a type
46/// which can fail `TryInto<usize>` is used, it may panic.
47impl ToU64 for usize {
48    fn to_u64(self) -> u64 {
49        const _: () = assert!(std::mem::size_of::<usize>() <= std::mem::size_of::<u64>());
50        self.try_into().ok().unwrap()
51    }
52}
53
54/// A trait to indicate a type can be infallibly converted to `usize`.
55/// This should only be implemented for infallible conversions, so only unsigned types are valid.
56pub(crate) trait ToUsize {
57    fn to_usize(self) -> usize;
58}
59
60/// Statically verify that the given type can fit within a `usize`.
61/// If the size won't fit on the given platform, this will fail at compile time, but if a type
62/// which can fail `TryInto<usize>` is used, it may panic.
63macro_rules! impl_to_usize_from {
64    ( $from_type:ty ) => {
65        impl ToUsize for $from_type {
66            fn to_usize(self) -> usize {
67                const _: () = assert!(std::mem::size_of::<$from_type>() <= std::mem::size_of::<usize>());
68                self.try_into().ok().unwrap()
69            }
70        }
71    };
72}
73
74impl_to_usize_from!(u8);
75impl_to_usize_from!(u16);
76impl_to_usize_from!(u32);
77
78/// Indicate the current offset (i.e., bytes already read) in a reader
79trait Offset {
80    fn offset(&self) -> u64;
81}
82
83/// Wraps a reader to track the current offset
84struct OffsetReader<'a, T> {
85    reader: &'a mut T,
86    offset: u64,
87}
88
89impl<'a, T> OffsetReader<'a, T> {
90    fn new(reader: &'a mut T) -> Self {
91        Self { reader, offset: 0 }
92    }
93}
94
95impl<T> Offset for OffsetReader<'_, T> {
96    fn offset(&self) -> u64 {
97        self.offset
98    }
99}
100
101impl<T: Read> Read for OffsetReader<'_, T> {
102    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
103        let bytes_read = self.reader.read(buf)?;
104        self.offset = self
105            .offset
106            .checked_add(bytes_read.to_u64())
107            .ok_or(Error::Unsupported("total bytes read too large for offset type"))?;
108        Ok(bytes_read)
109    }
110}
111
112#[doc(hidden)]
113pub type TryVec<T> = fallible_collections::TryVec<T>;
114type TryString = fallible_collections::TryVec<u8>;
115
116// To ensure we don't use stdlib allocating types by accident
117#[allow(dead_code)]
118struct Vec;
119#[allow(dead_code)]
120struct Box;
121#[allow(dead_code)]
122struct HashMap;
123#[allow(dead_code)]
124struct String;
125
126/// Describes parser failures.
127///
128/// This enum wraps the standard `io::Error` type, unified with
129/// our own parser error states and those of crates we use.
130#[derive(Debug)]
131pub enum Error {
132    /// Parse error caused by corrupt or malformed data.
133    InvalidData(&'static str),
134    /// Parse error caused by limited parser support rather than invalid data.
135    Unsupported(&'static str),
136    /// Reflect `std::io::ErrorKind::UnexpectedEof` for short data.
137    UnexpectedEOF,
138    /// Propagate underlying errors from `std::io`.
139    Io(std::io::Error),
140    /// `read_mp4` terminated without detecting a moov box.
141    NoMoov,
142    /// Out of memory
143    OutOfMemory,
144    /// Resource limit exceeded during parsing
145    ResourceLimitExceeded(&'static str),
146    /// Operation was stopped/cancelled
147    Stopped(enough::StopReason),
148}
149
150impl std::fmt::Display for Error {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        let msg = match self {
153            Self::InvalidData(s) | Self::Unsupported(s) | Self::ResourceLimitExceeded(s) => s,
154            Self::UnexpectedEOF => "EOF",
155            Self::Io(err) => return err.fmt(f),
156            Self::NoMoov => "Missing Moov box",
157            Self::OutOfMemory => "OOM",
158            Self::Stopped(reason) => return write!(f, "Stopped: {}", reason),
159        };
160        f.write_str(msg)
161    }
162}
163
164impl std::error::Error for Error {}
165
166impl From<bitreader::BitReaderError> for Error {
167    #[cold]
168    #[cfg_attr(debug_assertions, track_caller)]
169    fn from(err: bitreader::BitReaderError) -> Self {
170        log::warn!("bitreader: {err}");
171        debug_assert!(!matches!(err, bitreader::BitReaderError::TooManyBitsForType { .. })); // bug
172        Self::InvalidData("truncated bits")
173    }
174}
175
176impl From<std::io::Error> for Error {
177    fn from(err: std::io::Error) -> Self {
178        match err.kind() {
179            std::io::ErrorKind::UnexpectedEof => Self::UnexpectedEOF,
180            _ => Self::Io(err),
181        }
182    }
183}
184
185impl From<std::string::FromUtf8Error> for Error {
186    fn from(_: std::string::FromUtf8Error) -> Self {
187        Self::InvalidData("invalid utf8")
188    }
189}
190
191impl From<std::num::TryFromIntError> for Error {
192    fn from(_: std::num::TryFromIntError) -> Self {
193        Self::Unsupported("integer conversion failed")
194    }
195}
196
197impl From<Error> for std::io::Error {
198    fn from(err: Error) -> Self {
199        let kind = match err {
200            Error::InvalidData(_) => std::io::ErrorKind::InvalidData,
201            Error::UnexpectedEOF => std::io::ErrorKind::UnexpectedEof,
202            Error::Io(io_err) => return io_err,
203            _ => std::io::ErrorKind::Other,
204        };
205        Self::new(kind, err)
206    }
207}
208
209impl From<TryReserveError> for Error {
210    fn from(_: TryReserveError) -> Self {
211        Self::OutOfMemory
212    }
213}
214
215impl From<enough::StopReason> for Error {
216    fn from(reason: enough::StopReason) -> Self {
217        Self::Stopped(reason)
218    }
219}
220
221/// Result shorthand using our Error enum.
222pub type Result<T, E = Error> = std::result::Result<T, E>;
223
224/// Basic ISO box structure.
225///
226/// mp4 files are a sequence of possibly-nested 'box' structures.  Each box
227/// begins with a header describing the length of the box's data and a
228/// four-byte box type which identifies the type of the box. Together these
229/// are enough to interpret the contents of that section of the file.
230///
231/// See ISO 14496-12:2015 § 4.2
232#[derive(Debug, Clone, Copy)]
233struct BoxHeader {
234    /// Box type.
235    name: BoxType,
236    /// Size of the box in bytes.
237    size: u64,
238    /// Offset to the start of the contained data (or header size).
239    offset: u64,
240    /// Uuid for extended type.
241    #[allow(unused)]
242    uuid: Option<[u8; 16]>,
243}
244
245impl BoxHeader {
246    /// 4-byte size + 4-byte type
247    const MIN_SIZE: u64 = 8;
248    /// 4-byte size + 4-byte type + 16-byte size
249    const MIN_LARGE_SIZE: u64 = 16;
250}
251
252/// File type box 'ftyp'.
253#[derive(Debug)]
254#[allow(unused)]
255struct FileTypeBox {
256    major_brand: FourCC,
257    minor_version: u32,
258    compatible_brands: TryVec<FourCC>,
259}
260
261// Handler reference box 'hdlr'
262#[derive(Debug)]
263#[allow(unused)]
264struct HandlerBox {
265    handler_type: FourCC,
266}
267
268/// AV1 codec configuration from the `av1C` property box.
269///
270/// Contains the AV1 codec parameters as signaled in the container.
271/// See AV1-ISOBMFF § 2.3.
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct AV1Config {
274    /// AV1 seq_profile (0=Main, 1=High, 2=Professional)
275    pub profile: u8,
276    /// AV1 seq_level_idx for operating point 0
277    pub level: u8,
278    /// AV1 seq_tier for operating point 0
279    pub tier: u8,
280    /// Bit depth (8, 10, or 12)
281    pub bit_depth: u8,
282    /// True if monochrome (no chroma planes)
283    pub monochrome: bool,
284    /// Chroma subsampling X (1 = horizontally subsampled)
285    pub chroma_subsampling_x: u8,
286    /// Chroma subsampling Y (1 = vertically subsampled)
287    pub chroma_subsampling_y: u8,
288    /// Chroma sample position (0=unknown, 1=vertical, 2=colocated)
289    pub chroma_sample_position: u8,
290}
291
292/// Colour information from the `colr` property box.
293///
294/// Can be either CICP-based (`nclx`) or an ICC profile (`rICC`/`prof`).
295/// See ISOBMFF § 12.1.5.
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub enum ColorInformation {
298    /// CICP-based color information (colour_type = 'nclx')
299    Nclx {
300        /// Colour primaries (ITU-T H.273 Table 2)
301        color_primaries: u16,
302        /// Transfer characteristics (ITU-T H.273 Table 3)
303        transfer_characteristics: u16,
304        /// Matrix coefficients (ITU-T H.273 Table 4)
305        matrix_coefficients: u16,
306        /// True if full range (0-255 for 8-bit), false if limited/studio range
307        full_range: bool,
308    },
309    /// ICC profile (colour_type = 'rICC' or 'prof')
310    IccProfile(std::vec::Vec<u8>),
311}
312
313/// Image rotation from the `irot` property box.
314///
315/// Specifies a counter-clockwise rotation to apply after decoding.
316/// See ISOBMFF § 12.1.4.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct ImageRotation {
319    /// Rotation angle in degrees counter-clockwise: 0, 90, 180, or 270.
320    pub angle: u16,
321}
322
323/// Image mirror from the `imir` property box.
324///
325/// Specifies a mirror (flip) axis to apply after rotation.
326/// See ISOBMFF § 12.1.4.
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub struct ImageMirror {
329    /// Mirror axis: 0 = top-to-bottom (vertical axis, left-right flip),
330    /// 1 = left-to-right (horizontal axis, top-bottom flip).
331    pub axis: u8,
332}
333
334/// Clean aperture from the `clap` property box.
335///
336/// Defines a crop rectangle as a centered region. All values are
337/// stored as exact rationals (numerator/denominator).
338/// See ISOBMFF § 12.1.4.
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub struct CleanAperture {
341    /// Width of the clean aperture (numerator)
342    pub width_n: u32,
343    /// Width of the clean aperture (denominator)
344    pub width_d: u32,
345    /// Height of the clean aperture (numerator)
346    pub height_n: u32,
347    /// Height of the clean aperture (denominator)
348    pub height_d: u32,
349    /// Horizontal offset of the clean aperture center (numerator, signed)
350    pub horiz_off_n: i32,
351    /// Horizontal offset of the clean aperture center (denominator)
352    pub horiz_off_d: u32,
353    /// Vertical offset of the clean aperture center (numerator, signed)
354    pub vert_off_n: i32,
355    /// Vertical offset of the clean aperture center (denominator)
356    pub vert_off_d: u32,
357}
358
359/// Pixel aspect ratio from the `pasp` property box.
360///
361/// For AVIF, the spec requires this to be 1:1 if present.
362/// See ISOBMFF § 12.1.4.
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub struct PixelAspectRatio {
365    /// Horizontal spacing
366    pub h_spacing: u32,
367    /// Vertical spacing
368    pub v_spacing: u32,
369}
370
371/// Content light level info from the `clli` property box.
372///
373/// HDR metadata for display mapping.
374/// See ISOBMFF § 12.1.5 / ITU-T H.274.
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub struct ContentLightLevel {
377    /// Maximum content light level (cd/m²)
378    pub max_content_light_level: u16,
379    /// Maximum picture average light level (cd/m²)
380    pub max_pic_average_light_level: u16,
381}
382
383/// Mastering display colour volume from the `mdcv` property box.
384///
385/// HDR metadata describing the mastering display's color volume.
386/// See ISOBMFF § 12.1.5 / SMPTE ST 2086.
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub struct MasteringDisplayColourVolume {
389    /// Display primaries: [(x, y); 3] in 0.00002 units (CIE 1931)
390    /// Order: green, blue, red (per SMPTE ST 2086)
391    pub primaries: [(u16, u16); 3],
392    /// White point (x, y) in 0.00002 units
393    pub white_point: (u16, u16),
394    /// Maximum display luminance in 0.0001 cd/m² units
395    pub max_luminance: u32,
396    /// Minimum display luminance in 0.0001 cd/m² units
397    pub min_luminance: u32,
398}
399
400/// Content colour volume from the `cclv` property box.
401///
402/// Describes the colour volume of the content. Derived from H.265 D.2.40 /
403/// ITU-T H.274. All fields are optional, controlled by presence flags.
404/// See ISOBMFF § 12.1.5.
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub struct ContentColourVolume {
407    /// Content colour primaries (x, y) for 3 primaries, as signed i32.
408    /// Present only if `ccv_primaries_present_flag` was set.
409    pub primaries: Option<[(i32, i32); 3]>,
410    /// Minimum luminance value. Present only if flag was set.
411    pub min_luminance: Option<u32>,
412    /// Maximum luminance value. Present only if flag was set.
413    pub max_luminance: Option<u32>,
414    /// Average luminance value. Present only if flag was set.
415    pub avg_luminance: Option<u32>,
416}
417
418/// Ambient viewing environment from the `amve` property box.
419///
420/// Describes the ambient viewing conditions under which the content
421/// was authored. See ISOBMFF § 12.1.5 / H.265 D.2.39.
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub struct AmbientViewingEnvironment {
424    /// Ambient illuminance in units of 1/10000 cd/m²
425    pub ambient_illuminance: u32,
426    /// Ambient light x chromaticity (CIE 1931), units of 1/50000
427    pub ambient_light_x: u16,
428    /// Ambient light y chromaticity (CIE 1931), units of 1/50000
429    pub ambient_light_y: u16,
430}
431
432/// Operating point selector from the `a1op` property box.
433///
434/// Selects which AV1 operating point to decode for multi-operating-point images.
435/// See AVIF § 4.3.4.
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub struct OperatingPointSelector {
438    /// Operating point index (0..31)
439    pub op_index: u8,
440}
441
442/// Layer selector from the `lsel` property box.
443///
444/// Selects which spatial layer to render for layered/progressive images.
445/// See HEIF (ISO 23008-12).
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub struct LayerSelector {
448    /// Layer ID to render (0-3), or 0xFFFF for all layers (progressive)
449    pub layer_id: u16,
450}
451
452/// AV1 layered image indexing from the `a1lx` property box.
453///
454/// Provides byte sizes for the first 3 layers so decoders can seek
455/// to a specific layer without parsing the full bitstream.
456/// See AVIF § 4.3.6.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub struct AV1LayeredImageIndexing {
459    /// Byte sizes of layers 0, 1, 2. The last layer's size is implicit
460    /// (total item size minus the sum of these three).
461    pub layer_sizes: [u32; 3],
462}
463
464/// Options for parsing AVIF files
465///
466/// Prefer using [`DecodeConfig::lenient()`] with [`AvifParser`] instead.
467#[derive(Debug, Clone, Copy)]
468#[derive(Default)]
469pub struct ParseOptions {
470    /// Enable lenient parsing mode
471    ///
472    /// When true, non-critical validation errors (like non-zero flags in boxes
473    /// that expect zero flags) will be ignored instead of returning errors.
474    /// This allows parsing of slightly malformed but otherwise valid AVIF files.
475    ///
476    /// Default: false (strict validation)
477    pub lenient: bool,
478}
479
480/// Configuration for parsing AVIF files with resource limits and validation options
481///
482/// Provides fine-grained control over resource consumption during AVIF parsing,
483/// allowing defensive parsing against malicious or malformed files.
484///
485/// Resource limits are checked **before** allocations occur, preventing out-of-memory
486/// conditions from malicious files that claim unrealistic dimensions or counts.
487///
488/// # Examples
489///
490/// ```rust
491/// use zenavif_parse::DecodeConfig;
492///
493/// // Default limits (suitable for most apps)
494/// let config = DecodeConfig::default();
495///
496/// // Strict limits for untrusted input
497/// let config = DecodeConfig::default()
498///     .with_peak_memory_limit(100_000_000)  // 100MB
499///     .with_total_megapixels_limit(64)       // 64MP max
500///     .with_max_animation_frames(100);       // 100 frames
501///
502/// // No limits (backwards compatible with read_avif)
503/// let config = DecodeConfig::unlimited();
504/// ```
505#[derive(Debug, Clone)]
506pub struct DecodeConfig {
507    /// Maximum peak heap memory usage in bytes.
508    /// Default: 1GB (1,000,000,000 bytes)
509    pub peak_memory_limit: Option<u64>,
510
511    /// Maximum total megapixels for grid images.
512    /// Default: 512 megapixels
513    pub total_megapixels_limit: Option<u32>,
514
515    /// Maximum number of animation frames.
516    /// Default: 10,000 frames
517    pub max_animation_frames: Option<u32>,
518
519    /// Maximum number of grid tiles.
520    /// Default: 1,000 tiles
521    pub max_grid_tiles: Option<u32>,
522
523    /// Enable lenient parsing mode.
524    /// Default: false (strict validation)
525    pub lenient: bool,
526}
527
528impl Default for DecodeConfig {
529    fn default() -> Self {
530        Self {
531            peak_memory_limit: Some(1_000_000_000),
532            total_megapixels_limit: Some(512),
533            max_animation_frames: Some(10_000),
534            max_grid_tiles: Some(1_000),
535            lenient: false,
536        }
537    }
538}
539
540impl DecodeConfig {
541    /// Create a configuration with no resource limits.
542    ///
543    /// Equivalent to the behavior of `read_avif()` before resource limits were added.
544    pub fn unlimited() -> Self {
545        Self {
546            peak_memory_limit: None,
547            total_megapixels_limit: None,
548            max_animation_frames: None,
549            max_grid_tiles: None,
550            lenient: false,
551        }
552    }
553
554    /// Set the peak memory limit in bytes
555    pub fn with_peak_memory_limit(mut self, bytes: u64) -> Self {
556        self.peak_memory_limit = Some(bytes);
557        self
558    }
559
560    /// Set the total megapixels limit for grid images
561    pub fn with_total_megapixels_limit(mut self, megapixels: u32) -> Self {
562        self.total_megapixels_limit = Some(megapixels);
563        self
564    }
565
566    /// Set the maximum animation frame count
567    pub fn with_max_animation_frames(mut self, frames: u32) -> Self {
568        self.max_animation_frames = Some(frames);
569        self
570    }
571
572    /// Set the maximum grid tile count
573    pub fn with_max_grid_tiles(mut self, tiles: u32) -> Self {
574        self.max_grid_tiles = Some(tiles);
575        self
576    }
577
578    /// Enable lenient parsing mode
579    pub fn lenient(mut self, lenient: bool) -> Self {
580        self.lenient = lenient;
581        self
582    }
583}
584
585/// Grid configuration for tiled/grid-based AVIF images
586#[derive(Debug, Clone, PartialEq)]
587/// Grid image configuration
588///
589/// For tiled/grid AVIF images, this describes the grid layout.
590/// Grid images are composed of multiple AV1 image items (tiles) arranged in a rectangular grid.
591///
592/// ## Grid Layout Determination
593///
594/// Grid layout can be specified in two ways:
595/// 1. **Explicit ImageGrid property box** - contains rows, columns, and output dimensions
596/// 2. **Calculated from ispe properties** - when no ImageGrid box exists, dimensions are
597///    calculated by dividing the grid item's dimensions by a tile's dimensions
598///
599/// ## Output Dimensions
600///
601/// - `output_width` and `output_height` may be 0, indicating the decoder should calculate
602///   them from the tile dimensions
603/// - When non-zero, they specify the exact output dimensions of the composed image
604pub struct GridConfig {
605    /// Number of tile rows (1-256)
606    pub rows: u8,
607    /// Number of tile columns (1-256)
608    pub columns: u8,
609    /// Output width in pixels (0 = calculate from tiles)
610    pub output_width: u32,
611    /// Output height in pixels (0 = calculate from tiles)
612    pub output_height: u32,
613}
614
615/// Frame information for animated AVIF
616#[cfg(feature = "eager")]
617#[deprecated(since = "1.5.0", note = "Use `AvifParser::frame()` which returns `FrameRef` instead")]
618#[derive(Debug)]
619pub struct AnimationFrame {
620    /// AV1 bitstream data for this frame
621    pub data: TryVec<u8>,
622    /// Duration in milliseconds (0 if unknown)
623    pub duration_ms: u32,
624}
625
626/// Animation configuration for animated AVIF (avis brand)
627#[cfg(feature = "eager")]
628#[deprecated(since = "1.5.0", note = "Use `AvifParser::animation_info()` and `AvifParser::frames()` instead")]
629#[derive(Debug)]
630#[allow(deprecated)]
631pub struct AnimationConfig {
632    /// Number of times to loop (0 = infinite)
633    pub loop_count: u32,
634    /// All frames in the animation
635    pub frames: TryVec<AnimationFrame>,
636}
637
638// Internal structures for animation parsing
639
640#[derive(Debug)]
641struct MovieHeader {
642    _timescale: u32,
643    _duration: u64,
644}
645
646#[derive(Debug)]
647struct MediaHeader {
648    timescale: u32,
649    _duration: u64,
650}
651
652#[derive(Debug)]
653struct TimeToSampleEntry {
654    sample_count: u32,
655    sample_delta: u32,
656}
657
658#[derive(Debug)]
659struct SampleToChunkEntry {
660    first_chunk: u32,
661    samples_per_chunk: u32,
662    _sample_description_index: u32,
663}
664
665#[derive(Debug)]
666struct SampleTable {
667    time_to_sample: TryVec<TimeToSampleEntry>,
668    sample_to_chunk: TryVec<SampleToChunkEntry>,
669    sample_sizes: TryVec<u32>,
670    chunk_offsets: TryVec<u64>,
671}
672
673#[cfg(feature = "eager")]
674#[deprecated(since = "1.5.0", note = "Use `AvifParser` for zero-copy parsing instead")]
675#[derive(Debug, Default)]
676#[allow(deprecated)]
677pub struct AvifData {
678    /// AV1 data for the color channels.
679    ///
680    /// The collected data indicated by the `pitm` box, See ISO 14496-12:2015 § 8.11.4
681    pub primary_item: TryVec<u8>,
682    /// AV1 data for alpha channel.
683    ///
684    /// Associated alpha channel for the primary item, if any
685    pub alpha_item: Option<TryVec<u8>>,
686    /// If true, divide RGB values by the alpha value.
687    ///
688    /// See `prem` in MIAF § 7.3.5.2
689    pub premultiplied_alpha: bool,
690
691    /// Grid configuration for tiled images.
692    ///
693    /// If present, the image is a grid and `grid_tiles` contains the tile data.
694    /// Grid layout is determined either from an explicit ImageGrid property box or
695    /// calculated from ispe (Image Spatial Extents) properties.
696    ///
697    /// ## Example
698    ///
699    /// ```no_run
700    /// #[allow(deprecated)]
701    /// use std::fs::File;
702    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
703    /// #[allow(deprecated)]
704    /// let data = zenavif_parse::read_avif(&mut File::open("image.avif")?)?;
705    ///
706    /// if let Some(grid) = data.grid_config {
707    ///     println!("Grid: {}×{} tiles", grid.rows, grid.columns);
708    ///     println!("Output: {}×{}", grid.output_width, grid.output_height);
709    ///     println!("Tile count: {}", data.grid_tiles.len());
710    /// }
711    /// # Ok(())
712    /// # }
713    /// ```
714    pub grid_config: Option<GridConfig>,
715
716    /// AV1 payloads for grid image tiles.
717    ///
718    /// Empty for non-grid images. For grid images, contains one entry per tile.
719    ///
720    /// **Tile ordering:** Tiles are guaranteed to be in the correct order for grid assembly,
721    /// sorted by their dimgIdx (reference index). This is row-major order: tiles in the first
722    /// row from left to right, then the second row, etc.
723    pub grid_tiles: TryVec<TryVec<u8>>,
724
725    /// Animation configuration (for animated AVIF with avis brand)
726    ///
727    /// When present, primary_item contains the first frame
728    pub animation: Option<AnimationConfig>,
729
730    /// AV1 codec configuration from the container's `av1C` property.
731    pub av1_config: Option<AV1Config>,
732
733    /// Colour information from the container's `colr` property.
734    pub color_info: Option<ColorInformation>,
735
736    /// Image rotation from the container's `irot` property.
737    pub rotation: Option<ImageRotation>,
738
739    /// Image mirror from the container's `imir` property.
740    pub mirror: Option<ImageMirror>,
741
742    /// Clean aperture (crop) from the container's `clap` property.
743    pub clean_aperture: Option<CleanAperture>,
744
745    /// Pixel aspect ratio from the container's `pasp` property.
746    pub pixel_aspect_ratio: Option<PixelAspectRatio>,
747
748    /// Content light level from the container's `clli` property.
749    pub content_light_level: Option<ContentLightLevel>,
750
751    /// Mastering display colour volume from the container's `mdcv` property.
752    pub mastering_display: Option<MasteringDisplayColourVolume>,
753
754    /// Content colour volume from the container's `cclv` property.
755    pub content_colour_volume: Option<ContentColourVolume>,
756
757    /// Ambient viewing environment from the container's `amve` property.
758    pub ambient_viewing: Option<AmbientViewingEnvironment>,
759
760    /// Operating point selector from the container's `a1op` property.
761    pub operating_point: Option<OperatingPointSelector>,
762
763    /// Layer selector from the container's `lsel` property.
764    pub layer_selector: Option<LayerSelector>,
765
766    /// AV1 layered image indexing from the container's `a1lx` property.
767    pub layered_image_indexing: Option<AV1LayeredImageIndexing>,
768
769    /// Major brand from the `ftyp` box (e.g., `*b"avif"` or `*b"avis"`).
770    pub major_brand: [u8; 4],
771
772    /// Compatible brands from the `ftyp` box.
773    pub compatible_brands: std::vec::Vec<[u8; 4]>,
774}
775
776// # Memory Usage
777//
778// This implementation loads all image data into owned vectors (`TryVec<u8>`), which has
779// memory implications depending on the file type:
780//
781// - **Static images**: Single copy of compressed data (~5-50KB typical)
782//   - `primary_item`: compressed AV1 data
783//   - `alpha_item`: compressed alpha data (if present)
784//
785// - **Grid images**: All tiles loaded (~100KB-2MB for large grids)
786//   - `grid_tiles`: one compressed tile per grid cell
787//
788// - **Animated images**: All frames loaded eagerly (⚠️ HIGH MEMORY)
789//   - Internal mdat boxes: ~500KB for 95-frame video
790//   - Extracted frames: ~500KB duplicated in `animation.frames[].data`
791//   - **Total: ~2× file size in memory**
792//
793// For large animated files, consider using a streaming approach or processing frames
794// individually rather than loading the entire `AvifData` structure.
795
796#[cfg(feature = "eager")]
797#[allow(deprecated)]
798impl AvifData {
799    #[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader()` instead")]
800    pub fn from_reader<R: Read>(reader: &mut R) -> Result<Self> {
801        read_avif(reader)
802    }
803
804    /// Parses AV1 data to get basic properties of the opaque channel
805    pub fn primary_item_metadata(&self) -> Result<AV1Metadata> {
806        AV1Metadata::parse_av1_bitstream(&self.primary_item)
807    }
808
809    /// Parses AV1 data to get basic properties about the alpha channel, if any
810    pub fn alpha_item_metadata(&self) -> Result<Option<AV1Metadata>> {
811        self.alpha_item.as_deref().map(AV1Metadata::parse_av1_bitstream).transpose()
812    }
813}
814
815/// AV1 sequence header metadata parsed from an OBU bitstream.
816///
817/// See [`AvifParser::primary_metadata()`] and [`AV1Metadata::parse_av1_bitstream()`].
818#[non_exhaustive]
819#[derive(Debug, Clone)]
820pub struct AV1Metadata {
821    /// Should be true for non-animated AVIF
822    pub still_picture: bool,
823    pub max_frame_width: NonZeroU32,
824    pub max_frame_height: NonZeroU32,
825    /// 8, 10, or 12
826    pub bit_depth: u8,
827    /// 0, 1 or 2 for the level of complexity
828    pub seq_profile: u8,
829    /// Horizontal and vertical. `false` is full-res.
830    pub chroma_subsampling: (bool, bool),
831    pub monochrome: bool,
832}
833
834impl AV1Metadata {
835    /// Parses raw AV1 bitstream (OBU sequence header) only.
836    ///
837    /// This is for the bare image payload from an encoder, not an AVIF/HEIF file.
838    /// To parse AVIF files, see [`AvifParser::from_reader()`].
839    #[inline(never)]
840    pub fn parse_av1_bitstream(obu_bitstream: &[u8]) -> Result<Self> {
841        let h = obu::parse_obu(obu_bitstream)?;
842        Ok(Self {
843            still_picture: h.still_picture,
844            max_frame_width: h.max_frame_width,
845            max_frame_height: h.max_frame_height,
846            bit_depth: h.color.bit_depth,
847            seq_profile: h.seq_profile,
848            chroma_subsampling: h.color.chroma_subsampling,
849            monochrome: h.color.monochrome,
850        })
851    }
852}
853
854/// A single frame from an animated AVIF, with zero-copy when possible.
855///
856/// The `data` field is `Cow::Borrowed` when the frame lives in a single
857/// contiguous mdat extent, and `Cow::Owned` when extents must be concatenated.
858pub struct FrameRef<'a> {
859    pub data: Cow<'a, [u8]>,
860    pub duration_ms: u32,
861}
862
863/// Byte range of a media data box within the file.
864struct MdatBounds {
865    offset: u64,
866    length: u64,
867}
868
869/// Where an item's data lives: construction method + extent ranges.
870struct ItemExtents {
871    construction_method: ConstructionMethod,
872    extents: TryVec<ExtentRange>,
873}
874
875/// Zero-copy AVIF parser backed by a borrowed or owned byte buffer.
876///
877/// `AvifParser` records byte offsets during parsing but does **not** copy
878/// mdat payload data. Data access methods return `Cow<[u8]>` — borrowed
879/// when the item is a single contiguous extent, owned when extents must
880/// be concatenated.
881///
882/// # Constructors
883///
884/// | Method | Lifetime | Zero-copy? |
885/// |--------|----------|------------|
886/// | [`from_bytes`](Self::from_bytes) | `'data` | Yes — borrows the slice |
887/// | [`from_owned`](Self::from_owned) | `'static` | Within the owned buffer |
888/// | [`from_reader`](Self::from_reader) | `'static` | Reads all, then owned |
889///
890/// # Example
891///
892/// ```no_run
893/// use zenavif_parse::AvifParser;
894///
895/// let bytes = std::fs::read("image.avif")?;
896/// let parser = AvifParser::from_bytes(&bytes)?;
897/// let primary = parser.primary_data()?; // Cow::Borrowed for single-extent
898/// # Ok::<(), Box<dyn std::error::Error>>(())
899/// ```
900pub struct AvifParser<'data> {
901    raw: Cow<'data, [u8]>,
902    mdat_bounds: TryVec<MdatBounds>,
903    idat: Option<TryVec<u8>>,
904    primary: ItemExtents,
905    alpha: Option<ItemExtents>,
906    grid_config: Option<GridConfig>,
907    tiles: TryVec<ItemExtents>,
908    animation_data: Option<AnimationParserData>,
909    premultiplied_alpha: bool,
910    av1_config: Option<AV1Config>,
911    color_info: Option<ColorInformation>,
912    rotation: Option<ImageRotation>,
913    mirror: Option<ImageMirror>,
914    clean_aperture: Option<CleanAperture>,
915    pixel_aspect_ratio: Option<PixelAspectRatio>,
916    content_light_level: Option<ContentLightLevel>,
917    mastering_display: Option<MasteringDisplayColourVolume>,
918    content_colour_volume: Option<ContentColourVolume>,
919    ambient_viewing: Option<AmbientViewingEnvironment>,
920    operating_point: Option<OperatingPointSelector>,
921    layer_selector: Option<LayerSelector>,
922    layered_image_indexing: Option<AV1LayeredImageIndexing>,
923    major_brand: [u8; 4],
924    compatible_brands: std::vec::Vec<[u8; 4]>,
925}
926
927struct AnimationParserData {
928    media_timescale: u32,
929    sample_table: SampleTable,
930    loop_count: u32,
931}
932
933/// Animation metadata from [`AvifParser`]
934#[derive(Debug, Clone, Copy)]
935pub struct AnimationInfo {
936    pub frame_count: usize,
937    pub loop_count: u32,
938}
939
940/// Parsed structure from the box-level parse pass (no mdat data).
941struct ParsedStructure {
942    meta: AvifInternalMeta,
943    mdat_bounds: TryVec<MdatBounds>,
944    animation_data: Option<(u32, SampleTable, u32)>,
945    major_brand: [u8; 4],
946    compatible_brands: std::vec::Vec<[u8; 4]>,
947}
948
949impl<'data> AvifParser<'data> {
950    // ========================================
951    // Constructors
952    // ========================================
953
954    /// Parse AVIF from a borrowed byte slice (true zero-copy).
955    ///
956    /// The returned parser borrows `data` — single-extent items will be
957    /// returned as `Cow::Borrowed` slices into this buffer.
958    pub fn from_bytes(data: &'data [u8]) -> Result<Self> {
959        Self::from_bytes_with_config(data, &DecodeConfig::unlimited(), &Unstoppable)
960    }
961
962    /// Parse AVIF from a borrowed byte slice with resource limits.
963    pub fn from_bytes_with_config(
964        data: &'data [u8],
965        config: &DecodeConfig,
966        stop: &dyn Stop,
967    ) -> Result<Self> {
968        let parsed = Self::parse_raw(data, config, stop)?;
969        Self::build(Cow::Borrowed(data), parsed, config)
970    }
971
972    /// Parse AVIF from an owned buffer.
973    ///
974    /// The returned parser owns the data — single-extent items will still
975    /// be returned as `Cow::Borrowed` slices (borrowing from the internal buffer).
976    pub fn from_owned(data: std::vec::Vec<u8>) -> Result<AvifParser<'static>> {
977        AvifParser::from_owned_with_config(data, &DecodeConfig::unlimited(), &Unstoppable)
978    }
979
980    /// Parse AVIF from an owned buffer with resource limits.
981    pub fn from_owned_with_config(
982        data: std::vec::Vec<u8>,
983        config: &DecodeConfig,
984        stop: &dyn Stop,
985    ) -> Result<AvifParser<'static>> {
986        let parsed = AvifParser::parse_raw(&data, config, stop)?;
987        AvifParser::build(Cow::Owned(data), parsed, config)
988    }
989
990    /// Parse AVIF from a reader (reads all bytes, then parses).
991    pub fn from_reader<R: Read>(reader: &mut R) -> Result<AvifParser<'static>> {
992        AvifParser::from_reader_with_config(reader, &DecodeConfig::unlimited(), &Unstoppable)
993    }
994
995    /// Parse AVIF from a reader with resource limits.
996    pub fn from_reader_with_config<R: Read>(
997        reader: &mut R,
998        config: &DecodeConfig,
999        stop: &dyn Stop,
1000    ) -> Result<AvifParser<'static>> {
1001        let mut buf = std::vec::Vec::new();
1002        reader.read_to_end(&mut buf)?;
1003        AvifParser::from_owned_with_config(buf, config, stop)
1004    }
1005
1006    // ========================================
1007    // Internal: parse pass (records offsets, no mdat copy)
1008    // ========================================
1009
1010    /// Parse the AVIF box structure from raw bytes, recording mdat offsets
1011    /// without copying mdat content.
1012    fn parse_raw(data: &[u8], config: &DecodeConfig, stop: &dyn Stop) -> Result<ParsedStructure> {
1013        let parse_opts = ParseOptions { lenient: config.lenient };
1014        let mut cursor = std::io::Cursor::new(data);
1015        let mut f = OffsetReader::new(&mut cursor);
1016        let mut iter = BoxIter::new(&mut f);
1017
1018        // 'ftyp' box must occur first; see ISO 14496-12:2015 § 4.3.1
1019        let (major_brand, compatible_brands) = if let Some(mut b) = iter.next_box()? {
1020            if b.head.name == BoxType::FileTypeBox {
1021                let ftyp = read_ftyp(&mut b)?;
1022                if ftyp.major_brand != b"avif" && ftyp.major_brand != b"avis" {
1023                    return Err(Error::InvalidData("ftyp must be 'avif' or 'avis'"));
1024                }
1025                let major = ftyp.major_brand.value;
1026                let compat = ftyp.compatible_brands.iter().map(|b| b.value).collect();
1027                (major, compat)
1028            } else {
1029                return Err(Error::InvalidData("'ftyp' box must occur first"));
1030            }
1031        } else {
1032            return Err(Error::InvalidData("'ftyp' box must occur first"));
1033        };
1034
1035        let mut meta = None;
1036        let mut mdat_bounds = TryVec::new();
1037        let mut animation_data: Option<(u32, SampleTable, u32)> = None;
1038
1039        while let Some(mut b) = iter.next_box()? {
1040            stop.check()?;
1041
1042            match b.head.name {
1043                BoxType::MetadataBox => {
1044                    if meta.is_some() {
1045                        return Err(Error::InvalidData(
1046                            "There should be zero or one meta boxes per ISO 14496-12:2015 § 8.11.1.1",
1047                        ));
1048                    }
1049                    meta = Some(read_avif_meta(&mut b, &parse_opts)?);
1050                }
1051                BoxType::MovieBox => {
1052                    if let Some((media_timescale, sample_table)) = read_moov(&mut b)? {
1053                        animation_data = Some((media_timescale, sample_table, 0));
1054                    }
1055                }
1056                BoxType::MediaDataBox => {
1057                    if b.bytes_left() > 0 {
1058                        let offset = b.offset();
1059                        let length = b.bytes_left();
1060                        mdat_bounds.push(MdatBounds { offset, length })?;
1061                    }
1062                    // Skip the content — we'll slice into raw later
1063                    skip_box_content(&mut b)?;
1064                }
1065                _ => skip_box_content(&mut b)?,
1066            }
1067
1068            check_parser_state(&b.head, &b.content)?;
1069        }
1070
1071        let meta = meta.ok_or(Error::InvalidData("missing meta"))?;
1072
1073        Ok(ParsedStructure { meta, mdat_bounds, animation_data, major_brand, compatible_brands })
1074    }
1075
1076    /// Build an AvifParser from raw bytes + parsed structure.
1077    fn build(raw: Cow<'data, [u8]>, parsed: ParsedStructure, config: &DecodeConfig) -> Result<Self> {
1078        let tracker = ResourceTracker::new(config);
1079        let meta = parsed.meta;
1080
1081        // Get primary item extents
1082        let primary = Self::get_item_extents(&meta, meta.primary_item_id)?;
1083
1084        // Find alpha item and get its extents
1085        let alpha_item_id = meta
1086            .item_references
1087            .iter()
1088            .filter(|iref| {
1089                iref.to_item_id == meta.primary_item_id
1090                    && iref.from_item_id != meta.primary_item_id
1091                    && iref.item_type == b"auxl"
1092            })
1093            .map(|iref| iref.from_item_id)
1094            .find(|&item_id| {
1095                meta.properties.iter().any(|prop| {
1096                    prop.item_id == item_id
1097                        && match &prop.property {
1098                            ItemProperty::AuxiliaryType(urn) => {
1099                                urn.type_subtype().0 == b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"
1100                            }
1101                            _ => false,
1102                        }
1103                })
1104            });
1105
1106        let alpha = alpha_item_id
1107            .map(|id| Self::get_item_extents(&meta, id))
1108            .transpose()?;
1109
1110        // Check for premultiplied alpha
1111        let premultiplied_alpha = alpha_item_id.is_some_and(|alpha_id| {
1112            meta.item_references.iter().any(|iref| {
1113                iref.from_item_id == meta.primary_item_id
1114                    && iref.to_item_id == alpha_id
1115                    && iref.item_type == b"prem"
1116            })
1117        });
1118
1119        // Check if primary item is a grid (tiled image)
1120        let is_grid = meta
1121            .item_infos
1122            .iter()
1123            .find(|x| x.item_id == meta.primary_item_id)
1124            .is_some_and(|info| info.item_type == b"grid");
1125
1126        // Extract grid configuration and tile extents if this is a grid
1127        let (grid_config, tiles) = if is_grid {
1128            let mut tiles_with_index: TryVec<(u32, u16)> = TryVec::new();
1129            for iref in meta.item_references.iter() {
1130                if iref.from_item_id == meta.primary_item_id && iref.item_type == b"dimg" {
1131                    tiles_with_index.push((iref.to_item_id, iref.reference_index))?;
1132                }
1133            }
1134
1135            tracker.validate_grid_tiles(tiles_with_index.len() as u32)?;
1136            tiles_with_index.sort_by_key(|&(_, idx)| idx);
1137
1138            let mut tile_extents = TryVec::new();
1139            for (tile_id, _) in tiles_with_index.iter() {
1140                tile_extents.push(Self::get_item_extents(&meta, *tile_id)?)?;
1141            }
1142
1143            let mut tile_ids = TryVec::new();
1144            for (tile_id, _) in tiles_with_index.iter() {
1145                tile_ids.push(*tile_id)?;
1146            }
1147
1148            let grid_config = Self::calculate_grid_config(&meta, &tile_ids)?;
1149
1150            // AVIF 1.2: transformative properties SHALL NOT be on grid tile items
1151            for (tile_id, _) in tiles_with_index.iter() {
1152                for prop in meta.properties.iter() {
1153                    if prop.item_id == *tile_id {
1154                        match &prop.property {
1155                            ItemProperty::Rotation(_)
1156                            | ItemProperty::Mirror(_)
1157                            | ItemProperty::CleanAperture(_) => {
1158                                warn!("grid tile {} has a transformative property (irot/imir/clap), violating AVIF spec", tile_id);
1159                            }
1160                            _ => {}
1161                        }
1162                    }
1163                }
1164            }
1165
1166            (Some(grid_config), tile_extents)
1167        } else {
1168            (None, TryVec::new())
1169        };
1170
1171        // Extract properties for the primary item
1172        macro_rules! find_prop {
1173            ($variant:ident) => {
1174                meta.properties.iter().find_map(|p| {
1175                    if p.item_id == meta.primary_item_id {
1176                        match &p.property {
1177                            ItemProperty::$variant(c) => Some(c.clone()),
1178                            _ => None,
1179                        }
1180                    } else {
1181                        None
1182                    }
1183                })
1184            };
1185        }
1186
1187        let av1_config = find_prop!(AV1Config);
1188        let color_info = find_prop!(ColorInformation);
1189        let rotation = find_prop!(Rotation);
1190        let mirror = find_prop!(Mirror);
1191        let clean_aperture = find_prop!(CleanAperture);
1192        let pixel_aspect_ratio = find_prop!(PixelAspectRatio);
1193        let content_light_level = find_prop!(ContentLightLevel);
1194        let mastering_display = find_prop!(MasteringDisplayColourVolume);
1195        let content_colour_volume = find_prop!(ContentColourVolume);
1196        let ambient_viewing = find_prop!(AmbientViewingEnvironment);
1197        let operating_point = find_prop!(OperatingPointSelector);
1198        let layer_selector = find_prop!(LayerSelector);
1199        let layered_image_indexing = find_prop!(AV1LayeredImageIndexing);
1200
1201        // Store animation metadata if present
1202        let animation_data = if let Some((media_timescale, sample_table, loop_count)) = parsed.animation_data {
1203            tracker.validate_animation_frames(sample_table.sample_sizes.len() as u32)?;
1204            Some(AnimationParserData { media_timescale, sample_table, loop_count })
1205        } else {
1206            None
1207        };
1208
1209        // Clone idat
1210        let idat = if let Some(ref idat_data) = meta.idat {
1211            let mut cloned = TryVec::new();
1212            cloned.extend_from_slice(idat_data)?;
1213            Some(cloned)
1214        } else {
1215            None
1216        };
1217
1218        Ok(Self {
1219            raw,
1220            mdat_bounds: parsed.mdat_bounds,
1221            idat,
1222            primary,
1223            alpha,
1224            grid_config,
1225            tiles,
1226            animation_data,
1227            premultiplied_alpha,
1228            av1_config,
1229            color_info,
1230            rotation,
1231            mirror,
1232            clean_aperture,
1233            pixel_aspect_ratio,
1234            content_light_level,
1235            mastering_display,
1236            content_colour_volume,
1237            ambient_viewing,
1238            operating_point,
1239            layer_selector,
1240            layered_image_indexing,
1241            major_brand: parsed.major_brand,
1242            compatible_brands: parsed.compatible_brands,
1243        })
1244    }
1245
1246    // ========================================
1247    // Internal helpers
1248    // ========================================
1249
1250    /// Get item extents (construction method + ranges) from metadata.
1251    fn get_item_extents(meta: &AvifInternalMeta, item_id: u32) -> Result<ItemExtents> {
1252        let item = meta
1253            .iloc_items
1254            .iter()
1255            .find(|item| item.item_id == item_id)
1256            .ok_or(Error::InvalidData("item not found in iloc"))?;
1257
1258        let mut extents = TryVec::new();
1259        for extent in &item.extents {
1260            extents.push(extent.extent_range.clone())?;
1261        }
1262        Ok(ItemExtents {
1263            construction_method: item.construction_method,
1264            extents,
1265        })
1266    }
1267
1268    /// Resolve an item's data from the raw buffer, returning `Cow::Borrowed`
1269    /// for single-extent file items and `Cow::Owned` for multi-extent or idat.
1270    fn resolve_item(&self, item: &ItemExtents) -> Result<Cow<'_, [u8]>> {
1271        match item.construction_method {
1272            ConstructionMethod::Idat => self.resolve_idat_extents(&item.extents),
1273            ConstructionMethod::File => self.resolve_file_extents(&item.extents),
1274            ConstructionMethod::Item => Err(Error::Unsupported("construction_method 'item' not supported")),
1275        }
1276    }
1277
1278    /// Resolve file-based extents from the raw buffer.
1279    fn resolve_file_extents(&self, extents: &[ExtentRange]) -> Result<Cow<'_, [u8]>> {
1280        let raw = self.raw.as_ref();
1281
1282        // Fast path: single extent → borrow directly from raw
1283        if extents.len() == 1 {
1284            let extent = &extents[0];
1285            let (start, end) = self.extent_byte_range(extent)?;
1286            let slice = raw.get(start..end).ok_or(Error::InvalidData("extent out of bounds in raw buffer"))?;
1287            return Ok(Cow::Borrowed(slice));
1288        }
1289
1290        // Multi-extent: concatenate into owned buffer
1291        let mut data = TryVec::new();
1292        for extent in extents {
1293            let (start, end) = self.extent_byte_range(extent)?;
1294            let slice = raw.get(start..end).ok_or(Error::InvalidData("extent out of bounds in raw buffer"))?;
1295            data.extend_from_slice(slice)?;
1296        }
1297        Ok(Cow::Owned(data.to_vec()))
1298    }
1299
1300    /// Convert an ExtentRange to a (start, end) byte range within the raw buffer.
1301    fn extent_byte_range(&self, extent: &ExtentRange) -> Result<(usize, usize)> {
1302        let file_offset = extent.start();
1303        let start = usize::try_from(file_offset)?;
1304
1305        match extent {
1306            ExtentRange::WithLength(range) => {
1307                let len = range.end.checked_sub(range.start)
1308                    .ok_or(Error::InvalidData("extent range start > end"))?;
1309                let end = start.checked_add(usize::try_from(len)?)
1310                    .ok_or(Error::InvalidData("extent end overflow"))?;
1311                Ok((start, end))
1312            }
1313            ExtentRange::ToEnd(_) => {
1314                // Find the mdat that contains this offset and use its bounds
1315                for mdat in &self.mdat_bounds {
1316                    if file_offset >= mdat.offset && file_offset < mdat.offset + mdat.length {
1317                        let end = usize::try_from(mdat.offset + mdat.length)?;
1318                        return Ok((start, end));
1319                    }
1320                }
1321                // Fall back to end of raw buffer
1322                Ok((start, self.raw.len()))
1323            }
1324        }
1325    }
1326
1327    /// Resolve idat-based extents.
1328    fn resolve_idat_extents(&self, extents: &[ExtentRange]) -> Result<Cow<'_, [u8]>> {
1329        let idat_data = self.idat.as_ref()
1330            .ok_or(Error::InvalidData("idat box missing but construction_method is Idat"))?;
1331
1332        if extents.len() == 1 {
1333            let extent = &extents[0];
1334            let start = usize::try_from(extent.start())?;
1335            let slice = match extent {
1336                ExtentRange::WithLength(range) => {
1337                    let len = usize::try_from(range.end - range.start)?;
1338                    idat_data.get(start..start + len)
1339                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1340                }
1341                ExtentRange::ToEnd(_) => {
1342                    idat_data.get(start..)
1343                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1344                }
1345            };
1346            return Ok(Cow::Borrowed(slice));
1347        }
1348
1349        // Multi-extent idat: concatenate
1350        let mut data = TryVec::new();
1351        for extent in extents {
1352            let start = usize::try_from(extent.start())?;
1353            let slice = match extent {
1354                ExtentRange::WithLength(range) => {
1355                    let len = usize::try_from(range.end - range.start)?;
1356                    idat_data.get(start..start + len)
1357                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1358                }
1359                ExtentRange::ToEnd(_) => {
1360                    idat_data.get(start..)
1361                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1362                }
1363            };
1364            data.extend_from_slice(slice)?;
1365        }
1366        Ok(Cow::Owned(data.to_vec()))
1367    }
1368
1369    /// Resolve a single animation frame from the raw buffer.
1370    fn resolve_frame(&self, index: usize) -> Result<FrameRef<'_>> {
1371        let anim = self.animation_data.as_ref()
1372            .ok_or(Error::InvalidData("not an animated AVIF"))?;
1373
1374        if index >= anim.sample_table.sample_sizes.len() {
1375            return Err(Error::InvalidData("frame index out of bounds"));
1376        }
1377
1378        let duration_ms = self.calculate_frame_duration(&anim.sample_table, anim.media_timescale, index)?;
1379        let (offset, size) = self.calculate_sample_location(&anim.sample_table, index)?;
1380
1381        let start = usize::try_from(offset)?;
1382        let end = start.checked_add(size as usize)
1383            .ok_or(Error::InvalidData("frame end overflow"))?;
1384
1385        let raw = self.raw.as_ref();
1386        let slice = raw.get(start..end)
1387            .ok_or(Error::InvalidData("frame not found in raw buffer"))?;
1388
1389        Ok(FrameRef {
1390            data: Cow::Borrowed(slice),
1391            duration_ms,
1392        })
1393    }
1394
1395    /// Calculate grid configuration from metadata.
1396    fn calculate_grid_config(meta: &AvifInternalMeta, tile_ids: &[u32]) -> Result<GridConfig> {
1397        // Try explicit grid property first
1398        for prop in &meta.properties {
1399            if prop.item_id == meta.primary_item_id
1400                && let ItemProperty::ImageGrid(grid) = &prop.property {
1401                    return Ok(grid.clone());
1402                }
1403        }
1404
1405        // Fall back to ispe calculation
1406        let grid_dims = meta
1407            .properties
1408            .iter()
1409            .find(|p| p.item_id == meta.primary_item_id)
1410            .and_then(|p| match &p.property {
1411                ItemProperty::ImageSpatialExtents(e) => Some(e),
1412                _ => None,
1413            });
1414
1415        let tile_dims = tile_ids.first().and_then(|&tile_id| {
1416            meta.properties
1417                .iter()
1418                .find(|p| p.item_id == tile_id)
1419                .and_then(|p| match &p.property {
1420                    ItemProperty::ImageSpatialExtents(e) => Some(e),
1421                    _ => None,
1422                })
1423        });
1424
1425        if let (Some(grid), Some(tile)) = (grid_dims, tile_dims)
1426            && tile.width != 0
1427                && tile.height != 0
1428                && grid.width % tile.width == 0
1429                && grid.height % tile.height == 0
1430            {
1431                let columns = grid.width / tile.width;
1432                let rows = grid.height / tile.height;
1433
1434                if columns <= 255 && rows <= 255 {
1435                    return Ok(GridConfig {
1436                        rows: rows as u8,
1437                        columns: columns as u8,
1438                        output_width: grid.width,
1439                        output_height: grid.height,
1440                    });
1441                }
1442            }
1443
1444        let tile_count = tile_ids.len();
1445        Ok(GridConfig {
1446            rows: tile_count.min(255) as u8,
1447            columns: 1,
1448            output_width: 0,
1449            output_height: 0,
1450        })
1451    }
1452
1453    /// Calculate frame duration from sample table.
1454    fn calculate_frame_duration(
1455        &self,
1456        st: &SampleTable,
1457        timescale: u32,
1458        index: usize,
1459    ) -> Result<u32> {
1460        let mut current_sample = 0;
1461        for entry in &st.time_to_sample {
1462            if current_sample + entry.sample_count as usize > index {
1463                let duration_ms = if timescale > 0 {
1464                    ((entry.sample_delta as u64) * 1000) / (timescale as u64)
1465                } else {
1466                    0
1467                };
1468                return Ok(duration_ms as u32);
1469            }
1470            current_sample += entry.sample_count as usize;
1471        }
1472        Ok(0)
1473    }
1474
1475    /// Calculate sample location (offset and size) from sample table.
1476    fn calculate_sample_location(&self, st: &SampleTable, index: usize) -> Result<(u64, u32)> {
1477        let sample_size = *st
1478            .sample_sizes
1479            .get(index)
1480            .ok_or(Error::InvalidData("sample index out of bounds"))?;
1481
1482        let mut current_sample = 0;
1483        for (chunk_map_idx, entry) in st.sample_to_chunk.iter().enumerate() {
1484            let next_first_chunk = st
1485                .sample_to_chunk
1486                .get(chunk_map_idx + 1)
1487                .map(|e| e.first_chunk)
1488                .unwrap_or(u32::MAX);
1489
1490            for chunk_idx in entry.first_chunk..next_first_chunk {
1491                if chunk_idx == 0 || (chunk_idx as usize) > st.chunk_offsets.len() {
1492                    break;
1493                }
1494
1495                let chunk_offset = st.chunk_offsets[(chunk_idx - 1) as usize];
1496
1497                for sample_in_chunk in 0..entry.samples_per_chunk {
1498                    if current_sample == index {
1499                        let mut offset_in_chunk = 0u64;
1500                        for s in 0..sample_in_chunk {
1501                            let prev_idx = current_sample.saturating_sub((sample_in_chunk - s) as usize);
1502                            if let Some(&prev_size) = st.sample_sizes.get(prev_idx) {
1503                                offset_in_chunk += prev_size as u64;
1504                            }
1505                        }
1506
1507                        return Ok((chunk_offset + offset_in_chunk, sample_size));
1508                    }
1509                    current_sample += 1;
1510                }
1511            }
1512        }
1513
1514        Err(Error::InvalidData("sample not found in chunk table"))
1515    }
1516
1517    // ========================================
1518    // Public data access API (one way each)
1519    // ========================================
1520
1521    /// Get primary item data.
1522    ///
1523    /// Returns `Cow::Borrowed` for single-extent items, `Cow::Owned` for multi-extent.
1524    pub fn primary_data(&self) -> Result<Cow<'_, [u8]>> {
1525        self.resolve_item(&self.primary)
1526    }
1527
1528    /// Get alpha item data, if present.
1529    pub fn alpha_data(&self) -> Option<Result<Cow<'_, [u8]>>> {
1530        self.alpha.as_ref().map(|item| self.resolve_item(item))
1531    }
1532
1533    /// Get grid tile data by index.
1534    pub fn tile_data(&self, index: usize) -> Result<Cow<'_, [u8]>> {
1535        let item = self.tiles.get(index)
1536            .ok_or(Error::InvalidData("tile index out of bounds"))?;
1537        self.resolve_item(item)
1538    }
1539
1540    /// Get a single animation frame by index.
1541    pub fn frame(&self, index: usize) -> Result<FrameRef<'_>> {
1542        self.resolve_frame(index)
1543    }
1544
1545    /// Iterate over all animation frames.
1546    pub fn frames(&self) -> FrameIterator<'_> {
1547        let count = self
1548            .animation_info()
1549            .map(|info| info.frame_count)
1550            .unwrap_or(0);
1551        FrameIterator { parser: self, index: 0, count }
1552    }
1553
1554    // ========================================
1555    // Metadata (no data access)
1556    // ========================================
1557
1558    /// Get animation metadata (if animated).
1559    pub fn animation_info(&self) -> Option<AnimationInfo> {
1560        self.animation_data.as_ref().map(|data| AnimationInfo {
1561            frame_count: data.sample_table.sample_sizes.len(),
1562            loop_count: data.loop_count,
1563        })
1564    }
1565
1566    /// Get grid configuration (if grid image).
1567    pub fn grid_config(&self) -> Option<&GridConfig> {
1568        self.grid_config.as_ref()
1569    }
1570
1571    /// Get number of grid tiles.
1572    pub fn grid_tile_count(&self) -> usize {
1573        self.tiles.len()
1574    }
1575
1576    /// Check if alpha channel uses premultiplied alpha.
1577    pub fn premultiplied_alpha(&self) -> bool {
1578        self.premultiplied_alpha
1579    }
1580
1581    /// Get the AV1 codec configuration for the primary item, if present.
1582    ///
1583    /// This is parsed from the `av1C` property box in the container.
1584    pub fn av1_config(&self) -> Option<&AV1Config> {
1585        self.av1_config.as_ref()
1586    }
1587
1588    /// Get colour information for the primary item, if present.
1589    ///
1590    /// This is parsed from the `colr` property box in the container.
1591    /// For CICP/nclx values, this is the authoritative source and may
1592    /// differ from values in the AV1 bitstream sequence header.
1593    pub fn color_info(&self) -> Option<&ColorInformation> {
1594        self.color_info.as_ref()
1595    }
1596
1597    /// Get rotation for the primary item, if present.
1598    pub fn rotation(&self) -> Option<&ImageRotation> {
1599        self.rotation.as_ref()
1600    }
1601
1602    /// Get mirror for the primary item, if present.
1603    pub fn mirror(&self) -> Option<&ImageMirror> {
1604        self.mirror.as_ref()
1605    }
1606
1607    /// Get clean aperture (crop) for the primary item, if present.
1608    pub fn clean_aperture(&self) -> Option<&CleanAperture> {
1609        self.clean_aperture.as_ref()
1610    }
1611
1612    /// Get pixel aspect ratio for the primary item, if present.
1613    pub fn pixel_aspect_ratio(&self) -> Option<&PixelAspectRatio> {
1614        self.pixel_aspect_ratio.as_ref()
1615    }
1616
1617    /// Get content light level info for the primary item, if present.
1618    pub fn content_light_level(&self) -> Option<&ContentLightLevel> {
1619        self.content_light_level.as_ref()
1620    }
1621
1622    /// Get mastering display colour volume for the primary item, if present.
1623    pub fn mastering_display(&self) -> Option<&MasteringDisplayColourVolume> {
1624        self.mastering_display.as_ref()
1625    }
1626
1627    /// Get content colour volume for the primary item, if present.
1628    pub fn content_colour_volume(&self) -> Option<&ContentColourVolume> {
1629        self.content_colour_volume.as_ref()
1630    }
1631
1632    /// Get ambient viewing environment for the primary item, if present.
1633    pub fn ambient_viewing(&self) -> Option<&AmbientViewingEnvironment> {
1634        self.ambient_viewing.as_ref()
1635    }
1636
1637    /// Get operating point selector for the primary item, if present.
1638    pub fn operating_point(&self) -> Option<&OperatingPointSelector> {
1639        self.operating_point.as_ref()
1640    }
1641
1642    /// Get layer selector for the primary item, if present.
1643    pub fn layer_selector(&self) -> Option<&LayerSelector> {
1644        self.layer_selector.as_ref()
1645    }
1646
1647    /// Get AV1 layered image indexing for the primary item, if present.
1648    pub fn layered_image_indexing(&self) -> Option<&AV1LayeredImageIndexing> {
1649        self.layered_image_indexing.as_ref()
1650    }
1651
1652    /// Get the major brand from the `ftyp` box (e.g., `*b"avif"` or `*b"avis"`).
1653    pub fn major_brand(&self) -> &[u8; 4] {
1654        &self.major_brand
1655    }
1656
1657    /// Get the compatible brands from the `ftyp` box.
1658    pub fn compatible_brands(&self) -> &[[u8; 4]] {
1659        &self.compatible_brands
1660    }
1661
1662    /// Parse AV1 metadata from the primary item.
1663    pub fn primary_metadata(&self) -> Result<AV1Metadata> {
1664        let data = self.primary_data()?;
1665        AV1Metadata::parse_av1_bitstream(&data)
1666    }
1667
1668    /// Parse AV1 metadata from the alpha item, if present.
1669    pub fn alpha_metadata(&self) -> Option<Result<AV1Metadata>> {
1670        self.alpha.as_ref().map(|item| {
1671            let data = self.resolve_item(item)?;
1672            AV1Metadata::parse_av1_bitstream(&data)
1673        })
1674    }
1675
1676    // ========================================
1677    // Conversion
1678    // ========================================
1679
1680    /// Convert to [`AvifData`] (eagerly loads all frames and tiles).
1681    ///
1682    /// Provided for migration from the eager API. Prefer using `AvifParser`
1683    /// methods directly.
1684    #[cfg(feature = "eager")]
1685    #[deprecated(since = "1.5.0", note = "Use AvifParser methods directly instead of converting to AvifData")]
1686    #[allow(deprecated)]
1687    pub fn to_avif_data(&self) -> Result<AvifData> {
1688        let primary_data = self.primary_data()?;
1689        let mut primary_item = TryVec::new();
1690        primary_item.extend_from_slice(&primary_data)?;
1691
1692        let alpha_item = match self.alpha_data() {
1693            Some(Ok(data)) => {
1694                let mut v = TryVec::new();
1695                v.extend_from_slice(&data)?;
1696                Some(v)
1697            }
1698            Some(Err(e)) => return Err(e),
1699            None => None,
1700        };
1701
1702        let mut grid_tiles = TryVec::new();
1703        for i in 0..self.grid_tile_count() {
1704            let data = self.tile_data(i)?;
1705            let mut v = TryVec::new();
1706            v.extend_from_slice(&data)?;
1707            grid_tiles.push(v)?;
1708        }
1709
1710        let animation = if let Some(info) = self.animation_info() {
1711            let mut frames = TryVec::new();
1712            for i in 0..info.frame_count {
1713                let frame_ref = self.frame(i)?;
1714                let mut data = TryVec::new();
1715                data.extend_from_slice(&frame_ref.data)?;
1716                frames.push(AnimationFrame { data, duration_ms: frame_ref.duration_ms })?;
1717            }
1718            Some(AnimationConfig {
1719                loop_count: info.loop_count,
1720                frames,
1721            })
1722        } else {
1723            None
1724        };
1725
1726        Ok(AvifData {
1727            primary_item,
1728            alpha_item,
1729            premultiplied_alpha: self.premultiplied_alpha,
1730            grid_config: self.grid_config.clone(),
1731            grid_tiles,
1732            animation,
1733            av1_config: self.av1_config.clone(),
1734            color_info: self.color_info.clone(),
1735            rotation: self.rotation,
1736            mirror: self.mirror,
1737            clean_aperture: self.clean_aperture,
1738            pixel_aspect_ratio: self.pixel_aspect_ratio,
1739            content_light_level: self.content_light_level,
1740            mastering_display: self.mastering_display,
1741            content_colour_volume: self.content_colour_volume,
1742            ambient_viewing: self.ambient_viewing,
1743            operating_point: self.operating_point,
1744            layer_selector: self.layer_selector,
1745            layered_image_indexing: self.layered_image_indexing,
1746            major_brand: self.major_brand,
1747            compatible_brands: self.compatible_brands.clone(),
1748        })
1749    }
1750}
1751
1752/// Iterator over animation frames.
1753///
1754/// Created by [`AvifParser::frames()`]. Yields [`FrameRef`] on demand.
1755pub struct FrameIterator<'a> {
1756    parser: &'a AvifParser<'a>,
1757    index: usize,
1758    count: usize,
1759}
1760
1761impl<'a> Iterator for FrameIterator<'a> {
1762    type Item = Result<FrameRef<'a>>;
1763
1764    fn next(&mut self) -> Option<Self::Item> {
1765        if self.index >= self.count {
1766            return None;
1767        }
1768        let result = self.parser.frame(self.index);
1769        self.index += 1;
1770        Some(result)
1771    }
1772
1773    fn size_hint(&self) -> (usize, Option<usize>) {
1774        let remaining = self.count.saturating_sub(self.index);
1775        (remaining, Some(remaining))
1776    }
1777}
1778
1779impl ExactSizeIterator for FrameIterator<'_> {
1780    fn len(&self) -> usize {
1781        self.count.saturating_sub(self.index)
1782    }
1783}
1784
1785struct AvifInternalMeta {
1786    item_references: TryVec<SingleItemTypeReferenceBox>,
1787    properties: TryVec<AssociatedProperty>,
1788    primary_item_id: u32,
1789    iloc_items: TryVec<ItemLocationBoxItem>,
1790    item_infos: TryVec<ItemInfoEntry>,
1791    idat: Option<TryVec<u8>>,
1792}
1793
1794/// A Media Data Box
1795/// See ISO 14496-12:2015 § 8.1.1
1796#[cfg(feature = "eager")]
1797struct MediaDataBox {
1798    /// Offset of `data` from the beginning of the file. See `ConstructionMethod::File`
1799    offset: u64,
1800    data: TryVec<u8>,
1801}
1802
1803#[cfg(feature = "eager")]
1804impl MediaDataBox {
1805    /// Check whether the beginning of `extent` is within the bounds of the `MediaDataBox`.
1806    /// We assume extents to not cross box boundaries. If so, this will cause an error
1807    /// in `read_extent`.
1808    fn contains_extent(&self, extent: &ExtentRange) -> bool {
1809        if self.offset <= extent.start() {
1810            let start_offset = extent.start() - self.offset;
1811            start_offset < self.data.len().to_u64()
1812        } else {
1813            false
1814        }
1815    }
1816
1817    /// Check whether `extent` covers the `MediaDataBox` exactly.
1818    fn matches_extent(&self, extent: &ExtentRange) -> bool {
1819        if self.offset == extent.start() {
1820            match extent {
1821                ExtentRange::WithLength(range) => {
1822                    if let Some(end) = self.offset.checked_add(self.data.len().to_u64()) {
1823                        end == range.end
1824                    } else {
1825                        false
1826                    }
1827                },
1828                ExtentRange::ToEnd(_) => true,
1829            }
1830        } else {
1831            false
1832        }
1833    }
1834
1835    /// Copy the range specified by `extent` to the end of `buf` or return an error if the range
1836    /// is not fully contained within `MediaDataBox`.
1837    fn read_extent(&self, extent: &ExtentRange, buf: &mut TryVec<u8>) -> Result<()> {
1838        let start_offset = extent
1839            .start()
1840            .checked_sub(self.offset)
1841            .ok_or(Error::InvalidData("mdat does not contain extent"))?;
1842        let slice = match extent {
1843            ExtentRange::WithLength(range) => {
1844                let range_len = range
1845                    .end
1846                    .checked_sub(range.start)
1847                    .ok_or(Error::InvalidData("range start > end"))?;
1848                let end = start_offset
1849                    .checked_add(range_len)
1850                    .ok_or(Error::InvalidData("extent end overflow"))?;
1851                self.data.get(start_offset.try_into()?..end.try_into()?)
1852            },
1853            ExtentRange::ToEnd(_) => self.data.get(start_offset.try_into()?..),
1854        };
1855        let slice = slice.ok_or(Error::InvalidData("extent crosses box boundary"))?;
1856        buf.extend_from_slice(slice)?;
1857        Ok(())
1858    }
1859
1860}
1861
1862/// Used for 'infe' boxes within 'iinf' boxes
1863/// See ISO 14496-12:2015 § 8.11.6
1864/// Only versions {2, 3} are supported
1865#[derive(Debug)]
1866struct ItemInfoEntry {
1867    item_id: u32,
1868    item_type: FourCC,
1869}
1870
1871/// See ISO 14496-12:2015 § 8.11.12
1872#[derive(Debug)]
1873struct SingleItemTypeReferenceBox {
1874    item_type: FourCC,
1875    from_item_id: u32,
1876    to_item_id: u32,
1877    /// Index of this reference within the list of references of the same type from the same item
1878    /// (0-based). This is the dimgIdx for grid tiles.
1879    reference_index: u16,
1880}
1881
1882/// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box
1883/// See ISO 14496-12:2015 § 8.11.3
1884#[derive(Debug)]
1885enum IlocFieldSize {
1886    Zero,
1887    Four,
1888    Eight,
1889}
1890
1891impl IlocFieldSize {
1892    const fn to_bits(&self) -> u8 {
1893        match self {
1894            Self::Zero => 0,
1895            Self::Four => 32,
1896            Self::Eight => 64,
1897        }
1898    }
1899}
1900
1901impl TryFrom<u8> for IlocFieldSize {
1902    type Error = Error;
1903
1904    fn try_from(value: u8) -> Result<Self> {
1905        match value {
1906            0 => Ok(Self::Zero),
1907            4 => Ok(Self::Four),
1908            8 => Ok(Self::Eight),
1909            _ => Err(Error::InvalidData("value must be in the set {0, 4, 8}")),
1910        }
1911    }
1912}
1913
1914#[derive(PartialEq)]
1915enum IlocVersion {
1916    Zero,
1917    One,
1918    Two,
1919}
1920
1921impl TryFrom<u8> for IlocVersion {
1922    type Error = Error;
1923
1924    fn try_from(value: u8) -> Result<Self> {
1925        match value {
1926            0 => Ok(Self::Zero),
1927            1 => Ok(Self::One),
1928            2 => Ok(Self::Two),
1929            _ => Err(Error::Unsupported("unsupported version in 'iloc' box")),
1930        }
1931    }
1932}
1933
1934/// Used for 'iloc' boxes
1935/// See ISO 14496-12:2015 § 8.11.3
1936/// `base_offset` is omitted since it is integrated into the ranges in `extents`
1937/// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported
1938#[derive(Debug)]
1939struct ItemLocationBoxItem {
1940    item_id: u32,
1941    construction_method: ConstructionMethod,
1942    /// Unused for `ConstructionMethod::Idat`
1943    extents: TryVec<ItemLocationBoxExtent>,
1944}
1945
1946#[derive(Clone, Copy, Debug, PartialEq)]
1947enum ConstructionMethod {
1948    File,
1949    Idat,
1950    #[allow(dead_code)] // TODO: see https://github.com/mozilla/mp4parse-rust/issues/196
1951    Item,
1952}
1953
1954/// `extent_index` is omitted since it's only used for `ConstructionMethod::Item` which
1955/// is currently not implemented.
1956#[derive(Clone, Debug)]
1957struct ItemLocationBoxExtent {
1958    extent_range: ExtentRange,
1959}
1960
1961#[derive(Clone, Debug)]
1962enum ExtentRange {
1963    WithLength(Range<u64>),
1964    ToEnd(RangeFrom<u64>),
1965}
1966
1967impl ExtentRange {
1968    const fn start(&self) -> u64 {
1969        match self {
1970            Self::WithLength(r) => r.start,
1971            Self::ToEnd(r) => r.start,
1972        }
1973    }
1974}
1975
1976/// See ISO 14496-12:2015 § 4.2
1977struct BMFFBox<'a, T> {
1978    head: BoxHeader,
1979    content: Take<&'a mut T>,
1980}
1981
1982impl<T: Read> BMFFBox<'_, T> {
1983    fn read_into_try_vec(&mut self) -> std::io::Result<TryVec<u8>> {
1984        let limit = self.content.limit();
1985        // For size=0 boxes, size is set to u64::MAX, but after subtracting offset
1986        // (8 or 16 bytes), the limit will be slightly less. Check for values very
1987        // close to u64::MAX to detect these cases.
1988        let mut vec = if limit >= u64::MAX - BoxHeader::MIN_LARGE_SIZE {
1989            // Unknown size (size=0 box), read without pre-allocation
1990            std::vec::Vec::new()
1991        } else {
1992            let mut v = std::vec::Vec::new();
1993            v.try_reserve_exact(limit as usize)
1994                .map_err(|_| std::io::ErrorKind::OutOfMemory)?;
1995            v
1996        };
1997        self.content.read_to_end(&mut vec)?; // The default impl
1998        Ok(vec.into())
1999    }
2000}
2001
2002#[test]
2003fn box_read_to_end() {
2004    let tmp = &mut b"1234567890".as_slice();
2005    let mut src = BMFFBox {
2006        head: BoxHeader { name: BoxType::FileTypeBox, size: 5, offset: 0, uuid: None },
2007        content: <_ as Read>::take(tmp, 5),
2008    };
2009    let buf = src.read_into_try_vec().unwrap();
2010    assert_eq!(buf.len(), 5);
2011    assert_eq!(buf, b"12345".as_ref());
2012}
2013
2014#[test]
2015fn box_read_to_end_oom() {
2016    let tmp = &mut b"1234567890".as_slice();
2017    let mut src = BMFFBox {
2018        head: BoxHeader { name: BoxType::FileTypeBox, size: 5, offset: 0, uuid: None },
2019        // Use a very large value to trigger OOM, but not near u64::MAX (which indicates size=0 boxes)
2020        content: <_ as Read>::take(tmp, u64::MAX / 2),
2021    };
2022    assert!(src.read_into_try_vec().is_err());
2023}
2024
2025struct BoxIter<'a, T> {
2026    src: &'a mut T,
2027}
2028
2029impl<T: Read> BoxIter<'_, T> {
2030    fn new(src: &mut T) -> BoxIter<'_, T> {
2031        BoxIter { src }
2032    }
2033
2034    fn next_box(&mut self) -> Result<Option<BMFFBox<'_, T>>> {
2035        let r = read_box_header(self.src);
2036        match r {
2037            Ok(h) => Ok(Some(BMFFBox {
2038                head: h,
2039                content: self.src.take(h.size - h.offset),
2040            })),
2041            Err(Error::UnexpectedEOF) => Ok(None),
2042            Err(e) => Err(e),
2043        }
2044    }
2045}
2046
2047impl<T: Read> Read for BMFFBox<'_, T> {
2048    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2049        self.content.read(buf)
2050    }
2051}
2052
2053impl<T: Offset> Offset for BMFFBox<'_, T> {
2054    fn offset(&self) -> u64 {
2055        self.content.get_ref().offset()
2056    }
2057}
2058
2059impl<T: Read> BMFFBox<'_, T> {
2060    fn bytes_left(&self) -> u64 {
2061        self.content.limit()
2062    }
2063
2064    const fn get_header(&self) -> &BoxHeader {
2065        &self.head
2066    }
2067
2068    fn box_iter(&mut self) -> BoxIter<'_, Self> {
2069        BoxIter::new(self)
2070    }
2071}
2072
2073impl<T> Drop for BMFFBox<'_, T> {
2074    fn drop(&mut self) {
2075        if self.content.limit() > 0 {
2076            let name: FourCC = From::from(self.head.name);
2077            debug!("Dropping {} bytes in '{}'", self.content.limit(), name);
2078        }
2079    }
2080}
2081
2082/// Read and parse a box header.
2083///
2084/// Call this first to determine the type of a particular mp4 box
2085/// and its length. Used internally for dispatching to specific
2086/// parsers for the internal content, or to get the length to
2087/// skip unknown or uninteresting boxes.
2088///
2089/// See ISO 14496-12:2015 § 4.2
2090fn read_box_header<T: ReadBytesExt>(src: &mut T) -> Result<BoxHeader> {
2091    let size32 = be_u32(src)?;
2092    let name = BoxType::from(be_u32(src)?);
2093    let size = match size32 {
2094        // valid only for top-level box and indicates it's the last box in the file.  usually mdat.
2095        0 => {
2096            // Size=0 means box extends to EOF (ISOBMFF spec allows this for last box)
2097            u64::MAX
2098        },
2099        1 => {
2100            let size64 = be_u64(src)?;
2101            if size64 < BoxHeader::MIN_LARGE_SIZE {
2102                return Err(Error::InvalidData("malformed wide size"));
2103            }
2104            size64
2105        },
2106        _ => {
2107            if u64::from(size32) < BoxHeader::MIN_SIZE {
2108                return Err(Error::InvalidData("malformed size"));
2109            }
2110            u64::from(size32)
2111        },
2112    };
2113    let mut offset = match size32 {
2114        1 => BoxHeader::MIN_LARGE_SIZE,
2115        _ => BoxHeader::MIN_SIZE,
2116    };
2117    let uuid = if name == BoxType::UuidBox {
2118        if size >= offset + 16 {
2119            let mut buffer = [0u8; 16];
2120            let count = src.read(&mut buffer)?;
2121            offset += count.to_u64();
2122            if count == 16 {
2123                Some(buffer)
2124            } else {
2125                debug!("malformed uuid (short read), skipping");
2126                None
2127            }
2128        } else {
2129            debug!("malformed uuid, skipping");
2130            None
2131        }
2132    } else {
2133        None
2134    };
2135    assert!(offset <= size);
2136    Ok(BoxHeader { name, size, offset, uuid })
2137}
2138
2139/// Parse the extra header fields for a full box.
2140fn read_fullbox_extra<T: ReadBytesExt>(src: &mut T) -> Result<(u8, u32)> {
2141    let version = src.read_u8()?;
2142    let flags_a = src.read_u8()?;
2143    let flags_b = src.read_u8()?;
2144    let flags_c = src.read_u8()?;
2145    Ok((
2146        version,
2147        u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c),
2148    ))
2149}
2150
2151// Parse the extra fields for a full box whose flag fields must be zero.
2152fn read_fullbox_version_no_flags<T: ReadBytesExt>(src: &mut T, options: &ParseOptions) -> Result<u8> {
2153    let (version, flags) = read_fullbox_extra(src)?;
2154
2155    if flags != 0 && !options.lenient {
2156        return Err(Error::Unsupported("expected flags to be 0"));
2157    }
2158
2159    Ok(version)
2160}
2161
2162/// Skip over the entire contents of a box.
2163fn skip_box_content<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<()> {
2164    // Skip the contents of unknown chunks.
2165    let to_skip = {
2166        let header = src.get_header();
2167        debug!("{header:?} (skipped)");
2168        header
2169            .size
2170            .checked_sub(header.offset)
2171            .ok_or(Error::InvalidData("header offset > size"))?
2172    };
2173    assert_eq!(to_skip, src.bytes_left());
2174    skip(src, to_skip)
2175}
2176
2177/// Skip over the remain data of a box.
2178fn skip_box_remain<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<()> {
2179    let remain = {
2180        let header = src.get_header();
2181        let len = src.bytes_left();
2182        debug!("remain {len} (skipped) in {header:?}");
2183        len
2184    };
2185    skip(src, remain)
2186}
2187
2188struct ResourceTracker<'a> {
2189    config: &'a DecodeConfig,
2190    #[cfg(feature = "eager")]
2191    current_memory: u64,
2192    #[cfg(feature = "eager")]
2193    peak_memory: u64,
2194}
2195
2196impl<'a> ResourceTracker<'a> {
2197    fn new(config: &'a DecodeConfig) -> Self {
2198        Self {
2199            config,
2200            #[cfg(feature = "eager")]
2201            current_memory: 0,
2202            #[cfg(feature = "eager")]
2203            peak_memory: 0,
2204        }
2205    }
2206
2207    #[cfg(feature = "eager")]
2208    fn reserve(&mut self, bytes: u64) -> Result<()> {
2209        self.current_memory = self.current_memory.saturating_add(bytes);
2210        self.peak_memory = self.peak_memory.max(self.current_memory);
2211
2212        if let Some(limit) = self.config.peak_memory_limit
2213            && self.peak_memory > limit {
2214                return Err(Error::ResourceLimitExceeded("peak memory limit exceeded"));
2215            }
2216
2217        Ok(())
2218    }
2219
2220    #[cfg(feature = "eager")]
2221    fn release(&mut self, bytes: u64) {
2222        self.current_memory = self.current_memory.saturating_sub(bytes);
2223    }
2224
2225    #[cfg(feature = "eager")]
2226    fn validate_total_megapixels(&self, width: u32, height: u32) -> Result<()> {
2227        if let Some(limit) = self.config.total_megapixels_limit {
2228            let megapixels = (width as u64)
2229                .checked_mul(height as u64)
2230                .ok_or(Error::InvalidData("dimension overflow"))?
2231                / 1_000_000;
2232
2233            if megapixels > limit as u64 {
2234                return Err(Error::ResourceLimitExceeded("total megapixels limit exceeded"));
2235            }
2236        }
2237
2238        Ok(())
2239    }
2240
2241    fn validate_animation_frames(&self, count: u32) -> Result<()> {
2242        if let Some(limit) = self.config.max_animation_frames
2243            && count > limit {
2244                return Err(Error::ResourceLimitExceeded("animation frame count limit exceeded"));
2245            }
2246
2247        Ok(())
2248    }
2249
2250    fn validate_grid_tiles(&self, count: u32) -> Result<()> {
2251        if let Some(limit) = self.config.max_grid_tiles
2252            && count > limit {
2253                return Err(Error::ResourceLimitExceeded("grid tile count limit exceeded"));
2254            }
2255
2256        Ok(())
2257    }
2258}
2259
2260/// Read the contents of an AVIF file with resource limits and cancellation support
2261///
2262/// This is the primary parsing function with full control over resource limits
2263/// and cooperative cancellation via the [`Stop`] trait.
2264///
2265/// # Arguments
2266///
2267/// * `f` - Reader for the AVIF file
2268/// * `config` - Resource limits and parsing options
2269/// * `stop` - Cancellation token (use [`Unstoppable`] if not needed)
2270#[cfg(feature = "eager")]
2271#[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader_with_config()` instead")]
2272#[allow(deprecated)]
2273pub fn read_avif_with_config<T: Read>(
2274    f: &mut T,
2275    config: &DecodeConfig,
2276    stop: &dyn Stop,
2277) -> Result<AvifData> {
2278    let mut tracker = ResourceTracker::new(config);
2279    let mut f = OffsetReader::new(f);
2280
2281    let mut iter = BoxIter::new(&mut f);
2282
2283    // 'ftyp' box must occur first; see ISO 14496-12:2015 § 4.3.1
2284    let (major_brand, compatible_brands) = if let Some(mut b) = iter.next_box()? {
2285        if b.head.name == BoxType::FileTypeBox {
2286            let ftyp = read_ftyp(&mut b)?;
2287            // Accept both 'avif' (single-frame) and 'avis' (animated) brands
2288            if ftyp.major_brand != b"avif" && ftyp.major_brand != b"avis" {
2289                warn!("major_brand: {}", ftyp.major_brand);
2290                return Err(Error::InvalidData("ftyp must be 'avif' or 'avis'"));
2291            }
2292            let major = ftyp.major_brand.value;
2293            let compat = ftyp.compatible_brands.iter().map(|b| b.value).collect();
2294            (major, compat)
2295        } else {
2296            return Err(Error::InvalidData("'ftyp' box must occur first"));
2297        }
2298    } else {
2299        return Err(Error::InvalidData("'ftyp' box must occur first"));
2300    };
2301
2302    let mut meta = None;
2303    let mut mdats = TryVec::new();
2304    let mut animation_data: Option<(u32, SampleTable)> = None;
2305
2306    let parse_opts = ParseOptions { lenient: config.lenient };
2307
2308    while let Some(mut b) = iter.next_box()? {
2309        stop.check()?;
2310
2311        match b.head.name {
2312            BoxType::MetadataBox => {
2313                if meta.is_some() {
2314                    return Err(Error::InvalidData("There should be zero or one meta boxes per ISO 14496-12:2015 § 8.11.1.1"));
2315                }
2316                meta = Some(read_avif_meta(&mut b, &parse_opts)?);
2317            },
2318            BoxType::MovieBox => {
2319                animation_data = read_moov(&mut b)?;
2320            },
2321            BoxType::MediaDataBox => {
2322                if b.bytes_left() > 0 {
2323                    let offset = b.offset();
2324                    let size = b.bytes_left();
2325                    tracker.reserve(size)?;
2326                    let data = b.read_into_try_vec()?;
2327                    tracker.release(size);
2328                    mdats.push(MediaDataBox { offset, data })?;
2329                }
2330            },
2331            _ => skip_box_content(&mut b)?,
2332        }
2333
2334        check_parser_state(&b.head, &b.content)?;
2335    }
2336
2337    let meta = meta.ok_or(Error::InvalidData("missing meta"))?;
2338
2339    // Check if primary item is a grid (tiled image)
2340    let is_grid = meta
2341        .item_infos
2342        .iter()
2343        .find(|x| x.item_id == meta.primary_item_id)
2344        .is_some_and(|info| {
2345            let is_g = info.item_type == b"grid";
2346            if is_g {
2347                log::debug!("Grid image detected: primary_item_id={}", meta.primary_item_id);
2348            }
2349            is_g
2350        });
2351
2352    // Extract grid configuration if this is a grid image
2353    let mut grid_config = if is_grid {
2354        meta.properties
2355            .iter()
2356            .find(|prop| {
2357                prop.item_id == meta.primary_item_id
2358                    && matches!(prop.property, ItemProperty::ImageGrid(_))
2359            })
2360            .and_then(|prop| match &prop.property {
2361                ItemProperty::ImageGrid(config) => {
2362                    log::debug!("Grid: found explicit ImageGrid property: {:?}", config);
2363                    Some(config.clone())
2364                },
2365                _ => None,
2366            })
2367    } else {
2368        None
2369    };
2370
2371    // Find tile item IDs if this is a grid
2372    let tile_item_ids: TryVec<u32> = if is_grid {
2373        // Collect tiles with their reference index
2374        let mut tiles_with_index: TryVec<(u32, u16)> = TryVec::new();
2375        for iref in meta.item_references.iter() {
2376            // Grid items reference tiles via "dimg" (derived image) type
2377            if iref.from_item_id == meta.primary_item_id && iref.item_type == b"dimg" {
2378                tiles_with_index.push((iref.to_item_id, iref.reference_index))?;
2379            }
2380        }
2381
2382        // Validate tile count
2383        tracker.validate_grid_tiles(tiles_with_index.len() as u32)?;
2384
2385        // Sort tiles by reference_index to get correct grid order
2386        tiles_with_index.sort_by_key(|&(_, idx)| idx);
2387
2388        // Extract just the IDs in sorted order
2389        let mut ids = TryVec::new();
2390        for (tile_id, _) in tiles_with_index.iter() {
2391            ids.push(*tile_id)?;
2392        }
2393
2394        // No logging here - too verbose for production
2395
2396        // If no ImageGrid property found, calculate grid layout from ispe dimensions
2397        if grid_config.is_none() && !ids.is_empty() {
2398            // Try to calculate grid dimensions from ispe properties
2399            let grid_dims = meta.properties.iter()
2400                .find(|p| p.item_id == meta.primary_item_id)
2401                .and_then(|p| match &p.property {
2402                    ItemProperty::ImageSpatialExtents(e) => Some(e),
2403                    _ => None,
2404                });
2405
2406            let tile_dims = ids.first().and_then(|&tile_id| {
2407                meta.properties.iter()
2408                    .find(|p| p.item_id == tile_id)
2409                    .and_then(|p| match &p.property {
2410                        ItemProperty::ImageSpatialExtents(e) => Some(e),
2411                        _ => None,
2412                    })
2413            });
2414
2415            if let (Some(grid), Some(tile)) = (grid_dims, tile_dims) {
2416                // Validate grid output dimensions
2417                tracker.validate_total_megapixels(grid.width, grid.height)?;
2418
2419                // Validate tile dimensions are non-zero (already validated in read_ispe, but defensive)
2420                if tile.width == 0 || tile.height == 0 {
2421                    log::warn!("Grid: tile has zero dimensions, using fallback");
2422                } else if grid.width % tile.width == 0 && grid.height % tile.height == 0 {
2423                    // Calculate grid layout: grid_dims ÷ tile_dims
2424                    let columns = grid.width / tile.width;
2425                    let rows = grid.height / tile.height;
2426
2427                    // Validate grid dimensions fit in u8 (max 255×255 grid)
2428                    if columns > 255 || rows > 255 {
2429                        log::warn!("Grid: calculated dimensions {}×{} exceed 255, using fallback", rows, columns);
2430                    } else {
2431                        log::debug!("Grid: calculated {}×{} layout from ispe dimensions", rows, columns);
2432                        grid_config = Some(GridConfig {
2433                            rows: rows as u8,
2434                            columns: columns as u8,
2435                            output_width: grid.width,
2436                            output_height: grid.height,
2437                        });
2438                    }
2439                } else {
2440                    log::warn!("Grid: dimension mismatch - grid {}×{} not evenly divisible by tile {}×{}, using fallback",
2441                              grid.width, grid.height, tile.width, tile.height);
2442                }
2443            }
2444
2445            // Fallback: if calculation failed or ispe not available, use N×1 inference
2446            if grid_config.is_none() {
2447                log::debug!("Grid: using fallback {}×1 layout inference", ids.len());
2448                grid_config = Some(GridConfig {
2449                    rows: ids.len() as u8,  // Changed: vertical stack
2450                    columns: 1,              // Changed: single column
2451                    output_width: 0,  // Will be calculated from tiles
2452                    output_height: 0, // Will be calculated from tiles
2453                });
2454            }
2455        }
2456
2457        ids
2458    } else {
2459        TryVec::new()
2460    };
2461
2462    let alpha_item_id = meta
2463        .item_references
2464        .iter()
2465        // Auxiliary image for the primary image
2466        .filter(|iref| {
2467            iref.to_item_id == meta.primary_item_id
2468                && iref.from_item_id != meta.primary_item_id
2469                && iref.item_type == b"auxl"
2470        })
2471        .map(|iref| iref.from_item_id)
2472        // which has the alpha property
2473        .find(|&item_id| {
2474            meta.properties.iter().any(|prop| {
2475                prop.item_id == item_id
2476                    && match &prop.property {
2477                        ItemProperty::AuxiliaryType(urn) => {
2478                            urn.type_subtype().0 == b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"
2479                        }
2480                        _ => false,
2481                    }
2482            })
2483        });
2484
2485    // Extract properties for the primary item
2486    macro_rules! find_prop {
2487        ($variant:ident) => {
2488            meta.properties.iter().find_map(|p| {
2489                if p.item_id == meta.primary_item_id {
2490                    match &p.property {
2491                        ItemProperty::$variant(c) => Some(c.clone()),
2492                        _ => None,
2493                    }
2494                } else {
2495                    None
2496                }
2497            })
2498        };
2499    }
2500
2501    let av1_config = find_prop!(AV1Config);
2502    let color_info = find_prop!(ColorInformation);
2503    let rotation = find_prop!(Rotation);
2504    let mirror = find_prop!(Mirror);
2505    let clean_aperture = find_prop!(CleanAperture);
2506    let pixel_aspect_ratio = find_prop!(PixelAspectRatio);
2507    let content_light_level = find_prop!(ContentLightLevel);
2508    let mastering_display = find_prop!(MasteringDisplayColourVolume);
2509    let content_colour_volume = find_prop!(ContentColourVolume);
2510    let ambient_viewing = find_prop!(AmbientViewingEnvironment);
2511    let operating_point = find_prop!(OperatingPointSelector);
2512    let layer_selector = find_prop!(LayerSelector);
2513    let layered_image_indexing = find_prop!(AV1LayeredImageIndexing);
2514
2515    let mut context = AvifData {
2516        premultiplied_alpha: alpha_item_id.is_some_and(|alpha_item_id| {
2517            meta.item_references.iter().any(|iref| {
2518                iref.from_item_id == meta.primary_item_id
2519                    && iref.to_item_id == alpha_item_id
2520                    && iref.item_type == b"prem"
2521            })
2522        }),
2523        av1_config,
2524        color_info,
2525        rotation,
2526        mirror,
2527        clean_aperture,
2528        pixel_aspect_ratio,
2529        content_light_level,
2530        mastering_display,
2531        content_colour_volume,
2532        ambient_viewing,
2533        operating_point,
2534        layer_selector,
2535        layered_image_indexing,
2536        major_brand,
2537        compatible_brands,
2538        ..Default::default()
2539    };
2540
2541    // Helper to extract item data from either mdat or idat
2542    let mut extract_item_data = |loc: &ItemLocationBoxItem, buf: &mut TryVec<u8>| -> Result<()> {
2543        match loc.construction_method {
2544            ConstructionMethod::File => {
2545                for extent in loc.extents.iter() {
2546                    let mut found = false;
2547                    for mdat in mdats.iter_mut() {
2548                        if mdat.matches_extent(&extent.extent_range) {
2549                            buf.append(&mut mdat.data)?;
2550                            found = true;
2551                            break;
2552                        } else if mdat.contains_extent(&extent.extent_range) {
2553                            mdat.read_extent(&extent.extent_range, buf)?;
2554                            found = true;
2555                            break;
2556                        }
2557                    }
2558                    if !found {
2559                        return Err(Error::InvalidData("iloc contains an extent that is not in mdat"));
2560                    }
2561                }
2562                Ok(())
2563            },
2564            ConstructionMethod::Idat => {
2565                let idat_data = meta.idat.as_ref().ok_or(Error::InvalidData("idat box missing but construction_method is Idat"))?;
2566                for extent in loc.extents.iter() {
2567                    match &extent.extent_range {
2568                        ExtentRange::WithLength(range) => {
2569                            let start = usize::try_from(range.start).map_err(|_| Error::InvalidData("extent start too large"))?;
2570                            let end = usize::try_from(range.end).map_err(|_| Error::InvalidData("extent end too large"))?;
2571                            if end > idat_data.len() {
2572                                return Err(Error::InvalidData("extent exceeds idat size"));
2573                            }
2574                            buf.extend_from_slice(&idat_data[start..end]).map_err(|_| Error::OutOfMemory)?;
2575                        },
2576                        ExtentRange::ToEnd(range) => {
2577                            let start = usize::try_from(range.start).map_err(|_| Error::InvalidData("extent start too large"))?;
2578                            if start >= idat_data.len() {
2579                                return Err(Error::InvalidData("extent start exceeds idat size"));
2580                            }
2581                            buf.extend_from_slice(&idat_data[start..]).map_err(|_| Error::OutOfMemory)?;
2582                        },
2583                    }
2584                }
2585                Ok(())
2586            },
2587            ConstructionMethod::Item => {
2588                Err(Error::Unsupported("construction_method 'item' not supported"))
2589            },
2590        }
2591    };
2592
2593    // load data of relevant items
2594    // For grid images, we need to load tiles in the order specified by iref
2595    if is_grid {
2596        // Extract each tile in order
2597        for (idx, &tile_id) in tile_item_ids.iter().enumerate() {
2598            if idx % 16 == 0 {
2599                stop.check()?;
2600            }
2601
2602            let mut tile_data = TryVec::new();
2603
2604            if let Some(loc) = meta.iloc_items.iter().find(|loc| loc.item_id == tile_id) {
2605                extract_item_data(loc, &mut tile_data)?;
2606            } else {
2607                return Err(Error::InvalidData("grid tile not found in iloc"));
2608            }
2609
2610            context.grid_tiles.push(tile_data)?;
2611        }
2612
2613        // Set grid_config in context
2614        context.grid_config = grid_config;
2615    } else {
2616        // Standard single-frame AVIF: load primary_item and optional alpha_item
2617        for loc in meta.iloc_items.iter() {
2618            let item_data = if loc.item_id == meta.primary_item_id {
2619                &mut context.primary_item
2620            } else if Some(loc.item_id) == alpha_item_id {
2621                context.alpha_item.get_or_insert_with(TryVec::new)
2622            } else {
2623                continue;
2624            };
2625
2626            extract_item_data(loc, item_data)?;
2627        }
2628    }
2629
2630    // Extract animation frames if this is an animated AVIF
2631    if let Some((media_timescale, sample_table)) = animation_data {
2632        let frame_count = sample_table.sample_sizes.len() as u32;
2633        tracker.validate_animation_frames(frame_count)?;
2634
2635        log::debug!("Animation: extracting frames (media_timescale={})", media_timescale);
2636        match extract_animation_frames(&sample_table, media_timescale, &mut mdats) {
2637            Ok(frames) => {
2638                if !frames.is_empty() {
2639                    log::debug!("Animation: extracted {} frames", frames.len());
2640                    context.animation = Some(AnimationConfig {
2641                        loop_count: 0, // TODO: parse from edit list or meta
2642                        frames,
2643                    });
2644                }
2645            }
2646            Err(e) => {
2647                log::warn!("Animation: failed to extract frames: {}", e);
2648            }
2649        }
2650    }
2651
2652    Ok(context)
2653}
2654
2655/// Read the contents of an AVIF file with custom parsing options
2656///
2657/// Uses unlimited resource limits for backwards compatibility.
2658///
2659/// # Arguments
2660///
2661/// * `f` - Reader for the AVIF file
2662/// * `options` - Parsing options (e.g., lenient mode)
2663#[cfg(feature = "eager")]
2664#[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader_with_config()` with `DecodeConfig::lenient()` instead")]
2665#[allow(deprecated)]
2666pub fn read_avif_with_options<T: Read>(f: &mut T, options: &ParseOptions) -> Result<AvifData> {
2667    let config = DecodeConfig::unlimited().lenient(options.lenient);
2668    read_avif_with_config(f, &config, &Unstoppable)
2669}
2670
2671/// Read the contents of an AVIF file
2672///
2673/// Metadata is accumulated and returned in [`AvifData`] struct.
2674/// Uses strict validation and unlimited resource limits by default.
2675///
2676/// For resource limits, use [`read_avif_with_config`].
2677/// For lenient parsing, use [`read_avif_with_options`].
2678#[cfg(feature = "eager")]
2679#[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader()` instead")]
2680#[allow(deprecated)]
2681pub fn read_avif<T: Read>(f: &mut T) -> Result<AvifData> {
2682    read_avif_with_options(f, &ParseOptions::default())
2683}
2684
2685/// Parse a metadata box in the context of an AVIF
2686/// Currently requires the primary item to be an av01 item type and generates
2687/// an error otherwise.
2688/// See ISO 14496-12:2015 § 8.11.1
2689fn read_avif_meta<T: Read + Offset>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<AvifInternalMeta> {
2690    let version = read_fullbox_version_no_flags(src, options)?;
2691
2692    if version != 0 {
2693        return Err(Error::Unsupported("unsupported meta version"));
2694    }
2695
2696    let mut primary_item_id = None;
2697    let mut item_infos = None;
2698    let mut iloc_items = None;
2699    let mut item_references = TryVec::new();
2700    let mut properties = TryVec::new();
2701    let mut idat = None;
2702
2703    let mut iter = src.box_iter();
2704    while let Some(mut b) = iter.next_box()? {
2705        match b.head.name {
2706            BoxType::ItemInfoBox => {
2707                if item_infos.is_some() {
2708                    return Err(Error::InvalidData("There should be zero or one iinf boxes per ISO 14496-12:2015 § 8.11.6.1"));
2709                }
2710                item_infos = Some(read_iinf(&mut b, options)?);
2711            },
2712            BoxType::ItemLocationBox => {
2713                if iloc_items.is_some() {
2714                    return Err(Error::InvalidData("There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.3.1"));
2715                }
2716                iloc_items = Some(read_iloc(&mut b, options)?);
2717            },
2718            BoxType::PrimaryItemBox => {
2719                if primary_item_id.is_some() {
2720                    return Err(Error::InvalidData("There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.4.1"));
2721                }
2722                primary_item_id = Some(read_pitm(&mut b, options)?);
2723            },
2724            BoxType::ImageReferenceBox => {
2725                item_references.append(&mut read_iref(&mut b, options)?)?;
2726            },
2727            BoxType::ImagePropertiesBox => {
2728                properties = read_iprp(&mut b, options)?;
2729            },
2730            BoxType::ItemDataBox => {
2731                if idat.is_some() {
2732                    return Err(Error::InvalidData("There should be zero or one idat boxes"));
2733                }
2734                idat = Some(b.read_into_try_vec()?);
2735            },
2736            BoxType::HandlerBox => {
2737                let hdlr = read_hdlr(&mut b)?;
2738                if hdlr.handler_type != b"pict" {
2739                    warn!("hdlr handler_type: {}", hdlr.handler_type);
2740                    return Err(Error::InvalidData("meta handler_type must be 'pict' for AVIF"));
2741                }
2742            },
2743            _ => skip_box_content(&mut b)?,
2744        }
2745
2746        check_parser_state(&b.head, &b.content)?;
2747    }
2748
2749    let primary_item_id = primary_item_id.ok_or(Error::InvalidData("Required pitm box not present in meta box"))?;
2750
2751    let item_infos = item_infos.ok_or(Error::InvalidData("iinf missing"))?;
2752
2753    if let Some(item_info) = item_infos.iter().find(|x| x.item_id == primary_item_id) {
2754        // Allow both "av01" (standard single-frame) and "grid" (tiled) types
2755        if item_info.item_type != b"av01" && item_info.item_type != b"grid" {
2756            warn!("primary_item_id type: {}", item_info.item_type);
2757            return Err(Error::InvalidData("primary_item_id type is not av01 or grid"));
2758        }
2759    } else {
2760        return Err(Error::InvalidData("primary_item_id not present in iinf box"));
2761    }
2762
2763    Ok(AvifInternalMeta {
2764        properties,
2765        item_references,
2766        primary_item_id,
2767        iloc_items: iloc_items.ok_or(Error::InvalidData("iloc missing"))?,
2768        item_infos,
2769        idat,
2770    })
2771}
2772
2773/// Parse a Handler Reference Box
2774/// See ISO 14496-12:2015 § 8.4.3
2775fn read_hdlr<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<HandlerBox> {
2776    let (_version, _flags) = read_fullbox_extra(src)?;
2777    // pre_defined (4 bytes)
2778    skip(src, 4)?;
2779    // handler_type (4 bytes)
2780    let handler_type = be_u32(src)?;
2781    // reserved (3 × 4 bytes) + name (variable) — skip the rest
2782    skip_box_remain(src)?;
2783    Ok(HandlerBox {
2784        handler_type: FourCC::from(handler_type),
2785    })
2786}
2787
2788/// Parse a Primary Item Box
2789/// See ISO 14496-12:2015 § 8.11.4
2790fn read_pitm<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<u32> {
2791    let version = read_fullbox_version_no_flags(src, options)?;
2792
2793    let item_id = match version {
2794        0 => be_u16(src)?.into(),
2795        1 => be_u32(src)?,
2796        _ => return Err(Error::Unsupported("unsupported pitm version")),
2797    };
2798
2799    Ok(item_id)
2800}
2801
2802/// Parse an Item Information Box
2803/// See ISO 14496-12:2015 § 8.11.6
2804fn read_iinf<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<ItemInfoEntry>> {
2805    let version = read_fullbox_version_no_flags(src, options)?;
2806
2807    match version {
2808        0 | 1 => (),
2809        _ => return Err(Error::Unsupported("unsupported iinf version")),
2810    }
2811
2812    let entry_count = if version == 0 {
2813        be_u16(src)?.to_usize()
2814    } else {
2815        be_u32(src)?.to_usize()
2816    };
2817    let mut item_infos = TryVec::with_capacity(entry_count)?;
2818
2819    let mut iter = src.box_iter();
2820    while let Some(mut b) = iter.next_box()? {
2821        if b.head.name != BoxType::ItemInfoEntry {
2822            return Err(Error::InvalidData("iinf box should contain only infe boxes"));
2823        }
2824
2825        item_infos.push(read_infe(&mut b)?)?;
2826
2827        check_parser_state(&b.head, &b.content)?;
2828    }
2829
2830    Ok(item_infos)
2831}
2832
2833/// Parse an Item Info Entry
2834/// See ISO 14496-12:2015 § 8.11.6.2
2835fn read_infe<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ItemInfoEntry> {
2836    // According to the standard, it seems the flags field should be 0, but
2837    // at least one sample AVIF image has a nonzero value.
2838    let (version, _) = read_fullbox_extra(src)?;
2839
2840    // mif1 brand (see ISO 23008-12:2017 § 10.2.1) only requires v2 and 3
2841    let item_id = match version {
2842        2 => be_u16(src)?.into(),
2843        3 => be_u32(src)?,
2844        _ => return Err(Error::Unsupported("unsupported version in 'infe' box")),
2845    };
2846
2847    let item_protection_index = be_u16(src)?;
2848
2849    if item_protection_index != 0 {
2850        return Err(Error::Unsupported("protected items (infe.item_protection_index != 0) are not supported"));
2851    }
2852
2853    let item_type = FourCC::from(be_u32(src)?);
2854    debug!("infe item_id {item_id} item_type: {item_type}");
2855
2856    // There are some additional fields here, but they're not of interest to us
2857    skip_box_remain(src)?;
2858
2859    Ok(ItemInfoEntry { item_id, item_type })
2860}
2861
2862fn read_iref<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<SingleItemTypeReferenceBox>> {
2863    let mut item_references = TryVec::new();
2864    let version = read_fullbox_version_no_flags(src, options)?;
2865    if version > 1 {
2866        return Err(Error::Unsupported("iref version"));
2867    }
2868
2869    let mut iter = src.box_iter();
2870    while let Some(mut b) = iter.next_box()? {
2871        let from_item_id = if version == 0 {
2872            be_u16(&mut b)?.into()
2873        } else {
2874            be_u32(&mut b)?
2875        };
2876        let reference_count = be_u16(&mut b)?;
2877        for reference_index in 0..reference_count {
2878            let to_item_id = if version == 0 {
2879                be_u16(&mut b)?.into()
2880            } else {
2881                be_u32(&mut b)?
2882            };
2883            if from_item_id == to_item_id {
2884                return Err(Error::InvalidData("from_item_id and to_item_id must be different"));
2885            }
2886            item_references.push(SingleItemTypeReferenceBox {
2887                item_type: b.head.name.into(),
2888                from_item_id,
2889                to_item_id,
2890                reference_index,
2891            })?;
2892        }
2893        check_parser_state(&b.head, &b.content)?;
2894    }
2895    Ok(item_references)
2896}
2897
2898fn read_iprp<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<AssociatedProperty>> {
2899    let mut iter = src.box_iter();
2900    let mut properties = TryVec::new();
2901    let mut associations = TryVec::new();
2902
2903    while let Some(mut b) = iter.next_box()? {
2904        match b.head.name {
2905            BoxType::ItemPropertyContainerBox => {
2906                properties = read_ipco(&mut b, options)?;
2907            },
2908            BoxType::ItemPropertyAssociationBox => {
2909                associations = read_ipma(&mut b)?;
2910            },
2911            _ => return Err(Error::InvalidData("unexpected ipco child")),
2912        }
2913    }
2914
2915    let mut associated = TryVec::new();
2916    for a in associations {
2917        let index = match a.property_index {
2918            0 => continue,
2919            x => x as usize - 1,
2920        };
2921        if let Some(prop) = properties.get(index)
2922            && *prop != ItemProperty::Unsupported {
2923                associated.push(AssociatedProperty {
2924                    item_id: a.item_id,
2925                    property: prop.try_clone()?,
2926                })?;
2927            }
2928    }
2929    Ok(associated)
2930}
2931
2932/// Image spatial extents (dimensions)
2933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2934pub(crate) struct ImageSpatialExtents {
2935    pub(crate) width: u32,
2936    pub(crate) height: u32,
2937}
2938
2939#[derive(Debug, PartialEq)]
2940pub(crate) enum ItemProperty {
2941    Channels(ArrayVec<u8, 16>),
2942    AuxiliaryType(AuxiliaryTypeProperty),
2943    ImageSpatialExtents(ImageSpatialExtents),
2944    ImageGrid(GridConfig),
2945    AV1Config(AV1Config),
2946    ColorInformation(ColorInformation),
2947    Rotation(ImageRotation),
2948    Mirror(ImageMirror),
2949    CleanAperture(CleanAperture),
2950    PixelAspectRatio(PixelAspectRatio),
2951    ContentLightLevel(ContentLightLevel),
2952    MasteringDisplayColourVolume(MasteringDisplayColourVolume),
2953    ContentColourVolume(ContentColourVolume),
2954    AmbientViewingEnvironment(AmbientViewingEnvironment),
2955    OperatingPointSelector(OperatingPointSelector),
2956    LayerSelector(LayerSelector),
2957    AV1LayeredImageIndexing(AV1LayeredImageIndexing),
2958    Unsupported,
2959}
2960
2961impl TryClone for ItemProperty {
2962    fn try_clone(&self) -> Result<Self, TryReserveError> {
2963        Ok(match self {
2964            Self::Channels(val) => Self::Channels(val.clone()),
2965            Self::AuxiliaryType(val) => Self::AuxiliaryType(val.try_clone()?),
2966            Self::ImageSpatialExtents(val) => Self::ImageSpatialExtents(*val),
2967            Self::ImageGrid(val) => Self::ImageGrid(val.clone()),
2968            Self::AV1Config(val) => Self::AV1Config(val.clone()),
2969            Self::ColorInformation(val) => Self::ColorInformation(val.clone()),
2970            Self::Rotation(val) => Self::Rotation(*val),
2971            Self::Mirror(val) => Self::Mirror(*val),
2972            Self::CleanAperture(val) => Self::CleanAperture(*val),
2973            Self::PixelAspectRatio(val) => Self::PixelAspectRatio(*val),
2974            Self::ContentLightLevel(val) => Self::ContentLightLevel(*val),
2975            Self::MasteringDisplayColourVolume(val) => Self::MasteringDisplayColourVolume(*val),
2976            Self::ContentColourVolume(val) => Self::ContentColourVolume(*val),
2977            Self::AmbientViewingEnvironment(val) => Self::AmbientViewingEnvironment(*val),
2978            Self::OperatingPointSelector(val) => Self::OperatingPointSelector(*val),
2979            Self::LayerSelector(val) => Self::LayerSelector(*val),
2980            Self::AV1LayeredImageIndexing(val) => Self::AV1LayeredImageIndexing(*val),
2981            Self::Unsupported => Self::Unsupported,
2982        })
2983    }
2984}
2985
2986struct Association {
2987    item_id: u32,
2988    #[allow(unused)]
2989    essential: bool,
2990    property_index: u16,
2991}
2992
2993pub(crate) struct AssociatedProperty {
2994    pub item_id: u32,
2995    pub property: ItemProperty,
2996}
2997
2998fn read_ipma<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<Association>> {
2999    let (version, flags) = read_fullbox_extra(src)?;
3000
3001    let mut associations = TryVec::new();
3002
3003    let entry_count = be_u32(src)?;
3004    for _ in 0..entry_count {
3005        let item_id = if version == 0 {
3006            be_u16(src)?.into()
3007        } else {
3008            be_u32(src)?
3009        };
3010        let association_count = src.read_u8()?;
3011        for _ in 0..association_count {
3012            let num_association_bytes = if flags & 1 == 1 { 2 } else { 1 };
3013            let association = &mut [0; 2][..num_association_bytes];
3014            src.read_exact(association)?;
3015            let mut association = BitReader::new(association);
3016            let essential = association.read_bool()?;
3017            let property_index = association.read_u16(association.remaining().try_into()?)?;
3018            associations.push(Association {
3019                item_id,
3020                essential,
3021                property_index,
3022            })?;
3023        }
3024    }
3025    Ok(associations)
3026}
3027
3028fn read_ipco<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<ItemProperty>> {
3029    let mut properties = TryVec::new();
3030
3031    let mut iter = src.box_iter();
3032    while let Some(mut b) = iter.next_box()? {
3033        // Must push for every property to have correct index for them
3034        let prop = match b.head.name {
3035            BoxType::PixelInformationBox => ItemProperty::Channels(read_pixi(&mut b, options)?),
3036            BoxType::AuxiliaryTypeProperty => ItemProperty::AuxiliaryType(read_auxc(&mut b, options)?),
3037            BoxType::ImageSpatialExtentsBox => ItemProperty::ImageSpatialExtents(read_ispe(&mut b, options)?),
3038            BoxType::ImageGridBox => ItemProperty::ImageGrid(read_grid(&mut b, options)?),
3039            BoxType::AV1CodecConfigurationBox => ItemProperty::AV1Config(read_av1c(&mut b)?),
3040            BoxType::ColorInformationBox => {
3041                match read_colr(&mut b) {
3042                    Ok(colr) => ItemProperty::ColorInformation(colr),
3043                    Err(_) => ItemProperty::Unsupported,
3044                }
3045            },
3046            BoxType::ImageRotationBox => ItemProperty::Rotation(read_irot(&mut b)?),
3047            BoxType::ImageMirrorBox => ItemProperty::Mirror(read_imir(&mut b)?),
3048            BoxType::CleanApertureBox => ItemProperty::CleanAperture(read_clap(&mut b)?),
3049            BoxType::PixelAspectRatioBox => ItemProperty::PixelAspectRatio(read_pasp(&mut b)?),
3050            BoxType::ContentLightLevelBox => ItemProperty::ContentLightLevel(read_clli(&mut b)?),
3051            BoxType::MasteringDisplayColourVolumeBox => ItemProperty::MasteringDisplayColourVolume(read_mdcv(&mut b)?),
3052            BoxType::ContentColourVolumeBox => ItemProperty::ContentColourVolume(read_cclv(&mut b)?),
3053            BoxType::AmbientViewingEnvironmentBox => ItemProperty::AmbientViewingEnvironment(read_amve(&mut b)?),
3054            BoxType::OperatingPointSelectorBox => ItemProperty::OperatingPointSelector(read_a1op(&mut b)?),
3055            BoxType::LayerSelectorBox => ItemProperty::LayerSelector(read_lsel(&mut b)?),
3056            BoxType::AV1LayeredImageIndexingBox => ItemProperty::AV1LayeredImageIndexing(read_a1lx(&mut b)?),
3057            _ => {
3058                skip_box_remain(&mut b)?;
3059                ItemProperty::Unsupported
3060            },
3061        };
3062        properties.push(prop)?;
3063    }
3064    Ok(properties)
3065}
3066
3067fn read_pixi<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<ArrayVec<u8, 16>> {
3068    let version = read_fullbox_version_no_flags(src, options)?;
3069    if version != 0 {
3070        return Err(Error::Unsupported("pixi version"));
3071    }
3072
3073    let num_channels = usize::from(src.read_u8()?);
3074    let mut channels = ArrayVec::new();
3075    channels.extend((0..num_channels.min(channels.capacity())).map(|_| 0));
3076    debug_assert_eq!(num_channels, channels.len());
3077    src.read_exact(&mut channels).map_err(|_| Error::InvalidData("invalid num_channels"))?;
3078
3079    // In lenient mode, skip any extra bytes (e.g., extended_pixi.avif has 6 extra bytes)
3080    if options.lenient && src.bytes_left() > 0 {
3081        skip(src, src.bytes_left())?;
3082    }
3083
3084    check_parser_state(&src.head, &src.content)?;
3085    Ok(channels)
3086}
3087
3088#[derive(Debug, PartialEq)]
3089struct AuxiliaryTypeProperty {
3090    aux_data: TryString,
3091}
3092
3093impl AuxiliaryTypeProperty {
3094    #[must_use]
3095    fn type_subtype(&self) -> (&[u8], &[u8]) {
3096        let split = self.aux_data.iter().position(|&b| b == b'\0')
3097            .map(|pos| self.aux_data.split_at(pos));
3098        if let Some((aux_type, rest)) = split {
3099            (aux_type, &rest[1..])
3100        } else {
3101            (&self.aux_data, &[])
3102        }
3103    }
3104}
3105
3106impl TryClone for AuxiliaryTypeProperty {
3107    fn try_clone(&self) -> Result<Self, TryReserveError> {
3108        Ok(Self {
3109            aux_data: self.aux_data.try_clone()?,
3110        })
3111    }
3112}
3113
3114fn read_auxc<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<AuxiliaryTypeProperty> {
3115    let version = read_fullbox_version_no_flags(src, options)?;
3116    if version != 0 {
3117        return Err(Error::Unsupported("auxC version"));
3118    }
3119
3120    let aux_data = src.read_into_try_vec()?;
3121
3122    Ok(AuxiliaryTypeProperty { aux_data })
3123}
3124
3125/// Parse an AV1 Codec Configuration property box
3126/// See AV1-ISOBMFF § 2.3
3127fn read_av1c<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<AV1Config> {
3128    // av1C is NOT a FullBox — it has no version/flags
3129    let byte0 = src.read_u8()?;
3130    let marker = byte0 >> 7;
3131    let version = byte0 & 0x7F;
3132
3133    if marker != 1 {
3134        return Err(Error::InvalidData("av1C marker must be 1"));
3135    }
3136    if version != 1 {
3137        return Err(Error::Unsupported("av1C version must be 1"));
3138    }
3139
3140    let byte1 = src.read_u8()?;
3141    let profile = byte1 >> 5;
3142    let level = byte1 & 0x1F;
3143
3144    let byte2 = src.read_u8()?;
3145    let tier = byte2 >> 7;
3146    let high_bitdepth = (byte2 >> 6) & 1;
3147    let twelve_bit = (byte2 >> 5) & 1;
3148    let monochrome = (byte2 >> 4) & 1 != 0;
3149    let chroma_subsampling_x = (byte2 >> 3) & 1;
3150    let chroma_subsampling_y = (byte2 >> 2) & 1;
3151    let chroma_sample_position = byte2 & 0x03;
3152
3153    let byte3 = src.read_u8()?;
3154    // byte3: 3 bits reserved, 1 bit initial_presentation_delay_present, 4 bits delay/reserved
3155    // Not needed for image decoding.
3156    let _ = byte3;
3157
3158    let bit_depth = if high_bitdepth != 0 {
3159        if twelve_bit != 0 { 12 } else { 10 }
3160    } else {
3161        8
3162    };
3163
3164    // Skip any configOBUs (remainder of box)
3165    skip_box_remain(src)?;
3166
3167    Ok(AV1Config {
3168        profile,
3169        level,
3170        tier,
3171        bit_depth,
3172        monochrome,
3173        chroma_subsampling_x,
3174        chroma_subsampling_y,
3175        chroma_sample_position,
3176    })
3177}
3178
3179/// Parse a Colour Information property box
3180/// See ISOBMFF § 12.1.5
3181fn read_colr<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ColorInformation> {
3182    // colr is NOT a FullBox — no version/flags
3183    let colour_type = be_u32(src)?;
3184
3185    match &colour_type.to_be_bytes() {
3186        b"nclx" => {
3187            let color_primaries = be_u16(src)?;
3188            let transfer_characteristics = be_u16(src)?;
3189            let matrix_coefficients = be_u16(src)?;
3190            let full_range_byte = src.read_u8()?;
3191            let full_range = (full_range_byte >> 7) != 0;
3192            // Skip any remaining bytes
3193            skip_box_remain(src)?;
3194            Ok(ColorInformation::Nclx {
3195                color_primaries,
3196                transfer_characteristics,
3197                matrix_coefficients,
3198                full_range,
3199            })
3200        }
3201        b"rICC" | b"prof" => {
3202            let icc_data = src.read_into_try_vec()?;
3203            Ok(ColorInformation::IccProfile(icc_data.to_vec()))
3204        }
3205        _ => {
3206            skip_box_remain(src)?;
3207            Err(Error::Unsupported("unsupported colr colour_type"))
3208        }
3209    }
3210}
3211
3212/// Parse an Image Rotation property box.
3213/// See ISOBMFF § 12.1.4. NOT a FullBox.
3214fn read_irot<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ImageRotation> {
3215    let byte = src.read_u8()?;
3216    let angle_code = byte & 0x03;
3217    let angle = match angle_code {
3218        0 => 0,
3219        1 => 90,
3220        2 => 180,
3221        3 => 270,
3222        _ => unreachable!(),
3223    };
3224    skip_box_remain(src)?;
3225    Ok(ImageRotation { angle })
3226}
3227
3228/// Parse an Image Mirror property box.
3229/// See ISOBMFF § 12.1.4. NOT a FullBox.
3230fn read_imir<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ImageMirror> {
3231    let byte = src.read_u8()?;
3232    let axis = byte & 0x01;
3233    skip_box_remain(src)?;
3234    Ok(ImageMirror { axis })
3235}
3236
3237/// Parse a Clean Aperture property box.
3238/// See ISOBMFF § 12.1.4. NOT a FullBox.
3239fn read_clap<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<CleanAperture> {
3240    let width_n = be_u32(src)?;
3241    let width_d = be_u32(src)?;
3242    let height_n = be_u32(src)?;
3243    let height_d = be_u32(src)?;
3244    let horiz_off_n = be_i32(src)?;
3245    let horiz_off_d = be_u32(src)?;
3246    let vert_off_n = be_i32(src)?;
3247    let vert_off_d = be_u32(src)?;
3248    // Validate denominators are non-zero
3249    if width_d == 0 || height_d == 0 || horiz_off_d == 0 || vert_off_d == 0 {
3250        return Err(Error::InvalidData("clap denominator cannot be zero"));
3251    }
3252    skip_box_remain(src)?;
3253    Ok(CleanAperture {
3254        width_n, width_d,
3255        height_n, height_d,
3256        horiz_off_n, horiz_off_d,
3257        vert_off_n, vert_off_d,
3258    })
3259}
3260
3261/// Parse a Pixel Aspect Ratio property box.
3262/// See ISOBMFF § 12.1.4. NOT a FullBox.
3263fn read_pasp<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<PixelAspectRatio> {
3264    let h_spacing = be_u32(src)?;
3265    let v_spacing = be_u32(src)?;
3266    skip_box_remain(src)?;
3267    Ok(PixelAspectRatio { h_spacing, v_spacing })
3268}
3269
3270/// Parse a Content Light Level Info property box.
3271/// See ISOBMFF § 12.1.5 / ITU-T H.274. NOT a FullBox.
3272fn read_clli<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ContentLightLevel> {
3273    let max_content_light_level = be_u16(src)?;
3274    let max_pic_average_light_level = be_u16(src)?;
3275    skip_box_remain(src)?;
3276    Ok(ContentLightLevel {
3277        max_content_light_level,
3278        max_pic_average_light_level,
3279    })
3280}
3281
3282/// Parse a Mastering Display Colour Volume property box.
3283/// See ISOBMFF § 12.1.5 / SMPTE ST 2086. NOT a FullBox.
3284fn read_mdcv<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<MasteringDisplayColourVolume> {
3285    // 3 primaries, each (x, y) as u16
3286    let primaries = [
3287        (be_u16(src)?, be_u16(src)?),
3288        (be_u16(src)?, be_u16(src)?),
3289        (be_u16(src)?, be_u16(src)?),
3290    ];
3291    let white_point = (be_u16(src)?, be_u16(src)?);
3292    let max_luminance = be_u32(src)?;
3293    let min_luminance = be_u32(src)?;
3294    skip_box_remain(src)?;
3295    Ok(MasteringDisplayColourVolume {
3296        primaries,
3297        white_point,
3298        max_luminance,
3299        min_luminance,
3300    })
3301}
3302
3303/// Parse a Content Colour Volume property box.
3304/// See ISOBMFF § 12.1.5 / H.265 D.2.40. NOT a FullBox.
3305fn read_cclv<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ContentColourVolume> {
3306    let flags = src.read_u8()?;
3307    let primaries_present = flags & 0x20 != 0;
3308    let min_lum_present = flags & 0x10 != 0;
3309    let max_lum_present = flags & 0x08 != 0;
3310    let avg_lum_present = flags & 0x04 != 0;
3311
3312    let primaries = if primaries_present {
3313        Some([
3314            (be_i32(src)?, be_i32(src)?),
3315            (be_i32(src)?, be_i32(src)?),
3316            (be_i32(src)?, be_i32(src)?),
3317        ])
3318    } else {
3319        None
3320    };
3321
3322    let min_luminance = if min_lum_present { Some(be_u32(src)?) } else { None };
3323    let max_luminance = if max_lum_present { Some(be_u32(src)?) } else { None };
3324    let avg_luminance = if avg_lum_present { Some(be_u32(src)?) } else { None };
3325
3326    skip_box_remain(src)?;
3327    Ok(ContentColourVolume {
3328        primaries,
3329        min_luminance,
3330        max_luminance,
3331        avg_luminance,
3332    })
3333}
3334
3335/// Parse an Ambient Viewing Environment property box.
3336/// See ISOBMFF § 12.1.5 / H.265 D.2.39. NOT a FullBox.
3337fn read_amve<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<AmbientViewingEnvironment> {
3338    let ambient_illuminance = be_u32(src)?;
3339    let ambient_light_x = be_u16(src)?;
3340    let ambient_light_y = be_u16(src)?;
3341    skip_box_remain(src)?;
3342    Ok(AmbientViewingEnvironment {
3343        ambient_illuminance,
3344        ambient_light_x,
3345        ambient_light_y,
3346    })
3347}
3348
3349/// Parse an Operating Point Selector property box.
3350/// See AVIF § 4.3.4. NOT a FullBox.
3351fn read_a1op<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<OperatingPointSelector> {
3352    let op_index = src.read_u8()?;
3353    if op_index > 31 {
3354        return Err(Error::InvalidData("a1op op_index must be 0..31"));
3355    }
3356    skip_box_remain(src)?;
3357    Ok(OperatingPointSelector { op_index })
3358}
3359
3360/// Parse a Layer Selector property box.
3361/// See HEIF (ISO 23008-12). NOT a FullBox.
3362fn read_lsel<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<LayerSelector> {
3363    let layer_id = be_u16(src)?;
3364    skip_box_remain(src)?;
3365    Ok(LayerSelector { layer_id })
3366}
3367
3368/// Parse an AV1 Layered Image Indexing property box.
3369/// See AVIF § 4.3.6. NOT a FullBox.
3370fn read_a1lx<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<AV1LayeredImageIndexing> {
3371    let flags = src.read_u8()?;
3372    let large_size = flags & 0x01 != 0;
3373    let layer_sizes = if large_size {
3374        [be_u32(src)?, be_u32(src)?, be_u32(src)?]
3375    } else {
3376        [u32::from(be_u16(src)?), u32::from(be_u16(src)?), u32::from(be_u16(src)?)]
3377    };
3378    skip_box_remain(src)?;
3379    Ok(AV1LayeredImageIndexing { layer_sizes })
3380}
3381
3382/// Parse an Image Spatial Extents property box
3383/// See ISO/IEC 23008-12:2017 § 6.5.3
3384fn read_ispe<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<ImageSpatialExtents> {
3385    let _version = read_fullbox_version_no_flags(src, options)?;
3386    // Version is always 0 for ispe
3387
3388    let width = be_u32(src)?;
3389    let height = be_u32(src)?;
3390
3391    // Validate dimensions are non-zero (0×0 images are invalid)
3392    if width == 0 || height == 0 {
3393        return Err(Error::InvalidData("ispe dimensions cannot be zero"));
3394    }
3395
3396    Ok(ImageSpatialExtents { width, height })
3397}
3398
3399/// Parse a Movie Header box (mvhd)
3400/// See ISO/IEC 14496-12:2015 § 8.2.2
3401fn read_mvhd<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<MovieHeader> {
3402    let version = src.read_u8()?;
3403    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
3404
3405    let (timescale, duration) = if version == 1 {
3406        let _creation_time = be_u64(src)?;
3407        let _modification_time = be_u64(src)?;
3408        let timescale = be_u32(src)?;
3409        let duration = be_u64(src)?;
3410        (timescale, duration)
3411    } else {
3412        let _creation_time = be_u32(src)?;
3413        let _modification_time = be_u32(src)?;
3414        let timescale = be_u32(src)?;
3415        let duration = be_u32(src)?;
3416        (timescale, duration as u64)
3417    };
3418
3419    // Skip rest of mvhd (rate, volume, matrix, etc.)
3420    skip_box_remain(src)?;
3421
3422    Ok(MovieHeader { _timescale: timescale, _duration: duration })
3423}
3424
3425/// Parse a Media Header box (mdhd)
3426/// See ISO/IEC 14496-12:2015 § 8.4.2
3427fn read_mdhd<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<MediaHeader> {
3428    let version = src.read_u8()?;
3429    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
3430
3431    let (timescale, duration) = if version == 1 {
3432        let _creation_time = be_u64(src)?;
3433        let _modification_time = be_u64(src)?;
3434        let timescale = be_u32(src)?;
3435        let duration = be_u64(src)?;
3436        (timescale, duration)
3437    } else {
3438        let _creation_time = be_u32(src)?;
3439        let _modification_time = be_u32(src)?;
3440        let timescale = be_u32(src)?;
3441        let duration = be_u32(src)?;
3442        (timescale, duration as u64)
3443    };
3444
3445    // Skip language and pre_defined
3446    skip_box_remain(src)?;
3447
3448    Ok(MediaHeader { timescale, _duration: duration })
3449}
3450
3451/// Parse Time To Sample box (stts)
3452/// See ISO/IEC 14496-12:2015 § 8.6.1.2
3453fn read_stts<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<TimeToSampleEntry>> {
3454    let _version = src.read_u8()?;
3455    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
3456    let entry_count = be_u32(src)?;
3457
3458    let mut entries = TryVec::new();
3459    for _ in 0..entry_count {
3460        entries.push(TimeToSampleEntry {
3461            sample_count: be_u32(src)?,
3462            sample_delta: be_u32(src)?,
3463        })?;
3464    }
3465
3466    Ok(entries)
3467}
3468
3469/// Parse Sample To Chunk box (stsc)
3470/// See ISO/IEC 14496-12:2015 § 8.7.4
3471fn read_stsc<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<SampleToChunkEntry>> {
3472    let _version = src.read_u8()?;
3473    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
3474    let entry_count = be_u32(src)?;
3475
3476    let mut entries = TryVec::new();
3477    for _ in 0..entry_count {
3478        entries.push(SampleToChunkEntry {
3479            first_chunk: be_u32(src)?,
3480            samples_per_chunk: be_u32(src)?,
3481            _sample_description_index: be_u32(src)?,
3482        })?;
3483    }
3484
3485    Ok(entries)
3486}
3487
3488/// Parse Sample Size box (stsz)
3489/// See ISO/IEC 14496-12:2015 § 8.7.3
3490fn read_stsz<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<u32>> {
3491    let _version = src.read_u8()?;
3492    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
3493    let sample_size = be_u32(src)?;
3494    let sample_count = be_u32(src)?;
3495
3496    let mut sizes = TryVec::new();
3497    if sample_size == 0 {
3498        // Variable sizes - read each one
3499        for _ in 0..sample_count {
3500            sizes.push(be_u32(src)?)?;
3501        }
3502    } else {
3503        // Constant size for all samples
3504        for _ in 0..sample_count {
3505            sizes.push(sample_size)?;
3506        }
3507    }
3508
3509    Ok(sizes)
3510}
3511
3512/// Parse Chunk Offset box (stco or co64)
3513/// See ISO/IEC 14496-12:2015 § 8.7.5
3514fn read_chunk_offsets<T: Read>(src: &mut BMFFBox<'_, T>, is_64bit: bool) -> Result<TryVec<u64>> {
3515    let _version = src.read_u8()?;
3516    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
3517    let entry_count = be_u32(src)?;
3518
3519    let mut offsets = TryVec::new();
3520    for _ in 0..entry_count {
3521        let offset = if is_64bit {
3522            be_u64(src)?
3523        } else {
3524            be_u32(src)? as u64
3525        };
3526        offsets.push(offset)?;
3527    }
3528
3529    Ok(offsets)
3530}
3531
3532/// Parse Sample Table box (stbl)
3533/// See ISO/IEC 14496-12:2015 § 8.5
3534fn read_stbl<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<SampleTable> {
3535    let mut time_to_sample = TryVec::new();
3536    let mut sample_to_chunk = TryVec::new();
3537    let mut sample_sizes = TryVec::new();
3538    let mut chunk_offsets = TryVec::new();
3539
3540    let mut iter = src.box_iter();
3541    while let Some(mut b) = iter.next_box()? {
3542        match b.head.name {
3543            BoxType::TimeToSampleBox => {
3544                time_to_sample = read_stts(&mut b)?;
3545            }
3546            BoxType::SampleToChunkBox => {
3547                sample_to_chunk = read_stsc(&mut b)?;
3548            }
3549            BoxType::SampleSizeBox => {
3550                sample_sizes = read_stsz(&mut b)?;
3551            }
3552            BoxType::ChunkOffsetBox => {
3553                chunk_offsets = read_chunk_offsets(&mut b, false)?;
3554            }
3555            BoxType::ChunkLargeOffsetBox => {
3556                chunk_offsets = read_chunk_offsets(&mut b, true)?;
3557            }
3558            _ => {
3559                skip_box_remain(&mut b)?;
3560            }
3561        }
3562    }
3563
3564    Ok(SampleTable {
3565        time_to_sample,
3566        sample_to_chunk,
3567        sample_sizes,
3568        chunk_offsets,
3569    })
3570}
3571
3572/// Parse animation from moov box
3573/// Returns (media_timescale, sample_table)
3574fn read_moov<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<(u32, SampleTable)>> {
3575    let mut media_timescale: Option<u32> = None;
3576    let mut sample_table: Option<SampleTable> = None;
3577
3578    let mut iter = src.box_iter();
3579    while let Some(mut b) = iter.next_box()? {
3580        match b.head.name {
3581            BoxType::MovieHeaderBox => {
3582                let _mvhd = read_mvhd(&mut b)?;
3583            }
3584            BoxType::TrackBox => {
3585                // Parse track recursively
3586                // Only use first video track, but consume all tracks
3587                if media_timescale.is_none() {
3588                    if let Some((timescale, stbl)) = read_trak(&mut b)? {
3589                        media_timescale = Some(timescale);
3590                        sample_table = Some(stbl);
3591                    }
3592                } else {
3593                    skip_box_remain(&mut b)?;
3594                }
3595            }
3596            _ => {
3597                skip_box_remain(&mut b)?;
3598            }
3599        }
3600    }
3601
3602    if let (Some(timescale), Some(stbl)) = (media_timescale, sample_table) {
3603        Ok(Some((timescale, stbl)))
3604    } else {
3605        Ok(None)
3606    }
3607}
3608
3609/// Parse track box (trak)
3610/// Returns (media_timescale, sample_table) if this is a valid video track
3611fn read_trak<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<(u32, SampleTable)>> {
3612    let mut iter = src.box_iter();
3613    while let Some(mut b) = iter.next_box()? {
3614        if b.head.name == BoxType::MediaBox {
3615            return read_mdia(&mut b);
3616        } else {
3617            skip_box_remain(&mut b)?;
3618        }
3619    }
3620    Ok(None)
3621}
3622
3623/// Parse media box (mdia)
3624fn read_mdia<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<(u32, SampleTable)>> {
3625    let mut media_timescale = 1000; // default
3626    let mut sample_table: Option<SampleTable> = None;
3627
3628    let mut iter = src.box_iter();
3629    while let Some(mut b) = iter.next_box()? {
3630        match b.head.name {
3631            BoxType::MediaHeaderBox => {
3632                let mdhd = read_mdhd(&mut b)?;
3633                media_timescale = mdhd.timescale;
3634            }
3635            BoxType::MediaInformationBox => {
3636                sample_table = read_minf(&mut b)?;
3637            }
3638            _ => {
3639                skip_box_remain(&mut b)?;
3640            }
3641        }
3642    }
3643
3644    if let Some(stbl) = sample_table {
3645        Ok(Some((media_timescale, stbl)))
3646    } else {
3647        Ok(None)
3648    }
3649}
3650
3651/// Parse media information box (minf)
3652fn read_minf<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<SampleTable>> {
3653    let mut iter = src.box_iter();
3654    while let Some(mut b) = iter.next_box()? {
3655        if b.head.name == BoxType::SampleTableBox {
3656            return Ok(Some(read_stbl(&mut b)?));
3657        } else {
3658            skip_box_remain(&mut b)?;
3659        }
3660    }
3661    Ok(None)
3662}
3663
3664/// Extract animation frames using sample table
3665#[cfg(feature = "eager")]
3666#[allow(deprecated)]
3667fn extract_animation_frames(
3668    sample_table: &SampleTable,
3669    media_timescale: u32,
3670    mdats: &mut [MediaDataBox],
3671) -> Result<TryVec<AnimationFrame>> {
3672    let mut frames = TryVec::new();
3673
3674    // Build sample-to-chunk mapping (expand into per-sample info)
3675    let mut sample_to_chunk_map = TryVec::new();
3676    for (i, entry) in sample_table.sample_to_chunk.iter().enumerate() {
3677        let next_first_chunk = sample_table
3678            .sample_to_chunk
3679            .get(i + 1)
3680            .map(|e| e.first_chunk)
3681            .unwrap_or(u32::MAX);
3682
3683        for chunk_idx in entry.first_chunk..next_first_chunk {
3684            if chunk_idx > sample_table.chunk_offsets.len() as u32 {
3685                break;
3686            }
3687            sample_to_chunk_map.push((chunk_idx, entry.samples_per_chunk))?;
3688        }
3689    }
3690
3691    // Calculate frame durations from time-to-sample
3692    let mut frame_durations = TryVec::new();
3693    for entry in &sample_table.time_to_sample {
3694        for _ in 0..entry.sample_count {
3695            // Convert from media timescale to milliseconds
3696            let duration_ms = if media_timescale > 0 {
3697                ((entry.sample_delta as u64) * 1000) / (media_timescale as u64)
3698            } else {
3699                0
3700            };
3701            frame_durations.push(duration_ms as u32)?;
3702        }
3703    }
3704
3705    // Extract each frame
3706    let sample_count = sample_table.sample_sizes.len();
3707    let mut current_sample = 0;
3708
3709    for (chunk_idx_1based, samples_in_chunk) in &sample_to_chunk_map {
3710        let chunk_idx = (*chunk_idx_1based as usize).saturating_sub(1);
3711        if chunk_idx >= sample_table.chunk_offsets.len() {
3712            continue;
3713        }
3714
3715        let chunk_offset = sample_table.chunk_offsets[chunk_idx];
3716
3717        for sample_in_chunk in 0..*samples_in_chunk {
3718            if current_sample >= sample_count {
3719                break;
3720            }
3721
3722            let sample_size = sample_table.sample_sizes[current_sample];
3723            let duration_ms = frame_durations.get(current_sample).copied().unwrap_or(0);
3724
3725            // Calculate offset within chunk
3726            let mut offset_in_chunk = 0u64;
3727            for s in 0..sample_in_chunk {
3728                let prev_sample = current_sample.saturating_sub((sample_in_chunk - s) as usize);
3729                if prev_sample < sample_count {
3730                    offset_in_chunk += sample_table.sample_sizes[prev_sample] as u64;
3731                }
3732            }
3733
3734            let sample_offset = chunk_offset + offset_in_chunk;
3735
3736            // Extract frame data from mdat
3737            let mut frame_data = TryVec::new();
3738            let mut found = false;
3739
3740            for mdat in mdats.iter_mut() {
3741                let range = ExtentRange::WithLength(Range {
3742                    start: sample_offset,
3743                    end: sample_offset + sample_size as u64,
3744                });
3745
3746                if mdat.contains_extent(&range) {
3747                    mdat.read_extent(&range, &mut frame_data)?;
3748                    found = true;
3749                    break;
3750                }
3751            }
3752
3753            if !found {
3754                log::warn!("Animation frame {} not found in mdat", current_sample);
3755            }
3756
3757            frames.push(AnimationFrame {
3758                data: frame_data,
3759                duration_ms,
3760            })?;
3761
3762            current_sample += 1;
3763        }
3764    }
3765
3766    Ok(frames)
3767}
3768
3769/// Parse an ImageGrid property box
3770/// See ISO/IEC 23008-12:2017 § 6.6.2.3
3771fn read_grid<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<GridConfig> {
3772    let version = read_fullbox_version_no_flags(src, options)?;
3773    if version > 0 {
3774        return Err(Error::Unsupported("grid version > 0"));
3775    }
3776
3777    let flags_byte = src.read_u8()?;
3778    let rows = src.read_u8()?;
3779    let columns = src.read_u8()?;
3780
3781    // flags & 1 determines field size: 0 = 16-bit, 1 = 32-bit
3782    let (output_width, output_height) = if flags_byte & 1 == 0 {
3783        // 16-bit fields
3784        (u32::from(be_u16(src)?), u32::from(be_u16(src)?))
3785    } else {
3786        // 32-bit fields
3787        (be_u32(src)?, be_u32(src)?)
3788    };
3789
3790    Ok(GridConfig {
3791        rows,
3792        columns,
3793        output_width,
3794        output_height,
3795    })
3796}
3797
3798/// Parse an item location box inside a meta box
3799/// See ISO 14496-12:2015 § 8.11.3
3800fn read_iloc<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<ItemLocationBoxItem>> {
3801    let version: IlocVersion = read_fullbox_version_no_flags(src, options)?.try_into()?;
3802
3803    let iloc = src.read_into_try_vec()?;
3804    let mut iloc = BitReader::new(&iloc);
3805
3806    let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
3807    let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
3808    let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
3809
3810    let index_size: Option<IlocFieldSize> = match version {
3811        IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?),
3812        IlocVersion::Zero => {
3813            let _reserved = iloc.read_u8(4)?;
3814            None
3815        },
3816    };
3817
3818    let item_count = match version {
3819        IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?,
3820        IlocVersion::Two => iloc.read_u32(32)?,
3821    };
3822
3823    let mut items = TryVec::with_capacity(item_count.to_usize())?;
3824
3825    for _ in 0..item_count {
3826        let item_id = match version {
3827            IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?,
3828            IlocVersion::Two => iloc.read_u32(32)?,
3829        };
3830
3831        // The spec isn't entirely clear how an `iloc` should be interpreted for version 0,
3832        // which has no `construction_method` field. It does say:
3833        // "For maximum compatibility, version 0 of this box should be used in preference to
3834        //  version 1 with `construction_method==0`, or version 2 when possible."
3835        // We take this to imply version 0 can be interpreted as using file offsets.
3836        let construction_method = match version {
3837            IlocVersion::Zero => ConstructionMethod::File,
3838            IlocVersion::One | IlocVersion::Two => {
3839                let _reserved = iloc.read_u16(12)?;
3840                match iloc.read_u16(4)? {
3841                    0 => ConstructionMethod::File,
3842                    1 => ConstructionMethod::Idat,
3843                    2 => return Err(Error::Unsupported("construction_method 'item_offset' is not supported")),
3844                    _ => return Err(Error::InvalidData("construction_method is taken from the set 0, 1 or 2 per ISO 14496-12:2015 § 8.11.3.3")),
3845                }
3846            },
3847        };
3848
3849        let data_reference_index = iloc.read_u16(16)?;
3850
3851        if data_reference_index != 0 {
3852            return Err(Error::Unsupported("external file references (iloc.data_reference_index != 0) are not supported"));
3853        }
3854
3855        let base_offset = iloc.read_u64(base_offset_size.to_bits())?;
3856        let extent_count = iloc.read_u16(16)?;
3857
3858        if extent_count < 1 {
3859            return Err(Error::InvalidData("extent_count must have a value 1 or greater per ISO 14496-12:2015 § 8.11.3.3"));
3860        }
3861
3862        let mut extents = TryVec::with_capacity(extent_count.to_usize())?;
3863
3864        for _ in 0..extent_count {
3865            // Parsed but currently ignored, see `ItemLocationBoxExtent`
3866            let _extent_index = match &index_size {
3867                None | Some(IlocFieldSize::Zero) => None,
3868                Some(index_size) => {
3869                    debug_assert!(version == IlocVersion::One || version == IlocVersion::Two);
3870                    Some(iloc.read_u64(index_size.to_bits())?)
3871                },
3872            };
3873
3874            // Per ISO 14496-12:2015 § 8.11.3.1:
3875            // "If the offset is not identified (the field has a length of zero), then the
3876            //  beginning of the source (offset 0) is implied"
3877            // This behavior will follow from BitReader::read_u64(0) -> 0.
3878            let extent_offset = iloc.read_u64(offset_size.to_bits())?;
3879            let extent_length = iloc.read_u64(length_size.to_bits())?;
3880
3881            // "If the length is not specified, or specified as zero, then the entire length of
3882            //  the source is implied" (ibid)
3883            let start = base_offset
3884                .checked_add(extent_offset)
3885                .ok_or(Error::InvalidData("offset calculation overflow"))?;
3886            let extent_range = if extent_length == 0 {
3887                ExtentRange::ToEnd(RangeFrom { start })
3888            } else {
3889                let end = start
3890                    .checked_add(extent_length)
3891                    .ok_or(Error::InvalidData("end calculation overflow"))?;
3892                ExtentRange::WithLength(Range { start, end })
3893            };
3894
3895            extents.push(ItemLocationBoxExtent { extent_range })?;
3896        }
3897
3898        items.push(ItemLocationBoxItem { item_id, construction_method, extents })?;
3899    }
3900
3901    if iloc.remaining() == 0 {
3902        Ok(items)
3903    } else {
3904        Err(Error::InvalidData("invalid iloc size"))
3905    }
3906}
3907
3908/// Parse an ftyp box.
3909/// See ISO 14496-12:2015 § 4.3
3910fn read_ftyp<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<FileTypeBox> {
3911    let major = be_u32(src)?;
3912    let minor = be_u32(src)?;
3913    let bytes_left = src.bytes_left();
3914    if !bytes_left.is_multiple_of(4) {
3915        return Err(Error::InvalidData("invalid ftyp size"));
3916    }
3917    // Is a brand_count of zero valid?
3918    let brand_count = bytes_left / 4;
3919    let mut brands = TryVec::with_capacity(brand_count.try_into()?)?;
3920    for _ in 0..brand_count {
3921        brands.push(be_u32(src)?.into())?;
3922    }
3923    Ok(FileTypeBox {
3924        major_brand: From::from(major),
3925        minor_version: minor,
3926        compatible_brands: brands,
3927    })
3928}
3929
3930#[cfg_attr(debug_assertions, track_caller)]
3931fn check_parser_state<T>(header: &BoxHeader, left: &Take<T>) -> Result<(), Error> {
3932    let limit = left.limit();
3933    // Allow fully consumed boxes, or size=0 boxes (where original size was u64::MAX)
3934    if limit == 0 || header.size == u64::MAX {
3935        Ok(())
3936    } else {
3937        debug_assert_eq!(0, limit, "bad parser state bytes left");
3938        Err(Error::InvalidData("unread box content or bad parser sync"))
3939    }
3940}
3941
3942/// Skip a number of bytes that we don't care to parse.
3943fn skip<T: Read>(src: &mut T, bytes: u64) -> Result<()> {
3944    std::io::copy(&mut src.take(bytes), &mut std::io::sink())?;
3945    Ok(())
3946}
3947
3948fn be_u16<T: ReadBytesExt>(src: &mut T) -> Result<u16> {
3949    src.read_u16::<byteorder::BigEndian>().map_err(From::from)
3950}
3951
3952fn be_u32<T: ReadBytesExt>(src: &mut T) -> Result<u32> {
3953    src.read_u32::<byteorder::BigEndian>().map_err(From::from)
3954}
3955
3956fn be_i32<T: ReadBytesExt>(src: &mut T) -> Result<i32> {
3957    src.read_i32::<byteorder::BigEndian>().map_err(From::from)
3958}
3959
3960fn be_u64<T: ReadBytesExt>(src: &mut T) -> Result<u64> {
3961    src.read_u64::<byteorder::BigEndian>().map_err(From::from)
3962}