use std::time::Duration;
use crate::error::{AvifError as Error, Result};
use crate::image::{AvifFrame, AvifPixelFormat, AvifPlane};
use crate::meta::Colr;
use oxideav_heif::props as hprops;
pub type PixelFormat = AvifPixelFormat;
pub type Plane = AvifPlane;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ColorRange {
#[default]
Unspecified,
Limited,
Full,
}
impl ColorRange {
pub fn is_full(self) -> bool {
self != ColorRange::Limited
}
pub fn from_flag(full_range: bool) -> Self {
if full_range {
ColorRange::Full
} else {
ColorRange::Limited
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ColorInfo {
pub range: ColorRange,
pub primaries: u8,
pub transfer: u8,
pub matrix: u8,
}
impl ColorInfo {
pub const fn new(range: ColorRange, primaries: u8, transfer: u8, matrix: u8) -> Self {
Self {
range,
primaries,
transfer,
matrix,
}
}
pub const fn miaf_default() -> Self {
Self::new(ColorRange::Full, 1, 13, 6)
}
pub const fn identity_full_range() -> Self {
Self::new(ColorRange::Full, 1, 13, 0)
}
pub fn from_colr(colr: Option<&Colr>) -> Self {
match colr {
Some(Colr::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
}) => {
let code = |v: u16| u8::try_from(v).unwrap_or(2);
Self::new(
ColorRange::from_flag(*full_range),
code(*colour_primaries),
code(*transfer_characteristics),
code(*matrix_coefficients),
)
}
_ => Self::miaf_default(),
}
}
pub fn to_colr(&self) -> Colr {
Colr::Nclx {
colour_primaries: u16::from(self.primaries),
transfer_characteristics: u16::from(self.transfer),
matrix_coefficients: u16::from(self.matrix),
full_range: self.range.is_full(),
}
}
pub fn is_identity_matrix(&self) -> bool {
self.matrix == 0
}
pub fn with_range(mut self, range: ColorRange) -> Self {
self.range = range;
self
}
pub fn with_primaries(mut self, primaries: u8) -> Self {
self.primaries = primaries;
self
}
pub fn with_transfer(mut self, transfer: u8) -> Self {
self.transfer = transfer;
self
}
pub fn with_matrix(mut self, matrix: u8) -> Self {
self.matrix = matrix;
self
}
fn to_heif_colr(self) -> hprops::Colr {
hprops::Colr::from(&self.to_colr())
}
}
impl Default for ColorInfo {
fn default() -> Self {
Self::miaf_default()
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Metadata {
pub icc: Option<Vec<u8>>,
pub exif: Option<Vec<u8>>,
pub xmp: Option<Vec<u8>>,
pub gamma: Option<f32>,
}
impl Metadata {
pub fn new(
icc: Option<Vec<u8>>,
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
gamma: Option<f32>,
) -> Self {
Self {
icc,
exif,
xmp,
gamma,
}
}
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_gamma(mut self, gamma: Option<f32>) -> Self {
self.gamma = gamma;
self
}
pub fn is_empty(&self) -> bool {
self.icc.is_none() && self.exif.is_none() && self.xmp.is_none() && self.gamma.is_none()
}
}
#[cfg_attr(not(feature = "registry"), allow(dead_code))]
pub(crate) fn exif_tiff_bytes(body: &[u8]) -> Option<Vec<u8>> {
if body.len() < 4 {
return None;
}
let offset = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
let start = 4usize.checked_add(offset)?;
if start > body.len() {
return None;
}
Some(body[start..].to_vec())
}
#[cfg_attr(not(feature = "registry"), allow(dead_code))]
pub(crate) fn exif_item_body(tiff: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + tiff.len());
out.extend_from_slice(&[0, 0, 0, 0]);
out.extend_from_slice(tiff);
out
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RgbImage {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl RgbImage {
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
Self {
width,
height,
data,
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn into_raw(self) -> Vec<u8> {
self.data
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RgbaImage {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl RgbaImage {
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
Self {
width,
height,
data,
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn into_raw(self) -> Vec<u8> {
self.data
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub struct AvifImage {
pub width: u32,
pub height: u32,
pub format: PixelFormat,
pub planes: Vec<Plane>,
pub color: ColorInfo,
pub metadata: Metadata,
pub bit_depth: u8,
}
impl AvifImage {
pub fn new(width: u32, height: u32, format: PixelFormat, planes: Vec<Plane>) -> Result<Self> {
let img = Self {
width,
height,
format,
planes,
color: ColorInfo::default(),
metadata: Metadata::default(),
bit_depth: format.bit_depth(),
};
img.validate_geometry()?;
Ok(img)
}
pub(crate) fn validate_geometry(&self) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Err(Error::invalid(
"avif: picture dimensions must be at least 1x1",
));
}
let expect = self.format.plane_count();
if self.planes.len() != expect {
return Err(Error::invalid(format!(
"avif: {:?} needs {expect} planes, picture carries {}",
self.format,
self.planes.len()
)));
}
let bps = self.format.bytes_per_sample();
let (sx, sy) = self.format.chroma_subsampling();
let (w, h) = (self.width as usize, self.height as usize);
for (i, plane) in self.planes.iter().enumerate() {
let (cols, rows) = if self.format.is_packed_ya() {
(w * 2, h)
} else if i == 1 || i == 2 {
(w.div_ceil(1 << sx), h.div_ceil(1 << sy))
} else {
(w, h)
};
let row_bytes = cols * bps;
let need = plane
.stride
.checked_mul(rows - 1)
.and_then(|v| v.checked_add(row_bytes));
let short = match need {
Some(n) => plane.data.len() < n,
None => true,
};
if plane.stride < row_bytes || short {
return Err(Error::invalid(format!(
"avif: plane {i} ({} bytes, stride {}) does not cover {rows} rows of \
{row_bytes} bytes",
plane.data.len(),
plane.stride
)));
}
}
Ok(())
}
pub fn from_rgb8(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
let n = width as usize * height as usize;
if width == 0 || height == 0 || data.len() != n * 3 {
return Err(Error::invalid(format!(
"avif: from_rgb8 {width}x{height} needs {} RGB bytes, got {}",
n * 3,
data.len()
)));
}
let mut g = Vec::with_capacity(n);
let mut b = Vec::with_capacity(n);
let mut r = Vec::with_capacity(n);
for px in data.chunks_exact(3) {
r.push(px[0]);
g.push(px[1]);
b.push(px[2]);
}
let w = width as usize;
let planes = vec![Plane::new(w, g), Plane::new(w, b), Plane::new(w, r)];
Ok(Self::new(width, height, PixelFormat::Yuv444P, planes)?
.with_color(ColorInfo::identity_full_range()))
}
pub fn from_rgba8(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
let n = width as usize * height as usize;
if width == 0 || height == 0 || data.len() != n * 4 {
return Err(Error::invalid(format!(
"avif: from_rgba8 {width}x{height} needs {} RGBA bytes, got {}",
n * 4,
data.len()
)));
}
let mut g = Vec::with_capacity(n);
let mut b = Vec::with_capacity(n);
let mut r = Vec::with_capacity(n);
let mut a = Vec::with_capacity(n);
for px in data.chunks_exact(4) {
r.push(px[0]);
g.push(px[1]);
b.push(px[2]);
a.push(px[3]);
}
let w = width as usize;
let planes = vec![
Plane::new(w, g),
Plane::new(w, b),
Plane::new(w, r),
Plane::new(w, a),
];
Ok(Self::new(width, height, PixelFormat::Yuva444P, planes)?
.with_color(ColorInfo::identity_full_range()))
}
pub fn with_color(mut self, color: ColorInfo) -> Self {
self.color = color;
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_bit_depth(mut self, bit_depth: u8) -> Self {
self.bit_depth = bit_depth;
self
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn format(&self) -> PixelFormat {
self.format
}
pub fn bit_depth(&self) -> u8 {
self.bit_depth
}
pub fn has_alpha(&self) -> bool {
self.format.has_alpha()
}
pub fn as_bytes(&self) -> Option<&[u8]> {
if self.planes.len() == 1 {
self.planes.first().map(|p| p.data.as_slice())
} else {
None
}
}
pub fn into_raw(self) -> Vec<u8> {
let total: usize = self.planes.iter().map(|p| p.data.len()).sum();
let mut out = Vec::with_capacity(total);
for p in self.planes {
out.extend_from_slice(&p.data);
}
out
}
pub fn to_rgb8(&self) -> Vec<u8> {
self.rgb_bytes_or_bt601(3)
.expect("AvifImage::to_rgb8: planes inconsistent with width/height/format")
}
pub fn to_rgba8(&self) -> Vec<u8> {
self.rgb_bytes_or_bt601(4)
.expect("AvifImage::to_rgba8: planes inconsistent with width/height/format")
}
pub fn try_to_rgb8(&self) -> Result<Vec<u8>> {
self.rgb_bytes(3, self.color)
}
pub fn try_to_rgba8(&self) -> Result<Vec<u8>> {
self.rgb_bytes(4, self.color)
}
fn rgb_bytes_or_bt601(&self, channels: usize) -> Result<Vec<u8>> {
match self.rgb_bytes(channels, self.color) {
Err(Error::Unsupported(_)) => self.rgb_bytes(channels, self.color.with_matrix(6)),
other => other,
}
}
pub(crate) fn to_heif_frame(&self) -> Result<oxideav_heif::HeifFrame> {
let frame = AvifFrame::new(None, self.planes.clone());
crate::frame_bridge::to_heif(&frame, self.format, self.bit_depth, self.width, self.height)
}
fn rgb_bytes(&self, channels: usize, color: ColorInfo) -> Result<Vec<u8>> {
if !matches!(self.bit_depth, 8 | 10 | 12) {
return Err(Error::invalid(format!(
"avif: bit_depth {} is not 8, 10 or 12",
self.bit_depth
)));
}
let frame = self.to_heif_frame()?;
let colr = color.to_heif_colr();
let rgb = oxideav_heif::rgb::to_rgb(&frame, Some(&colr))?;
let n = self.width as usize * self.height as usize;
if rgb.channels < 3 || rgb.data.len() < n * rgb.channels {
return Err(Error::invalid(
"avif: RGB conversion returned fewer samples than the picture holds",
));
}
let depth = rgb.bit_depth;
let max = (1u32 << depth) - 1;
let half = max / 2;
let to8 = |v: u16| -> u8 {
if depth == 8 {
v as u8
} else {
((u32::from(v).min(max) * 255 + half) / max) as u8
}
};
let mut out = Vec::with_capacity(n * channels);
for px in rgb.data.chunks_exact(rgb.channels).take(n) {
out.push(to8(px[0]));
out.push(to8(px[1]));
out.push(to8(px[2]));
if channels == 4 {
out.push(if rgb.channels >= 4 { to8(px[3]) } else { 255 });
}
}
Ok(out)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub struct Frame {
pub image: AvifImage,
pub delay: Option<Duration>,
}
impl Frame {
pub fn new(image: AvifImage, delay: Option<Duration>) -> Self {
Self { image, delay }
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageInfo {
pub width: u32,
pub height: u32,
pub format: PixelFormat,
pub frames: u32,
pub has_alpha: bool,
pub color: ColorInfo,
pub has_icc: bool,
pub has_exif: bool,
pub has_xmp: bool,
pub bit_depth: u8,
pub is_sequence: bool,
}
impl ImageInfo {
#[allow(clippy::too_many_arguments)]
pub fn new(
width: u32,
height: u32,
format: PixelFormat,
frames: u32,
has_alpha: bool,
color: ColorInfo,
has_icc: bool,
has_exif: bool,
has_xmp: bool,
bit_depth: u8,
is_sequence: bool,
) -> Self {
Self {
width,
height,
format,
frames,
has_alpha,
color,
has_icc,
has_exif,
has_xmp,
bit_depth,
is_sequence,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DecodeOptions {
pub max_width: Option<u32>,
pub max_height: Option<u32>,
pub max_pixels: Option<u64>,
pub max_bytes: Option<u64>,
pub strict: bool,
pub tone_mapped: bool,
pub reference_white_nits: f64,
pub layer: Option<u16>,
pub threads: Option<usize>,
}
impl Default for DecodeOptions {
fn default() -> Self {
Self {
max_width: Some(16_384),
max_height: Some(16_384),
max_pixels: Some(1 << 28),
max_bytes: Some(1 << 30),
strict: false,
tone_mapped: false,
reference_white_nits: oxideav_heif::gainmap::DEFAULT_HDR_REFERENCE_WHITE_NITS,
layer: None,
threads: None,
}
}
}
impl DecodeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_width(mut self, max_width: Option<u32>) -> Self {
self.max_width = max_width;
self
}
pub fn with_max_height(mut self, max_height: Option<u32>) -> Self {
self.max_height = max_height;
self
}
pub fn with_max_pixels(mut self, max_pixels: Option<u64>) -> Self {
self.max_pixels = max_pixels;
self
}
pub fn with_max_bytes(mut self, max_bytes: Option<u64>) -> Self {
self.max_bytes = max_bytes;
self
}
pub fn with_strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn with_tone_mapped(mut self, tone_mapped: bool) -> Self {
self.tone_mapped = tone_mapped;
self
}
pub fn with_reference_white_nits(mut self, reference_white_nits: f64) -> Self {
self.reference_white_nits = reference_white_nits;
self
}
pub fn with_layer(mut self, layer: Option<u16>) -> Self {
self.layer = layer;
self
}
pub fn with_threads(mut self, threads: Option<usize>) -> Self {
self.threads = threads;
self
}
pub fn worker_count(&self) -> usize {
self.threads.unwrap_or(1).max(1)
}
pub fn check_bytes(&self, len: usize) -> Result<()> {
if let Some(max) = self.max_bytes {
if len as u64 > max {
return Err(Error::limit(format!(
"avif: input is {len} bytes, more than the {max} allowed"
)));
}
}
Ok(())
}
pub fn check_dims(&self, width: u32, height: u32) -> Result<()> {
if self.max_width.is_some_and(|m| width > m) || self.max_height.is_some_and(|m| height > m)
{
return Err(Error::limit(format!(
"avif: picture is {width}x{height}, more than the {:?}x{:?} allowed",
self.max_width, self.max_height
)));
}
let pixels = u64::from(width) * u64::from(height);
if let Some(max) = self.max_pixels {
if pixels > max {
return Err(Error::limit(format!(
"avif: picture holds {pixels} pixels, more than the {max} allowed"
)));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StillChroma {
#[default]
Yuv420,
Yuv422,
Yuv444,
Monochrome,
}
#[cfg_attr(not(feature = "registry"), allow(dead_code))]
impl StillChroma {
pub(crate) fn subsampling(self) -> (u32, u32) {
match self {
StillChroma::Yuv420 => (1, 1),
StillChroma::Yuv422 => (1, 0),
StillChroma::Yuv444 => (0, 0),
StillChroma::Monochrome => (0, 0), }
}
pub(crate) fn has_chroma(self) -> bool {
self != StillChroma::Monochrome
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EncodeSpeed {
#[default]
Fast,
Balanced,
Thorough,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EncodeOptions {
pub base_q_idx: u8,
pub alpha_q_idx: u8,
pub premultiplied_alpha: bool,
pub chroma: StillChroma,
pub embed_exif: bool,
pub embed_xmp: bool,
pub embed_icc: bool,
pub speed: EncodeSpeed,
pub threads: usize,
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
base_q_idx: 0,
alpha_q_idx: 0,
premultiplied_alpha: false,
chroma: StillChroma::Yuv420,
embed_exif: true,
embed_xmp: true,
embed_icc: true,
speed: EncodeSpeed::Fast,
threads: 1,
}
}
}
impl EncodeOptions {
pub fn new(base_q_idx: u8, alpha_q_idx: u8, premultiplied_alpha: bool) -> Self {
Self {
base_q_idx,
alpha_q_idx,
premultiplied_alpha,
..Self::default()
}
}
pub fn with_base_q_idx(mut self, base_q_idx: u8) -> Self {
self.base_q_idx = base_q_idx;
self
}
pub fn with_alpha_q_idx(mut self, alpha_q_idx: u8) -> Self {
self.alpha_q_idx = alpha_q_idx;
self
}
pub fn with_premultiplied_alpha(mut self, premultiplied_alpha: bool) -> Self {
self.premultiplied_alpha = premultiplied_alpha;
self
}
pub fn with_chroma(mut self, chroma: StillChroma) -> Self {
self.chroma = chroma;
self
}
pub fn with_embed_exif(mut self, embed_exif: bool) -> Self {
self.embed_exif = embed_exif;
self
}
pub fn with_embed_xmp(mut self, embed_xmp: bool) -> Self {
self.embed_xmp = embed_xmp;
self
}
pub fn with_embed_icc(mut self, embed_icc: bool) -> Self {
self.embed_icc = embed_icc;
self
}
pub fn with_speed(mut self, speed: EncodeSpeed) -> Self {
self.speed = speed;
self
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads.max(1);
self
}
pub fn with_quality(mut self, quality: u8) -> Self {
let q = u32::from(quality.min(100));
self.base_q_idx = (((100 - q) * 255 + 50) / 100) as u8;
self
}
}
const BRAND_AVIF: [u8; 4] = *b"avif";
const BRAND_AVIS: [u8; 4] = *b"avis";
const BRAND_AVIO: [u8; 4] = *b"avio";
pub fn probe(bytes: &[u8]) -> bool {
if bytes.len() < 16 || &bytes[4..8] != b"ftyp" {
return false;
}
let size32 = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let (size, brands_start) = match size32 {
0 => (bytes.len(), 16),
1 => {
if bytes.len() < 24 {
return false;
}
let large = u64::from_be_bytes([
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
bytes[15],
]);
(usize::try_from(large).unwrap_or(usize::MAX), 24)
}
n => (n, 16),
};
let major_at = brands_start - 8;
if size < brands_start {
return false;
}
let end = size.min(bytes.len());
let is_avif = |b: &[u8]| b == BRAND_AVIF || b == BRAND_AVIS || b == BRAND_AVIO;
if is_avif(&bytes[major_at..major_at + 4]) {
return true;
}
bytes[brands_start..end].chunks_exact(4).any(is_avif)
}
pub fn info(bytes: &[u8]) -> Result<ImageInfo> {
let (ftyp_payload, _) = crate::box_parser::find_box(bytes, b"ftyp")?
.ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
let (major, _minor, compat) = crate::parser::parse_ftyp(ftyp_payload)?;
let brands = crate::parser::classify_brands(&major, &compat)?;
if (brands.is_sequence || brands.has_msf1) && has_moov(bytes) {
return sequence_info(bytes);
}
still_info(bytes)
}
pub(crate) fn has_moov(bytes: &[u8]) -> bool {
crate::box_parser::find_box(bytes, b"moov")
.ok()
.flatten()
.is_some()
}
pub(crate) fn item_colrs(
meta: &crate::meta::Meta,
item_id: u32,
) -> (Option<Colr>, Option<Vec<u8>>) {
let mut nclx = None;
let mut icc = None;
for prop in meta.properties_for(item_id) {
if let crate::meta::Property::Colr(c) = prop {
match c {
Colr::Nclx { .. } if nclx.is_none() => nclx = Some(c.clone()),
Colr::Icc(bytes) if icc.is_none() => icc = Some(bytes.clone()),
_ => {}
}
}
}
(nclx, icc)
}
pub(crate) fn layout_of_info(
bit_depth: u8,
monochrome: bool,
chroma: Option<(bool, bool)>,
has_alpha: bool,
) -> Result<PixelFormat> {
let (sx, sy) = match chroma {
Some((x, y)) => (u8::from(x), u8::from(y)),
None => (1, 1),
};
PixelFormat::from_layout(bit_depth, sx, sy, monochrome, has_alpha).ok_or_else(|| {
Error::unsupported(format!(
"avif: no pixel layout for {bit_depth}-bit subsampling ({sx}, {sy}) \
monochrome={monochrome} alpha={has_alpha}"
))
})
}
pub(crate) fn burst_members(meta: &crate::meta::Meta, item_id: u32) -> Vec<u32> {
meta.groups()
.unwrap_or_default()
.into_iter()
.find(|g| g.is_burst() && g.entity_ids.contains(&item_id))
.map(|g| g.entity_ids)
.unwrap_or_default()
}
fn still_info(bytes: &[u8]) -> Result<ImageInfo> {
let hdr = crate::parser::parse_header(bytes)?;
let primary_id = hdr
.meta
.primary_item_id
.ok_or_else(|| Error::invalid("avif: missing pitm"))?;
let info = crate::inspect::inspect(bytes)?;
let bit_depth = info.bit_depth.unwrap_or(8);
let format = layout_of_info(
bit_depth,
info.monochrome,
info.chroma_subsampling,
info.has_alpha,
)?;
let (nclx, icc) = item_colrs(&hdr.meta, primary_id);
let colour = nclx.or_else(|| match &info.colour {
Some(c @ Colr::Nclx { .. }) => Some(c.clone()),
_ => None,
});
let has_icc = icc.is_some() || matches!(info.colour, Some(Colr::Icc(_)));
let burst = burst_members(&hdr.meta, primary_id);
let frames = if burst.is_empty() {
1
} else {
u32::try_from(burst.len()).unwrap_or(u32::MAX)
};
Ok(ImageInfo::new(
info.width,
info.height,
format,
frames,
info.has_alpha,
ColorInfo::from_colr(colour.as_ref()),
has_icc,
info.exif_item_id.is_some(),
info.xmp_item_id.is_some(),
bit_depth,
false,
))
}
fn sequence_info(bytes: &[u8]) -> Result<ImageInfo> {
let meta = crate::avis::parse_avis(bytes)?;
let av1c = meta.av1_codec_config.as_deref().ok_or_else(|| {
Error::invalid("avis: track stsd → av01 → av1C is missing (av1-avif §2.2.1)")
})?;
let cfg = oxideav_heif::Av1Config::parse(av1c)?;
let bit_depth = cfg.bit_depth();
let format = layout_of_info(
bit_depth,
cfg.monochrome,
Some((cfg.chroma_subsampling_x, cfg.chroma_subsampling_y)),
false,
)?;
let (width, height) = meta.display_dims.unwrap_or((0, 0));
let (has_exif, has_xmp, has_icc) = match crate::parser::parse_header(bytes) {
Ok(hdr) => {
let primary = hdr.meta.primary_item_id;
match primary.and_then(|_| crate::inspect::inspect(bytes).ok()) {
Some(i) => (
i.exif_item_id.is_some(),
i.xmp_item_id.is_some(),
matches!(i.colour, Some(Colr::Icc(_)))
|| primary
.map(|p| item_colrs(&hdr.meta, p).1.is_some())
.unwrap_or(false),
),
None => (false, false, false),
}
}
Err(_) => (false, false, false),
};
Ok(ImageInfo::new(
width,
height,
format,
u32::try_from(meta.samples.len()).unwrap_or(u32::MAX),
false,
ColorInfo::from_colr(meta.colr.as_ref()),
has_icc,
has_exif,
has_xmp,
bit_depth,
true,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn ftyp(major: &[u8; 4], compat: &[&[u8; 4]]) -> Vec<u8> {
let mut v = Vec::new();
let size = 16 + 4 * compat.len();
v.extend_from_slice(&(size as u32).to_be_bytes());
v.extend_from_slice(b"ftyp");
v.extend_from_slice(major);
v.extend_from_slice(&[0, 0, 0, 0]);
for c in compat {
v.extend_from_slice(*c);
}
v
}
#[test]
fn probe_accepts_avif_brands_only() {
assert!(probe(&ftyp(b"avif", &[b"mif1", b"miaf"])));
assert!(probe(&ftyp(b"mif1", &[b"avif"])));
assert!(probe(&ftyp(b"avis", &[b"msf1"])));
assert!(probe(&ftyp(b"avio", &[])));
assert!(!probe(&ftyp(b"heic", &[b"mif1", b"miaf"])));
assert!(!probe(&ftyp(b"mif1", &[b"heic"])));
assert!(!probe(b""));
assert!(!probe(b"\0\0\0\x10ftypavif"));
assert!(!probe(b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0"));
let mut t = ftyp(b"mif1", &[b"miaf", b"avif"]);
t.truncate(22);
assert!(!probe(&t));
let mut large = Vec::new();
large.extend_from_slice(&1u32.to_be_bytes());
large.extend_from_slice(b"ftyp");
large.extend_from_slice(&28u64.to_be_bytes());
large.extend_from_slice(b"avif");
large.extend_from_slice(&[0, 0, 0, 0]);
large.extend_from_slice(b"mif1");
assert!(probe(&large));
}
#[test]
fn rgb_round_trips_through_identity_planes() {
let rgb: Vec<u8> = (0..3 * 4 * 2).map(|i| (i * 7 % 256) as u8).collect();
let img = AvifImage::from_rgb8(4, 2, rgb.clone()).unwrap();
assert_eq!(img.format(), PixelFormat::Yuv444P);
assert!(img.color.is_identity_matrix());
assert_eq!(img.to_rgb8(), rgb);
let rgba = img.to_rgba8();
assert_eq!(rgba.len(), 4 * 2 * 4);
assert!(rgba.chunks_exact(4).all(|p| p[3] == 255));
let with_alpha: Vec<u8> = (0..4 * 4 * 2).map(|i| (i * 13 % 256) as u8).collect();
let img = AvifImage::from_rgba8(4, 2, with_alpha.clone()).unwrap();
assert_eq!(img.format(), PixelFormat::Yuva444P);
assert_eq!(img.to_rgba8(), with_alpha);
assert_eq!(
img.to_rgb8(),
with_alpha
.chunks_exact(4)
.flat_map(|p| p[..3].to_vec())
.collect::<Vec<_>>()
);
}
#[test]
fn limited_range_luma_maps_to_rgb_extremes() {
let planes = vec![
Plane::new(2, vec![16, 235, 128, 128]),
Plane::new(1, vec![128]),
Plane::new(1, vec![128]),
];
let img = AvifImage::new(2, 2, PixelFormat::Yuv420P, planes)
.unwrap()
.with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 1));
let rgb = img.to_rgb8();
assert_eq!(&rgb[0..3], &[0, 0, 0]);
assert_eq!(&rgb[3..6], &[255, 255, 255]);
assert_eq!(&rgb[6..9], &[130, 130, 130]);
let full = img
.clone()
.with_color(ColorInfo::new(ColorRange::Full, 1, 1, 1));
let rgb = full.to_rgb8();
assert_eq!(&rgb[0..3], &[16, 16, 16]);
assert_eq!(&rgb[3..6], &[235, 235, 235]);
}
#[test]
fn deep_samples_scale_to_eight_bits_by_rounding() {
let words: Vec<u8> = [0u16, 1023, 512, 2]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
let img = AvifImage::new(4, 1, PixelFormat::Gray10Le, vec![Plane::new(8, words)]).unwrap();
let rgb = img.to_rgb8();
assert_eq!(rgb[0], 0);
assert_eq!(rgb[3], 255);
assert_eq!(rgb[6], 128);
assert_eq!(rgb[9], 0); assert_eq!(img.as_bytes().map(|b| b.len()), Some(8));
}
#[test]
fn unknown_matrix_falls_back_to_bt601_in_the_infallible_view() {
let planes = vec![
Plane::new(2, vec![16, 235, 128, 128]),
Plane::new(1, vec![128]),
Plane::new(1, vec![128]),
];
let img = AvifImage::new(2, 2, PixelFormat::Yuv420P, planes)
.unwrap()
.with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 131));
assert!(matches!(img.try_to_rgb8(), Err(Error::Unsupported(_))));
let bt601 = img
.clone()
.with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 6));
assert_eq!(img.to_rgb8(), bt601.to_rgb8());
assert_eq!(img.to_rgba8(), bt601.to_rgba8());
}
#[test]
fn packed_ya_carries_alpha() {
let img = AvifImage::new(
2,
1,
PixelFormat::Ya8,
vec![Plane::new(4, vec![200, 10, 50, 255])],
)
.unwrap();
let rgba = img.to_rgba8();
assert_eq!(rgba, vec![200, 200, 200, 10, 50, 50, 50, 255]);
assert!(img.has_alpha());
}
#[test]
fn inconsistent_planes_cannot_be_constructed() {
assert!(matches!(
AvifImage::new(4, 4, PixelFormat::Yuv420P, vec![Plane::new(4, vec![0; 4])]),
Err(Error::InvalidData(_))
));
let short = vec![
Plane::new(4, vec![0; 16]),
Plane::new(2, vec![0; 3]),
Plane::new(2, vec![0; 4]),
];
assert!(AvifImage::new(4, 4, PixelFormat::Yuv420P, short).is_err());
assert!(AvifImage::new(0, 4, PixelFormat::Gray8, vec![Plane::new(0, vec![])]).is_err());
let odd = vec![
Plane::new(3, vec![0; 9]),
Plane::new(4, vec![0; 8]),
Plane::new(2, vec![0; 4]),
];
let img = AvifImage::new(3, 3, PixelFormat::Yuv420P, odd).unwrap();
assert!(img.as_bytes().is_none());
assert_eq!(img.to_rgb8().len(), 27);
let mut broken =
AvifImage::new(2, 2, PixelFormat::Gray8, vec![Plane::new(2, vec![0; 4])]).unwrap();
broken.planes.clear();
assert!(broken.try_to_rgb8().is_err());
assert_eq!(broken.into_raw().len(), 0);
}
#[test]
fn quality_scale_maps_to_q_idx() {
assert_eq!(EncodeOptions::default().with_quality(100).base_q_idx, 0);
assert_eq!(EncodeOptions::default().with_quality(90).base_q_idx, 26);
assert_eq!(EncodeOptions::default().with_quality(50).base_q_idx, 128);
assert_eq!(EncodeOptions::default().with_quality(0).base_q_idx, 255);
assert_eq!(EncodeOptions::default().with_quality(200).base_q_idx, 0);
let o = EncodeOptions::new(3, 4, true);
assert_eq!(
(o.base_q_idx, o.alpha_q_idx, o.premultiplied_alpha),
(3, 4, true)
);
assert_eq!(o.chroma, StillChroma::Yuv420);
assert!(o.embed_exif && o.embed_xmp && o.embed_icc);
}
#[test]
fn exif_offset_word_is_stripped_and_rewritten() {
let body = [0, 0, 0, 2, 0xAA, 0xBB, b'M', b'M', 0, 42];
assert_eq!(exif_tiff_bytes(&body), Some(vec![b'M', b'M', 0, 42]));
assert_eq!(exif_tiff_bytes(&[0, 0, 0, 9, 1]), None);
assert_eq!(exif_tiff_bytes(&[0, 0]), None);
assert_eq!(exif_item_body(b"II"), vec![0, 0, 0, 0, b'I', b'I']);
}
#[test]
fn decode_limits_are_reported_as_limit_exceeded() {
let o = DecodeOptions::default()
.with_max_width(Some(8))
.with_max_pixels(Some(10));
assert!(matches!(o.check_dims(9, 1), Err(Error::LimitExceeded(_))));
assert!(matches!(o.check_dims(4, 4), Err(Error::LimitExceeded(_))));
assert!(o.check_dims(5, 2).is_ok());
assert!(matches!(
DecodeOptions::default()
.with_max_bytes(Some(3))
.check_bytes(4),
Err(Error::LimitExceeded(_))
));
let unlimited = DecodeOptions::default()
.with_max_width(None)
.with_max_height(None)
.with_max_pixels(None)
.with_max_bytes(None);
assert!(unlimited.check_dims(u32::MAX, u32::MAX).is_ok());
assert!(unlimited.check_bytes(usize::MAX).is_ok());
}
#[test]
fn info_describes_fixture_headers() {
let bytes = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/yuv420_10bit_full.avif"
))
.unwrap();
assert!(probe(&bytes));
let i = info(&bytes).unwrap();
assert_eq!(i.format, PixelFormat::Yuv420P10Le);
assert_eq!(i.bit_depth, 10);
assert_eq!(i.frames, 1);
assert!(!i.is_sequence);
assert_eq!(i.color.range, ColorRange::Full);
assert!(i.width > 0 && i.height > 0);
let bytes = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/alpha_video.avif"
))
.unwrap();
let i = info(&bytes).unwrap();
assert!(i.is_sequence);
assert!(i.frames > 1);
let bytes = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/monochrome.avif"
))
.unwrap();
let i = info(&bytes).unwrap();
assert_eq!(i.format, PixelFormat::Gray8);
assert!(!i.has_alpha);
assert!(info(b"not an avif file at all, just bytes").is_err());
}
}