use yuv::{YuvPlanarImage, yuv420_to_bgra, yuv420_to_rgba};
use crate::{Color, Error, Size, Surface};
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Config {
pub color: Option<Color>,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone)]
pub struct Rgba {
width: u32,
height: u32,
stride: usize,
data: Vec<u8>,
}
impl Rgba {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn stride(&self) -> usize {
self.stride
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn into_data(self) -> Vec<u8> {
self.data
}
}
#[derive(Clone)]
pub struct Bgra {
width: u32,
height: u32,
stride: usize,
data: Vec<u8>,
}
impl Bgra {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn stride(&self) -> usize {
self.stride
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn into_data(self) -> Vec<u8> {
self.data
}
}
struct Packed {
width: u32,
height: u32,
stride: usize,
data: Vec<u8>,
}
type Convert =
fn(&YuvPlanarImage<'_, u8>, &mut [u8], u32, yuv::YuvRange, yuv::YuvStandardMatrix) -> Result<(), yuv::YuvError>;
fn packed(surface: &Surface, config: &Config, convert: Convert, name: &str) -> Result<Packed, Error> {
let size = Size::new(surface.width(), surface.height());
let color = config
.color
.or_else(|| surface.color())
.unwrap_or_else(|| Color::infer(size));
let i420 = surface.to_i420()?;
let luma = usize::try_from(size.pixels()).map_err(|_| {
Error::Codec(anyhow::anyhow!(
"{name} frame {size}: dimensions too large to represent"
))
})?;
let stride = size.width.checked_mul(4).ok_or_else(|| {
Error::Codec(anyhow::anyhow!(
"{name} frame {size}: row stride is too large to represent"
))
})?;
let stride_usize = usize::try_from(stride).map_err(|_| {
Error::Codec(anyhow::anyhow!(
"{name} frame {size}: row stride is too large to represent"
))
})?;
let len = stride_usize.checked_mul(size.height as usize).ok_or_else(|| {
Error::Codec(anyhow::anyhow!(
"{name} frame {size}: byte length is too large to represent"
))
})?;
let chroma = luma / 4;
let planar = YuvPlanarImage {
y_plane: &i420.data[..luma],
y_stride: size.width,
u_plane: &i420.data[luma..luma + chroma],
u_stride: size.width / 2,
v_plane: &i420.data[luma + chroma..],
v_stride: size.width / 2,
width: size.width,
height: size.height,
};
let mut data = vec![0; len];
let (range, matrix) = color.yuv();
convert(&planar, &mut data, stride, range, matrix)
.map_err(|e| Error::Codec(anyhow::anyhow!("{name} conversion failed for {size}: {e}")))?;
Ok(Packed {
width: size.width,
height: size.height,
stride: stride_usize,
data,
})
}
pub(crate) fn rgba(surface: &Surface, config: &Config) -> Result<Rgba, Error> {
let packed = packed(surface, config, yuv420_to_rgba, "RGBA")?;
Ok(Rgba {
width: packed.width,
height: packed.height,
stride: packed.stride,
data: packed.data,
})
}
pub(crate) fn bgra(surface: &Surface, config: &Config) -> Result<Bgra, Error> {
let packed = packed(surface, config, yuv420_to_bgra, "BGRA")?;
Ok(Bgra {
width: packed.width,
height: packed.height,
stride: packed.stride,
data: packed.data,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::I420;
#[test]
fn conversion_uses_the_surface_color() {
let source_size = Size::new(64, 64);
let red = [255u8, 0, 0, 255].repeat(source_size.pixels() as usize);
let source = I420::from_rgba(&red, source_size.width * 4, source_size.width, source_size.height).unwrap();
let source = source.resize(1280, 720).unwrap();
assert_eq!(source.color(), Some(Color::Bt601Limited));
assert_eq!(Color::infer(Size::new(1280, 720)), Color::Bt709Limited);
let image = rgba(&Surface::I420(source), &Config::default()).unwrap();
let center = (image.height as usize / 2 * image.stride) + image.width as usize / 2 * 4;
let pixel = &image.data[center..center + 4];
assert!(pixel[0] >= 250, "red channel drifted: {pixel:?}");
assert!(pixel[1] <= 2 && pixel[2] <= 2, "surface matrix was ignored: {pixel:?}");
assert_eq!(pixel[3], 255);
}
#[test]
fn conversion_reports_a_tightly_packed_layout() {
let size = Size::new(64, 32);
let surface = Surface::I420(I420::new(size.width, size.height, vec![128; I420::len(64, 32)]).unwrap());
let image = rgba(&surface, &Config::default()).unwrap();
assert_eq!(image.width(), size.width);
assert_eq!(image.height(), size.height);
assert_eq!(image.stride(), size.width as usize * 4);
assert_eq!(image.data().len(), image.stride() * size.height as usize);
}
#[test]
fn bgra_is_rgba_with_red_and_blue_exchanged() {
let size = Size::new(64, 64);
let source = [200u8, 40, 90, 255].repeat(size.pixels() as usize);
let i420 = I420::from_rgba(&source, size.width * 4, size.width, size.height).unwrap();
let surface = Surface::I420(i420);
let as_rgba = rgba(&surface, &Config::default()).unwrap();
let as_bgra = bgra(&surface, &Config::default()).unwrap();
assert_eq!(as_bgra.width(), as_rgba.width());
assert_eq!(as_bgra.height(), as_rgba.height());
assert_eq!(as_bgra.stride(), as_rgba.stride());
for (index, (rgba, bgra)) in as_rgba
.data()
.as_chunks::<4>()
.0
.iter()
.zip(as_bgra.data().as_chunks::<4>().0.iter())
.enumerate()
{
assert_eq!(
[bgra[0], bgra[1], bgra[2], bgra[3]],
[rgba[2], rgba[1], rgba[0], rgba[3]],
"pixel {index}: {bgra:?} is not {rgba:?} with red and blue exchanged",
);
}
}
#[test]
fn conversion_leaves_the_surface_alone() {
let size = Size::new(32, 32);
let surface = Surface::I420(I420::new(size.width, size.height, vec![128; I420::len(32, 32)]).unwrap());
let first = rgba(&surface, &Config::default()).unwrap();
let second = bgra(&surface, &Config::default()).unwrap();
assert_eq!(first.data().len(), second.data().len());
assert_eq!(surface.width(), size.width);
}
}