#[cfg(feature = "registry")]
use std::collections::VecDeque;
#[cfg(feature = "registry")]
use oxideav_core::Encoder;
#[cfg(feature = "registry")]
use oxideav_core::{
CodecId, CodecParameters, Frame, MediaType, Packet, PixelFormat, Rational, TimeBase,
VideoFrame, VideoPlane,
};
#[cfg(feature = "registry")]
use oxideav_vp8::encoder::{
make_encoder_with_config, LoopFilterMode, Vp8EncoderConfig, DEFAULT_QINDEX,
};
use crate::error::{Result, WebpError as Error};
use crate::riff::AlphChunkBytes;
#[cfg(feature = "registry")]
use crate::riff::{build_webp_file, ImageKind, WebpMetadata};
use crate::vp8l::encode_vp8l_argb;
#[cfg(feature = "registry")]
use crate::CODEC_ID_VP8;
#[cfg(feature = "registry")]
pub fn make_encoder(params: &CodecParameters) -> oxideav_core::Result<Box<dyn Encoder>> {
make_encoder_with_qindex(params, DEFAULT_QINDEX)
}
#[cfg(feature = "registry")]
pub fn make_encoder_with_quality(
params: &CodecParameters,
quality: f32,
) -> oxideav_core::Result<Box<dyn Encoder>> {
make_encoder_with_qindex(params, quality_to_qindex(quality))
}
pub fn quality_to_qindex(quality: f32) -> u8 {
if quality.is_nan() {
return 127;
}
let q = quality.clamp(0.0, 100.0);
((100.0 - q) * 1.27).round().clamp(0.0, 127.0) as u8
}
fn segment_quant_deltas_for_qindex(qindex: u8) -> [i32; 4] {
let span = (qindex as f32) / 127.0;
let smooth = -((2.0 + span * 10.0).round() as i32);
let mid_low = -((1.0 + span * 5.0).round() as i32);
let high = (1.0 + span * 7.0).round() as i32;
[
smooth.clamp(-15, 15),
mid_low.clamp(-15, 15),
0,
high.clamp(-15, 15),
]
}
fn freq_deltas_for_qindex(qindex: u8) -> Vp8FreqDeltas {
let qi = qindex.min(127);
let span = (qi as f32) / 127.0;
let y2_dc = -((span * 2.0).round() as i32);
let high_ac = (span * 4.0).round() as i32;
Vp8FreqDeltas {
y_dc_delta: 0,
y2_dc_delta: y2_dc.clamp(-15, 15),
y2_ac_delta: high_ac.clamp(-15, 15),
uv_dc_delta: 0,
uv_ac_delta: high_ac.clamp(-15, 15),
}
}
fn segment_lf_deltas_for_qindex(qindex: u8) -> [i32; 4] {
let span = (qindex as f32) / 127.0;
let smooth_lf = -((span * 3.0).round() as i32);
let mid_low_lf = -((span * 2.0).round() as i32);
let high_lf = (1.0 + span * 3.0).round() as i32;
[
smooth_lf.clamp(-63, 63),
mid_low_lf.clamp(-63, 63),
0,
high_lf.clamp(-63, 63),
]
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Vp8FreqDeltas {
pub y_dc_delta: i32,
pub y2_dc_delta: i32,
pub y2_ac_delta: i32,
pub uv_dc_delta: i32,
pub uv_ac_delta: i32,
}
#[cfg(feature = "registry")]
fn webp_lossy_config(qindex: u8, freq_deltas: Vp8FreqDeltas) -> Vp8EncoderConfig {
let qi = qindex.min(127);
Vp8EncoderConfig {
qindex: qi,
enable_scene_cut: false,
enable_lookahead_altref: false,
loop_filter_mode: LoopFilterMode::Normal,
enable_segments: true,
segment_quant_deltas: segment_quant_deltas_for_qindex(qi),
segment_lf_deltas: segment_lf_deltas_for_qindex(qi),
y_dc_delta: freq_deltas.y_dc_delta,
y2_dc_delta: freq_deltas.y2_dc_delta,
y2_ac_delta: freq_deltas.y2_ac_delta,
uv_dc_delta: freq_deltas.uv_dc_delta,
uv_ac_delta: freq_deltas.uv_ac_delta,
..Vp8EncoderConfig::default()
}
}
#[cfg(feature = "registry")]
fn encode_keyframe_with_segments(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
frame: &VideoFrame,
) -> Result<Vec<u8>> {
let cfg = webp_lossy_config(qindex, freq_deltas);
let mut p = CodecParameters::video(CodecId::new(oxideav_vp8::CODEC_ID_STR));
p.width = Some(width);
p.height = Some(height);
p.pixel_format = Some(PixelFormat::Yuv420P);
let mut enc = match make_encoder_with_config(&p, cfg) {
Ok(e) => e,
Err(e) => return Err(Error::invalid(format!("vp8 segment encoder: {e}"))),
};
let f = Frame::Video(frame.clone());
if let Err(e) = enc.send_frame(&f) {
return Err(Error::invalid(format!("vp8 segment encoder send: {e}")));
}
if let Err(e) = enc.flush() {
return Err(Error::invalid(format!("vp8 segment encoder flush: {e}")));
}
let pkt = match enc.receive_packet() {
Ok(p) => p,
Err(e) => return Err(Error::invalid(format!("vp8 segment encoder receive: {e}"))),
};
Ok(pkt.data)
}
#[cfg(feature = "registry")]
pub fn make_encoder_with_qindex(
params: &CodecParameters,
qindex: u8,
) -> oxideav_core::Result<Box<dyn Encoder>> {
make_encoder_with_qindex_and_freq_deltas(params, qindex, freq_deltas_for_qindex(qindex))
}
#[cfg(feature = "registry")]
pub fn make_encoder_with_qindex_and_freq_deltas(
params: &CodecParameters,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
) -> oxideav_core::Result<Box<dyn Encoder>> {
let width = params
.width
.ok_or_else(|| oxideav_core::Error::invalid("VP8 WebP encoder: missing width"))?;
let height = params
.height
.ok_or_else(|| oxideav_core::Error::invalid("VP8 WebP encoder: missing height"))?;
if width == 0 || height == 0 || width > 16383 || height > 16383 {
return Err(oxideav_core::Error::invalid(format!(
"VP8 WebP encoder: dimensions {width}x{height} out of range (1..=16383)"
)));
}
let pix = params.pixel_format.unwrap_or(PixelFormat::Yuv420P);
if !matches!(
pix,
PixelFormat::Yuv420P | PixelFormat::Yuva420P | PixelFormat::Rgba | PixelFormat::Rgb24
) {
return Err(oxideav_core::Error::unsupported(format!(
"VP8 WebP encoder: pixel format {pix:?} not supported — \
feed Yuv420P / Yuva420P / Rgba / Rgb24"
)));
}
let frame_rate = params.frame_rate.unwrap_or(Rational::new(1, 1));
let mut output_params = params.clone();
output_params.media_type = MediaType::Video;
output_params.codec_id = CodecId::new(CODEC_ID_VP8);
output_params.pixel_format = Some(pix);
output_params.width = Some(width);
output_params.height = Some(height);
output_params.frame_rate = Some(frame_rate);
let time_base = TimeBase::new(1, 1000);
Ok(Box::new(Vp8WebpEncoder {
output_params,
width,
height,
qindex: qindex.min(127),
freq_deltas,
input_format: pix,
time_base,
pending: VecDeque::new(),
eof: false,
}))
}
#[cfg(feature = "registry")]
pub fn make_encoder_with_quality_and_freq_deltas(
params: &CodecParameters,
quality: f32,
freq_deltas: Vp8FreqDeltas,
) -> oxideav_core::Result<Box<dyn Encoder>> {
make_encoder_with_qindex_and_freq_deltas(params, quality_to_qindex(quality), freq_deltas)
}
#[cfg(feature = "registry")]
struct Vp8WebpEncoder {
output_params: CodecParameters,
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
input_format: PixelFormat,
time_base: TimeBase,
pending: VecDeque<Packet>,
eof: bool,
}
#[cfg(feature = "registry")]
impl Encoder for Vp8WebpEncoder {
fn codec_id(&self) -> &CodecId {
&self.output_params.codec_id
}
fn output_params(&self) -> &CodecParameters {
&self.output_params
}
fn send_frame(&mut self, frame: &Frame) -> oxideav_core::Result<()> {
let v = match frame {
Frame::Video(v) => v,
_ => {
return Err(oxideav_core::Error::invalid(
"VP8 WebP encoder: video frames only",
))
}
};
let bytes = match self.input_format {
PixelFormat::Yuv420P => {
let vp8 = encode_keyframe_with_segments(
self.width,
self.height,
self.qindex,
self.freq_deltas,
v,
)?;
build_webp_file(
ImageKind::Vp8Lossy,
&vp8,
self.width,
self.height,
None,
&WebpMetadata::default(),
)
}
PixelFormat::Yuva420P => {
encode_yuva420_lossy(self.width, self.height, self.qindex, self.freq_deltas, v)?
}
PixelFormat::Rgba => {
encode_rgba_lossy(self.width, self.height, self.qindex, self.freq_deltas, v)?
}
PixelFormat::Rgb24 => {
encode_rgb24_lossy(self.width, self.height, self.qindex, self.freq_deltas, v)?
}
other => {
return Err(oxideav_core::Error::unsupported(format!(
"VP8 WebP encoder: frame format {other:?} unsupported"
)))
}
};
let mut pkt = Packet::new(0, self.time_base, bytes);
pkt.pts = v.pts;
pkt.dts = pkt.pts;
pkt.flags.keyframe = true;
self.pending.push_back(pkt);
Ok(())
}
fn receive_packet(&mut self) -> oxideav_core::Result<Packet> {
if let Some(p) = self.pending.pop_front() {
return Ok(p);
}
if self.eof {
Err(oxideav_core::Error::Eof)
} else {
Err(oxideav_core::Error::NeedMore)
}
}
fn flush(&mut self) -> oxideav_core::Result<()> {
self.eof = true;
Ok(())
}
}
#[cfg(feature = "registry")]
fn encode_yuva420_lossy(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
v: &VideoFrame,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if v.planes.len() < 4 {
return Err(Error::invalid(
"VP8 WebP encoder: Yuva420P frame needs 4 planes (Y, U, V, A)",
));
}
let cw = w / 2 + (w & 1);
if v.planes[0].stride < w
|| v.planes[1].stride < cw
|| v.planes[2].stride < cw
|| v.planes[3].stride < w
{
return Err(Error::invalid(
"VP8 WebP encoder: Yuva420P plane stride too small",
));
}
let yuv_frame = VideoFrame {
pts: v.pts,
planes: vec![
v.planes[0].clone(),
v.planes[1].clone(),
v.planes[2].clone(),
],
};
let vp8_bytes = encode_keyframe_with_segments(width, height, qindex, freq_deltas, &yuv_frame)?;
let alpha_plane = &v.planes[3];
let mut alpha = Vec::with_capacity(w * h);
for j in 0..h {
let row_start = j * alpha_plane.stride;
alpha.extend_from_slice(&alpha_plane.data[row_start..row_start + w]);
}
let alph = encode_alph_chunk(width, height, &alpha)?;
Ok(build_webp_file(
ImageKind::Vp8Lossy,
&vp8_bytes,
width,
height,
Some(&alph),
&WebpMetadata::default(),
))
}
#[cfg(feature = "registry")]
fn encode_rgb24_lossy(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
v: &VideoFrame,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if v.planes.is_empty() {
return Err(Error::invalid(
"VP8 WebP encoder: RGB24 frame has no planes",
));
}
let plane = &v.planes[0];
if plane.stride < w * 3 {
return Err(Error::invalid(
"VP8 WebP encoder: RGB24 stride too small for frame width",
));
}
let (y, u, v_chroma) = rgb24_rows_to_yuv420(w, h, plane.stride, &plane.data);
let yuv_frame = VideoFrame {
pts: v.pts,
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: w / 2 + (w & 1),
data: u,
},
VideoPlane {
stride: w / 2 + (w & 1),
data: v_chroma,
},
],
};
let vp8_bytes = encode_keyframe_with_segments(width, height, qindex, freq_deltas, &yuv_frame)?;
Ok(build_webp_file(
ImageKind::Vp8Lossy,
&vp8_bytes,
width,
height,
None,
&WebpMetadata::default(),
))
}
#[cfg(feature = "registry")]
fn encode_rgba_lossy(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
v: &VideoFrame,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if v.planes.is_empty() {
return Err(Error::invalid("VP8 WebP encoder: RGBA frame has no planes"));
}
let plane = &v.planes[0];
if plane.stride < w * 4 {
return Err(Error::invalid(
"VP8 WebP encoder: RGBA stride too small for frame width",
));
}
let mut alpha = Vec::with_capacity(w * h);
let (y, u, v_chroma) = rgba_rows_to_yuv420(w, h, plane.stride, &plane.data, &mut alpha);
let yuv_frame = VideoFrame {
pts: v.pts,
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: w / 2 + (w & 1),
data: u,
},
VideoPlane {
stride: w / 2 + (w & 1),
data: v_chroma,
},
],
};
let vp8_bytes = encode_keyframe_with_segments(width, height, qindex, freq_deltas, &yuv_frame)?;
let alph = encode_alph_chunk(width, height, &alpha)?;
Ok(build_webp_file(
ImageKind::Vp8Lossy,
&vp8_bytes,
width,
height,
Some(&alph),
&WebpMetadata::default(),
))
}
pub(crate) fn rgba_rows_to_yuv420(
w: usize,
h: usize,
stride: usize,
rgba: &[u8],
alpha: &mut Vec<u8>,
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let cw = w / 2 + (w & 1);
let ch = h / 2 + (h & 1);
let mut y_plane = vec![0u8; w * h];
let mut u_plane = vec![0u8; cw * ch];
let mut v_plane = vec![0u8; cw * ch];
for j in 0..h {
let row_start = j * stride;
for i in 0..w {
let px = &rgba[row_start + i * 4..row_start + i * 4 + 4];
let r = px[0] as i32;
let g = px[1] as i32;
let b = px[2] as i32;
alpha.push(px[3]);
let y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
y_plane[j * w + i] = y.clamp(0, 255) as u8;
}
}
for cy in 0..ch {
for cx in 0..cw {
let mut u_sum = 0i32;
let mut v_sum = 0i32;
let mut n = 0i32;
for dy in 0..2 {
let jj = cy * 2 + dy;
if jj >= h {
break;
}
for dx in 0..2 {
let ii = cx * 2 + dx;
if ii >= w {
break;
}
let px = &rgba[jj * stride + ii * 4..jj * stride + ii * 4 + 4];
let r = px[0] as i32;
let g = px[1] as i32;
let b = px[2] as i32;
u_sum += (-38 * r - 74 * g + 112 * b + 128) >> 8;
v_sum += (112 * r - 94 * g - 18 * b + 128) >> 8;
n += 1;
}
}
let u = (u_sum / n) + 128;
let v = (v_sum / n) + 128;
u_plane[cy * cw + cx] = u.clamp(0, 255) as u8;
v_plane[cy * cw + cx] = v.clamp(0, 255) as u8;
}
}
(y_plane, u_plane, v_plane)
}
fn rgb24_rows_to_yuv420(
w: usize,
h: usize,
stride: usize,
rgb: &[u8],
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let cw = w / 2 + (w & 1);
let ch = h / 2 + (h & 1);
let mut y_plane = vec![0u8; w * h];
let mut u_plane = vec![0u8; cw * ch];
let mut v_plane = vec![0u8; cw * ch];
for j in 0..h {
let row_start = j * stride;
for i in 0..w {
let px = &rgb[row_start + i * 3..row_start + i * 3 + 3];
let r = px[0] as i32;
let g = px[1] as i32;
let b = px[2] as i32;
let y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
y_plane[j * w + i] = y.clamp(0, 255) as u8;
}
}
for cy in 0..ch {
for cx in 0..cw {
let mut u_sum = 0i32;
let mut v_sum = 0i32;
let mut n = 0i32;
for dy in 0..2 {
let jj = cy * 2 + dy;
if jj >= h {
break;
}
for dx in 0..2 {
let ii = cx * 2 + dx;
if ii >= w {
break;
}
let px = &rgb[jj * stride + ii * 3..jj * stride + ii * 3 + 3];
let r = px[0] as i32;
let g = px[1] as i32;
let b = px[2] as i32;
u_sum += (-38 * r - 74 * g + 112 * b + 128) >> 8;
v_sum += (112 * r - 94 * g - 18 * b + 128) >> 8;
n += 1;
}
}
let u = (u_sum / n) + 128;
let v = (v_sum / n) + 128;
u_plane[cy * cw + cx] = u.clamp(0, 255) as u8;
v_plane[cy * cw + cx] = v.clamp(0, 255) as u8;
}
}
(y_plane, u_plane, v_plane)
}
fn encode_alpha_plane_as_vp8l(width: u32, height: u32, alpha: &[u8]) -> Result<Vec<u8>> {
debug_assert_eq!(alpha.len(), (width as usize) * (height as usize));
let mut pixels = Vec::with_capacity(alpha.len());
for &a in alpha {
let g = a as u32;
pixels.push(0xff00_0000 | (g << 8));
}
let full_bitstream = encode_vp8l_argb(width, height, &pixels, false)?;
if full_bitstream.len() <= 5 {
return Err(Error::invalid(
"VP8 WebP encoder: VP8L alpha bitstream too short to strip header",
));
}
Ok(full_bitstream[5..].to_vec())
}
fn apply_alph_filter(plane: &mut [u8], w: usize, h: usize, mode: u8) {
match mode {
0 => {}
1 => {
for y in 0..h {
for x in (1..w).rev() {
let i = y * w + x;
let left = plane[i - 1];
plane[i] = plane[i].wrapping_sub(left);
}
}
}
2 => {
for y in (1..h).rev() {
for x in 0..w {
let i = y * w + x;
let top = plane[i - w];
plane[i] = plane[i].wrapping_sub(top);
}
}
}
3 => {
for y in (0..h).rev() {
for x in (0..w).rev() {
let i = y * w + x;
let pred: i32 = if y == 0 && x == 0 {
0
} else if y == 0 {
plane[i - 1] as i32
} else if x == 0 {
plane[i - w] as i32
} else {
let l = plane[i - 1] as i32;
let t = plane[i - w] as i32;
let tl = plane[i - w - 1] as i32;
(l + t - tl).clamp(0, 255)
};
plane[i] = (plane[i] as i32 - pred) as u8;
}
}
}
_ => {}
}
}
fn alph_filter_cost(plane: &[u8]) -> u64 {
let mut s: u64 = 0;
for &b in plane {
let bb = b as u64;
s += bb.min(256 - bb);
}
s
}
pub(crate) fn encode_alph_chunk(width: u32, height: u32, alpha: &[u8]) -> Result<AlphChunkBytes> {
let w = width as usize;
let h = height as usize;
debug_assert_eq!(alpha.len(), w * h);
let mut best_mode: u8 = 0;
let mut best_cost = alph_filter_cost(alpha);
for mode in 1u8..=3 {
let mut scratch = alpha.to_vec();
apply_alph_filter(&mut scratch, w, h, mode);
let cost = alph_filter_cost(&scratch);
if cost < best_cost {
best_cost = cost;
best_mode = mode;
}
}
let mut filtered = alpha.to_vec();
apply_alph_filter(&mut filtered, w, h, best_mode);
let payload = encode_alpha_plane_as_vp8l(width, height, &filtered)?;
let header_byte = ((best_mode & 0b11) << 2) | 0b01;
Ok(AlphChunkBytes {
header_byte,
payload,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn riff_wrapper_layout_even_payload() {
let payload = vec![0xAAu8; 10];
let out = build_webp_file(
ImageKind::Vp8Lossy,
&payload,
16,
16,
None,
&WebpMetadata::default(),
);
assert_eq!(&out[0..4], b"RIFF");
assert_eq!(&out[8..12], b"WEBP");
assert_eq!(&out[12..16], b"VP8 ");
let riff_size = u32::from_le_bytes([out[4], out[5], out[6], out[7]]);
assert_eq!(riff_size, 22);
let chunk_len = u32::from_le_bytes([out[16], out[17], out[18], out[19]]);
assert_eq!(chunk_len, 10);
assert_eq!(&out[20..30], &payload[..]);
assert_eq!(out.len(), 30);
}
#[test]
fn riff_wrapper_layout_odd_payload_pads() {
let payload = vec![0x55u8; 11];
let out = build_webp_file(
ImageKind::Vp8Lossy,
&payload,
16,
16,
None,
&WebpMetadata::default(),
);
let riff_size = u32::from_le_bytes([out[4], out[5], out[6], out[7]]);
assert_eq!(riff_size, 24);
assert_eq!(out.len(), 32);
assert_eq!(out[31], 0x00);
}
#[test]
fn quality_to_qindex_endpoints_and_clamp() {
assert_eq!(quality_to_qindex(0.0), 127);
assert_eq!(quality_to_qindex(100.0), 0);
assert_eq!(quality_to_qindex(50.0), 64);
assert_eq!(quality_to_qindex(75.0), 32); assert_eq!(quality_to_qindex(-10.0), 127);
assert_eq!(quality_to_qindex(150.0), 0);
assert_eq!(quality_to_qindex(f32::NAN), 127);
}
#[test]
fn segment_quant_deltas_widen_with_qindex() {
let lo_q = segment_quant_deltas_for_qindex(0);
let mid_q = segment_quant_deltas_for_qindex(64);
let hi_q = segment_quant_deltas_for_qindex(127);
assert!(lo_q[0] < 0 && mid_q[0] < 0 && hi_q[0] < 0);
assert!(lo_q[3] > 0 && mid_q[3] > 0 && hi_q[3] > 0);
assert_eq!(lo_q[2], 0);
assert_eq!(mid_q[2], 0);
assert_eq!(hi_q[2], 0);
assert!(hi_q[0] <= mid_q[0] && mid_q[0] <= lo_q[0]);
assert!(hi_q[3] >= mid_q[3] && mid_q[3] >= lo_q[3]);
for d in lo_q.iter().chain(mid_q.iter()).chain(hi_q.iter()) {
assert!(*d >= -15 && *d <= 15, "delta {d} out of [-15, 15]");
}
}
#[test]
fn freq_deltas_collapse_to_zero_at_top_quality() {
let d = freq_deltas_for_qindex(0);
assert_eq!(d.y_dc_delta, 0);
assert_eq!(d.y2_dc_delta, 0);
assert_eq!(d.y2_ac_delta, 0);
assert_eq!(d.uv_dc_delta, 0);
assert_eq!(d.uv_ac_delta, 0);
}
#[test]
fn freq_deltas_widen_high_freq_at_low_quality() {
let d = freq_deltas_for_qindex(127);
assert_eq!(d.y_dc_delta, 0);
assert_eq!(d.y2_dc_delta, -2);
assert_eq!(d.y2_ac_delta, 4);
assert_eq!(d.uv_dc_delta, 0);
assert_eq!(d.uv_ac_delta, 4);
}
#[test]
fn freq_deltas_monotone_in_qindex() {
let mut prev = freq_deltas_for_qindex(0);
for qi in 1u8..=127 {
let cur = freq_deltas_for_qindex(qi);
assert!(
cur.y2_ac_delta >= prev.y2_ac_delta,
"qi={qi}: y2_ac_delta {} < prev {}",
cur.y2_ac_delta,
prev.y2_ac_delta
);
assert!(
cur.uv_ac_delta >= prev.uv_ac_delta,
"qi={qi}: uv_ac_delta {} < prev {}",
cur.uv_ac_delta,
prev.uv_ac_delta
);
assert!(
cur.y2_dc_delta <= prev.y2_dc_delta,
"qi={qi}: y2_dc_delta {} > prev {}",
cur.y2_dc_delta,
prev.y2_dc_delta
);
assert_eq!(cur.y_dc_delta, 0);
assert_eq!(cur.uv_dc_delta, 0);
for v in [
cur.y_dc_delta,
cur.y2_dc_delta,
cur.y2_ac_delta,
cur.uv_dc_delta,
cur.uv_ac_delta,
] {
assert!(
(-15..=15).contains(&v),
"qi={qi}: delta {v} out of [-15, 15]"
);
}
prev = cur;
}
}
#[test]
fn segment_lf_deltas_smooth_negative_textured_positive() {
for qi in [0u8, 32, 64, 96, 127] {
let lf = segment_lf_deltas_for_qindex(qi);
assert!(
lf[0] <= 0,
"qindex {qi}: smooth LF delta {} should be <= 0",
lf[0]
);
assert_eq!(lf[2], 0, "qindex {qi}: midline segment must be 0");
assert!(
lf[3] >= 1,
"qindex {qi}: textured LF delta {} should be >= 1",
lf[3]
);
for d in lf.iter() {
assert!(
*d >= -63 && *d <= 63,
"qindex {qi}: lf delta {d} out of range"
);
}
}
}
#[test]
fn quality_to_qindex_is_monotonically_decreasing() {
let mut prev = quality_to_qindex(0.0);
let mut q = 0.0_f32;
while q <= 100.0 {
let cur = quality_to_qindex(q);
assert!(
cur <= prev,
"quality {q} produced qindex {cur} > previous {prev} — mapping not monotone"
);
prev = cur;
q += 1.0;
}
}
#[test]
fn alph_filter_horizontal_picked_for_row_step_pattern() {
let w = 64usize;
let h = 64usize;
let mut alpha = vec![0u8; w * h];
for y in 0..h {
let row_val = ((y % 64) * 4) as u8;
for x in 0..w {
alpha[y * w + x] = row_val;
}
}
let chunk = encode_alph_chunk(w as u32, h as u32, &alpha).expect("encode_alph_chunk");
let filter_mode = (chunk.header_byte >> 2) & 0b11;
assert!(
filter_mode != 0,
"row-step pattern must select a non-identity filter (got mode {filter_mode})",
);
assert_eq!(chunk.header_byte & 0b11, 1, "compression bit must be VP8L");
let mut synth = Vec::with_capacity(chunk.payload.len() + 5);
synth.push(0x2f);
let pw = (w as u32).saturating_sub(1) & 0x3fff;
let ph = (h as u32).saturating_sub(1) & 0x3fff;
let packed = pw | (ph << 14);
synth.extend_from_slice(&packed.to_le_bytes());
synth.extend_from_slice(&chunk.payload);
let img = crate::vp8l::decode(&synth).expect("vp8l decode of alph payload");
let mut plane: Vec<u8> = img.pixels.iter().map(|p| ((p >> 8) & 0xff) as u8).collect();
match filter_mode {
0 => {}
1 => {
for y in 0..h {
for x in 1..w {
let i = y * w + x;
let left = plane[i - 1];
plane[i] = plane[i].wrapping_add(left);
}
}
}
2 => {
for y in 1..h {
for x in 0..w {
let i = y * w + x;
let top = plane[i - w];
plane[i] = plane[i].wrapping_add(top);
}
}
}
3 => {
for y in 0..h {
for x in 0..w {
let i = y * w + x;
let pred: i32 = if y == 0 && x == 0 {
0
} else if y == 0 {
plane[i - 1] as i32
} else if x == 0 {
plane[i - w] as i32
} else {
let l = plane[i - 1] as i32;
let t = plane[i - w] as i32;
let tl = plane[i - w - 1] as i32;
(l + t - tl).clamp(0, 255)
};
plane[i] = ((plane[i] as i32 + pred) & 0xff) as u8;
}
}
}
_ => unreachable!(),
}
assert_eq!(plane, alpha, "filtered ALPH must round-trip");
}
#[test]
fn alph_filter_identity_picked_for_constant_alpha() {
let w = 32usize;
let h = 32usize;
let alpha = vec![0xffu8; w * h];
let chunk = encode_alph_chunk(w as u32, h as u32, &alpha).expect("encode_alph_chunk");
let filter_mode = (chunk.header_byte >> 2) & 0b11;
assert!(
(1..=3).contains(&filter_mode),
"constant 0xff alpha must select a non-identity filter (got mode {filter_mode})",
);
}
#[test]
fn alph_filter_apply_then_unfilter_roundtrips_all_modes() {
let w = 17usize;
let h = 13usize;
let mut alpha = vec![0u8; w * h];
let mut s: u32 = 0x5eed_d00d;
for b in alpha.iter_mut() {
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
*b = (s & 0xff) as u8;
}
for mode in 0u8..=3 {
let mut filtered = alpha.clone();
apply_alph_filter(&mut filtered, w, h, mode);
let mut restored = filtered.clone();
match mode {
0 => {}
1 => {
for y in 0..h {
for x in 1..w {
let i = y * w + x;
let left = restored[i - 1];
restored[i] = restored[i].wrapping_add(left);
}
}
}
2 => {
for y in 1..h {
for x in 0..w {
let i = y * w + x;
let top = restored[i - w];
restored[i] = restored[i].wrapping_add(top);
}
}
}
3 => {
for y in 0..h {
for x in 0..w {
let i = y * w + x;
let pred: i32 = if y == 0 && x == 0 {
0
} else if y == 0 {
restored[i - 1] as i32
} else if x == 0 {
restored[i - w] as i32
} else {
let l = restored[i - 1] as i32;
let t = restored[i - w] as i32;
let tl = restored[i - w - 1] as i32;
(l + t - tl).clamp(0, 255)
};
restored[i] = ((restored[i] as i32 + pred) & 0xff) as u8;
}
}
}
_ => unreachable!(),
}
assert_eq!(
restored, alpha,
"filter mode {mode}: forward + inverse must be identity"
);
}
}
}