use oxideav_core::frame::{VideoFrame, VideoPlane};
use oxideav_core::Decoder;
use oxideav_core::{
CodecId, CodecParameters, ColorSignal, Error, ExecutionContext, Frame, Packet, PixelFormat,
Result, TimeBase,
};
use crate::api::DecodeOptions;
use crate::av1_config::{parse_av1c, Av1CodecConfig};
use crate::alpha::find_alpha_item_id;
use crate::box_parser::{b, BoxType};
use crate::cicp::effective_cicp;
use crate::derived::{ImageOverlay, MAX_DERIVATION_DEPTH};
use crate::frame_bridge::to_heif;
use crate::grid::ImageGrid;
use crate::image::{AvifFrame, AvifPixelFormat, AvifPlane};
use crate::inspect::AvifInfo;
use crate::meta::{Property, ITEM_TYPE_IDEN, ITEM_TYPE_IOVL, ITEM_TYPE_TMAP};
use crate::parser::{classify_brands, AvifHeader, ITEM_TYPE_AV01, ITEM_TYPE_GRID};
use crate::signal::{color_signal_for, labelled_pixel_format};
use crate::transform::{clap_extent, crop_top_left, rotation_keeps_layout};
use oxideav_heif::compose;
use oxideav_heif::gainmap::{
reconstruct_tone_map, GainMapMetadata, DEFAULT_HDR_REFERENCE_WHITE_NITS,
};
use oxideav_heif::props as hprops;
use oxideav_heif::{Chroma, HeifFrame, HeifPixelFormat, Plane};
pub use crate::inspect::{inspect, transforms_for};
fn from_core_pix(fmt: PixelFormat) -> Result<AvifPixelFormat> {
AvifPixelFormat::try_from(fmt).map_err(core_err)
}
pub(crate) fn core_to_avif_frame(vf: VideoFrame) -> AvifFrame {
let image_planes = vf.image_plane_count();
AvifFrame {
pts: vf.pts,
planes: vf
.planes
.into_iter()
.take(image_planes)
.map(|p| AvifPlane {
stride: p.stride,
data: p.data,
})
.collect(),
}
}
pub(crate) fn infer_av1_pixmap(
frame: &VideoFrame,
cfg: &Av1CodecConfig,
) -> Result<(PixelFormat, u32, u32)> {
if frame.planes.is_empty() {
return Err(Error::invalid("avif: AV1 frame has no planes"));
}
let depth = cfg.bit_depth();
let bps = if cfg.high_bitdepth { 2usize } else { 1 };
let y = &frame.planes[0];
if y.stride == 0 || y.stride % bps != 0 {
return Err(Error::invalid(format!(
"avif: AV1 frame Y plane stride {} incompatible with {depth}-bit storage",
y.stride
)));
}
let width = (y.stride / bps) as u32;
let height = (y.data.len() / y.stride) as u32;
let format = match frame.image_plane_count() {
1 => match depth {
8 => PixelFormat::Gray8,
10 => PixelFormat::Gray10Le,
_ => PixelFormat::Gray12Le,
},
3 => {
let u = &frame.planes[1];
if u.stride == 0 {
return Err(Error::invalid("avif: AV1 frame U plane has zero stride"));
}
let chroma_h = u.data.len() / u.stride;
let u_stride_doubled = u.stride.saturating_mul(2);
let chroma_h_doubled = chroma_h.saturating_mul(2);
let sub = if u_stride_doubled == y.stride && chroma_h_doubled >= height as usize {
if chroma_h as u32 == height.div_ceil(2) {
(1u8, 1u8)
} else {
(1, 0)
}
} else if u.stride == y.stride {
(0, 0)
} else {
return Err(Error::unsupported(format!(
"avif: cannot infer AV1 frame format (Y stride {}, U stride {}, U rows {})",
y.stride, u.stride, chroma_h
)));
};
match (sub, depth) {
((1, 1), 8) => PixelFormat::Yuv420P,
((1, 1), 10) => PixelFormat::Yuv420P10Le,
((1, 1), _) => PixelFormat::Yuv420P12Le,
((1, 0), 8) => PixelFormat::Yuv422P,
((1, 0), 10) => PixelFormat::Yuv422P10Le,
((1, 0), _) => PixelFormat::Yuv422P12Le,
((_, _), 8) => PixelFormat::Yuv444P,
((_, _), 10) => PixelFormat::Yuv444P10Le,
((_, _), _) => PixelFormat::Yuv444P12Le,
}
}
n => {
return Err(Error::unsupported(format!(
"avif: AV1 frame has {n} planes, expected 1 or 3"
)))
}
};
Ok((format, width, height))
}
const AV1C: BoxType = b(b"av1C");
const ISPE: BoxType = b(b"ispe");
const DIMG: BoxType = b(b"dimg");
pub struct AvifDecoder {
codec_id: CodecId,
pending: Vec<Frame>,
info: Option<AvifInfo>,
color_signal: Option<ColorSignal>,
output_format: Option<PixelFormat>,
options: DecodeOptions,
exec_threads: usize,
}
impl AvifDecoder {
pub fn new(codec_id: CodecId) -> Self {
Self {
codec_id,
pending: Vec::new(),
info: None,
color_signal: None,
output_format: None,
options: DecodeOptions::default(),
exec_threads: 1,
}
}
pub fn effective_options(&self) -> DecodeOptions {
let mut o = self.options;
if o.threads.is_none() && self.exec_threads > 1 {
o.threads = Some(self.exec_threads);
}
o
}
pub fn threads(&self) -> usize {
self.effective_options().worker_count()
}
pub fn with_params(params: &CodecParameters) -> Result<Self> {
let o = oxideav_core::parse_options::<AvifDecoderOptions>(¶ms.options)?;
let mut d = Self::new(params.codec_id.clone());
d.options = o.apply_to(d.options);
d.set_reference_white(o.reference_white)?;
Ok(d)
}
pub fn set_tone_mapped(&mut self, tone_mapped: bool) {
self.options.tone_mapped = tone_mapped;
}
pub fn with_tone_mapped(mut self, tone_mapped: bool) -> Self {
self.options.tone_mapped = tone_mapped;
self
}
pub fn tone_mapped(&self) -> bool {
self.options.tone_mapped
}
pub fn options(&self) -> &DecodeOptions {
&self.options
}
pub fn set_options(&mut self, options: DecodeOptions) {
self.options = options;
}
pub fn with_options(mut self, options: DecodeOptions) -> Self {
self.options = options;
self
}
pub fn set_reference_white(&mut self, nits: f64) -> Result<()> {
if !(nits.is_finite() && nits > 0.0) {
return Err(Error::invalid(format!(
"avif: reference white {nits} cd/m² is not a positive finite luminance"
)));
}
self.options.reference_white_nits = nits;
Ok(())
}
pub fn color_signal(&self) -> Option<ColorSignal> {
self.color_signal
}
pub fn output_format(&self) -> Option<PixelFormat> {
self.output_format
}
pub fn decode_file(&mut self, file: &[u8]) -> Result<AvifInfo> {
let decoded =
crate::api_codec::decode_primary(file, &self.effective_options()).map_err(core_err)?;
let format = decoded.image.format;
let signal = color_signal_for(Some(&decoded.image.color.to_colr()));
let out = VideoFrame::from(decoded.image);
self.color_signal = Some(signal);
self.output_format = Some(labelled_pixel_format(format, &signal));
self.pending.push(Frame::Video(out));
self.info = Some(decoded.info.clone());
Ok(decoded.info)
}
pub fn info(&self) -> Option<&AvifInfo> {
self.info.as_ref()
}
pub fn decode_avis_file(&mut self, file: &[u8]) -> Result<usize> {
let mut frames_queued = 0usize;
let opts = self.effective_options();
let seq =
crate::api_codec::decode_sequence_with(file, &opts, None, |frame, seq, _duration| {
self.pending.push(with_signal(frame, seq.signal));
frames_queued += 1;
Ok(())
})
.map_err(core_err)?;
self.color_signal = Some(seq.signal);
self.output_format = seq.layout.map(|l| labelled_pixel_format(l, &seq.signal));
Ok(frames_queued)
}
}
fn with_signal(frame: Frame, signal: ColorSignal) -> Frame {
match frame {
Frame::Video(v) => Frame::Video(v.with_color_signal(signal)),
other => other,
}
}
pub(crate) fn core_err(e: crate::error::AvifError) -> Error {
Error::from(e)
}
pub(crate) fn validate_av1_config(cfg: &Av1CodecConfig) -> Result<()> {
if cfg.seq_profile > 2 {
return Err(Error::invalid(format!(
"av1C: seq_profile={} > 2 (AV1 §A.4)",
cfg.seq_profile
)));
}
if (24..=30).contains(&cfg.seq_level_idx_0) {
return Err(Error::invalid(format!(
"av1C: seq_level_idx_0={} is reserved (AV1 §A.3)",
cfg.seq_level_idx_0
)));
}
let sub_x = cfg.chroma_subsampling_x;
let sub_y = cfg.chroma_subsampling_y;
if cfg.monochrome && !(sub_x && sub_y) {
return Err(Error::invalid(format!(
"av1C: monochrome=1 requires chroma_subsampling_x=chroma_subsampling_y=1, \
got x={sub_x} y={sub_y} (AV1 §5.5.2)"
)));
}
if !cfg.monochrome && sub_x && !sub_y && cfg.seq_profile != 2 {
return Err(Error::invalid(format!(
"av1C: 4:2:2 subsampling requires seq_profile=2, got profile={} (AV1 §5.5.2)",
cfg.seq_profile
)));
}
if !cfg.monochrome && !sub_x && !sub_y && cfg.seq_profile == 0 {
return Err(Error::invalid(
"av1C: 4:4:4 subsampling requires seq_profile in {1, 2}, got profile=0 (AV1 §5.5.2)",
));
}
Ok(())
}
pub(crate) const MAX_AV1_ITEM_BYTES: usize = 32 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Default)]
struct LayerSelect {
operating_point: Option<u8>,
layer_id: Option<u16>,
}
fn decode_av01_item(
obu_bytes: &[u8],
av1c: &[u8],
ispe: Option<(u32, u32)>,
select: LayerSelect,
threads: usize,
) -> Result<(VideoFrame, PixelFormat, u32, u32)> {
if obu_bytes.len() > MAX_AV1_ITEM_BYTES {
return Err(Error::invalid(format!(
"avif: av01 item payload {} bytes exceeds soft cap {} bytes",
obu_bytes.len(),
MAX_AV1_ITEM_BYTES
)));
}
let cfg = parse_av1c(av1c)?; validate_av1_config(&cfg)?;
check_av1c_matches_stream(obu_bytes, &cfg)?;
let frames: Vec<VideoFrame> = match select.operating_point {
Some(op) if op > 0 => decode_at_operating_point(obu_bytes, ispe, op)?,
_ => {
let mut params = CodecParameters::video(CodecId::new("av1"));
if let Some((w, h)) = ispe {
params.width = Some(w);
params.height = Some(h);
}
params.extradata = av1c.to_vec();
let mut av1 = oxideav_av1::registry::make_decoder(¶ms)?;
if threads > 1 {
av1.set_execution_context(&ExecutionContext::with_threads(threads));
}
let pkt = Packet::new(0, TimeBase::new(1, 90_000), obu_bytes.to_vec());
av1.send_packet(&pkt)?;
let mut out = Vec::new();
loop {
match av1.receive_frame() {
Ok(Frame::Video(v)) => out.push(v),
Ok(other) => {
return Err(Error::unsupported(format!(
"avif: AV1 decoder returned non-video frame: {other:?}"
)))
}
Err(Error::NeedMore) => break,
Err(e) => {
if out.is_empty() {
return Err(e);
}
break;
}
}
}
out
}
};
if frames.is_empty() {
return Err(Error::invalid("avif: AV1 item decoded to no shown frame"));
}
let frame = match select.layer_id {
Some(id) if id != 0xFFFF => {
let idx = usize::from(id);
if idx >= frames.len() {
return Err(Error::invalid(format!(
"avif: lsel layer_id {id} but the item decodes {} layer frame(s) \
(av1-avif §2.3.2.2: the layer shall be present and produce an output frame)",
frames.len()
)));
}
frames.into_iter().nth(idx).expect("index checked")
}
_ => frames.into_iter().last().expect("non-empty"),
};
let (format, width, height) = infer_av1_pixmap(&frame, &cfg)?;
Ok((frame, format, width, height))
}
fn check_av1c_matches_stream(obu_bytes: &[u8], cfg: &Av1CodecConfig) -> Result<()> {
use oxideav_av1::{parse_obu, ObuType};
let mut off = 0usize;
while off < obu_bytes.len() {
let Ok((desc, consumed)) = parse_obu(&obu_bytes[off..]) else {
return Ok(());
};
if desc.obu_type == ObuType::SequenceHeader {
let Ok(seq) = oxideav_av1::sequence_header::parse_sequence_header(desc.payload) else {
return Ok(());
};
let cc = &seq.color_config;
let stream = (
cc.bit_depth,
cc.mono_chrome,
cc.subsampling_x,
cc.subsampling_y,
);
let record = (
cfg.bit_depth(),
cfg.monochrome,
cfg.chroma_subsampling_x,
cfg.chroma_subsampling_y,
);
if stream != record {
return Err(Error::invalid(format!(
"avif: av1C (bit depth {}, monochrome {}, subsampling {}/{}) disagrees with \
the Sequence Header OBU (bit depth {}, monochrome {}, subsampling {}/{}) — \
av1-avif §2.2.1",
record.0,
record.1,
u8::from(record.2),
u8::from(record.3),
stream.0,
stream.1,
u8::from(stream.2),
u8::from(stream.3)
)));
}
return Ok(());
}
if consumed == 0 {
return Ok(());
}
off += consumed;
}
Ok(())
}
fn decode_at_operating_point(
obu_bytes: &[u8],
ispe: Option<(u32, u32)>,
op: u8,
) -> Result<Vec<VideoFrame>> {
use oxideav_av1::encoder::ivf::{build_file_header, build_frame_header, FOURCC_AV01};
let (w, h) = ispe.unwrap_or((0, 0));
let mut ivf = Vec::with_capacity(44 + obu_bytes.len());
ivf.extend_from_slice(&build_file_header(
FOURCC_AV01,
w.min(u32::from(u16::MAX)) as u16,
h.min(u32::from(u16::MAX)) as u16,
30,
1,
1,
));
ivf.extend_from_slice(&build_frame_header(obu_bytes.len() as u32, 0));
ivf.extend_from_slice(obu_bytes);
let frames = oxideav_av1::decode_av1_at_operating_point(&ivf, op).map_err(|e| {
Error::invalid(format!(
"avif: AV1 decode at operating point {op} failed: {e}"
))
})?;
frames
.into_iter()
.map(|f| match f {
oxideav_av1::decoder::Frame::Spec(sf) => {
let bps = if sf.bit_depth > 8 { 2 } else { 1 };
Ok(VideoFrame {
pts: None,
planes: sf
.planes
.iter()
.zip(&sf.plane_dims)
.map(|(data, &(pw, _))| VideoPlane {
stride: pw as usize * bps,
data: data.clone(),
})
.collect(),
})
}
#[allow(unreachable_patterns)]
other => Err(Error::unsupported(format!(
"avif: AV1 decoder returned an unexpected frame kind: {other:?}"
))),
})
.collect()
}
pub const MAX_GRID_CANVAS_PIXELS: u64 = 1 << 28;
pub const MAX_ITEM_DECODES: u32 = 4096;
pub(crate) struct DecodeCtx {
stack: Vec<u32>,
cache: std::collections::HashMap<(u32, bool), ItemImage>,
decodes: u32,
tone_mapped: bool,
reference_white_nits: f64,
max_pixels: u64,
layer: Option<u16>,
threads: usize,
}
impl DecodeCtx {
pub(crate) fn new(opts: &crate::api::DecodeOptions) -> Self {
Self {
stack: Vec::new(),
cache: std::collections::HashMap::new(),
decodes: 0,
tone_mapped: opts.tone_mapped,
reference_white_nits: opts.reference_white_nits,
max_pixels: opts.max_pixels.unwrap_or(u64::MAX),
layer: opts.layer,
threads: opts.worker_count(),
}
}
}
pub(crate) type ItemImage = HeifFrame;
fn heif_err(e: oxideav_heif::HeifError) -> Error {
core_err(crate::error::AvifError::from(e))
}
pub(crate) fn decode_item_output(
hdr: &AvifHeader<'_>,
item_id: u32,
depth: u32,
ctx: &mut DecodeCtx,
with_alpha: bool,
) -> Result<ItemImage> {
if depth > MAX_DERIVATION_DEPTH {
return Err(Error::invalid(format!(
"avif: derivation chain deeper than {MAX_DERIVATION_DEPTH} at item {item_id}"
)));
}
if ctx.stack.contains(&item_id) {
return Err(Error::invalid(format!(
"avif: derivation cycle through item {item_id}"
)));
}
if let Some(cached) = ctx.cache.get(&(item_id, with_alpha)) {
return Ok(cached.clone());
}
if ctx.decodes >= MAX_ITEM_DECODES {
return Err(Error::invalid(format!(
"avif: derivation graph needs more than {MAX_ITEM_DECODES} item decodes"
)));
}
ctx.decodes += 1;
ctx.stack.push(item_id);
let result = decode_item_output_inner(hdr, item_id, depth, ctx, with_alpha);
ctx.stack.pop();
if let Ok(image) = &result {
ctx.cache.insert((item_id, with_alpha), image.clone());
}
result
}
fn decode_item_output_inner(
hdr: &AvifHeader<'_>,
item_id: u32,
depth: u32,
ctx: &mut DecodeCtx,
with_alpha: bool,
) -> Result<ItemImage> {
let info = hdr
.meta
.item_by_id(item_id)
.ok_or_else(|| Error::invalid(format!("avif: item {item_id} unknown")))?;
let item_type = info.item_type;
let mut image = if item_type == ITEM_TYPE_AV01 {
let layer = if depth == 0 { ctx.layer } else { None };
decode_coded_item(hdr, item_id, layer, ctx.threads)?
} else if item_type == ITEM_TYPE_TMAP {
let (base, _gain) = tone_map_inputs(hdr, item_id)?;
if depth == 0 && !ctx.tone_mapped {
return decode_item_output(hdr, base, depth + 1, ctx, with_alpha);
}
decode_tone_map_item(hdr, item_id, depth, ctx, with_alpha)?
} else if item_type == ITEM_TYPE_GRID {
decode_grid_item(hdr, item_id, depth, ctx)?
} else if item_type == ITEM_TYPE_IOVL {
decode_overlay_item(hdr, item_id, depth, ctx)?
} else if item_type == ITEM_TYPE_IDEN {
let inputs = hdr.meta.iref_targets(&DIMG, item_id);
if inputs.len() != 1 {
return Err(Error::invalid(format!(
"avif: iden item {item_id} has {} dimg inputs, expected exactly 1 (HEIF §6.6.2.1)",
inputs.len()
)));
}
decode_item_output(hdr, inputs[0], depth + 1, ctx, with_alpha)?
} else {
return Err(Error::unsupported(format!(
"avif: item {item_id} type '{}' cannot be decoded to pixels",
String::from_utf8_lossy(&item_type)
)));
};
if with_alpha {
if let Some(alpha_id) = find_alpha_item_id(&hdr.meta, item_id) {
if image.format.has_alpha {
return Err(Error::unsupported(format!(
"avif: item {item_id} already carries alpha and has an alpha auxiliary {alpha_id}"
)));
}
let alpha = decode_item_output(hdr, alpha_id, depth + 1, ctx, false)?;
let alpha = alpha_as_gray(&alpha, image.width, image.height)?;
if alpha.format.bit_depth != image.format.bit_depth {
return Err(Error::invalid(format!(
"avif alpha: alpha item {alpha_id} is {}-bit but master item {item_id} is \
{}-bit — the auxiliary shall be encoded at the master's bit depth \
(av1-avif §4.1)",
alpha.format.bit_depth, image.format.bit_depth
)));
}
image = compose::attach_alpha(&image, &alpha).map_err(heif_err)?;
}
}
for prop in transforms_for(&hdr.meta, item_id) {
image = match prop {
Property::Clap(clap) => {
if clap_extent(clap, image.width, image.height).is_none() {
continue;
}
compose::apply_clap(&image, &hprops::Clap::from(clap))
}
Property::Irot(irot) => {
let turns = irot.angle & 0x03;
if turns != 0 {
let (sx, sy) = image.format.chroma.shift();
rotation_keeps_layout((sx as u8, sy as u8), turns).map_err(core_err)?;
}
compose::apply_irot(&image, &hprops::Irot::new(turns))
}
Property::Imir(imir) => {
compose::apply_imir(&image, &hprops::Imir::new(imir.axis & 0x01))
}
_ => continue,
}
.map_err(heif_err)?;
}
Ok(image)
}
fn decode_coded_item(
hdr: &AvifHeader<'_>,
item_id: u32,
layer: Option<u16>,
threads: usize,
) -> Result<ItemImage> {
let bytes = hdr.item_data(item_id).map_err(core_err)?;
let av1c = match hdr.meta.property_for(item_id, &AV1C) {
Some(Property::Av1C(b)) => b.clone(),
_ => {
return Err(Error::invalid(format!(
"avif: item {item_id} missing av1C property"
)))
}
};
let ispe = match hdr.meta.property_for(item_id, &ISPE) {
Some(Property::Ispe(e)) => Some((e.width, e.height)),
_ => None,
};
let select = LayerSelect {
operating_point: match hdr.meta.property_for(item_id, b"a1op") {
Some(Property::A1op(a)) => Some(a.op_index),
_ => None,
},
layer_id: layer.or(match hdr.meta.property_for(item_id, b"lsel") {
Some(Property::Lsel(l)) => Some(l.layer_id),
_ => None,
}),
};
let (core_frame, fmt_core, mut w, mut h) =
decode_av01_item(&bytes, &av1c, ispe, select, threads)?;
let bit_depth = parse_av1c(&av1c)?.bit_depth();
let mut frame = core_to_avif_frame(core_frame);
let format = from_core_pix(fmt_core)?;
if let Some((iw, ih)) = ispe {
if iw > 0 && ih > 0 && iw <= w && ih <= h && (iw != w || ih != h) {
frame = crop_top_left(&frame, format, w, h, iw, ih).map_err(core_err)?;
w = iw;
h = ih;
}
}
to_heif(&frame, format, bit_depth, w, h).map_err(core_err)
}
fn decode_grid_item(
hdr: &AvifHeader<'_>,
grid_id: u32,
depth: u32,
ctx: &mut DecodeCtx,
) -> Result<ItemImage> {
if hdr.meta.location_by_id(grid_id).is_none() {
return Err(Error::invalid("avif: grid item missing in iloc"));
}
let grid_bytes = hdr.item_data(grid_id).map_err(core_err)?;
let grid = ImageGrid::parse(&grid_bytes).map_err(core_err)?;
let tile_ids = hdr.meta.iref_targets(&DIMG, grid_id);
if tile_ids.is_empty() {
return Err(Error::invalid("avif: grid item has no dimg iref"));
}
if tile_ids.len() != grid.expected_tile_count() {
return Err(Error::invalid(format!(
"avif: grid declares {} tiles but dimg lists {}",
grid.expected_tile_count(),
tile_ids.len()
)));
}
if grid.output_width == 0 || grid.output_height == 0 {
return Err(Error::invalid("avif: grid output dimensions zero"));
}
let canvas = u64::from(grid.output_width) * u64::from(grid.output_height);
if canvas > MAX_GRID_CANVAS_PIXELS {
return Err(Error::invalid(format!(
"avif: grid canvas {}x{} exceeds {MAX_GRID_CANVAS_PIXELS} pixels",
grid.output_width, grid.output_height
)));
}
if canvas > ctx.max_pixels {
return Err(Error::ResourceExhausted(format!(
"avif: grid canvas {}x{} exceeds the {} pixels allowed",
grid.output_width, grid.output_height, ctx.max_pixels
)));
}
let mut tiles: Vec<HeifFrame> = Vec::with_capacity(tile_ids.len());
if ctx.threads > 1 && tile_ids.len() > 1 {
decode_tiles_parallel(hdr, &tile_ids, depth + 1, ctx)?;
}
for (i, tid) in tile_ids.iter().enumerate() {
let tile = decode_item_output(hdr, *tid, depth + 1, ctx, false)?;
if let Some(first) = tiles.first() {
if first.format != tile.format {
return Err(Error::invalid(format!(
"avif: grid tile {i} format {:?} differs from tile 0 {:?}",
tile.format, first.format
)));
}
if (first.width, first.height) != (tile.width, tile.height) {
return Err(Error::invalid(format!(
"avif: grid tile {i} dims {}x{} differ from tile 0 {}x{}",
tile.width, tile.height, first.width, first.height
)));
}
}
tiles.push(tile);
}
crate::grid::composite_grid_frames(&grid, &tiles).map_err(core_err)
}
fn decode_tiles_parallel(
hdr: &AvifHeader<'_>,
tile_ids: &[u32],
depth: u32,
ctx: &mut DecodeCtx,
) -> Result<()> {
let mut todo: Vec<u32> = Vec::with_capacity(tile_ids.len());
for &tid in tile_ids {
if !todo.contains(&tid) && !ctx.cache.contains_key(&(tid, false)) {
todo.push(tid);
}
}
if todo.len() < 2 {
return Ok(());
}
if ctx.decodes.saturating_add(todo.len() as u32) > MAX_ITEM_DECODES {
return Err(Error::invalid(format!(
"avif: derivation graph needs more than {MAX_ITEM_DECODES} item decodes"
)));
}
let workers = ExecutionContext::with_threads(ctx.threads).effective_workers(todo.len());
let per_tile = (ctx.threads / workers).max(1);
let seed = DecodeCtx {
stack: ctx.stack.clone(),
cache: std::collections::HashMap::new(),
decodes: 0,
tone_mapped: ctx.tone_mapped,
reference_white_nits: ctx.reference_white_nits,
max_pixels: ctx.max_pixels,
layer: None,
threads: per_tile,
};
type TileOut = (usize, u32, Result<ItemImage>, u32);
let outs: Vec<TileOut> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|w| {
let todo = &todo;
let seed = &seed;
scope.spawn(move || {
let mut out: Vec<TileOut> = Vec::new();
for (i, &tid) in todo.iter().enumerate().skip(w).step_by(workers) {
let mut child = DecodeCtx {
stack: seed.stack.clone(),
cache: std::collections::HashMap::new(),
..*seed
};
let r = decode_item_output(hdr, tid, depth, &mut child, false);
out.push((i, tid, r, child.decodes));
}
out
})
})
.collect();
let mut all = Vec::with_capacity(todo.len());
for h in handles {
match h.join() {
Ok(v) => all.extend(v),
Err(_) => all.push((
usize::MAX,
0,
Err(Error::invalid("avif: a grid tile decode worker panicked")),
0,
)),
}
}
all
});
let mut outs = outs;
outs.sort_by_key(|o| o.0);
let mut spent = 0u32;
for (_, tid, r, n) in outs {
spent = spent.saturating_add(n);
let image = r?;
ctx.cache.insert((tid, false), image);
}
ctx.decodes = ctx.decodes.saturating_add(spent);
if ctx.decodes > MAX_ITEM_DECODES {
return Err(Error::invalid(format!(
"avif: derivation graph needs more than {MAX_ITEM_DECODES} item decodes"
)));
}
Ok(())
}
const COLR: BoxType = b(b"colr");
const PREM: BoxType = b(b"prem");
const PIXI: BoxType = b(b"pixi");
const ALTR: BoxType = b(b"altr");
pub(crate) fn tone_map_of(hdr: &AvifHeader<'_>, primary_id: u32) -> Option<u32> {
let meta = &hdr.meta;
if meta.item_by_id(primary_id)?.item_type == ITEM_TYPE_TMAP {
return Some(primary_id);
}
let candidates: Vec<u32> = meta
.items
.iter()
.filter(|i| i.item_type == ITEM_TYPE_TMAP)
.filter(|i| meta.iref_targets(&DIMG, i.id).first() == Some(&primary_id))
.map(|i| i.id)
.collect();
if candidates.is_empty() {
return None;
}
let groups = meta.groups().unwrap_or_default();
candidates
.iter()
.copied()
.find(|&t| {
groups.iter().any(|g| {
g.grouping_type == ALTR
&& g.entity_ids.contains(&t)
&& g.entity_ids.contains(&primary_id)
})
})
.or(candidates.first().copied())
}
fn tone_map_inputs(hdr: &AvifHeader<'_>, tmap_id: u32) -> Result<(u32, u32)> {
let inputs = hdr.meta.iref_targets(&DIMG, tmap_id);
match inputs.as_slice() {
[base, gain] => Ok((*base, *gain)),
other => Err(Error::invalid(format!(
"avif: tmap item {tmap_id} has {} dimg inputs, expected exactly 2 (base, gain map) \
— HEIF Amd 1 §6.6.2.4.1",
other.len()
))),
}
}
fn container_colr(hdr: &AvifHeader<'_>, item_id: u32) -> Option<hprops::Colr> {
match hdr.meta.property_for(item_id, &COLR) {
Some(Property::Colr(c)) => Some(hprops::Colr::from(c)),
_ => None,
}
}
fn decode_tone_map_item(
hdr: &AvifHeader<'_>,
tmap_id: u32,
depth: u32,
ctx: &mut DecodeCtx,
with_alpha: bool,
) -> Result<ItemImage> {
let (base_id, gain_id) = tone_map_inputs(hdr, tmap_id)?;
if hdr.meta.location_by_id(tmap_id).is_none() {
return Err(Error::invalid("avif: tmap item missing in iloc"));
}
let body = hdr.item_data(tmap_id).map_err(core_err)?;
let metadata = GainMapMetadata::parse_tmap_body(&body).map_err(heif_err)?;
let base = decode_item_output(hdr, base_id, depth + 1, ctx, with_alpha)?;
let gain = decode_item_output(hdr, gain_id, depth + 1, ctx, false)?;
let base_colr = container_colr(hdr, base_id);
let gain_colr = container_colr(hdr, gain_id);
let alternate_colr = container_colr(hdr, tmap_id);
let bit_depth = match hdr.meta.property_for(tmap_id, &PIXI) {
Some(Property::Pixi(p)) if p.max_bit_depth() > 0 => p.max_bit_depth(),
_ => base.format.bit_depth,
};
let reference_white = if ctx.reference_white_nits > 0.0 {
ctx.reference_white_nits
} else {
DEFAULT_HDR_REFERENCE_WHITE_NITS
};
reconstruct_tone_map(
&base,
base_colr.as_ref(),
&gain,
gain_colr.as_ref(),
alternate_colr.as_ref(),
&metadata,
bit_depth,
reference_white,
)
.map_err(heif_err)
}
fn is_premultiplied(hdr: &AvifHeader<'_>, item_id: u32) -> bool {
hdr.meta
.irefs
.iter()
.any(|e| e.reference_type == PREM && (e.from_id == item_id || e.to_ids.contains(&item_id)))
}
fn decode_overlay_item(
hdr: &AvifHeader<'_>,
iovl_id: u32,
depth: u32,
ctx: &mut DecodeCtx,
) -> Result<ItemImage> {
if hdr.meta.location_by_id(iovl_id).is_none() {
return Err(Error::invalid("avif: iovl item missing in iloc"));
}
let bytes = hdr.item_data(iovl_id).map_err(core_err)?;
let sources = hdr.meta.iref_targets(&DIMG, iovl_id);
if sources.is_empty() {
return Err(Error::invalid("avif: iovl item has no dimg iref"));
}
let desc = ImageOverlay::parse(&bytes, sources.len()).map_err(core_err)?;
let area = u64::from(desc.output_width) * u64::from(desc.output_height);
if desc.output_width == 0 || desc.output_height == 0 {
return Err(Error::invalid("avif: iovl canvas dimensions zero"));
}
if area > crate::overlay::MAX_OVERLAY_CANVAS_PIXELS {
return Err(Error::invalid(format!(
"avif: iovl canvas {}x{} exceeds {} pixels",
desc.output_width,
desc.output_height,
crate::overlay::MAX_OVERLAY_CANVAS_PIXELS
)));
}
if area > ctx.max_pixels {
return Err(Error::ResourceExhausted(format!(
"avif: iovl canvas {}x{} exceeds the {} pixels allowed",
desc.output_width, desc.output_height, ctx.max_pixels
)));
}
let mut inputs: Vec<(HeifFrame, bool)> = Vec::with_capacity(sources.len());
for (i, &src) in sources.iter().enumerate() {
let image = decode_item_output(hdr, src, depth + 1, ctx, true)?;
if let Some((first, _)) = inputs.first() {
if (first.format.chroma, first.format.bit_depth)
!= (image.format.chroma, image.format.bit_depth)
{
return Err(Error::unsupported(format!(
"avif overlay: input {i} layout ({}-bit {:?}) differs from input 0 \
({}-bit {:?}) — mixed layouts are not composited",
image.format.bit_depth,
image.format.chroma,
first.format.bit_depth,
first.format.chroma
)));
}
}
inputs.push((image, is_premultiplied(hdr, src)));
}
let colr = match hdr.meta.property_for(iovl_id, &COLR) {
Some(Property::Colr(c)) => Some(c.clone()),
_ => match hdr.meta.property_for(sources[0], &COLR) {
Some(Property::Colr(c)) => Some(c.clone()),
_ => None,
},
};
let colr = effective_cicp(colr.as_ref()).to_colr();
let overlay_inputs: Vec<compose::OverlayInput<'_>> = inputs
.iter()
.map(|(frame, premultiplied)| compose::OverlayInput::new(frame, *premultiplied))
.collect();
crate::overlay::composite_overlay_frames(&desc, &overlay_inputs, &colr).map_err(core_err)
}
fn alpha_as_gray(alpha: &ItemImage, master_w: u32, master_h: u32) -> Result<ItemImage> {
let depth = alpha.format.bit_depth;
let bps = alpha.format.bytes_per_sample();
let (aw, ah) = (alpha.width, alpha.height);
let luma = &alpha.planes[0];
let mut data = Vec::with_capacity(master_w as usize * master_h as usize * bps);
if (aw, ah) == (master_w, master_h) {
for r in 0..ah as usize {
let s = r * luma.stride;
data.extend_from_slice(&luma.data[s..s + aw as usize * bps]);
}
} else {
for r in 0..master_h {
let sr = (u64::from(r) * u64::from(ah) / u64::from(master_h)) as usize;
let row = sr * luma.stride;
for c in 0..master_w {
let sc = (u64::from(c) * u64::from(aw) / u64::from(master_w)) as usize;
data.extend_from_slice(&luma.data[row + sc * bps..row + (sc + 1) * bps]);
}
}
}
let format = HeifPixelFormat::new(Chroma::Mono, depth, false).map_err(heif_err)?;
let out = HeifFrame {
width: master_w,
height: master_h,
format,
planes: vec![Plane {
stride: master_w as usize * bps,
data,
}],
};
out.validate().map_err(heif_err)?;
Ok(out)
}
impl Decoder for AvifDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
let (ftyp_payload, _) = crate::box_parser::find_box(&packet.data, &b(b"ftyp"))
.map_err(core_err)?
.ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
let (major_brand, _minor, compatible_brands) =
crate::parser::parse_ftyp(ftyp_payload).map_err(core_err)?;
let brands = classify_brands(&major_brand, &compatible_brands).map_err(core_err)?;
if brands.is_sequence || brands.has_msf1 {
if crate::box_parser::find_box(&packet.data, &b(b"moov"))
.ok()
.flatten()
.is_some()
{
self.decode_avis_file(&packet.data).map(|_| ())
} else {
self.decode_file(&packet.data).map(|_| ())
}
} else {
self.decode_file(&packet.data).map(|_| ())
}
}
fn receive_frame(&mut self) -> Result<Frame> {
if self.pending.is_empty() {
return Err(Error::NeedMore);
}
Ok(self.pending.remove(0))
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.pending.clear();
self.info = None;
self.color_signal = None;
self.output_format = None;
Ok(())
}
fn set_execution_context(&mut self, ctx: &ExecutionContext) {
self.exec_threads = ctx.threads.max(1);
}
fn output_pixel_format(&self) -> Option<PixelFormat> {
self.output_format
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AvifDecoderOptions {
pub tone_mapped: bool,
pub reference_white: f64,
pub threads: u32,
pub max_width: u32,
pub max_height: u32,
pub max_pixels: u32,
pub max_bytes: u32,
pub strict: bool,
pub layer: i32,
}
impl Default for AvifDecoderOptions {
fn default() -> Self {
let d = DecodeOptions::default();
Self {
tone_mapped: d.tone_mapped,
reference_white: d.reference_white_nits,
threads: 0,
max_width: d.max_width.unwrap_or(0),
max_height: d.max_height.unwrap_or(0),
max_pixels: d
.max_pixels
.map(|p| p.min(u64::from(u32::MAX)) as u32)
.unwrap_or(0),
max_bytes: d
.max_bytes
.map(|p| p.min(u64::from(u32::MAX)) as u32)
.unwrap_or(0),
strict: d.strict,
layer: -1,
}
}
}
impl AvifDecoderOptions {
pub fn new() -> Self {
Self::default()
}
pub fn apply_to(&self, mut o: DecodeOptions) -> DecodeOptions {
let nz32 = |v: u32| (v > 0).then_some(v);
o.tone_mapped = self.tone_mapped;
o.reference_white_nits = self.reference_white;
o.threads = (self.threads > 0).then_some(self.threads as usize);
o.max_width = nz32(self.max_width);
o.max_height = nz32(self.max_height);
o.max_pixels = nz32(self.max_pixels).map(u64::from);
o.max_bytes = nz32(self.max_bytes).map(u64::from);
o.strict = self.strict;
o.layer = u16::try_from(self.layer).ok();
o
}
}
impl oxideav_core::CodecOptionsStruct for AvifDecoderOptions {
const SCHEMA: &'static [oxideav_core::OptionField] = &[
oxideav_core::OptionField {
name: "gain_map",
kind: oxideav_core::OptionKind::Enum(&["base", "apply"]),
default: oxideav_core::OptionValue::String(String::new()),
help: "base (default): the primary as readers without tone-map support show it; apply: the tmap alternative with its gain map applied",
},
oxideav_core::OptionField {
name: "reference_white",
kind: oxideav_core::OptionKind::F32,
default: oxideav_core::OptionValue::F32(203.0),
help: "HDR reference white in cd/m2 for a PQ-coded tone-mapped reconstruction (default 203)",
},
oxideav_core::OptionField {
name: "threads",
kind: oxideav_core::OptionKind::U32,
default: oxideav_core::OptionValue::U32(0),
help: "AV1 decode worker budget (tile / band / wave / stripe parallel, byte-identical); 0 = the execution context's budget (serial unless granted)",
},
oxideav_core::OptionField {
name: "max_width",
kind: oxideav_core::OptionKind::U32,
default: oxideav_core::OptionValue::U32(16_384),
help: "Refuse a picture wider than this (0 = unlimited)",
},
oxideav_core::OptionField {
name: "max_height",
kind: oxideav_core::OptionKind::U32,
default: oxideav_core::OptionValue::U32(16_384),
help: "Refuse a picture taller than this (0 = unlimited)",
},
oxideav_core::OptionField {
name: "max_pixels",
kind: oxideav_core::OptionKind::U32,
default: oxideav_core::OptionValue::U32(1 << 28),
help: "Refuse a picture or derived canvas with more pixels than this (0 = unlimited)",
},
oxideav_core::OptionField {
name: "max_bytes",
kind: oxideav_core::OptionKind::U32,
default: oxideav_core::OptionValue::U32(1 << 30),
help: "Refuse an input longer than this many bytes (0 = unlimited)",
},
oxideav_core::OptionField {
name: "strict",
kind: oxideav_core::OptionKind::Bool,
default: oxideav_core::OptionValue::Bool(false),
help: "Strict conformance: an AVIF brand is required and a claimed mif1 must carry its mandatory boxes",
},
oxideav_core::OptionField {
name: "layer",
kind: oxideav_core::OptionKind::I32,
default: oxideav_core::OptionValue::I32(-1),
help: "Spatial layer of a layered primary to render (-1 = the file's lsel)",
},
];
fn apply(&mut self, key: &str, value: &oxideav_core::OptionValue) -> Result<()> {
match key {
"gain_map" => self.tone_mapped = value.as_str()? == "apply",
"reference_white" => self.reference_white = f64::from(value.as_f32()?),
"threads" => self.threads = value.as_u32()?,
"max_width" => self.max_width = value.as_u32()?,
"max_height" => self.max_height = value.as_u32()?,
"max_pixels" => self.max_pixels = value.as_u32()?,
"max_bytes" => self.max_bytes = value.as_u32()?,
"strict" => self.strict = value.as_bool()?,
"layer" => {
let l = value.as_i32()?;
if l < -1 || l > i32::from(u16::MAX) {
return Err(Error::invalid(format!(
"avif: layer {l} is outside -1..=65535"
)));
}
self.layer = l;
}
other => return Err(Error::invalid(format!("unknown option '{other}'"))),
}
Ok(())
}
}
pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(AvifDecoder::with_params(params)?))
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/monochrome.avif");
#[test]
fn validate_av1_config_rejects_high_profile() {
let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
cfg.seq_profile = 3;
let err = validate_av1_config(&cfg).unwrap_err();
match err {
Error::InvalidData(s) => assert!(s.contains("seq_profile"), "got: {s}"),
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn validate_av1_config_rejects_reserved_level() {
let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
cfg.seq_level_idx_0 = 27;
let err = validate_av1_config(&cfg).unwrap_err();
match err {
Error::InvalidData(s) => assert!(s.contains("seq_level_idx_0"), "got: {s}"),
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn validate_av1_config_rejects_monochrome_without_subsampling() {
let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
cfg.monochrome = true;
cfg.chroma_subsampling_x = false;
cfg.chroma_subsampling_y = true;
let err = validate_av1_config(&cfg).unwrap_err();
match err {
Error::InvalidData(s) => assert!(s.contains("monochrome"), "got: {s}"),
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn validate_av1_config_rejects_422_outside_profile_2() {
let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
cfg.seq_profile = 0;
cfg.chroma_subsampling_x = true;
cfg.chroma_subsampling_y = false;
let err = validate_av1_config(&cfg).unwrap_err();
match err {
Error::InvalidData(s) => assert!(s.contains("4:2:2"), "got: {s}"),
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn validate_av1_config_rejects_444_in_profile_0() {
let mut cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
cfg.seq_profile = 0;
cfg.chroma_subsampling_x = false;
cfg.chroma_subsampling_y = false;
let err = validate_av1_config(&cfg).unwrap_err();
match err {
Error::InvalidData(s) => assert!(s.contains("4:4:4"), "got: {s}"),
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn validate_av1_config_accepts_canonical_420_profile0() {
let cfg = parse_av1c(&[0x81, 0x00, 0x0c, 0x00]).unwrap();
validate_av1_config(&cfg).expect("canonical 4:2:0 / profile 0 must validate");
}
#[test]
fn decoder_decodes_monochrome_fixture_end_to_end() {
let mut d = AvifDecoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet::new(0, TimeBase::new(1, 1), FIXTURE.to_vec());
d.send_packet(&pkt).expect("send_packet");
let frame = d.receive_frame().expect("receive_frame");
let vf = match frame {
Frame::Video(v) => v,
other => panic!("expected VideoFrame, got {other:?}"),
};
assert_eq!(vf.image_plane_count(), 1);
let y = &vf.planes[0];
assert_eq!(y.stride, 1280);
assert_eq!(y.data.len() / y.stride, 720);
}
}