Skip to main content

zenavif_parse/
lib.rs

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