Upgrade crabbyavif to 963898a53d056da5ab97193fb3087e208c88dc34

This project was upgraded with external_updater.
Usage: tools/external_updater/updater.sh update external/rust/crabbyavif
For more info, check https://cs.android.com/android/platform/superproject/main/+/main:tools/external_updater/README.md

Test: TreeHugger
Change-Id: I9c08ceb6216151c874236455753bb3bff230ad7c
diff --git a/.gitignore b/.gitignore
index cb180ce..cdb0b5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@
 /external/libavif
 /match_stats.csv
 /out_comparison.txt
+/sys/aom-sys/aom
+/sys/aom-sys/aom.rs
 /sys/dav1d-sys/dav1d
 /sys/dav1d-sys/dav1d.rs
 /sys/libgav1-sys/libgav1
diff --git a/Cargo.toml b/Cargo.toml
index 59aee55..cdd294c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,5 @@
 workspace = { members = [
+  "sys/aom-sys",
   "sys/dav1d-sys",
   "sys/libyuv-sys",
   "sys/libgav1-sys",
@@ -19,6 +20,7 @@
 dav1d-sys = { version = "0.1.0", path = "sys/dav1d-sys", optional = true }
 libgav1-sys = { version = "0.1.0", path = "sys/libgav1-sys", optional = true }
 libyuv-sys = { version = "0.1.0", path = "sys/libyuv-sys", optional = true }
+aom-sys = { version = "0.1.0", path = "sys/aom-sys", optional = true }
 
 [dev-dependencies]
 test-case = "3.3.1"
@@ -43,6 +45,8 @@
 libyuv = ["dep:libyuv-sys"]
 android_mediacodec = ["dep:ndk-sys"]
 heic = []
+disable_cfi = []
+aom = ["dep:aom-sys"]
 
 [package.metadata.capi.header]
 name = "avif"
diff --git a/METADATA b/METADATA
index 8f49d29..469cc6a 100644
--- a/METADATA
+++ b/METADATA
@@ -8,14 +8,14 @@
   license_type: NOTICE
   last_upgrade_date {
     year: 2025
-    month: 2
-    day: 19
+    month: 3
+    day: 18
   }
   homepage: "https://github.com/webmproject/CrabbyAvif"
   identifier {
     type: "Git"
     value: "https://github.com/webmproject/CrabbyAvif.git"
-    version: "cbc72d88fb179f62da63d8bd50c9fab5d1bbd15f"
+    version: "963898a53d056da5ab97193fb3087e208c88dc34"
     primary_source: true
   }
 }
diff --git a/c_api_tests/decoder_tests.cc b/c_api_tests/decoder_tests.cc
index 2df5a5c..345c92c 100644
--- a/c_api_tests/decoder_tests.cc
+++ b/c_api_tests/decoder_tests.cc
@@ -56,6 +56,22 @@
   EXPECT_GT(decoder->image->alphaRowBytes, 0u);
 }
 
