use crate::{
BitDepth, ChromaFormat, EncodeConfig, EncodeError, encode_yuv8, encode_yuv8_266, encode_yuv10,
encode_yuv10_266, encode_yuv12, encode_yuv12_266, encode_yuva8_with_alpha,
encode_yuva10_with_alpha, encode_yuva12_with_alpha,
};
#[derive(Clone, Copy, Debug)]
pub struct Yuv<'a, T> {
pub y: &'a [T],
pub y_stride: u32,
pub u: &'a [T],
pub v: &'a [T],
pub uv_stride: u32,
pub alpha: Option<&'a [T]>,
pub alpha_stride: u32,
pub width: u32,
pub height: u32,
pub chroma: ChromaFormat,
}
impl<'a, T> Yuv<'a, T> {
pub fn new(
y: &'a [T],
u: &'a [T],
v: &'a [T],
width: u32,
height: u32,
chroma: ChromaFormat,
) -> Self {
let uv_stride = if chroma.is_monochrome() {
0
} else {
(width as usize).div_ceil(chroma.sub_w()) as u32
};
Self {
y,
y_stride: width,
u,
v,
uv_stride,
alpha: None,
alpha_stride: 0,
width,
height,
chroma,
}
}
pub fn mono(y: &'a [T], width: u32, height: u32) -> Self {
Self::new(y, &[], &[], width, height, ChromaFormat::Monochrome)
}
pub fn with_strides(mut self, y_stride: u32, uv_stride: u32) -> Self {
self.y_stride = y_stride;
self.uv_stride = uv_stride;
self
}
pub fn with_alpha(mut self, alpha: &'a [T]) -> Self {
self.alpha = Some(alpha);
self.alpha_stride = self.width;
self
}
pub fn with_alpha_strided(mut self, alpha: &'a [T], stride: u32) -> Self {
self.alpha = Some(alpha);
self.alpha_stride = stride;
self
}
fn chroma_dims(&self) -> (usize, usize) {
if self.chroma.is_monochrome() {
(0, 0)
} else {
(
(self.width as usize).div_ceil(self.chroma.sub_w()),
(self.height as usize).div_ceil(self.chroma.sub_h()),
)
}
}
}
fn check_plane<T>(p: &[T], stride: usize, cols: usize, rows: usize) -> Result<(), EncodeError> {
if rows == 0 || cols == 0 {
return Ok(());
}
if stride < cols || p.len() < (rows - 1) * stride + cols {
return Err(EncodeError::Unsupported(
"a YUV/alpha plane is too small for its stride and dimensions",
));
}
Ok(())
}
fn tighten<T: Copy>(out: &mut Vec<T>, p: &[T], stride: usize, cols: usize, rows: usize) {
for r in 0..rows {
let start = r * stride;
out.extend_from_slice(&p[start..start + cols]);
}
}
impl<T: Copy> Yuv<'_, T> {
fn pack_color(&self) -> Result<Vec<T>, EncodeError> {
let (w, h) = (self.width as usize, self.height as usize);
let (dcw, dch) = self.chroma_dims();
let ys = self.y_stride as usize;
check_plane(self.y, ys, w, h)?;
let mut out = Vec::with_capacity(w * h + 2 * dcw * dch);
tighten(&mut out, self.y, ys, w, h);
if !self.chroma.is_monochrome() {
let cs = self.uv_stride as usize;
check_plane(self.u, cs, dcw, dch)?;
check_plane(self.v, cs, dcw, dch)?;
tighten(&mut out, self.u, cs, dcw, dch);
tighten(&mut out, self.v, cs, dcw, dch);
}
Ok(out)
}
fn pack_alpha(&self, alpha: &[T]) -> Result<Vec<T>, EncodeError> {
let (w, h) = (self.width as usize, self.height as usize);
let st = self.alpha_stride as usize;
check_plane(alpha, st, w, h)?;
let mut out = Vec::with_capacity(w * h);
tighten(&mut out, alpha, st, w, h);
Ok(out)
}
}
impl Yuv<'_, u8> {
pub fn encode_266(&self, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
let cfg = cfg.clone().with_chroma(self.chroma);
let packed = self.pack_color()?;
encode_yuv8_266(&packed, self.width, self.height, &cfg)
}
pub fn encode(&self, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
let cfg = cfg.clone().with_chroma(self.chroma);
let packed = self.pack_color()?;
match self.alpha {
None => encode_yuv8(&packed, self.width, self.height, &cfg),
Some(a) => {
let ap = self.pack_alpha(a)?;
encode_yuva8_with_alpha(&packed, &ap, self.width, self.height, &cfg)
}
}
}
}
impl Yuv<'_, u16> {
pub fn encode_266(&self, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
let cfg = cfg.clone().with_chroma(self.chroma);
let packed = self.pack_color()?;
match cfg.bit_depth {
BitDepth::Ten => encode_yuv10_266(&packed, self.width, self.height, &cfg),
BitDepth::Twelve => encode_yuv12_266(&packed, self.width, self.height, &cfg),
BitDepth::Eight => Err(EncodeError::Unsupported(
"u16 Yuv requires a 10- or 12-bit EncodeConfig",
)),
}
}
pub fn encode(&self, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
let cfg = cfg.clone().with_chroma(self.chroma);
let packed = self.pack_color()?;
let (w, h) = (self.width, self.height);
match (cfg.bit_depth, self.alpha) {
(BitDepth::Ten, None) => encode_yuv10(&packed, w, h, &cfg),
(BitDepth::Twelve, None) => encode_yuv12(&packed, w, h, &cfg),
(BitDepth::Ten, Some(a)) => {
let ap = self.pack_alpha(a)?;
encode_yuva10_with_alpha(&packed, &ap, w, h, &cfg)
}
(BitDepth::Twelve, Some(a)) => {
let ap = self.pack_alpha(a)?;
encode_yuva12_with_alpha(&packed, &ap, w, h, &cfg)
}
(BitDepth::Eight, _) => Err(EncodeError::Unsupported(
"u16 Yuv requires a 10- or 12-bit EncodeConfig",
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
decode, encode_yuv8_266, encode_yuv10_266, encode_yuv12_266, encode_yuva8_with_alpha,
encode_yuva10_with_alpha, encode_yuva12_with_alpha,
};
fn dims(w: usize, h: usize, c: ChromaFormat) -> (usize, usize) {
if c.is_monochrome() {
(0, 0)
} else {
(w.div_ceil(c.sub_w()), h.div_ceil(c.sub_h()))
}
}
fn make<T: From<u8> + Copy>(
w: usize,
h: usize,
c: ChromaFormat,
mask: u32,
) -> (Vec<u16>, Vec<u16>, Vec<u16>, Vec<u16>) {
let _ = std::marker::PhantomData::<T>;
let (cw, ch) = dims(w, h, c);
let y: Vec<u16> = (0..w * h)
.map(|i| ((i as u32 * 37 + 11) & mask) as u16)
.collect();
let cb: Vec<u16> = (0..cw * ch)
.map(|i| ((i as u32 * 53 + 7) & mask) as u16)
.collect();
let cr: Vec<u16> = (0..cw * ch)
.map(|i| ((i as u32 * 29 + 19) & mask) as u16)
.collect();
let a: Vec<u16> = (0..w * h)
.map(|i| ((i as u32 * 17 + 3) & mask) as u16)
.collect();
(y, cb, cr, a)
}
fn packed16(y: &[u16], cb: &[u16], cr: &[u16]) -> Vec<u16> {
let mut p = Vec::with_capacity(y.len() + cb.len() + cr.len());
p.extend_from_slice(y);
p.extend_from_slice(cb);
p.extend_from_slice(cr);
p
}
#[test]
fn struct_matches_packed_8bit_all_formats() {
let (w, h) = (24usize, 16usize);
let cfg = EncodeConfig::new().with_quality(80);
for c in [
ChromaFormat::Yuv444,
ChromaFormat::Yuv422,
ChromaFormat::Yuv420,
ChromaFormat::Monochrome,
] {
let (y16, cb16, cr16, _) = make::<u8>(w, h, c, 0xFF);
let (y, cb, cr): (Vec<u8>, Vec<u8>, Vec<u8>) = (
y16.iter().map(|&v| v as u8).collect(),
cb16.iter().map(|&v| v as u8).collect(),
cr16.iter().map(|&v| v as u8).collect(),
);
let mut packed = y.clone();
if !c.is_monochrome() {
packed.extend_from_slice(&cb);
packed.extend_from_slice(&cr);
}
let reference =
encode_yuv8_266(&packed, w as u32, h as u32, &cfg.clone().with_chroma(c)).unwrap();
let img = if c.is_monochrome() {
Yuv::mono(&y, w as u32, h as u32)
} else {
Yuv::new(&y, &cb, &cr, w as u32, h as u32, c)
};
let got = img.encode_266(&cfg).unwrap();
assert_eq!(got, reference, "8-bit {c:?} struct vs packed");
}
}
#[test]
fn struct_matches_packed_high_bit() {
let (w, h) = (16usize, 16usize);
for (bd, mask) in [(BitDepth::Ten, 0x3FFu32), (BitDepth::Twelve, 0xFFF)] {
let cfg = EncodeConfig::new().with_quality(85).with_bit_depth(bd);
for c in [
ChromaFormat::Yuv444,
ChromaFormat::Yuv422,
ChromaFormat::Yuv420,
] {
let (y, cb, cr, _) = make::<u16>(w, h, c, mask);
let packed = packed16(&y, &cb, &cr);
let reference = match bd {
BitDepth::Ten => {
encode_yuv10_266(&packed, w as u32, h as u32, &cfg.clone().with_chroma(c))
.unwrap()
}
_ => encode_yuv12_266(&packed, w as u32, h as u32, &cfg.clone().with_chroma(c))
.unwrap(),
};
let got = Yuv::new(&y, &cb, &cr, w as u32, h as u32, c)
.encode_266(&cfg)
.unwrap();
assert_eq!(got, reference, "{bd:?} {c:?} struct vs packed");
}
}
}
#[test]
fn strided_planes_match_tight() {
let (w, h) = (20usize, 12usize);
let c = ChromaFormat::Yuv420;
let (cw, ch) = dims(w, h, c);
let cfg = EncodeConfig::new().with_quality(75);
let (y16, cb16, cr16, _) = make::<u8>(w, h, c, 0xFF);
let (y, cb, cr): (Vec<u8>, Vec<u8>, Vec<u8>) = (
y16.iter().map(|&v| v as u8).collect(),
cb16.iter().map(|&v| v as u8).collect(),
cr16.iter().map(|&v| v as u8).collect(),
);
let tight = Yuv::new(&y, &cb, &cr, w as u32, h as u32, c)
.encode_266(&cfg)
.unwrap();
let (ypad, cpad) = (7usize, 5usize);
let mut ys = vec![0u8; (w + ypad) * h];
for r in 0..h {
ys[r * (w + ypad)..r * (w + ypad) + w].copy_from_slice(&y[r * w..r * w + w]);
}
let mut cbs = vec![0u8; (cw + cpad) * ch];
let mut crs = vec![0u8; (cw + cpad) * ch];
for r in 0..ch {
cbs[r * (cw + cpad)..r * (cw + cpad) + cw].copy_from_slice(&cb[r * cw..r * cw + cw]);
crs[r * (cw + cpad)..r * (cw + cpad) + cw].copy_from_slice(&cr[r * cw..r * cw + cw]);
}
let strided = Yuv::new(&ys, &cbs, &crs, w as u32, h as u32, c)
.with_strides((w + ypad) as u32, (cw + cpad) as u32)
.encode_266(&cfg)
.unwrap();
assert_eq!(
strided, tight,
"strided planes must produce the tight stream"
);
}
#[test]
fn alpha_struct_matches_packed_all_depths() {
let (w, h) = (16usize, 16usize);
let c = ChromaFormat::Yuv444;
{
let cfg = EncodeConfig::new().with_quality(70);
let (y16, cb16, cr16, a16) = make::<u8>(w, h, c, 0xFF);
let (y, cb, cr, a): (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>) = (
y16.iter().map(|&v| v as u8).collect(),
cb16.iter().map(|&v| v as u8).collect(),
cr16.iter().map(|&v| v as u8).collect(),
a16.iter().map(|&v| v as u8).collect(),
);
let mut packed = y.clone();
packed.extend_from_slice(&cb);
packed.extend_from_slice(&cr);
let reference = encode_yuva8_with_alpha(
&packed,
&a,
w as u32,
h as u32,
&cfg.clone().with_chroma(c),
)
.unwrap();
let got = Yuv::new(&y, &cb, &cr, w as u32, h as u32, c)
.with_alpha(&a)
.encode(&cfg)
.unwrap();
assert_eq!(got, reference, "8-bit alpha struct vs packed");
}
for (bd, mask) in [(BitDepth::Ten, 0x3FFu32), (BitDepth::Twelve, 0xFFF)] {
let cfg = EncodeConfig::new().with_quality(70).with_bit_depth(bd);
let (y, cb, cr, a) = make::<u16>(w, h, c, mask);
let packed = packed16(&y, &cb, &cr);
let reference = match bd {
BitDepth::Ten => encode_yuva10_with_alpha(
&packed,
&a,
w as u32,
h as u32,
&cfg.clone().with_chroma(c),
)
.unwrap(),
_ => encode_yuva12_with_alpha(
&packed,
&a,
w as u32,
h as u32,
&cfg.clone().with_chroma(c),
)
.unwrap(),
};
let got = Yuv::new(&y, &cb, &cr, w as u32, h as u32, c)
.with_alpha(&a)
.encode(&cfg)
.unwrap();
assert_eq!(got, reference, "{bd:?} alpha struct vs packed");
}
}
#[test]
fn lossless_roundtrip_through_struct() {
let (w, h) = (16usize, 16usize);
let cfg = EncodeConfig::new().with_lossless(true);
for c in [
ChromaFormat::Yuv444,
ChromaFormat::Yuv422,
ChromaFormat::Yuv420,
] {
let (y16, cb16, cr16, _) = make::<u8>(w, h, c, 0xFF);
let (y, cb, cr): (Vec<u8>, Vec<u8>, Vec<u8>) = (
y16.iter().map(|&v| v as u8).collect(),
cb16.iter().map(|&v| v as u8).collect(),
cr16.iter().map(|&v| v as u8).collect(),
);
let heif = Yuv::new(&y, &cb, &cr, w as u32, h as u32, c)
.encode(&cfg)
.unwrap();
let dec = decode(&heif).unwrap();
let (cw, ch) = dims(w, h, c);
let n = w * h;
assert_eq!(&dec.planes[..n], &y[..], "luma {c:?}");
assert_eq!(&dec.planes[n..n + cw * ch], &cb[..], "Cb {c:?}");
assert_eq!(
&dec.planes[n + cw * ch..n + 2 * cw * ch],
&cr[..],
"Cr {c:?}"
);
}
}
}