diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 567ee21d..10be6efb 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -246,6 +246,7 @@ pub enum Status { IrefRecursion, IspeMissing, ItemTypeMissing, + LselBadLayerId, LselNoEssential, MdhdBadTimescale, MdhdBadVersion, @@ -304,14 +305,9 @@ impl Feature { | Self::Irot | Self::Ispe | Self::Pasp - | Self::Pixi => true, - Self::A1lx - | Self::A1op - | Self::Clap - | Self::Grid - | Self::Ipro - | Self::Lsel - | Self::Avis => false, + | Self::Pixi + | Self::Lsel => true, + Self::A1lx | Self::A1op | Self::Clap | Self::Grid | Self::Ipro | Self::Avis => false, } } } @@ -328,7 +324,7 @@ impl TryFrom<&ItemProperty> for Feature { ItemProperty::Colour(_) => Self::Colr, ItemProperty::ImageSpatialExtents(_) => Self::Ispe, ItemProperty::LayeredImageIndexing => Self::A1lx, - ItemProperty::LayerSelection => Self::Lsel, + ItemProperty::LayerSelection(_) => Self::Lsel, ItemProperty::Mirroring(_) => Self::Imir, ItemProperty::OperatingPointSelector => Self::A1op, ItemProperty::PixelAspectRatio(_) => Self::Pasp, @@ -693,6 +689,11 @@ impl From for &str { "LayerSelectorProperty (lsel) shall be marked as essential \ per HEIF (ISO/IEC 23008-12:2017) § 6.5.11.1" } + Status::LselBadLayerId => { + "LayerSelectorProperty (lsel) shall be between 0 and 3, \ + or the special value 0xFFFF \ + per https://aomediacodec.github.io/av1-avif/#layer-selector-property" + } Status::MdhdBadTimescale => { "zero timescale in mdhd" } @@ -1728,6 +1729,21 @@ impl AvifContext { } } + pub fn layer_selector_ptr(&self) -> Result<*const LayerSelector> { + if let Some(primary_item) = &self.primary_item { + match self + .item_properties + .get(primary_item.id, BoxType::LayerSelectorProperty)? + { + Some(ItemProperty::LayerSelection(lsel)) => Ok(lsel), + Some(other_property) => panic!("property key mismatch: {:?}", other_property), + None => Ok(std::ptr::null()), + } + } else { + Ok(std::ptr::null()) + } + } + /// A helper for the various `AvifItem`s to expose a reference to the /// underlying data while avoiding copies. fn item_as_slice<'a>(&'a self, item: &'a AvifItem) -> &'a [u8] { @@ -3193,29 +3209,31 @@ fn read_iprp( } } - // The following properties are unsupported, but we still enforce that - // they've been correctly marked as essential or not. - ItemProperty::LayeredImageIndexing => { - assert!(feature.is_ok() && unsupported_features.contains(feature?)); - if a.essential { + ItemProperty::LayerSelection(lsel) => { + if !a.essential { fail_with_status_if( strictness != ParseStrictness::Permissive, - Status::A1lxEssential, + Status::LselNoEssential, )?; } + + match lsel.layer_id { + 0..=3 | 0xffff => (), + _ => fail_with_status_if( + strictness != ParseStrictness::Permissive, + Status::LselBadLayerId, + )?, + } } - ItemProperty::LayerSelection => { + // The following properties are unsupported, but we still enforce that + // they've been correctly marked as essential or not. + ItemProperty::LayeredImageIndexing => { assert!(feature.is_ok() && unsupported_features.contains(feature?)); if a.essential { - assert!( - forbidden_items.contains(&association_entry.item_id) - || strictness == ParseStrictness::Permissive - ); - } else { fail_with_status_if( strictness != ParseStrictness::Permissive, - Status::LselNoEssential, + Status::A1lxEssential, )?; } } @@ -3302,7 +3320,7 @@ pub enum ItemProperty { Colour(ColourInformation), ImageSpatialExtents(ImageSpatialExtentsProperty), LayeredImageIndexing, - LayerSelection, + LayerSelection(LayerSelector), Mirroring(ImageMirror), OperatingPointSelector, PixelAspectRatio(PixelAspectRatio), @@ -3319,7 +3337,7 @@ impl From<&ItemProperty> for BoxType { ItemProperty::CleanAperture => BoxType::CleanApertureBox, ItemProperty::Colour(_) => BoxType::ColourInformationBox, ItemProperty::LayeredImageIndexing => BoxType::AV1LayeredImageIndexingProperty, - ItemProperty::LayerSelection => BoxType::LayerSelectorProperty, + ItemProperty::LayerSelection(_) => BoxType::LayerSelectorProperty, ItemProperty::Mirroring(_) => BoxType::ImageMirror, ItemProperty::OperatingPointSelector => BoxType::OperatingPointSelectorProperty, ItemProperty::PixelAspectRatio(_) => BoxType::PixelAspectRatioBox, @@ -3681,6 +3699,7 @@ fn read_ipco( } BoxType::PixelAspectRatioBox => ItemProperty::PixelAspectRatio(read_pasp(&mut b)?), BoxType::PixelInformationBox => ItemProperty::Channels(read_pixi(&mut b)?), + BoxType::LayerSelectorProperty => ItemProperty::LayerSelection(read_lsel(&mut b)?), other_box_type => { // Even if we didn't do anything with other property types, we still store @@ -3689,7 +3708,6 @@ fn read_ipco( let item_property = match other_box_type { BoxType::AV1LayeredImageIndexingProperty => ItemProperty::LayeredImageIndexing, BoxType::CleanApertureBox => ItemProperty::CleanAperture, - BoxType::LayerSelectorProperty => ItemProperty::LayerSelection, BoxType::OperatingPointSelectorProperty => ItemProperty::OperatingPointSelector, _ => { warn!("No ItemProperty variant for {:?}", other_box_type); @@ -3785,6 +3803,20 @@ fn read_pixi(src: &mut BMFFBox) -> Result { Ok(PixelInformation { bits_per_channel }) } +#[repr(C)] +#[derive(Debug)] +pub struct LayerSelector { + pub layer_id: u16, +} + +/// Parse layer selection +/// See HEIF (ISO 23008-12:2017) § 6.5.11 +fn read_lsel(src: &mut BMFFBox) -> Result { + let layer_id = be_u16(src)?; + + Ok(LayerSelector { layer_id }) +} + /// Despite [Rec. ITU-T H.273] (12/2016) defining the CICP fields as having a /// range of 0-255, and only a small fraction of those values being used, /// ISOBMFF (ISO 14496-12:2020) § 12.1.5 defines them as 16-bit values in the diff --git a/mp4parse/tests/a1lx-new-lsel.avif b/mp4parse/tests/a1lx-new-lsel.avif new file mode 100644 index 00000000..238bb009 Binary files /dev/null and b/mp4parse/tests/a1lx-new-lsel.avif differ diff --git a/mp4parse/tests/corrupt/lsel-bad-layer-id.avif b/mp4parse/tests/corrupt/lsel-bad-layer-id.avif new file mode 100644 index 00000000..9eebeb9b Binary files /dev/null and b/mp4parse/tests/corrupt/lsel-bad-layer-id.avif differ diff --git a/mp4parse/tests/public.rs b/mp4parse/tests/public.rs index bb3ed2ed..c81e1496 100644 --- a/mp4parse/tests/public.rs +++ b/mp4parse/tests/public.rs @@ -71,11 +71,15 @@ static AVIF_TEST_DIRS: &[&str] = &["tests", "av1-avif/testFiles", "link-u-avif-s // av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op.avif // av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1lx.avif // av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_lsel.avif -// respectively, but with https://github.com/AOMediaCodec/av1-avif/issues/174 fixed +// respectively, before https://github.com/AOMediaCodec/av1-avif/pull/191 +// and with https://github.com/AOMediaCodec/av1-avif/issues/174 fixed static AVIF_A1OP: &str = "tests/a1op.avif"; static AVIF_A1LX: &str = "tests/a1lx.avif"; static AVIF_LSEL: &str = "tests/lsel.avif"; +static AVIF_A1LX_NEW_LSEL: &str = "tests/a1lx-new-lsel.avif"; +static AVIF_LSEL_BAD_LAYER_ID: &str = "tests/corrupt/lsel-bad-layer-id.avif"; + static AVIF_CLAP: &str = "tests/clap-basic-1_3x3-to-1x1.avif"; static AVIF_GRID: &str = "av1-avif/testFiles/Microsoft/Summer_in_Tomsk_720p_5x4_grid.avif"; static AVIF_GRID_A1LX: &str = @@ -89,18 +93,17 @@ static AVIF_AVIS_MAJOR_NO_MOOV: &str = "tests/corrupt/alpha_video_moov_is_moop.a static AVIF_NO_PIXI_IMAGES: &[&str] = &[IMAGE_AVIF_NO_PIXI, IMAGE_AVIF_NO_ALPHA_PIXI]; static AVIF_UNSUPPORTED_IMAGES: &[&str] = &[ AVIF_A1LX, + AVIF_A1LX_NEW_LSEL, AVIF_A1OP, AVIF_CLAP, IMAGE_AVIF_CLAP_MISSING_ESSENTIAL, AVIF_GRID, AVIF_GRID_A1LX, - AVIF_LSEL, AVIF_AVIS_MAJOR_NO_PITM, AVIF_AVIS_MAJOR_WITH_PITM_AND_ALPHA, "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1lx.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op_lsel.avif", - "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_lsel.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_lsel.avif", "av1-avif/testFiles/Link-U/kimono.crop.avif", "av1-avif/testFiles/Link-U/kimono.mirror-vertical.rotate270.crop.avif", @@ -1171,7 +1174,29 @@ fn public_avif_a1op_missing_essential() { #[test] fn public_avif_lsel() { - assert_unsupported_essential(AVIF_LSEL, mp4::Feature::Lsel); + let input = &mut File::open(AVIF_LSEL).expect("Unknown file"); + let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); + let ptr = context.layer_selector_ptr().unwrap(); + assert_ne!(ptr, std::ptr::null()); + unsafe { + assert_eq!(1, ptr.read().layer_id); + } +} + +#[test] +fn public_avif_a1lx_new_lsel() { + let input = &mut File::open(AVIF_A1LX_NEW_LSEL).expect("Unknown file"); + let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); + let ptr = context.layer_selector_ptr().unwrap(); + assert_ne!(ptr, std::ptr::null()); + unsafe { + assert_eq!(0xffff, ptr.read().layer_id); + } +} + +#[test] +fn public_avif_lsel_bad_layer_id() { + assert_avif_shall(AVIF_LSEL_BAD_LAYER_ID, Status::LselBadLayerId); } #[test] diff --git a/mp4parse_capi/src/lib.rs b/mp4parse_capi/src/lib.rs index a4364636..0a9f4424 100644 --- a/mp4parse_capi/src/lib.rs +++ b/mp4parse_capi/src/lib.rs @@ -326,6 +326,7 @@ pub struct Mp4parseAvifImage { pub image_rotation: mp4parse::ImageRotation, pub image_mirror: *const mp4parse::ImageMirror, pub pixel_aspect_ratio: *const mp4parse::PixelAspectRatio, + pub layer_selector: *const mp4parse::LayerSelector, /// If no alpha item exists, members' `.length` will be 0 and `.data` will be null pub alpha_image: Mp4parseAvifImageItem, pub premultiplied_alpha: bool, @@ -1079,6 +1080,7 @@ pub fn mp4parse_avif_get_image_safe( image_rotation: context.image_rotation()?, image_mirror: context.image_mirror_ptr()?, pixel_aspect_ratio: context.pixel_aspect_ratio_ptr()?, + layer_selector: context.layer_selector_ptr()?, alpha_image, premultiplied_alpha: context.premultiplied_alpha, major_brand: context.major_brand.value,