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