#![allow(dead_code)]
#![deny(unsafe_code)]
use crate::error::{Error, Result};
use crate::image::{ChromaSampling, ColorRange};
use crate::yuv_convert::{self, YuvMatrix, YuvRange};
use rgb::{Rgb, Rgba};
use whereat::at;
use zenpixels::{PixelBuffer, PixelDescriptor};
use rav1d_safe::src::managed::{Frame, Planes};
pub(crate) struct StripConverter {
state: ConversionState,
descriptor: PixelDescriptor,
display_width: usize,
display_height: usize,
}
#[allow(clippy::large_enum_variant)]
enum ConversionState {
Frames8 {
primary: Frame,
alpha: Option<Frame>,
chroma_sampling: ChromaSampling,
yuv_range: YuvRange,
yuv_matrix: YuvMatrix,
alpha_range: ColorRange,
premultiplied: bool,
#[allow(dead_code)]
buffer_width: usize,
buffer_height: usize,
},
FullPixels(PixelBuffer),
}
impl StripConverter {
#[allow(clippy::too_many_arguments)]
pub fn new(
primary: Frame,
alpha: Option<Frame>,
chroma_sampling: ChromaSampling,
yuv_range: YuvRange,
yuv_matrix: YuvMatrix,
alpha_range: ColorRange,
premultiplied: bool,
display_width: usize,
display_height: usize,
buffer_width: usize,
buffer_height: usize,
descriptor: PixelDescriptor,
) -> Self {
let bit_depth = primary.bit_depth();
if bit_depth == 8 && !matches!(chroma_sampling, ChromaSampling::Monochrome) {
StripConverter {
state: ConversionState::Frames8 {
primary,
alpha,
chroma_sampling,
yuv_range,
yuv_matrix,
alpha_range,
premultiplied,
buffer_width,
buffer_height,
},
descriptor,
display_width,
display_height,
}
} else {
panic!(
"StripConverter::new called for unsupported format: bit_depth={}, chroma={:?}. \
Use new_from_pixels for 16-bit and monochrome images.",
bit_depth, chroma_sampling
);
}
}
pub fn new_from_pixels(pixels: PixelBuffer) -> Self {
let w = pixels.width() as usize;
let h = pixels.height() as usize;
let desc = pixels.descriptor();
StripConverter {
state: ConversionState::FullPixels(pixels),
descriptor: desc,
display_width: w,
display_height: h,
}
}
pub fn descriptor(&self) -> PixelDescriptor {
self.descriptor
}
pub fn display_width(&self) -> usize {
self.display_width
}
pub fn display_height(&self) -> usize {
self.display_height
}
#[allow(dead_code)]
pub fn is_true_streaming(&self) -> bool {
matches!(self.state, ConversionState::Frames8 { .. })
}
pub fn optimal_strip_height(&self) -> usize {
let width = self.display_width;
let bpp = self.descriptor.bytes_per_pixel();
let row_bytes_out = width * bpp;
let row_bytes_in = width + width; let total_per_row = row_bytes_out + row_bytes_in;
let target_bytes = 256 * 1024;
let mut h = if total_per_row > 0 {
target_bytes / total_per_row
} else {
16
};
h = h.clamp(2, 64);
if let ConversionState::Frames8 {
chroma_sampling: ChromaSampling::Cs420,
..
} = &self.state
{
h &= !1; if h == 0 {
h = 2;
}
}
h
}
pub fn convert_strip(
&self,
y_start: usize,
strip_height: usize,
out_buf: &mut PixelBuffer,
) -> Result<()> {
match &self.state {
ConversionState::Frames8 {
primary,
alpha,
chroma_sampling,
yuv_range,
yuv_matrix,
alpha_range,
premultiplied,
buffer_width: _,
buffer_height,
} => self.convert_strip_8bit(
primary,
alpha.as_ref(),
*chroma_sampling,
*yuv_range,
*yuv_matrix,
*alpha_range,
*premultiplied,
*buffer_height,
y_start,
strip_height,
out_buf,
),
ConversionState::FullPixels(pixels) => {
let bpp = pixels.descriptor().bytes_per_pixel();
let src_slice = pixels.as_slice();
let mut dst = out_buf.as_slice_mut();
let width = self.display_width;
for row in 0..strip_height {
let src_row = src_slice.row((y_start + row) as u32);
let dst_row = dst.row_mut(row as u32);
let copy_bytes = width * bpp;
dst_row[..copy_bytes].copy_from_slice(&src_row[..copy_bytes]);
}
Ok(())
}
}
}
#[allow(clippy::too_many_arguments)]
fn convert_strip_8bit(
&self,
primary: &Frame,
alpha: Option<&Frame>,
chroma_sampling: ChromaSampling,
yuv_range: YuvRange,
yuv_matrix: YuvMatrix,
alpha_range: ColorRange,
premultiplied: bool,
buffer_height: usize,
y_start: usize,
strip_height: usize,
out_buf: &mut PixelBuffer,
) -> Result<()> {
let Planes::Depth8(planes) = primary.planes() else {
return Err(at!(Error::Decode {
code: -1,
msg: "Expected 8-bit planes",
}));
};
let y_view = planes.y();
let width = self.display_width;
let has_alpha = alpha.is_some();
if has_alpha {
let mut img = out_buf
.try_as_imgref_mut::<Rgba<u8>>()
.ok_or_else(|| at!(Error::Unsupported("expected RGBA8 buffer for alpha image")))?;
let out_rgba = img.buf_mut();
let u_view = planes.u().ok_or_else(|| {
at!(Error::Decode {
code: -1,
msg: "Missing U plane",
})
})?;
let v_view = planes.v().ok_or_else(|| {
at!(Error::Decode {
code: -1,
msg: "Missing V plane",
})
})?;
match chroma_sampling {
ChromaSampling::Cs420 => yuv_convert::yuv420_to_rgba8_strip(
y_view.as_slice(),
y_view.stride(),
u_view.as_slice(),
u_view.stride(),
v_view.as_slice(),
v_view.stride(),
width,
buffer_height,
y_start,
strip_height,
yuv_range,
yuv_matrix,
out_rgba,
),
ChromaSampling::Cs422 => yuv_convert::yuv422_to_rgba8_strip(
y_view.as_slice(),
y_view.stride(),
u_view.as_slice(),
u_view.stride(),
v_view.as_slice(),
v_view.stride(),
width,
y_start,
strip_height,
yuv_range,
yuv_matrix,
out_rgba,
),
ChromaSampling::Cs444 => yuv_convert::yuv444_to_rgba8_strip(
y_view.as_slice(),
y_view.stride(),
u_view.as_slice(),
u_view.stride(),
v_view.as_slice(),
v_view.stride(),
width,
y_start,
strip_height,
yuv_range,
yuv_matrix,
out_rgba,
),
ChromaSampling::Monochrome => {
return Err(at!(Error::Decode {
code: -1,
msg: "Monochrome should not reach strip chroma conversion",
}));
}
}
if let Some(alpha_frame) = alpha {
let Planes::Depth8(alpha_planes) = alpha_frame.planes() else {
return Err(at!(Error::Decode {
code: -1,
msg: "Expected 8-bit alpha plane",
}));
};
let alpha_y = alpha_planes.y();
for row in 0..strip_height {
let src_y = y_start + row;
if src_y >= alpha_y.height() {
break;
}
let alpha_row = alpha_y.row(src_y);
let out_row = &mut out_rgba[row * width..(row + 1) * width];
for (px, &a) in out_row.iter_mut().zip(alpha_row.iter()) {
px.a = match alpha_range {
ColorRange::Full => a,
ColorRange::Limited => limited_to_full_8(a),
};
}
if premultiplied {
crate::convert::unpremultiply8(out_row);
}
}
}
} else {
let mut img = out_buf.try_as_imgref_mut::<Rgb<u8>>().ok_or_else(|| {
at!(Error::Unsupported(
"expected RGB8 buffer for non-alpha image",
))
})?;
let out_rgb = img.buf_mut();
let u_view = planes.u().ok_or_else(|| {
at!(Error::Decode {
code: -1,
msg: "Missing U plane",
})
})?;
let v_view = planes.v().ok_or_else(|| {
at!(Error::Decode {
code: -1,
msg: "Missing V plane",
})
})?;
match chroma_sampling {
ChromaSampling::Cs420 => yuv_convert::yuv420_to_rgb8_strip(
y_view.as_slice(),
y_view.stride(),
u_view.as_slice(),
u_view.stride(),
v_view.as_slice(),
v_view.stride(),
width,
buffer_height,
y_start,
strip_height,
yuv_range,
yuv_matrix,
out_rgb,
),
ChromaSampling::Cs422 => yuv_convert::yuv422_to_rgb8_strip(
y_view.as_slice(),
y_view.stride(),
u_view.as_slice(),
u_view.stride(),
v_view.as_slice(),
v_view.stride(),
width,
y_start,
strip_height,
yuv_range,
yuv_matrix,
out_rgb,
),
ChromaSampling::Cs444 => yuv_convert::yuv444_to_rgb8_strip(
y_view.as_slice(),
y_view.stride(),
u_view.as_slice(),
u_view.stride(),
v_view.as_slice(),
v_view.stride(),
width,
y_start,
strip_height,
yuv_range,
yuv_matrix,
out_rgb,
),
ChromaSampling::Monochrome => {
return Err(at!(Error::Decode {
code: -1,
msg: "Monochrome should not reach strip chroma conversion",
}));
}
}
}
Ok(())
}
}
#[inline]
fn limited_to_full_8(y: u8) -> u8 {
let y = y as i32;
((y - 16).max(0) * 255 / 219).min(255) as u8
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yuv_convert::{YuvMatrix, YuvRange};
use rgb::RGB8;
#[test]
fn strip_matches_full_frame_420() {
let width = 24;
let height = 16;
let chroma_w = width / 2;
let chroma_h = height / 2;
let mut y_plane = vec![0u8; width * height];
let mut u_plane = vec![128u8; chroma_w * chroma_h];
let mut v_plane = vec![128u8; chroma_w * chroma_h];
for y in 0..height {
for x in 0..width {
y_plane[y * width + x] = ((x * 10 + y * 7) % 256) as u8;
}
}
for y in 0..chroma_h {
for x in 0..chroma_w {
u_plane[y * chroma_w + x] = ((x * 13 + y * 11 + 64) % 256) as u8;
v_plane[y * chroma_w + x] = ((x * 17 + y * 3 + 96) % 256) as u8;
}
}
let full = crate::yuv_convert::yuv420_to_rgb8(
&y_plane,
width,
&u_plane,
chroma_w,
&v_plane,
chroma_w,
width,
height,
YuvRange::Full,
YuvMatrix::Bt709,
);
for strip_h in [2, 4, 6, 8, 16] {
let mut strip_result = vec![RGB8::default(); width * height];
let mut y_start = 0;
while y_start < height {
let h = strip_h.min(height - y_start);
let strip_out = &mut strip_result[y_start * width..(y_start + h) * width];
crate::yuv_convert::yuv420_to_rgb8_strip(
&y_plane,
width,
&u_plane,
chroma_w,
&v_plane,
chroma_w,
width,
height,
y_start,
h,
YuvRange::Full,
YuvMatrix::Bt709,
strip_out,
);
y_start += h;
}
assert_eq!(
full.buf(),
strip_result.as_slice(),
"Strip conversion (h={strip_h}) doesn't match full-frame"
);
}
}
#[test]
fn rgba_strip_has_alpha_255() {
let width = 8;
let height = 4;
let y_plane = vec![128u8; width * height];
let u_plane = vec![128u8; (width / 2) * (height / 2)];
let v_plane = vec![128u8; (width / 2) * (height / 2)];
let mut out = vec![
Rgba {
r: 0,
g: 0,
b: 0,
a: 0
};
width * height
];
crate::yuv_convert::yuv420_to_rgba8_strip(
&y_plane,
width,
&u_plane,
width / 2,
&v_plane,
width / 2,
width,
height,
0,
height,
YuvRange::Full,
YuvMatrix::Bt709,
&mut out,
);
for px in &out {
assert_eq!(px.a, 255, "Alpha should be 255");
}
}
}