mod boxes;
pub mod constants;
mod writer;
use crate::boxes::*;
use arrayvec::ArrayVec;
use std::io;
pub struct Aviffy {
premultiplied_alpha: bool,
colr: ColrBox,
clli: Option<ClliBox>,
mdcv: Option<MdcvBox>,
min_seq_profile: u8,
chroma_subsampling: (bool, bool),
monochrome: bool,
width: u32,
height: u32,
bit_depth: u8,
exif: Option<Vec<u8>>,
}
pub fn serialize<W: io::Write>(into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<()> {
Aviffy::new()
.set_width(width)
.set_height(height)
.set_bit_depth(depth_bits)
.write_slice(into_output, color_av1_data, alpha_av1_data)
}
impl Aviffy {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
premultiplied_alpha: false,
min_seq_profile: 1,
chroma_subsampling: (false, false),
monochrome: false,
width: 0,
height: 0,
bit_depth: 0,
colr: ColrBox::default(),
clli: None,
mdcv: None,
exif: None,
}
}
#[inline]
pub fn set_matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
self.colr.matrix_coefficients = matrix_coefficients;
self
}
#[doc(hidden)]
pub fn matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
self.set_matrix_coefficients(matrix_coefficients)
}
#[inline]
pub fn set_transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
self.colr.transfer_characteristics = transfer_characteristics;
self
}
#[doc(hidden)]
pub fn transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
self.set_transfer_characteristics(transfer_characteristics)
}
#[inline]
pub fn set_color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
self.colr.color_primaries = color_primaries;
self
}
#[doc(hidden)]
pub fn color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
self.set_color_primaries(color_primaries)
}
#[inline]
pub fn set_full_color_range(&mut self, full_range: bool) -> &mut Self {
self.colr.full_range_flag = full_range;
self
}
#[doc(hidden)]
pub fn full_color_range(&mut self, full_range: bool) -> &mut Self {
self.set_full_color_range(full_range)
}
#[inline]
pub fn set_content_light_level(&mut self, max_content_light_level: u16, max_pic_average_light_level: u16) -> &mut Self {
self.clli = Some(ClliBox {
max_content_light_level,
max_pic_average_light_level,
});
self
}
#[inline]
pub fn set_mastering_display(&mut self, primaries: [(u16, u16); 3], white_point: (u16, u16), max_luminance: u32, min_luminance: u32) -> &mut Self {
self.mdcv = Some(MdcvBox {
primaries,
white_point,
max_luminance,
min_luminance,
});
self
}
#[inline]
pub fn write<W: io::Write>(&self, into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<()> {
self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits)?.write(into_output)
}
#[inline]
pub fn write_slice<W: io::Write>(&self, into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>) -> io::Result<()> {
self.make_boxes(color_av1_data, alpha_av1_data, self.width, self.height, self.bit_depth)?.write(into_output)
}
fn make_boxes<'data>(&'data self, color_av1_data: &'data [u8], alpha_av1_data: Option<&'data [u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<AvifFile<'data>> {
if ![8, 10, 12].contains(&depth_bits) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "depth must be 8/10/12"));
}
let mut image_items = ArrayVec::new();
let mut iloc_items = ArrayVec::new();
let mut ipma_entries = ArrayVec::new();
let mut irefs = ArrayVec::new();
let mut ipco = IpcoBox::new();
let color_image_id = 1;
let alpha_image_id = 2;
let exif_id = 3;
const ESSENTIAL_BIT: u8 = 0x80;
let color_depth_bits = depth_bits;
let alpha_depth_bits = depth_bits;
image_items.push(InfeBox {
id: color_image_id,
typ: FourCC(*b"av01"),
name: "",
});
let ispe_prop = ipco.push(IpcoProp::Ispe(IspeBox { width, height })).ok_or(io::ErrorKind::InvalidInput)?;
let av1c_color_prop = ipco.push(IpcoProp::Av1C(Av1CBox {
seq_profile: self.min_seq_profile.max(if color_depth_bits >= 12 { 2 } else { 0 }),
seq_level_idx_0: 31,
seq_tier_0: false,
high_bitdepth: color_depth_bits >= 10,
twelve_bit: color_depth_bits >= 12,
monochrome: self.monochrome,
chroma_subsampling_x: self.chroma_subsampling.0,
chroma_subsampling_y: self.chroma_subsampling.1,
chroma_sample_position: 0,
})).ok_or(io::ErrorKind::InvalidInput)?;
let pixi_3 = ipco.push(IpcoProp::Pixi(PixiBox {
channels: 3,
depth: color_depth_bits,
})).ok_or(io::ErrorKind::InvalidInput)?;
let mut ipma = IpmaEntry {
item_id: color_image_id,
prop_ids: from_array([ispe_prop, av1c_color_prop | ESSENTIAL_BIT, pixi_3]),
};
if self.colr != ColrBox::default() {
let colr_color_prop = ipco.push(IpcoProp::Colr(self.colr)).ok_or(io::ErrorKind::InvalidInput)?;
ipma.prop_ids.push(colr_color_prop);
}
if let Some(clli) = self.clli {
let clli_prop = ipco.push(IpcoProp::Clli(clli)).ok_or(io::ErrorKind::InvalidInput)?;
ipma.prop_ids.push(clli_prop);
}
if let Some(mdcv) = self.mdcv {
let mdcv_prop = ipco.push(IpcoProp::Mdcv(mdcv)).ok_or(io::ErrorKind::InvalidInput)?;
ipma.prop_ids.push(mdcv_prop);
}
ipma_entries.push(ipma);
if let Some(exif_data) = self.exif.as_deref() {
image_items.push(InfeBox {
id: exif_id,
typ: FourCC(*b"Exif"),
name: "",
});
iloc_items.push(IlocItem {
id: exif_id,
extents: [IlocExtent { data: exif_data }],
});
irefs.push(IrefEntryBox {
from_id: exif_id,
to_id: color_image_id,
typ: FourCC(*b"cdsc"),
});
}
if let Some(alpha_data) = alpha_av1_data {
image_items.push(InfeBox {
id: alpha_image_id,
typ: FourCC(*b"av01"),
name: "",
});
irefs.push(IrefEntryBox {
from_id: alpha_image_id,
to_id: color_image_id,
typ: FourCC(*b"auxl"),
});
if self.premultiplied_alpha {
irefs.push(IrefEntryBox {
from_id: color_image_id,
to_id: alpha_image_id,
typ: FourCC(*b"prem"),
});
}
let av1c_alpha_prop = ipco.push(boxes::IpcoProp::Av1C(Av1CBox {
seq_profile: if alpha_depth_bits >= 12 { 2 } else { 0 },
seq_level_idx_0: 31,
seq_tier_0: false,
high_bitdepth: alpha_depth_bits >= 10,
twelve_bit: alpha_depth_bits >= 12,
monochrome: true,
chroma_subsampling_x: true,
chroma_subsampling_y: true,
chroma_sample_position: 0,
})).ok_or(io::ErrorKind::InvalidInput)?;
let pixi_1 = ipco.push(IpcoProp::Pixi(PixiBox {
channels: 1,
depth: alpha_depth_bits,
})).ok_or(io::ErrorKind::InvalidInput)?;
let auxc_prop = ipco.push(IpcoProp::AuxC(AuxCBox {
urn: "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
})).ok_or(io::ErrorKind::InvalidInput)?;
ipma_entries.push(IpmaEntry {
item_id: alpha_image_id,
prop_ids: from_array([ispe_prop, av1c_alpha_prop | ESSENTIAL_BIT, auxc_prop, pixi_1]),
});
iloc_items.push(IlocItem {
id: alpha_image_id,
extents: [IlocExtent { data: alpha_data }],
});
}
iloc_items.push(IlocItem {
id: color_image_id,
extents: [IlocExtent { data: color_av1_data }],
});
Ok(AvifFile {
ftyp: FtypBox {
major_brand: FourCC(*b"avif"),
minor_version: 0,
compatible_brands: [FourCC(*b"mif1"), FourCC(*b"miaf")].into(),
},
meta: MetaBox {
hdlr: HdlrBox {},
iinf: IinfBox { items: image_items },
pitm: PitmBox(color_image_id),
iloc: IlocBox {
absolute_offset_start: None,
items: iloc_items,
},
iprp: IprpBox {
ipco,
ipma: IpmaBox { entries: ipma_entries },
},
iref: IrefBox { entries: irefs },
},
mdat: MdatBox,
})
}
#[must_use]
#[track_caller]
pub fn to_vec(&self, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
let mut file = self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits).unwrap();
let mut out = Vec::new();
file.write_to_vec(&mut out).unwrap();
out
}
#[inline]
pub fn set_chroma_subsampling(&mut self, subsampled_xy: (bool, bool)) -> &mut Self {
self.chroma_subsampling = subsampled_xy;
self
}
#[inline]
pub fn set_monochrome(&mut self, monochrome: bool) -> &mut Self {
self.monochrome = monochrome;
self
}
#[inline]
pub fn set_exif(&mut self, exif: Vec<u8>) -> &mut Self {
self.exif = Some(exif);
self
}
#[inline]
pub fn set_seq_profile(&mut self, seq_profile: u8) -> &mut Self {
self.min_seq_profile = seq_profile;
self
}
#[inline]
pub fn set_width(&mut self, width: u32) -> &mut Self {
self.width = width;
self
}
#[inline]
pub fn set_height(&mut self, height: u32) -> &mut Self {
self.height = height;
self
}
#[inline]
pub fn set_bit_depth(&mut self, bit_depth: u8) -> &mut Self {
self.bit_depth = bit_depth;
self
}
#[inline]
pub fn set_premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
self.premultiplied_alpha = is_premultiplied;
self
}
#[doc(hidden)]
pub fn premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
self.set_premultiplied_alpha(is_premultiplied)
}
}
#[inline(always)]
fn from_array<const L1: usize, const L2: usize, T: Copy>(array: [T; L1]) -> ArrayVec<T, L2> {
assert!(L1 <= L2);
let mut tmp = ArrayVec::new_const();
let _ = tmp.try_extend_from_slice(&array);
tmp
}
#[must_use]
#[track_caller]
pub fn serialize_to_vec(color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
Aviffy::new().to_vec(color_av1_data, alpha_av1_data, width, height, depth_bits)
}
#[test]
fn test_roundtrip_parse_mp4() {
let test_img = b"av12356abc";
let avif = serialize_to_vec(test_img, None, 10, 20, 8);
let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
}
#[test]
fn test_roundtrip_parse_mp4_alpha() {
let test_img = b"av12356abc";
let test_a = b"alpha";
let avif = serialize_to_vec(test_img, Some(test_a), 10, 20, 8);
let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
assert_eq!(&test_a[..], ctx.alpha_item_coded_data().unwrap());
}
#[test]
fn test_roundtrip_parse_exif() {
let test_img = b"av12356abc";
let test_a = b"alpha";
let avif = Aviffy::new()
.set_exif(b"lol".to_vec())
.to_vec(test_img, Some(test_a), 10, 20, 8);
let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
assert_eq!(&test_img[..], ctx.primary_item_coded_data().unwrap());
assert_eq!(&test_a[..], ctx.alpha_item_coded_data().unwrap());
}
#[test]
fn test_roundtrip_parse_avif() {
let test_img = [1, 2, 3, 4, 5, 6];
let test_alpha = [77, 88, 99];
let avif = serialize_to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
assert_eq!(&test_img[..], ctx.primary_item.as_slice());
assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
}
#[test]
fn test_roundtrip_parse_avif_colr() {
let test_img = [1, 2, 3, 4, 5, 6];
let test_alpha = [77, 88, 99];
let avif = Aviffy::new()
.matrix_coefficients(constants::MatrixCoefficients::Bt709)
.to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
assert_eq!(&test_img[..], ctx.primary_item.as_slice());
assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
}
#[test]
fn premultiplied_flag() {
let test_img = [1,2,3,4];
let test_alpha = [55,66,77,88,99];
let avif = Aviffy::new().premultiplied_alpha(true).to_vec(&test_img, Some(&test_alpha), 5, 5, 8);
let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
assert!(ctx.premultiplied_alpha);
assert_eq!(&test_img[..], ctx.primary_item.as_slice());
assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
}
#[test]
fn size_required() {
assert!(Aviffy::new().set_bit_depth(10).write_slice(&mut vec![], &[], None).is_err());
}
#[test]
fn depth_required() {
assert!(Aviffy::new().set_width(1).set_height(1).write_slice(&mut vec![], &[], None).is_err());
}
#[test]
fn clli_roundtrip() {
let test_img = [1, 2, 3, 4, 5, 6];
let avif = Aviffy::new()
.set_content_light_level(1000, 400)
.to_vec(&test_img, None, 10, 20, 8);
let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
let cll = parser.content_light_level.expect("clli box should be present");
assert_eq!(cll.max_content_light_level, 1000);
assert_eq!(cll.max_pic_average_light_level, 400);
}
#[test]
fn mdcv_roundtrip() {
let test_img = [1, 2, 3, 4, 5, 6];
let primaries = [
(8500, 39850), (6550, 2300), (35400, 14600), ];
let white_point = (15635, 16450); let max_luminance = 10_000_000; let min_luminance = 1;
let avif = Aviffy::new()
.set_mastering_display(primaries, white_point, max_luminance, min_luminance)
.to_vec(&test_img, None, 10, 20, 8);
let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
let mdcv = parser.mastering_display.expect("mdcv box should be present");
assert_eq!(mdcv.primaries, primaries);
assert_eq!(mdcv.white_point, white_point);
assert_eq!(mdcv.max_luminance, max_luminance);
assert_eq!(mdcv.min_luminance, min_luminance);
}
#[test]
fn hdr10_full_metadata() {
let test_img = [1, 2, 3, 4, 5, 6];
let test_alpha = [77, 88, 99];
let primaries = [
(8500, 39850),
(6550, 2300),
(35400, 14600),
];
let white_point = (15635, 16450);
let avif = Aviffy::new()
.set_transfer_characteristics(constants::TransferCharacteristics::Smpte2084)
.set_color_primaries(constants::ColorPrimaries::Bt2020)
.set_matrix_coefficients(constants::MatrixCoefficients::Bt2020Ncl)
.set_content_light_level(4000, 1000)
.set_mastering_display(primaries, white_point, 40_000_000, 50)
.to_vec(&test_img, Some(&test_alpha), 10, 20, 10);
let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
let cll = parser.content_light_level.expect("clli box should be present");
assert_eq!(cll.max_content_light_level, 4000);
assert_eq!(cll.max_pic_average_light_level, 1000);
let mdcv = parser.mastering_display.expect("mdcv box should be present");
assert_eq!(mdcv.primaries, primaries);
assert_eq!(mdcv.white_point, white_point);
assert_eq!(mdcv.max_luminance, 40_000_000);
assert_eq!(mdcv.min_luminance, 50);
let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
assert_eq!(ctx.primary_item.as_slice(), &test_img[..]);
assert_eq!(ctx.alpha_item.as_deref().unwrap(), &test_alpha[..]);
}
#[test]
fn no_hdr_metadata_by_default() {
let test_img = [1, 2, 3, 4, 5, 6];
let avif = serialize_to_vec(&test_img, None, 10, 20, 8);
let parser = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
assert!(parser.content_light_level.is_none());
assert!(parser.mastering_display.is_none());
}