use gamut_color::Bt601Range;
use gamut_core::{DecodeImage, Dimensions, Error, ImageBuf, Result, Rgb8, Rgba8};
use gamut_riff::{RiffReader, WebpChunkId};
use crate::alpha;
use crate::vp8l::decoder::{argb_to_rgb8, argb_to_rgba8, decode as decode_vp8l};
#[derive(Debug, Clone, Default)]
pub struct WebpDecoder {
_private: (),
}
impl WebpDecoder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn decode_rgb8_into(&self, data: &[u8], out: &mut Vec<u8>) -> Result<Dimensions> {
for chunk in RiffReader::new(data)? {
let chunk = chunk?;
match WebpChunkId::from(chunk.fourcc) {
WebpChunkId::Vp8l => {
let (dims, argb) = decode_vp8l(chunk.payload)?;
argb_to_rgb8(&argb, out);
return Ok(dims);
}
WebpChunkId::Vp8 => {
let recon = crate::vp8::frame::decode_frame(chunk.payload)?;
let yuv = recon.to_yuv420();
let dims = Dimensions {
width: yuv.width(),
height: yuv.height(),
};
out.extend_from_slice(&yuv.to_rgb8(Bt601Range::Limited));
return Ok(dims);
}
WebpChunkId::Vp8x => {
gamut_riff::Vp8xHeader::from_payload(chunk.payload)?;
continue;
}
_ => continue,
}
}
Err(Error::InvalidInput(
"WebP: no VP8/VP8L/VP8X bitstream chunk",
))
}
fn decode_rgba8_into(&self, data: &[u8], out: &mut Vec<u8>) -> Result<Dimensions> {
let mut alph: Option<&[u8]> = None;
for chunk in RiffReader::new(data)? {
let chunk = chunk?;
match WebpChunkId::from(chunk.fourcc) {
WebpChunkId::Vp8x => {
gamut_riff::Vp8xHeader::from_payload(chunk.payload)?;
}
WebpChunkId::Alpha => alph = Some(chunk.payload),
WebpChunkId::Vp8l => {
let (dims, argb) = decode_vp8l(chunk.payload)?;
argb_to_rgba8(&argb, out);
return Ok(dims);
}
WebpChunkId::Vp8 => {
let yuv = crate::vp8::frame::decode_frame(chunk.payload)?.to_yuv420();
let dims = Dimensions {
width: yuv.width(),
height: yuv.height(),
};
let (w, h) = (dims.width as usize, dims.height as usize);
let alpha = match alph {
Some(payload) => alpha::read_alph(payload, w, h)?,
None => vec![0xffu8; w * h],
};
let rgb = yuv.to_rgb8(Bt601Range::Limited);
out.reserve(w * h * 4);
for (px, &a) in rgb.chunks_exact(3).zip(alpha.iter()) {
out.extend_from_slice(&[px[0], px[1], px[2], a]);
}
return Ok(dims);
}
_ => continue,
}
}
Err(Error::InvalidInput("WebP: no VP8/VP8L bitstream chunk"))
}
}
impl DecodeImage<Rgb8> for WebpDecoder {
fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgb8>> {
let mut px = Vec::new();
let dims = self.decode_rgb8_into(data, &mut px)?;
ImageBuf::new(px, dims)
}
}
impl DecodeImage<Rgba8> for WebpDecoder {
fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgba8>> {
let mut px = Vec::new();
let dims = self.decode_rgba8_into(data, &mut px)?;
ImageBuf::new(px, dims)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vp8l::bit_io::BitWriter;
use crate::vp8l::header::Vp8lHeader;
use crate::vp8l::prefix::write_simple_prefix_code;
use gamut_riff::{FourCc, RiffWriter, write_simple_lossless, write_simple_lossy};
fn solid_lossless_webp(width: u32, height: u32, r: u8, g: u8, b: u8) -> Vec<u8> {
let mut w = BitWriter::new();
Vp8lHeader::from_dimensions(Dimensions { width, height }, false)
.unwrap()
.write(&mut w);
w.write_bits(0, 1); w.write_bits(0, 1); w.write_bits(0, 1); write_simple_prefix_code(&mut w, &[u16::from(g)]);
write_simple_prefix_code(&mut w, &[u16::from(r)]);
write_simple_prefix_code(&mut w, &[u16::from(b)]);
write_simple_prefix_code(&mut w, &[0xff]); write_simple_prefix_code(&mut w, &[0]); write_simple_lossless(&w.finish())
}
#[test]
fn decodes_lossless_container_to_rgb8() {
let file = solid_lossless_webp(2, 2, 0x12, 0x34, 0x56);
let got: ImageBuf<Rgb8> = WebpDecoder::new().decode_image(&file).unwrap();
assert_eq!(
got.dimensions(),
Dimensions {
width: 2,
height: 2
}
);
assert_eq!(got.as_samples(), [0x12, 0x34, 0x56].repeat(4).as_slice());
}
#[test]
fn routes_lossy_container_to_vp8() {
let file = write_simple_lossy(&[0x9d, 0x01, 0x2a]);
let got: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(&file);
assert!(got.is_err());
}
#[test]
fn decodes_extended_container_with_inner_bitstream() {
use gamut_riff::{Vp8xHeader, write_extended};
let inner = solid_lossless_webp(2, 2, 0x11, 0x22, 0x33);
let vp8l = RiffReader::new(&inner)
.unwrap()
.next()
.unwrap()
.unwrap()
.payload
.to_vec();
let header = Vp8xHeader {
canvas_width: 2,
canvas_height: 2,
..Default::default()
};
let file = write_extended(&header, &[(FourCc::VP8L, &vp8l)]);
let got: ImageBuf<Rgb8> = WebpDecoder::new()
.decode_image(&file)
.expect("decode VP8X file");
assert_eq!(
got.dimensions(),
Dimensions {
width: 2,
height: 2
}
);
assert_eq!(got.as_samples(), [0x11, 0x22, 0x33].repeat(4).as_slice());
}
#[test]
fn rejects_extended_container_without_bitstream() {
let header = gamut_riff::Vp8xHeader {
canvas_width: 4,
canvas_height: 4,
..Default::default()
};
let file = gamut_riff::write_extended(&header, &[]);
let got: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(&file);
assert!(matches!(got, Err(Error::InvalidInput(_))));
}
#[test]
fn skips_leading_metadata_then_decodes_bitstream() {
let vp8l = {
let full = solid_lossless_webp(1, 1, 9, 8, 7);
RiffReader::new(&full)
.unwrap()
.next()
.unwrap()
.unwrap()
.payload
.to_vec()
};
let mut w = RiffWriter::new();
w.write_chunk(FourCc::ICCP, &[1, 2, 3, 4]);
w.write_chunk(FourCc::VP8L, &vp8l);
let file = w.finish();
let got: ImageBuf<Rgb8> = WebpDecoder::new().decode_image(&file).unwrap();
assert_eq!(
got.dimensions(),
Dimensions {
width: 1,
height: 1
}
);
assert_eq!(got.as_samples(), [9, 8, 7].as_slice());
}
#[test]
fn errors_when_no_bitstream_chunk() {
let mut w = RiffWriter::new();
w.write_chunk(FourCc::EXIF, &[0xee; 6]);
let file = w.finish();
let err: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(&file);
assert!(matches!(err, Err(Error::InvalidInput(_))));
}
#[test]
fn rejects_non_riff_data() {
let err: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(b"not a webp");
assert!(matches!(err, Err(Error::InvalidInput(_))));
}
}