+TEST(DecoderTest, AlphaPremultiplied) {
+  if (!testutil::Av1DecoderAvailable()) {
+    GTEST_SKIP() << "AV1 Codec unavailable, skip test.";
+  }
+  auto decoder = CreateDecoder("alpha_premultiplied.avif");
+  ASSERT_NE(decoder, nullptr);
+  ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK);
+  EXPECT_EQ(decoder->compressionFormat, COMPRESSION_FORMAT_AVIF);
+  EXPECT_EQ(decoder->alphaPresent, AVIF_TRUE);
+  ASSERT_NE(decoder->image, nullptr);
+  EXPECT_EQ(decoder->image->alphaPremultiplied, AVIF_TRUE);
+  EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK);
+  EXPECT_NE(decoder->image->alphaPlane, nullptr);
+  EXPECT_GT(decoder->image->alphaRowBytes, 0u);
+}
+
 TEST(DecoderTest, AnimatedImage) {
   if (!testutil::Av1DecoderAvailable()) {
     GTEST_SKIP() << "AV1 Codec unavailable, skip test.";
diff --git a/examples/crabby_decode.rs b/examples/crabby_decode.rs
index 6eb94a9..5f2071e 100644
--- a/examples/crabby_decode.rs
+++ b/examples/crabby_decode.rs
@@ -78,6 +78,14 @@
     #[arg(long)]
     dimension_limit: Option<u32>,
 
+    /// If the input file contains embedded Exif metadata, ignore it (no-op if absent)
+    #[arg(long, default_value = "false")]
+    ignore_exif: bool,
+
+    /// If the input file contains embedded XMP metadata, ignore it (no-op if absent)
+    #[arg(long, default_value = "false")]
+    ignore_xmp: bool,
+
     /// Input AVIF file
     #[arg(allow_hyphen_values = false)]
     input_file: String,
@@ -299,6 +307,8 @@
         image_content_to_decode: ImageContentType::All,
         max_threads: max_threads(&args.jobs),
         allow_progressive: args.progressive,
+        ignore_exif: args.ignore_exif,
+        ignore_xmp: args.ignore_xmp,
         ..Settings::default()
     };
     // These values cannot be initialized in the list above since we need the default values to be
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 68237dd..1ae166c 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -18,14 +18,14 @@
 
 namespace crabbyavif {
 
+constexpr static const size_t CRABBY_AVIF_MAX_AV1_LAYER_COUNT = 4;
+
 constexpr static const uint32_t CRABBY_AVIF_DEFAULT_IMAGE_SIZE_LIMIT = (16384 * 16384);
 
 constexpr static const uint32_t CRABBY_AVIF_DEFAULT_IMAGE_DIMENSION_LIMIT = 32768;
 
 constexpr static const uint32_t CRABBY_AVIF_DEFAULT_IMAGE_COUNT_LIMIT = ((12 * 3600) * 60);
 
-constexpr static const size_t CRABBY_AVIF_MAX_AV1_LAYER_COUNT = 4;
-
 constexpr static const int CRABBY_AVIF_TRUE = 1;
 
 constexpr static const int CRABBY_AVIF_FALSE = 0;
diff --git a/src/capi/gainmap.rs b/src/capi/gainmap.rs
index 098361b..9b9d1d0 100644
--- a/src/capi/gainmap.rs
+++ b/src/capi/gainmap.rs
@@ -18,8 +18,7 @@
 
 use crate::decoder::gainmap::*;
 use crate::image::YuvRange;
-use crate::internal_utils::*;
-use crate::parser::mp4box::*;
+use crate::utils::*;
 use crate::*;
 
 pub type avifContentLightLevelInformationBox = ContentLightLevelInformation;
diff --git a/src/capi/image.rs b/src/capi/image.rs
index f7c49eb..ab7ad1d 100644
--- a/src/capi/image.rs
+++ b/src/capi/image.rs
@@ -18,8 +18,8 @@
 
 use crate::image::*;
 use crate::internal_utils::*;
-use crate::parser::mp4box::*;
 use crate::utils::clap::*;
+use crate::utils::*;
 use crate::*;
 
 use std::os::raw::c_int;
diff --git a/src/capi/io.rs b/src/capi/io.rs
index 180ce45..45b2bfd 100644
--- a/src/capi/io.rs
+++ b/src/capi/io.rs
@@ -164,6 +164,7 @@
 }
 
 impl crate::decoder::IO for avifIOWrapper {
+    #[cfg_attr(feature = "disable_cfi", no_sanitize(cfi))]
     fn read(&mut self, offset: u64, size: usize) -> AvifResult<&[u8]> {
         let res = unsafe {
             (self.io.read)(
diff --git a/src/capi/reformat.rs b/src/capi/reformat.rs
index e8a7c13..64ca63d 100644
--- a/src/capi/reformat.rs
+++ b/src/capi/reformat.rs
@@ -15,7 +15,6 @@
 use super::image::*;
 use super::types::*;
 
-use crate::decoder::Category;
 use crate::image::*;
 use crate::internal_utils::pixels::*;
 use crate::internal_utils::*;
diff --git a/src/capi/types.rs b/src/capi/types.rs
index e6b47b1..658786c 100644
--- a/src/capi/types.rs
+++ b/src/capi/types.rs
@@ -188,7 +188,7 @@
 pub type avifStrictFlags = u32;
 
 pub const AVIF_IMAGE_CONTENT_NONE: u32 = 0;
-pub const AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA: u32 = 1 << 0 | 1 << 1;
+pub const AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA: u32 = (1 << 0) | (1 << 1);
 pub const AVIF_IMAGE_CONTENT_GAIN_MAP: u32 = 1 << 2;
 pub const AVIF_IMAGE_CONTENT_ALL: u32 =
     AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_GAIN_MAP;
diff --git a/src/codecs/android_mediacodec.rs b/src/codecs/android_mediacodec.rs
index c84b090..bf82156 100644
--- a/src/codecs/android_mediacodec.rs
+++ b/src/codecs/android_mediacodec.rs
@@ -14,7 +14,8 @@
 
 use crate::codecs::Decoder;
 use crate::codecs::DecoderConfig;
-use crate::decoder::Category;
+use crate::decoder::CodecChoice;
+use crate::decoder::GridImageHelper;
 use crate::image::Image;
 use crate::image::YuvRange;
 use crate::internal_utils::pixels::*;
@@ -269,12 +270,15 @@
             color_format: self.color_format()?.into(),
             ..Default::default()
         };
+        // Clippy suggests using an iterator with an enumerator which does not seem more readable
+        // than using explicit indices.
+        #[allow(clippy::needless_range_loop)]
         for plane_index in 0usize..3 {
             plane_info.offset[plane_index] = isize_from_u32(planes[plane_index].mOffset)?;
             plane_info.row_stride[plane_index] = u32_from_i32(planes[plane_index].mRowInc)?;
             plane_info.column_stride[plane_index] = u32_from_i32(planes[plane_index].mColInc)?;
         }
-        return Ok(plane_info);
+        Ok(plane_info)
     }
 }
 
@@ -379,8 +383,10 @@
 impl MediaCodec {
     const AV1_MIME: &str = "video/av01";
     const HEVC_MIME: &str = "video/hevc";
+    const MAX_RETRIES: u32 = 100;
+    const TIMEOUT: u32 = 10000;
 
-    fn initialize_impl(&mut self) -> AvifResult<()> {
+    fn initialize_impl(&mut self, low_latency: bool) -> AvifResult<()> {
         let config = self.config.unwrap_ref();
         if self.codec_index >= self.codec_initializers.len() {
             return Err(AvifError::NoCodecAvailable);
@@ -411,10 +417,12 @@
                     AndroidMediaCodecOutputColorFormat::P010
                 } as i32,
             );
-            // low-latency is documented but isn't exposed as a constant in the NDK:
-            // https://developer.android.com/reference/android/media/MediaFormat#KEY_LOW_LATENCY
-            c_str!(low_latency, low_latency_tmp, "low-latency");
-            AMediaFormat_setInt32(format, low_latency, 1);
+            if low_latency {
+                // low-latency is documented but isn't exposed as a constant in the NDK:
+                // https://developer.android.com/reference/android/media/MediaFormat#KEY_LOW_LATENCY
+                c_str!(low_latency_str, low_latency_tmp, "low-latency");
+                AMediaFormat_setInt32(format, low_latency_str, 1);
+            }
             AMediaFormat_setInt32(
                 format,
                 AMEDIAFORMAT_KEY_MAX_INPUT_SIZE,
@@ -466,126 +474,15 @@
         Ok(())
     }
 
-    fn get_next_image_impl(
-        &mut self,
-        payload: &[u8],
-        _spatial_id: u8,
+    fn output_buffer_to_image(
+        &self,
+        buffer: *mut u8,
         image: &mut Image,
         category: Category,
     ) -> AvifResult<()> {
-        if self.codec.is_none() {
-            self.initialize_impl()?;
-        }
-        let codec = self.codec.unwrap();
-        if self.output_buffer_index.is_some() {
-            // Release any existing output buffer.
-            unsafe {
-                AMediaCodec_releaseOutputBuffer(codec, self.output_buffer_index.unwrap(), false);
-            }
-        }
-        let mut retry_count = 0;
-        unsafe {
-            while retry_count < 100 {
-                retry_count += 1;
-                let input_index = AMediaCodec_dequeueInputBuffer(codec, 10000);
-                if input_index >= 0 {
-                    let mut input_buffer_size: usize = 0;
-                    let input_buffer = AMediaCodec_getInputBuffer(
-                        codec,
-                        input_index as usize,
-                        &mut input_buffer_size as *mut _,
-                    );
-                    if input_buffer.is_null() {
-                        return Err(AvifError::UnknownError(format!(
-                            "input buffer at index {input_index} was null"
-                        )));
-                    }
-                    let hevc_whole_nal_units = self.hevc_whole_nal_units(payload)?;
-                    let codec_payload = match &hevc_whole_nal_units {
-                        Some(hevc_payload) => hevc_payload,
-                        None => payload,
-                    };
-                    if input_buffer_size < codec_payload.len() {
-                        return Err(AvifError::UnknownError(format!(
-                        "input buffer (size {input_buffer_size}) was not big enough. required size: {}",
-                        codec_payload.len()
-                    )));
-                    }
-                    ptr::copy_nonoverlapping(
-                        codec_payload.as_ptr(),
-                        input_buffer,
-                        codec_payload.len(),
-                    );
-
-                    if AMediaCodec_queueInputBuffer(
-                        codec,
-                        usize_from_isize(input_index)?,
-                        /*offset=*/ 0,
-                        codec_payload.len(),
-                        /*pts=*/ 0,
-                        /*flags=*/ 0,
-                    ) != media_status_t_AMEDIA_OK
-                    {
-                        return Err(AvifError::UnknownError("".into()));
-                    }
-                    break;
-                } else if input_index == AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
-                    continue;
-                } else {
-                    return Err(AvifError::UnknownError(format!(
-                        "got input index < 0: {input_index}"
-                    )));
-                }
-            }
-        }
-        let mut buffer: Option<*mut u8> = None;
-        let mut buffer_size: usize = 0;
-        let mut buffer_info = AMediaCodecBufferInfo::default();
-        retry_count = 0;
-        while retry_count < 100 {
-            retry_count += 1;
-            unsafe {
-                let output_index =
-                    AMediaCodec_dequeueOutputBuffer(codec, &mut buffer_info as *mut _, 10000);
-                if output_index >= 0 {
-                    let output_buffer = AMediaCodec_getOutputBuffer(
-                        codec,
-                        usize_from_isize(output_index)?,
-                        &mut buffer_size as *mut _,
-                    );
-                    if output_buffer.is_null() {
-                        return Err(AvifError::UnknownError("output buffer is null".into()));
-                    }
-                    buffer = Some(output_buffer);
-                    self.output_buffer_index = Some(usize_from_isize(output_index)?);
-                    break;
-                } else if output_index == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED as isize {
-                    continue;
-                } else if output_index == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED as isize {
-                    let format = AMediaCodec_getOutputFormat(codec);
-                    if format.is_null() {
-                        return Err(AvifError::UnknownError("output format was null".into()));
-                    }
-                    self.format = Some(MediaFormat { format });
-                    continue;
-                } else if output_index == AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
-                    continue;
-                } else {
-                    return Err(AvifError::UnknownError(format!(
-                        "mediacodec dequeue_output_buffer failed: {output_index}"
-                    )));
-                }
-            }
-        }
-        if buffer.is_none() {
-            return Err(AvifError::UnknownError(
-                "did not get buffer from mediacodec".into(),
-            ));
-        }
         if self.format.is_none() {
             return Err(AvifError::UnknownError("format is none".into()));
         }
-        let buffer = buffer.unwrap();
         let format = self.format.unwrap_ref();
         image.width = format.width()? as u32;
         image.height = format.height()? as u32;
@@ -595,7 +492,6 @@
         image.yuv_format = plane_info.pixel_format();
         match category {
             Category::Alpha => {
-                // TODO: make sure alpha plane matches previous alpha plane.
                 image.row_bytes[3] = plane_info.row_stride[0];
                 image.planes[3] = Some(Pixels::from_raw_pointer(
                     unsafe { buffer.offset(plane_info.offset[0]) },
@@ -643,6 +539,223 @@
         Ok(())
     }
 
+    fn enqueue_payload(&self, input_index: isize, payload: &[u8], flags: u32) -> AvifResult<()> {
+        let codec = self.codec.unwrap();
+        let mut input_buffer_size: usize = 0;
+        let input_buffer = unsafe {
+            AMediaCodec_getInputBuffer(
+                codec,
+                input_index as usize,
+                &mut input_buffer_size as *mut _,
+            )
+        };
+        if input_buffer.is_null() {
+            return Err(AvifError::UnknownError(format!(
+                "input buffer at index {input_index} was null"
+            )));
+        }
+        let hevc_whole_nal_units = self.hevc_whole_nal_units(payload)?;
+        let codec_payload = match &hevc_whole_nal_units {
+            Some(hevc_payload) => hevc_payload,
+            None => payload,
+        };
+        if input_buffer_size < codec_payload.len() {
+            return Err(AvifError::UnknownError(format!(
+                "input buffer (size {input_buffer_size}) was not big enough. required size: {}",
+                codec_payload.len()
+            )));
+        }
+        unsafe {
+            ptr::copy_nonoverlapping(codec_payload.as_ptr(), input_buffer, codec_payload.len());
+
+            if AMediaCodec_queueInputBuffer(
+                codec,
+                usize_from_isize(input_index)?,
+                /*offset=*/ 0,
+                codec_payload.len(),
+                /*pts=*/ 0,
+                flags,
+            ) != media_status_t_AMEDIA_OK
+            {
+                return Err(AvifError::UnknownError("".into()));
+            }
+        }
+        Ok(())
+    }
+
+    fn get_next_image_impl(
+        &mut self,
+        payload: &[u8],
+        _spatial_id: u8,
+        image: &mut Image,
+        category: Category,
+    ) -> AvifResult<()> {
+        if self.codec.is_none() {
+            self.initialize_impl(/*low_latency=*/ true)?;
+        }
+        let codec = self.codec.unwrap();
+        if self.output_buffer_index.is_some() {
+            // Release any existing output buffer.
+            unsafe {
+                AMediaCodec_releaseOutputBuffer(codec, self.output_buffer_index.unwrap(), false);
+            }
+        }
+        let mut retry_count = 0;
+        unsafe {
+            while retry_count < Self::MAX_RETRIES {
+                retry_count += 1;
+                let input_index = AMediaCodec_dequeueInputBuffer(codec, Self::TIMEOUT as _);
+                if input_index >= 0 {
+                    self.enqueue_payload(input_index, payload, 0)?;
+                    break;
+                } else if input_index == AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
+                    continue;
+                } else {
+                    return Err(AvifError::UnknownError(format!(
+                        "got input index < 0: {input_index}"
+                    )));
+                }
+            }
+        }
+        let mut buffer: Option<*mut u8> = None;
+        let mut buffer_size: usize = 0;
+        let mut buffer_info = AMediaCodecBufferInfo::default();
+        retry_count = 0;
+        while retry_count < Self::MAX_RETRIES {
+            retry_count += 1;
+            unsafe {
+                let output_index = AMediaCodec_dequeueOutputBuffer(
+                    codec,
+                    &mut buffer_info as *mut _,
+                    Self::TIMEOUT as _,
+                );
+                if output_index >= 0 {
+                    let output_buffer = AMediaCodec_getOutputBuffer(
+                        codec,
+                        usize_from_isize(output_index)?,
+                        &mut buffer_size as *mut _,
+                    );
+                    if output_buffer.is_null() {
+                        return Err(AvifError::UnknownError("output buffer is null".into()));
+                    }
+                    buffer = Some(output_buffer);
+                    self.output_buffer_index = Some(usize_from_isize(output_index)?);
+                    break;
+                } else if output_index == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED as isize {
+                    continue;
+                } else if output_index == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED as isize {
+                    let format = AMediaCodec_getOutputFormat(codec);
+                    if format.is_null() {
+                        return Err(AvifError::UnknownError("output format was null".into()));
+                    }
+                    self.format = Some(MediaFormat { format });
+                    continue;
+                } else if output_index == AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
+                    continue;
+                } else {
+                    return Err(AvifError::UnknownError(format!(
+                        "mediacodec dequeue_output_buffer failed: {output_index}"
+                    )));
+                }
+            }
+        }
+        if buffer.is_none() {
+            return Err(AvifError::UnknownError(
+                "did not get buffer from mediacodec".into(),
+            ));
+        }
+        self.output_buffer_to_image(buffer.unwrap(), image, category)?;
+        Ok(())
+    }
+
+    fn get_next_image_grid_impl(
+        &mut self,
+        payloads: &[Vec<u8>],
+        grid_image_helper: &mut GridImageHelper,
+    ) -> AvifResult<()> {
+        if self.codec.is_none() {
+            self.initialize_impl(/*low_latency=*/ false)?;
+        }
+        let codec = self.codec.unwrap();
+        let mut retry_count = 0;
+        let mut payloads_iter = payloads.iter().peekable();
+        unsafe {
+            while !grid_image_helper.is_grid_complete()? {
+                // Queue as many inputs as we possibly can, then block on dequeuing outputs. After
+                // getting each output, come back and queue the inputs again to keep the decoder as
+                // busy as possible.
+                while payloads_iter.peek().is_some() {
+                    let input_index = AMediaCodec_dequeueInputBuffer(codec, 0);
+                    if input_index < 0 {
+                        if retry_count >= Self::MAX_RETRIES {
+                            return Err(AvifError::UnknownError("max retries exceeded".into()));
+                        }
+                        break;
+                    }
+                    let payload = payloads_iter.next().unwrap();
+                    self.enqueue_payload(
+                        input_index,
+                        payload,
+                        if payloads_iter.peek().is_some() {
+                            0
+                        } else {
+                            AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM as u32
+                        },
+                    )?;
+                }
+                loop {
+                    let mut buffer_info = AMediaCodecBufferInfo::default();
+                    let output_index = AMediaCodec_dequeueOutputBuffer(
+                        codec,
+                        &mut buffer_info as *mut _,
+                        Self::TIMEOUT as _,
+                    );
+                    if output_index == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED as isize {
+                        continue;
+                    } else if output_index == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED as isize {
+                        let format = AMediaCodec_getOutputFormat(codec);
+                        if format.is_null() {
+                            return Err(AvifError::UnknownError("output format was null".into()));
+                        }
+                        self.format = Some(MediaFormat { format });
+                        continue;
+                    } else if output_index == AMEDIACODEC_INFO_TRY_AGAIN_LATER as isize {
+                        retry_count += 1;
+                        if retry_count >= Self::MAX_RETRIES {
+                            return Err(AvifError::UnknownError("max retries exceeded".into()));
+                        }
+                        break;
+                    } else if output_index < 0 {
+                        return Err(AvifError::UnknownError("".into()));
+                    } else {
+                        let mut buffer_size: usize = 0;
+                        let output_buffer = AMediaCodec_getOutputBuffer(
+                            codec,
+                            usize_from_isize(output_index)?,
+                            &mut buffer_size as *mut _,
+                        );
+                        if output_buffer.is_null() {
+                            return Err(AvifError::UnknownError("output buffer is null".into()));
+                        }
+                        let mut cell_image = Image::default();
+                        self.output_buffer_to_image(
+                            output_buffer,
+                            &mut cell_image,
+                            grid_image_helper.category,
+                        )?;
+                        grid_image_helper.copy_from_cell_image(&cell_image)?;
+                        if !grid_image_helper.is_grid_complete()? {
+                            // The last output buffer will be released when the codec is dropped.
+                            AMediaCodec_releaseOutputBuffer(codec, output_index as _, false);
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+
     fn drop_impl(&mut self) {
         if self.codec.is_some() {
             if self.output_buffer_index.is_some() {
@@ -666,6 +779,10 @@
 }
 
 impl Decoder for MediaCodec {
+    fn codec(&self) -> CodecChoice {
+        CodecChoice::MediaCodec
+    }
+
     fn initialize(&mut self, config: &DecoderConfig) -> AvifResult<()> {
         self.codec_initializers = get_codec_initializers(config);
         self.config = Some(config.clone());
@@ -694,6 +811,26 @@
             "all the codecs failed to extract an image".into(),
         ))
     }
+
+    fn get_next_image_grid(
+        &mut self,
+        payloads: &[Vec<u8>],
+        _spatial_id: u8,
+        grid_image_helper: &mut GridImageHelper,
+    ) -> AvifResult<()> {
+        while self.codec_index < self.codec_initializers.len() {
+            let res = self.get_next_image_grid_impl(payloads, grid_image_helper);
+            if res.is_ok() {
+                return Ok(());
+            }
+            // Drop the current codec and try the next one.
+            self.drop_impl();
+            self.codec_index += 1;
+        }
+        Err(AvifError::UnknownError(
+            "all the codecs failed to extract an image".into(),
+        ))
+    }
 }
 
 impl MediaCodec {
diff --git a/src/codecs/dav1d.rs b/src/codecs/dav1d.rs
index 5ceec1b..3db8120 100644
--- a/src/codecs/dav1d.rs
+++ b/src/codecs/dav1d.rs
@@ -14,7 +14,8 @@
 
 use crate::codecs::Decoder;
 use crate::codecs::DecoderConfig;
-use crate::decoder::Category;
+use crate::decoder::CodecChoice;
+use crate::decoder::GridImageHelper;
 use crate::image::Image;
 use crate::image::YuvRange;
 use crate::internal_utils::pixels::*;
@@ -24,10 +25,11 @@
 
 use std::mem::MaybeUninit;
 
-#[derive(Debug, Default)]
+#[derive(Default)]
 pub struct Dav1d {
     context: Option<*mut Dav1dContext>,
     picture: Option<Dav1dPicture>,
+    config: Option<DecoderConfig>,
 }
 
 unsafe extern "C" fn avif_dav1d_free_callback(
@@ -40,19 +42,18 @@
 // See https://code.videolan.org/videolan/dav1d/-/blob/9849ede1304da1443cfb4a86f197765081034205/include/dav1d/common.h#L55-59
 const DAV1D_EAGAIN: i32 = if libc::EPERM > 0 { -libc::EAGAIN } else { libc::EAGAIN };
 
-// The type of the fields from dav1d_sys::bindings::* are dependent on the
-// compiler that is used to generate the bindings, version of dav1d, etc.
-// So allow clippy to ignore unnecessary cast warnings.
-#[allow(clippy::unnecessary_cast)]
-impl Decoder for Dav1d {
-    fn initialize(&mut self, config: &DecoderConfig) -> AvifResult<()> {
+impl Dav1d {
+    fn initialize_impl(&mut self, low_latency: bool) -> AvifResult<()> {
         if self.context.is_some() {
             return Ok(());
         }
+        let config = self.config.unwrap_ref();
         let mut settings_uninit: MaybeUninit<Dav1dSettings> = MaybeUninit::uninit();
         unsafe { dav1d_default_settings(settings_uninit.as_mut_ptr()) };
         let mut settings = unsafe { settings_uninit.assume_init() };
-        settings.max_frame_delay = 1;
+        if low_latency {
+            settings.max_frame_delay = 1;
+        }
         settings.n_threads = i32::try_from(config.max_threads).unwrap_or(1);
         settings.operating_point = config.operating_point as i32;
         settings.all_layers = if config.all_layers { 1 } else { 0 };
@@ -78,7 +79,94 @@
             )));
         }
         self.context = Some(unsafe { dec.assume_init() });
+        Ok(())
+    }
 
+    fn picture_to_image(
+        &self,
+        dav1d_picture: &Dav1dPicture,
+        image: &mut Image,
+        category: Category,
+    ) -> AvifResult<()> {
+        match category {
+            Category::Alpha => {
+                if image.width > 0
+                    && image.height > 0
+                    && (image.width != (dav1d_picture.p.w as u32)
+                        || image.height != (dav1d_picture.p.h as u32)
+                        || image.depth != (dav1d_picture.p.bpc as u8))
+                {
+                    // Alpha plane does not match the previous alpha plane.
+                    return Err(AvifError::UnknownError("".into()));
+                }
+                image.width = dav1d_picture.p.w as u32;
+                image.height = dav1d_picture.p.h as u32;
+                image.depth = dav1d_picture.p.bpc as u8;
+                image.row_bytes[3] = dav1d_picture.stride[0] as u32;
+                image.planes[3] = Some(Pixels::from_raw_pointer(
+                    dav1d_picture.data[0] as *mut u8,
+                    image.depth as u32,
+                    image.height,
+                    image.row_bytes[3],
+                )?);
+                image.image_owns_planes[3] = false;
+                let seq_hdr = unsafe { &(*dav1d_picture.seq_hdr) };
+                image.yuv_range =
+                    if seq_hdr.color_range == 0 { YuvRange::Limited } else { YuvRange::Full };
+            }
+            _ => {
+                image.width = dav1d_picture.p.w as u32;
+                image.height = dav1d_picture.p.h as u32;
+                image.depth = dav1d_picture.p.bpc as u8;
+
+                image.yuv_format = match dav1d_picture.p.layout {
+                    0 => PixelFormat::Yuv400,
+                    1 => PixelFormat::Yuv420,
+                    2 => PixelFormat::Yuv422,
+                    3 => PixelFormat::Yuv444,
+                    _ => return Err(AvifError::UnknownError("".into())), // not reached.
+                };
+                let seq_hdr = unsafe { &(*dav1d_picture.seq_hdr) };
+                image.yuv_range =
+                    if seq_hdr.color_range == 0 { YuvRange::Limited } else { YuvRange::Full };
+                image.chroma_sample_position = (seq_hdr.chr as u32).into();
+
+                image.color_primaries = (seq_hdr.pri as u16).into();
+                image.transfer_characteristics = (seq_hdr.trc as u16).into();
+                image.matrix_coefficients = (seq_hdr.mtrx as u16).into();
+
+                for plane in 0usize..image.yuv_format.plane_count() {
+                    let stride_index = if plane == 0 { 0 } else { 1 };
+                    image.row_bytes[plane] = dav1d_picture.stride[stride_index] as u32;
+                    image.planes[plane] = Some(Pixels::from_raw_pointer(
+                        dav1d_picture.data[plane] as *mut u8,
+                        image.depth as u32,
+                        image.height,
+                        image.row_bytes[plane],
+                    )?);
+                    image.image_owns_planes[plane] = false;
+                }
+                if image.yuv_format == PixelFormat::Yuv400 {
+                    // Clear left over chroma planes from previous frames.
+                    image.clear_chroma_planes();
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+// The type of the fields from dav1d_sys::bindings::* are dependent on the
+// compiler that is used to generate the bindings, version of dav1d, etc.
+// So allow clippy to ignore unnecessary cast warnings.
+#[allow(clippy::unnecessary_cast)]
+impl Decoder for Dav1d {
+    fn codec(&self) -> CodecChoice {
+        CodecChoice::Dav1d
+    }
+
+    fn initialize(&mut self, config: &DecoderConfig) -> AvifResult<()> {
+        self.config = Some(config.clone());
         Ok(())
     }
 
@@ -90,7 +178,7 @@
         category: Category,
     ) -> AvifResult<()> {
         if self.context.is_none() {
-            self.initialize(&DecoderConfig::default())?;
+            self.initialize_impl(true)?;
         }
         unsafe {
             let mut data: Dav1dData = std::mem::zeroed();
@@ -187,74 +275,18 @@
                 return Err(AvifError::UnknownError("".into()));
             }
         }
-
-        let dav1d_picture = self.picture.unwrap_ref();
-        match category {
-            Category::Alpha => {
-                if image.width > 0
-                    && image.height > 0
-                    && (image.width != (dav1d_picture.p.w as u32)
-                        || image.height != (dav1d_picture.p.h as u32)
-                        || image.depth != (dav1d_picture.p.bpc as u8))
-                {
-                    // Alpha plane does not match the previous alpha plane.
-                    return Err(AvifError::UnknownError("".into()));
-                }
-                image.width = dav1d_picture.p.w as u32;
-                image.height = dav1d_picture.p.h as u32;
-                image.depth = dav1d_picture.p.bpc as u8;
-                image.row_bytes[3] = dav1d_picture.stride[0] as u32;
-                image.planes[3] = Some(Pixels::from_raw_pointer(
-                    dav1d_picture.data[0] as *mut u8,
-                    image.depth as u32,
-                    image.height,
-                    image.row_bytes[3],
-                )?);
-                image.image_owns_planes[3] = false;
-                let seq_hdr = unsafe { &(*dav1d_picture.seq_hdr) };
-                image.yuv_range =
-                    if seq_hdr.color_range == 0 { YuvRange::Limited } else { YuvRange::Full };
-            }
-            _ => {
-                image.width = dav1d_picture.p.w as u32;
-                image.height = dav1d_picture.p.h as u32;
-                image.depth = dav1d_picture.p.bpc as u8;
-
-                image.yuv_format = match dav1d_picture.p.layout {
-                    0 => PixelFormat::Yuv400,
-                    1 => PixelFormat::Yuv420,
-                    2 => PixelFormat::Yuv422,
-                    3 => PixelFormat::Yuv444,
-                    _ => return Err(AvifError::UnknownError("".into())), // not reached.
-                };
-                let seq_hdr = unsafe { &(*dav1d_picture.seq_hdr) };
-                image.yuv_range =
-                    if seq_hdr.color_range == 0 { YuvRange::Limited } else { YuvRange::Full };
-                image.chroma_sample_position = (seq_hdr.chr as u32).into();
-
-                image.color_primaries = (seq_hdr.pri as u16).into();
-                image.transfer_characteristics = (seq_hdr.trc as u16).into();
-                image.matrix_coefficients = (seq_hdr.mtrx as u16).into();
-
-                for plane in 0usize..image.yuv_format.plane_count() {
-                    let stride_index = if plane == 0 { 0 } else { 1 };
-                    image.row_bytes[plane] = dav1d_picture.stride[stride_index] as u32;
-                    image.planes[plane] = Some(Pixels::from_raw_pointer(
-                        dav1d_picture.data[plane] as *mut u8,
-                        image.depth as u32,
-                        image.height,
-                        image.row_bytes[plane],
-                    )?);
-                    image.image_owns_planes[plane] = false;
-                }
-                if image.yuv_format == PixelFormat::Yuv400 {
-                    // Clear left over chroma planes from previous frames.
-                    image.clear_chroma_planes();
-                }
-            }
-        }
+        self.picture_to_image(self.picture.unwrap_ref(), image, category)?;
         Ok(())
     }
+
+    fn get_next_image_grid(
+        &mut self,
+        _payloads: &[Vec<u8>],
+        _spatial_id: u8,
+        _grid_image_helper: &mut GridImageHelper,
+    ) -> AvifResult<()> {
+        Err(AvifError::NotImplemented)
+    }
 }
 
 impl Drop for Dav1d {
diff --git a/src/codecs/libgav1.rs b/src/codecs/libgav1.rs
index dc015ab..30acd95 100644
--- a/src/codecs/libgav1.rs
+++ b/src/codecs/libgav1.rs
@@ -14,7 +14,8 @@
 
 use crate::codecs::Decoder;
 use crate::codecs::DecoderConfig;
-use crate::decoder::Category;
+use crate::decoder::CodecChoice;
+use crate::decoder::GridImageHelper;
 use crate::image::Image;
 use crate::image::YuvRange;
 use crate::internal_utils::pixels::*;
@@ -36,6 +37,10 @@
 // unnecessary cast warnings.
 #[allow(clippy::unnecessary_cast)]
 impl Decoder for Libgav1 {
+    fn codec(&self) -> CodecChoice {
+        CodecChoice::Libgav1
+    }
+
     fn initialize(&mut self, config: &DecoderConfig) -> AvifResult<()> {
         if self.decoder.is_some() {
             return Ok(()); // Already initialized.
@@ -188,6 +193,15 @@
         }
         Ok(())
     }
+
+    fn get_next_image_grid(
+        &mut self,
+        _payloads: &[Vec<u8>],
+        _spatial_id: u8,
+        _grid_image_helper: &mut GridImageHelper,
+    ) -> AvifResult<()> {
+        Err(AvifError::NotImplemented)
+    }
 }
 
 impl Drop for Libgav1 {
diff --git a/src/codecs/mod.rs b/src/codecs/mod.rs
index e53eee3..eb42e57 100644
--- a/src/codecs/mod.rs
+++ b/src/codecs/mod.rs
@@ -21,11 +21,13 @@
 #[cfg(feature = "android_mediacodec")]
 pub mod android_mediacodec;
 
-use crate::decoder::Category;
+use crate::decoder::CodecChoice;
+use crate::decoder::GridImageHelper;
 use crate::image::Image;
 use crate::parser::mp4box::CodecConfiguration;
 use crate::AndroidMediaCodecOutputColorFormat;
 use crate::AvifResult;
+use crate::Category;
 
 use std::num::NonZero;
 
@@ -45,7 +47,9 @@
 }
 
 pub trait Decoder {
+    fn codec(&self) -> CodecChoice;
     fn initialize(&mut self, config: &DecoderConfig) -> AvifResult<()>;
+    // Decode a single image and write the output into |image|.
     fn get_next_image(
         &mut self,
         av1_payload: &[u8],
@@ -53,5 +57,12 @@
         image: &mut Image,
         category: Category,
     ) -> AvifResult<()>;
+    // Decode a list of input images and outputs them into the |grid_image_helper|.
+    fn get_next_image_grid(
+        &mut self,
+        payloads: &[Vec<u8>],
+        spatial_id: u8,
+        grid_image_helper: &mut GridImageHelper,
+    ) -> AvifResult<()>;
     // Destruction must be implemented using Drop.
 }
diff --git a/src/decoder/gainmap.rs b/src/decoder/gainmap.rs
index d1e50f2..7a2ece1 100644
--- a/src/decoder/gainmap.rs
+++ b/src/decoder/gainmap.rs
@@ -14,8 +14,7 @@
 
 use crate::decoder::Image;
 use crate::image::YuvRange;
-use crate::internal_utils::*;
-use crate::parser::mp4box::ContentLightLevelInformation;
+use crate::utils::*;
 use crate::*;
 
 #[derive(Debug, Default)]
@@ -30,6 +29,24 @@
     pub use_base_color_space: bool,
 }
 
+impl GainMapMetadata {
+    pub(crate) fn is_valid(&self) -> AvifResult<()> {
+        for i in 0..3 {
+            self.min[i].is_valid()?;
+            self.max[i].is_valid()?;
+            self.gamma[i].is_valid()?;
+            self.base_offset[i].is_valid()?;
+            self.alternate_offset[i].is_valid()?;
+            if self.max[i].as_f64()? < self.min[i].as_f64()? || self.gamma[i].0 == 0 {
+                return Err(AvifError::InvalidArgument);
+            }
+        }
+        self.base_hdr_headroom.is_valid()?;
+        self.alternate_hdr_headroom.is_valid()?;
+        Ok(())
+    }
+}
+
 #[derive(Default)]
 pub struct GainMap {
     pub image: Image,
diff --git a/src/decoder/item.rs b/src/decoder/item.rs
index 220c509..1f187ee 100644
--- a/src/decoder/item.rs
+++ b/src/decoder/item.rs
@@ -39,7 +39,10 @@
     pub has_unsupported_essential_property: bool,
     pub progressive: bool,
     pub idat: Vec<u8>,
-    pub derived_item_ids: Vec<u32>,
+    // Item ids of source items of a derived image item, in the same order as
+    // they appear in the `dimg` box. E.g. item ids for the cells of a grid
+    // item, or for the layers of an overlay item.
+    pub source_item_ids: Vec<u32>,
     pub data_buffer: Option<Vec<u8>>,
     pub is_made_up: bool, // Placeholder grid alpha item if true.
 }
@@ -114,101 +117,96 @@
         size_limit: Option<NonZero<u32>>,
         dimension_limit: Option<NonZero<u32>>,
     ) -> AvifResult<()> {
-        match self.item_type.as_str() {
-            "grid" => {
-                let mut stream = self.stream(io)?;
-                // unsigned int(8) version = 0;
-                let version = stream.read_u8()?;
-                if version != 0 {
-                    return Err(AvifError::InvalidImageGrid(
-                        "unsupported version for grid".into(),
-                    ));
-                }
-                // unsigned int(8) flags;
-                let flags = stream.read_u8()?;
-                // unsigned int(8) rows_minus_one;
-                grid.rows = stream.read_u8()? as u32 + 1;
-                // unsigned int(8) columns_minus_one;
-                grid.columns = stream.read_u8()? as u32 + 1;
-                if (flags & 1) == 1 {
-                    // unsigned int(32) output_width;
-                    grid.width = stream.read_u32()?;
-                    // unsigned int(32) output_height;
-                    grid.height = stream.read_u32()?;
-                } else {
-                    // unsigned int(16) output_width;
-                    grid.width = stream.read_u16()? as u32;
-                    // unsigned int(16) output_height;
-                    grid.height = stream.read_u16()? as u32;
-                }
-                Self::validate_derived_image_dimensions(
-                    grid.width,
-                    grid.height,
-                    size_limit,
-                    dimension_limit,
-                )?;
-                if stream.has_bytes_left()? {
-                    return Err(AvifError::InvalidImageGrid(
-                        "found unknown extra bytes in the grid box".into(),
-                    ));
-                }
-                Ok(())
+        if self.is_grid_item() {
+            let mut stream = self.stream(io)?;
+            // unsigned int(8) version = 0;
+            let version = stream.read_u8()?;
+            if version != 0 {
+                return Err(AvifError::InvalidImageGrid(
+                    "unsupported version for grid".into(),
+                ));
             }
-            "iovl" => {
-                let reference_count = self.derived_item_ids.len();
-                let mut stream = self.stream(io)?;
-                // unsigned int(8) version = 0;
-                let version = stream.read_u8()?;
-                if version != 0 {
-                    return Err(AvifError::InvalidImageGrid(format!(
-                        "unsupported version {version} for iovl"
-                    )));
-                }
-                // unsigned int(8) flags;
-                let flags = stream.read_u8()?;
-                for j in 0..4 {
-                    // unsigned int(16) canvas_fill_value;
-                    overlay.canvas_fill_value[j] = stream.read_u16()?;
-                }
-                if (flags & 1) == 1 {
-                    // unsigned int(32) output_width;
-                    overlay.width = stream.read_u32()?;
-                    // unsigned int(32) output_height;
-                    overlay.height = stream.read_u32()?;
-                } else {
-                    // unsigned int(16) output_width;
-                    overlay.width = stream.read_u16()? as u32;
-                    // unsigned int(16) output_height;
-                    overlay.height = stream.read_u16()? as u32;
-                }
-                Self::validate_derived_image_dimensions(
-                    overlay.width,
-                    overlay.height,
-                    size_limit,
-                    dimension_limit,
-                )?;
-                for _ in 0..reference_count {
-                    if (flags & 1) == 1 {
-                        // unsigned int(32) horizontal_offset;
-                        overlay.horizontal_offsets.push(stream.read_i32()?);
-                        // unsigned int(32) vertical_offset;
-                        overlay.vertical_offsets.push(stream.read_i32()?);
-                    } else {
-                        // unsigned int(16) horizontal_offset;
-                        overlay.horizontal_offsets.push(stream.read_i16()? as i32);
-                        // unsigned int(16) vertical_offset;
-                        overlay.vertical_offsets.push(stream.read_i16()? as i32);
-                    }
-                }
-                if stream.has_bytes_left()? {
-                    return Err(AvifError::InvalidImageGrid(
-                        "found unknown extra bytes in the iovl box".into(),
-                    ));
-                }
-                Ok(())
+            // unsigned int(8) flags;
+            let flags = stream.read_u8()?;
+            // unsigned int(8) rows_minus_one;
+            grid.rows = stream.read_u8()? as u32 + 1;
+            // unsigned int(8) columns_minus_one;
+            grid.columns = stream.read_u8()? as u32 + 1;
+            if (flags & 1) == 1 {
+                // unsigned int(32) output_width;
+                grid.width = stream.read_u32()?;
+                // unsigned int(32) output_height;
+                grid.height = stream.read_u32()?;
+            } else {
+                // unsigned int(16) output_width;
+                grid.width = stream.read_u16()? as u32;
+                // unsigned int(16) output_height;
+                grid.height = stream.read_u16()? as u32;
             }
-            _ => Ok(()),
+            Self::validate_derived_image_dimensions(
+                grid.width,
+                grid.height,
+                size_limit,
+                dimension_limit,
+            )?;
+            if stream.has_bytes_left()? {
+                return Err(AvifError::InvalidImageGrid(
+                    "found unknown extra bytes in the grid box".into(),
+                ));
+            }
+        } else if self.is_overlay_item() {
+            let reference_count = self.source_item_ids.len();
+            let mut stream = self.stream(io)?;
+            // unsigned int(8) version = 0;
+            let version = stream.read_u8()?;
+            if version != 0 {
+                return Err(AvifError::InvalidImageGrid(format!(
+                    "unsupported version {version} for iovl"
+                )));
+            }
+            // unsigned int(8) flags;
+            let flags = stream.read_u8()?;
+            for j in 0..4 {
+                // unsigned int(16) canvas_fill_value;
+                overlay.canvas_fill_value[j] = stream.read_u16()?;
+            }
+            if (flags & 1) == 1 {
+                // unsigned int(32) output_width;
+                overlay.width = stream.read_u32()?;
+                // unsigned int(32) output_height;
+                overlay.height = stream.read_u32()?;
+            } else {
+                // unsigned int(16) output_width;
+                overlay.width = stream.read_u16()? as u32;
+                // unsigned int(16) output_height;
+                overlay.height = stream.read_u16()? as u32;
+            }
+            Self::validate_derived_image_dimensions(
+                overlay.width,
+                overlay.height,
+                size_limit,
+                dimension_limit,
+            )?;
+            for _ in 0..reference_count {
+                if (flags & 1) == 1 {
+                    // unsigned int(32) horizontal_offset;
+                    overlay.horizontal_offsets.push(stream.read_i32()?);
+                    // unsigned int(32) vertical_offset;
+                    overlay.vertical_offsets.push(stream.read_i32()?);
+                } else {
+                    // unsigned int(16) horizontal_offset;
+                    overlay.horizontal_offsets.push(stream.read_i16()? as i32);
+                    // unsigned int(16) vertical_offset;
+                    overlay.vertical_offsets.push(stream.read_i16()? as i32);
+                }
+            }
+            if stream.has_bytes_left()? {
+                return Err(AvifError::InvalidImageGrid(
+                    "found unknown extra bytes in the iovl box".into(),
+                ));
+            }
         }
+        Ok(())
     }
 
     pub(crate) fn operating_point(&self) -> u8 {
@@ -270,8 +268,8 @@
         let codec_config = self
             .codec_config()
             .ok_or(AvifError::BmffParseFailed("missing av1C property".into()))?;
-        if self.item_type == "grid" || self.item_type == "iovl" {
-            for derived_item_id in &self.derived_item_ids {
+        if self.is_derived_image_item() {
+            for derived_item_id in &self.source_item_ids {
                 let derived_item = items.get(derived_item_id).unwrap();
                 let derived_codec_config =
                     derived_item
@@ -326,9 +324,8 @@
     }
 
     pub(crate) fn is_auxiliary_alpha(&self) -> bool {
-        matches!(find_property!(self.properties, AuxiliaryType),
-                 Some(aux_type) if aux_type == "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha" ||
-                                   aux_type == "urn:mpeg:hevc:2015:auxid:1")
+        matches!(find_property!(&self.properties, AuxiliaryType),
+                 Some(aux_type) if is_auxiliary_type_alpha(aux_type))
     }
 
     pub(crate) fn is_image_codec_item(&self) -> bool {
@@ -340,8 +337,21 @@
         .contains(&self.item_type.as_str())
     }
 
+    pub(crate) fn is_grid_item(&self) -> bool {
+        self.item_type == "grid"
+    }
+
+    pub(crate) fn is_overlay_item(&self) -> bool {
+        self.item_type == "iovl"
+    }
+
+    pub(crate) fn is_derived_image_item(&self) -> bool {
+        self.is_grid_item() || self.is_overlay_item() || self.is_tmap()
+    }
+
     pub(crate) fn is_image_item(&self) -> bool {
-        self.is_image_codec_item() || self.item_type == "grid" || self.item_type == "iovl"
+        // Adding || self.is_tmap() here would cause differences with libavif.
+        self.is_image_codec_item() || self.is_grid_item() || self.is_overlay_item()
     }
 
     pub(crate) fn should_skip(&self) -> bool {
diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs
index 60fe1d8..c9da492 100644
--- a/src/decoder/mod.rs
+++ b/src/decoder/mod.rs
@@ -66,7 +66,7 @@
 pub type GenericIO = Box<dyn IO>;
 pub type Codec = Box<dyn crate::codecs::Decoder>;
 
-#[derive(Debug, Default)]
+#[derive(Debug, Default, PartialEq)]
 pub enum CodecChoice {
     #[default]
     Auto,
@@ -317,32 +317,47 @@
     Heic = 1,
 }
 
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub enum Category {
-    #[default]
-    Color,
-    Alpha,
-    Gainmap,
+pub struct GridImageHelper<'a> {
+    grid: &'a Grid,
+    image: &'a mut Image,
+    pub(crate) category: Category,
+    cell_index: usize,
+    codec_config: &'a CodecConfiguration,
+    first_cell_image: Option<Image>,
 }
 
-impl Category {
-    const COUNT: usize = 3;
-    const ALL: [Category; Category::COUNT] = [Self::Color, Self::Alpha, Self::Gainmap];
-    const ALL_USIZE: [usize; Category::COUNT] = [0, 1, 2];
-
-    pub(crate) fn usize(self) -> usize {
-        match self {
-            Category::Color => 0,
-            Category::Alpha => 1,
-            Category::Gainmap => 2,
-        }
+// These functions are not used in all configurations.
+#[allow(unused)]
+impl GridImageHelper<'_> {
+    pub(crate) fn is_grid_complete(&self) -> AvifResult<bool> {
+        Ok(self.cell_index as u32 == checked_mul!(self.grid.rows, self.grid.columns)?)
     }
 
-    pub(crate) fn planes(&self) -> &[Plane] {
-        match self {
-            Category::Alpha => &A_PLANE,
-            _ => &YUV_PLANES,
+    pub(crate) fn copy_from_cell_image(&mut self, cell_image: &Image) -> AvifResult<()> {
+        if self.is_grid_complete()? {
+            return Ok(());
         }
+        if self.cell_index == 0 {
+            validate_grid_image_dimensions(cell_image, self.grid)?;
+            if self.category != Category::Alpha {
+                self.image.width = self.grid.width;
+                self.image.height = self.grid.height;
+                self.image
+                    .copy_properties_from(cell_image, self.codec_config);
+            }
+            self.image.allocate_planes(self.category)?;
+        } else if !cell_image.has_same_properties_and_cicp(self.first_cell_image.unwrap_ref()) {
+            return Err(AvifError::InvalidImageGrid(
+                "grid image contains mismatched tiles".into(),
+            ));
+        }
+        self.image
+            .copy_from_tile(cell_image, self.grid, self.cell_index as u32, self.category)?;
+        if self.cell_index == 0 {
+            self.first_cell_image = Some(cell_image.shallow_clone());
+        }
+        self.cell_index += 1;
+        Ok(())
     }
 }
 
@@ -418,13 +433,13 @@
         }) {
             return Ok(Some(*item.0));
         }
-        if color_item.item_type != "grid" || color_item.derived_item_ids.is_empty() {
+        if !color_item.is_grid_item() || color_item.source_item_ids.is_empty() {
             return Ok(None);
         }
         // If color item is a grid, check if there is an alpha channel which is represented as an
         // auxl item to each color tile item.
-        let mut alpha_item_indices: Vec<u32> = create_vec_exact(color_item.derived_item_ids.len())?;
-        for color_grid_item_id in &color_item.derived_item_ids {
+        let mut alpha_item_indices: Vec<u32> = create_vec_exact(color_item.source_item_ids.len())?;
+        for color_grid_item_id in &color_item.source_item_ids {
             match self
                 .items
                 .iter()
@@ -460,7 +475,7 @@
             item_type: String::from("grid"),
             width: color_item.width,
             height: color_item.height,
-            derived_item_ids: alpha_item_indices,
+            source_item_ids: alpha_item_indices,
             properties,
             is_made_up: true,
             ..Item::default()
@@ -596,7 +611,7 @@
             .items
             .get(&item_id)
             .ok_or(AvifError::MissingImageItem)?;
-        if item.derived_item_ids.is_empty() {
+        if item.source_item_ids.is_empty() {
             if item.size == 0 {
                 return Err(AvifError::MissingImageItem);
             }
@@ -609,15 +624,13 @@
             tile.input.category = category;
             tiles.push(tile);
         } else {
-            if !self.tile_info[category.usize()].is_grid()
-                && !self.tile_info[category.usize()].is_overlay()
-            {
+            if !self.tile_info[category.usize()].is_derived_image() {
                 return Err(AvifError::InvalidImageGrid(
-                    "dimg items were found but image is not grid or overlay.".into(),
+                    "dimg items were found but image is not a derived image.".into(),
                 ));
             }
             let mut progressive = true;
-            for derived_item_id in item.derived_item_ids.clone() {
+            for derived_item_id in item.source_item_ids.clone() {
                 let derived_item = self
                     .items
                     .get_mut(&derived_item_id)
@@ -679,11 +692,14 @@
         Ok(())
     }
 
-    fn populate_overlay_item_ids(&mut self, item_id: u32) -> AvifResult<()> {
-        if self.items.get(&item_id).unwrap().item_type != "iovl" {
+    // Populates the source item ids for a derived image item.
+    // These are the ids that are in the item's `dimg` box.
+    fn populate_source_item_ids(&mut self, item_id: u32) -> AvifResult<()> {
+        if !self.items.get(&item_id).unwrap().is_derived_image_item() {
             return Ok(());
         }
-        let mut overlay_item_ids: Vec<u32> = vec![];
+
+        let mut source_item_ids: Vec<u32> = vec![];
         let mut first_codec_config: Option<CodecConfiguration> = None;
         // Collect all the dimg items.
         for dimg_item_id in self.items.keys() {
@@ -699,7 +715,7 @@
             }
             if !dimg_item.is_image_codec_item() || dimg_item.has_unsupported_essential_property {
                 return Err(AvifError::InvalidImageGrid(
-                    "invalid input item in dimg grid".into(),
+                    "invalid input item in dimg".into(),
                 ));
             }
             if first_codec_config.is_none() {
@@ -714,86 +730,41 @@
                         .clone(),
                 );
             }
-            overlay_item_ids.push(*dimg_item_id);
+            source_item_ids.push(*dimg_item_id);
         }
         if first_codec_config.is_none() {
             // No derived images were found.
             return Ok(());
         }
-        // ISO/IEC 23008-12: The input images are listed in the order they are layered, i.e. the
-        // bottom-most input image first and the top-most input image last, in the
-        // SingleItemTypeReferenceBox of type 'dimg' for this derived image item within the
-        // ItemReferenceBox.
-        // Sort the overlay items by dimg_index. dimg_index is the order in which the items appear
-        // in the 'iref' box.
-        overlay_item_ids.sort_by_key(|k| self.items.get(k).unwrap().dimg_index);
+        // The order of derived item ids matters: sort them by dimg_index, which is the order that
+        // items appear in the 'iref' box.
+        source_item_ids.sort_by_key(|k| self.items.get(k).unwrap().dimg_index);
         let item = self.items.get_mut(&item_id).unwrap();
         item.properties.push(ItemProperty::CodecConfiguration(
             first_codec_config.unwrap(),
         ));
-        item.derived_item_ids = overlay_item_ids;
+        item.source_item_ids = source_item_ids;
         Ok(())
     }
 
-    fn populate_grid_item_ids(&mut self, item_id: u32, category: Category) -> AvifResult<()> {
-        if self.items.get(&item_id).unwrap().item_type != "grid" {
-            return Ok(());
-        }
-        let tile_count = self.tile_info[category.usize()].grid_tile_count()? as usize;
-        let mut grid_item_ids: Vec<u32> = create_vec_exact(tile_count)?;
-        let mut first_codec_config: Option<CodecConfiguration> = None;
-        // Collect all the dimg items.
-        for dimg_item_id in self.items.keys() {
-            if *dimg_item_id == item_id {
-                continue;
-            }
-            let dimg_item = self
-                .items
-                .get(dimg_item_id)
-                .ok_or(AvifError::InvalidImageGrid("".into()))?;
-            if dimg_item.dimg_for_id != item_id {
-                continue;
-            }
-            if !dimg_item.is_image_codec_item() || dimg_item.has_unsupported_essential_property {
-                return Err(AvifError::InvalidImageGrid(
-                    "invalid input item in dimg grid".into(),
-                ));
-            }
-            if first_codec_config.is_none() {
-                // Adopt the configuration property of the first tile.
-                // validate_properties() makes sure they are all equal.
-                first_codec_config = Some(
-                    dimg_item
-                        .codec_config()
-                        .ok_or(AvifError::BmffParseFailed(
-                            "missing codec config property".into(),
-                        ))?
-                        .clone(),
-                );
-            }
-            if grid_item_ids.len() >= tile_count {
+    fn validate_source_item_counts(&self, item_id: u32, tile_info: &TileInfo) -> AvifResult<()> {
+        let item = self.items.get(&item_id).unwrap();
+        if item.is_grid_item() {
+            let tile_count = tile_info.grid_tile_count()? as usize;
+            if item.source_item_ids.len() != tile_count {
                 return Err(AvifError::InvalidImageGrid(
                     "Expected number of tiles not found".into(),
                 ));
             }
-            grid_item_ids.push(*dimg_item_id);
-        }
-        if grid_item_ids.len() != tile_count {
-            return Err(AvifError::InvalidImageGrid(
-                "Expected number of tiles not found".into(),
+        } else if item.is_overlay_item() && item.source_item_ids.is_empty() {
+            return Err(AvifError::BmffParseFailed(
+                "No dimg items found for iovl".into(),
+            ));
+        } else if item.is_tmap() && item.source_item_ids.len() != 2 {
+            return Err(AvifError::InvalidToneMappedImage(
+                "Expected tmap to have 2 dimg items".into(),
             ));
         }
-        // ISO/IEC 23008-12: The input images are inserted in row-major order,
-        // top-row first, left to right, in the order of SingleItemTypeReferenceBox of type 'dimg'
-        // for this derived image item within the ItemReferenceBox.
-        // Sort the grid items by dimg_index. dimg_index is the order in which the items appear in
-        // the 'iref' box.
-        grid_item_ids.sort_by_key(|k| self.items.get(k).unwrap().dimg_index);
-        let item = self.items.get_mut(&item_id).unwrap();
-        item.properties.push(ItemProperty::CodecConfiguration(
-            first_codec_config.unwrap(),
-        ));
-        item.derived_item_ids = grid_item_ids;
         Ok(())
     }
 
@@ -836,10 +807,12 @@
             if !self.tracks.is_empty() {
                 self.image.image_sequence_track_present = true;
                 for track in &self.tracks {
-                    if !track.check_limits(
-                        self.settings.image_size_limit,
-                        self.settings.image_dimension_limit,
-                    ) {
+                    if track.is_video_handler()
+                        && !track.check_limits(
+                            self.settings.image_size_limit,
+                            self.settings.image_dimension_limit,
+                        )
+                    {
                         return Err(AvifError::BmffParseFailed(
                             "track dimension too large".into(),
                         ));
@@ -909,7 +882,11 @@
                 )?);
                 self.tile_info[Category::Color.usize()].tile_count = 1;
 
-                if let Some(alpha_track) = self.tracks.iter().find(|x| x.is_aux(color_track.id)) {
+                if let Some(alpha_track) = self
+                    .tracks
+                    .iter()
+                    .find(|x| x.is_aux(color_track.id) && x.is_auxiliary_alpha())
+                {
                     self.tiles[Category::Alpha.usize()].push(Tile::create_from_track(
                         alpha_track,
                         self.settings.image_count_limit,
@@ -1038,8 +1015,10 @@
                 let color_item = self.items.get(&item_ids[Category::Color.usize()]).unwrap();
                 self.image.width = color_item.width;
                 self.image.height = color_item.height;
-                self.image.alpha_present = item_ids[Category::Alpha.usize()] != 0;
-                // alphapremultiplied.
+                let alpha_item_id = item_ids[Category::Alpha.usize()];
+                self.image.alpha_present = alpha_item_id != 0;
+                self.image.alpha_premultiplied =
+                    alpha_item_id != 0 && color_item.prem_by_id == alpha_item_id;
 
                 if color_item.progressive {
                     self.image.progressive_state = ProgressiveState::Available;
@@ -1170,7 +1149,7 @@
         if item_id == 0 {
             return Ok(());
         }
-        self.populate_overlay_item_ids(item_id)?;
+        self.populate_source_item_ids(item_id)?;
         self.items.get_mut(&item_id).unwrap().read_and_parse(
             self.io.unwrap_mut(),
             &mut self.tile_info[category.usize()].grid,
@@ -1178,7 +1157,7 @@
             self.settings.image_size_limit,
             self.settings.image_dimension_limit,
         )?;
-        self.populate_grid_item_ids(item_id, category)
+        self.validate_source_item_counts(item_id, &self.tile_info[category.usize()])
     }
 
     fn can_use_single_codec(&self) -> AvifResult<bool> {
@@ -1348,65 +1327,6 @@
         Ok(())
     }
 
-    fn validate_grid_image_dimensions(image: &Image, grid: &Grid) -> AvifResult<()> {
-        if checked_mul!(image.width, grid.columns)? < grid.width
-            || checked_mul!(image.height, grid.rows)? < grid.height
-        {
-            return Err(AvifError::InvalidImageGrid(
-                        "Grid image tiles do not completely cover the image (HEIF (ISO/IEC 23008-12:2017), Section 6.6.2.3.1)".into(),
-                    ));
-        }
-        if checked_mul!(image.width, grid.columns)? < grid.width
-            || checked_mul!(image.height, grid.rows)? < grid.height
-        {
-            return Err(AvifError::InvalidImageGrid(
-                "Grid image tiles do not completely cover the image (HEIF (ISO/IEC 23008-12:2017), \
-                    Section 6.6.2.3.1)"
-                    .into(),
-            ));
-        }
-        if checked_mul!(image.width, grid.columns - 1)? >= grid.width
-            || checked_mul!(image.height, grid.rows - 1)? >= grid.height
-        {
-            return Err(AvifError::InvalidImageGrid(
-                "Grid image tiles in the rightmost column and bottommost row do not overlap the \
-                     reconstructed image grid canvas. See MIAF (ISO/IEC 23000-22:2019), Section \
-                     7.3.11.4.2, Figure 2"
-                    .into(),
-            ));
-        }
-        // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
-        //   - the tile_width shall be greater than or equal to 64, and should be a multiple of 64
-        //   - the tile_height shall be greater than or equal to 64, and should be a multiple of 64
-        // The "should" part is ignored here.
-        if image.width < 64 || image.height < 64 {
-            return Err(AvifError::InvalidImageGrid(format!(
-                "Grid image tile width ({}) or height ({}) cannot be smaller than 64. See MIAF \
-                     (ISO/IEC 23000-22:2019), Section 7.3.11.4.2",
-                image.width, image.height
-            )));
-        }
-        // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
-        //   - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets
-        //     and widths, and the output width, shall be even numbers;
-        //   - when the images are in the 4:2:0 chroma sampling format both the horizontal and
-        //     vertical tile offsets and widths, and the output width and height, shall be even
-        //     numbers.
-        if ((image.yuv_format == PixelFormat::Yuv420 || image.yuv_format == PixelFormat::Yuv422)
-            && (grid.width % 2 != 0 || image.width % 2 != 0))
-            || (image.yuv_format == PixelFormat::Yuv420
-                && (grid.height % 2 != 0 || image.height % 2 != 0))
-        {
-            return Err(AvifError::InvalidImageGrid(format!(
-                "Grid image width ({}) or height ({}) or tile width ({}) or height ({}) shall be \
-                    even if chroma is subsampled in that dimension. See MIAF \
-                    (ISO/IEC 23000-22:2019), Section 7.3.11.4.2",
-                grid.width, grid.height, image.width, image.height
-            )));
-        }
-        Ok(())
-    }
-
     fn decode_tile(
         &mut self,
         image_index: usize,
@@ -1454,12 +1374,13 @@
         if self.tile_info[category.usize()].is_grid() {
             if tile_index == 0 {
                 let grid = &self.tile_info[category.usize()].grid;
-                Self::validate_grid_image_dimensions(&tile.image, grid)?;
+                validate_grid_image_dimensions(&tile.image, grid)?;
                 match category {
                     Category::Color => {
                         self.image.width = grid.width;
                         self.image.height = grid.height;
-                        self.image.copy_properties_from(tile);
+                        self.image
+                            .copy_properties_from(&tile.image, &tile.codec_config);
                         self.image.allocate_planes(category)?;
                     }
                     Category::Alpha => {
@@ -1470,39 +1391,33 @@
                     Category::Gainmap => {
                         self.gainmap.image.width = grid.width;
                         self.gainmap.image.height = grid.height;
-                        self.gainmap.image.copy_properties_from(tile);
+                        self.gainmap
+                            .image
+                            .copy_properties_from(&tile.image, &tile.codec_config);
                         self.gainmap.image.allocate_planes(category)?;
                     }
                 }
             }
-            if !tiles_slice1.is_empty() {
-                let first_tile_image = &tiles_slice1[0].image;
-                if tile.image.width != first_tile_image.width
-                    || tile.image.height != first_tile_image.height
-                    || tile.image.depth != first_tile_image.depth
-                    || tile.image.yuv_format != first_tile_image.yuv_format
-                    || tile.image.yuv_range != first_tile_image.yuv_range
-                    || tile.image.color_primaries != first_tile_image.color_primaries
-                    || tile.image.transfer_characteristics
-                        != first_tile_image.transfer_characteristics
-                    || tile.image.matrix_coefficients != first_tile_image.matrix_coefficients
-                {
-                    return Err(AvifError::InvalidImageGrid(
-                        "grid image contains mismatched tiles".into(),
-                    ));
-                }
+            if !tiles_slice1.is_empty()
+                && !tile
+                    .image
+                    .has_same_properties_and_cicp(&tiles_slice1[0].image)
+            {
+                return Err(AvifError::InvalidImageGrid(
+                    "grid image contains mismatched tiles".into(),
+                ));
             }
             match category {
                 Category::Gainmap => self.gainmap.image.copy_from_tile(
                     &tile.image,
-                    &self.tile_info[category.usize()],
+                    &self.tile_info[category.usize()].grid,
                     tile_index as u32,
                     category,
                 )?,
                 _ => {
                     self.image.copy_from_tile(
                         &tile.image,
-                        &self.tile_info[category.usize()],
+                        &self.tile_info[category.usize()].grid,
                         tile_index as u32,
                         category,
                     )?;
@@ -1517,7 +1432,8 @@
                     Category::Color => {
                         self.image.width = overlay.width;
                         self.image.height = overlay.height;
-                        self.image.copy_properties_from(tile);
+                        self.image
+                            .copy_properties_from(&tile.image, &tile.codec_config);
                         self.image
                             .allocate_planes_with_default_values(category, canvas_fill_values)?;
                     }
@@ -1530,7 +1446,9 @@
                     Category::Gainmap => {
                         self.gainmap.image.width = overlay.width;
                         self.gainmap.image.height = overlay.height;
-                        self.gainmap.image.copy_properties_from(tile);
+                        self.gainmap
+                            .image
+                            .copy_properties_from(&tile.image, &tile.codec_config);
                         self.gainmap
                             .image
                             .allocate_planes_with_default_values(category, canvas_fill_values)?;
@@ -1576,7 +1494,8 @@
                 Category::Color => {
                     self.image.width = tile.image.width;
                     self.image.height = tile.image.height;
-                    self.image.copy_properties_from(tile);
+                    self.image
+                        .copy_properties_from(&tile.image, &tile.codec_config);
                     self.image
                         .steal_or_copy_planes_from(&tile.image, category)?;
                 }
@@ -1590,7 +1509,9 @@
                 Category::Gainmap => {
                     self.gainmap.image.width = tile.image.width;
                     self.gainmap.image.height = tile.image.height;
-                    self.gainmap.image.copy_properties_from(tile);
+                    self.gainmap
+                        .image
+                        .copy_properties_from(&tile.image, &tile.codec_config);
                     self.gainmap
                         .image
                         .steal_or_copy_planes_from(&tile.image, category)?;
@@ -1600,15 +1521,95 @@
         Ok(())
     }
 
+    fn decode_grid(&mut self, image_index: usize, category: Category) -> AvifResult<()> {
+        let tile_count = self.tiles[category.usize()].len();
+        if tile_count == 0 {
+            return Ok(());
+        }
+        let previous_decoded_tile_count =
+            self.tile_info[category.usize()].decoded_tile_count as usize;
+        let mut payloads = vec![];
+        for tile_index in previous_decoded_tile_count..tile_count {
+            let tile = &self.tiles[category.usize()][tile_index];
+            let sample = &tile.input.samples[image_index];
+            let item_data_buffer = if sample.item_id == 0 {
+                &None
+            } else {
+                &self.items.get(&sample.item_id).unwrap().data_buffer
+            };
+            let io = &mut self.io.unwrap_mut();
+            let data = sample.data(io, item_data_buffer)?;
+            payloads.push(data.to_vec());
+        }
+        let grid = &self.tile_info[category.usize()].grid;
+        if checked_mul!(grid.rows, grid.columns)? != payloads.len() as u32 {
+            return Err(AvifError::InvalidArgument);
+        }
+        let first_tile = &self.tiles[category.usize()][previous_decoded_tile_count];
+        let mut grid_image_helper = GridImageHelper {
+            grid,
+            image: if category == Category::Gainmap {
+                &mut self.gainmap.image
+            } else {
+                &mut self.image
+            },
+            category,
+            cell_index: 0,
+            codec_config: &first_tile.codec_config,
+            first_cell_image: None,
+        };
+        let codec = &mut self.codecs[first_tile.codec_index];
+        let next_image_result = codec.get_next_image_grid(
+            &payloads,
+            first_tile.input.samples[image_index].spatial_id,
+            &mut grid_image_helper,
+        );
+        if next_image_result.is_err() {
+            if cfg!(feature = "android_mediacodec")
+                && cfg!(feature = "heic")
+                && first_tile.codec_config.is_heic()
+                && category == Category::Alpha
+            {
+                // When decoding HEIC on Android, if the alpha channel decoding fails, simply
+                // ignore it and return the rest of the image.
+            } else {
+                return next_image_result;
+            }
+        }
+        if !grid_image_helper.is_grid_complete()? {
+            return Err(AvifError::UnknownError(
+                "codec did not decode all cells".into(),
+            ));
+        }
+        checked_incr!(
+            self.tile_info[category.usize()].decoded_tile_count,
+            u32_from_usize(payloads.len())?
+        );
+        Ok(())
+    }
+
     fn decode_tiles(&mut self, image_index: usize) -> AvifResult<()> {
         let mut decoded_something = false;
         for category in self.settings.image_content_to_decode.categories() {
-            let previous_decoded_tile_count =
-                self.tile_info[category.usize()].decoded_tile_count as usize;
             let tile_count = self.tiles[category.usize()].len();
-            for tile_index in previous_decoded_tile_count..tile_count {
-                self.decode_tile(image_index, category, tile_index)?;
+            if tile_count == 0 {
+                continue;
+            }
+            let first_tile = &self.tiles[category.usize()][0];
+            let codec = self.codecs[first_tile.codec_index].codec();
+            if codec == CodecChoice::MediaCodec
+                && !self.settings.allow_incremental
+                && self.tile_info[category.usize()].is_grid()
+            {
+                self.decode_grid(image_index, category)?;
                 decoded_something = true;
+            } else {
+                let previous_decoded_tile_count =
+                    self.tile_info[category.usize()].decoded_tile_count as usize;
+                for tile_index in previous_decoded_tile_count..tile_count {
+                    self.decode_tile(image_index, category, tile_index)?;
+                    decoded_something = true;
+                }
             }
         }
         if decoded_something {
diff --git a/src/decoder/tile.rs b/src/decoder/tile.rs
index a0c849c..4dd1776 100644
--- a/src/decoder/tile.rs
+++ b/src/decoder/tile.rs
@@ -17,8 +17,6 @@
 
 use std::num::NonZero;
 
-pub const MAX_AV1_LAYER_COUNT: usize = 4;
-
 #[derive(Debug, Default)]
 pub struct DecodeSample {
     pub item_id: u32, // 1-based. 0 if it comes from a track.
@@ -70,14 +68,6 @@
     pub category: Category,
 }
 
-#[derive(Clone, Copy, Debug, Default)]
-pub struct Grid {
-    pub rows: u32,
-    pub columns: u32,
-    pub width: u32,
-    pub height: u32,
-}
-
 #[derive(Debug, Default)]
 pub struct Overlay {
     pub canvas_fill_value: [u16; 4],
@@ -88,7 +78,7 @@
 }
 
 #[derive(Debug, Default)]
-pub struct TileInfo {
+pub(crate) struct TileInfo {
     pub tile_count: u32,
     pub decoded_tile_count: u32,
     pub grid: Grid,
@@ -104,6 +94,10 @@
         !self.overlay.horizontal_offsets.is_empty() && !self.overlay.vertical_offsets.is_empty()
     }
 
+    pub(crate) fn is_derived_image(&self) -> bool {
+        self.is_grid() || self.is_overlay()
+    }
+
     pub(crate) fn grid_tile_count(&self) -> AvifResult<u32> {
         if self.is_grid() {
             checked_mul!(self.grid.rows, self.grid.columns)
diff --git a/src/decoder/track.rs b/src/decoder/track.rs
index b818e01..f4b3eed 100644
--- a/src/decoder/track.rs
+++ b/src/decoder/track.rs
@@ -47,6 +47,7 @@
     pub sample_table: Option<SampleTable>,
     pub elst_seen: bool,
     pub meta: Option<MetaBox>,
+    pub handler_type: String,
 }
 
 impl Track {
@@ -65,13 +66,31 @@
             false
         }
     }
+    pub(crate) fn is_video_handler(&self) -> bool {
+        // Handler types known to be associated with video content.
+        self.handler_type == "pict" || self.handler_type == "vide" || self.handler_type == "auxv"
+    }
     pub(crate) fn is_aux(&self, primary_track_id: u32) -> bool {
+        // Do not check the track's handler_type. It should be "auxv" according to
+        // HEIF (ISO/IEC 23008-12:2022), Section 7.5.3.1, but old versions of libavif used to write
+        // "pict" instead.
         self.has_av1_samples() && self.aux_for_id == Some(primary_track_id)
     }
     pub(crate) fn is_color(&self) -> bool {
+        // Do not check the track's handler_type. It should be "pict" according to
+        // HEIF (ISO/IEC 23008-12:2022), Section 7 but some existing files might be using "vide".
         self.has_av1_samples() && self.aux_for_id.is_none()
     }
 
+    pub(crate) fn is_auxiliary_alpha(&self) -> bool {
+        if let Some(properties) = self.get_properties() {
+            if let Some(aux_type) = &find_property!(properties, AuxiliaryType) {
+                return is_auxiliary_type_alpha(aux_type);
+            }
+        }
+        true // Assume alpha if no type is present
+    }
+
     pub(crate) fn get_properties(&self) -> Option<&Vec<ItemProperty>> {
         self.sample_table.as_ref()?.get_properties()
     }
diff --git a/src/image.rs b/src/image.rs
index 2e4ae5e..266210d 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -12,13 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::decoder::tile::Tile;
 use crate::decoder::tile::TileInfo;
-use crate::decoder::Category;
 use crate::decoder::ProgressiveState;
 use crate::internal_utils::pixels::*;
 use crate::internal_utils::*;
-use crate::parser::mp4box::*;
+use crate::parser::mp4box::CodecConfiguration;
 use crate::reformat::coeffs::*;
 use crate::utils::clap::CleanAperture;
 use crate::*;
@@ -118,6 +116,33 @@
 }
 
 impl Image {
+    pub(crate) fn shallow_clone(&self) -> Self {
+        Self {
+            width: self.width,
+            height: self.height,
+            depth: self.depth,
+            yuv_format: self.yuv_format,
+            yuv_range: self.yuv_range,
+            chroma_sample_position: self.chroma_sample_position,
+            alpha_present: self.alpha_present,
+            alpha_premultiplied: self.alpha_premultiplied,
+            color_primaries: self.color_primaries,
+            transfer_characteristics: self.transfer_characteristics,
+            matrix_coefficients: self.matrix_coefficients,
+            clli: self.clli,
+            pasp: self.pasp,
+            clap: self.clap,
+            irot_angle: self.irot_angle,
+            imir_axis: self.imir_axis,
+            exif: self.exif.clone(),
+            icc: self.icc.clone(),
+            xmp: self.xmp.clone(),
+            image_sequence_track_present: self.image_sequence_track_present,
+            progressive_state: self.progressive_state,
+            ..Default::default()
+        }
+    }
+
     pub(crate) fn depth_valid(&self) -> bool {
         matches!(self.depth, 8 | 10 | 12 | 16)
     }
@@ -150,6 +175,20 @@
         self.width == other.width && self.height == other.height && self.depth == other.depth
     }
 
+    fn has_same_cicp(&self, other: &Image) -> bool {
+        self.depth == other.depth
+            && self.yuv_format == other.yuv_format
+            && self.yuv_range == other.yuv_range
+            && self.chroma_sample_position == other.chroma_sample_position
+            && self.color_primaries == other.color_primaries
+            && self.transfer_characteristics == other.transfer_characteristics
+            && self.matrix_coefficients == other.matrix_coefficients
+    }
+
+    pub(crate) fn has_same_properties_and_cicp(&self, other: &Image) -> bool {
+        self.has_same_properties(other) && self.has_same_cicp(other)
+    }
+
     pub fn width(&self, plane: Plane) -> usize {
         match plane {
             Plane::Y | Plane::A => self.width as usize,
@@ -296,16 +335,20 @@
         self.allocate_planes_with_default_values(category, [0, 0, 0, self.max_channel()])
     }
 
-    pub(crate) fn copy_properties_from(&mut self, tile: &Tile) {
-        self.yuv_format = tile.image.yuv_format;
-        self.depth = tile.image.depth;
-        if cfg!(feature = "heic") && tile.codec_config.is_heic() {
+    pub(crate) fn copy_properties_from(
+        &mut self,
+        image: &Image,
+        codec_config: &CodecConfiguration,
+    ) {
+        self.yuv_format = image.yuv_format;
+        self.depth = image.depth;
+        if cfg!(feature = "heic") && codec_config.is_heic() {
             // For AVIF, the information in the `colr` box takes precedence over what is reported
             // by the decoder. For HEIC, we always honor what is reported by the decoder.
-            self.yuv_range = tile.image.yuv_range;
-            self.color_primaries = tile.image.color_primaries;
-            self.transfer_characteristics = tile.image.transfer_characteristics;
-            self.matrix_coefficients = tile.image.matrix_coefficients;
+            self.yuv_range = image.yuv_range;
+            self.color_primaries = image.color_primaries;
+            self.transfer_characteristics = image.transfer_characteristics;
+            self.matrix_coefficients = image.matrix_coefficients;
         }
     }
 
@@ -330,13 +373,13 @@
     pub(crate) fn copy_from_tile(
         &mut self,
         tile: &Image,
-        tile_info: &TileInfo,
+        grid: &Grid,
         tile_index: u32,
         category: Category,
     ) -> AvifResult<()> {
         // This function is used only when |tile| contains pointers and self contains buffers.
-        let row_index = tile_index / tile_info.grid.columns;
-        let column_index = tile_index % tile_info.grid.columns;
+        let row_index = tile_index / grid.columns;
+        let column_index = tile_index % grid.columns;
         for plane in category.planes() {
             let plane = *plane;
             let src_plane = tile.plane_data(plane);
@@ -345,7 +388,7 @@
             }
             let src_plane = src_plane.unwrap();
             // If this is the last tile column, clamp to left over width.
-            let src_width_to_copy = if column_index == tile_info.grid.columns - 1 {
+            let src_width_to_copy = if column_index == grid.columns - 1 {
                 let width_so_far = checked_mul!(src_plane.width, column_index)?;
                 checked_sub!(self.width(plane), usize_from_u32(width_so_far)?)?
             } else {
@@ -353,7 +396,7 @@
             };
 
             // If this is the last tile row, clamp to left over height.
-            let src_height_to_copy = if row_index == tile_info.grid.rows - 1 {
+            let src_height_to_copy = if row_index == grid.rows - 1 {
                 let height_so_far = checked_mul!(src_plane.height, row_index)?;
                 checked_sub!(u32_from_usize(self.height(plane))?, height_so_far)?
             } else {
diff --git a/src/internal_utils/mod.rs b/src/internal_utils/mod.rs
index 7b971df..aaefe6b 100644
--- a/src/internal_utils/mod.rs
+++ b/src/internal_utils/mod.rs
@@ -17,24 +17,12 @@
 pub mod stream;
 
 use crate::parser::mp4box::*;
+use crate::utils::*;
 use crate::*;
 
 use std::num::NonZero;
 use std::ops::Range;
 
-// Some HEIF fractional fields can be negative, hence Fraction and UFraction.
-// The denominator is always unsigned.
-
-/// cbindgen:field-names=[n,d]
-#[derive(Clone, Copy, Debug, Default)]
-#[repr(C)]
-pub struct Fraction(pub i32, pub u32);
-
-/// cbindgen:field-names=[n,d]
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-#[repr(C)]
-pub struct UFraction(pub u32, pub u32);
-
 // 'clap' fractions do not follow this pattern: both numerators and denominators
 // are used as i32, but they are signalled as u32 according to the specification
 // as of 2024. This may be fixed in later versions of the specification, see
@@ -296,3 +284,67 @@
     }
     Ok(())
 }
+
+pub(crate) fn is_auxiliary_type_alpha(aux_type: &str) -> bool {
+    aux_type == "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"
+        || aux_type == "urn:mpeg:hevc:2015:auxid:1"
+}
+
+pub(crate) fn validate_grid_image_dimensions(image: &Image, grid: &Grid) -> AvifResult<()> {
+    if checked_mul!(image.width, grid.columns)? < grid.width
+        || checked_mul!(image.height, grid.rows)? < grid.height
+    {
+        return Err(AvifError::InvalidImageGrid(
+                        "Grid image tiles do not completely cover the image (HEIF (ISO/IEC 23008-12:2017), Section 6.6.2.3.1)".into(),
+                    ));
+    }
+    if checked_mul!(image.width, grid.columns)? < grid.width
+        || checked_mul!(image.height, grid.rows)? < grid.height
+    {
+        return Err(AvifError::InvalidImageGrid(
+            "Grid image tiles do not completely cover the image (HEIF (ISO/IEC 23008-12:2017), \
+                    Section 6.6.2.3.1)"
+                .into(),
+        ));
+    }
+    if checked_mul!(image.width, grid.columns - 1)? >= grid.width
+        || checked_mul!(image.height, grid.rows - 1)? >= grid.height
+    {
+        return Err(AvifError::InvalidImageGrid(
+            "Grid image tiles in the rightmost column and bottommost row do not overlap the \
+                     reconstructed image grid canvas. See MIAF (ISO/IEC 23000-22:2019), Section \
+                     7.3.11.4.2, Figure 2"
+                .into(),
+        ));
+    }
+    // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
+    //   - the tile_width shall be greater than or equal to 64, and should be a multiple of 64
+    //   - the tile_height shall be greater than or equal to 64, and should be a multiple of 64
+    // The "should" part is ignored here.
+    if image.width < 64 || image.height < 64 {
+        return Err(AvifError::InvalidImageGrid(format!(
+            "Grid image tile width ({}) or height ({}) cannot be smaller than 64. See MIAF \
+                     (ISO/IEC 23000-22:2019), Section 7.3.11.4.2",
+            image.width, image.height
+        )));
+    }
+    // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
+    //   - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets
+    //     and widths, and the output width, shall be even numbers;
+    //   - when the images are in the 4:2:0 chroma sampling format both the horizontal and
+    //     vertical tile offsets and widths, and the output width and height, shall be even
+    //     numbers.
+    if ((image.yuv_format == PixelFormat::Yuv420 || image.yuv_format == PixelFormat::Yuv422)
+        && (grid.width % 2 != 0 || image.width % 2 != 0))
+        || (image.yuv_format == PixelFormat::Yuv420
+            && (grid.height % 2 != 0 || image.height % 2 != 0))
+    {
+        return Err(AvifError::InvalidImageGrid(format!(
+            "Grid image width ({}) or height ({}) or tile width ({}) or height ({}) shall be \
+                    even if chroma is subsampled in that dimension. See MIAF \
+                    (ISO/IEC 23000-22:2019), Section 7.3.11.4.2",
+            grid.width, grid.height, image.width, image.height
+        )));
+    }
+    Ok(())
+}
diff --git a/src/lib.rs b/src/lib.rs
index c3f239f..74202a0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #![deny(unsafe_op_in_unsafe_fn)]
+#![cfg_attr(feature = "disable_cfi", feature(no_sanitize))]
 
 #[macro_use]
 mod internal_utils;
@@ -30,6 +31,8 @@
 
 mod parser;
 
+use image::*;
+
 // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1516634.
 #[derive(Default)]
 pub struct NonRandomHasherState;
@@ -409,3 +412,66 @@
 pub(crate) use checked_incr;
 pub(crate) use checked_mul;
 pub(crate) use checked_sub;
+
+#[derive(Clone, Copy, Debug, Default)]
+pub struct Grid {
+    pub rows: u32,
+    pub columns: u32,
+    pub width: u32,
+    pub height: u32,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub enum Category {
+    #[default]
+    Color,
+    Alpha,
+    Gainmap,
+}
+
+impl Category {
+    const COUNT: usize = 3;
+    const ALL: [Category; Category::COUNT] = [Self::Color, Self::Alpha, Self::Gainmap];
+    const ALL_USIZE: [usize; Category::COUNT] = [0, 1, 2];
+
+    pub(crate) fn usize(self) -> usize {
+        match self {
+            Category::Color => 0,
+            Category::Alpha => 1,
+            Category::Gainmap => 2,
+        }
+    }
+
+    pub fn planes(&self) -> &[Plane] {
+        match self {
+            Category::Alpha => &A_PLANE,
+            _ => &YUV_PLANES,
+        }
+    }
+}
+
+/// cbindgen:rename-all=CamelCase
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+#[repr(C)]
+pub struct PixelAspectRatio {
+    pub h_spacing: u32,
+    pub v_spacing: u32,
+}
+
+/// cbindgen:field-names=[maxCLL, maxPALL]
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct ContentLightLevelInformation {
+    pub max_cll: u16,
+    pub max_pall: u16,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct Nclx {
+    pub color_primaries: ColorPrimaries,
+    pub transfer_characteristics: TransferCharacteristics,
+    pub matrix_coefficients: MatrixCoefficients,
+    pub yuv_range: YuvRange,
+}
+
+pub const MAX_AV1_LAYER_COUNT: usize = 4;
diff --git a/src/parser/mp4box.rs b/src/parser/mp4box.rs
index 058641e..b8c4d4b 100644
--- a/src/parser/mp4box.rs
+++ b/src/parser/mp4box.rs
@@ -260,14 +260,6 @@
     }
 }
 
-#[derive(Clone, Debug, Default)]
-pub struct Nclx {
-    pub color_primaries: ColorPrimaries,
-    pub transfer_characteristics: TransferCharacteristics,
-    pub matrix_coefficients: MatrixCoefficients,
-    pub yuv_range: YuvRange,
-}
-
 #[derive(Clone, Debug)]
 pub enum ColorInformation {
     Icc(Vec<u8>),
@@ -275,22 +267,6 @@
     Unknown,
 }
 
-/// cbindgen:rename-all=CamelCase
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-#[repr(C)]
-pub struct PixelAspectRatio {
-    pub h_spacing: u32,
-    pub v_spacing: u32,
-}
-
-/// cbindgen:field-names=[maxCLL, maxPALL]
-#[repr(C)]
-#[derive(Clone, Copy, Debug, Default)]
-pub struct ContentLightLevelInformation {
-    pub max_cll: u16,
-    pub max_pall: u16,
-}
-
 #[derive(Clone, Debug, PartialEq)]
 pub enum CodecConfiguration {
     Av1(Av1CodecConfiguration),
@@ -459,7 +435,7 @@
     })
 }
 
-fn parse_hdlr(stream: &mut IStream) -> AvifResult<()> {
+fn parse_hdlr(stream: &mut IStream) -> AvifResult<String> {
     // Section 8.4.3.2 of ISO/IEC 14496-12.
     let (_version, _flags) = stream.read_and_enforce_version_and_flags(0)?;
     // unsigned int(32) pre_defined = 0;
@@ -471,16 +447,6 @@
     }
     // unsigned int(32) handler_type;
     let handler_type = stream.read_string(4)?;
-    if handler_type != "pict" {
-        // Section 6.2 of ISO/IEC 23008-12:
-        //   The handler type for the MetaBox shall be 'pict'.
-        // https://aomediacodec.github.io/av1-avif/v1.1.0.html#image-sequences does not apply
-        // because this function is only called for the MetaBox but it would work too:
-        //   The track handler for an AV1 Image Sequence shall be pict.
-        return Err(AvifError::BmffParseFailed(
-            "Box[hdlr] handler_type is not 'pict'".into(),
-        ));
-    }
     // const unsigned int(32)[3] reserved = 0;
     if stream.read_u32()? != 0 || stream.read_u32()? != 0 || stream.read_u32()? != 0 {
         return Err(AvifError::BmffParseFailed(
@@ -492,7 +458,7 @@
     //   name gives a human-readable name for the track type (for debugging and inspection
     //   purposes).
     stream.read_c_string()?;
-    Ok(())
+    Ok(handler_type)
 }
 
 fn parse_iloc(stream: &mut IStream) -> AvifResult<ItemLocationBox> {
@@ -980,7 +946,7 @@
     Ok(ItemProperty::ContentLightLevelInformation(clli))
 }
 
-fn parse_ipco(stream: &mut IStream) -> AvifResult<Vec<ItemProperty>> {
+fn parse_ipco(stream: &mut IStream, is_track: bool) -> AvifResult<Vec<ItemProperty>> {
     // Section 8.11.14.2 of ISO/IEC 14496-12.
     let mut properties: Vec<ItemProperty> = Vec::new();
     while stream.has_bytes_left()? {
@@ -992,7 +958,8 @@
             "av1C" => properties.push(parse_av1C(&mut sub_stream)?),
             "colr" => properties.push(parse_colr(&mut sub_stream)?),
             "pasp" => properties.push(parse_pasp(&mut sub_stream)?),
-            "auxC" => properties.push(parse_auxC(&mut sub_stream)?),
+            "auxC" if !is_track => properties.push(parse_auxC(&mut sub_stream)?),
+            "auxi" if is_track => properties.push(parse_auxC(&mut sub_stream)?),
             "clap" => properties.push(parse_clap(&mut sub_stream)?),
             "irot" => properties.push(parse_irot(&mut sub_stream)?),
             "imir" => properties.push(parse_imir(&mut sub_stream)?),
@@ -1072,7 +1039,7 @@
     // Parse ipco box.
     {
         let mut sub_stream = stream.sub_stream(&header.size)?;
-        iprp.properties = parse_ipco(&mut sub_stream)?;
+        iprp.properties = parse_ipco(&mut sub_stream, /*is_track=*/ false)?;
     }
     // Parse ipma boxes.
     while stream.has_bytes_left()? {
@@ -1240,7 +1207,17 @@
                 "first box in meta is not hdlr".into(),
             ));
         }
-        parse_hdlr(&mut stream.sub_stream(&header.size)?)?;
+        let handler_type = parse_hdlr(&mut stream.sub_stream(&header.size)?)?;
+        if handler_type != "pict" {
+            // Section 6.2 of ISO/IEC 23008-12:
+            //   The handler type for the MetaBox shall be 'pict'.
+            // https://aomediacodec.github.io/av1-avif/v1.1.0.html#image-sequences does not apply
+            // because this function is only called for the MetaBox but it would work too:
+            //   The track handler for an AV1 Image Sequence shall be pict.
+            return Err(AvifError::BmffParseFailed(
+                "Box[hdlr] handler_type is not 'pict'".into(),
+            ));
+        }
     }
 
     let mut boxes_seen: HashSet<String> = HashSet::with_hasher(NonRandomHasherState);
@@ -1339,11 +1316,6 @@
     // unsigned int(32) height;
     track.height = stream.read_u32()? >> 16;
 
-    if track.width == 0 || track.height == 0 {
-        return Err(AvifError::BmffParseFailed(
-            "invalid track dimensions".into(),
-        ));
-    }
     Ok(())
 }
 
@@ -1576,7 +1548,10 @@
         // PixelAspectRatioBox pasp; // optional
 
         // Now read any of 'av1C', 'clap', 'pasp' etc.
-        sample_entry.properties = parse_ipco(&mut stream.sub_stream(&BoxSize::UntilEndOfStream)?)?;
+        sample_entry.properties = parse_ipco(
+            &mut stream.sub_stream(&BoxSize::UntilEndOfStream)?,
+            /*is_track=*/ true,
+        )?;
 
         if !sample_entry
             .properties
@@ -1687,6 +1662,7 @@
         match header.box_type.as_str() {
             "mdhd" => parse_mdhd(&mut sub_stream, track)?,
             "minf" => parse_minf(&mut sub_stream, track)?,
+            "hdlr" => track.handler_type = parse_hdlr(&mut sub_stream)?,
             _ => {}
         }
     }
@@ -1842,7 +1818,13 @@
         let header = parse_header(stream, /*top_level=*/ false)?;
         let mut sub_stream = stream.sub_stream(&header.size)?;
         if header.box_type == "trak" {
-            tracks.push(parse_trak(&mut sub_stream)?);
+            let track = parse_trak(&mut sub_stream)?;
+            if track.is_video_handler() && (track.width == 0 || track.height == 0) {
+                return Err(AvifError::BmffParseFailed(
+                    "invalid track dimensions".into(),
+                ));
+            }
+            tracks.push(track);
         }
     }
     if tracks.is_empty() {
@@ -2022,6 +2004,7 @@
             "invalid trailing bytes in tmap box".into(),
         ));
     }
+    metadata.is_valid()?;
     Ok(Some(metadata))
 }
 
diff --git a/src/reformat/alpha.rs b/src/reformat/alpha.rs
index 9d8be2c..2dff10e 100644
--- a/src/reformat/alpha.rs
+++ b/src/reformat/alpha.rs
@@ -17,7 +17,6 @@
 
 use super::rgb;
 
-use crate::decoder::Category;
 use crate::image::Plane;
 use crate::internal_utils::*;
 use crate::reformat::rgb::Format;
diff --git a/src/reformat/libyuv.rs b/src/reformat/libyuv.rs
index c673c87..0f1e688 100644
--- a/src/reformat/libyuv.rs
+++ b/src/reformat/libyuv.rs
@@ -15,7 +15,6 @@
 use super::rgb;
 use super::rgb::*;
 
-use crate::decoder::Category;
 use crate::image::*;
 use crate::internal_utils::*;
 use crate::*;
@@ -361,6 +360,7 @@
     }
 }
 
+#[cfg_attr(feature = "disable_cfi", no_sanitize(cfi))]
 pub(crate) fn yuv_to_rgb(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<bool> {
     if (rgb.depth != 8 && rgb.depth != 10) || !image.depth_valid() {
         return Err(AvifError::NotImplemented);
diff --git a/src/reformat/mod.rs b/src/reformat/mod.rs
index 50ff208..9c6c813 100644
--- a/src/reformat/mod.rs
+++ b/src/reformat/mod.rs
@@ -26,7 +26,6 @@
 // without it.
 #[cfg(not(feature = "libyuv"))]
 pub mod libyuv {
-    use crate::decoder::Category;
     use crate::reformat::*;
     use crate::*;
 
diff --git a/src/reformat/rgb.rs b/src/reformat/rgb.rs
index 6ee1f65..d43bf57 100644
--- a/src/reformat/rgb.rs
+++ b/src/reformat/rgb.rs
@@ -465,10 +465,10 @@
 mod tests {
     use super::*;
 
-    use crate::decoder::Category;
     use crate::image::YuvRange;
     use crate::image::ALL_PLANES;
     use crate::image::MAX_PLANE_COUNT;
+    use crate::Category;
 
     use test_case::test_case;
     use test_case::test_matrix;
diff --git a/src/reformat/rgb_impl.rs b/src/reformat/rgb_impl.rs
index 595e837..96728c8 100644
--- a/src/reformat/rgb_impl.rs
+++ b/src/reformat/rgb_impl.rs
@@ -726,7 +726,7 @@
                 yuv_range: YuvRange::Limited,
                 ..Default::default()
             };
-            assert!(yuv.allocate_planes(decoder::Category::Color).is_ok());
+            assert!(yuv.allocate_planes(Category::Color).is_ok());
             for plane in image::YUV_PLANES {
                 let samples = if plane == Plane::Y {
                     &y
diff --git a/src/reformat/scale.rs b/src/reformat/scale.rs
index 27f451d..fd05d39 100644
--- a/src/reformat/scale.rs
+++ b/src/reformat/scale.rs
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::decoder::Category;
 use crate::image::*;
 use crate::internal_utils::*;
 use crate::*;
diff --git a/src/utils/clap.rs b/src/utils/clap.rs
index c758bca..d2137eb 100644
--- a/src/utils/clap.rs
+++ b/src/utils/clap.rs
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 use crate::internal_utils::*;
-use crate::*;
+use crate::utils::*;
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct CleanAperture {
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index e267d7f..f087aa8 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -12,4 +12,42 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+use crate::*;
+
 pub mod clap;
+
+// Some HEIF fractional fields can be negative, hence Fraction and UFraction.
+// The denominator is always unsigned.
+
+/// cbindgen:field-names=[n,d]
+#[derive(Clone, Copy, Debug, Default)]
+#[repr(C)]
+pub struct Fraction(pub i32, pub u32);
+
+/// cbindgen:field-names=[n,d]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+#[repr(C)]
+pub struct UFraction(pub u32, pub u32);
+
+impl Fraction {
+    pub(crate) fn is_valid(&self) -> AvifResult<()> {
+        match self.1 {
+            0 => Err(AvifError::InvalidArgument),
+            _ => Ok(()),
+        }
+    }
+
+    pub(crate) fn as_f64(&self) -> AvifResult<f64> {
+        self.is_valid()?;
+        Ok(self.0 as f64 / self.1 as f64)
+    }
+}
+
+impl UFraction {
+    pub(crate) fn is_valid(&self) -> AvifResult<()> {
+        match self.1 {
+            0 => Err(AvifError::InvalidArgument),
+            _ => Ok(()),
+        }
+    }
+}
diff --git a/sys/aom-sys/Cargo.toml b/sys/aom-sys/Cargo.toml
new file mode 100644
index 0000000..7bb776d
--- /dev/null
+++ b/sys/aom-sys/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "aom-sys"
+version = "0.1.0"
+edition = "2021"
+
+[build-dependencies]
+bindgen = "0.69.2"
+pkg-config = "0.3.29"
diff --git a/sys/aom-sys/aom.cmd b/sys/aom-sys/aom.cmd
new file mode 100755
index 0000000..3f3804e
--- /dev/null
+++ b/sys/aom-sys/aom.cmd
@@ -0,0 +1,19 @@
+: # If you want to use a local build of libaom, you must clone the aom repo in this directory first, then set CMake's AVIF_CODEC_AOM to LOCAL options.
+: # The git SHA below is known to work, and will occasionally be updated. Feel free to use a more recent commit.
+
+: # The odd choice of comment style in this file is to try to share this script between *nix and win32.
+
+: # cmake and ninja must be in your PATH.
+
+: # If you're running this on Windows, be sure you've already run this (from your VC2019 install dir):
+: #     "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat"
+
+git clone -b v3.12.0 --depth 1 https://aomedia.googlesource.com/aom
+
+cd aom
+mkdir build.libavif
+cd build.libavif
+
+cmake -G Ninja -DBUILD_SHARED_LIBS=OFF -DCONFIG_PIC=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_DOCS=0 -DENABLE_EXAMPLES=0 -DENABLE_TESTDATA=0 -DENABLE_TESTS=0 -DENABLE_TOOLS=0 ..
+cd ../..
+ninja -C aom/build.libavif
diff --git a/sys/aom-sys/build.rs b/sys/aom-sys/build.rs
new file mode 100644
index 0000000..4047955
--- /dev/null
+++ b/sys/aom-sys/build.rs
@@ -0,0 +1,97 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Build rust library and bindings for libaom.
+
+use std::env;
+use std::path::Path;
+use std::path::PathBuf;
+
+extern crate pkg_config;
+
+fn main() {
+    println!("cargo:rerun-if-changed=build.rs");
+
+    let build_target = std::env::var("TARGET").unwrap();
+    let build_dir = if build_target.contains("android") {
+        if build_target.contains("x86_64") {
+            "build.android/x86_64"
+        } else if build_target.contains("x86") {
+            "build.android/x86"
+        } else if build_target.contains("aarch64") {
+            "build.android/aarch64"
+        } else if build_target.contains("arm") {
+            "build.android/arm"
+        } else {
+            panic!("Unknown target_arch for android. Must be one of x86, x86_64, arm, aarch64.");
+        }
+    } else {
+        "build.libavif"
+    };
+
+    let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+    // Prefer locally built libaom if available.
+    let abs_library_dir = PathBuf::from(&project_root).join("aom");
+    let abs_object_dir = PathBuf::from(&abs_library_dir).join(build_dir);
+    let library_file = PathBuf::from(&abs_object_dir).join("libaom.a");
+    let mut include_paths: Vec<String> = Vec::new();
+    if Path::new(&library_file).exists() {
+        println!("cargo:rustc-link-search={}", abs_object_dir.display());
+        println!("cargo:rustc-link-lib=static=aom");
+        let version_dir = PathBuf::from(&abs_library_dir)
+            .join(build_dir)
+            .join("config");
+        include_paths.push(format!("-I{}", version_dir.display()));
+        let include_dir = PathBuf::from(&abs_library_dir);
+        include_paths.push(format!("-I{}", include_dir.display()));
+    } else {
+        let library = pkg_config::Config::new().probe("aom");
+        if library.is_err() {
+            println!(
+                "aom could not be found with pkg-config. Install the system library or run aom.cmd"
+            );
+        }
+        let library = library.unwrap();
+        for lib in &library.libs {
+            println!("cargo:rustc-link-lib={lib}");
+        }
+        for link_path in &library.link_paths {
+            println!("cargo:rustc-link-search={}", link_path.display());
+        }
+        for include_path in &library.include_paths {
+            include_paths.push(format!("-I{}", include_path.display()));
+        }
+    }
+
+    // Generate bindings.
+    let header_file = PathBuf::from(&project_root).join("wrapper.h");
+    let outfile = PathBuf::from(&project_root).join("aom.rs");
+    let bindings = bindgen::Builder::default()
+        .header(header_file.into_os_string().into_string().unwrap())
+        .clang_args(&include_paths)
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+        .layout_tests(false)
+        .generate_comments(false);
+    // TODO: b/402941742 - Add an allowlist to only generate bindings for necessary items.
+    let bindings = bindings
+        .generate()
+        .unwrap_or_else(|_| panic!("Unable to generate bindings for aom."));
+    bindings
+        .write_to_file(outfile.as_path())
+        .unwrap_or_else(|_| panic!("Couldn't write bindings for aom"));
+    println!(
+        "cargo:rustc-env=CRABBYAVIF_AOM_BINDINGS_RS={}",
+        outfile.display()
+    );
+}
diff --git a/sys/aom-sys/src/lib.rs b/sys/aom-sys/src/lib.rs
new file mode 100644
index 0000000..34191b2
--- /dev/null
+++ b/sys/aom-sys/src/lib.rs
@@ -0,0 +1,18 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![allow(warnings)]
+pub mod bindings {
+    include!(env!("CRABBYAVIF_AOM_BINDINGS_RS"));
+}
diff --git a/sys/aom-sys/wrapper.h b/sys/aom-sys/wrapper.h
new file mode 100644
index 0000000..4980445
--- /dev/null
+++ b/sys/aom-sys/wrapper.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <aom/aom_encoder.h>
+#include <aom/aomcx.h>
diff --git a/tests/data/alpha_premultiplied.avif b/tests/data/alpha_premultiplied.avif
new file mode 100644
index 0000000..4d42519
--- /dev/null
+++ b/tests/data/alpha_premultiplied.avif
Binary files differ
diff --git a/tests/data/colors-animated-8bpc-audio.avif b/tests/data/colors-animated-8bpc-audio.avif
new file mode 100644
index 0000000..93d2cb5
--- /dev/null
+++ b/tests/data/colors-animated-8bpc-audio.avif
Binary files differ
diff --git a/tests/data/colors-animated-8bpc-depth-exif-xmp.avif b/tests/data/colors-animated-8bpc-depth-exif-xmp.avif
new file mode 100644
index 0000000..5ce9bad
--- /dev/null
+++ b/tests/data/colors-animated-8bpc-depth-exif-xmp.avif
Binary files differ
diff --git a/tests/decoder_tests.rs b/tests/decoder_tests.rs
index cbf1757..186ded4 100644
--- a/tests/decoder_tests.rs
+++ b/tests/decoder_tests.rs
@@ -57,10 +57,32 @@
     assert!(alpha_plane.unwrap().row_bytes > 0);
 }
 
-// From avifanimationtest.cc
 #[test]
-fn animated_image() {
-    let mut decoder = get_decoder("colors-animated-8bpc.avif");
+fn alpha_premultiplied() {
+    let mut decoder = get_decoder("alpha_premultiplied.avif");
+    let res = decoder.parse();
+    assert!(res.is_ok());
+    let image = decoder.image().expect("image was none");
+    assert!(image.alpha_present);
+    assert!(image.alpha_premultiplied);
+    if !HAS_DECODER {
+        return;
+    }
+    let res = decoder.next_image();
+    assert!(res.is_ok());
+    let image = decoder.image().expect("image was none");
+    assert!(image.alpha_present);
+    assert!(image.alpha_premultiplied);
+    let alpha_plane = image.plane_data(Plane::A);
+    assert!(alpha_plane.is_some());
+    assert!(alpha_plane.unwrap().row_bytes > 0);
+}
+
+// From avifanimationtest.cc
+#[test_case::test_case("colors-animated-8bpc.avif")]
+#[test_case::test_case("colors-animated-8bpc-audio.avif")]
+fn animated_image(filename: &str) {
+    let mut decoder = get_decoder(filename);
     let res = decoder.parse();
     assert!(res.is_ok());
     assert_eq!(decoder.compression_format(), CompressionFormat::Avif);
@@ -81,9 +103,10 @@
 }
 
 // From avifanimationtest.cc
-#[test]
-fn animated_image_with_source_set_to_primary_item() {
-    let mut decoder = get_decoder("colors-animated-8bpc.avif");
+#[test_case::test_case("colors-animated-8bpc.avif")]
+#[test_case::test_case("colors-animated-8bpc-audio.avif")]
+fn animated_image_with_source_set_to_primary_item(filename: &str) {
+    let mut decoder = get_decoder(filename);
     decoder.settings.source = decoder::Source::PrimaryItem;
     let res = decoder.parse();
     assert!(res.is_ok());
@@ -128,6 +151,54 @@
     }
 }
 
+#[test]
+fn animated_image_with_depth_and_metadata() {
+    // Depth map data is not supported and should be ignored.
+    let mut decoder = get_decoder("colors-animated-8bpc-depth-exif-xmp.avif");
+    let res = decoder.parse();
+    assert!(res.is_ok());
+    assert_eq!(decoder.compression_format(), CompressionFormat::Avif);
+    let image = decoder.image().expect("image was none");
+    assert!(!image.alpha_present);
+    assert!(image.image_sequence_track_present);
+    assert_eq!(decoder.image_count(), 5);
+    assert_eq!(decoder.repetition_count(), RepetitionCount::Infinite);
+    assert_eq!(image.exif.len(), 1126);
+    assert_eq!(image.xmp.len(), 3898);
+    if !HAS_DECODER {
+        return;
+    }
+    for _ in 0..5 {
+        assert!(decoder.next_image().is_ok());
+    }
+}
+
+#[test]
+fn animated_image_with_depth_and_metadata_source_set_to_primary_item() {
+    // Depth map data is not supported and should be ignored.
+    let mut decoder = get_decoder("colors-animated-8bpc-depth-exif-xmp.avif");
+    decoder.settings.source = decoder::Source::PrimaryItem;
+    let res = decoder.parse();
+    assert!(res.is_ok());
+    assert_eq!(decoder.compression_format(), CompressionFormat::Avif);
+    let image = decoder.image().expect("image was none");
+    assert!(!image.alpha_present);
+    // This will be reported as true irrespective of the preferred source.
+    assert!(image.image_sequence_track_present);
+    // imageCount is expected to be 1 because we are using primary item as the
+    // preferred source.
+    assert_eq!(decoder.image_count(), 1);
+    assert_eq!(decoder.repetition_count(), RepetitionCount::Finite(0));
+    if !HAS_DECODER {
+        return;
+    }
+    // Get the first (and only) image.
+    assert!(decoder.next_image().is_ok());
+    // Subsequent calls should not return anything since there is only one
+    // image in the preferred source.
+    assert!(decoder.next_image().is_err());
+}
+
 // From avifkeyframetest.cc
 #[test]
 fn keyframes() {