use crate::color::Cicp;
use crate::encoder::{
encode_lossy_gray_obu, encode_still_lossy, encode_still_lossy_420, encode_still_lossy_422,
encode_yuv420_obu, encode_yuv422_obu, encode_yuv444_obu,
};
use crate::err::EncodeError;
use crate::metadata::{ContentLightLevel, Metadata, Orientation};
use crate::{BitDepth, PlanarImage, encode_lossless_gray_obu, isobmff};
use std::num::NonZeroUsize;
const MIN_DIM: u32 = 1;
const MAX_DIM: u32 = 16_383;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ChromaFormat {
#[default]
Yuv420,
Yuv422,
Yuv444,
Monochrome,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Speed {
#[default]
Slow,
Medium,
Fast,
}
impl Speed {
pub(crate) fn per_candidate_rdoq(self) -> bool {
matches!(self, Speed::Slow)
}
pub(crate) fn try_adst(self) -> bool {
true
}
pub(crate) fn try_angle_deltas(self) -> bool {
matches!(self, Speed::Slow)
}
pub(crate) fn reduced_modes(self) -> bool {
matches!(self, Speed::Fast)
}
pub(crate) fn chroma_angle_directional(self) -> bool {
!matches!(self, Speed::Fast)
}
pub(crate) fn try_directional(&self) -> bool {
true
}
}
#[derive(Clone, Debug)]
pub struct EncodeConfig {
pub quality: u8,
pub chroma: ChromaFormat,
pub color_encoding: Option<Cicp>,
pub icc: Option<Vec<u8>>,
pub metadata: Metadata,
pub threads: usize,
pub speed: Speed,
pub adaptive_quant: bool,
pub variance_boost: bool,
pub dark_aq: bool,
pub cdef: bool,
pub wiener: bool,
pub quantization_matrices: bool,
pub qmatrix_level: Option<u8>,
}
impl Default for EncodeConfig {
fn default() -> Self {
EncodeConfig {
quality: 80,
chroma: ChromaFormat::Yuv420,
color_encoding: Some(Cicp::srgb_ycbcr()),
icc: None,
metadata: Metadata::default(),
threads: std::thread::available_parallelism()
.unwrap_or(NonZeroUsize::new(1).unwrap())
.get(),
speed: Speed::Slow,
adaptive_quant: true,
variance_boost: true,
dark_aq: true,
cdef: false,
wiener: false,
quantization_matrices: false,
qmatrix_level: None,
}
}
}
impl EncodeConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_quality(mut self, quality: u8) -> Self {
self.quality = quality;
self
}
pub fn with_chroma(mut self, chroma: ChromaFormat) -> Self {
self.chroma = chroma;
self
}
pub fn with_cicp(mut self, color: Cicp) -> Self {
self.color_encoding = Some(color);
self
}
pub fn without_cicp(mut self) -> Self {
self.color_encoding = None;
self
}
pub fn with_icc_profile(mut self, icc: Vec<u8>) -> Self {
self.icc = Some(icc);
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_orientation(mut self, o: Orientation) -> Self {
self.metadata.orientation = o;
self
}
pub fn with_content_light_level(mut self, cll: ContentLightLevel) -> Self {
self.metadata.content_light_level = Some(cll);
self
}
pub fn with_exif(mut self, exif: Vec<u8>) -> Self {
self.metadata.exif = Some(exif);
self
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads;
self
}
pub fn with_speed(mut self, speed: Speed) -> Self {
self.speed = speed;
self
}
pub fn with_adaptive_quant(mut self, v: bool) -> Self {
self.adaptive_quant = v;
self
}
pub fn with_variance_boost(mut self, v: bool) -> Self {
self.variance_boost = v;
self
}
pub fn with_dark_aq(mut self, v: bool) -> Self {
self.dark_aq = v;
self
}
pub fn with_cdef(mut self, v: bool) -> Self {
self.cdef = v;
self
}
pub fn with_wiener(mut self, v: bool) -> Self {
self.wiener = v;
self
}
pub fn with_quantization_matrices(mut self, v: bool) -> Self {
self.quantization_matrices = v;
self
}
pub fn with_qmatrix_level(mut self, level: u8) -> Self {
self.quantization_matrices = true;
self.qmatrix_level = Some(level);
self
}
pub(crate) fn vb(&self) -> crate::coder::VarianceBoost {
let mut vb = if self.variance_boost {
crate::coder::VarianceBoost::on()
} else {
crate::coder::VarianceBoost::off()
};
vb.dark = if self.dark_aq {
crate::aq_common::DarkAq::on()
} else {
crate::aq_common::DarkAq::off()
};
vb.qm = if self.quantization_matrices {
self.qmatrix_level
.map(crate::quant::QmLevels::uniform)
.unwrap_or_else(|| crate::quant::QmLevels::uniform(10))
} else {
crate::quant::QmLevels::FLAT
};
vb
}
pub(crate) fn validate(&self) -> Result<(), EncodeError> {
validate_quality(self.quality)?;
if self.qmatrix_level.is_some_and(|level| level > 15) {
return Err(EncodeError::InvalidQuality);
}
Ok(())
}
}
pub(crate) fn validate_dims(width: u32, height: u32) -> Result<(), EncodeError> {
if width < MIN_DIM || height < MIN_DIM || width > MAX_DIM || height > MAX_DIM {
return Err(EncodeError::InvalidDimensions { width, height });
}
Ok(())
}
fn validate_quality(quality: u8) -> Result<(), EncodeError> {
if quality > 100 {
return Err(EncodeError::InvalidQuality);
}
Ok(())
}
pub(crate) fn validate_buf<T>(buf: &[T], w: u32, h: u32, ch: usize) -> Result<(), EncodeError> {
let needed = checked_buffer_size::<T>(w as usize, h as usize, ch)?;
if buf.len() != needed {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
pub(crate) fn checked_buffer_size<T>(w: usize, h: usize, ch: usize) -> Result<usize, EncodeError> {
_ = w
.checked_mul(h)
.and_then(|n| n.checked_mul(ch))
.and_then(|n| n.checked_mul(size_of::<T>()))
.and_then(|n| isize::try_from(n).ok())
.ok_or(EncodeError::DimensionTooLarge {
width: w,
height: h,
})?;
w.checked_mul(h)
.and_then(|n| n.checked_mul(ch))
.ok_or(EncodeError::DimensionTooLarge {
width: w,
height: h,
})
}
fn quality_to_q(quality: u8) -> u8 {
(1u32 + (100 - quality as u32) * 254 / 99).clamp(1, 255) as u8
}
fn av1_profile(bit_depth: u8, chroma: ChromaFormat) -> u8 {
match chroma {
_ if bit_depth == 12 => 2,
ChromaFormat::Yuv422 => 2,
ChromaFormat::Yuv444 => 1,
ChromaFormat::Yuv420 | ChromaFormat::Monochrome => 0,
}
}
pub(crate) fn make_av1c(
_obu: &[u8],
bit_depth: u8,
width: u32,
height: u32,
chroma: ChromaFormat,
) -> isobmff::Av1cParams {
let (sub_x, sub_y) = match chroma {
ChromaFormat::Yuv420 => (true, true),
ChromaFormat::Yuv422 => (true, false),
ChromaFormat::Yuv444 | ChromaFormat::Monochrome => (false, false),
};
isobmff::Av1cParams {
seq_profile: av1_profile(bit_depth, chroma),
seq_level_idx: isobmff::level_for(width, height),
high_bitdepth: bit_depth > 8,
twelve_bit: bit_depth == 12,
monochrome: matches!(chroma, ChromaFormat::Monochrome),
chroma_sub_x: sub_x,
chroma_sub_y: sub_y,
seq_header_obu: vec![],
}
}
pub(crate) fn finalize_color(
av1_obu: Vec<u8>,
width: u32,
height: u32,
bit_depth: u8,
chroma: ChromaFormat,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
let av1c = make_av1c(&av1_obu, bit_depth, width, height, chroma);
let channels: u8 = if matches!(chroma, ChromaFormat::Monochrome) {
1
} else {
3
};
isobmff::wrap_av1_image(
&av1_obu,
width,
height,
bit_depth,
channels,
&av1c,
cfg.color_encoding.as_ref(),
cfg.icc.as_deref(),
&cfg.metadata,
)
}
pub(crate) fn finalize_with_alpha(
color_obu: Vec<u8>,
alpha_obu: Vec<u8>,
width: u32,
height: u32,
bit_depth: u8,
chroma: ChromaFormat,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
let av1c_color = make_av1c(&color_obu, bit_depth, width, height, chroma);
let av1c_alpha = make_av1c(
&alpha_obu,
bit_depth,
width,
height,
ChromaFormat::Monochrome,
);
isobmff::wrap_av1_image_with_alpha(
&color_obu,
&alpha_obu,
width,
height,
bit_depth,
&av1c_color,
&av1c_alpha,
cfg.color_encoding.as_ref(),
cfg.icc.as_deref(),
&cfg.metadata,
)
}
#[allow(clippy::too_many_arguments)]
fn dispatch_lossy<T: crate::Pixel>(
img: &PlanarImage<T>,
q: u8,
chroma: ChromaFormat,
color: Option<&Cicp>,
threads: usize,
speed: Speed,
aq: bool,
vb: crate::coder::VarianceBoost,
cdef: bool,
wiener: bool,
) -> Vec<u8> {
match chroma {
ChromaFormat::Yuv420 | ChromaFormat::Monochrome => {
encode_still_lossy_420(img, q, color, threads, speed, aq, vb, cdef, wiener)
}
ChromaFormat::Yuv422 => {
encode_still_lossy_422(img, q, color, threads, speed, aq, vb, cdef, wiener)
}
ChromaFormat::Yuv444 => {
encode_still_lossy(img, q, color, threads, speed, aq, vb, cdef, wiener)
}
}
}
pub fn encode_rgb8(img: &PlanarImage<u8>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
let obu = dispatch_lossy(
&img.packed_3(),
quality_to_q(cfg.quality),
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
finalize_color(obu, img.width as u32, img.height as u32, 8, cfg.chroma, cfg)
}
pub fn encode_rgba8(img: &PlanarImage<u8>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let obu = dispatch_lossy(
&img.packed_3(),
quality_to_q(cfg.quality),
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
finalize_color(obu, img.width as u32, img.height as u32, 8, cfg.chroma, cfg)
}
pub fn encode_rgba8_with_alpha(
img: &PlanarImage<u8>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = dispatch_lossy(
&img.packed_3(),
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_4(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
8,
cfg.chroma,
cfg,
)
}
pub fn encode_rgb10(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
let obu = dispatch_lossy(
&img.packed_3(),
quality_to_q(cfg.quality),
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
finalize_color(
obu,
img.width as u32,
img.height as u32,
10,
cfg.chroma,
cfg,
)
}
pub fn encode_rgba10(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let obu = dispatch_lossy(
&img.packed_3(),
quality_to_q(cfg.quality),
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
finalize_color(
obu,
img.width as u32,
img.height as u32,
10,
cfg.chroma,
cfg,
)
}
pub fn encode_rgba10_with_alpha(
img: &PlanarImage<u16>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = dispatch_lossy(
&img.packed_3(),
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_4(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
10,
cfg.chroma,
cfg,
)
}
pub fn encode_rgb12(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
let obu = dispatch_lossy(
&img.packed_3(),
quality_to_q(cfg.quality),
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
finalize_color(
obu,
img.width as u32,
img.height as u32,
12,
cfg.chroma,
cfg,
)
}
pub fn encode_rgba12(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
let obu = dispatch_lossy(
&img.packed_3(),
quality_to_q(cfg.quality),
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
finalize_color(
obu,
img.width as u32,
img.height as u32,
12,
cfg.chroma,
cfg,
)
}
pub fn encode_rgba12_with_alpha(
img: &PlanarImage<u16>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[2], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = dispatch_lossy(
&img.packed_3(),
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
);
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_4(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
12,
cfg.chroma,
cfg,
)
}
pub fn encode_gray8(img: &PlanarImage<u8>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let obu = encode_lossy_gray_obu(
&img.packed_1(),
BitDepth::Eight,
q,
true,
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
finalize_color(
obu,
img.width as u32,
img.height as u32,
8,
ChromaFormat::Monochrome,
cfg,
)
}
pub fn encode_gray10(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let obu = encode_lossy_gray_obu(
&img.packed_1(),
BitDepth::Ten,
q,
true,
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
finalize_color(
obu,
img.width as u32,
img.height as u32,
10,
ChromaFormat::Monochrome,
cfg,
)
}
pub fn encode_gray12(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let obu = encode_lossy_gray_obu(
&img.packed_1(),
BitDepth::Twelve,
q,
true,
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
finalize_color(
obu,
img.width as u32,
img.height as u32,
12,
ChromaFormat::Monochrome,
cfg,
)
}
pub fn encode_yuv8(img: &PlanarImage<u8>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
img.validate_with(cfg.chroma)?;
let q = quality_to_q(cfg.quality);
let obu = dispatch_yuv_u8(
img,
BitDepth::Eight,
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
finalize_color(obu, img.width as u32, img.height as u32, 8, cfg.chroma, cfg)
}
pub fn encode_yuv10(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
img.validate_with(cfg.chroma)?;
let q = quality_to_q(cfg.quality);
let obu = dispatch_yuv_u16(
img,
BitDepth::Ten,
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
finalize_color(
obu,
img.width as u32,
img.height as u32,
10,
cfg.chroma,
cfg,
)
}
pub fn encode_yuv12(img: &PlanarImage<u16>, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
img.validate_with(cfg.chroma)?;
let q = quality_to_q(cfg.quality);
let obu = dispatch_yuv_u16(
img,
BitDepth::Twelve,
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
finalize_color(
obu,
img.width as u32,
img.height as u32,
12,
cfg.chroma,
cfg,
)
}
pub fn encode_yuva8_with_alpha(
img: &PlanarImage<u8>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
img.validate_with(cfg.chroma)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = dispatch_yuv_u8(
&img.packed_3(),
BitDepth::Eight,
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_4(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
8,
cfg.chroma,
cfg,
)
}
pub fn encode_yuva10_with_alpha(
img: &PlanarImage<u16>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
img.validate_with(cfg.chroma)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = dispatch_yuv_u16(
&img.packed_3(),
BitDepth::Ten,
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_4(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
10,
cfg.chroma,
cfg,
)
}
pub fn encode_yuva12_with_alpha(
img: &PlanarImage<u16>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
img.validate_with(cfg.chroma)?;
validate_buf(&img.planes[3], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = dispatch_yuv_u16(
&img.packed_3(),
BitDepth::Twelve,
q,
cfg.chroma,
cfg.color_encoding.as_ref(),
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_4(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
12,
cfg.chroma,
cfg,
)
}
pub fn encode_gray_alpha8(
img: &PlanarImage<u8>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Eight {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = encode_lossy_gray_obu(
&img.packed_1(),
BitDepth::Eight,
q,
true,
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_2(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
8,
ChromaFormat::Monochrome,
cfg,
)
}
pub fn encode_gray_alpha10(
img: &PlanarImage<u16>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Ten {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = encode_lossy_gray_obu(
&img.packed_1(),
BitDepth::Ten,
q,
true,
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_2(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
10,
ChromaFormat::Monochrome,
cfg,
)
}
pub fn encode_gray_alpha12(
img: &PlanarImage<u16>,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if img.bit_depth != BitDepth::Twelve {
return Err(EncodeError::UnsupportedChromaBitDepth(img.bit_depth));
}
validate_dims(img.width as u32, img.height as u32)?;
cfg.validate()?;
validate_buf(&img.planes[0], img.width as u32, img.height as u32, 1)?;
validate_buf(&img.planes[1], img.width as u32, img.height as u32, 1)?;
let q = quality_to_q(cfg.quality);
let color_obu = encode_lossy_gray_obu(
&img.packed_1(),
BitDepth::Twelve,
q,
true,
cfg.threads,
cfg.speed,
cfg.adaptive_quant,
cfg.vb(),
cfg.cdef,
cfg.wiener,
)?;
let alpha_obu = encode_lossless_gray_obu(&img.packed_alpha_2(), true, cfg.threads)?;
finalize_with_alpha(
color_obu,
alpha_obu,
img.width as u32,
img.height as u32,
12,
ChromaFormat::Monochrome,
cfg,
)
}
#[allow(clippy::too_many_arguments)]
fn dispatch_yuv_u8(
planar_image: &PlanarImage<u8>,
bd: BitDepth,
q: u8,
chroma: ChromaFormat,
color: Option<&Cicp>,
threads: usize,
speed: Speed,
aq: bool,
vb: crate::coder::VarianceBoost,
cdef: bool,
wiener: bool,
) -> Result<Vec<u8>, EncodeError> {
planar_image.validate_with(chroma)?;
match chroma {
ChromaFormat::Yuv420 => encode_yuv420_obu(
planar_image,
bd,
q,
color,
threads,
speed,
aq,
vb,
cdef,
wiener,
),
ChromaFormat::Yuv422 => encode_yuv422_obu(
planar_image,
bd,
q,
color,
threads,
speed,
aq,
vb,
cdef,
wiener,
),
ChromaFormat::Yuv444 | ChromaFormat::Monochrome => encode_yuv444_obu(
planar_image,
bd,
q,
color,
threads,
speed,
aq,
vb,
cdef,
wiener,
),
}
}
#[allow(clippy::too_many_arguments)]
fn dispatch_yuv_u16(
planar_image: &PlanarImage<u16>,
bd: BitDepth,
q: u8,
chroma: ChromaFormat,
color: Option<&Cicp>,
threads: usize,
speed: Speed,
aq: bool,
vb: crate::coder::VarianceBoost,
cdef: bool,
wiener: bool,
) -> Result<Vec<u8>, EncodeError> {
planar_image.validate_with(chroma)?;
match chroma {
ChromaFormat::Yuv420 => encode_yuv420_obu(
planar_image,
bd,
q,
color,
threads,
speed,
aq,
vb,
cdef,
wiener,
),
ChromaFormat::Yuv422 => encode_yuv422_obu(
planar_image,
bd,
q,
color,
threads,
speed,
aq,
vb,
cdef,
wiener,
),
ChromaFormat::Yuv444 | ChromaFormat::Monochrome => encode_yuv444_obu(
planar_image,
bd,
q,
color,
threads,
speed,
aq,
vb,
cdef,
wiener,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::process::Command;
fn patterned_rgb(width: usize, height: usize) -> PlanarImage<u8> {
let mut rgb = vec![0u8; width * height * 3];
for y in 0..height {
for x in 0..width {
let i = (y * width + x) * 3;
rgb[i] = ((x * 37 + y * 11) & 255) as u8;
rgb[i + 1] = (((x / 4) * 83 + (y / 7) * 29) & 255) as u8;
rgb[i + 2] = ((x * 13 + y * 53 + ((x ^ y) & 15) * 7) & 255) as u8;
}
}
PlanarImage::from_interleaved_rgb(width, height, BitDepth::Eight, &rgb).unwrap()
}
fn png_dimensions(bytes: &[u8]) -> Option<(usize, usize)> {
if bytes.len() < 24 || &bytes[..8] != b"\x89PNG\r\n\x1a\n" || &bytes[12..16] != b"IHDR" {
return None;
}
let width = u32::from_be_bytes(bytes[16..20].try_into().ok()?) as usize;
let height = u32::from_be_bytes(bytes[20..24].try_into().ok()?) as usize;
Some((width, height))
}
fn temp_path(stem: &str, ext: &str) -> PathBuf {
std::env::temp_dir().join(format!("maroontree-{stem}-{}.{ext}", std::process::id()))
}
#[test]
fn chroma_partition_boundary_sizes_encode_and_decode() {
let sizes = [
(8, 8),
(9, 9),
(15, 17),
(16, 16),
(17, 19),
(31, 33),
(32, 32),
(33, 35),
(63, 65),
(64, 64),
(65, 67),
(127, 95),
];
let formats = [
ChromaFormat::Yuv420,
ChromaFormat::Yuv422,
ChromaFormat::Yuv444,
];
let avifdec = std::env::var_os("AVIFDEC");
for &(width, height) in &sizes {
let image = patterned_rgb(width, height);
for &chroma in &formats {
let cfg = EncodeConfig::new()
.with_quality(35)
.with_chroma(chroma)
.with_threads(1)
.with_speed(Speed::Fast)
.with_adaptive_quant(false);
let avif = encode_rgb8(&image, &cfg)
.unwrap_or_else(|e| panic!("{width}x{height} {chroma:?} encode failed: {e}"));
assert!(
!avif.is_empty(),
"{width}x{height} {chroma:?} produced an empty AVIF"
);
if let Some(decoder) = &avifdec {
let tag = format!("{width}x{height}-{chroma:?}");
let input = temp_path(&tag, "avif");
let output = temp_path(&tag, "png");
std::fs::write(&input, &avif).unwrap();
let result = Command::new(decoder)
.arg(&input)
.arg(&output)
.output()
.unwrap();
assert!(
result.status.success(),
"avifdec rejected {width}x{height} {chroma:?}: {}",
String::from_utf8_lossy(&result.stderr)
);
let decoded = std::fs::read(&output).unwrap();
assert_eq!(
png_dimensions(&decoded),
Some((width, height)),
"decoded dimensions changed for {width}x{height} {chroma:?}"
);
let _ = std::fs::remove_file(input);
let _ = std::fs::remove_file(output);
}
}
}
}
}