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#[derive(Debug)]
269#[allow(unused)]
270pub(crate) struct AV1ConfigBox {
271    pub(crate) profile: u8,
272    pub(crate) level: u8,
273    pub(crate) tier: u8,
274    pub(crate) bit_depth: u8,
275    pub(crate) monochrome: bool,
276    pub(crate) chroma_subsampling_x: u8,
277    pub(crate) chroma_subsampling_y: u8,
278    pub(crate) chroma_sample_position: u8,
279    pub(crate) initial_presentation_delay_present: bool,
280    pub(crate) initial_presentation_delay_minus_one: u8,
281    pub(crate) config_obus: TryVec<u8>,
282}
283
284/// Options for parsing AVIF files
285///
286/// Prefer using [`DecodeConfig::lenient()`] with [`AvifParser`] instead.
287#[derive(Debug, Clone, Copy)]
288#[derive(Default)]
289pub struct ParseOptions {
290    /// Enable lenient parsing mode
291    ///
292    /// When true, non-critical validation errors (like non-zero flags in boxes
293    /// that expect zero flags) will be ignored instead of returning errors.
294    /// This allows parsing of slightly malformed but otherwise valid AVIF files.
295    ///
296    /// Default: false (strict validation)
297    pub lenient: bool,
298}
299
300/// Configuration for parsing AVIF files with resource limits and validation options
301///
302/// Provides fine-grained control over resource consumption during AVIF parsing,
303/// allowing defensive parsing against malicious or malformed files.
304///
305/// Resource limits are checked **before** allocations occur, preventing out-of-memory
306/// conditions from malicious files that claim unrealistic dimensions or counts.
307///
308/// # Examples
309///
310/// ```rust
311/// use zenavif_parse::DecodeConfig;
312///
313/// // Default limits (suitable for most apps)
314/// let config = DecodeConfig::default();
315///
316/// // Strict limits for untrusted input
317/// let config = DecodeConfig::default()
318///     .with_peak_memory_limit(100_000_000)  // 100MB
319///     .with_total_megapixels_limit(64)       // 64MP max
320///     .with_max_animation_frames(100);       // 100 frames
321///
322/// // No limits (backwards compatible with read_avif)
323/// let config = DecodeConfig::unlimited();
324/// ```
325#[derive(Debug, Clone)]
326pub struct DecodeConfig {
327    /// Maximum peak heap memory usage in bytes.
328    /// Default: 1GB (1,000,000,000 bytes)
329    pub peak_memory_limit: Option<u64>,
330
331    /// Maximum total megapixels for grid images.
332    /// Default: 512 megapixels
333    pub total_megapixels_limit: Option<u32>,
334
335    /// Maximum number of animation frames.
336    /// Default: 10,000 frames
337    pub max_animation_frames: Option<u32>,
338
339    /// Maximum number of grid tiles.
340    /// Default: 1,000 tiles
341    pub max_grid_tiles: Option<u32>,
342
343    /// Enable lenient parsing mode.
344    /// Default: false (strict validation)
345    pub lenient: bool,
346}
347
348impl Default for DecodeConfig {
349    fn default() -> Self {
350        Self {
351            peak_memory_limit: Some(1_000_000_000),
352            total_megapixels_limit: Some(512),
353            max_animation_frames: Some(10_000),
354            max_grid_tiles: Some(1_000),
355            lenient: false,
356        }
357    }
358}
359
360impl DecodeConfig {
361    /// Create a configuration with no resource limits.
362    ///
363    /// Equivalent to the behavior of `read_avif()` before resource limits were added.
364    pub fn unlimited() -> Self {
365        Self {
366            peak_memory_limit: None,
367            total_megapixels_limit: None,
368            max_animation_frames: None,
369            max_grid_tiles: None,
370            lenient: false,
371        }
372    }
373
374    /// Set the peak memory limit in bytes
375    pub fn with_peak_memory_limit(mut self, bytes: u64) -> Self {
376        self.peak_memory_limit = Some(bytes);
377        self
378    }
379
380    /// Set the total megapixels limit for grid images
381    pub fn with_total_megapixels_limit(mut self, megapixels: u32) -> Self {
382        self.total_megapixels_limit = Some(megapixels);
383        self
384    }
385
386    /// Set the maximum animation frame count
387    pub fn with_max_animation_frames(mut self, frames: u32) -> Self {
388        self.max_animation_frames = Some(frames);
389        self
390    }
391
392    /// Set the maximum grid tile count
393    pub fn with_max_grid_tiles(mut self, tiles: u32) -> Self {
394        self.max_grid_tiles = Some(tiles);
395        self
396    }
397
398    /// Enable lenient parsing mode
399    pub fn lenient(mut self, lenient: bool) -> Self {
400        self.lenient = lenient;
401        self
402    }
403}
404
405/// Grid configuration for tiled/grid-based AVIF images
406#[derive(Debug, Clone, PartialEq)]
407/// Grid image configuration
408///
409/// For tiled/grid AVIF images, this describes the grid layout.
410/// Grid images are composed of multiple AV1 image items (tiles) arranged in a rectangular grid.
411///
412/// ## Grid Layout Determination
413///
414/// Grid layout can be specified in two ways:
415/// 1. **Explicit ImageGrid property box** - contains rows, columns, and output dimensions
416/// 2. **Calculated from ispe properties** - when no ImageGrid box exists, dimensions are
417///    calculated by dividing the grid item's dimensions by a tile's dimensions
418///
419/// ## Output Dimensions
420///
421/// - `output_width` and `output_height` may be 0, indicating the decoder should calculate
422///   them from the tile dimensions
423/// - When non-zero, they specify the exact output dimensions of the composed image
424pub struct GridConfig {
425    /// Number of tile rows (1-256)
426    pub rows: u8,
427    /// Number of tile columns (1-256)
428    pub columns: u8,
429    /// Output width in pixels (0 = calculate from tiles)
430    pub output_width: u32,
431    /// Output height in pixels (0 = calculate from tiles)
432    pub output_height: u32,
433}
434
435/// Frame information for animated AVIF
436#[cfg(feature = "eager")]
437#[deprecated(since = "1.5.0", note = "Use `AvifParser::frame()` which returns `FrameRef` instead")]
438#[derive(Debug)]
439pub struct AnimationFrame {
440    /// AV1 bitstream data for this frame
441    pub data: TryVec<u8>,
442    /// Duration in milliseconds (0 if unknown)
443    pub duration_ms: u32,
444}
445
446/// Animation configuration for animated AVIF (avis brand)
447#[cfg(feature = "eager")]
448#[deprecated(since = "1.5.0", note = "Use `AvifParser::animation_info()` and `AvifParser::frames()` instead")]
449#[derive(Debug)]
450#[allow(deprecated)]
451pub struct AnimationConfig {
452    /// Number of times to loop (0 = infinite)
453    pub loop_count: u32,
454    /// All frames in the animation
455    pub frames: TryVec<AnimationFrame>,
456}
457
458// Internal structures for animation parsing
459
460#[derive(Debug)]
461struct MovieHeader {
462    _timescale: u32,
463    _duration: u64,
464}
465
466#[derive(Debug)]
467struct MediaHeader {
468    timescale: u32,
469    _duration: u64,
470}
471
472#[derive(Debug)]
473struct TimeToSampleEntry {
474    sample_count: u32,
475    sample_delta: u32,
476}
477
478#[derive(Debug)]
479struct SampleToChunkEntry {
480    first_chunk: u32,
481    samples_per_chunk: u32,
482    _sample_description_index: u32,
483}
484
485#[derive(Debug)]
486struct SampleTable {
487    time_to_sample: TryVec<TimeToSampleEntry>,
488    sample_to_chunk: TryVec<SampleToChunkEntry>,
489    sample_sizes: TryVec<u32>,
490    chunk_offsets: TryVec<u64>,
491}
492
493#[cfg(feature = "eager")]
494#[deprecated(since = "1.5.0", note = "Use `AvifParser` for zero-copy parsing instead")]
495#[derive(Debug, Default)]
496#[allow(deprecated)]
497pub struct AvifData {
498    /// AV1 data for the color channels.
499    ///
500    /// The collected data indicated by the `pitm` box, See ISO 14496-12:2015 § 8.11.4
501    pub primary_item: TryVec<u8>,
502    /// AV1 data for alpha channel.
503    ///
504    /// Associated alpha channel for the primary item, if any
505    pub alpha_item: Option<TryVec<u8>>,
506    /// If true, divide RGB values by the alpha value.
507    ///
508    /// See `prem` in MIAF § 7.3.5.2
509    pub premultiplied_alpha: bool,
510
511    /// Grid configuration for tiled images.
512    ///
513    /// If present, the image is a grid and `grid_tiles` contains the tile data.
514    /// Grid layout is determined either from an explicit ImageGrid property box or
515    /// calculated from ispe (Image Spatial Extents) properties.
516    ///
517    /// ## Example
518    ///
519    /// ```no_run
520    /// #[allow(deprecated)]
521    /// use std::fs::File;
522    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
523    /// #[allow(deprecated)]
524    /// let data = zenavif_parse::read_avif(&mut File::open("image.avif")?)?;
525    ///
526    /// if let Some(grid) = data.grid_config {
527    ///     println!("Grid: {}×{} tiles", grid.rows, grid.columns);
528    ///     println!("Output: {}×{}", grid.output_width, grid.output_height);
529    ///     println!("Tile count: {}", data.grid_tiles.len());
530    /// }
531    /// # Ok(())
532    /// # }
533    /// ```
534    pub grid_config: Option<GridConfig>,
535
536    /// AV1 payloads for grid image tiles.
537    ///
538    /// Empty for non-grid images. For grid images, contains one entry per tile.
539    ///
540    /// **Tile ordering:** Tiles are guaranteed to be in the correct order for grid assembly,
541    /// sorted by their dimgIdx (reference index). This is row-major order: tiles in the first
542    /// row from left to right, then the second row, etc.
543    pub grid_tiles: TryVec<TryVec<u8>>,
544
545    /// Animation configuration (for animated AVIF with avis brand)
546    ///
547    /// When present, primary_item contains the first frame
548    pub animation: Option<AnimationConfig>,
549}
550
551// # Memory Usage
552//
553// This implementation loads all image data into owned vectors (`TryVec<u8>`), which has
554// memory implications depending on the file type:
555//
556// - **Static images**: Single copy of compressed data (~5-50KB typical)
557//   - `primary_item`: compressed AV1 data
558//   - `alpha_item`: compressed alpha data (if present)
559//
560// - **Grid images**: All tiles loaded (~100KB-2MB for large grids)
561//   - `grid_tiles`: one compressed tile per grid cell
562//
563// - **Animated images**: All frames loaded eagerly (⚠️ HIGH MEMORY)
564//   - Internal mdat boxes: ~500KB for 95-frame video
565//   - Extracted frames: ~500KB duplicated in `animation.frames[].data`
566//   - **Total: ~2× file size in memory**
567//
568// For large animated files, consider using a streaming approach or processing frames
569// individually rather than loading the entire `AvifData` structure.
570
571#[cfg(feature = "eager")]
572#[allow(deprecated)]
573impl AvifData {
574    #[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader()` instead")]
575    pub fn from_reader<R: Read>(reader: &mut R) -> Result<Self> {
576        read_avif(reader)
577    }
578
579    /// Parses AV1 data to get basic properties of the opaque channel
580    pub fn primary_item_metadata(&self) -> Result<AV1Metadata> {
581        AV1Metadata::parse_av1_bitstream(&self.primary_item)
582    }
583
584    /// Parses AV1 data to get basic properties about the alpha channel, if any
585    pub fn alpha_item_metadata(&self) -> Result<Option<AV1Metadata>> {
586        self.alpha_item.as_deref().map(AV1Metadata::parse_av1_bitstream).transpose()
587    }
588}
589
590/// AV1 sequence header metadata parsed from an OBU bitstream.
591///
592/// See [`AvifParser::primary_metadata()`] and [`AV1Metadata::parse_av1_bitstream()`].
593#[non_exhaustive]
594#[derive(Debug, Clone)]
595pub struct AV1Metadata {
596    /// Should be true for non-animated AVIF
597    pub still_picture: bool,
598    pub max_frame_width: NonZeroU32,
599    pub max_frame_height: NonZeroU32,
600    /// 8, 10, or 12
601    pub bit_depth: u8,
602    /// 0, 1 or 2 for the level of complexity
603    pub seq_profile: u8,
604    /// Horizontal and vertical. `false` is full-res.
605    pub chroma_subsampling: (bool, bool),
606    pub monochrome: bool,
607}
608
609impl AV1Metadata {
610    /// Parses raw AV1 bitstream (OBU sequence header) only.
611    ///
612    /// This is for the bare image payload from an encoder, not an AVIF/HEIF file.
613    /// To parse AVIF files, see [`AvifParser::from_reader()`].
614    #[inline(never)]
615    pub fn parse_av1_bitstream(obu_bitstream: &[u8]) -> Result<Self> {
616        let h = obu::parse_obu(obu_bitstream)?;
617        Ok(Self {
618            still_picture: h.still_picture,
619            max_frame_width: h.max_frame_width,
620            max_frame_height: h.max_frame_height,
621            bit_depth: h.color.bit_depth,
622            seq_profile: h.seq_profile,
623            chroma_subsampling: h.color.chroma_subsampling,
624            monochrome: h.color.monochrome,
625        })
626    }
627}
628
629/// A single frame from an animated AVIF, with zero-copy when possible.
630///
631/// The `data` field is `Cow::Borrowed` when the frame lives in a single
632/// contiguous mdat extent, and `Cow::Owned` when extents must be concatenated.
633pub struct FrameRef<'a> {
634    pub data: Cow<'a, [u8]>,
635    pub duration_ms: u32,
636}
637
638/// Byte range of a media data box within the file.
639struct MdatBounds {
640    offset: u64,
641    length: u64,
642}
643
644/// Where an item's data lives: construction method + extent ranges.
645struct ItemExtents {
646    construction_method: ConstructionMethod,
647    extents: TryVec<ExtentRange>,
648}
649
650/// Zero-copy AVIF parser backed by a borrowed or owned byte buffer.
651///
652/// `AvifParser` records byte offsets during parsing but does **not** copy
653/// mdat payload data. Data access methods return `Cow<[u8]>` — borrowed
654/// when the item is a single contiguous extent, owned when extents must
655/// be concatenated.
656///
657/// # Constructors
658///
659/// | Method | Lifetime | Zero-copy? |
660/// |--------|----------|------------|
661/// | [`from_bytes`](Self::from_bytes) | `'data` | Yes — borrows the slice |
662/// | [`from_owned`](Self::from_owned) | `'static` | Within the owned buffer |
663/// | [`from_reader`](Self::from_reader) | `'static` | Reads all, then owned |
664///
665/// # Example
666///
667/// ```no_run
668/// use zenavif_parse::AvifParser;
669///
670/// let bytes = std::fs::read("image.avif")?;
671/// let parser = AvifParser::from_bytes(&bytes)?;
672/// let primary = parser.primary_data()?; // Cow::Borrowed for single-extent
673/// # Ok::<(), Box<dyn std::error::Error>>(())
674/// ```
675pub struct AvifParser<'data> {
676    raw: Cow<'data, [u8]>,
677    mdat_bounds: TryVec<MdatBounds>,
678    idat: Option<TryVec<u8>>,
679    primary: ItemExtents,
680    alpha: Option<ItemExtents>,
681    grid_config: Option<GridConfig>,
682    tiles: TryVec<ItemExtents>,
683    animation_data: Option<AnimationParserData>,
684    premultiplied_alpha: bool,
685}
686
687struct AnimationParserData {
688    media_timescale: u32,
689    sample_table: SampleTable,
690    loop_count: u32,
691}
692
693/// Animation metadata from [`AvifParser`]
694#[derive(Debug, Clone, Copy)]
695pub struct AnimationInfo {
696    pub frame_count: usize,
697    pub loop_count: u32,
698}
699
700/// Parsed structure from the box-level parse pass (no mdat data).
701struct ParsedStructure {
702    meta: AvifInternalMeta,
703    mdat_bounds: TryVec<MdatBounds>,
704    animation_data: Option<(u32, SampleTable, u32)>,
705}
706
707impl<'data> AvifParser<'data> {
708    // ========================================
709    // Constructors
710    // ========================================
711
712    /// Parse AVIF from a borrowed byte slice (true zero-copy).
713    ///
714    /// The returned parser borrows `data` — single-extent items will be
715    /// returned as `Cow::Borrowed` slices into this buffer.
716    pub fn from_bytes(data: &'data [u8]) -> Result<Self> {
717        Self::from_bytes_with_config(data, &DecodeConfig::unlimited(), &Unstoppable)
718    }
719
720    /// Parse AVIF from a borrowed byte slice with resource limits.
721    pub fn from_bytes_with_config(
722        data: &'data [u8],
723        config: &DecodeConfig,
724        stop: &dyn Stop,
725    ) -> Result<Self> {
726        let parsed = Self::parse_raw(data, config, stop)?;
727        Self::build(Cow::Borrowed(data), parsed, config)
728    }
729
730    /// Parse AVIF from an owned buffer.
731    ///
732    /// The returned parser owns the data — single-extent items will still
733    /// be returned as `Cow::Borrowed` slices (borrowing from the internal buffer).
734    pub fn from_owned(data: std::vec::Vec<u8>) -> Result<AvifParser<'static>> {
735        AvifParser::from_owned_with_config(data, &DecodeConfig::unlimited(), &Unstoppable)
736    }
737
738    /// Parse AVIF from an owned buffer with resource limits.
739    pub fn from_owned_with_config(
740        data: std::vec::Vec<u8>,
741        config: &DecodeConfig,
742        stop: &dyn Stop,
743    ) -> Result<AvifParser<'static>> {
744        let parsed = AvifParser::parse_raw(&data, config, stop)?;
745        AvifParser::build(Cow::Owned(data), parsed, config)
746    }
747
748    /// Parse AVIF from a reader (reads all bytes, then parses).
749    pub fn from_reader<R: Read>(reader: &mut R) -> Result<AvifParser<'static>> {
750        AvifParser::from_reader_with_config(reader, &DecodeConfig::unlimited(), &Unstoppable)
751    }
752
753    /// Parse AVIF from a reader with resource limits.
754    pub fn from_reader_with_config<R: Read>(
755        reader: &mut R,
756        config: &DecodeConfig,
757        stop: &dyn Stop,
758    ) -> Result<AvifParser<'static>> {
759        let mut buf = std::vec::Vec::new();
760        reader.read_to_end(&mut buf)?;
761        AvifParser::from_owned_with_config(buf, config, stop)
762    }
763
764    // ========================================
765    // Internal: parse pass (records offsets, no mdat copy)
766    // ========================================
767
768    /// Parse the AVIF box structure from raw bytes, recording mdat offsets
769    /// without copying mdat content.
770    fn parse_raw(data: &[u8], config: &DecodeConfig, stop: &dyn Stop) -> Result<ParsedStructure> {
771        let parse_opts = ParseOptions { lenient: config.lenient };
772        let mut cursor = std::io::Cursor::new(data);
773        let mut f = OffsetReader::new(&mut cursor);
774        let mut iter = BoxIter::new(&mut f);
775
776        // 'ftyp' box must occur first; see ISO 14496-12:2015 § 4.3.1
777        if let Some(mut b) = iter.next_box()? {
778            if b.head.name == BoxType::FileTypeBox {
779                let ftyp = read_ftyp(&mut b)?;
780                if ftyp.major_brand != b"avif" && ftyp.major_brand != b"avis" {
781                    return Err(Error::InvalidData("ftyp must be 'avif' or 'avis'"));
782                }
783            } else {
784                return Err(Error::InvalidData("'ftyp' box must occur first"));
785            }
786        }
787
788        let mut meta = None;
789        let mut mdat_bounds = TryVec::new();
790        let mut animation_data: Option<(u32, SampleTable, u32)> = None;
791
792        while let Some(mut b) = iter.next_box()? {
793            stop.check()?;
794
795            match b.head.name {
796                BoxType::MetadataBox => {
797                    if meta.is_some() {
798                        return Err(Error::InvalidData(
799                            "There should be zero or one meta boxes per ISO 14496-12:2015 § 8.11.1.1",
800                        ));
801                    }
802                    meta = Some(read_avif_meta(&mut b, &parse_opts)?);
803                }
804                BoxType::MovieBox => {
805                    if let Some((media_timescale, sample_table)) = read_moov(&mut b)? {
806                        animation_data = Some((media_timescale, sample_table, 0));
807                    }
808                }
809                BoxType::MediaDataBox => {
810                    if b.bytes_left() > 0 {
811                        let offset = b.offset();
812                        let length = b.bytes_left();
813                        mdat_bounds.push(MdatBounds { offset, length })?;
814                    }
815                    // Skip the content — we'll slice into raw later
816                    skip_box_content(&mut b)?;
817                }
818                _ => skip_box_content(&mut b)?,
819            }
820
821            check_parser_state(&b.head, &b.content)?;
822        }
823
824        let meta = meta.ok_or(Error::InvalidData("missing meta"))?;
825
826        Ok(ParsedStructure { meta, mdat_bounds, animation_data })
827    }
828
829    /// Build an AvifParser from raw bytes + parsed structure.
830    fn build(raw: Cow<'data, [u8]>, parsed: ParsedStructure, config: &DecodeConfig) -> Result<Self> {
831        let tracker = ResourceTracker::new(config);
832        let meta = parsed.meta;
833
834        // Get primary item extents
835        let primary = Self::get_item_extents(&meta, meta.primary_item_id)?;
836
837        // Find alpha item and get its extents
838        let alpha_item_id = meta
839            .item_references
840            .iter()
841            .filter(|iref| {
842                iref.to_item_id == meta.primary_item_id
843                    && iref.from_item_id != meta.primary_item_id
844                    && iref.item_type == b"auxl"
845            })
846            .map(|iref| iref.from_item_id)
847            .find(|&item_id| {
848                meta.properties.iter().any(|prop| {
849                    prop.item_id == item_id
850                        && match &prop.property {
851                            ItemProperty::AuxiliaryType(urn) => {
852                                urn.type_subtype().0 == b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"
853                            }
854                            _ => false,
855                        }
856                })
857            });
858
859        let alpha = alpha_item_id
860            .map(|id| Self::get_item_extents(&meta, id))
861            .transpose()?;
862
863        // Check for premultiplied alpha
864        let premultiplied_alpha = alpha_item_id.map_or(false, |alpha_id| {
865            meta.item_references.iter().any(|iref| {
866                iref.from_item_id == meta.primary_item_id
867                    && iref.to_item_id == alpha_id
868                    && iref.item_type == b"prem"
869            })
870        });
871
872        // Check if primary item is a grid (tiled image)
873        let is_grid = meta
874            .item_infos
875            .iter()
876            .find(|x| x.item_id == meta.primary_item_id)
877            .map_or(false, |info| info.item_type == b"grid");
878
879        // Extract grid configuration and tile extents if this is a grid
880        let (grid_config, tiles) = if is_grid {
881            let mut tiles_with_index: TryVec<(u32, u16)> = TryVec::new();
882            for iref in meta.item_references.iter() {
883                if iref.from_item_id == meta.primary_item_id && iref.item_type == b"dimg" {
884                    tiles_with_index.push((iref.to_item_id, iref.reference_index))?;
885                }
886            }
887
888            tracker.validate_grid_tiles(tiles_with_index.len() as u32)?;
889            tiles_with_index.sort_by_key(|&(_, idx)| idx);
890
891            let mut tile_extents = TryVec::new();
892            for (tile_id, _) in tiles_with_index.iter() {
893                tile_extents.push(Self::get_item_extents(&meta, *tile_id)?)?;
894            }
895
896            let mut tile_ids = TryVec::new();
897            for (tile_id, _) in tiles_with_index.iter() {
898                tile_ids.push(*tile_id)?;
899            }
900
901            let grid_config = Self::calculate_grid_config(&meta, &tile_ids)?;
902            (Some(grid_config), tile_extents)
903        } else {
904            (None, TryVec::new())
905        };
906
907        // Store animation metadata if present
908        let animation_data = if let Some((media_timescale, sample_table, loop_count)) = parsed.animation_data {
909            tracker.validate_animation_frames(sample_table.sample_sizes.len() as u32)?;
910            Some(AnimationParserData { media_timescale, sample_table, loop_count })
911        } else {
912            None
913        };
914
915        // Clone idat
916        let idat = if let Some(ref idat_data) = meta.idat {
917            let mut cloned = TryVec::new();
918            cloned.extend_from_slice(idat_data)?;
919            Some(cloned)
920        } else {
921            None
922        };
923
924        Ok(Self {
925            raw,
926            mdat_bounds: parsed.mdat_bounds,
927            idat,
928            primary,
929            alpha,
930            grid_config,
931            tiles,
932            animation_data,
933            premultiplied_alpha,
934        })
935    }
936
937    // ========================================
938    // Internal helpers
939    // ========================================
940
941    /// Get item extents (construction method + ranges) from metadata.
942    fn get_item_extents(meta: &AvifInternalMeta, item_id: u32) -> Result<ItemExtents> {
943        let item = meta
944            .iloc_items
945            .iter()
946            .find(|item| item.item_id == item_id)
947            .ok_or(Error::InvalidData("item not found in iloc"))?;
948
949        let mut extents = TryVec::new();
950        for extent in &item.extents {
951            extents.push(extent.extent_range.clone())?;
952        }
953        Ok(ItemExtents {
954            construction_method: item.construction_method,
955            extents,
956        })
957    }
958
959    /// Resolve an item's data from the raw buffer, returning `Cow::Borrowed`
960    /// for single-extent file items and `Cow::Owned` for multi-extent or idat.
961    fn resolve_item(&self, item: &ItemExtents) -> Result<Cow<'_, [u8]>> {
962        match item.construction_method {
963            ConstructionMethod::Idat => self.resolve_idat_extents(&item.extents),
964            ConstructionMethod::File => self.resolve_file_extents(&item.extents),
965            ConstructionMethod::Item => Err(Error::Unsupported("construction_method 'item' not supported")),
966        }
967    }
968
969    /// Resolve file-based extents from the raw buffer.
970    fn resolve_file_extents(&self, extents: &[ExtentRange]) -> Result<Cow<'_, [u8]>> {
971        let raw = self.raw.as_ref();
972
973        // Fast path: single extent → borrow directly from raw
974        if extents.len() == 1 {
975            let extent = &extents[0];
976            let (start, end) = self.extent_byte_range(extent)?;
977            let slice = raw.get(start..end).ok_or(Error::InvalidData("extent out of bounds in raw buffer"))?;
978            return Ok(Cow::Borrowed(slice));
979        }
980
981        // Multi-extent: concatenate into owned buffer
982        let mut data = TryVec::new();
983        for extent in extents {
984            let (start, end) = self.extent_byte_range(extent)?;
985            let slice = raw.get(start..end).ok_or(Error::InvalidData("extent out of bounds in raw buffer"))?;
986            data.extend_from_slice(slice)?;
987        }
988        Ok(Cow::Owned(data.to_vec()))
989    }
990
991    /// Convert an ExtentRange to a (start, end) byte range within the raw buffer.
992    fn extent_byte_range(&self, extent: &ExtentRange) -> Result<(usize, usize)> {
993        let file_offset = extent.start();
994        let start = usize::try_from(file_offset)?;
995
996        match extent {
997            ExtentRange::WithLength(range) => {
998                let len = range.end.checked_sub(range.start)
999                    .ok_or(Error::InvalidData("extent range start > end"))?;
1000                let end = start.checked_add(usize::try_from(len)?)
1001                    .ok_or(Error::InvalidData("extent end overflow"))?;
1002                Ok((start, end))
1003            }
1004            ExtentRange::ToEnd(_) => {
1005                // Find the mdat that contains this offset and use its bounds
1006                for mdat in &self.mdat_bounds {
1007                    if file_offset >= mdat.offset && file_offset < mdat.offset + mdat.length {
1008                        let end = usize::try_from(mdat.offset + mdat.length)?;
1009                        return Ok((start, end));
1010                    }
1011                }
1012                // Fall back to end of raw buffer
1013                Ok((start, self.raw.len()))
1014            }
1015        }
1016    }
1017
1018    /// Resolve idat-based extents.
1019    fn resolve_idat_extents(&self, extents: &[ExtentRange]) -> Result<Cow<'_, [u8]>> {
1020        let idat_data = self.idat.as_ref()
1021            .ok_or(Error::InvalidData("idat box missing but construction_method is Idat"))?;
1022
1023        if extents.len() == 1 {
1024            let extent = &extents[0];
1025            let start = usize::try_from(extent.start())?;
1026            let slice = match extent {
1027                ExtentRange::WithLength(range) => {
1028                    let len = usize::try_from(range.end - range.start)?;
1029                    idat_data.get(start..start + len)
1030                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1031                }
1032                ExtentRange::ToEnd(_) => {
1033                    idat_data.get(start..)
1034                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1035                }
1036            };
1037            return Ok(Cow::Borrowed(slice));
1038        }
1039
1040        // Multi-extent idat: concatenate
1041        let mut data = TryVec::new();
1042        for extent in extents {
1043            let start = usize::try_from(extent.start())?;
1044            let slice = match extent {
1045                ExtentRange::WithLength(range) => {
1046                    let len = usize::try_from(range.end - range.start)?;
1047                    idat_data.get(start..start + len)
1048                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1049                }
1050                ExtentRange::ToEnd(_) => {
1051                    idat_data.get(start..)
1052                        .ok_or(Error::InvalidData("idat extent out of bounds"))?
1053                }
1054            };
1055            data.extend_from_slice(slice)?;
1056        }
1057        Ok(Cow::Owned(data.to_vec()))
1058    }
1059
1060    /// Resolve a single animation frame from the raw buffer.
1061    fn resolve_frame(&self, index: usize) -> Result<FrameRef<'_>> {
1062        let anim = self.animation_data.as_ref()
1063            .ok_or(Error::InvalidData("not an animated AVIF"))?;
1064
1065        if index >= anim.sample_table.sample_sizes.len() {
1066            return Err(Error::InvalidData("frame index out of bounds"));
1067        }
1068
1069        let duration_ms = self.calculate_frame_duration(&anim.sample_table, anim.media_timescale, index)?;
1070        let (offset, size) = self.calculate_sample_location(&anim.sample_table, index)?;
1071
1072        let start = usize::try_from(offset)?;
1073        let end = start.checked_add(size as usize)
1074            .ok_or(Error::InvalidData("frame end overflow"))?;
1075
1076        let raw = self.raw.as_ref();
1077        let slice = raw.get(start..end)
1078            .ok_or(Error::InvalidData("frame not found in raw buffer"))?;
1079
1080        Ok(FrameRef {
1081            data: Cow::Borrowed(slice),
1082            duration_ms,
1083        })
1084    }
1085
1086    /// Calculate grid configuration from metadata.
1087    fn calculate_grid_config(meta: &AvifInternalMeta, tile_ids: &[u32]) -> Result<GridConfig> {
1088        // Try explicit grid property first
1089        for prop in &meta.properties {
1090            if prop.item_id == meta.primary_item_id {
1091                if let ItemProperty::ImageGrid(grid) = &prop.property {
1092                    return Ok(grid.clone());
1093                }
1094            }
1095        }
1096
1097        // Fall back to ispe calculation
1098        let grid_dims = meta
1099            .properties
1100            .iter()
1101            .find(|p| p.item_id == meta.primary_item_id)
1102            .and_then(|p| match &p.property {
1103                ItemProperty::ImageSpatialExtents(e) => Some(e),
1104                _ => None,
1105            });
1106
1107        let tile_dims = tile_ids.first().and_then(|&tile_id| {
1108            meta.properties
1109                .iter()
1110                .find(|p| p.item_id == tile_id)
1111                .and_then(|p| match &p.property {
1112                    ItemProperty::ImageSpatialExtents(e) => Some(e),
1113                    _ => None,
1114                })
1115        });
1116
1117        if let (Some(grid), Some(tile)) = (grid_dims, tile_dims) {
1118            if tile.width != 0
1119                && tile.height != 0
1120                && grid.width % tile.width == 0
1121                && grid.height % tile.height == 0
1122            {
1123                let columns = grid.width / tile.width;
1124                let rows = grid.height / tile.height;
1125
1126                if columns <= 255 && rows <= 255 {
1127                    return Ok(GridConfig {
1128                        rows: rows as u8,
1129                        columns: columns as u8,
1130                        output_width: grid.width,
1131                        output_height: grid.height,
1132                    });
1133                }
1134            }
1135        }
1136
1137        let tile_count = tile_ids.len();
1138        Ok(GridConfig {
1139            rows: tile_count.min(255) as u8,
1140            columns: 1,
1141            output_width: 0,
1142            output_height: 0,
1143        })
1144    }
1145
1146    /// Calculate frame duration from sample table.
1147    fn calculate_frame_duration(
1148        &self,
1149        st: &SampleTable,
1150        timescale: u32,
1151        index: usize,
1152    ) -> Result<u32> {
1153        let mut current_sample = 0;
1154        for entry in &st.time_to_sample {
1155            if current_sample + entry.sample_count as usize > index {
1156                let duration_ms = if timescale > 0 {
1157                    ((entry.sample_delta as u64) * 1000) / (timescale as u64)
1158                } else {
1159                    0
1160                };
1161                return Ok(duration_ms as u32);
1162            }
1163            current_sample += entry.sample_count as usize;
1164        }
1165        Ok(0)
1166    }
1167
1168    /// Calculate sample location (offset and size) from sample table.
1169    fn calculate_sample_location(&self, st: &SampleTable, index: usize) -> Result<(u64, u32)> {
1170        let sample_size = *st
1171            .sample_sizes
1172            .get(index)
1173            .ok_or(Error::InvalidData("sample index out of bounds"))?;
1174
1175        let mut current_sample = 0;
1176        for (chunk_map_idx, entry) in st.sample_to_chunk.iter().enumerate() {
1177            let next_first_chunk = st
1178                .sample_to_chunk
1179                .get(chunk_map_idx + 1)
1180                .map(|e| e.first_chunk)
1181                .unwrap_or(u32::MAX);
1182
1183            for chunk_idx in entry.first_chunk..next_first_chunk {
1184                if chunk_idx == 0 || (chunk_idx as usize) > st.chunk_offsets.len() {
1185                    break;
1186                }
1187
1188                let chunk_offset = st.chunk_offsets[(chunk_idx - 1) as usize];
1189
1190                for sample_in_chunk in 0..entry.samples_per_chunk {
1191                    if current_sample == index {
1192                        let mut offset_in_chunk = 0u64;
1193                        for s in 0..sample_in_chunk {
1194                            let prev_idx = current_sample.saturating_sub((sample_in_chunk - s) as usize);
1195                            if let Some(&prev_size) = st.sample_sizes.get(prev_idx) {
1196                                offset_in_chunk += prev_size as u64;
1197                            }
1198                        }
1199
1200                        return Ok((chunk_offset + offset_in_chunk, sample_size));
1201                    }
1202                    current_sample += 1;
1203                }
1204            }
1205        }
1206
1207        Err(Error::InvalidData("sample not found in chunk table"))
1208    }
1209
1210    // ========================================
1211    // Public data access API (one way each)
1212    // ========================================
1213
1214    /// Get primary item data.
1215    ///
1216    /// Returns `Cow::Borrowed` for single-extent items, `Cow::Owned` for multi-extent.
1217    pub fn primary_data(&self) -> Result<Cow<'_, [u8]>> {
1218        self.resolve_item(&self.primary)
1219    }
1220
1221    /// Get alpha item data, if present.
1222    pub fn alpha_data(&self) -> Option<Result<Cow<'_, [u8]>>> {
1223        self.alpha.as_ref().map(|item| self.resolve_item(item))
1224    }
1225
1226    /// Get grid tile data by index.
1227    pub fn tile_data(&self, index: usize) -> Result<Cow<'_, [u8]>> {
1228        let item = self.tiles.get(index)
1229            .ok_or(Error::InvalidData("tile index out of bounds"))?;
1230        self.resolve_item(item)
1231    }
1232
1233    /// Get a single animation frame by index.
1234    pub fn frame(&self, index: usize) -> Result<FrameRef<'_>> {
1235        self.resolve_frame(index)
1236    }
1237
1238    /// Iterate over all animation frames.
1239    pub fn frames(&self) -> FrameIterator<'_> {
1240        let count = self
1241            .animation_info()
1242            .map(|info| info.frame_count)
1243            .unwrap_or(0);
1244        FrameIterator { parser: self, index: 0, count }
1245    }
1246
1247    // ========================================
1248    // Metadata (no data access)
1249    // ========================================
1250
1251    /// Get animation metadata (if animated).
1252    pub fn animation_info(&self) -> Option<AnimationInfo> {
1253        self.animation_data.as_ref().map(|data| AnimationInfo {
1254            frame_count: data.sample_table.sample_sizes.len(),
1255            loop_count: data.loop_count,
1256        })
1257    }
1258
1259    /// Get grid configuration (if grid image).
1260    pub fn grid_config(&self) -> Option<&GridConfig> {
1261        self.grid_config.as_ref()
1262    }
1263
1264    /// Get number of grid tiles.
1265    pub fn grid_tile_count(&self) -> usize {
1266        self.tiles.len()
1267    }
1268
1269    /// Check if alpha channel uses premultiplied alpha.
1270    pub fn premultiplied_alpha(&self) -> bool {
1271        self.premultiplied_alpha
1272    }
1273
1274    /// Parse AV1 metadata from the primary item.
1275    pub fn primary_metadata(&self) -> Result<AV1Metadata> {
1276        let data = self.primary_data()?;
1277        AV1Metadata::parse_av1_bitstream(&data)
1278    }
1279
1280    /// Parse AV1 metadata from the alpha item, if present.
1281    pub fn alpha_metadata(&self) -> Option<Result<AV1Metadata>> {
1282        self.alpha.as_ref().map(|item| {
1283            let data = self.resolve_item(item)?;
1284            AV1Metadata::parse_av1_bitstream(&data)
1285        })
1286    }
1287
1288    // ========================================
1289    // Conversion
1290    // ========================================
1291
1292    /// Convert to [`AvifData`] (eagerly loads all frames and tiles).
1293    ///
1294    /// Provided for migration from the eager API. Prefer using `AvifParser`
1295    /// methods directly.
1296    #[cfg(feature = "eager")]
1297    #[deprecated(since = "1.5.0", note = "Use AvifParser methods directly instead of converting to AvifData")]
1298    #[allow(deprecated)]
1299    pub fn to_avif_data(&self) -> Result<AvifData> {
1300        let primary_data = self.primary_data()?;
1301        let mut primary_item = TryVec::new();
1302        primary_item.extend_from_slice(&primary_data)?;
1303
1304        let alpha_item = match self.alpha_data() {
1305            Some(Ok(data)) => {
1306                let mut v = TryVec::new();
1307                v.extend_from_slice(&data)?;
1308                Some(v)
1309            }
1310            Some(Err(e)) => return Err(e),
1311            None => None,
1312        };
1313
1314        let mut grid_tiles = TryVec::new();
1315        for i in 0..self.grid_tile_count() {
1316            let data = self.tile_data(i)?;
1317            let mut v = TryVec::new();
1318            v.extend_from_slice(&data)?;
1319            grid_tiles.push(v)?;
1320        }
1321
1322        let animation = if let Some(info) = self.animation_info() {
1323            let mut frames = TryVec::new();
1324            for i in 0..info.frame_count {
1325                let frame_ref = self.frame(i)?;
1326                let mut data = TryVec::new();
1327                data.extend_from_slice(&frame_ref.data)?;
1328                frames.push(AnimationFrame { data, duration_ms: frame_ref.duration_ms })?;
1329            }
1330            Some(AnimationConfig {
1331                loop_count: info.loop_count,
1332                frames,
1333            })
1334        } else {
1335            None
1336        };
1337
1338        Ok(AvifData {
1339            primary_item,
1340            alpha_item,
1341            premultiplied_alpha: self.premultiplied_alpha,
1342            grid_config: self.grid_config.clone(),
1343            grid_tiles,
1344            animation,
1345        })
1346    }
1347}
1348
1349/// Iterator over animation frames.
1350///
1351/// Created by [`AvifParser::frames()`]. Yields [`FrameRef`] on demand.
1352pub struct FrameIterator<'a> {
1353    parser: &'a AvifParser<'a>,
1354    index: usize,
1355    count: usize,
1356}
1357
1358impl<'a> Iterator for FrameIterator<'a> {
1359    type Item = Result<FrameRef<'a>>;
1360
1361    fn next(&mut self) -> Option<Self::Item> {
1362        if self.index >= self.count {
1363            return None;
1364        }
1365        let result = self.parser.frame(self.index);
1366        self.index += 1;
1367        Some(result)
1368    }
1369
1370    fn size_hint(&self) -> (usize, Option<usize>) {
1371        let remaining = self.count.saturating_sub(self.index);
1372        (remaining, Some(remaining))
1373    }
1374}
1375
1376impl ExactSizeIterator for FrameIterator<'_> {
1377    fn len(&self) -> usize {
1378        self.count.saturating_sub(self.index)
1379    }
1380}
1381
1382struct AvifInternalMeta {
1383    item_references: TryVec<SingleItemTypeReferenceBox>,
1384    properties: TryVec<AssociatedProperty>,
1385    primary_item_id: u32,
1386    iloc_items: TryVec<ItemLocationBoxItem>,
1387    item_infos: TryVec<ItemInfoEntry>,
1388    idat: Option<TryVec<u8>>,
1389}
1390
1391/// A Media Data Box
1392/// See ISO 14496-12:2015 § 8.1.1
1393#[cfg(feature = "eager")]
1394struct MediaDataBox {
1395    /// Offset of `data` from the beginning of the file. See `ConstructionMethod::File`
1396    offset: u64,
1397    data: TryVec<u8>,
1398}
1399
1400#[cfg(feature = "eager")]
1401impl MediaDataBox {
1402    /// Check whether the beginning of `extent` is within the bounds of the `MediaDataBox`.
1403    /// We assume extents to not cross box boundaries. If so, this will cause an error
1404    /// in `read_extent`.
1405    fn contains_extent(&self, extent: &ExtentRange) -> bool {
1406        if self.offset <= extent.start() {
1407            let start_offset = extent.start() - self.offset;
1408            start_offset < self.data.len().to_u64()
1409        } else {
1410            false
1411        }
1412    }
1413
1414    /// Check whether `extent` covers the `MediaDataBox` exactly.
1415    fn matches_extent(&self, extent: &ExtentRange) -> bool {
1416        if self.offset == extent.start() {
1417            match extent {
1418                ExtentRange::WithLength(range) => {
1419                    if let Some(end) = self.offset.checked_add(self.data.len().to_u64()) {
1420                        end == range.end
1421                    } else {
1422                        false
1423                    }
1424                },
1425                ExtentRange::ToEnd(_) => true,
1426            }
1427        } else {
1428            false
1429        }
1430    }
1431
1432    /// Copy the range specified by `extent` to the end of `buf` or return an error if the range
1433    /// is not fully contained within `MediaDataBox`.
1434    fn read_extent(&self, extent: &ExtentRange, buf: &mut TryVec<u8>) -> Result<()> {
1435        let start_offset = extent
1436            .start()
1437            .checked_sub(self.offset)
1438            .ok_or(Error::InvalidData("mdat does not contain extent"))?;
1439        let slice = match extent {
1440            ExtentRange::WithLength(range) => {
1441                let range_len = range
1442                    .end
1443                    .checked_sub(range.start)
1444                    .ok_or(Error::InvalidData("range start > end"))?;
1445                let end = start_offset
1446                    .checked_add(range_len)
1447                    .ok_or(Error::InvalidData("extent end overflow"))?;
1448                self.data.get(start_offset.try_into()?..end.try_into()?)
1449            },
1450            ExtentRange::ToEnd(_) => self.data.get(start_offset.try_into()?..),
1451        };
1452        let slice = slice.ok_or(Error::InvalidData("extent crosses box boundary"))?;
1453        buf.extend_from_slice(slice)?;
1454        Ok(())
1455    }
1456
1457}
1458
1459/// Used for 'infe' boxes within 'iinf' boxes
1460/// See ISO 14496-12:2015 § 8.11.6
1461/// Only versions {2, 3} are supported
1462#[derive(Debug)]
1463struct ItemInfoEntry {
1464    item_id: u32,
1465    item_type: FourCC,
1466}
1467
1468/// See ISO 14496-12:2015 § 8.11.12
1469#[derive(Debug)]
1470struct SingleItemTypeReferenceBox {
1471    item_type: FourCC,
1472    from_item_id: u32,
1473    to_item_id: u32,
1474    /// Index of this reference within the list of references of the same type from the same item
1475    /// (0-based). This is the dimgIdx for grid tiles.
1476    reference_index: u16,
1477}
1478
1479/// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box
1480/// See ISO 14496-12:2015 § 8.11.3
1481#[derive(Debug)]
1482enum IlocFieldSize {
1483    Zero,
1484    Four,
1485    Eight,
1486}
1487
1488impl IlocFieldSize {
1489    const fn to_bits(&self) -> u8 {
1490        match self {
1491            Self::Zero => 0,
1492            Self::Four => 32,
1493            Self::Eight => 64,
1494        }
1495    }
1496}
1497
1498impl TryFrom<u8> for IlocFieldSize {
1499    type Error = Error;
1500
1501    fn try_from(value: u8) -> Result<Self> {
1502        match value {
1503            0 => Ok(Self::Zero),
1504            4 => Ok(Self::Four),
1505            8 => Ok(Self::Eight),
1506            _ => Err(Error::InvalidData("value must be in the set {0, 4, 8}")),
1507        }
1508    }
1509}
1510
1511#[derive(PartialEq)]
1512enum IlocVersion {
1513    Zero,
1514    One,
1515    Two,
1516}
1517
1518impl TryFrom<u8> for IlocVersion {
1519    type Error = Error;
1520
1521    fn try_from(value: u8) -> Result<Self> {
1522        match value {
1523            0 => Ok(Self::Zero),
1524            1 => Ok(Self::One),
1525            2 => Ok(Self::Two),
1526            _ => Err(Error::Unsupported("unsupported version in 'iloc' box")),
1527        }
1528    }
1529}
1530
1531/// Used for 'iloc' boxes
1532/// See ISO 14496-12:2015 § 8.11.3
1533/// `base_offset` is omitted since it is integrated into the ranges in `extents`
1534/// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported
1535#[derive(Debug)]
1536struct ItemLocationBoxItem {
1537    item_id: u32,
1538    construction_method: ConstructionMethod,
1539    /// Unused for `ConstructionMethod::Idat`
1540    extents: TryVec<ItemLocationBoxExtent>,
1541}
1542
1543#[derive(Clone, Copy, Debug, PartialEq)]
1544enum ConstructionMethod {
1545    File,
1546    Idat,
1547    #[allow(dead_code)] // TODO: see https://github.com/mozilla/mp4parse-rust/issues/196
1548    Item,
1549}
1550
1551/// `extent_index` is omitted since it's only used for `ConstructionMethod::Item` which
1552/// is currently not implemented.
1553#[derive(Clone, Debug)]
1554struct ItemLocationBoxExtent {
1555    extent_range: ExtentRange,
1556}
1557
1558#[derive(Clone, Debug)]
1559enum ExtentRange {
1560    WithLength(Range<u64>),
1561    ToEnd(RangeFrom<u64>),
1562}
1563
1564impl ExtentRange {
1565    const fn start(&self) -> u64 {
1566        match self {
1567            Self::WithLength(r) => r.start,
1568            Self::ToEnd(r) => r.start,
1569        }
1570    }
1571}
1572
1573/// See ISO 14496-12:2015 § 4.2
1574struct BMFFBox<'a, T> {
1575    head: BoxHeader,
1576    content: Take<&'a mut T>,
1577}
1578
1579impl<T: Read> BMFFBox<'_, T> {
1580    fn read_into_try_vec(&mut self) -> std::io::Result<TryVec<u8>> {
1581        let limit = self.content.limit();
1582        // For size=0 boxes, size is set to u64::MAX, but after subtracting offset
1583        // (8 or 16 bytes), the limit will be slightly less. Check for values very
1584        // close to u64::MAX to detect these cases.
1585        let mut vec = if limit >= u64::MAX - BoxHeader::MIN_LARGE_SIZE {
1586            // Unknown size (size=0 box), read without pre-allocation
1587            std::vec::Vec::new()
1588        } else {
1589            let mut v = std::vec::Vec::new();
1590            v.try_reserve_exact(limit as usize)
1591                .map_err(|_| std::io::ErrorKind::OutOfMemory)?;
1592            v
1593        };
1594        self.content.read_to_end(&mut vec)?; // The default impl
1595        Ok(vec.into())
1596    }
1597}
1598
1599#[test]
1600fn box_read_to_end() {
1601    let tmp = &mut b"1234567890".as_slice();
1602    let mut src = BMFFBox {
1603        head: BoxHeader { name: BoxType::FileTypeBox, size: 5, offset: 0, uuid: None },
1604        content: <_ as Read>::take(tmp, 5),
1605    };
1606    let buf = src.read_into_try_vec().unwrap();
1607    assert_eq!(buf.len(), 5);
1608    assert_eq!(buf, b"12345".as_ref());
1609}
1610
1611#[test]
1612fn box_read_to_end_oom() {
1613    let tmp = &mut b"1234567890".as_slice();
1614    let mut src = BMFFBox {
1615        head: BoxHeader { name: BoxType::FileTypeBox, size: 5, offset: 0, uuid: None },
1616        // Use a very large value to trigger OOM, but not near u64::MAX (which indicates size=0 boxes)
1617        content: <_ as Read>::take(tmp, u64::MAX / 2),
1618    };
1619    assert!(src.read_into_try_vec().is_err());
1620}
1621
1622struct BoxIter<'a, T> {
1623    src: &'a mut T,
1624}
1625
1626impl<T: Read> BoxIter<'_, T> {
1627    fn new(src: &mut T) -> BoxIter<'_, T> {
1628        BoxIter { src }
1629    }
1630
1631    fn next_box(&mut self) -> Result<Option<BMFFBox<'_, T>>> {
1632        let r = read_box_header(self.src);
1633        match r {
1634            Ok(h) => Ok(Some(BMFFBox {
1635                head: h,
1636                content: self.src.take(h.size - h.offset),
1637            })),
1638            Err(Error::UnexpectedEOF) => Ok(None),
1639            Err(e) => Err(e),
1640        }
1641    }
1642}
1643
1644impl<T: Read> Read for BMFFBox<'_, T> {
1645    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1646        self.content.read(buf)
1647    }
1648}
1649
1650impl<T: Offset> Offset for BMFFBox<'_, T> {
1651    fn offset(&self) -> u64 {
1652        self.content.get_ref().offset()
1653    }
1654}
1655
1656impl<T: Read> BMFFBox<'_, T> {
1657    fn bytes_left(&self) -> u64 {
1658        self.content.limit()
1659    }
1660
1661    const fn get_header(&self) -> &BoxHeader {
1662        &self.head
1663    }
1664
1665    fn box_iter(&mut self) -> BoxIter<'_, Self> {
1666        BoxIter::new(self)
1667    }
1668}
1669
1670impl<T> Drop for BMFFBox<'_, T> {
1671    fn drop(&mut self) {
1672        if self.content.limit() > 0 {
1673            let name: FourCC = From::from(self.head.name);
1674            debug!("Dropping {} bytes in '{}'", self.content.limit(), name);
1675        }
1676    }
1677}
1678
1679/// Read and parse a box header.
1680///
1681/// Call this first to determine the type of a particular mp4 box
1682/// and its length. Used internally for dispatching to specific
1683/// parsers for the internal content, or to get the length to
1684/// skip unknown or uninteresting boxes.
1685///
1686/// See ISO 14496-12:2015 § 4.2
1687fn read_box_header<T: ReadBytesExt>(src: &mut T) -> Result<BoxHeader> {
1688    let size32 = be_u32(src)?;
1689    let name = BoxType::from(be_u32(src)?);
1690    let size = match size32 {
1691        // valid only for top-level box and indicates it's the last box in the file.  usually mdat.
1692        0 => {
1693            // Size=0 means box extends to EOF (ISOBMFF spec allows this for last box)
1694            u64::MAX
1695        },
1696        1 => {
1697            let size64 = be_u64(src)?;
1698            if size64 < BoxHeader::MIN_LARGE_SIZE {
1699                return Err(Error::InvalidData("malformed wide size"));
1700            }
1701            size64
1702        },
1703        _ => {
1704            if u64::from(size32) < BoxHeader::MIN_SIZE {
1705                return Err(Error::InvalidData("malformed size"));
1706            }
1707            u64::from(size32)
1708        },
1709    };
1710    let mut offset = match size32 {
1711        1 => BoxHeader::MIN_LARGE_SIZE,
1712        _ => BoxHeader::MIN_SIZE,
1713    };
1714    let uuid = if name == BoxType::UuidBox {
1715        if size >= offset + 16 {
1716            let mut buffer = [0u8; 16];
1717            let count = src.read(&mut buffer)?;
1718            offset += count.to_u64();
1719            if count == 16 {
1720                Some(buffer)
1721            } else {
1722                debug!("malformed uuid (short read), skipping");
1723                None
1724            }
1725        } else {
1726            debug!("malformed uuid, skipping");
1727            None
1728        }
1729    } else {
1730        None
1731    };
1732    assert!(offset <= size);
1733    Ok(BoxHeader { name, size, offset, uuid })
1734}
1735
1736/// Parse the extra header fields for a full box.
1737fn read_fullbox_extra<T: ReadBytesExt>(src: &mut T) -> Result<(u8, u32)> {
1738    let version = src.read_u8()?;
1739    let flags_a = src.read_u8()?;
1740    let flags_b = src.read_u8()?;
1741    let flags_c = src.read_u8()?;
1742    Ok((
1743        version,
1744        u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c),
1745    ))
1746}
1747
1748// Parse the extra fields for a full box whose flag fields must be zero.
1749fn read_fullbox_version_no_flags<T: ReadBytesExt>(src: &mut T, options: &ParseOptions) -> Result<u8> {
1750    let (version, flags) = read_fullbox_extra(src)?;
1751
1752    if flags != 0 && !options.lenient {
1753        return Err(Error::Unsupported("expected flags to be 0"));
1754    }
1755
1756    Ok(version)
1757}
1758
1759/// Skip over the entire contents of a box.
1760fn skip_box_content<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<()> {
1761    // Skip the contents of unknown chunks.
1762    let to_skip = {
1763        let header = src.get_header();
1764        debug!("{header:?} (skipped)");
1765        header
1766            .size
1767            .checked_sub(header.offset)
1768            .ok_or(Error::InvalidData("header offset > size"))?
1769    };
1770    assert_eq!(to_skip, src.bytes_left());
1771    skip(src, to_skip)
1772}
1773
1774/// Skip over the remain data of a box.
1775fn skip_box_remain<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<()> {
1776    let remain = {
1777        let header = src.get_header();
1778        let len = src.bytes_left();
1779        debug!("remain {len} (skipped) in {header:?}");
1780        len
1781    };
1782    skip(src, remain)
1783}
1784
1785struct ResourceTracker<'a> {
1786    config: &'a DecodeConfig,
1787    #[cfg(feature = "eager")]
1788    current_memory: u64,
1789    #[cfg(feature = "eager")]
1790    peak_memory: u64,
1791}
1792
1793impl<'a> ResourceTracker<'a> {
1794    fn new(config: &'a DecodeConfig) -> Self {
1795        Self {
1796            config,
1797            #[cfg(feature = "eager")]
1798            current_memory: 0,
1799            #[cfg(feature = "eager")]
1800            peak_memory: 0,
1801        }
1802    }
1803
1804    #[cfg(feature = "eager")]
1805    fn reserve(&mut self, bytes: u64) -> Result<()> {
1806        self.current_memory = self.current_memory.saturating_add(bytes);
1807        self.peak_memory = self.peak_memory.max(self.current_memory);
1808
1809        if let Some(limit) = self.config.peak_memory_limit {
1810            if self.peak_memory > limit {
1811                return Err(Error::ResourceLimitExceeded("peak memory limit exceeded"));
1812            }
1813        }
1814
1815        Ok(())
1816    }
1817
1818    #[cfg(feature = "eager")]
1819    fn release(&mut self, bytes: u64) {
1820        self.current_memory = self.current_memory.saturating_sub(bytes);
1821    }
1822
1823    #[cfg(feature = "eager")]
1824    fn validate_total_megapixels(&self, width: u32, height: u32) -> Result<()> {
1825        if let Some(limit) = self.config.total_megapixels_limit {
1826            let megapixels = (width as u64)
1827                .checked_mul(height as u64)
1828                .ok_or(Error::InvalidData("dimension overflow"))?
1829                / 1_000_000;
1830
1831            if megapixels > limit as u64 {
1832                return Err(Error::ResourceLimitExceeded("total megapixels limit exceeded"));
1833            }
1834        }
1835
1836        Ok(())
1837    }
1838
1839    fn validate_animation_frames(&self, count: u32) -> Result<()> {
1840        if let Some(limit) = self.config.max_animation_frames {
1841            if count > limit {
1842                return Err(Error::ResourceLimitExceeded("animation frame count limit exceeded"));
1843            }
1844        }
1845
1846        Ok(())
1847    }
1848
1849    fn validate_grid_tiles(&self, count: u32) -> Result<()> {
1850        if let Some(limit) = self.config.max_grid_tiles {
1851            if count > limit {
1852                return Err(Error::ResourceLimitExceeded("grid tile count limit exceeded"));
1853            }
1854        }
1855
1856        Ok(())
1857    }
1858}
1859
1860/// Read the contents of an AVIF file with resource limits and cancellation support
1861///
1862/// This is the primary parsing function with full control over resource limits
1863/// and cooperative cancellation via the [`Stop`] trait.
1864///
1865/// # Arguments
1866///
1867/// * `f` - Reader for the AVIF file
1868/// * `config` - Resource limits and parsing options
1869/// * `stop` - Cancellation token (use [`Unstoppable`] if not needed)
1870#[cfg(feature = "eager")]
1871#[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader_with_config()` instead")]
1872#[allow(deprecated)]
1873pub fn read_avif_with_config<T: Read>(
1874    f: &mut T,
1875    config: &DecodeConfig,
1876    stop: &dyn Stop,
1877) -> Result<AvifData> {
1878    let mut tracker = ResourceTracker::new(config);
1879    let mut f = OffsetReader::new(f);
1880
1881    let mut iter = BoxIter::new(&mut f);
1882
1883    // 'ftyp' box must occur first; see ISO 14496-12:2015 § 4.3.1
1884    if let Some(mut b) = iter.next_box()? {
1885        if b.head.name == BoxType::FileTypeBox {
1886            let ftyp = read_ftyp(&mut b)?;
1887            // Accept both 'avif' (single-frame) and 'avis' (animated) brands
1888            if ftyp.major_brand != b"avif" && ftyp.major_brand != b"avis" {
1889                warn!("major_brand: {}", ftyp.major_brand);
1890                return Err(Error::InvalidData("ftyp must be 'avif' or 'avis'"));
1891            }
1892            let _is_animated = ftyp.major_brand == b"avis";
1893        } else {
1894            return Err(Error::InvalidData("'ftyp' box must occur first"));
1895        }
1896    }
1897
1898    let mut meta = None;
1899    let mut mdats = TryVec::new();
1900    let mut animation_data: Option<(u32, SampleTable)> = None;
1901
1902    let parse_opts = ParseOptions { lenient: config.lenient };
1903
1904    while let Some(mut b) = iter.next_box()? {
1905        stop.check()?;
1906
1907        match b.head.name {
1908            BoxType::MetadataBox => {
1909                if meta.is_some() {
1910                    return Err(Error::InvalidData("There should be zero or one meta boxes per ISO 14496-12:2015 § 8.11.1.1"));
1911                }
1912                meta = Some(read_avif_meta(&mut b, &parse_opts)?);
1913            },
1914            BoxType::MovieBox => {
1915                animation_data = read_moov(&mut b)?;
1916            },
1917            BoxType::MediaDataBox => {
1918                if b.bytes_left() > 0 {
1919                    let offset = b.offset();
1920                    let size = b.bytes_left();
1921                    tracker.reserve(size)?;
1922                    let data = b.read_into_try_vec()?;
1923                    tracker.release(size);
1924                    mdats.push(MediaDataBox { offset, data })?;
1925                }
1926            },
1927            _ => skip_box_content(&mut b)?,
1928        }
1929
1930        check_parser_state(&b.head, &b.content)?;
1931    }
1932
1933    let meta = meta.ok_or(Error::InvalidData("missing meta"))?;
1934
1935    // Check if primary item is a grid (tiled image)
1936    let is_grid = meta
1937        .item_infos
1938        .iter()
1939        .find(|x| x.item_id == meta.primary_item_id)
1940        .map_or(false, |info| {
1941            let is_g = info.item_type == b"grid";
1942            if is_g {
1943                log::debug!("Grid image detected: primary_item_id={}", meta.primary_item_id);
1944            }
1945            is_g
1946        });
1947
1948    // Extract grid configuration if this is a grid image
1949    let mut grid_config = if is_grid {
1950        meta.properties
1951            .iter()
1952            .find(|prop| {
1953                prop.item_id == meta.primary_item_id
1954                    && matches!(prop.property, ItemProperty::ImageGrid(_))
1955            })
1956            .and_then(|prop| match &prop.property {
1957                ItemProperty::ImageGrid(config) => {
1958                    log::debug!("Grid: found explicit ImageGrid property: {:?}", config);
1959                    Some(config.clone())
1960                },
1961                _ => None,
1962            })
1963    } else {
1964        None
1965    };
1966
1967    // Find tile item IDs if this is a grid
1968    let tile_item_ids: TryVec<u32> = if is_grid {
1969        // Collect tiles with their reference index
1970        let mut tiles_with_index: TryVec<(u32, u16)> = TryVec::new();
1971        for iref in meta.item_references.iter() {
1972            // Grid items reference tiles via "dimg" (derived image) type
1973            if iref.from_item_id == meta.primary_item_id && iref.item_type == b"dimg" {
1974                tiles_with_index.push((iref.to_item_id, iref.reference_index))?;
1975            }
1976        }
1977
1978        // Validate tile count
1979        tracker.validate_grid_tiles(tiles_with_index.len() as u32)?;
1980
1981        // Sort tiles by reference_index to get correct grid order
1982        tiles_with_index.sort_by_key(|&(_, idx)| idx);
1983
1984        // Extract just the IDs in sorted order
1985        let mut ids = TryVec::new();
1986        for (tile_id, _) in tiles_with_index.iter() {
1987            ids.push(*tile_id)?;
1988        }
1989
1990        // No logging here - too verbose for production
1991
1992        // If no ImageGrid property found, calculate grid layout from ispe dimensions
1993        if grid_config.is_none() && !ids.is_empty() {
1994            // Try to calculate grid dimensions from ispe properties
1995            let grid_dims = meta.properties.iter()
1996                .find(|p| p.item_id == meta.primary_item_id)
1997                .and_then(|p| match &p.property {
1998                    ItemProperty::ImageSpatialExtents(e) => Some(e),
1999                    _ => None,
2000                });
2001
2002            let tile_dims = ids.first().and_then(|&tile_id| {
2003                meta.properties.iter()
2004                    .find(|p| p.item_id == tile_id)
2005                    .and_then(|p| match &p.property {
2006                        ItemProperty::ImageSpatialExtents(e) => Some(e),
2007                        _ => None,
2008                    })
2009            });
2010
2011            if let (Some(grid), Some(tile)) = (grid_dims, tile_dims) {
2012                // Validate grid output dimensions
2013                tracker.validate_total_megapixels(grid.width, grid.height)?;
2014
2015                // Validate tile dimensions are non-zero (already validated in read_ispe, but defensive)
2016                if tile.width == 0 || tile.height == 0 {
2017                    log::warn!("Grid: tile has zero dimensions, using fallback");
2018                } else if grid.width % tile.width == 0 && grid.height % tile.height == 0 {
2019                    // Calculate grid layout: grid_dims ÷ tile_dims
2020                    let columns = grid.width / tile.width;
2021                    let rows = grid.height / tile.height;
2022
2023                    // Validate grid dimensions fit in u8 (max 255×255 grid)
2024                    if columns > 255 || rows > 255 {
2025                        log::warn!("Grid: calculated dimensions {}×{} exceed 255, using fallback", rows, columns);
2026                    } else {
2027                        log::debug!("Grid: calculated {}×{} layout from ispe dimensions", rows, columns);
2028                        grid_config = Some(GridConfig {
2029                            rows: rows as u8,
2030                            columns: columns as u8,
2031                            output_width: grid.width,
2032                            output_height: grid.height,
2033                        });
2034                    }
2035                } else {
2036                    log::warn!("Grid: dimension mismatch - grid {}×{} not evenly divisible by tile {}×{}, using fallback",
2037                              grid.width, grid.height, tile.width, tile.height);
2038                }
2039            }
2040
2041            // Fallback: if calculation failed or ispe not available, use N×1 inference
2042            if grid_config.is_none() {
2043                log::debug!("Grid: using fallback {}×1 layout inference", ids.len());
2044                grid_config = Some(GridConfig {
2045                    rows: ids.len() as u8,  // Changed: vertical stack
2046                    columns: 1,              // Changed: single column
2047                    output_width: 0,  // Will be calculated from tiles
2048                    output_height: 0, // Will be calculated from tiles
2049                });
2050            }
2051        }
2052
2053        ids
2054    } else {
2055        TryVec::new()
2056    };
2057
2058    let alpha_item_id = meta
2059        .item_references
2060        .iter()
2061        // Auxiliary image for the primary image
2062        .filter(|iref| {
2063            iref.to_item_id == meta.primary_item_id
2064                && iref.from_item_id != meta.primary_item_id
2065                && iref.item_type == b"auxl"
2066        })
2067        .map(|iref| iref.from_item_id)
2068        // which has the alpha property
2069        .find(|&item_id| {
2070            meta.properties.iter().any(|prop| {
2071                prop.item_id == item_id
2072                    && match &prop.property {
2073                        ItemProperty::AuxiliaryType(urn) => {
2074                            urn.type_subtype().0 == b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"
2075                        }
2076                        _ => false,
2077                    }
2078            })
2079        });
2080
2081    let mut context = AvifData {
2082        premultiplied_alpha: alpha_item_id.map_or(false, |alpha_item_id| {
2083            meta.item_references.iter().any(|iref| {
2084                iref.from_item_id == meta.primary_item_id
2085                    && iref.to_item_id == alpha_item_id
2086                    && iref.item_type == b"prem"
2087            })
2088        }),
2089        ..Default::default()
2090    };
2091
2092    // Helper to extract item data from either mdat or idat
2093    let mut extract_item_data = |loc: &ItemLocationBoxItem, buf: &mut TryVec<u8>| -> Result<()> {
2094        match loc.construction_method {
2095            ConstructionMethod::File => {
2096                for extent in loc.extents.iter() {
2097                    let mut found = false;
2098                    for mdat in mdats.iter_mut() {
2099                        if mdat.matches_extent(&extent.extent_range) {
2100                            buf.append(&mut mdat.data)?;
2101                            found = true;
2102                            break;
2103                        } else if mdat.contains_extent(&extent.extent_range) {
2104                            mdat.read_extent(&extent.extent_range, buf)?;
2105                            found = true;
2106                            break;
2107                        }
2108                    }
2109                    if !found {
2110                        return Err(Error::InvalidData("iloc contains an extent that is not in mdat"));
2111                    }
2112                }
2113                Ok(())
2114            },
2115            ConstructionMethod::Idat => {
2116                let idat_data = meta.idat.as_ref().ok_or(Error::InvalidData("idat box missing but construction_method is Idat"))?;
2117                for extent in loc.extents.iter() {
2118                    match &extent.extent_range {
2119                        ExtentRange::WithLength(range) => {
2120                            let start = usize::try_from(range.start).map_err(|_| Error::InvalidData("extent start too large"))?;
2121                            let end = usize::try_from(range.end).map_err(|_| Error::InvalidData("extent end too large"))?;
2122                            if end > idat_data.len() {
2123                                return Err(Error::InvalidData("extent exceeds idat size"));
2124                            }
2125                            buf.extend_from_slice(&idat_data[start..end]).map_err(|_| Error::OutOfMemory)?;
2126                        },
2127                        ExtentRange::ToEnd(range) => {
2128                            let start = usize::try_from(range.start).map_err(|_| Error::InvalidData("extent start too large"))?;
2129                            if start >= idat_data.len() {
2130                                return Err(Error::InvalidData("extent start exceeds idat size"));
2131                            }
2132                            buf.extend_from_slice(&idat_data[start..]).map_err(|_| Error::OutOfMemory)?;
2133                        },
2134                    }
2135                }
2136                Ok(())
2137            },
2138            ConstructionMethod::Item => {
2139                Err(Error::Unsupported("construction_method 'item' not supported"))
2140            },
2141        }
2142    };
2143
2144    // load data of relevant items
2145    // For grid images, we need to load tiles in the order specified by iref
2146    if is_grid {
2147        // Extract each tile in order
2148        for (idx, &tile_id) in tile_item_ids.iter().enumerate() {
2149            if idx % 16 == 0 {
2150                stop.check()?;
2151            }
2152
2153            let mut tile_data = TryVec::new();
2154
2155            if let Some(loc) = meta.iloc_items.iter().find(|loc| loc.item_id == tile_id) {
2156                extract_item_data(loc, &mut tile_data)?;
2157            } else {
2158                return Err(Error::InvalidData("grid tile not found in iloc"));
2159            }
2160
2161            context.grid_tiles.push(tile_data)?;
2162        }
2163
2164        // Set grid_config in context
2165        context.grid_config = grid_config;
2166    } else {
2167        // Standard single-frame AVIF: load primary_item and optional alpha_item
2168        for loc in meta.iloc_items.iter() {
2169            let item_data = if loc.item_id == meta.primary_item_id {
2170                &mut context.primary_item
2171            } else if Some(loc.item_id) == alpha_item_id {
2172                context.alpha_item.get_or_insert_with(TryVec::new)
2173            } else {
2174                continue;
2175            };
2176
2177            extract_item_data(loc, item_data)?;
2178        }
2179    }
2180
2181    // Extract animation frames if this is an animated AVIF
2182    if let Some((media_timescale, sample_table)) = animation_data {
2183        let frame_count = sample_table.sample_sizes.len() as u32;
2184        tracker.validate_animation_frames(frame_count)?;
2185
2186        log::debug!("Animation: extracting frames (media_timescale={})", media_timescale);
2187        match extract_animation_frames(&sample_table, media_timescale, &mut mdats) {
2188            Ok(frames) => {
2189                if !frames.is_empty() {
2190                    log::debug!("Animation: extracted {} frames", frames.len());
2191                    context.animation = Some(AnimationConfig {
2192                        loop_count: 0, // TODO: parse from edit list or meta
2193                        frames,
2194                    });
2195                }
2196            }
2197            Err(e) => {
2198                log::warn!("Animation: failed to extract frames: {}", e);
2199            }
2200        }
2201    }
2202
2203    Ok(context)
2204}
2205
2206/// Read the contents of an AVIF file with custom parsing options
2207///
2208/// Uses unlimited resource limits for backwards compatibility.
2209///
2210/// # Arguments
2211///
2212/// * `f` - Reader for the AVIF file
2213/// * `options` - Parsing options (e.g., lenient mode)
2214#[cfg(feature = "eager")]
2215#[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader_with_config()` with `DecodeConfig::lenient()` instead")]
2216#[allow(deprecated)]
2217pub fn read_avif_with_options<T: Read>(f: &mut T, options: &ParseOptions) -> Result<AvifData> {
2218    let config = DecodeConfig::unlimited().lenient(options.lenient);
2219    read_avif_with_config(f, &config, &Unstoppable)
2220}
2221
2222/// Read the contents of an AVIF file
2223///
2224/// Metadata is accumulated and returned in [`AvifData`] struct.
2225/// Uses strict validation and unlimited resource limits by default.
2226///
2227/// For resource limits, use [`read_avif_with_config`].
2228/// For lenient parsing, use [`read_avif_with_options`].
2229#[cfg(feature = "eager")]
2230#[deprecated(since = "1.5.0", note = "Use `AvifParser::from_reader()` instead")]
2231#[allow(deprecated)]
2232pub fn read_avif<T: Read>(f: &mut T) -> Result<AvifData> {
2233    read_avif_with_options(f, &ParseOptions::default())
2234}
2235
2236/// Parse a metadata box in the context of an AVIF
2237/// Currently requires the primary item to be an av01 item type and generates
2238/// an error otherwise.
2239/// See ISO 14496-12:2015 § 8.11.1
2240fn read_avif_meta<T: Read + Offset>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<AvifInternalMeta> {
2241    let version = read_fullbox_version_no_flags(src, options)?;
2242
2243    if version != 0 {
2244        return Err(Error::Unsupported("unsupported meta version"));
2245    }
2246
2247    let mut primary_item_id = None;
2248    let mut item_infos = None;
2249    let mut iloc_items = None;
2250    let mut item_references = TryVec::new();
2251    let mut properties = TryVec::new();
2252    let mut idat = None;
2253
2254    let mut iter = src.box_iter();
2255    while let Some(mut b) = iter.next_box()? {
2256        match b.head.name {
2257            BoxType::ItemInfoBox => {
2258                if item_infos.is_some() {
2259                    return Err(Error::InvalidData("There should be zero or one iinf boxes per ISO 14496-12:2015 § 8.11.6.1"));
2260                }
2261                item_infos = Some(read_iinf(&mut b, options)?);
2262            },
2263            BoxType::ItemLocationBox => {
2264                if iloc_items.is_some() {
2265                    return Err(Error::InvalidData("There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.3.1"));
2266                }
2267                iloc_items = Some(read_iloc(&mut b, options)?);
2268            },
2269            BoxType::PrimaryItemBox => {
2270                if primary_item_id.is_some() {
2271                    return Err(Error::InvalidData("There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.4.1"));
2272                }
2273                primary_item_id = Some(read_pitm(&mut b, options)?);
2274            },
2275            BoxType::ImageReferenceBox => {
2276                item_references.append(&mut read_iref(&mut b, options)?)?;
2277            },
2278            BoxType::ImagePropertiesBox => {
2279                properties = read_iprp(&mut b, options)?;
2280            },
2281            BoxType::ItemDataBox => {
2282                if idat.is_some() {
2283                    return Err(Error::InvalidData("There should be zero or one idat boxes"));
2284                }
2285                idat = Some(b.read_into_try_vec()?);
2286            },
2287            _ => skip_box_content(&mut b)?,
2288        }
2289
2290        check_parser_state(&b.head, &b.content)?;
2291    }
2292
2293    let primary_item_id = primary_item_id.ok_or(Error::InvalidData("Required pitm box not present in meta box"))?;
2294
2295    let item_infos = item_infos.ok_or(Error::InvalidData("iinf missing"))?;
2296
2297    if let Some(item_info) = item_infos.iter().find(|x| x.item_id == primary_item_id) {
2298        // Allow both "av01" (standard single-frame) and "grid" (tiled) types
2299        if item_info.item_type != b"av01" && item_info.item_type != b"grid" {
2300            warn!("primary_item_id type: {}", item_info.item_type);
2301            return Err(Error::InvalidData("primary_item_id type is not av01 or grid"));
2302        }
2303    } else {
2304        return Err(Error::InvalidData("primary_item_id not present in iinf box"));
2305    }
2306
2307    Ok(AvifInternalMeta {
2308        properties,
2309        item_references,
2310        primary_item_id,
2311        iloc_items: iloc_items.ok_or(Error::InvalidData("iloc missing"))?,
2312        item_infos,
2313        idat,
2314    })
2315}
2316
2317/// Parse a Primary Item Box
2318/// See ISO 14496-12:2015 § 8.11.4
2319fn read_pitm<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<u32> {
2320    let version = read_fullbox_version_no_flags(src, options)?;
2321
2322    let item_id = match version {
2323        0 => be_u16(src)?.into(),
2324        1 => be_u32(src)?,
2325        _ => return Err(Error::Unsupported("unsupported pitm version")),
2326    };
2327
2328    Ok(item_id)
2329}
2330
2331/// Parse an Item Information Box
2332/// See ISO 14496-12:2015 § 8.11.6
2333fn read_iinf<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<ItemInfoEntry>> {
2334    let version = read_fullbox_version_no_flags(src, options)?;
2335
2336    match version {
2337        0 | 1 => (),
2338        _ => return Err(Error::Unsupported("unsupported iinf version")),
2339    }
2340
2341    let entry_count = if version == 0 {
2342        be_u16(src)?.to_usize()
2343    } else {
2344        be_u32(src)?.to_usize()
2345    };
2346    let mut item_infos = TryVec::with_capacity(entry_count)?;
2347
2348    let mut iter = src.box_iter();
2349    while let Some(mut b) = iter.next_box()? {
2350        if b.head.name != BoxType::ItemInfoEntry {
2351            return Err(Error::InvalidData("iinf box should contain only infe boxes"));
2352        }
2353
2354        item_infos.push(read_infe(&mut b)?)?;
2355
2356        check_parser_state(&b.head, &b.content)?;
2357    }
2358
2359    Ok(item_infos)
2360}
2361
2362/// Parse an Item Info Entry
2363/// See ISO 14496-12:2015 § 8.11.6.2
2364fn read_infe<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<ItemInfoEntry> {
2365    // According to the standard, it seems the flags field should be 0, but
2366    // at least one sample AVIF image has a nonzero value.
2367    let (version, _) = read_fullbox_extra(src)?;
2368
2369    // mif1 brand (see ISO 23008-12:2017 § 10.2.1) only requires v2 and 3
2370    let item_id = match version {
2371        2 => be_u16(src)?.into(),
2372        3 => be_u32(src)?,
2373        _ => return Err(Error::Unsupported("unsupported version in 'infe' box")),
2374    };
2375
2376    let item_protection_index = be_u16(src)?;
2377
2378    if item_protection_index != 0 {
2379        return Err(Error::Unsupported("protected items (infe.item_protection_index != 0) are not supported"));
2380    }
2381
2382    let item_type = FourCC::from(be_u32(src)?);
2383    debug!("infe item_id {item_id} item_type: {item_type}");
2384
2385    // There are some additional fields here, but they're not of interest to us
2386    skip_box_remain(src)?;
2387
2388    Ok(ItemInfoEntry { item_id, item_type })
2389}
2390
2391fn read_iref<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<SingleItemTypeReferenceBox>> {
2392    let mut item_references = TryVec::new();
2393    let version = read_fullbox_version_no_flags(src, options)?;
2394    if version > 1 {
2395        return Err(Error::Unsupported("iref version"));
2396    }
2397
2398    let mut iter = src.box_iter();
2399    while let Some(mut b) = iter.next_box()? {
2400        let from_item_id = if version == 0 {
2401            be_u16(&mut b)?.into()
2402        } else {
2403            be_u32(&mut b)?
2404        };
2405        let reference_count = be_u16(&mut b)?;
2406        for reference_index in 0..reference_count {
2407            let to_item_id = if version == 0 {
2408                be_u16(&mut b)?.into()
2409            } else {
2410                be_u32(&mut b)?
2411            };
2412            if from_item_id == to_item_id {
2413                return Err(Error::InvalidData("from_item_id and to_item_id must be different"));
2414            }
2415            item_references.push(SingleItemTypeReferenceBox {
2416                item_type: b.head.name.into(),
2417                from_item_id,
2418                to_item_id,
2419                reference_index,
2420            })?;
2421        }
2422        check_parser_state(&b.head, &b.content)?;
2423    }
2424    Ok(item_references)
2425}
2426
2427fn read_iprp<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<AssociatedProperty>> {
2428    let mut iter = src.box_iter();
2429    let mut properties = TryVec::new();
2430    let mut associations = TryVec::new();
2431
2432    while let Some(mut b) = iter.next_box()? {
2433        match b.head.name {
2434            BoxType::ItemPropertyContainerBox => {
2435                properties = read_ipco(&mut b, options)?;
2436            },
2437            BoxType::ItemPropertyAssociationBox => {
2438                associations = read_ipma(&mut b)?;
2439            },
2440            _ => return Err(Error::InvalidData("unexpected ipco child")),
2441        }
2442    }
2443
2444    let mut associated = TryVec::new();
2445    for a in associations {
2446        let index = match a.property_index {
2447            0 => continue,
2448            x => x as usize - 1,
2449        };
2450        if let Some(prop) = properties.get(index) {
2451            if *prop != ItemProperty::Unsupported {
2452                associated.push(AssociatedProperty {
2453                    item_id: a.item_id,
2454                    property: prop.try_clone()?,
2455                })?;
2456            }
2457        }
2458    }
2459    Ok(associated)
2460}
2461
2462/// Image spatial extents (dimensions)
2463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2464pub(crate) struct ImageSpatialExtents {
2465    pub(crate) width: u32,
2466    pub(crate) height: u32,
2467}
2468
2469#[derive(Debug, PartialEq)]
2470pub(crate) enum ItemProperty {
2471    Channels(ArrayVec<u8, 16>),
2472    AuxiliaryType(AuxiliaryTypeProperty),
2473    ImageSpatialExtents(ImageSpatialExtents),
2474    ImageGrid(GridConfig),
2475    Unsupported,
2476}
2477
2478impl TryClone for ItemProperty {
2479    fn try_clone(&self) -> Result<Self, TryReserveError> {
2480        Ok(match self {
2481            Self::Channels(val) => Self::Channels(val.clone()),
2482            Self::AuxiliaryType(val) => Self::AuxiliaryType(val.try_clone()?),
2483            Self::ImageSpatialExtents(val) => Self::ImageSpatialExtents(*val),
2484            Self::ImageGrid(val) => Self::ImageGrid(val.clone()),
2485            Self::Unsupported => Self::Unsupported,
2486        })
2487    }
2488}
2489
2490struct Association {
2491    item_id: u32,
2492    #[allow(unused)]
2493    essential: bool,
2494    property_index: u16,
2495}
2496
2497pub(crate) struct AssociatedProperty {
2498    pub item_id: u32,
2499    pub property: ItemProperty,
2500}
2501
2502fn read_ipma<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<Association>> {
2503    let (version, flags) = read_fullbox_extra(src)?;
2504
2505    let mut associations = TryVec::new();
2506
2507    let entry_count = be_u32(src)?;
2508    for _ in 0..entry_count {
2509        let item_id = if version == 0 {
2510            be_u16(src)?.into()
2511        } else {
2512            be_u32(src)?
2513        };
2514        let association_count = src.read_u8()?;
2515        for _ in 0..association_count {
2516            let num_association_bytes = if flags & 1 == 1 { 2 } else { 1 };
2517            let association = &mut [0; 2][..num_association_bytes];
2518            src.read_exact(association)?;
2519            let mut association = BitReader::new(association);
2520            let essential = association.read_bool()?;
2521            let property_index = association.read_u16(association.remaining().try_into()?)?;
2522            associations.push(Association {
2523                item_id,
2524                essential,
2525                property_index,
2526            })?;
2527        }
2528    }
2529    Ok(associations)
2530}
2531
2532fn read_ipco<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<ItemProperty>> {
2533    let mut properties = TryVec::new();
2534
2535    let mut iter = src.box_iter();
2536    while let Some(mut b) = iter.next_box()? {
2537        // Must push for every property to have correct index for them
2538        let prop = match b.head.name {
2539            BoxType::PixelInformationBox => ItemProperty::Channels(read_pixi(&mut b, options)?),
2540            BoxType::AuxiliaryTypeProperty => ItemProperty::AuxiliaryType(read_auxc(&mut b, options)?),
2541            BoxType::ImageSpatialExtentsBox => ItemProperty::ImageSpatialExtents(read_ispe(&mut b, options)?),
2542            BoxType::ImageGridBox => ItemProperty::ImageGrid(read_grid(&mut b, options)?),
2543            _ => {
2544                skip_box_remain(&mut b)?;
2545                ItemProperty::Unsupported
2546            },
2547        };
2548        properties.push(prop)?;
2549    }
2550    Ok(properties)
2551}
2552
2553fn read_pixi<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<ArrayVec<u8, 16>> {
2554    let version = read_fullbox_version_no_flags(src, options)?;
2555    if version != 0 {
2556        return Err(Error::Unsupported("pixi version"));
2557    }
2558
2559    let num_channels = usize::from(src.read_u8()?);
2560    let mut channels = ArrayVec::new();
2561    channels.extend((0..num_channels.min(channels.capacity())).map(|_| 0));
2562    debug_assert_eq!(num_channels, channels.len());
2563    src.read_exact(&mut channels).map_err(|_| Error::InvalidData("invalid num_channels"))?;
2564
2565    // In lenient mode, skip any extra bytes (e.g., extended_pixi.avif has 6 extra bytes)
2566    if options.lenient && src.bytes_left() > 0 {
2567        skip(src, src.bytes_left())?;
2568    }
2569
2570    check_parser_state(&src.head, &src.content)?;
2571    Ok(channels)
2572}
2573
2574#[derive(Debug, PartialEq)]
2575struct AuxiliaryTypeProperty {
2576    aux_data: TryString,
2577}
2578
2579impl AuxiliaryTypeProperty {
2580    #[must_use]
2581    fn type_subtype(&self) -> (&[u8], &[u8]) {
2582        let split = self.aux_data.iter().position(|&b| b == b'\0')
2583            .map(|pos| self.aux_data.split_at(pos));
2584        if let Some((aux_type, rest)) = split {
2585            (aux_type, &rest[1..])
2586        } else {
2587            (&self.aux_data, &[])
2588        }
2589    }
2590}
2591
2592impl TryClone for AuxiliaryTypeProperty {
2593    fn try_clone(&self) -> Result<Self, TryReserveError> {
2594        Ok(Self {
2595            aux_data: self.aux_data.try_clone()?,
2596        })
2597    }
2598}
2599
2600fn read_auxc<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<AuxiliaryTypeProperty> {
2601    let version = read_fullbox_version_no_flags(src, options)?;
2602    if version != 0 {
2603        return Err(Error::Unsupported("auxC version"));
2604    }
2605
2606    let aux_data = src.read_into_try_vec()?;
2607
2608    Ok(AuxiliaryTypeProperty { aux_data })
2609}
2610
2611/// Parse an Image Spatial Extents property box
2612/// See ISO/IEC 23008-12:2017 § 6.5.3
2613fn read_ispe<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<ImageSpatialExtents> {
2614    let _version = read_fullbox_version_no_flags(src, options)?;
2615    // Version is always 0 for ispe
2616
2617    let width = be_u32(src)?;
2618    let height = be_u32(src)?;
2619
2620    // Validate dimensions are non-zero (0×0 images are invalid)
2621    if width == 0 || height == 0 {
2622        return Err(Error::InvalidData("ispe dimensions cannot be zero"));
2623    }
2624
2625    Ok(ImageSpatialExtents { width, height })
2626}
2627
2628/// Parse a Movie Header box (mvhd)
2629/// See ISO/IEC 14496-12:2015 § 8.2.2
2630fn read_mvhd<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<MovieHeader> {
2631    let version = src.read_u8()?;
2632    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
2633
2634    let (timescale, duration) = if version == 1 {
2635        let _creation_time = be_u64(src)?;
2636        let _modification_time = be_u64(src)?;
2637        let timescale = be_u32(src)?;
2638        let duration = be_u64(src)?;
2639        (timescale, duration)
2640    } else {
2641        let _creation_time = be_u32(src)?;
2642        let _modification_time = be_u32(src)?;
2643        let timescale = be_u32(src)?;
2644        let duration = be_u32(src)?;
2645        (timescale, duration as u64)
2646    };
2647
2648    // Skip rest of mvhd (rate, volume, matrix, etc.)
2649    skip_box_remain(src)?;
2650
2651    Ok(MovieHeader { _timescale: timescale, _duration: duration })
2652}
2653
2654/// Parse a Media Header box (mdhd)
2655/// See ISO/IEC 14496-12:2015 § 8.4.2
2656fn read_mdhd<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<MediaHeader> {
2657    let version = src.read_u8()?;
2658    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
2659
2660    let (timescale, duration) = if version == 1 {
2661        let _creation_time = be_u64(src)?;
2662        let _modification_time = be_u64(src)?;
2663        let timescale = be_u32(src)?;
2664        let duration = be_u64(src)?;
2665        (timescale, duration)
2666    } else {
2667        let _creation_time = be_u32(src)?;
2668        let _modification_time = be_u32(src)?;
2669        let timescale = be_u32(src)?;
2670        let duration = be_u32(src)?;
2671        (timescale, duration as u64)
2672    };
2673
2674    // Skip language and pre_defined
2675    skip_box_remain(src)?;
2676
2677    Ok(MediaHeader { timescale, _duration: duration })
2678}
2679
2680/// Parse Time To Sample box (stts)
2681/// See ISO/IEC 14496-12:2015 § 8.6.1.2
2682fn read_stts<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<TimeToSampleEntry>> {
2683    let _version = src.read_u8()?;
2684    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
2685    let entry_count = be_u32(src)?;
2686
2687    let mut entries = TryVec::new();
2688    for _ in 0..entry_count {
2689        entries.push(TimeToSampleEntry {
2690            sample_count: be_u32(src)?,
2691            sample_delta: be_u32(src)?,
2692        })?;
2693    }
2694
2695    Ok(entries)
2696}
2697
2698/// Parse Sample To Chunk box (stsc)
2699/// See ISO/IEC 14496-12:2015 § 8.7.4
2700fn read_stsc<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<SampleToChunkEntry>> {
2701    let _version = src.read_u8()?;
2702    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
2703    let entry_count = be_u32(src)?;
2704
2705    let mut entries = TryVec::new();
2706    for _ in 0..entry_count {
2707        entries.push(SampleToChunkEntry {
2708            first_chunk: be_u32(src)?,
2709            samples_per_chunk: be_u32(src)?,
2710            _sample_description_index: be_u32(src)?,
2711        })?;
2712    }
2713
2714    Ok(entries)
2715}
2716
2717/// Parse Sample Size box (stsz)
2718/// See ISO/IEC 14496-12:2015 § 8.7.3
2719fn read_stsz<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<TryVec<u32>> {
2720    let _version = src.read_u8()?;
2721    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
2722    let sample_size = be_u32(src)?;
2723    let sample_count = be_u32(src)?;
2724
2725    let mut sizes = TryVec::new();
2726    if sample_size == 0 {
2727        // Variable sizes - read each one
2728        for _ in 0..sample_count {
2729            sizes.push(be_u32(src)?)?;
2730        }
2731    } else {
2732        // Constant size for all samples
2733        for _ in 0..sample_count {
2734            sizes.push(sample_size)?;
2735        }
2736    }
2737
2738    Ok(sizes)
2739}
2740
2741/// Parse Chunk Offset box (stco or co64)
2742/// See ISO/IEC 14496-12:2015 § 8.7.5
2743fn read_chunk_offsets<T: Read>(src: &mut BMFFBox<'_, T>, is_64bit: bool) -> Result<TryVec<u64>> {
2744    let _version = src.read_u8()?;
2745    let _flags = [src.read_u8()?, src.read_u8()?, src.read_u8()?];
2746    let entry_count = be_u32(src)?;
2747
2748    let mut offsets = TryVec::new();
2749    for _ in 0..entry_count {
2750        let offset = if is_64bit {
2751            be_u64(src)?
2752        } else {
2753            be_u32(src)? as u64
2754        };
2755        offsets.push(offset)?;
2756    }
2757
2758    Ok(offsets)
2759}
2760
2761/// Parse Sample Table box (stbl)
2762/// See ISO/IEC 14496-12:2015 § 8.5
2763fn read_stbl<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<SampleTable> {
2764    let mut time_to_sample = TryVec::new();
2765    let mut sample_to_chunk = TryVec::new();
2766    let mut sample_sizes = TryVec::new();
2767    let mut chunk_offsets = TryVec::new();
2768
2769    let mut iter = src.box_iter();
2770    while let Some(mut b) = iter.next_box()? {
2771        match b.head.name {
2772            BoxType::TimeToSampleBox => {
2773                time_to_sample = read_stts(&mut b)?;
2774            }
2775            BoxType::SampleToChunkBox => {
2776                sample_to_chunk = read_stsc(&mut b)?;
2777            }
2778            BoxType::SampleSizeBox => {
2779                sample_sizes = read_stsz(&mut b)?;
2780            }
2781            BoxType::ChunkOffsetBox => {
2782                chunk_offsets = read_chunk_offsets(&mut b, false)?;
2783            }
2784            BoxType::ChunkLargeOffsetBox => {
2785                chunk_offsets = read_chunk_offsets(&mut b, true)?;
2786            }
2787            _ => {
2788                skip_box_remain(&mut b)?;
2789            }
2790        }
2791    }
2792
2793    Ok(SampleTable {
2794        time_to_sample,
2795        sample_to_chunk,
2796        sample_sizes,
2797        chunk_offsets,
2798    })
2799}
2800
2801/// Parse animation from moov box
2802/// Returns (media_timescale, sample_table)
2803fn read_moov<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<(u32, SampleTable)>> {
2804    let mut media_timescale: Option<u32> = None;
2805    let mut sample_table: Option<SampleTable> = None;
2806
2807    let mut iter = src.box_iter();
2808    while let Some(mut b) = iter.next_box()? {
2809        match b.head.name {
2810            BoxType::MovieHeaderBox => {
2811                let _mvhd = read_mvhd(&mut b)?;
2812            }
2813            BoxType::TrackBox => {
2814                // Parse track recursively
2815                // Only use first video track, but consume all tracks
2816                if media_timescale.is_none() {
2817                    if let Some((timescale, stbl)) = read_trak(&mut b)? {
2818                        media_timescale = Some(timescale);
2819                        sample_table = Some(stbl);
2820                    }
2821                } else {
2822                    skip_box_remain(&mut b)?;
2823                }
2824            }
2825            _ => {
2826                skip_box_remain(&mut b)?;
2827            }
2828        }
2829    }
2830
2831    if let (Some(timescale), Some(stbl)) = (media_timescale, sample_table) {
2832        Ok(Some((timescale, stbl)))
2833    } else {
2834        Ok(None)
2835    }
2836}
2837
2838/// Parse track box (trak)
2839/// Returns (media_timescale, sample_table) if this is a valid video track
2840fn read_trak<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<(u32, SampleTable)>> {
2841    let mut iter = src.box_iter();
2842    while let Some(mut b) = iter.next_box()? {
2843        if b.head.name == BoxType::MediaBox {
2844            return read_mdia(&mut b);
2845        } else {
2846            skip_box_remain(&mut b)?;
2847        }
2848    }
2849    Ok(None)
2850}
2851
2852/// Parse media box (mdia)
2853fn read_mdia<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<(u32, SampleTable)>> {
2854    let mut media_timescale = 1000; // default
2855    let mut sample_table: Option<SampleTable> = None;
2856
2857    let mut iter = src.box_iter();
2858    while let Some(mut b) = iter.next_box()? {
2859        match b.head.name {
2860            BoxType::MediaHeaderBox => {
2861                let mdhd = read_mdhd(&mut b)?;
2862                media_timescale = mdhd.timescale;
2863            }
2864            BoxType::MediaInformationBox => {
2865                sample_table = read_minf(&mut b)?;
2866            }
2867            _ => {
2868                skip_box_remain(&mut b)?;
2869            }
2870        }
2871    }
2872
2873    if let Some(stbl) = sample_table {
2874        Ok(Some((media_timescale, stbl)))
2875    } else {
2876        Ok(None)
2877    }
2878}
2879
2880/// Parse media information box (minf)
2881fn read_minf<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<Option<SampleTable>> {
2882    let mut iter = src.box_iter();
2883    while let Some(mut b) = iter.next_box()? {
2884        if b.head.name == BoxType::SampleTableBox {
2885            return Ok(Some(read_stbl(&mut b)?));
2886        } else {
2887            skip_box_remain(&mut b)?;
2888        }
2889    }
2890    Ok(None)
2891}
2892
2893/// Extract animation frames using sample table
2894#[cfg(feature = "eager")]
2895#[allow(deprecated)]
2896fn extract_animation_frames(
2897    sample_table: &SampleTable,
2898    media_timescale: u32,
2899    mdats: &mut [MediaDataBox],
2900) -> Result<TryVec<AnimationFrame>> {
2901    let mut frames = TryVec::new();
2902
2903    // Build sample-to-chunk mapping (expand into per-sample info)
2904    let mut sample_to_chunk_map = TryVec::new();
2905    for (i, entry) in sample_table.sample_to_chunk.iter().enumerate() {
2906        let next_first_chunk = sample_table
2907            .sample_to_chunk
2908            .get(i + 1)
2909            .map(|e| e.first_chunk)
2910            .unwrap_or(u32::MAX);
2911
2912        for chunk_idx in entry.first_chunk..next_first_chunk {
2913            if chunk_idx > sample_table.chunk_offsets.len() as u32 {
2914                break;
2915            }
2916            sample_to_chunk_map.push((chunk_idx, entry.samples_per_chunk))?;
2917        }
2918    }
2919
2920    // Calculate frame durations from time-to-sample
2921    let mut frame_durations = TryVec::new();
2922    for entry in &sample_table.time_to_sample {
2923        for _ in 0..entry.sample_count {
2924            // Convert from media timescale to milliseconds
2925            let duration_ms = if media_timescale > 0 {
2926                ((entry.sample_delta as u64) * 1000) / (media_timescale as u64)
2927            } else {
2928                0
2929            };
2930            frame_durations.push(duration_ms as u32)?;
2931        }
2932    }
2933
2934    // Extract each frame
2935    let sample_count = sample_table.sample_sizes.len();
2936    let mut current_sample = 0;
2937
2938    for (chunk_idx_1based, samples_in_chunk) in &sample_to_chunk_map {
2939        let chunk_idx = (*chunk_idx_1based as usize).saturating_sub(1);
2940        if chunk_idx >= sample_table.chunk_offsets.len() {
2941            continue;
2942        }
2943
2944        let chunk_offset = sample_table.chunk_offsets[chunk_idx];
2945
2946        for sample_in_chunk in 0..*samples_in_chunk {
2947            if current_sample >= sample_count {
2948                break;
2949            }
2950
2951            let sample_size = sample_table.sample_sizes[current_sample];
2952            let duration_ms = frame_durations.get(current_sample).copied().unwrap_or(0);
2953
2954            // Calculate offset within chunk
2955            let mut offset_in_chunk = 0u64;
2956            for s in 0..sample_in_chunk {
2957                let prev_sample = current_sample.saturating_sub((sample_in_chunk - s) as usize);
2958                if prev_sample < sample_count {
2959                    offset_in_chunk += sample_table.sample_sizes[prev_sample] as u64;
2960                }
2961            }
2962
2963            let sample_offset = chunk_offset + offset_in_chunk;
2964
2965            // Extract frame data from mdat
2966            let mut frame_data = TryVec::new();
2967            let mut found = false;
2968
2969            for mdat in mdats.iter_mut() {
2970                let range = ExtentRange::WithLength(Range {
2971                    start: sample_offset,
2972                    end: sample_offset + sample_size as u64,
2973                });
2974
2975                if mdat.contains_extent(&range) {
2976                    mdat.read_extent(&range, &mut frame_data)?;
2977                    found = true;
2978                    break;
2979                }
2980            }
2981
2982            if !found {
2983                log::warn!("Animation frame {} not found in mdat", current_sample);
2984            }
2985
2986            frames.push(AnimationFrame {
2987                data: frame_data,
2988                duration_ms,
2989            })?;
2990
2991            current_sample += 1;
2992        }
2993    }
2994
2995    Ok(frames)
2996}
2997
2998/// Parse an ImageGrid property box
2999/// See ISO/IEC 23008-12:2017 § 6.6.2.3
3000fn read_grid<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<GridConfig> {
3001    let version = read_fullbox_version_no_flags(src, options)?;
3002    if version > 0 {
3003        return Err(Error::Unsupported("grid version > 0"));
3004    }
3005
3006    let flags_byte = src.read_u8()?;
3007    let rows = src.read_u8()?;
3008    let columns = src.read_u8()?;
3009
3010    // flags & 1 determines field size: 0 = 16-bit, 1 = 32-bit
3011    let (output_width, output_height) = if flags_byte & 1 == 0 {
3012        // 16-bit fields
3013        (u32::from(be_u16(src)?), u32::from(be_u16(src)?))
3014    } else {
3015        // 32-bit fields
3016        (be_u32(src)?, be_u32(src)?)
3017    };
3018
3019    Ok(GridConfig {
3020        rows,
3021        columns,
3022        output_width,
3023        output_height,
3024    })
3025}
3026
3027/// Parse an item location box inside a meta box
3028/// See ISO 14496-12:2015 § 8.11.3
3029fn read_iloc<T: Read>(src: &mut BMFFBox<'_, T>, options: &ParseOptions) -> Result<TryVec<ItemLocationBoxItem>> {
3030    let version: IlocVersion = read_fullbox_version_no_flags(src, options)?.try_into()?;
3031
3032    let iloc = src.read_into_try_vec()?;
3033    let mut iloc = BitReader::new(&iloc);
3034
3035    let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
3036    let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
3037    let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
3038
3039    let index_size: Option<IlocFieldSize> = match version {
3040        IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?),
3041        IlocVersion::Zero => {
3042            let _reserved = iloc.read_u8(4)?;
3043            None
3044        },
3045    };
3046
3047    let item_count = match version {
3048        IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?,
3049        IlocVersion::Two => iloc.read_u32(32)?,
3050    };
3051
3052    let mut items = TryVec::with_capacity(item_count.to_usize())?;
3053
3054    for _ in 0..item_count {
3055        let item_id = match version {
3056            IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?,
3057            IlocVersion::Two => iloc.read_u32(32)?,
3058        };
3059
3060        // The spec isn't entirely clear how an `iloc` should be interpreted for version 0,
3061        // which has no `construction_method` field. It does say:
3062        // "For maximum compatibility, version 0 of this box should be used in preference to
3063        //  version 1 with `construction_method==0`, or version 2 when possible."
3064        // We take this to imply version 0 can be interpreted as using file offsets.
3065        let construction_method = match version {
3066            IlocVersion::Zero => ConstructionMethod::File,
3067            IlocVersion::One | IlocVersion::Two => {
3068                let _reserved = iloc.read_u16(12)?;
3069                match iloc.read_u16(4)? {
3070                    0 => ConstructionMethod::File,
3071                    1 => ConstructionMethod::Idat,
3072                    2 => return Err(Error::Unsupported("construction_method 'item_offset' is not supported")),
3073                    _ => return Err(Error::InvalidData("construction_method is taken from the set 0, 1 or 2 per ISO 14496-12:2015 § 8.11.3.3")),
3074                }
3075            },
3076        };
3077
3078        let data_reference_index = iloc.read_u16(16)?;
3079
3080        if data_reference_index != 0 {
3081            return Err(Error::Unsupported("external file references (iloc.data_reference_index != 0) are not supported"));
3082        }
3083
3084        let base_offset = iloc.read_u64(base_offset_size.to_bits())?;
3085        let extent_count = iloc.read_u16(16)?;
3086
3087        if extent_count < 1 {
3088            return Err(Error::InvalidData("extent_count must have a value 1 or greater per ISO 14496-12:2015 § 8.11.3.3"));
3089        }
3090
3091        let mut extents = TryVec::with_capacity(extent_count.to_usize())?;
3092
3093        for _ in 0..extent_count {
3094            // Parsed but currently ignored, see `ItemLocationBoxExtent`
3095            let _extent_index = match &index_size {
3096                None | Some(IlocFieldSize::Zero) => None,
3097                Some(index_size) => {
3098                    debug_assert!(version == IlocVersion::One || version == IlocVersion::Two);
3099                    Some(iloc.read_u64(index_size.to_bits())?)
3100                },
3101            };
3102
3103            // Per ISO 14496-12:2015 § 8.11.3.1:
3104            // "If the offset is not identified (the field has a length of zero), then the
3105            //  beginning of the source (offset 0) is implied"
3106            // This behavior will follow from BitReader::read_u64(0) -> 0.
3107            let extent_offset = iloc.read_u64(offset_size.to_bits())?;
3108            let extent_length = iloc.read_u64(length_size.to_bits())?;
3109
3110            // "If the length is not specified, or specified as zero, then the entire length of
3111            //  the source is implied" (ibid)
3112            let start = base_offset
3113                .checked_add(extent_offset)
3114                .ok_or(Error::InvalidData("offset calculation overflow"))?;
3115            let extent_range = if extent_length == 0 {
3116                ExtentRange::ToEnd(RangeFrom { start })
3117            } else {
3118                let end = start
3119                    .checked_add(extent_length)
3120                    .ok_or(Error::InvalidData("end calculation overflow"))?;
3121                ExtentRange::WithLength(Range { start, end })
3122            };
3123
3124            extents.push(ItemLocationBoxExtent { extent_range })?;
3125        }
3126
3127        items.push(ItemLocationBoxItem { item_id, construction_method, extents })?;
3128    }
3129
3130    if iloc.remaining() == 0 {
3131        Ok(items)
3132    } else {
3133        Err(Error::InvalidData("invalid iloc size"))
3134    }
3135}
3136
3137/// Parse an ftyp box.
3138/// See ISO 14496-12:2015 § 4.3
3139fn read_ftyp<T: Read>(src: &mut BMFFBox<'_, T>) -> Result<FileTypeBox> {
3140    let major = be_u32(src)?;
3141    let minor = be_u32(src)?;
3142    let bytes_left = src.bytes_left();
3143    if bytes_left % 4 != 0 {
3144        return Err(Error::InvalidData("invalid ftyp size"));
3145    }
3146    // Is a brand_count of zero valid?
3147    let brand_count = bytes_left / 4;
3148    let mut brands = TryVec::with_capacity(brand_count.try_into()?)?;
3149    for _ in 0..brand_count {
3150        brands.push(be_u32(src)?.into())?;
3151    }
3152    Ok(FileTypeBox {
3153        major_brand: From::from(major),
3154        minor_version: minor,
3155        compatible_brands: brands,
3156    })
3157}
3158
3159#[cfg_attr(debug_assertions, track_caller)]
3160fn check_parser_state<T>(header: &BoxHeader, left: &Take<T>) -> Result<(), Error> {
3161    let limit = left.limit();
3162    // Allow fully consumed boxes, or size=0 boxes (where original size was u64::MAX)
3163    if limit == 0 || header.size == u64::MAX {
3164        Ok(())
3165    } else {
3166        debug_assert_eq!(0, limit, "bad parser state bytes left");
3167        Err(Error::InvalidData("unread box content or bad parser sync"))
3168    }
3169}
3170
3171/// Skip a number of bytes that we don't care to parse.
3172fn skip<T: Read>(src: &mut T, bytes: u64) -> Result<()> {
3173    std::io::copy(&mut src.take(bytes), &mut std::io::sink())?;
3174    Ok(())
3175}
3176
3177fn be_u16<T: ReadBytesExt>(src: &mut T) -> Result<u16> {
3178    src.read_u16::<byteorder::BigEndian>().map_err(From::from)
3179}
3180
3181fn be_u32<T: ReadBytesExt>(src: &mut T) -> Result<u32> {
3182    src.read_u32::<byteorder::BigEndian>().map_err(From::from)
3183}
3184
3185fn be_u64<T: ReadBytesExt>(src: &mut T) -> Result<u64> {
3186    src.read_u64::<byteorder::BigEndian>().map_err(From::from)
3187}