use oxideav_vp8::{decode_vp8, DecodeError, Vp8DecodedFrame};
pub fn decode_lossy_rgba(bitstream: &[u8]) -> Result<(u32, u32, Vec<u8>), DecodeError> {
let frame = decode_vp8(bitstream)?;
let (w, h) = (frame.width, frame.height);
let rgba = yuv420_to_rgba(&frame);
Ok((w, h, rgba))
}
pub fn yuv420_to_rgba(frame: &Vp8DecodedFrame) -> Vec<u8> {
let w = frame.width as usize;
let h = frame.height as usize;
let uv_w = w.div_ceil(2);
let mut rgba = vec![0u8; w * h * 4];
let y_plane = &frame.y[..w * h];
let u_plane = &frame.u[..uv_w * h.div_ceil(2)];
let v_plane = &frame.v[..uv_w * h.div_ceil(2)];
for y in 0..h {
let y_row = &y_plane[y * w..y * w + w];
let uv_base = (y / 2) * uv_w;
let u_row = &u_plane[uv_base..uv_base + uv_w];
let v_row = &v_plane[uv_base..uv_base + uv_w];
let out_row = &mut rgba[y * w * 4..y * w * 4 + w * 4];
let mut k = 0usize;
let mut x = 0usize;
while x + 1 < w {
let (cr_off, cg_off, cb_off) = chroma_offsets(u_row[k], v_row[k]);
let p0 = x * 4;
let y0 = y_row[x] as i32;
out_row[p0] = clamp_u8(((y0 << 16) + cr_off) >> 16);
out_row[p0 + 1] = clamp_u8(((y0 << 16) + cg_off) >> 16);
out_row[p0 + 2] = clamp_u8(((y0 << 16) + cb_off) >> 16);
out_row[p0 + 3] = 0xff;
let p1 = (x + 1) * 4;
let y1 = y_row[x + 1] as i32;
out_row[p1] = clamp_u8(((y1 << 16) + cr_off) >> 16);
out_row[p1 + 1] = clamp_u8(((y1 << 16) + cg_off) >> 16);
out_row[p1 + 2] = clamp_u8(((y1 << 16) + cb_off) >> 16);
out_row[p1 + 3] = 0xff;
k += 1;
x += 2;
}
if x < w {
let (cr_off, cg_off, cb_off) = chroma_offsets(u_row[k], v_row[k]);
let p = x * 4;
let yv = y_row[x] as i32;
out_row[p] = clamp_u8(((yv << 16) + cr_off) >> 16);
out_row[p + 1] = clamp_u8(((yv << 16) + cg_off) >> 16);
out_row[p + 2] = clamp_u8(((yv << 16) + cb_off) >> 16);
out_row[p + 3] = 0xff;
}
}
rgba
}
#[inline]
fn chroma_offsets(cb: u8, cv: u8) -> (i32, i32, i32) {
const HALF: i32 = 1 << 15; let d = cb as i32 - 128; let e = cv as i32 - 128; (
91_881 * e + HALF,
-22_554 * d - 46_802 * e + HALF,
116_130 * d + HALF,
)
}
#[cfg(test)]
fn ycbcr_to_rgb(y: u8, cb: u8, cv: u8) -> [u8; 3] {
let yi = (y as i32) << 16;
let (cr_off, cg_off, cb_off) = chroma_offsets(cb, cv);
let r = (yi + cr_off) >> 16;
let g = (yi + cg_off) >> 16;
let b = (yi + cb_off) >> 16;
[clamp_u8(r), clamp_u8(g), clamp_u8(b)]
}
#[inline]
fn clamp_u8(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ycbcr_neutral_chroma_is_grey() {
for y in [0u8, 1, 64, 127, 128, 200, 255] {
assert_eq!(ycbcr_to_rgb(y, 128, 128), [y, y, y]);
}
}
#[test]
fn ycbcr_pure_primaries_round_to_expected() {
let red = ycbcr_to_rgb(128, 128, 255);
assert_eq!(red[0], 255); assert!(red[1] < 60, "green low for max-Cr, got {}", red[1]);
assert_eq!(red[2], 128);
let blue = ycbcr_to_rgb(128, 255, 128);
assert_eq!(blue[2], 255); assert_eq!(blue[0], 128); assert!(blue[1] < 100, "green low for max-Cb, got {}", blue[1]);
}
#[test]
fn ycbcr_clamps_out_of_range() {
assert_eq!(ycbcr_to_rgb(255, 128, 255)[0], 255);
assert_eq!(ycbcr_to_rgb(0, 128, 0)[0], 0);
}
#[test]
fn yuv420_to_rgba_produces_flat_buffer_with_opaque_alpha() {
let frame = Vp8DecodedFrame {
width: 2,
height: 2,
y: vec![10, 20, 30, 40],
u: vec![128],
v: vec![128],
};
let rgba = yuv420_to_rgba(&frame);
assert_eq!(rgba.len(), 2 * 2 * 4);
assert_eq!(&rgba[0..4], &[10, 10, 10, 0xff]);
assert_eq!(&rgba[4..8], &[20, 20, 20, 0xff]);
assert_eq!(&rgba[8..12], &[30, 30, 30, 0xff]);
assert_eq!(&rgba[12..16], &[40, 40, 40, 0xff]);
}
fn yuv420_to_rgba_reference(frame: &Vp8DecodedFrame) -> Vec<u8> {
let w = frame.width as usize;
let h = frame.height as usize;
let uv_w = w.div_ceil(2);
let mut rgba = Vec::with_capacity(w * h * 4);
for y in 0..h {
let y_row = y * w;
let uv_row = (y / 2) * uv_w;
for x in 0..w {
let [r, g, b] = ycbcr_to_rgb(
frame.y[y_row + x],
frame.u[uv_row + (x / 2)],
frame.v[uv_row + (x / 2)],
);
rgba.extend_from_slice(&[r, g, b, 0xff]);
}
}
rgba
}
#[test]
fn yuv420_to_rgba_matches_per_pixel_reference_across_dimensions() {
for &(w, h) in &[
(1u32, 1u32),
(2, 2),
(3, 1),
(1, 3),
(3, 3),
(5, 4),
(16, 16),
(17, 9),
(32, 31),
] {
let (wu, hu) = (w as usize, h as usize);
let uv_w = wu.div_ceil(2);
let uv_h = hu.div_ceil(2);
let y: Vec<u8> = (0..wu * hu).map(|i| ((i * 37 + 11) & 0xff) as u8).collect();
let u: Vec<u8> = (0..uv_w * uv_h)
.map(|i| ((i * 53 + 200) & 0xff) as u8)
.collect();
let v: Vec<u8> = (0..uv_w * uv_h)
.map(|i| ((i * 71 + 7) & 0xff) as u8)
.collect();
let frame = Vp8DecodedFrame {
width: w,
height: h,
y,
u,
v,
};
assert_eq!(
yuv420_to_rgba(&frame),
yuv420_to_rgba_reference(&frame),
"mismatch at {}x{}",
w,
h
);
}
}
#[test]
fn yuv420_to_rgba_handles_odd_dimensions() {
let frame = Vp8DecodedFrame {
width: 3,
height: 1,
y: vec![100, 110, 120],
u: vec![128, 128],
v: vec![128, 128],
};
let rgba = yuv420_to_rgba(&frame);
assert_eq!(rgba.len(), 3 * 4);
assert_eq!(&rgba[0..4], &[100, 100, 100, 0xff]);
assert_eq!(&rgba[4..8], &[110, 110, 110, 0xff]);
assert_eq!(&rgba[8..12], &[120, 120, 120, 0xff]);
}
}