Add Rust implementation
Change-Id: I749b335ee4f123d3adb7d9d92d7c14eb9948d90c
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..099f53a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "avifinfo"
+description = "Basic AVIF features parsing library"
+version = "1.0.0"
+authors = ["Yannis Guyon <yguyon@google.com>"]
+repository = "https://aomedia.googlesource.com/libavifinfo"
+keywords = ["avif"]
+license = "Alliance for Open Media Patent License 1.0"
+include = ["avifinfo.rs", "Cargo.toml", "README.md"]
+edition = "2021"
+
+[lib]
+name = "avifinfo"
+path = "avifinfo.rs"
diff --git a/README.md b/README.md
index d44001b..43d0b04 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,22 @@
php avifinfo_test.php
```
+## Rust implementation
+
+The Rust implementation of libavifinfo is similar to the C API.
+
+```shell
+cargo build
+```
+
+See `tests/avifinfo_test.rs` for a usage example.
+
+### Rust test
+
+```shell
+cargo test
+```
+
## Development
### Submitting patches {#submitting-patches}
diff --git a/avifinfo.rs b/avifinfo.rs
new file mode 100644
index 0000000..3c20319
--- /dev/null
+++ b/avifinfo.rs
@@ -0,0 +1,884 @@
+// Copyright (c) 2024, Alliance for Open Media. All rights reserved
+//
+// This source code is subject to the terms of the BSD 2 Clause License and
+// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+// was not distributed with this source code in the LICENSE file, you can
+// obtain it at www.aomedia.org/license/software. If the Alliance for Open
+// Media Patent License 1.0 was not distributed with this source code in the
+// PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+
+//-----------------------------------------------------------------------------
+
+#[derive(PartialEq, Debug)]
+pub enum AvifInfoError {
+ // The input bitstream was correctly parsed until now but bytes are missing. The request
+ // should be repeated with more input bytes.
+ NotEnoughData,
+ // The input bitstream was correctly parsed until now but it is too complex. The parsing was
+ // stopped to avoid any timeout or crash.
+ TooComplex,
+ // The input bitstream is not a valid AVIF file, truncated or not.
+ InvalidFile,
+}
+
+// Ok means that the file was correctly parsed and the requested information was
+// extracted. It is not guaranteed that the input bitstream is a valid complete
+// AVIF file.
+type AvifInfoResult<T> = Result<T, AvifInfoError>;
+
+#[derive(Debug, Default, PartialEq)]
+pub struct Features {
+ // In number of pixels. Ignores crop and rotation.
+ pub width: u32,
+ pub height: u32,
+
+ // Likely 8, 10 or 12 bits per channel per pixel.
+ pub bit_depth: u8,
+
+ // Likely 1, 2, 3 or 4 channels: (1 monochrome or 3 colors) + (0 or 1 alpha)
+ pub num_channels: u8,
+
+ // True if a gain map was found.
+ pub has_gainmap: bool,
+
+ // Id of the gain map item. Assumes there is at most one. If there are several gain map items
+ // (e.g. because the main image is tiled and each tile has an independent gain map), then this
+ // is one of the ids, arbitrarily chosen.
+ pub gainmap_item_id: u8,
+
+ // Start location in bytes of the primary item id, relative to the beginning of the given
+ // payload. The primary item id is a big endian number stored on bytes primary_item_id_location
+ // to primary_item_id_location+primary_item_id_bytes-1 inclusive.
+ pub primary_item_id_location: usize,
+
+ // Number of bytes of the primary item id.
+ pub primary_item_id_bytes: u8,
+}
+
+//------------------------------------------------------------------------------
+
+// Status returned when reading the content of a box (or file).
+#[derive(PartialEq)]
+enum InternalError {
+ NotFound, // Input correctly parsed but information is missing or elsewhere.
+ Truncated, // Input correctly parsed until missing bytes to continue.
+ Aborted, // Input correctly parsed until stopped to avoid timeout or crash.
+ Invalid, // Input incorrectly parsed.
+}
+
+// Ok means "Input correctly parsed and information retrieved".
+type InternalResult<T> = Result<T, InternalError>;
+
+impl From<InternalError> for AvifInfoError {
+ fn from(error: InternalError) -> Self {
+ match error {
+ InternalError::NotFound | InternalError::Truncated => AvifInfoError::NotEnoughData,
+ InternalError::Aborted => AvifInfoError::TooComplex,
+ InternalError::Invalid => AvifInfoError::InvalidFile,
+ }
+ }
+}
+
+// Be reasonable. Avoid timeouts and out-of-memory.
+const AVIFINFO_MAX_NUM_BOXES: u32 = 4096;
+// InternalFeatures uses u8 to store values.
+const AVIFINFO_MAX_VALUE: u8 = u8::MAX;
+// Maximum number of stored associations. Past that, they are skipped.
+const AVIFINFO_MAX_TILES: usize = 16;
+const AVIFINFO_MAX_PROPS: usize = 32;
+const AVIFINFO_MAX_FEATURES: usize = 8;
+const AVIFINFO_UNDEFINED: u8 = 0;
+
+//------------------------------------------------------------------------------
+// Streamed input struct and helper functions.
+
+struct Stream<'a> {
+ // The available bytes.
+ data: Option<&'a [u8]>,
+ // The known size of the parent box, if any.
+ size: Option<usize>,
+ // How many bytes were read or skipped.
+ offset: usize,
+}
+
+impl Stream<'_> {
+ fn read(&mut self, num_bytes: usize) -> InternalResult<&[u8]> {
+ if num_bytes == 0 {
+ return Ok(&[]);
+ }
+ let offset = self.offset;
+ self.skip(num_bytes)?;
+ match &self.data {
+ Some(data) if self.offset <= data.len() => Ok(&data[offset..self.offset]),
+ _ => Err(InternalError::Truncated),
+ }
+ }
+
+ fn read_u8(&mut self) -> InternalResult<u8> {
+ Ok(self.read(1)?[0])
+ }
+
+ fn read_u16(&mut self) -> InternalResult<u16> {
+ Ok(u16::from_be_bytes(self.read(2)?.try_into().unwrap()))
+ }
+
+ fn read_u24(&mut self) -> InternalResult<u32> {
+ Ok((self.read_uint(1)? << 16) | self.read_uint(2)?)
+ }
+
+ fn read_u32(&mut self) -> InternalResult<u32> {
+ Ok(u32::from_be_bytes(self.read(4)?.try_into().unwrap()))
+ }
+
+ fn read_u64(&mut self) -> InternalResult<u64> {
+ Ok(u64::from_be_bytes(self.read(8)?.try_into().unwrap()))
+ }
+
+ fn read_uint(&mut self, num_bytes: u8) -> InternalResult<u32> {
+ match num_bytes {
+ 1 => Ok(self.read_u8()? as u32),
+ 2 => Ok(self.read_u16()? as u32),
+ 4 => Ok(self.read_u32()?),
+ _ => Err(InternalError::Aborted),
+ }
+ }
+
+ fn read_4cc(&mut self) -> InternalResult<&[u8; 4]> {
+ Ok(self.read(4)?.try_into().unwrap())
+ }
+
+ fn skip(&mut self, num_bytes: usize) -> InternalResult<()> {
+ self.offset = self.offset.checked_add(num_bytes).ok_or(InternalError::Aborted)?;
+ if let Some(size) = self.size {
+ if self.offset > size {
+ return Err(InternalError::Invalid);
+ }
+ }
+ Ok(())
+ }
+
+ fn num_read_bytes(&self) -> usize {
+ self.offset // Includes the unchecked skipped bytes.
+ }
+
+ fn has_more_bytes(&self) -> bool {
+ match self.size {
+ Some(size) => self.offset < size,
+ None => true,
+ }
+ }
+
+ // Returns a portion of the stream. The size of the portion is either given
+ // or all remaining bytes are returned.
+ fn substream(&mut self, num_bytes: Option<usize>) -> InternalResult<Stream> {
+ let offset = self.offset;
+ let num_transferred_bytes = num_bytes.unwrap_or(match &self.data {
+ Some(data) => data.len().saturating_sub(offset),
+ None => 0,
+ });
+ self.skip(num_transferred_bytes)?;
+ self.offset = offset.checked_add(num_transferred_bytes).ok_or(InternalError::Aborted)?;
+ Ok(Stream {
+ data: if let Some(data) = &self.data {
+ let available_size = data.len().saturating_sub(offset);
+ let size = std::cmp::min(available_size, num_transferred_bytes);
+ if size != 0 { Some(&data[offset..offset + size]) } else { None }
+ } else {
+ None
+ },
+ size: num_bytes,
+ offset: 0,
+ })
+ }
+}
+
+//------------------------------------------------------------------------------
+// Features are parsed into temporary property associations.
+
+#[derive(Default)]
+struct InternalTile {
+ tile_item_id: u8,
+ parent_item_id: u8,
+ dimg_idx: u8, // Index of this association in the dimg box (0-based).
+}
+
+#[derive(Default)]
+struct InternalProp {
+ property_index: u8,
+ item_id: u8,
+}
+
+#[derive(Default)]
+struct InternalDimProp {
+ property_index: u8,
+ width: u32,
+ height: u32,
+}
+
+#[derive(Default)]
+struct InternalChanProp {
+ property_index: u8,
+ bit_depth: u8,
+ num_channels: u8,
+}
+
+#[derive(Default)]
+struct InternalFeatures {
+ has_primary_item: bool, // True if "pitm" was parsed.
+ has_alpha: bool, // True if an alpha "auxC" was parsed.
+ gainmap_property_index: u8, // Index of the gain map auxC property.
+ primary_item_id: u8,
+ primary_item_features: Features, // Deduced from the data below.
+ data_was_skipped: bool, // True if some loops/indices were skipped.
+ tone_mapped_item_id: u8, // Id of the "tmap" box, > 0 if present.
+ iinf_parsed: bool, // True if the "iinf" (item info) box was parsed.
+ iref_parsed: bool, // True if the "iref" (item reference) box was parsed.
+
+ num_tiles: usize,
+ tiles: [InternalTile; AVIFINFO_MAX_TILES],
+ num_props: usize,
+ props: [InternalProp; AVIFINFO_MAX_PROPS],
+ num_dim_props: usize,
+ dim_props: [InternalDimProp; AVIFINFO_MAX_FEATURES],
+ num_chan_props: usize,
+ chan_props: [InternalChanProp; AVIFINFO_MAX_FEATURES],
+}
+
+impl InternalFeatures {
+ fn get_item_features(&mut self, target_item_id: u8, tile_depth: u32) -> InternalResult<()> {
+ for prop_item in 0..self.num_props {
+ if self.props[prop_item].item_id != target_item_id {
+ continue;
+ }
+ let property_index = self.props[prop_item].property_index;
+
+ // Retrieve the width and height of the primary item if not already done.
+ if target_item_id == self.primary_item_id
+ && (self.primary_item_features.width == AVIFINFO_UNDEFINED as u32
+ || self.primary_item_features.height == AVIFINFO_UNDEFINED as u32)
+ {
+ for i in 0..self.num_dim_props {
+ if self.dim_props[i].property_index != property_index {
+ continue;
+ }
+ self.primary_item_features.width = self.dim_props[i].width;
+ self.primary_item_features.height = self.dim_props[i].height;
+ if self.primary_item_features.bit_depth != AVIFINFO_UNDEFINED
+ && self.primary_item_features.num_channels != AVIFINFO_UNDEFINED
+ {
+ return Ok(());
+ }
+ break;
+ }
+ }
+ // Retrieve the bit depth and number of channels of the target item if not
+ // already done.
+ if self.primary_item_features.bit_depth == AVIFINFO_UNDEFINED
+ || self.primary_item_features.num_channels == AVIFINFO_UNDEFINED
+ {
+ for i in 0..self.num_chan_props {
+ if self.chan_props[i].property_index != property_index {
+ continue;
+ }
+ self.primary_item_features.bit_depth = self.chan_props[i].bit_depth;
+ self.primary_item_features.num_channels = self.chan_props[i].num_channels;
+ if self.primary_item_features.width != AVIFINFO_UNDEFINED as u32
+ && self.primary_item_features.height != AVIFINFO_UNDEFINED as u32
+ {
+ return Ok(());
+ }
+ break;
+ }
+ }
+ }
+
+ // Check for the bit_depth and num_channels in a tile if not yet found.
+ if tile_depth < 3 {
+ for tile in 0..self.num_tiles {
+ if self.tiles[tile].parent_item_id != target_item_id {
+ continue;
+ }
+ match self.get_item_features(self.tiles[tile].tile_item_id, tile_depth + 1) {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {} // Keep searching.
+ Err(error) => return Err(error),
+ }
+ }
+ }
+ Err(InternalError::NotFound)
+ }
+
+ // Generates the primary_item_features.
+ // Returns InternalError::NotFound if there is not enough information.
+ fn get_primary_item_features(&mut self) -> InternalResult<()> {
+ // Nothing to do without the primary item ID.
+ if !self.has_primary_item {
+ return Err(InternalError::NotFound);
+ }
+ // Early exit.
+ if self.num_dim_props == 0 || self.num_chan_props == 0 {
+ return Err(InternalError::NotFound);
+ }
+
+ // Look for a gain map.
+ // HEIF scheme: gain map is a hidden input of a derived item.
+ if self.tone_mapped_item_id != AVIFINFO_UNDEFINED {
+ for tile in 0..self.num_tiles {
+ if self.tiles[tile].parent_item_id == self.tone_mapped_item_id
+ && self.tiles[tile].dimg_idx == 1
+ {
+ self.primary_item_features.has_gainmap = true;
+ self.primary_item_features.gainmap_item_id = self.tiles[tile].tile_item_id;
+ break;
+ }
+ }
+ }
+ // Adobe scheme: gain map is an auxiliary item.
+ if !self.primary_item_features.has_gainmap && self.gainmap_property_index > 0 {
+ for prop_item in 0..self.num_props {
+ if self.props[prop_item].property_index == self.gainmap_property_index {
+ self.primary_item_features.has_gainmap = true;
+ self.primary_item_features.gainmap_item_id = self.props[prop_item].item_id;
+ break;
+ }
+ }
+ }
+ // If the gain map has not been found but we haven't read all the relevant
+ // metadata, we might still find one later and cannot stop now.
+ if !self.primary_item_features.has_gainmap
+ && (!self.iinf_parsed
+ || (self.tone_mapped_item_id != AVIFINFO_UNDEFINED && !self.iref_parsed))
+ {
+ return Err(InternalError::NotFound);
+ }
+
+ self.get_item_features(self.primary_item_id, /* tile_depth= */ 0)?;
+
+ // "auxC" is parsed before the "ipma" properties so it is known now, if any.
+ if self.has_alpha {
+ self.primary_item_features.num_channels += 1;
+ }
+ Ok(())
+ }
+}
+
+//------------------------------------------------------------------------------
+// Box header parsing and various size checks.
+
+struct InternalBox {
+ box_type: [u8; 4], // Four characters.
+ version: u8, // 0 or actual version if this is a full box.
+ flags: u32, // 0 or actual value if this is a full box.
+ content_size: Option<usize>, // If known, size of the box in bytes, header exclusive.
+}
+
+// Reads the header of a box starting at the beginning of a stream.
+fn parse_box(
+ nesting_level: u32,
+ stream: &mut Stream,
+ num_parsed_boxes: &mut u32,
+) -> InternalResult<InternalBox> {
+ // See ISO/IEC 14496-12:2012(E) 4.2
+ let mut box_header_size = 8usize; // box 32-bit size + 32-bit type (at least)
+ let mut box_size: Option<usize> =
+ Some(stream.read_u32()?.try_into().or(Err(InternalError::Aborted))?);
+ let mut box_type = *stream.read_4cc()?;
+ // box_size==1 means 64-bit size should be read after the box type.
+ // box_size==0 means this box extends to all remaining bytes.
+ if box_size == Some(1) {
+ box_header_size += 8;
+ box_size = Some(stream.read_u64()?.try_into().or(Err(InternalError::Aborted))?);
+ } else if box_size == Some(0) {
+ if nesting_level != 0 {
+ // ISO/IEC 14496-12 4.2.2:
+ // if size is 0, then this box shall be in a top-level box
+ // (i.e. not contained in another box)
+ return Err(InternalError::Invalid);
+ }
+ box_size = None;
+ }
+ // 16 bytes of usertype should be read here if the box type is 'uuid'.
+ // 'uuid' boxes are skipped so usertype is part of the skipped body.
+
+ let has_fullbox_header = matches!(
+ &box_type,
+ b"meta" | b"pitm" | b"ipma" | b"ispe" | b"pixi" | b"iref" | b"auxC" | b"iinf" | b"infe"
+ );
+ if has_fullbox_header {
+ box_header_size += 4;
+ }
+ let content_size = match box_size {
+ Some(box_size) => {
+ Some(box_size.checked_sub(box_header_size).ok_or(InternalError::Invalid)?)
+ }
+ None => None,
+ };
+ if let Some(size) = stream.size {
+ if content_size.unwrap_or(box_header_size) > size.saturating_sub(stream.offset) {
+ return Err(InternalError::Invalid);
+ }
+ }
+ // get_features() can be called on a full stream or on a stream
+ // where the 'ftyp' box was already read. Do not count 'ftyp' boxes towards
+ // AVIFINFO_MAX_NUM_BOXES, so that this function returns the same status in
+ // both situations (because of the AVIFINFO_MAX_NUM_BOXES check that would
+ // compare a different box count otherwise). This is fine because top-level
+ // 'ftyp' boxes are just skipped anyway.
+ if nesting_level != 0 || &box_type != b"ftyp" {
+ // Avoid timeouts. The maximum number of parsed boxes is arbitrary.
+ *num_parsed_boxes += 1;
+ if *num_parsed_boxes >= AVIFINFO_MAX_NUM_BOXES {
+ return Err(InternalError::Aborted);
+ }
+ }
+
+ let mut version = 0;
+ let mut flags = 0;
+ if has_fullbox_header {
+ version = stream.read_u8()?;
+ flags = stream.read_u24()?;
+ // See AV1 Image File Format (AVIF) 8.1
+ // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when
+ // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged).
+ let is_parsable = (&box_type == b"meta" && version == 0)
+ || (&box_type == b"pitm" && version <= 1)
+ || (&box_type == b"ipma" && version <= 1)
+ || (&box_type == b"ispe" && version == 0)
+ || (&box_type == b"pixi" && version == 0)
+ || (&box_type == b"iref" && version <= 1)
+ || (&box_type == b"auxC" && version == 0)
+ || (&box_type == b"iinf" && version <= 1)
+ || (&box_type == b"infe" && version <= 2);
+ // Instead of considering this file as invalid, skip unparsable boxes.
+ if !is_parsable {
+ box_type = *b"skip"; // FreeSpaceBox. To be ignored by readers.
+ }
+ }
+
+ Ok(InternalBox { box_type, version, flags, content_size })
+}
+
+//------------------------------------------------------------------------------
+
+impl InternalFeatures {
+ // Parses a stream of an 'ipco' box.
+ // 'ispe' is used for width and height, 'pixi' and 'av1C' are used for bit depth
+ // and number of channels, and 'auxC' is used for alpha.
+ fn parse_ipco(
+ &mut self,
+ nesting_level: u32,
+ stream: &mut Stream,
+ num_parsed_boxes: &mut u32,
+ ) -> InternalResult<()> {
+ let mut box_index = 1u8; // 1-based index. Used for iterating over properties.
+ while stream.has_more_bytes() {
+ let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?;
+ let mut box_stream = stream.substream(box_features.content_size)?;
+
+ match &box_features.box_type {
+ b"ispe" => {
+ // See ISO/IEC 23008-12:2017(E) 6.5.3.2
+ let width = box_stream.read_u32()?;
+ let height = box_stream.read_u32()?;
+ if width == 0 || height == 0 {
+ return Err(InternalError::Invalid);
+ }
+ if self.num_dim_props < AVIFINFO_MAX_FEATURES {
+ self.dim_props[self.num_dim_props].property_index = box_index;
+ self.dim_props[self.num_dim_props].width = width;
+ self.dim_props[self.num_dim_props].height = height;
+ self.num_dim_props += 1;
+ } else {
+ self.data_was_skipped = true;
+ }
+ }
+ b"pixi" => {
+ // See ISO/IEC 23008-12:2017(E) 6.5.6.2
+ let num_channels = box_stream.read_u8()?;
+ if num_channels == 0 || num_channels > 3 {
+ return Err(InternalError::Invalid);
+ }
+ let bit_depth = box_stream.read_u8()?;
+ if bit_depth == 0 {
+ return Err(InternalError::Invalid);
+ }
+ for _ in 1..num_channels {
+ // Bit depth should be the same for all channels.
+ if box_stream.read_u8()? != bit_depth {
+ return Err(InternalError::Invalid);
+ }
+ }
+ if self.num_chan_props < AVIFINFO_MAX_FEATURES {
+ self.chan_props[self.num_chan_props].property_index = box_index;
+ self.chan_props[self.num_chan_props].bit_depth = bit_depth;
+ self.chan_props[self.num_chan_props].num_channels = num_channels;
+ self.num_chan_props += 1;
+ } else {
+ self.data_was_skipped = true;
+ }
+ }
+ b"av1C" => {
+ // See AV1 Codec ISO Media File Format Binding 2.3.1
+ // at https://aomediacodec.github.io/av1-isobmff/#av1c
+ // Only parse the necessary third byte. Assume that the others are valid.
+ box_stream.skip(2)?;
+ let data = box_stream.read_u8()?;
+ let high_bitdepth = (data & 0x40) != 0;
+ let twelve_bit = (data & 0x20) != 0;
+ let monochrome = (data & 0x10) != 0;
+ if twelve_bit && !high_bitdepth {
+ return Err(InternalError::Invalid);
+ }
+ if self.num_chan_props < AVIFINFO_MAX_FEATURES {
+ self.chan_props[self.num_chan_props].property_index = box_index;
+ self.chan_props[self.num_chan_props].bit_depth =
+ if high_bitdepth { if twelve_bit { 12 } else { 10 } } else { 8 };
+ self.chan_props[self.num_chan_props].num_channels =
+ if monochrome { 1 } else { 3 };
+ self.num_chan_props += 1;
+ } else {
+ self.data_was_skipped = true;
+ }
+ }
+ b"auxC" => {
+ // See AV1 Image File Format (AVIF) 4
+ // at https://aomediacodec.github.io/av1-avif/#auxiliary-images
+ const ALPHA_STR: &[u8] = b"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0";
+ const GAINMAP_STR: &[u8] = b"urn:com:photo:aux:hdrgainmap\0";
+ // Check for a gain map or for an alpha plane.
+ let content_size = box_features.content_size.unwrap();
+ let data = box_stream.read(std::cmp::min(content_size, ALPHA_STR.len()))?;
+ if &data[..std::cmp::min(data.len(), ALPHA_STR.len())] == ALPHA_STR {
+ // Note: It is unlikely but possible that this alpha plane does not belong
+ // to the primary item or a tile. Ignore this issue.
+ self.has_alpha = true;
+ } else if &data[..std::cmp::min(data.len(), GAINMAP_STR.len())] == GAINMAP_STR {
+ // Note: It is unlikely but possible that this gain map does not belong to
+ // the primary item or a tile. Ignore this issue.
+ self.gainmap_property_index = box_index;
+ }
+ }
+ _ => {}
+ }
+
+ if box_index == AVIFINFO_MAX_VALUE {
+ self.data_was_skipped = true;
+ return Err(InternalError::NotFound);
+ }
+ box_index += 1;
+ }
+ Err(InternalError::NotFound)
+ }
+
+ // Parses a stream of an 'iprp' box.
+ // The 'ipco' box contains the properties which are linked to items by the
+ // 'ipma' box.
+ fn parse_iprp(
+ &mut self,
+ nesting_level: u32,
+ stream: &mut Stream,
+ num_parsed_boxes: &mut u32,
+ ) -> InternalResult<()> {
+ while stream.has_more_bytes() {
+ let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?;
+ let mut box_stream = stream.substream(box_features.content_size)?;
+
+ match &box_features.box_type {
+ b"ipco" => {
+ match self.parse_ipco(nesting_level + 1, &mut box_stream, num_parsed_boxes) {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {} // Keep searching.
+ Err(error) => return Err(error),
+ }
+ }
+ b"ipma" => {
+ // See ISO/IEC 23008-12:2017(E) 9.3.2
+ let entry_count = box_stream.read_u32()?;
+ let id_num_bytes = if box_features.version < 1 { 2 } else { 4 };
+ let index_num_bytes = if box_features.flags & 1 != 0 { 2 } else { 1 };
+ let essential_bit_mask =
+ if box_features.flags & 1 != 0 { 0x8000 } else { 0x80 };
+
+ for entry in 0..entry_count as usize {
+ if entry >= AVIFINFO_MAX_PROPS || self.num_props >= AVIFINFO_MAX_PROPS {
+ self.data_was_skipped = true;
+ break;
+ }
+ let item_id = box_stream.read_uint(id_num_bytes)?;
+ let association_count = box_stream.read_u8()? as usize;
+ let mut property = 0usize;
+ while property < association_count {
+ if property >= AVIFINFO_MAX_PROPS
+ || self.num_props >= AVIFINFO_MAX_PROPS
+ {
+ self.data_was_skipped = true;
+ break;
+ }
+ let value = box_stream.read_uint(index_num_bytes)?;
+ // let essential = (value & essential_bit_mask); // Unused.
+ let property_index = value & (!essential_bit_mask);
+ if property_index <= AVIFINFO_MAX_VALUE as u32
+ && item_id <= AVIFINFO_MAX_VALUE as u32
+ {
+ self.props[self.num_props].property_index = property_index as u8;
+ self.props[self.num_props].item_id = item_id as u8;
+ self.num_props += 1;
+ } else {
+ self.data_was_skipped = true;
+ }
+ property += 1;
+ }
+ if property < association_count {
+ break; // Do not read garbage.
+ }
+ }
+
+ // If all features are available now, do not look further.
+ match self.get_primary_item_features() {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {}
+ Err(error) => return Err(error),
+ }
+ }
+ _ => {}
+ }
+ }
+ Err(InternalError::NotFound)
+ }
+
+ // Parses a stream of an 'iref' box.
+ // The 'dimg' boxes contain links between tiles and their parent items, which
+ // can be used to infer bit depth and number of channels for the primary item
+ // when the latter does not have these properties.
+ fn parse_iref(
+ &mut self,
+ nesting_level: u32,
+ stream: &mut Stream,
+ num_parsed_boxes: &mut u32,
+ ) -> InternalResult<()> {
+ self.iref_parsed = true;
+
+ while stream.has_more_bytes() {
+ let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?;
+ let mut box_stream = stream.substream(box_features.content_size)?;
+
+ if let b"dimg" = &box_features.box_type {
+ // See ISO/IEC 14496-12:2015(E) 8.11.12.2
+ let num_bytes_per_id = if box_features.version == 0 { 2 } else { 4 };
+ let from_item_id = box_stream.read_uint(num_bytes_per_id)?;
+ let reference_count = box_stream.read_u16()?;
+ for i in 0..reference_count {
+ if i as usize >= AVIFINFO_MAX_TILES {
+ self.data_was_skipped = true;
+ break;
+ }
+ let to_item_id = box_stream.read_uint(num_bytes_per_id)?;
+ if from_item_id <= AVIFINFO_MAX_VALUE as u32
+ && to_item_id <= AVIFINFO_MAX_VALUE as u32
+ && self.num_tiles < AVIFINFO_MAX_TILES
+ {
+ self.tiles[self.num_tiles].tile_item_id = to_item_id as u8;
+ self.tiles[self.num_tiles].parent_item_id = from_item_id as u8;
+ self.tiles[self.num_tiles].dimg_idx = i as u8;
+ self.num_tiles += 1;
+ } else {
+ self.data_was_skipped = true;
+ }
+ }
+
+ // If all features are available now, do not look further.
+ match self.get_primary_item_features() {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {}
+ Err(error) => return Err(error),
+ }
+ }
+ }
+ Err(InternalError::NotFound)
+ }
+
+ // Parses a stream of an 'iinf' box.
+ fn parse_iinf(
+ &mut self,
+ nesting_level: u32,
+ stream: &mut Stream,
+ box_version: u8,
+ num_parsed_boxes: &mut u32,
+ ) -> InternalResult<()> {
+ self.iinf_parsed = true;
+
+ let num_bytes_per_entry_count = if box_version == 0 { 2 } else { 4 };
+ let entry_count = stream.read_uint(num_bytes_per_entry_count)?;
+ for _ in 0..entry_count {
+ let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?;
+ let mut box_stream = stream.substream(box_features.content_size)?;
+
+ if let b"infe" = &box_features.box_type {
+ // See ISO/IEC 14496-12:2015(E) 8.11.6.2
+ let num_bytes_per_id = if box_features.version == 2 { 2 } else { 4 };
+ let item_id = box_stream.read_uint(num_bytes_per_id)?;
+
+ // Skip item_protection_index.
+ box_stream.skip(2)?;
+
+ if box_stream.read_4cc()? == b"tmap" {
+ // Tone Mapped Image: indicates the presence of a gain map.
+ if item_id <= AVIFINFO_MAX_VALUE as u32 {
+ self.tone_mapped_item_id = item_id as u8;
+ } else {
+ self.data_was_skipped = true;
+ }
+ }
+ }
+
+ if !stream.has_more_bytes() {
+ break; // Ignore entry_count bigger than box.
+ }
+ }
+ Err(InternalError::NotFound)
+ }
+
+ // Parses a stream of a 'meta' box. It looks for the primary item ID in the
+ // 'pitm' box and recurses into other boxes to find the features.
+ fn parse_meta(
+ &mut self,
+ nesting_level: u32,
+ stream_offset: usize,
+ stream: &mut Stream,
+ num_parsed_boxes: &mut u32,
+ ) -> InternalResult<()> {
+ while stream.has_more_bytes() {
+ let box_features = parse_box(nesting_level, stream, num_parsed_boxes)?;
+ let box_header_size = stream.num_read_bytes();
+ let mut box_stream = stream.substream(box_features.content_size)?;
+
+ match &box_features.box_type {
+ b"pitm" => {
+ // See ISO/IEC 14496-12:2015(E) 8.11.4.2
+ let num_bytes_per_id = if box_features.version == 0 { 2 } else { 4 };
+ let primary_item_id_location =
+ stream_offset.checked_add(box_header_size).ok_or(InternalError::Aborted)?;
+ let primary_item_id = box_stream.read_uint(num_bytes_per_id)?;
+ if primary_item_id > AVIFINFO_MAX_VALUE as u32 {
+ return Err(InternalError::Aborted);
+ }
+ self.has_primary_item = true;
+ self.primary_item_id = primary_item_id as u8;
+ self.primary_item_features.primary_item_id_location = primary_item_id_location;
+ self.primary_item_features.primary_item_id_bytes = num_bytes_per_id;
+ }
+ b"iprp" => {
+ match self.parse_iprp(nesting_level + 1, &mut box_stream, num_parsed_boxes) {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {} // Keep searching.
+ Err(error) => return Err(error),
+ }
+ }
+ b"iref" => {
+ match self.parse_iref(nesting_level + 1, &mut box_stream, num_parsed_boxes) {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {} // Keep searching.
+ Err(error) => return Err(error),
+ }
+ }
+ b"iinf" => {
+ match self.parse_iinf(
+ nesting_level + 1,
+ &mut box_stream,
+ box_features.version,
+ num_parsed_boxes,
+ ) {
+ Ok(()) => return Ok(()),
+ Err(InternalError::NotFound) => {} // Keep searching.
+ Err(error) => return Err(error),
+ }
+ }
+ _ => {}
+ }
+ }
+ // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one 'meta'.
+ Err(if self.data_was_skipped { InternalError::Aborted } else { InternalError::Invalid })
+ }
+}
+
+//------------------------------------------------------------------------------
+
+// Parses a file stream. The file type is checked through the 'ftyp' box.
+fn parse_ftyp(stream: &mut Stream) -> InternalResult<()> {
+ let mut num_parsed_boxes = 0u32;
+ let nesting_level = 0;
+ let box_features = parse_box(nesting_level, stream, &mut num_parsed_boxes)?;
+ if &box_features.box_type != b"ftyp" {
+ return Err(InternalError::Invalid);
+ }
+ // Consider a FileTypeBox running till the end of the file as invalid,
+ // because it should be first and a MetaBox should follow.
+ let content_size = box_features.content_size.ok_or(InternalError::Invalid)?;
+ // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
+ if content_size < 8 {
+ // major_brand,minor_version
+ return Err(InternalError::Invalid);
+ }
+ for i in 0..content_size / 4 {
+ let brand = stream.read_4cc()?;
+ if i == 1 {
+ continue; // Skip minor_version.
+ }
+ if brand == b"avif" || brand == b"avis" {
+ stream.skip(content_size - (i * 4 + 4))?;
+ return Ok(());
+ }
+ if i > 32 {
+ return Err(InternalError::Aborted); // Be reasonable.
+ }
+ }
+ Err(InternalError::Invalid)
+}
+
+// Parses a file stream. Features are extracted from the 'meta' box.
+impl InternalFeatures {
+ fn parse_file(&mut self, stream: &mut Stream) -> InternalResult<()> {
+ let mut num_parsed_boxes = 0u32;
+ loop {
+ let box_features =
+ parse_box(/* nesting_level= */ 0, stream, &mut num_parsed_boxes)?;
+ let offset = stream.num_read_bytes();
+ let mut box_stream = stream.substream(box_features.content_size)?;
+
+ if &box_features.box_type == b"meta" {
+ return self.parse_meta(
+ /* nesting_level= */ 1,
+ offset,
+ &mut box_stream,
+ &mut num_parsed_boxes,
+ );
+ } else if box_features.content_size.is_none() {
+ // This non-MetaBox runs till the end of the file. 'meta' is missing.
+ return Err(InternalError::Invalid);
+ }
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+// Fixed-size input public API
+
+pub fn identify(data: &[u8]) -> AvifInfoResult<()> {
+ match parse_ftyp(&mut Stream { data: Some(data), size: None, offset: 0 }) {
+ Ok(()) => Ok(()),
+ Err(error) => Err(error.into()),
+ }
+}
+
+pub fn get_features(data: &[u8]) -> AvifInfoResult<Features> {
+ let mut features = InternalFeatures { ..Default::default() };
+ match features.parse_file(&mut Stream { data: Some(data), size: None, offset: 0 }) {
+ Ok(()) => Ok(features.primary_item_features),
+ Err(error) => Err(error.into()),
+ }
+}
+
+//------------------------------------------------------------------------------
+// Streamed input API
+
+// There is no streamed input API yet.
diff --git a/tests/avifinfo_test.rs b/tests/avifinfo_test.rs
new file mode 100644
index 0000000..a3d685b
--- /dev/null
+++ b/tests/avifinfo_test.rs
@@ -0,0 +1,306 @@
+// Copyright (c) 2024, Alliance for Open Media. All rights reserved
+//
+// This source code is subject to the terms of the BSD 2 Clause License and
+// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+// was not distributed with this source code in the LICENSE file, you can
+// obtain it at www.aomedia.org/license/software. If the Alliance for Open
+// Media Patent License 1.0 was not distributed with this source code in the
+// PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+
+use avifinfo::{get_features, identify, AvifInfoError, Features};
+use std::{fs::File, io::Read};
+
+#[cfg(test)]
+fn load_file(path: &str) -> Vec<u8> {
+ let mut bytes = Vec::new();
+ File::open(path).unwrap().read_to_end(&mut bytes).unwrap();
+ bytes
+}
+
+//------------------------------------------------------------------------------
+// Positive tests
+
+#[test]
+fn single_pixel() {
+ let file = load_file("tests/avifinfo_test_1x1.avif");
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 1,
+ height: 1,
+ bit_depth: 8,
+ num_channels: 3,
+ has_gainmap: false,
+ gainmap_item_id: 0,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn with_alpha() {
+ let file = load_file("tests/avifinfo_test_2x2_alpha.avif");
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 2,
+ height: 2,
+ bit_depth: 8,
+ num_channels: 4,
+ has_gainmap: false,
+ gainmap_item_id: 0,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn with_gainmap() {
+ let file = load_file("tests/avifinfo_test_20x20_gainmap.avif");
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 20,
+ height: 20,
+ bit_depth: 8,
+ num_channels: 3,
+ has_gainmap: true,
+ gainmap_item_id: 2,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn set_primary_item_id_to_be_gainmap_item_id() {
+ let mut file = load_file("tests/avifinfo_test_20x20_gainmap.avif");
+ file[97] = 2;
+ // TODO(maryla-uc): find a small test file with a gainmap that is smaller
+ // than the main image.
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 20,
+ height: 20,
+ bit_depth: 8,
+ num_channels: 1, // the gainmap is monochrome
+ has_gainmap: true,
+ gainmap_item_id: 2,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn with_gainmap_tmap() {
+ for file_path in [
+ "tests/avifinfo_test_12x34_gainmap_tmap.avif",
+ "tests/avifinfo_test_12x34_gainmap_tmap_iref_after_iprp.avif",
+ ] {
+ let file = load_file(file_path);
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 12,
+ height: 34,
+ bit_depth: 10,
+ num_channels: 4,
+ has_gainmap: true,
+ gainmap_item_id: 4,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+ }
+}
+
+#[test]
+fn no_pixi_10b() {
+ // Same as above but "meta" box size is stored as 64 bits, "av1C" has
+ // 'high_bitdepth' set to true, "pixi" was renamed to "pixy" and "mdat" size
+ // is 0 (extends to the end of the file).
+ let file = load_file("tests/avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif");
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 1,
+ height: 1,
+ bit_depth: 10,
+ num_channels: 3,
+ has_gainmap: false,
+ gainmap_item_id: 0,
+ primary_item_id_location: 104,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn enough_bytes() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ // Truncate 'input' just after the required information (discard AV1 box).
+ let mdat_position = file.windows(4).position(|window| window == b"mdat");
+ file.resize(mdat_position.unwrap(), 0);
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 1,
+ height: 1,
+ bit_depth: 8,
+ num_channels: 3,
+ has_gainmap: false,
+ gainmap_item_id: 0,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn metabox_is_big() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ let meta_position = file.windows(4).position(|window| window == b"meta");
+ // 32-bit "1" then 4-char "meta" then 64-bit size.
+ file[meta_position.unwrap() - 4] = 0;
+ file[meta_position.unwrap() - 3] = 0;
+ file[meta_position.unwrap() - 2] = 0;
+ file[meta_position.unwrap() - 1] = 1;
+ for _ in 0..8 {
+ file.insert(meta_position.unwrap() + 4, 1);
+ }
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 1,
+ height: 1,
+ bit_depth: 8,
+ num_channels: 3,
+ has_gainmap: false,
+ gainmap_item_id: 0,
+ primary_item_id_location: 104,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+#[test]
+fn metabox_runs_till_end_of_file() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ let meta_position = file.windows(4).position(|window| window == b"meta");
+ // 32-bit "0" then 4-char "meta".
+ file[meta_position.unwrap() - 4] = 0;
+ file[meta_position.unwrap() - 3] = 0;
+ file[meta_position.unwrap() - 2] = 0;
+ file[meta_position.unwrap() - 1] = 0;
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(
+ get_features(file.as_slice()),
+ Ok(Features {
+ width: 1,
+ height: 1,
+ bit_depth: 8,
+ num_channels: 3,
+ has_gainmap: false,
+ gainmap_item_id: 0,
+ primary_item_id_location: 96,
+ primary_item_id_bytes: 2,
+ })
+ );
+}
+
+//------------------------------------------------------------------------------
+// Negative tests
+
+#[test]
+fn empty() {
+ assert_eq!(identify(&[]), Err(AvifInfoError::NotEnoughData));
+ assert_eq!(get_features(&[]), Err(AvifInfoError::NotEnoughData));
+}
+
+#[test]
+fn not_enough_bytes() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ // Truncate 'input' before having all the required information.
+ let ipma_position = file.windows(4).position(|window| window == b"ipma");
+ file.resize(ipma_position.unwrap(), 0);
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::NotEnoughData));
+}
+
+#[test]
+fn broken() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ // Change "ispe" to "aspe".
+ let ispe_position = file.windows(4).position(|window| window == b"ispe");
+ file[ispe_position.unwrap()] = b'a';
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::InvalidFile));
+}
+
+#[test]
+fn metabox_is_too_big() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ let meta_position = file.windows(4).position(|window| window == b"meta");
+ // 32-bit "1" then 4-char "meta" then 64-bit size.
+ file[meta_position.unwrap() - 4] = 0;
+ file[meta_position.unwrap() - 3] = 0;
+ file[meta_position.unwrap() - 2] = 0;
+ file[meta_position.unwrap() - 1] = 1;
+ for _ in 0..8 {
+ file.insert(meta_position.unwrap() + 4, 255);
+ }
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::TooComplex));
+}
+
+#[test]
+fn filetypebox_runs_till_end_of_file() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ let ftyp_position = file.windows(4).position(|window| window == b"ftyp");
+ file[ftyp_position.unwrap() - 1] = 0;
+
+ assert_eq!(identify(file.as_slice()), Err(AvifInfoError::InvalidFile));
+ assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::InvalidFile));
+}
+
+#[test]
+fn imagespatialextentsproperty_runs_till_end_of_file() {
+ let mut file = load_file("tests/avifinfo_test_1x1.avif");
+ let ispe_position = file.windows(4).position(|window| window == b"ispe");
+ file[ispe_position.unwrap() - 1] = 0;
+
+ assert_eq!(identify(file.as_slice()), Ok(()));
+ assert_eq!(get_features(file.as_slice()), Err(AvifInfoError::InvalidFile));
+}
+
+#[test]
+fn too_many_boxes() {
+ // Create a valid-ish input with too many boxes to parse.
+ let mut input: Vec<u8> =
+ vec![0, 0, 0, 16, b'f', b't', b'y', b'p', b'a', b'v', b'i', b'f', 0, 0, 0, 0];
+ let num_boxes = 12345;
+ input.reserve(input.len() + num_boxes * 8);
+ for _ in 0..num_boxes {
+ input.extend_from_slice(&[0, 0, 0, 8, b'a', b'b', b'c', b'd']);
+ }
+
+ assert_eq!(identify(input.as_slice()), Ok(()));
+ assert_eq!(get_features(input.as_slice()), Err(AvifInfoError::TooComplex));
+}