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