use crate::api::EncodeSpeed;
use crate::error::{AvifError as Error, Result};
use crate::meta::{Clap, Colr};
use crate::mux::{
AvifGridMuxer, AvifMuxer, AvifOverlayMuxer, GridTile, IdentityDerivation, OverlayLayer,
ToneMapItem,
};
use oxideav_av1::encoder::scalable_gop::encode_spatial_layered_gop_yuv_with_q;
use oxideav_av1::encoder::still::{
auto_tile_layout, encode_key_frame_yuv_with_options, encode_still_yuv, StillOptions, StillSpeed,
};
use oxideav_av1::encoder::temporal_unit::encode_sequence_header_obu;
use oxideav_av1::encoder::yuv_frame::{ChromaFormat, YuvFrame};
use oxideav_av1::{parse_obu, ObuType, SequenceHeader};
pub use crate::api::{EncodeOptions, StillChroma};
#[deprecated(
since = "0.0.13",
note = "renamed to `oxideav_avif::EncodeOptions` (image-crate API contract)"
)]
pub type StillEncodeOptions = EncodeOptions;
impl StillChroma {
pub(crate) fn to_av1(self) -> ChromaFormat {
match self {
StillChroma::Yuv420 => ChromaFormat::Yuv420,
StillChroma::Yuv422 => ChromaFormat::Yuv422,
StillChroma::Yuv444 => ChromaFormat::Yuv444,
StillChroma::Monochrome => ChromaFormat::Monochrome,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct StillImage {
pub width: u32,
pub height: u32,
pub bit_depth: u8,
pub chroma: StillChroma,
pub y: Vec<u16>,
pub u: Vec<u16>,
pub v: Vec<u16>,
pub alpha: Option<Vec<u16>>,
pub depth_map: Option<Vec<u16>>,
pub colr: Option<Colr>,
pub props: StillProperties,
}
impl StillImage {
#[allow(clippy::too_many_arguments)]
pub fn new(
width: u32,
height: u32,
bit_depth: u8,
chroma: StillChroma,
y: Vec<u16>,
u: Vec<u16>,
v: Vec<u16>,
alpha: Option<Vec<u16>>,
depth_map: Option<Vec<u16>>,
colr: Option<Colr>,
props: StillProperties,
) -> Self {
Self {
width,
height,
bit_depth,
chroma,
y,
u,
v,
alpha,
depth_map,
colr,
props,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct StillProperties {
pub exif: Option<Vec<u8>>,
pub xmp: Option<Vec<u8>>,
pub mdcv: Option<crate::meta::Mdcv>,
pub clli: Option<crate::meta::Clli>,
pub amve: Option<crate::meta::Amve>,
pub irot: Option<u8>,
pub imir: Option<u8>,
pub pasp: Option<crate::meta::Pasp>,
pub identity_derivation: Option<IdentityDerivation>,
pub gain_map: Option<Box<GainMapSpec>>,
pub icc: Option<Vec<u8>>,
}
impl StillProperties {
#[allow(clippy::too_many_arguments)]
pub fn new(
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
mdcv: Option<crate::meta::Mdcv>,
clli: Option<crate::meta::Clli>,
amve: Option<crate::meta::Amve>,
irot: Option<u8>,
imir: Option<u8>,
pasp: Option<crate::meta::Pasp>,
identity_derivation: Option<IdentityDerivation>,
gain_map: Option<Box<GainMapSpec>>,
) -> Self {
Self {
exif,
xmp,
mdcv,
clli,
amve,
irot,
imir,
pasp,
identity_derivation,
gain_map,
icc: None,
}
}
pub fn with_icc(mut self, icc: Option<Vec<u8>>) -> Self {
self.icc = icc;
self
}
pub fn with_exif(mut self, exif: Option<Vec<u8>>) -> Self {
self.exif = exif;
self
}
pub fn with_xmp(mut self, xmp: Option<Vec<u8>>) -> Self {
self.xmp = xmp;
self
}
pub fn with_mdcv(mut self, mdcv: Option<crate::meta::Mdcv>) -> Self {
self.mdcv = mdcv;
self
}
pub fn with_clli(mut self, clli: Option<crate::meta::Clli>) -> Self {
self.clli = clli;
self
}
pub fn with_amve(mut self, amve: Option<crate::meta::Amve>) -> Self {
self.amve = amve;
self
}
pub fn with_irot(mut self, irot: Option<u8>) -> Self {
self.irot = irot;
self
}
pub fn with_imir(mut self, imir: Option<u8>) -> Self {
self.imir = imir;
self
}
pub fn with_pasp(mut self, pasp: Option<crate::meta::Pasp>) -> Self {
self.pasp = pasp;
self
}
pub fn with_identity_derivation(
mut self,
identity_derivation: Option<IdentityDerivation>,
) -> Self {
self.identity_derivation = identity_derivation;
self
}
pub fn with_gain_map(mut self, gain_map: Option<Box<GainMapSpec>>) -> Self {
self.gain_map = gain_map;
self
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct GainMapSpec {
pub map: StillImage,
pub metadata: oxideav_heif::gainmap::GainMapMetadata,
pub alternate_colr: Colr,
pub alternate_clli: Option<crate::meta::Clli>,
pub alternate_bit_depth: Option<u8>,
pub q: u8,
}
impl GainMapSpec {
#[allow(clippy::too_many_arguments)]
pub fn new(
map: StillImage,
metadata: oxideav_heif::gainmap::GainMapMetadata,
alternate_colr: Colr,
alternate_clli: Option<crate::meta::Clli>,
alternate_bit_depth: Option<u8>,
q: u8,
) -> Self {
Self {
map,
metadata,
alternate_colr,
alternate_clli,
alternate_bit_depth,
q,
}
}
pub fn with_map(mut self, map: StillImage) -> Self {
self.map = map;
self
}
pub fn with_metadata(mut self, metadata: oxideav_heif::gainmap::GainMapMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_alternate_colr(mut self, alternate_colr: Colr) -> Self {
self.alternate_colr = alternate_colr;
self
}
pub fn with_alternate_clli(mut self, alternate_clli: Option<crate::meta::Clli>) -> Self {
self.alternate_clli = alternate_clli;
self
}
pub fn with_alternate_bit_depth(mut self, alternate_bit_depth: Option<u8>) -> Self {
self.alternate_bit_depth = alternate_bit_depth;
self
}
pub fn with_q(mut self, q: u8) -> Self {
self.q = q;
self
}
}
impl StillImage {
pub fn yuv(
width: u32,
height: u32,
bit_depth: u8,
chroma: StillChroma,
y: Vec<u16>,
u: Vec<u16>,
v: Vec<u16>,
) -> Result<Self> {
let img = Self {
width,
height,
bit_depth,
chroma,
y,
u,
v,
alpha: None,
depth_map: None,
colr: None,
props: StillProperties::default(),
};
img.validate()?;
Ok(img)
}
pub fn yuv420_8(width: u32, height: u32, y: &[u8], u: &[u8], v: &[u8]) -> Result<Self> {
Self::yuv(
width,
height,
8,
StillChroma::Yuv420,
widen(y),
widen(u),
widen(v),
)
}
pub fn yuv422_8(width: u32, height: u32, y: &[u8], u: &[u8], v: &[u8]) -> Result<Self> {
Self::yuv(
width,
height,
8,
StillChroma::Yuv422,
widen(y),
widen(u),
widen(v),
)
}
pub fn yuv444_8(width: u32, height: u32, y: &[u8], u: &[u8], v: &[u8]) -> Result<Self> {
Self::yuv(
width,
height,
8,
StillChroma::Yuv444,
widen(y),
widen(u),
widen(v),
)
}
pub fn gray_8(width: u32, height: u32, y: &[u8]) -> Result<Self> {
Self::yuv(
width,
height,
8,
StillChroma::Monochrome,
widen(y),
Vec::new(),
Vec::new(),
)
}
pub fn rgb8(width: u32, height: u32, rgb: &[u8]) -> Result<Self> {
let n = (width as usize) * (height as usize);
if rgb.len() != n * 3 {
return Err(Error::invalid(format!(
"avif still: rgb8 expects {} bytes for {width}x{height}, got {}",
n * 3,
rgb.len()
)));
}
let mut y = Vec::with_capacity(n);
let mut u = Vec::with_capacity(n);
let mut v = Vec::with_capacity(n);
for px in rgb.chunks_exact(3) {
v.push(px[0] as u16); y.push(px[1] as u16); u.push(px[2] as u16); }
let mut img = Self::yuv(width, height, 8, StillChroma::Yuv444, y, u, v)?;
img.colr = Some(identity_full_range_colr());
Ok(img)
}
pub fn rgba8(width: u32, height: u32, rgba: &[u8]) -> Result<Self> {
let n = (width as usize) * (height as usize);
if rgba.len() != n * 4 {
return Err(Error::invalid(format!(
"avif still: rgba8 expects {} bytes for {width}x{height}, got {}",
n * 4,
rgba.len()
)));
}
let mut y = Vec::with_capacity(n);
let mut u = Vec::with_capacity(n);
let mut v = Vec::with_capacity(n);
let mut a = Vec::with_capacity(n);
for px in rgba.chunks_exact(4) {
v.push(px[0] as u16);
y.push(px[1] as u16);
u.push(px[2] as u16);
a.push(px[3] as u16);
}
let mut img = Self::yuv(width, height, 8, StillChroma::Yuv444, y, u, v)?;
img.colr = Some(identity_full_range_colr());
img.alpha = Some(a);
img.validate()?;
Ok(img)
}
pub fn with_depth_map(mut self, depth_map: Vec<u16>) -> Result<Self> {
if depth_map.len() != self.width as usize * self.height as usize {
return Err(Error::invalid(format!(
"avif still: depth map has {} samples, expected {}x{}",
depth_map.len(),
self.width,
self.height
)));
}
self.depth_map = Some(depth_map);
Ok(self)
}
pub fn with_alpha(mut self, alpha: Vec<u16>) -> Result<Self> {
self.alpha = Some(alpha);
self.validate()?;
Ok(self)
}
pub fn with_colr(mut self, colr: Colr) -> Self {
self.colr = Some(colr);
self
}
pub fn with_props(mut self, props: StillProperties) -> Self {
self.props = props;
self
}
pub(crate) fn chroma_dims(&self) -> (u32, u32) {
if !self.chroma.has_chroma() {
return (0, 0);
}
let (sx, sy) = self.chroma.subsampling();
(self.width >> sx, self.height >> sy)
}
pub(crate) fn validate(&self) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Err(Error::invalid(
"avif still: dimensions must be at least 1x1",
));
}
if !matches!(self.bit_depth, 8 | 10 | 12) {
return Err(Error::invalid(format!(
"avif still: bit_depth must be 8, 10 or 12, got {}",
self.bit_depth
)));
}
let (sx, sy) = self.chroma.subsampling();
if self.chroma.has_chroma()
&& ((sx == 1 && self.width % 2 != 0) || (sy == 1 && self.height % 2 != 0))
{
return Err(Error::invalid(format!(
"avif still: {:?} requires even subsampled extents, got {}x{}",
self.chroma, self.width, self.height
)));
}
let luma_len = (self.width as usize) * (self.height as usize);
if self.y.len() != luma_len {
return Err(Error::invalid(format!(
"avif still: Y plane length {} != {}x{}",
self.y.len(),
self.width,
self.height
)));
}
let (cw, ch) = self.chroma_dims();
let chroma_len = (cw as usize) * (ch as usize);
if self.u.len() != chroma_len || self.v.len() != chroma_len {
return Err(Error::invalid(format!(
"avif still: chroma plane lengths ({}, {}) != expected {chroma_len}",
self.u.len(),
self.v.len()
)));
}
if let Some(a) = &self.alpha {
if a.len() != luma_len {
return Err(Error::invalid(format!(
"avif still: alpha plane length {} != {}x{} (alpha is full resolution)",
a.len(),
self.width,
self.height
)));
}
}
let ceil = 1u32 << self.bit_depth;
let in_range = |p: &[u16]| p.iter().all(|&s| (s as u32) < ceil);
if !in_range(&self.y)
|| !in_range(&self.u)
|| !in_range(&self.v)
|| !self.alpha.as_deref().map(in_range).unwrap_or(true)
|| !self.depth_map.as_deref().map(in_range).unwrap_or(true)
{
return Err(Error::invalid(format!(
"avif still: sample exceeds bit_depth {} range",
self.bit_depth
)));
}
Ok(())
}
}
pub const STILL_MAX_CODED_DIM: u32 = 4096;
pub const GRID_MIN_TILE_DIM: u32 = 64;
pub fn elect_grid_tiling(width: u32, height: u32, max_tile: u32) -> Option<(u16, u16)> {
if width == 0 || height == 0 || max_tile < GRID_MIN_TILE_DIM {
return None;
}
let per_axis = |extent: u32| -> Option<u16> {
let mut count = 1u32;
loop {
let tile = coded_extent(extent.div_ceil(count));
if tile <= max_tile {
if (count - 1) * tile >= extent || (count > 1 && tile < GRID_MIN_TILE_DIM) {
return None;
}
return u16::try_from(count).ok();
}
count += 1;
if count > 256 {
return None;
}
}
};
Some((per_axis(width)?, per_axis(height)?))
}
pub fn encode_still_auto(img: &StillImage, opts: &EncodeOptions) -> Result<Vec<u8>> {
let (pw, ph) = (coded_extent(img.width), coded_extent(img.height));
if pw <= STILL_MAX_CODED_DIM && ph <= STILL_MAX_CODED_DIM {
return encode_still(img, opts);
}
let (columns, rows) = elect_grid_tiling(img.width, img.height, STILL_MAX_CODED_DIM)
.ok_or_else(|| {
Error::unsupported(format!(
"avif still: no grid tiling for {}x{} within {STILL_MAX_CODED_DIM}-pixel tiles",
img.width, img.height
))
})?;
encode_still_grid(img, opts, columns, rows)
}
fn widen(p: &[u8]) -> Vec<u16> {
p.iter().map(|&s| s as u16).collect()
}
pub(crate) fn identity_full_range_colr() -> Colr {
Colr::Nclx {
colour_primaries: 1,
transfer_characteristics: 13,
matrix_coefficients: 0,
full_range: true,
}
}
pub(crate) fn coded_extent(v: u32) -> u32 {
v.max(8).div_ceil(8) * 8
}
pub(crate) fn pad_plane(src: &[u16], w: usize, h: usize, pw: usize, ph: usize) -> Vec<u16> {
debug_assert!(pw >= w && ph >= h && src.len() == w * h);
if (pw, ph) == (w, h) {
return src.to_vec();
}
let mut out = Vec::with_capacity(pw * ph);
for row in 0..ph {
let sr = row.min(h - 1);
let base = sr * w;
out.extend_from_slice(&src[base..base + w]);
let edge = src[base + w - 1];
out.extend(std::iter::repeat(edge).take(pw - w));
}
out
}
fn extract_rect(
src: &[u16],
w: usize,
h: usize,
x0: usize,
y0: usize,
tw: usize,
th: usize,
) -> Vec<u16> {
debug_assert!(src.len() == w * h && x0 < w && y0 < h);
let mut out = Vec::with_capacity(tw * th);
for row in 0..th {
let sr = (y0 + row).min(h - 1);
let base = sr * w;
let avail = w - x0;
let copy = avail.min(tw);
out.extend_from_slice(&src[base + x0..base + x0 + copy]);
if copy < tw {
let edge = src[base + w - 1];
out.extend(std::iter::repeat(edge).take(tw - copy));
}
}
out
}
pub(crate) fn av1c_from_seq(seq: &SequenceHeader) -> Vec<u8> {
let op = &seq.operating_points[0];
let cc = &seq.color_config;
let b0 = 0x80 | 0x01; let b1 = (seq.seq_profile << 5) | (op.seq_level_idx & 0x1f);
let b2 = ((op.seq_tier & 0x1) << 7)
| ((cc.high_bitdepth as u8) << 6)
| ((cc.twelve_bit as u8) << 5)
| ((cc.mono_chrome as u8) << 4)
| ((cc.subsampling_x as u8) << 3)
| ((cc.subsampling_y as u8) << 2)
| (cc.chroma_sample_position & 0x3);
vec![b0, b1, b2, 0x00]
}
pub(crate) fn av1_err(stage: &str, e: oxideav_av1::Error) -> Error {
Error::invalid(format!("avif still: AV1 {stage} encode failed: {e}"))
}
pub(crate) fn top_left_clap(w: u32, h: u32, pw: u32, ph: u32) -> Clap {
Clap {
clean_aperture_width_n: w as i32,
clean_aperture_width_d: 1,
clean_aperture_height_n: h as i32,
clean_aperture_height_d: 1,
horiz_off_n: w as i32 - pw as i32,
horiz_off_d: 2,
vert_off_n: h as i32 - ph as i32,
vert_off_d: 2,
}
}
pub(crate) struct CodedItem {
pub(crate) payload: Vec<u8>,
pub(crate) av1c: Vec<u8>,
pub(crate) seq_profile: u8,
}
pub(crate) fn full_range_temporal_unit(unit: &[u8], seq: &SequenceHeader) -> Result<Vec<u8>> {
let mut seq_full = seq.clone();
seq_full.color_config.color_range = true;
let new_sh = encode_sequence_header_obu(&seq_full);
let mut out = Vec::with_capacity(unit.len());
let mut off = 0usize;
let mut replaced = false;
while off < unit.len() {
let (desc, consumed) = parse_obu(&unit[off..])
.map_err(|e| Error::invalid(format!("avif still: OBU walk failed: {e}")))?;
if desc.obu_type == ObuType::SequenceHeader {
out.extend_from_slice(&new_sh);
replaced = true;
} else {
out.extend_from_slice(&unit[off..off + consumed]);
}
off += consumed;
}
if !replaced {
return Err(Error::invalid(
"avif still: temporal unit carries no Sequence Header OBU",
));
}
Ok(out)
}
pub const TILE_LAYOUT_THREADS: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct CodedTuning {
pub(crate) speed: EncodeSpeed,
pub(crate) threads: usize,
pub(crate) still: bool,
}
impl CodedTuning {
pub(crate) fn of(opts: &EncodeOptions) -> Self {
Self {
speed: opts.speed,
threads: opts.threads.max(1),
still: true,
}
}
pub(crate) fn sequence(speed: EncodeSpeed, threads: usize) -> Self {
Self {
speed,
threads: threads.max(1),
still: false,
}
}
}
fn still_speed(speed: EncodeSpeed) -> StillSpeed {
match speed {
EncodeSpeed::Fast => StillSpeed::Fast,
EncodeSpeed::Balanced => StillSpeed::Balanced,
EncodeSpeed::Thorough => StillSpeed::Thorough,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_coded_item(
pw: u32,
ph: u32,
bit_depth: u8,
format: ChromaFormat,
y: Vec<u16>,
u: Vec<u16>,
v: Vec<u16>,
q: u8,
full_range: bool,
tuning: CodedTuning,
stage: &str,
) -> Result<CodedItem> {
let frame = YuvFrame {
width: pw,
height: ph,
bit_depth,
format,
y,
u,
v,
};
let mut so = StillOptions::new(q);
so.speed = still_speed(tuning.speed);
so.full_range = full_range;
so.reduced_header = true;
let (cols, rows) = auto_tile_layout(pw, ph, TILE_LAYOUT_THREADS);
so.tile_cols_log2 = cols;
so.tile_rows_log2 = rows;
so.auto_tiles = false;
so.threads = tuning.threads.max(1);
let coded = if tuning.still {
encode_still_yuv(&frame, &so)
} else {
encode_key_frame_yuv_with_options(&frame, &so)
}
.map_err(|e| av1_err(stage, e))?;
let av1c = av1c_from_seq(&coded.seq);
Ok(CodedItem {
payload: coded.temporal_unit_bytes,
av1c,
seq_profile: coded.seq.seq_profile,
})
}
pub(crate) fn colr_is_full_range(img: &StillImage) -> bool {
matches!(
img.colr,
Some(Colr::Nclx {
full_range: true,
..
})
)
}
fn encode_primary(
img: &StillImage,
pw: u32,
ph: u32,
q: u8,
tuning: CodedTuning,
) -> Result<CodedItem> {
let (w, h) = (img.width as usize, img.height as usize);
let y = pad_plane(&img.y, w, h, pw as usize, ph as usize);
let (u, v) = if img.chroma.has_chroma() {
let (sx, sy) = img.chroma.subsampling();
let (cw, ch) = img.chroma_dims();
let (pcw, pch) = ((pw >> sx) as usize, (ph >> sy) as usize);
(
pad_plane(&img.u, cw as usize, ch as usize, pcw, pch),
pad_plane(&img.v, cw as usize, ch as usize, pcw, pch),
)
} else {
(Vec::new(), Vec::new())
};
encode_coded_item(
pw,
ph,
img.bit_depth,
img.chroma.to_av1(),
y,
u,
v,
q,
colr_is_full_range(img),
tuning,
"primary",
)
}
fn encode_alpha(
img: &StillImage,
alpha: &[u16],
pw: u32,
ph: u32,
q: u8,
tuning: CodedTuning,
) -> Result<CodedItem> {
let (w, h) = (img.width as usize, img.height as usize);
let a = pad_plane(alpha, w, h, pw as usize, ph as usize);
encode_coded_item(
pw,
ph,
img.bit_depth,
ChromaFormat::Monochrome,
a,
Vec::new(),
Vec::new(),
q,
true,
tuning,
"alpha",
)
}
pub(crate) fn pixi_bits(img: &StillImage) -> Vec<u8> {
let channels = if img.chroma.has_chroma() { 3 } else { 1 };
vec![img.bit_depth; channels]
}
pub fn encode_still(img: &StillImage, opts: &EncodeOptions) -> Result<Vec<u8>> {
let (mux, max_profile) = still_muxer(img, opts)?;
apply_profile_brand_mux(mux, max_profile).build()
}
pub fn encode_still_burst(images: &[StillImage], opts: &EncodeOptions) -> Result<Vec<u8>> {
let Some(first) = images.first() else {
return Err(Error::invalid("avif burst: no images"));
};
let (mut mux, mut max_profile) = still_muxer(first, opts)?;
for (i, img) in images.iter().enumerate().skip(1) {
img.validate()?;
let (pw, ph) = (coded_extent(img.width), coded_extent(img.height));
if pw > STILL_MAX_CODED_DIM || ph > STILL_MAX_CODED_DIM {
return Err(Error::unsupported(format!(
"avif burst: image {i} coded extents {pw}x{ph} exceed the single-item bound \
{STILL_MAX_CODED_DIM}",
)));
}
let coded = encode_primary(img, pw, ph, opts.base_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded.seq_profile);
let mut member = crate::mux::BurstMember::new(pw, ph, coded.payload, coded.av1c);
member.pixi = Some(pixi_bits(img));
member.colr = img.colr.clone();
member.icc = img.props.icc.clone();
if (pw, ph) != (img.width, img.height) {
member.clap = Some(top_left_clap(img.width, img.height, pw, ph));
}
if let Some(alpha) = &img.alpha {
let coded_alpha =
encode_alpha(img, alpha, pw, ph, opts.alpha_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded_alpha.seq_profile);
member.alpha = Some(crate::mux::BurstAlpha::new(
coded_alpha.payload,
coded_alpha.av1c,
Some(vec![img.bit_depth]),
opts.premultiplied_alpha,
));
}
mux = mux.with_burst_member(member);
}
apply_profile_brand_mux(mux, max_profile).build()
}
fn still_muxer(img: &StillImage, opts: &EncodeOptions) -> Result<(AvifMuxer, u8)> {
img.validate()?;
let (pw, ph) = (coded_extent(img.width), coded_extent(img.height));
if pw > STILL_MAX_CODED_DIM || ph > STILL_MAX_CODED_DIM {
return Err(Error::unsupported(format!(
"avif still: coded extents {pw}x{ph} exceed the single-item bound \
{STILL_MAX_CODED_DIM}; use encode_still_grid",
)));
}
let primary = encode_primary(img, pw, ph, opts.base_q_idx, CodedTuning::of(opts))?;
let mut max_profile = primary.seq_profile;
let mut mux = AvifMuxer::new(pw, ph, primary.payload, primary.av1c).with_pixi(pixi_bits(img));
if (pw, ph) != (img.width, img.height) {
mux = mux.with_clap(top_left_clap(img.width, img.height, pw, ph));
}
if let Some(colr) = &img.colr {
mux = mux.with_colr(colr.clone());
}
let props = &img.props;
if let Some(exif) = &props.exif {
mux = mux.with_exif(exif.clone());
}
if let Some(xmp) = &props.xmp {
mux = mux.with_xmp(xmp.clone());
}
if let Some(icc) = &props.icc {
mux = mux.with_icc(icc.clone());
}
if let Some(mdcv) = props.mdcv {
mux = mux.with_mdcv(mdcv);
}
if let Some(clli) = props.clli {
mux = mux.with_clli(clli);
}
if let Some(amve) = props.amve {
mux = mux.with_amve(amve);
}
if let Some(pasp) = props.pasp {
mux = mux.with_pasp(pasp);
}
if let Some(angle) = props.irot {
mux = mux.with_irot(angle);
}
if let Some(axis) = props.imir {
mux = mux.with_imir(axis);
}
if let Some(iden) = &props.identity_derivation {
mux = mux.with_identity_derivation(iden.clone());
}
if let Some(gm) = &props.gain_map {
gm.map.validate()?;
let (gw, gh) = (coded_extent(gm.map.width), coded_extent(gm.map.height));
if (gw, gh) != (gm.map.width, gm.map.height) {
return Err(Error::unsupported(format!(
"avif still: gain map extents {}x{} are not on the coded 8-grid (a padded gain \
map would need its own clap, which the tmap derivation does not carry)",
gm.map.width, gm.map.height
)));
}
let coded_map = encode_primary(&gm.map, gw, gh, gm.q, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded_map.seq_profile);
let channels = if gm.map.chroma == StillChroma::Monochrome {
1
} else {
3
};
let alt_depth = gm.alternate_bit_depth.unwrap_or(img.bit_depth);
mux = mux.with_tone_map(ToneMapItem {
payload: coded_map.payload,
av1c: coded_map.av1c,
width: gw,
height: gh,
pixi: Some(vec![gm.map.bit_depth; channels]),
colr: gm.map.colr.clone().unwrap_or(Colr::Nclx {
colour_primaries: 2,
transfer_characteristics: 2,
matrix_coefficients: 2,
full_range: colr_is_full_range(&gm.map),
}),
metadata: gm.metadata.clone(),
alternate_colr: gm.alternate_colr.clone(),
alternate_clli: gm.alternate_clli,
alternate_pixi: Some(vec![alt_depth; pixi_bits(img).len()]),
});
}
if let Some(alpha) = &img.alpha {
let coded_alpha =
encode_alpha(img, alpha, pw, ph, opts.alpha_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded_alpha.seq_profile);
mux = mux
.with_alpha(
coded_alpha.payload,
coded_alpha.av1c,
opts.premultiplied_alpha,
)
.with_alpha_pixi(vec![img.bit_depth]);
}
if let Some(depth) = &img.depth_map {
let coded_depth =
encode_alpha(img, depth, pw, ph, opts.alpha_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded_depth.seq_profile);
mux = mux.with_depth(coded_depth.payload, coded_depth.av1c);
}
Ok((mux, max_profile))
}
pub(crate) fn apply_profile_brand_mux(mux: AvifMuxer, seq_profile: u8) -> AvifMuxer {
match seq_profile {
0 => mux, 1 => mux.advanced_profile(),
_ => mux.no_profile_brand(),
}
}
pub fn encode_still_grid(
img: &StillImage,
opts: &EncodeOptions,
columns: u16,
rows: u16,
) -> Result<Vec<u8>> {
img.validate()?;
if img.props.identity_derivation.is_some() {
return Err(Error::unsupported(
"avif still: an identity derivation over a grid primary is not supported",
));
}
if img.depth_map.is_some() {
return Err(Error::unsupported(
"avif still: a depth map on the grid encode path is not supported",
));
}
if columns == 0 || rows == 0 {
return Err(Error::invalid("avif still: grid needs at least 1x1 tiles"));
}
let tile_w = coded_extent(img.width.div_ceil(columns as u32));
let tile_h = coded_extent(img.height.div_ceil(rows as u32));
if tile_w > STILL_MAX_CODED_DIM || tile_h > STILL_MAX_CODED_DIM {
return Err(Error::unsupported(format!(
"avif still: {columns}x{rows} tiling of {}x{} needs {tile_w}x{tile_h} tiles, \
beyond the per-tile bound {STILL_MAX_CODED_DIM}",
img.width, img.height
)));
}
if tile_w < GRID_MIN_TILE_DIM || tile_h < GRID_MIN_TILE_DIM {
return Err(Error::invalid(format!(
"avif still: {columns}x{rows} tiling of {}x{} yields {tile_w}x{tile_h} tiles, \
below the {GRID_MIN_TILE_DIM}-pixel grid-tile floor external readers enforce",
img.width, img.height
)));
}
if (columns as u32 - 1) * tile_w >= img.width || (rows as u32 - 1) * tile_h >= img.height {
return Err(Error::invalid(format!(
"avif still: {columns}x{rows} tiling of {}x{} leaves fully-trimmed tiles \
(tile extents {tile_w}x{tile_h})",
img.width, img.height
)));
}
let (w, h) = (img.width as usize, img.height as usize);
let (sx, sy) = img.chroma.subsampling();
let (cw, ch) = img.chroma_dims();
let mut muxer =
AvifGridMuxer::new(rows, columns, img.width, img.height).with_pixi(pixi_bits(img));
if let Some(colr) = &img.colr {
muxer = muxer.with_colr(colr.clone());
}
let props = &img.props;
if let Some(exif) = &props.exif {
muxer = muxer.with_exif(exif.clone());
}
if let Some(xmp) = &props.xmp {
muxer = muxer.with_xmp(xmp.clone());
}
if let Some(icc) = &props.icc {
muxer = muxer.with_icc(icc.clone());
}
if let Some(mdcv) = props.mdcv {
muxer = muxer.with_mdcv(mdcv);
}
if let Some(clli) = props.clli {
muxer = muxer.with_clli(clli);
}
if let Some(amve) = props.amve {
muxer = muxer.with_amve(amve);
}
if let Some(pasp) = props.pasp {
muxer = muxer.with_pasp(pasp);
}
if let Some(angle) = props.irot {
muxer = muxer.with_irot(angle);
}
if let Some(axis) = props.imir {
muxer = muxer.with_imir(axis);
}
let mut max_profile = 0u8;
for r in 0..rows as usize {
for c in 0..columns as usize {
let x0 = c * tile_w as usize;
let y0 = r * tile_h as usize;
let ty = extract_rect(&img.y, w, h, x0, y0, tile_w as usize, tile_h as usize);
let (tu, tv) = if img.chroma.has_chroma() {
let (tcx, tcy) = (x0 >> sx, y0 >> sy);
let (tcw, tch) = ((tile_w >> sx) as usize, (tile_h >> sy) as usize);
(
extract_rect(&img.u, cw as usize, ch as usize, tcx, tcy, tcw, tch),
extract_rect(&img.v, cw as usize, ch as usize, tcx, tcy, tcw, tch),
)
} else {
(Vec::new(), Vec::new())
};
let coded = encode_coded_item(
tile_w,
tile_h,
img.bit_depth,
img.chroma.to_av1(),
ty,
tu,
tv,
opts.base_q_idx,
colr_is_full_range(img),
CodedTuning::of(opts),
"grid tile",
)?;
max_profile = max_profile.max(coded.seq_profile);
muxer = muxer.tile(GridTile {
width: tile_w,
height: tile_h,
payload: coded.payload,
av1c: coded.av1c,
});
if let Some(alpha) = &img.alpha {
let ta = extract_rect(alpha, w, h, x0, y0, tile_w as usize, tile_h as usize);
let coded_alpha = encode_coded_item(
tile_w,
tile_h,
img.bit_depth,
ChromaFormat::Monochrome,
ta,
Vec::new(),
Vec::new(),
opts.alpha_q_idx,
true,
CodedTuning::of(opts),
"grid alpha tile",
)?;
max_profile = max_profile.max(coded_alpha.seq_profile);
muxer = muxer.alpha_tile(GridTile {
width: tile_w,
height: tile_h,
payload: coded_alpha.payload,
av1c: coded_alpha.av1c,
});
}
}
}
if img.alpha.is_some() {
muxer = muxer
.with_alpha_pixi(vec![img.bit_depth])
.premultiplied_alpha(opts.premultiplied_alpha);
}
muxer = match max_profile {
0 => muxer,
1 => muxer.advanced_profile(),
_ => muxer.no_profile_brand(),
};
muxer.build()
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct OverlayCanvas {
pub width: u32,
pub height: u32,
pub fill_rgba: [u16; 4],
pub colr: Option<Colr>,
pub irot: Option<u8>,
pub imir: Option<u8>,
}
impl OverlayCanvas {
pub fn with_width(mut self, width: u32) -> Self {
self.width = width;
self
}
pub fn with_height(mut self, height: u32) -> Self {
self.height = height;
self
}
pub fn with_fill_rgba(mut self, fill_rgba: [u16; 4]) -> Self {
self.fill_rgba = fill_rgba;
self
}
pub fn with_colr(mut self, colr: Option<Colr>) -> Self {
self.colr = colr;
self
}
pub fn with_irot(mut self, irot: Option<u8>) -> Self {
self.irot = irot;
self
}
pub fn with_imir(mut self, imir: Option<u8>) -> Self {
self.imir = imir;
self
}
}
impl OverlayCanvas {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
fill_rgba: [0, 0, 0, 65535],
colr: None,
irot: None,
imir: None,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct OverlayLayerImage<'a> {
pub image: &'a StillImage,
pub x: i32,
pub y: i32,
}
impl<'a> OverlayLayerImage<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(image: &'a StillImage, x: i32, y: i32) -> Self {
Self { image, x, y }
}
pub fn with_image(mut self, image: &'a StillImage) -> Self {
self.image = image;
self
}
pub fn with_x(mut self, x: i32) -> Self {
self.x = x;
self
}
pub fn with_y(mut self, y: i32) -> Self {
self.y = y;
self
}
}
pub fn encode_still_overlay(
canvas: &OverlayCanvas,
layers: &[OverlayLayerImage<'_>],
opts: &EncodeOptions,
) -> Result<Vec<u8>> {
if layers.is_empty() {
return Err(Error::invalid(
"avif still: overlay needs at least one layer",
));
}
if canvas.width == 0 || canvas.height == 0 {
return Err(Error::invalid("avif still: overlay canvas dimensions zero"));
}
let first = layers[0].image;
let mut muxer = AvifOverlayMuxer::new(canvas.width, canvas.height)
.with_fill(canvas.fill_rgba)
.with_pixi(pixi_bits(first));
if let Some(colr) = canvas.colr.as_ref().or(first.colr.as_ref()) {
muxer = muxer.with_colr(colr.clone());
}
if let Some(angle) = canvas.irot {
muxer = muxer.with_irot(angle);
}
if let Some(axis) = canvas.imir {
muxer = muxer.with_imir(axis);
}
let mut max_profile = 0u8;
for (i, layer) in layers.iter().enumerate() {
let img = layer.image;
img.validate()?;
if (img.bit_depth, img.chroma) != (first.bit_depth, first.chroma) {
return Err(Error::unsupported(format!(
"avif still: overlay layer {i} is {}-bit {:?} but layer 0 is {}-bit {:?} — \
layers must share one coded layout",
img.bit_depth, img.chroma, first.bit_depth, first.chroma
)));
}
let (pw, ph) = (coded_extent(img.width), coded_extent(img.height));
if pw > STILL_MAX_CODED_DIM || ph > STILL_MAX_CODED_DIM {
return Err(Error::unsupported(format!(
"avif still: overlay layer {i} coded extents {pw}x{ph} exceed {STILL_MAX_CODED_DIM}"
)));
}
let coded = encode_primary(img, pw, ph, opts.base_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded.seq_profile);
let clap = ((pw, ph) != (img.width, img.height))
.then(|| top_left_clap(img.width, img.height, pw, ph));
let alpha = match &img.alpha {
Some(a) => {
let coded_alpha =
encode_alpha(img, a, pw, ph, opts.alpha_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded_alpha.seq_profile);
Some((coded_alpha.payload, coded_alpha.av1c))
}
None => None,
};
muxer = muxer.layer(OverlayLayer {
width: pw,
height: ph,
payload: coded.payload,
av1c: coded.av1c,
pixi: Some(pixi_bits(img)),
colr: img.colr.clone(),
clap,
alpha_pixi: alpha.as_ref().map(|_| vec![img.bit_depth]),
alpha,
premultiplied: opts.premultiplied_alpha,
offset_x: layer.x,
offset_y: layer.y,
});
}
muxer = match max_profile {
0 => muxer,
1 => muxer.advanced_profile(),
_ => muxer.no_profile_brand(),
};
muxer.build()
}
#[doc(hidden)]
pub fn layer_byte_sizes(payload: &[u8]) -> Result<Vec<u32>> {
let mut sizes = [0u32; 4];
let mut max_layer = 0usize;
let mut off = 0usize;
while off < payload.len() {
let (obu, consumed) = parse_obu(&payload[off..])
.map_err(|e| Error::invalid(format!("avif still: layered payload OBU walk: {e}")))?;
let layer = usize::from(obu.spatial_id.min(3));
max_layer = max_layer.max(layer);
sizes[layer] += consumed as u32;
off += consumed;
}
Ok(sizes[..=max_layer].to_vec())
}
pub fn encode_still_layered(
layers: &[&StillImage],
render_layer: Option<u8>,
opts: &EncodeOptions,
) -> Result<Vec<u8>> {
if !(2..=4).contains(&layers.len()) {
return Err(Error::invalid(format!(
"avif still: layered encode needs 2..=4 layers, got {}",
layers.len()
)));
}
let top = layers[layers.len() - 1];
top.validate()?;
let mut frames: Vec<Vec<YuvFrame>> = Vec::with_capacity(layers.len());
let mut coded_dims = Vec::with_capacity(layers.len());
for (i, layer) in layers.iter().enumerate() {
layer.validate()?;
if (layer.bit_depth, layer.chroma) != (top.bit_depth, top.chroma) {
return Err(Error::unsupported(format!(
"avif still: layer {i} is {}-bit {:?} but the top layer is {}-bit {:?}",
layer.bit_depth, layer.chroma, top.bit_depth, top.chroma
)));
}
if layer.width > top.width || layer.height > top.height {
return Err(Error::invalid(format!(
"avif still: layer {i} ({}x{}) exceeds the top layer ({}x{})",
layer.width, layer.height, top.width, top.height
)));
}
let (pw, ph) = (coded_extent(layer.width), coded_extent(layer.height));
if pw > STILL_MAX_CODED_DIM || ph > STILL_MAX_CODED_DIM {
return Err(Error::unsupported(format!(
"avif still: layer {i} coded extents {pw}x{ph} exceed {STILL_MAX_CODED_DIM}"
)));
}
let (w, h) = (layer.width as usize, layer.height as usize);
let y = pad_plane(&layer.y, w, h, pw as usize, ph as usize);
let (u, v) = if layer.chroma.has_chroma() {
let (sx, sy) = layer.chroma.subsampling();
let (cw, ch) = layer.chroma_dims();
let (pcw, pch) = ((pw >> sx) as usize, (ph >> sy) as usize);
(
pad_plane(&layer.u, cw as usize, ch as usize, pcw, pch),
pad_plane(&layer.v, cw as usize, ch as usize, pcw, pch),
)
} else {
(Vec::new(), Vec::new())
};
frames.push(vec![YuvFrame {
width: pw,
height: ph,
bit_depth: layer.bit_depth,
format: layer.chroma.to_av1(),
y,
u,
v,
}]);
coded_dims.push((pw, ph));
}
let selected = match render_layer {
Some(s) if usize::from(s) < layers.len() => usize::from(s),
Some(s) => {
return Err(Error::invalid(format!(
"avif still: render_layer {s} out of range for {} layers",
layers.len()
)))
}
None => layers.len() - 1,
};
let coded = encode_spatial_layered_gop_yuv_with_q(&frames, opts.base_q_idx)
.map_err(|e| av1_err("layered", e))?;
let unit =
coded.temporal_units.into_iter().next().ok_or_else(|| {
Error::invalid("avif still: layered encode produced no temporal unit")
})?;
let payload = if colr_is_full_range(top) {
full_range_temporal_unit(&unit, &coded.seq)?
} else {
unit
};
let sizes = layer_byte_sizes(&payload)?;
if sizes.len() != layers.len() {
return Err(Error::invalid(format!(
"avif still: layered payload carries {} spatial layers, expected {}",
sizes.len(),
layers.len()
)));
}
let mut a1lx = [0u32; 3];
for (slot, size) in a1lx.iter_mut().zip(&sizes[..sizes.len() - 1]) {
*slot = *size;
}
let av1c = av1c_from_seq(&coded.seq);
let mut max_profile = coded.seq.seq_profile;
let (pw, ph) = coded_dims[selected];
let sel_img = layers[selected];
let mut mux = AvifMuxer::new(pw, ph, payload, av1c)
.with_pixi(pixi_bits(top))
.with_layered_index(a1lx)
.with_layer_selector(render_layer.map_or(0xFFFF, u16::from));
if (pw, ph) != (sel_img.width, sel_img.height) {
mux = mux.with_clap(top_left_clap(sel_img.width, sel_img.height, pw, ph));
}
if let Some(colr) = &top.colr {
mux = mux.with_colr(colr.clone());
}
let props = &top.props;
if let Some(exif) = &props.exif {
mux = mux.with_exif(exif.clone());
}
if let Some(xmp) = &props.xmp {
mux = mux.with_xmp(xmp.clone());
}
if let Some(icc) = &props.icc {
mux = mux.with_icc(icc.clone());
}
if let Some(mdcv) = props.mdcv {
mux = mux.with_mdcv(mdcv);
}
if let Some(clli) = props.clli {
mux = mux.with_clli(clli);
}
if let Some(amve) = props.amve {
mux = mux.with_amve(amve);
}
if let Some(pasp) = props.pasp {
mux = mux.with_pasp(pasp);
}
if let Some(angle) = props.irot {
mux = mux.with_irot(angle);
}
if let Some(axis) = props.imir {
mux = mux.with_imir(axis);
}
if selected == layers.len() - 1 {
if let Some(alpha) = &top.alpha {
let coded_alpha =
encode_alpha(top, alpha, pw, ph, opts.alpha_q_idx, CodedTuning::of(opts))?;
max_profile = max_profile.max(coded_alpha.seq_profile);
mux = mux
.with_alpha(
coded_alpha.payload,
coded_alpha.av1c,
opts.premultiplied_alpha,
)
.with_alpha_pixi(vec![top.bit_depth]);
}
}
mux = apply_profile_brand_mux(mux, max_profile);
mux.build()
}