From ca7a6609bb31fe58d0fed5ff855a840bced108d4 Mon Sep 17 00:00:00 2001 From: "L. E. Segovia" Date: Thu, 9 Apr 2026 16:17:48 -0300 Subject: [PATCH 1/2] Add safe wrappers for AVIF metadata boxes --- mp4parse/src/lib.rs | 94 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 58bdcc36..55f99731 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -1640,6 +1640,37 @@ impl AvifContext { } } + pub fn av1_config(&self) -> Result<&AV1ConfigBox> { + if let Some(primary_item) = &self.primary_item { + match self + .item_properties + .get(primary_item.id, BoxType::AV1CodecConfigurationBox)? + { + Some(ItemProperty::AV1Config(av1c)) => Ok(av1c), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Err(Error::from(Status::Av1cMissing)), + } + } else { + Err(Error::from(Status::PitmMissing)) + } + } + + + pub fn spatial_extents(&self) -> Result<&ImageSpatialExtentsProperty> { + if let Some(primary_item) = &self.primary_item { + match self + .item_properties + .get(primary_item.id, BoxType::ImageSpatialExtentsProperty)? + { + Some(ItemProperty::ImageSpatialExtents(ispe)) => Ok(ispe), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Err(Error::from(Status::IspeMissing)), + } + } else { + Err(Error::from(Status::PitmMissing)) + } + } + pub fn spatial_extents_ptr(&self) -> Result<*const ImageSpatialExtentsProperty> { if let Some(primary_item) = &self.primary_item { match self @@ -1661,6 +1692,21 @@ impl AvifContext { } } + pub fn colour_information(&self) -> Result<&ColourInformation> { + if let Some(primary_item) = &self.primary_item { + match self + .item_properties + .get(primary_item.id, BoxType::ColourInformationBox)? + { + Some(ItemProperty::Colour(v)) => Ok(v), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Err(Error::from(Status::ItemTypeMissing)), + } + } else { + Err(Error::from(Status::PitmMissing)) + } + } + /// Returns None if there is no primary item or it has no associated NCLX colour boxes. pub fn nclx_colour_information_ptr(&self) -> Option> { if let Some(primary_item) = &self.primary_item { @@ -1722,6 +1768,21 @@ impl AvifContext { } } + pub fn image_mirror(&self) -> Result<&ImageMirror> { + if let Some(primary_item) = &self.primary_item { + match self + .item_properties + .get(primary_item.id, BoxType::ImageMirror)? + { + Some(ItemProperty::Mirroring(imir)) => Ok(imir), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Err(Error::from(Status::ItemTypeMissing)), + } + } else { + Err(Error::from(Status::PitmMissing)) + } + } + pub fn image_mirror_ptr(&self) -> Result<*const ImageMirror> { if let Some(primary_item) = &self.primary_item { match self @@ -1737,6 +1798,21 @@ impl AvifContext { } } + pub fn pixel_aspect_ratio(&self) -> Result<&PixelAspectRatio> { + if let Some(primary_item) = &self.primary_item { + match self + .item_properties + .get(primary_item.id, BoxType::PixelAspectRatioBox)? + { + Some(ItemProperty::PixelAspectRatio(pasp)) => Ok(pasp), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Err(Error::from(Status::ItemTypeMissing)), + } + } else { + Err(Error::from(Status::PitmMissing)) + } + } + pub fn pixel_aspect_ratio_ptr(&self) -> Result<*const PixelAspectRatio> { if let Some(primary_item) = &self.primary_item { match self @@ -3645,8 +3721,8 @@ fn read_ipco( #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ImageSpatialExtentsProperty { - image_width: u32, - image_height: u32, + pub image_width: u32, + pub image_height: u32, } /// Parse image spatial extents property @@ -3669,8 +3745,8 @@ fn read_ispe(src: &mut BMFFBox) -> Result(src: &mut BMFFBox) -> Result { #[repr(C)] #[derive(Debug)] pub struct NclxColourInformation { - colour_primaries: u8, - transfer_characteristics: u8, - matrix_coefficients: u8, - full_range_flag: bool, + pub colour_primaries: u8, + pub transfer_characteristics: u8, + pub matrix_coefficients: u8, + pub full_range_flag: bool, } /// The raw bytes of the ICC profile #[repr(C)] pub struct IccColourInformation { - bytes: TryVec, + pub bytes: TryVec, } impl fmt::Debug for IccColourInformation { From 4575afd3b58bb1374da113abd6542804b040d08a Mon Sep 17 00:00:00 2001 From: "L. E. Segovia" Date: Thu, 9 Apr 2026 16:18:57 -0300 Subject: [PATCH 2/2] Read and expose Exif and XMP metadata in the AvifContext --- mp4parse/src/lib.rs | 142 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 22 deletions(-) diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 55f99731..2079774a 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -227,6 +227,8 @@ pub enum Status { IlocOffsetOverflow, ImageItemType, InfeFlagsNonzero, + InfeStringNoNul, + InfeStringNotUtf8, InvalidUtf8, IpcoIndexOverflow, IpmaBadIndex, @@ -611,6 +613,14 @@ impl From for &str { "'infe' flags field shall be 0 \ per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2" } + Status::InfeStringNoNul => { + "'infe' strings shall be null-terminated \ + per ISOBMFF (ISO 14496-12:2020) § 8.11.6.3" + } + Status::InfeStringNotUtf8 => { + "'infe' strings field shall be valid utf8 \ + per ISOBMFF (ISO 14496-12:2020) § 8.11.6.3" + } Status::InvalidUtf8 => { "invalid utf8" } @@ -1594,6 +1604,10 @@ pub struct AvifContext { pub sequence: Option, /// A collection of unsupported features encountered during the parse pub unsupported_features: UnsupportedFeatures, + /// AVIF box containing the Exif metadata, if any + exif_metadata: Option, + /// AVIF box containing the XMP metadata, if any + xmp_metadata: Option, } impl AvifContext { @@ -1656,6 +1670,18 @@ impl AvifContext { } + pub fn exif_metadata(&self) -> Option<&[u8]> { + self.exif_metadata + .as_ref() + .map(|item| self.item_as_slice(item)) + } + + pub fn xmp_metadata(&self) -> Option<&[u8]> { + self.xmp_metadata + .as_ref() + .map(|item| self.item_as_slice(item)) + } + pub fn spatial_extents(&self) -> Result<&ImageSpatialExtentsProperty> { if let Some(primary_item) = &self.primary_item { match self @@ -1970,7 +1996,7 @@ impl DataBox { #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct PropertyIndex(u16); #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] -struct ItemId(u32); +pub struct ItemId(u32); impl ItemId { fn read(src: &mut impl ReadBytesExt, version: u8) -> Result { @@ -1986,9 +2012,13 @@ impl ItemId { /// See ISOBMFF (ISO 14496-12:2020) § 8.11.6 /// Only versions {2, 3} are supported #[derive(Debug)] -struct ItemInfoEntry { - item_id: ItemId, - item_type: u32, +pub struct ItemInfoEntry { + pub item_id: ItemId, + pub item_type: u32, + pub item_name: TryVec, + pub content_type: Option>, + pub content_encoding: Option>, + pub uri_type: Option>, } /// See ISOBMFF (ISO 14496-12:2020) § 8.11.12 @@ -2577,24 +2607,17 @@ pub fn read_avif(f: &mut T, strictness: ParseStrictness) -> Result = Default::default(); // store data or record location of relevant items for (item_id, loc) in iloc_items { - let item = if Some(item_id) == primary_item_id { - &mut primary_item - } else if Some(item_id) == alpha_item_id { - &mut alpha_item - } else { - continue; - }; - - assert!(item.is_none()); + let mut item: Option = None; // If our item is spread over multiple extents, we'll need to copy it // into a contiguous buffer. Otherwise, we can just store the extent // and return a pointer into the mdat/idat later to avoid the copy. if loc.extents.len() > 1 { - *item = Some(AvifItem::with_inline_data(item_id)) + item = Some(AvifItem::with_inline_data(item_id)) } trace!( @@ -2607,10 +2630,10 @@ pub fn read_avif(f: &mut T, strictness: ParseStrictness) -> Result Result { if let Some(extent_slice) = dat.get(extent) { - match item { + match &mut item { None => { trace!("Using IsobmffItem::Location"); - *item = Some(AvifItem { + item = Some(AvifItem { id: item_id, image_data: dat.location(extent), }); @@ -2670,6 +2693,14 @@ pub fn read_avif(f: &mut T, strictness: ParseStrictness) -> Result(f: &mut T, strictness: ParseStrictness) -> Result(f: &mut T, strictness: ParseStrictness) -> Result( + src: &mut BMFFBox, + strictness: ParseStrictness, +) -> Result> { + let mut s = TryVec::new(); + loop { + match src.read_u8() { + Ok(v) => { + s.push(v)?; + if v == 0 { + break; + } + }, + Err(_) => break, + } + } + match std::str::from_utf8(&s) { + Ok(s) => { + if !s.bytes().any(|b| b == b'\0') { + fail_with_status_if( + strictness != ParseStrictness::Permissive, + Status::InfeStringNoNul, + )?; + } + } + Err(_) => fail_with_status_if( + strictness != ParseStrictness::Permissive, + Status::InfeStringNotUtf8, + )?, + } + Ok(s) +} + /// Parse an Item Info Entry /// See ISOBMFF (ISO 14496-12:2020) § 8.11.6.2 fn read_infe( @@ -2988,15 +3069,32 @@ fn read_infe( let item_type = be_u32(src)?; debug!("infe {:?} item_type: {}", item_id, U32BE(item_type)); - // There are some additional fields here, but they're not of interest to us - skip_box_remain(src)?; - if item_protection_index != 0 { unsupported_features.insert(Feature::Ipro); - Ok(None) - } else { - Ok(Some(ItemInfoEntry { item_id, item_type })) + skip_box_remain(src)?; + return Ok(None); } + + let item_name = read_infe_string(src, strictness)?; + + let (content_type, content_encoding, uri_type) = if &item_type.to_be_bytes() == b"mime" { + ( + Some(read_infe_string(src, strictness)?), + Some(read_infe_string(src, strictness)?), + None + ) + } else { + (None, None, Some(read_infe_string(src, strictness)?)) + }; + + Ok(Some(ItemInfoEntry { + item_id, + item_type, + item_name, + content_type, + content_encoding, + uri_type + })) } /// Parse an Item Reference Box