use std::collections::VecDeque;
use oxideav_core::{
CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag,
ContainerRegistry, Decoder, Encoder, Error as CoreError, Frame, MediaType, Packet, PixelFormat,
RuntimeContext, TimeBase, VideoFrame, VideoPlane,
};
use crate::{
decode_webp_image, encode_vp8l_argb_with_metadata, DecodedWebp, Error, UnsupportedKind,
WebpError, WebpMetadata, WebpMetadataOwned, CODEC_ID_VP8L,
};
pub const CODEC_ID_STR: &str = "webp";
impl From<Error> for CoreError {
fn from(e: Error) -> Self {
match e {
Error::Unsupported(kind) => CoreError::Unsupported(match kind {
UnsupportedKind::LossyVp8 => {
"oxideav-webp: VP8 lossy bitstream (route to a VP8 decoder)".to_string()
}
UnsupportedKind::NoImageData => {
"oxideav-webp: no VP8L/VP8 image-data chunk (animation or header-only)"
.to_string()
}
}),
Error::NotImplemented => {
CoreError::Unsupported("oxideav-webp: code path not implemented yet".to_string())
}
Error::Vp8(ref v) => match WebpError::from(v.clone()) {
WebpError::Unsupported => CoreError::Unsupported(e.to_string()),
_ => CoreError::InvalidData(e.to_string()),
},
other => CoreError::InvalidData(other.to_string()),
}
}
}
fn decoded_webp_to_video_frame(img: DecodedWebp, pts: Option<i64>) -> VideoFrame {
let stride = (img.width as usize).saturating_mul(4);
VideoFrame {
pts,
planes: vec![VideoPlane {
stride,
data: img.rgba,
}],
}
}
pub fn decode_webp_to_frame(bytes: &[u8], pts: Option<i64>) -> oxideav_core::Result<VideoFrame> {
let img = decode_webp_image(bytes)?;
Ok(decoded_webp_to_video_frame(img, pts))
}
pub fn make_decoder(params: &CodecParameters) -> oxideav_core::Result<Box<dyn Decoder>> {
Ok(Box::new(WebpDecoder::new(params.clone())))
}
#[derive(Debug)]
pub struct WebpDecoder {
params: CodecParameters,
pending: Option<Packet>,
eof: bool,
}
impl WebpDecoder {
pub fn new(params: CodecParameters) -> Self {
let mut p = params;
p.media_type = MediaType::Video;
p.codec_id = CodecId::new(CODEC_ID_STR);
p.pixel_format = Some(PixelFormat::Rgba);
Self {
params: p,
pending: None,
eof: false,
}
}
pub fn params(&self) -> &CodecParameters {
&self.params
}
}
impl Decoder for WebpDecoder {
fn codec_id(&self) -> &CodecId {
&self.params.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> oxideav_core::Result<()> {
if self.pending.is_some() {
return Err(CoreError::other(
"oxideav-webp decoder: receive_frame must be called before sending another packet",
));
}
self.pending = Some(packet.clone());
Ok(())
}
fn receive_frame(&mut self) -> oxideav_core::Result<Frame> {
let Some(pkt) = self.pending.take() else {
return if self.eof {
Err(CoreError::Eof)
} else {
Err(CoreError::NeedMore)
};
};
let img = decode_webp_image(&pkt.data)?;
self.params.width = Some(img.width);
self.params.height = Some(img.height);
self.params.pixel_format = Some(PixelFormat::Rgba);
let vf = decoded_webp_to_video_frame(img, pkt.pts);
Ok(Frame::Video(vf))
}
fn flush(&mut self) -> oxideav_core::Result<()> {
self.eof = true;
Ok(())
}
}
fn video_frame_to_argb(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
) -> oxideav_core::Result<(Vec<u32>, bool)> {
let plane = frame
.planes
.first()
.ok_or_else(|| CoreError::invalid("webp_vp8l encoder: frame has no planes"))?;
let w = width as usize;
let h = height as usize;
let stride = plane.stride;
let mut pixels = Vec::with_capacity(w * h);
let mut alpha_is_used = false;
match pix {
PixelFormat::Rgba => {
for y in 0..h {
let row = &plane.data[y * stride..];
for x in 0..w {
let p = &row[x * 4..x * 4 + 4];
let (r, g, b, a) = (p[0] as u32, p[1] as u32, p[2] as u32, p[3] as u32);
if a != 0xff {
alpha_is_used = true;
}
pixels.push((a << 24) | (r << 16) | (g << 8) | b);
}
}
}
PixelFormat::Rgb24 => {
for y in 0..h {
let row = &plane.data[y * stride..];
for x in 0..w {
let p = &row[x * 3..x * 3 + 3];
let (r, g, b) = (p[0] as u32, p[1] as u32, p[2] as u32);
pixels.push((0xff << 24) | (r << 16) | (g << 8) | b);
}
}
}
other => {
return Err(CoreError::invalid(format!(
"webp_vp8l encoder: unsupported input pixel format {other:?} (want Rgba or Rgb24)"
)));
}
}
Ok((pixels, alpha_is_used))
}
pub fn make_encoder(params: &CodecParameters) -> oxideav_core::Result<Box<dyn Encoder>> {
make_encoder_with_metadata(params, WebpMetadataOwned::default())
}
pub fn make_encoder_with_metadata(
params: &CodecParameters,
metadata: WebpMetadataOwned,
) -> oxideav_core::Result<Box<dyn Encoder>> {
let width = params
.width
.ok_or_else(|| CoreError::invalid("webp_vp8l encoder: missing width"))?;
let height = params
.height
.ok_or_else(|| CoreError::invalid("webp_vp8l encoder: missing height"))?;
let pix = params.pixel_format.unwrap_or(PixelFormat::Rgba);
if !matches!(pix, PixelFormat::Rgba | PixelFormat::Rgb24) {
return Err(CoreError::invalid(format!(
"webp_vp8l encoder: unsupported input pixel format {pix:?} (want Rgba or Rgb24)"
)));
}
let mut output_params = params.clone();
output_params.media_type = MediaType::Video;
output_params.codec_id = CodecId::new(CODEC_ID_VP8L);
output_params.width = Some(width);
output_params.height = Some(height);
output_params.pixel_format = Some(pix);
Ok(Box::new(WebpVp8lEncoder {
output_params,
width,
height,
pix,
metadata,
pending_out: VecDeque::new(),
eof: false,
}))
}
#[derive(Debug)]
pub struct WebpVp8lEncoder {
output_params: CodecParameters,
width: u32,
height: u32,
pix: PixelFormat,
metadata: WebpMetadataOwned,
pending_out: VecDeque<Packet>,
eof: bool,
}
impl Encoder for WebpVp8lEncoder {
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 Frame::Video(v) = frame else {
return Err(CoreError::invalid("webp_vp8l encoder: video frames only"));
};
let (argb, frame_alpha) = video_frame_to_argb(v, self.width, self.height, self.pix)?;
let has_alpha = frame_alpha;
let meta = self.metadata.as_borrowed();
let bytes =
encode_vp8l_argb_with_metadata(self.width, self.height, &argb, has_alpha, &meta)
.map_err(|e| CoreError::InvalidData(e.to_string()))?;
let mut pkt = Packet::new(0, TimeBase::new(1, 1000), bytes);
pkt.pts = v.pts;
pkt.dts = v.pts;
pkt.flags.keyframe = true;
self.pending_out.push_back(pkt);
Ok(())
}
fn receive_packet(&mut self) -> oxideav_core::Result<Packet> {
if let Some(p) = self.pending_out.pop_front() {
return Ok(p);
}
if self.eof {
Err(CoreError::Eof)
} else {
Err(CoreError::NeedMore)
}
}
fn flush(&mut self) -> oxideav_core::Result<()> {
self.eof = true;
Ok(())
}
}
pub fn encode_vp8l_frame(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
metadata: &WebpMetadata<'_>,
) -> oxideav_core::Result<Vec<u8>> {
let (argb, alpha_is_used) = video_frame_to_argb(frame, width, height, pix)?;
encode_vp8l_argb_with_metadata(width, height, &argb, alpha_is_used, metadata)
.map_err(|e| CoreError::InvalidData(e.to_string()))
}
pub fn register_codecs(reg: &mut CodecRegistry) {
let caps = CodecCapabilities::video("webp_sw")
.with_intra_only(true)
.with_lossless(true)
.with_max_size(16384, 16384)
.with_pixel_formats(vec![PixelFormat::Rgba]);
reg.register(
CodecInfo::new(CodecId::new(CODEC_ID_STR))
.capabilities(caps)
.decoder(make_decoder)
.tag(CodecTag::fourcc(b"WEBP")),
);
let vp8l_caps = CodecCapabilities::video("webp_vp8l_sw")
.with_intra_only(true)
.with_lossless(true)
.with_max_size(16384, 16384)
.with_pixel_formats(vec![PixelFormat::Rgba, PixelFormat::Rgb24]);
reg.register(
CodecInfo::new(CodecId::new(CODEC_ID_VP8L))
.capabilities(vp8l_caps)
.decoder(make_decoder)
.encoder(make_encoder),
);
}
pub fn register_containers(reg: &mut ContainerRegistry) {
reg.register_extension("webp", CODEC_ID_STR);
}
pub fn register(ctx: &mut RuntimeContext) {
register_codecs(&mut ctx.codecs);
register_containers(&mut ctx.containers);
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::TimeBase;
const LOSSLESS_1X1: &[u8] = include_bytes!("../tests/data/lossless-1x1.webp");
const LOSSY_1X1: &[u8] = include_bytes!("../tests/data/lossy-1x1.webp");
#[test]
fn register_via_runtime_context_installs_decoder_factory() {
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let id = CodecId::new(CODEC_ID_STR);
assert!(
ctx.codecs.has_decoder(&id),
"webp decoder factory not installed via RuntimeContext"
);
assert!(!ctx.codecs.has_encoder(&id));
assert_eq!(ctx.containers.container_for_extension("webp"), Some("webp"));
assert_eq!(ctx.containers.container_for_extension("WEBP"), Some("webp"));
}
#[test]
fn register_via_runtime_context_resolves_webp_fourcc_tag() {
use oxideav_core::ProbeContext;
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let tag = CodecTag::fourcc(b"WEBP");
let id = ctx
.codecs
.resolve_tag_ref(&ProbeContext::new(&tag))
.expect("WEBP fourcc resolves to a registered codec");
assert_eq!(id.as_str(), CODEC_ID_STR);
}
#[test]
fn first_decoder_returns_a_webp_decoder() {
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
let dec = ctx
.codecs
.first_decoder(¶ms)
.expect("webp decoder factory");
assert_eq!(dec.codec_id().as_str(), CODEC_ID_STR);
}
#[test]
fn end_to_end_lossless_decode_via_runtime_context() {
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
let mut dec = ctx
.codecs
.first_decoder(¶ms)
.expect("webp decoder factory");
let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSLESS_1X1.to_vec());
dec.send_packet(&pkt).expect("send_packet accepts file");
let frame = dec.receive_frame().expect("receive_frame yields a frame");
let v = match frame {
Frame::Video(v) => v,
other => panic!("expected Frame::Video, got {other:?}"),
};
assert_eq!(v.planes.len(), 1, "RGBA is a single interleaved plane");
assert_eq!(v.planes[0].stride, 4, "1px-wide × 4 bytes/pixel");
assert_eq!(v.planes[0].data.len(), 4, "1×1 image × 4 bytes/pixel");
assert_eq!(v.planes[0].data, [0xB4, 0x3C, 0x5A, 0xFF]);
let again = dec.receive_frame();
assert!(matches!(again, Err(CoreError::NeedMore)));
}
#[test]
fn vp8_lossy_packet_decodes_via_registered_decoder() {
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
let mut dec = ctx
.codecs
.first_decoder(¶ms)
.expect("webp decoder factory");
let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSY_1X1.to_vec());
dec.send_packet(&pkt).expect("send_packet accepts file");
let frame = dec
.receive_frame()
.expect("VP8 lossy now decodes via oxideav-vp8");
let v = match frame {
Frame::Video(v) => v,
other => panic!("expected Frame::Video, got {other:?}"),
};
assert_eq!(v.planes.len(), 1, "RGBA is a single interleaved plane");
assert_eq!(v.planes[0].data.len(), 4, "1×1 image × 4 bytes/pixel");
assert_eq!(v.planes[0].data[3], 0xff);
let again = dec.receive_frame();
assert!(matches!(again, Err(CoreError::NeedMore)));
}
#[test]
fn decoder_params_carry_dims_and_pixel_format_after_first_frame() {
let mut dec = WebpDecoder::new(CodecParameters::video(CodecId::new(CODEC_ID_STR)));
assert_eq!(dec.params().pixel_format, Some(PixelFormat::Rgba));
assert_eq!(dec.params().width, None);
assert_eq!(dec.params().height, None);
let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSLESS_1X1.to_vec());
dec.send_packet(&pkt).unwrap();
let _ = dec.receive_frame().expect("decodes");
assert_eq!(dec.params().width, Some(1));
assert_eq!(dec.params().height, Some(1));
assert_eq!(dec.params().pixel_format, Some(PixelFormat::Rgba));
assert_eq!(dec.params().codec_id.as_str(), CODEC_ID_STR);
assert_eq!(dec.params().media_type, MediaType::Video);
}
#[test]
fn double_send_packet_without_receive_is_rejected() {
let mut dec = WebpDecoder::new(CodecParameters::video(CodecId::new(CODEC_ID_STR)));
let pkt = Packet::new(0, TimeBase::new(1, 1000), LOSSLESS_1X1.to_vec());
dec.send_packet(&pkt).unwrap();
let err = dec
.send_packet(&pkt)
.expect_err("second send_packet without receive_frame must fail");
let s = err.to_string();
assert!(
s.contains("receive_frame"),
"error message should mention receive_frame: {s}"
);
}
#[test]
fn flush_then_receive_with_no_pending_returns_eof() {
let mut dec = WebpDecoder::new(CodecParameters::video(CodecId::new(CODEC_ID_STR)));
dec.flush().unwrap();
let err = dec
.receive_frame()
.expect_err("post-flush, no pending packet → Eof");
assert!(matches!(err, CoreError::Eof));
}
#[test]
fn decode_webp_to_frame_returns_rgba_video_frame() {
let frame = decode_webp_to_frame(LOSSLESS_1X1, Some(123)).expect("decodes");
assert_eq!(frame.pts, Some(123));
assert_eq!(frame.planes.len(), 1);
assert_eq!(frame.planes[0].stride, 4);
assert_eq!(frame.planes[0].data, [0xB4, 0x3C, 0x5A, 0xFF]);
}
#[test]
fn unsupported_error_conversion_maps_to_core_unsupported() {
let lossy: CoreError = Error::Unsupported(UnsupportedKind::LossyVp8).into();
assert!(matches!(lossy, CoreError::Unsupported(_)));
let none: CoreError = Error::Unsupported(UnsupportedKind::NoImageData).into();
assert!(matches!(none, CoreError::Unsupported(_)));
}
fn rgba_frame(width: u32, height: u32, fill: impl Fn(u32, u32) -> [u8; 4]) -> Frame {
let mut data = Vec::with_capacity((width * height * 4) as usize);
for y in 0..height {
for x in 0..width {
data.extend_from_slice(&fill(x, y));
}
}
Frame::Video(VideoFrame {
pts: Some(0),
planes: vec![VideoPlane {
stride: (width * 4) as usize,
data,
}],
})
}
fn vp8l_params(width: u32, height: u32, pix: PixelFormat) -> CodecParameters {
let mut p = CodecParameters::video(CodecId::new(CODEC_ID_VP8L));
p.width = Some(width);
p.height = Some(height);
p.pixel_format = Some(pix);
p
}
#[test]
fn register_installs_vp8l_encoder_factory() {
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let id = CodecId::new(CODEC_ID_VP8L);
assert!(
ctx.codecs.has_encoder(&id),
"webp_vp8l encoder factory not installed"
);
assert!(
ctx.codecs.has_decoder(&id),
"webp_vp8l decoder factory not installed"
);
}
#[test]
fn vp8l_encoder_round_trips_rgba_through_registry() {
let (w, h) = (4u32, 3u32);
let frame = rgba_frame(w, h, |x, y| {
[(x * 40) as u8, (y * 60) as u8, ((x + y) * 25) as u8, 0xff]
});
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let mut enc = ctx
.codecs
.first_encoder(&vp8l_params(w, h, PixelFormat::Rgba))
.expect("webp_vp8l encoder factory");
enc.send_frame(&frame).expect("send_frame");
let pkt = enc.receive_packet().expect("one packet out");
let img = crate::decode_webp(&pkt.data).expect("decode our own webp");
assert_eq!(img.frames.len(), 1);
assert_eq!(img.frames[0].width, w);
assert_eq!(img.frames[0].height, h);
let Frame::Video(v) = &frame else {
unreachable!()
};
assert_eq!(img.frames[0].rgba, v.planes[0].data);
}
#[test]
fn vp8l_encoder_streams_rgb24_as_opaque() {
let (w, h) = (3u32, 2u32);
let mut data = Vec::new();
for y in 0..h {
for x in 0..w {
data.extend_from_slice(&[(x * 50) as u8, (y * 70) as u8, 0x33]);
}
}
let frame = Frame::Video(VideoFrame {
pts: Some(0),
planes: vec![VideoPlane {
stride: (w * 3) as usize,
data,
}],
});
let mut enc =
make_encoder(&vp8l_params(w, h, PixelFormat::Rgb24)).expect("make_encoder rgb24");
enc.send_frame(&frame).unwrap();
let pkt = enc.receive_packet().unwrap();
let c = crate::parse_container(&pkt.data).unwrap();
assert!(c
.first_chunk_with_fourcc(crate::container::fourcc::VP8X)
.is_none());
let img = crate::decode_webp(&pkt.data).unwrap();
for px in img.frames[0].rgba.chunks_exact(4) {
assert_eq!(px[3], 0xff);
}
}
#[test]
fn vp8l_encoder_with_metadata_promotes_to_vp8x() {
let (w, h) = (2u32, 2u32);
let frame = rgba_frame(w, h, |x, _| [(x * 100) as u8, 0x10, 0x20, 0x80]);
let meta = WebpMetadataOwned {
icc: Some(b"icc-profile".to_vec()),
exif: Some(b"Exif\x00\x00II".to_vec()),
xmp: None,
};
let mut enc = make_encoder_with_metadata(&vp8l_params(w, h, PixelFormat::Rgba), meta)
.expect("make_encoder_with_metadata");
enc.send_frame(&frame).unwrap();
let pkt = enc.receive_packet().unwrap();
let c = crate::parse_container(&pkt.data).unwrap();
assert!(c
.first_chunk_with_fourcc(crate::container::fourcc::VP8X)
.is_some());
let read = crate::extract_metadata(&pkt.data).unwrap();
assert_eq!(read.icc.as_deref(), Some(&b"icc-profile"[..]));
assert_eq!(read.exif.as_deref(), Some(&b"Exif\x00\x00II"[..]));
assert_eq!(read.xmp, None);
let img = crate::decode_webp(&pkt.data).unwrap();
let Frame::Video(v) = &frame else {
unreachable!()
};
assert_eq!(img.frames[0].rgba, v.planes[0].data);
}
#[test]
fn vp8l_encoder_receive_before_send_is_need_more() {
let mut enc = make_encoder(&vp8l_params(1, 1, PixelFormat::Rgba)).unwrap();
assert!(matches!(enc.receive_packet(), Err(CoreError::NeedMore)));
enc.flush().unwrap();
assert!(matches!(enc.receive_packet(), Err(CoreError::Eof)));
}
}