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() {