use otf_pixels_core::{
Animation, Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation,
PixelFormat, PixelsError, Result, Source,
};
const MAX_COMPRESSED: usize = 256 * 1024 * 1024;
#[derive(Debug)]
pub struct WebPDecoder {
descriptor: ImageDescriptor,
pixels: Vec<u8>,
row: u32,
orientation: Orientation,
icc: Option<Vec<u8>>,
animation: Option<Animation>,
}
impl WebPDecoder {
pub fn new<S: Source>(mut source: S, limits: Limits) -> Result<Self> {
let mut bytes = Vec::new();
let mut chunk = [0_u8; 64 * 1024];
loop {
if bytes.len() > MAX_COMPRESSED {
return Err(PixelsError::malformed(
"webp",
format!("stream exceeds {MAX_COMPRESSED} bytes"),
));
}
match source.read(&mut chunk)? {
0 => break,
read => {
let Some(filled) = chunk.get(..read) else {
break;
};
bytes.extend_from_slice(filled);
}
}
}
let container = crate::riff::parse(&bytes)?;
let orientation = container
.exif
.and_then(Orientation::from_exif_block)
.unwrap_or_default();
let pixel = if container.has_alpha {
PixelFormat::Rgba8
} else {
PixelFormat::Rgb8
};
let descriptor =
ImageDescriptor::with_limits(container.width, container.height, pixel, &limits)?;
let frame = container.frame;
let rgba = decode_rgba(
container.bitstream,
frame.width as usize,
frame.height as usize,
)?;
let channels = pixel.channels();
let (canvas_width, frame_width) = (container.width as usize, frame.width as usize);
let mut pixels =
vec![0_u8; container.width as usize * container.height as usize * channels];
for (y, source) in rgba.chunks_exact(frame_width * 4).enumerate() {
let row = (frame.y as usize + y) * canvas_width + frame.x as usize;
let Some(target) = pixels.get_mut(row * channels..(row + frame_width) * channels)
else {
return Err(PixelsError::malformed(
"webp",
"a frame overruns its canvas",
));
};
for (out, sample) in target
.chunks_exact_mut(channels)
.zip(source.chunks_exact(4))
{
out.copy_from_slice(sample.get(..channels).unwrap_or(&[]));
}
}
Ok(Self {
descriptor,
pixels,
row: 0,
orientation,
icc: container.icc.map(<[u8]>::to_vec),
animation: Animation::new(container.frame_durations_ms.clone(), container.loop_count),
})
}
}
fn decode_rgba(
bitstream: crate::riff::Bitstream<'_>,
width: usize,
height: usize,
) -> Result<Vec<u8>> {
match bitstream {
crate::riff::Bitstream::Lossless(stream) => {
let argb = crate::vp8l::decode(stream, width, height)?;
Ok(argb
.into_iter()
.flat_map(|p| {
let [blue, green, red, alpha] = p.to_le_bytes();
[red, green, blue, alpha]
})
.collect())
}
crate::riff::Bitstream::Lossy { vp8, alpha } => {
let frame = crate::vp8::decode(vp8)?;
if (frame.width, frame.height) != (width, height) {
return Err(PixelsError::malformed(
"webp",
"the VP8 frame's size differs from the container's",
));
}
let alpha = match alpha {
Some(chunk) => crate::alpha::decode(chunk, width, height)?,
None => vec![255; width * height],
};
Ok(crate::yuv::to_rgb(
&frame.y,
frame.y_stride,
&frame.u,
&frame.v,
frame.uv_stride,
width,
height,
Some(&alpha),
))
}
}
}
impl Decoder for WebPDecoder {
fn descriptor(&self) -> ImageDescriptor {
self.descriptor
}
fn orientation(&self) -> Orientation {
self.orientation
}
fn icc_profile(&self) -> Option<&[u8]> {
self.icc.as_deref()
}
fn animation(&self) -> Option<Animation> {
self.animation.clone()
}
fn capability(&self) -> DecodeCapability {
DecodeCapability::Sequential
}
fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
if self.row >= self.descriptor.height {
return Err(PixelsError::invalid_argument(
"out",
format!("all {} rows have already been read", self.descriptor.height),
));
}
let row_bytes = self.descriptor.row_bytes();
if out.len() != row_bytes {
return Err(PixelsError::invalid_argument(
"out",
format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
));
}
let start = self.row as usize * row_bytes;
let row = self
.pixels
.get(start..)
.and_then(|rest| rest.get(..row_bytes))
.ok_or_else(|| PixelsError::malformed("webp", "decoded image is short"))?;
out.copy_from_slice(row);
self.row += 1;
Ok(())
}
}
#[must_use]
pub fn probe(prefix: &[u8]) -> bool {
prefix.get(..4) == Some(&crate::SIGNATURE_RIFF[..])
&& prefix.get(8..12) == Some(&crate::SIGNATURE_WEBP[..])
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WebPCodec;
impl Codec for WebPCodec {
fn format(&self) -> Format {
Format::WebP
}
fn magic_len(&self) -> usize {
12
}
fn probe(&self, prefix: &[u8]) -> bool {
probe(prefix)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
#[test]
fn probe_needs_the_form_type_not_just_riff() {
let mut header = Vec::from(*b"RIFF");
header.extend_from_slice(&[0, 0, 0, 0]);
header.extend_from_slice(b"WEBP");
assert!(probe(&header));
let mut wav = Vec::from(*b"RIFF");
wav.extend_from_slice(&[0, 0, 0, 0]);
wav.extend_from_slice(b"WAVE");
assert!(!probe(&wav));
assert!(!probe(b"RIFF"));
assert!(!probe(b""));
assert!(!probe(b"\x89PNG\r\n\x1a\n"));
}
#[test]
fn a_stream_that_is_not_a_webp_is_rejected() {
let error = WebPDecoder::new(&b"not a webp at all"[..], Limits::default()).unwrap_err();
assert_eq!(
error.code(),
otf_pixels_core::ErrorCode::Malformed,
"{error}"
);
}
}