use oxideav_heif::compose;
use crate::error::{AvifError as Error, Result};
use crate::frame_bridge::{from_heif, to_heif};
#[cfg(test)]
use crate::image::AvifPlane as VideoPlane;
use crate::image::{AvifFrame as VideoFrame, AvifPixelFormat as PixelFormat};
use crate::box_parser::{b, BoxType};
use crate::meta::{Meta, Property};
pub const ALPHA_URN_PREFIX: &str = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
const AUXL: BoxType = b(b"auxl");
const AUXC: BoxType = b(b"auxC");
pub fn find_alpha_item_id(meta: &Meta, primary_id: u32) -> Option<u32> {
let candidate = meta.iref_source_of(&AUXL, primary_id)?;
if let Some(Property::AuxC(aux)) = meta.property_for(candidate, &AUXC) {
if aux.aux_type.starts_with(ALPHA_URN_PREFIX) {
return Some(candidate);
}
}
None
}
pub(crate) fn expected_alpha_format(color_format: PixelFormat) -> Result<PixelFormat> {
match color_format.bit_depth() {
8 => Ok(PixelFormat::Gray8),
10 => Ok(PixelFormat::Gray10Le),
12 => Ok(PixelFormat::Gray12Le),
other => Err(Error::unsupported(format!(
"avif alpha: colour format {color_format:?} (depth {other}) cannot take an \
alpha auxiliary"
))),
}
}
pub fn composite_alpha(
color: &VideoFrame,
color_format: PixelFormat,
width: u32,
height: u32,
alpha: &VideoFrame,
alpha_format: PixelFormat,
) -> Result<(VideoFrame, PixelFormat)> {
let expected_alpha = expected_alpha_format(color_format)?;
if alpha_format != expected_alpha {
return Err(Error::InvalidData(format!(
"avif alpha: alpha plane format {alpha_format:?} != {expected_alpha:?} — the \
auxiliary shall be encoded at the master's bit depth (av1-avif §4.1)"
)));
}
if color_format.has_alpha() {
return Err(Error::invalid(format!(
"avif alpha: colour format {color_format:?} already carries alpha"
)));
}
let depth = color_format.bit_depth();
let master = to_heif(color, color_format, depth, width, height)
.map_err(|e| Error::invalid(format!("avif alpha: {e}")))?;
let plane = to_heif(alpha, alpha_format, depth, width, height)
.map_err(|e| Error::invalid(format!("avif alpha: {e}")))?;
let attached = compose::attach_alpha(&master, &plane)?;
let (mut out, format) = from_heif(&attached)?;
out.pts = color.pts;
Ok((out, format))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_gray(w: u32, h: u32, fill: u8) -> VideoFrame {
VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride: w as usize,
data: vec![fill; (w * h) as usize],
}],
}
}
fn make_yuv420(w: u32, h: u32) -> VideoFrame {
assert!(w % 2 == 0 && h % 2 == 0);
VideoFrame {
pts: None,
planes: vec![
VideoPlane {
stride: w as usize,
data: vec![100u8; (w * h) as usize],
},
VideoPlane {
stride: (w / 2) as usize,
data: vec![128u8; ((w / 2) * (h / 2)) as usize],
},
VideoPlane {
stride: (w / 2) as usize,
data: vec![128u8; ((w / 2) * (h / 2)) as usize],
},
],
}
}
#[test]
fn composite_yuv420_with_alpha() {
let color = make_yuv420(4, 4);
let alpha = make_gray(4, 4, 200);
let (out, fmt) = composite_alpha(
&color,
PixelFormat::Yuv420P,
4,
4,
&alpha,
PixelFormat::Gray8,
)
.unwrap();
assert_eq!(fmt, PixelFormat::Yuva420P);
assert_eq!(out.planes.len(), 4);
assert_eq!(out.planes[3].data.len(), 16);
assert!(out.planes[3].data.iter().all(|&v| v == 200));
}
#[test]
fn composite_gray_with_alpha_makes_ya8() {
let color = make_gray(2, 2, 50);
let alpha = make_gray(2, 2, 150);
let (out, fmt) =
composite_alpha(&color, PixelFormat::Gray8, 2, 2, &alpha, PixelFormat::Gray8).unwrap();
assert_eq!(fmt, PixelFormat::Ya8);
assert_eq!(out.planes[0].data, vec![50, 150, 50, 150, 50, 150, 50, 150]);
}
#[test]
fn composite_yuv444_with_alpha() {
let color = VideoFrame {
pts: None,
planes: (0..3)
.map(|i| VideoPlane {
stride: 4,
data: vec![(i * 40) as u8; 16],
})
.collect(),
};
let alpha = make_gray(4, 4, 77);
let (out, fmt) = composite_alpha(
&color,
PixelFormat::Yuv444P,
4,
4,
&alpha,
PixelFormat::Gray8,
)
.unwrap();
assert_eq!(fmt, PixelFormat::Yuva444P);
assert_eq!(out.planes.len(), 4);
assert_eq!(out.planes[1].data.len(), 16);
assert!(out.planes[3].data.iter().all(|&v| v == 77));
}
#[test]
fn composite_yuv422_with_alpha() {
let color = VideoFrame {
pts: None,
planes: vec![
VideoPlane {
stride: 4,
data: vec![10u8; 16],
},
VideoPlane {
stride: 2,
data: vec![20u8; 8],
},
VideoPlane {
stride: 2,
data: vec![30u8; 8],
},
],
};
let alpha = make_gray(4, 4, 200);
let (out, fmt) = composite_alpha(
&color,
PixelFormat::Yuv422P,
4,
4,
&alpha,
PixelFormat::Gray8,
)
.unwrap();
assert_eq!(fmt, PixelFormat::Yuva422P);
assert_eq!(out.planes.len(), 4);
assert_eq!(out.planes[1].data.len(), 8);
assert_eq!(out.planes[3].data.len(), 16);
}
#[test]
fn composite_alpha_format_mismatch_errors() {
let c = make_yuv420(4, 4);
let a = make_gray(4, 4, 0);
let err =
composite_alpha(&c, PixelFormat::Yuv420P, 4, 4, &a, PixelFormat::Yuv420P).unwrap_err();
match err {
Error::InvalidData(_) => {}
other => panic!("expected InvalidData, got {other:?}"),
}
}
fn make_gray16(w: u32, h: u32, fill: u16) -> VideoFrame {
let mut data = Vec::with_capacity((w * h) as usize * 2);
for _ in 0..w * h {
data.extend_from_slice(&fill.to_le_bytes());
}
VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride: (w as usize) * 2,
data,
}],
}
}
#[test]
fn composite_yuv420_10bit_with_alpha() {
let mk = |w: u32, h: u32, fill: u16| {
let mut data = Vec::with_capacity((w * h) as usize * 2);
for _ in 0..w * h {
data.extend_from_slice(&fill.to_le_bytes());
}
VideoPlane {
stride: (w as usize) * 2,
data,
}
};
let color = VideoFrame {
pts: None,
planes: vec![mk(4, 4, 600), mk(2, 2, 512), mk(2, 2, 700)],
};
let alpha = make_gray16(4, 4, 1023);
let (out, fmt) = composite_alpha(
&color,
PixelFormat::Yuv420P10Le,
4,
4,
&alpha,
PixelFormat::Gray10Le,
)
.unwrap();
assert_eq!(fmt, PixelFormat::Yuva420P10Le);
assert_eq!(out.planes.len(), 4);
assert_eq!(out.planes[0].stride, 8);
assert_eq!(out.planes[3].data.len(), 32);
for pair in out.planes[3].data.chunks_exact(2) {
assert_eq!(u16::from_le_bytes([pair[0], pair[1]]), 1023);
}
}
#[test]
fn composite_gray12_with_alpha_makes_ya16le() {
let color = make_gray16(2, 2, 4000);
let alpha = make_gray16(2, 2, 123);
let (out, fmt) = composite_alpha(
&color,
PixelFormat::Gray12Le,
2,
2,
&alpha,
PixelFormat::Gray12Le,
)
.unwrap();
assert_eq!(fmt, PixelFormat::Ya16Le);
assert_eq!(out.planes.len(), 1);
assert_eq!(out.planes[0].stride, 8);
let words: Vec<u16> = out.planes[0]
.data
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
assert_eq!(words, vec![4000, 123, 4000, 123, 4000, 123, 4000, 123]);
}
#[test]
fn composite_alpha_depth_mismatch_rejected() {
let color = make_gray16(2, 2, 500);
let alpha = make_gray(2, 2, 100);
let err = composite_alpha(
&color,
PixelFormat::Gray10Le,
2,
2,
&alpha,
PixelFormat::Gray8,
)
.unwrap_err();
match err {
Error::InvalidData(msg) => assert!(msg.contains("§4.1"), "{msg}"),
other => panic!("expected InvalidData, got {other:?}"),
}
}
}