#[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),
}
}
#[cfg(feature = "registry")]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PsyStats {
pub mean_activity: f32,
pub high_variance_fraction: f32,
pub mb_count: u32,
}
#[cfg(feature = "registry")]
pub fn compute_psy_stats(width: u32, height: u32, y_plane: &[u8], y_stride: usize) -> PsyStats {
let w = width as usize;
let h = height as usize;
let mb_x = w / 16;
let mb_y = h / 16;
if mb_x == 0 || mb_y == 0 {
return PsyStats::default();
}
const HI_VAR_THRESHOLD: u64 = 3200 * 256;
let mut activity_sum: f64 = 0.0;
let mut hi_var_count: u32 = 0;
for my in 0..mb_y {
for mx in 0..mb_x {
let base = my * 16 * y_stride + mx * 16;
let mut mb_act: u32 = 0;
let mut mb_sum: u32 = 0;
let mut mb_sum2: u32 = 0;
for r in 0..16 {
let row = &y_plane[base + r * y_stride..base + r * y_stride + 16];
let mut row_sum: u32 = 0;
for &p in row {
row_sum += p as u32;
}
let row_mean = (row_sum + 8) >> 4;
let mut row_mad: u32 = 0;
for &p in row {
let d = (p as i32 - row_mean as i32).unsigned_abs();
row_mad += d;
mb_sum2 += (p as u32) * (p as u32);
}
mb_act += row_mad;
mb_sum += row_sum;
}
activity_sum += (mb_act as f64) / 256.0;
let n = 256u64;
let s = mb_sum as u64;
let s2 = mb_sum2 as u64;
let var = s2.saturating_sub((s * s) / n);
if var >= HI_VAR_THRESHOLD {
hi_var_count += 1;
}
}
}
let mb_count = (mb_x * mb_y) as u32;
let mean_activity = (activity_sum / mb_count as f64) as f32;
let high_variance_fraction = hi_var_count as f32 / mb_count as f32;
PsyStats {
mean_activity,
high_variance_fraction,
mb_count,
}
}
#[cfg(feature = "registry")]
fn psy_modulate_freq_deltas(base: Vp8FreqDeltas, stats: PsyStats, qindex: u8) -> Vp8FreqDeltas {
if stats.mb_count == 0 || qindex == 0 {
return base;
}
let strength = (qindex as f32) / 127.0;
let activity = stats.mean_activity;
let mod_step: i32 = if activity >= 16.0 {
(1.0 * strength).round() as i32
} else if activity < 6.0 {
-(1.0 * strength).round() as i32
} else {
0
};
Vp8FreqDeltas {
y_dc_delta: base.y_dc_delta,
y2_dc_delta: base.y2_dc_delta,
y2_ac_delta: (base.y2_ac_delta + mod_step).clamp(-15, 15),
uv_dc_delta: base.uv_dc_delta,
uv_ac_delta: (base.uv_ac_delta + mod_step).clamp(-15, 15),
}
}
#[cfg(feature = "registry")]
fn psy_modulate_segment_deltas(base: [i32; 4], stats: PsyStats, qindex: u8) -> [i32; 4] {
if stats.mb_count == 0 || qindex == 0 {
return base;
}
let strength = (qindex as f32) / 127.0;
let frac = stats.high_variance_fraction;
let bias: i32 = if frac >= 0.5 {
(1.0 * strength).round() as i32
} else if frac < 0.05 {
-(1.0 * strength).round() as i32
} else {
0
};
[base[0], base[1], base[2], (base[3] + bias).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_with_segments(
qindex: u8,
freq_deltas: Vp8FreqDeltas,
segment_quant_deltas: [i32; 4],
) -> 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_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_explicit_segments(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
segment_quant_deltas: [i32; 4],
frame: &VideoFrame,
) -> Result<Vec<u8>> {
let cfg = webp_lossy_config_with_segments(qindex, freq_deltas, segment_quant_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>> {
build_encoder(
params,
qindex,
freq_deltas_for_qindex(qindex),
true,
None,
)
}
#[cfg(feature = "registry")]
pub fn make_encoder_with_target_size(
params: &CodecParameters,
target_bytes: usize,
) -> oxideav_core::Result<Box<dyn Encoder>> {
build_encoder(
params,
DEFAULT_QINDEX,
freq_deltas_for_qindex(DEFAULT_QINDEX),
true,
Some(target_bytes),
)
}
#[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>> {
build_encoder(
params,
qindex,
freq_deltas,
false,
None,
)
}
#[cfg(feature = "registry")]
fn build_encoder(
params: &CodecParameters,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
psy_enabled: bool,
target_bytes: Option<usize>,
) -> 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,
psy_enabled,
target_bytes,
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,
psy_enabled: bool,
target_bytes: Option<usize>,
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 psy_stats = if self.psy_enabled {
extract_psy_stats(self.width, self.height, self.input_format, v)
} else {
PsyStats::default()
};
let chosen_qindex = if let Some(target) = self.target_bytes {
self.bisect_qindex_for_target(target, v, psy_stats)?
} else {
self.qindex
};
let chosen_freq_deltas = if self.psy_enabled {
psy_modulate_freq_deltas(
freq_deltas_for_qindex(chosen_qindex),
psy_stats,
chosen_qindex,
)
} else {
self.freq_deltas
};
let bytes = self.encode_at(chosen_qindex, chosen_freq_deltas, psy_stats, v)?;
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")]
impl Vp8WebpEncoder {
fn encode_at(
&self,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
psy_stats: PsyStats,
v: &VideoFrame,
) -> oxideav_core::Result<Vec<u8>> {
let segment_deltas = if self.psy_enabled {
psy_modulate_segment_deltas(
segment_quant_deltas_for_qindex(qindex.min(127)),
psy_stats,
qindex,
)
} else {
segment_quant_deltas_for_qindex(qindex.min(127))
};
let bytes = match self.input_format {
PixelFormat::Yuv420P => {
let vp8 = encode_keyframe_with_explicit_segments(
self.width,
self.height,
qindex,
freq_deltas,
segment_deltas,
v,
)
.map_err(|e| oxideav_core::Error::invalid(format!("{e}")))?;
build_webp_file(
ImageKind::Vp8Lossy,
&vp8,
self.width,
self.height,
None,
&WebpMetadata::default(),
)
}
PixelFormat::Yuva420P => encode_yuva420_lossy_with_segments(
self.width,
self.height,
qindex,
freq_deltas,
segment_deltas,
v,
)
.map_err(|e| oxideav_core::Error::invalid(format!("{e}")))?,
PixelFormat::Rgba => encode_rgba_lossy_with_segments(
self.width,
self.height,
qindex,
freq_deltas,
segment_deltas,
v,
)
.map_err(|e| oxideav_core::Error::invalid(format!("{e}")))?,
PixelFormat::Rgb24 => encode_rgb24_lossy_with_segments(
self.width,
self.height,
qindex,
freq_deltas,
segment_deltas,
v,
)
.map_err(|e| oxideav_core::Error::invalid(format!("{e}")))?,
other => {
return Err(oxideav_core::Error::unsupported(format!(
"VP8 WebP encoder: frame format {other:?} unsupported"
)))
}
};
Ok(bytes)
}
fn bisect_qindex_for_target(
&self,
target_bytes: usize,
v: &VideoFrame,
psy_stats: PsyStats,
) -> oxideav_core::Result<u8> {
const MAX_ITERS: usize = 5;
let lo_target = (target_bytes as f64 * 0.9) as usize;
let hi_target = (target_bytes as f64 * 1.1) as usize;
let mut lo: u8 = 0;
let mut hi: u8 = 127;
let mut best_qindex: u8 = self.qindex;
let mut best_dist: usize;
let try_encode = |qi: u8| -> oxideav_core::Result<usize> {
let fd = if self.psy_enabled {
psy_modulate_freq_deltas(freq_deltas_for_qindex(qi), psy_stats, qi)
} else {
self.freq_deltas
};
let bytes = self.encode_at(qi, fd, psy_stats, v)?;
Ok(bytes.len())
};
let initial = try_encode(self.qindex)?;
if initial >= lo_target && initial <= hi_target {
return Ok(self.qindex);
}
best_dist = initial.abs_diff(target_bytes);
if initial > target_bytes {
lo = self.qindex.saturating_add(1);
} else {
hi = self.qindex.saturating_sub(1);
}
for _ in 0..MAX_ITERS {
if lo > hi {
break;
}
let mid = lo + (hi - lo) / 2;
let sz = try_encode(mid)?;
let dist = sz.abs_diff(target_bytes);
if dist < best_dist {
best_dist = dist;
best_qindex = mid;
}
if sz >= lo_target && sz <= hi_target {
return Ok(mid);
}
if sz > target_bytes {
lo = mid.saturating_add(1);
} else if mid == 0 {
break;
} else {
hi = mid - 1;
}
}
Ok(best_qindex)
}
}
#[cfg(feature = "registry")]
fn extract_psy_stats(
width: u32,
height: u32,
input_format: PixelFormat,
v: &VideoFrame,
) -> PsyStats {
let w = width as usize;
let h = height as usize;
match input_format {
PixelFormat::Yuv420P | PixelFormat::Yuva420P => {
if v.planes.is_empty() {
return PsyStats::default();
}
let y_plane = &v.planes[0];
if y_plane.stride < w || y_plane.data.len() < y_plane.stride * h {
return PsyStats::default();
}
compute_psy_stats(width, height, &y_plane.data, y_plane.stride)
}
PixelFormat::Rgb24 => {
if v.planes.is_empty() {
return PsyStats::default();
}
let plane = &v.planes[0];
if plane.stride < w * 3 || plane.data.len() < plane.stride * h {
return PsyStats::default();
}
let mut y = vec![0u8; w * h];
for j in 0..h {
let row_start = j * plane.stride;
for i in 0..w {
let px = &plane.data[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 yv = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
y[j * w + i] = yv.clamp(0, 255) as u8;
}
}
compute_psy_stats(width, height, &y, w)
}
PixelFormat::Rgba => {
if v.planes.is_empty() {
return PsyStats::default();
}
let plane = &v.planes[0];
if plane.stride < w * 4 || plane.data.len() < plane.stride * h {
return PsyStats::default();
}
let mut y = vec![0u8; w * h];
for j in 0..h {
let row_start = j * plane.stride;
for i in 0..w {
let px = &plane.data[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;
let yv = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
y[j * w + i] = yv.clamp(0, 255) as u8;
}
}
compute_psy_stats(width, height, &y, w)
}
_ => PsyStats::default(),
}
}
#[cfg(feature = "registry")]
fn encode_yuva420_lossy_with_segments(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
segment_deltas: [i32; 4],
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_explicit_segments(
width,
height,
qindex,
freq_deltas,
segment_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_with_segments(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
segment_deltas: [i32; 4],
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_explicit_segments(
width,
height,
qindex,
freq_deltas,
segment_deltas,
&yuv_frame,
)?;
Ok(build_webp_file(
ImageKind::Vp8Lossy,
&vp8_bytes,
width,
height,
None,
&WebpMetadata::default(),
))
}
#[cfg(feature = "registry")]
fn encode_rgba_lossy_with_segments(
width: u32,
height: u32,
qindex: u8,
freq_deltas: Vp8FreqDeltas,
segment_deltas: [i32; 4],
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_explicit_segments(
width,
height,
qindex,
freq_deltas,
segment_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"
);
}
}
}