1use gamut_color::Bt601Range;
8use gamut_core::{DecodeImage, Dimensions, Error, ImageBuf, Result, Rgb8, Rgba8};
9use gamut_riff::{RiffReader, WebpChunkId};
10
11use crate::alpha;
12use crate::vp8l::decoder::{argb_to_rgb8, argb_to_rgba8, decode as decode_vp8l};
13
14#[derive(Debug, Clone, Default)]
19pub struct WebpDecoder {
20 _private: (),
22}
23
24impl WebpDecoder {
25 #[must_use]
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 fn decode_rgb8_into(&self, data: &[u8], out: &mut Vec<u8>) -> Result<Dimensions> {
34 for chunk in RiffReader::new(data)? {
37 let chunk = chunk?;
38 match WebpChunkId::from(chunk.fourcc) {
39 WebpChunkId::Vp8l => {
40 let (dims, argb) = decode_vp8l(chunk.payload)?;
41 argb_to_rgb8(&argb, out);
42 return Ok(dims);
43 }
44 WebpChunkId::Vp8 => {
45 let recon = crate::vp8::frame::decode_frame(chunk.payload)?;
46 let yuv = recon.to_yuv420();
47 let dims = Dimensions {
48 width: yuv.width(),
49 height: yuv.height(),
50 };
51 out.extend_from_slice(&yuv.to_rgb8(Bt601Range::Limited));
53 return Ok(dims);
54 }
55 WebpChunkId::Vp8x => {
56 gamut_riff::Vp8xHeader::from_payload(chunk.payload)?;
60 continue;
61 }
62 _ => continue,
63 }
64 }
65 Err(Error::InvalidInput(
66 "WebP: no VP8/VP8L/VP8X bitstream chunk",
67 ))
68 }
69
70 fn decode_rgba8_into(&self, data: &[u8], out: &mut Vec<u8>) -> Result<Dimensions> {
75 let mut alph: Option<&[u8]> = None;
76 for chunk in RiffReader::new(data)? {
77 let chunk = chunk?;
78 match WebpChunkId::from(chunk.fourcc) {
79 WebpChunkId::Vp8x => {
80 gamut_riff::Vp8xHeader::from_payload(chunk.payload)?;
81 }
82 WebpChunkId::Alpha => alph = Some(chunk.payload),
83 WebpChunkId::Vp8l => {
84 let (dims, argb) = decode_vp8l(chunk.payload)?;
85 argb_to_rgba8(&argb, out);
86 return Ok(dims);
87 }
88 WebpChunkId::Vp8 => {
89 let yuv = crate::vp8::frame::decode_frame(chunk.payload)?.to_yuv420();
90 let dims = Dimensions {
91 width: yuv.width(),
92 height: yuv.height(),
93 };
94 let (w, h) = (dims.width as usize, dims.height as usize);
95 let alpha = match alph {
96 Some(payload) => alpha::read_alph(payload, w, h)?,
97 None => vec![0xffu8; w * h],
98 };
99 let rgb = yuv.to_rgb8(Bt601Range::Limited);
100 out.reserve(w * h * 4);
101 for (px, &a) in rgb.chunks_exact(3).zip(alpha.iter()) {
102 out.extend_from_slice(&[px[0], px[1], px[2], a]);
103 }
104 return Ok(dims);
105 }
106 _ => continue,
107 }
108 }
109 Err(Error::InvalidInput("WebP: no VP8/VP8L bitstream chunk"))
110 }
111}
112
113impl DecodeImage<Rgb8> for WebpDecoder {
114 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgb8>> {
115 let mut px = Vec::new();
116 let dims = self.decode_rgb8_into(data, &mut px)?;
117 ImageBuf::new(px, dims)
118 }
119}
120
121impl DecodeImage<Rgba8> for WebpDecoder {
122 fn decode_image(&self, data: &[u8]) -> Result<ImageBuf<Rgba8>> {
123 let mut px = Vec::new();
124 let dims = self.decode_rgba8_into(data, &mut px)?;
125 ImageBuf::new(px, dims)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::vp8l::bit_io::BitWriter;
133 use crate::vp8l::header::Vp8lHeader;
134 use crate::vp8l::prefix::write_simple_prefix_code;
135 use gamut_riff::{FourCc, RiffWriter, write_simple_lossless, write_simple_lossy};
136
137 fn solid_lossless_webp(width: u32, height: u32, r: u8, g: u8, b: u8) -> Vec<u8> {
139 let mut w = BitWriter::new();
140 Vp8lHeader::from_dimensions(Dimensions { width, height }, false)
141 .unwrap()
142 .write(&mut w);
143 w.write_bits(0, 1); w.write_bits(0, 1); w.write_bits(0, 1); write_simple_prefix_code(&mut w, &[u16::from(g)]);
147 write_simple_prefix_code(&mut w, &[u16::from(r)]);
148 write_simple_prefix_code(&mut w, &[u16::from(b)]);
149 write_simple_prefix_code(&mut w, &[0xff]); write_simple_prefix_code(&mut w, &[0]); write_simple_lossless(&w.finish())
152 }
153
154 #[test]
155 fn decodes_lossless_container_to_rgb8() {
156 let file = solid_lossless_webp(2, 2, 0x12, 0x34, 0x56);
157 let got: ImageBuf<Rgb8> = WebpDecoder::new().decode_image(&file).unwrap();
158 assert_eq!(
159 got.dimensions(),
160 Dimensions {
161 width: 2,
162 height: 2
163 }
164 );
165 assert_eq!(got.as_samples(), [0x12, 0x34, 0x56].repeat(4).as_slice());
166 }
167
168 #[test]
169 fn routes_lossy_container_to_vp8() {
170 let file = write_simple_lossy(&[0x9d, 0x01, 0x2a]);
173 let got: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(&file);
174 assert!(got.is_err());
175 }
176
177 #[test]
178 fn decodes_extended_container_with_inner_bitstream() {
179 use gamut_riff::{Vp8xHeader, write_extended};
180 let inner = solid_lossless_webp(2, 2, 0x11, 0x22, 0x33);
183 let vp8l = RiffReader::new(&inner)
184 .unwrap()
185 .next()
186 .unwrap()
187 .unwrap()
188 .payload
189 .to_vec();
190 let header = Vp8xHeader {
191 canvas_width: 2,
192 canvas_height: 2,
193 ..Default::default()
194 };
195 let file = write_extended(&header, &[(FourCc::VP8L, &vp8l)]);
196 let got: ImageBuf<Rgb8> = WebpDecoder::new()
197 .decode_image(&file)
198 .expect("decode VP8X file");
199 assert_eq!(
200 got.dimensions(),
201 Dimensions {
202 width: 2,
203 height: 2
204 }
205 );
206 assert_eq!(got.as_samples(), [0x11, 0x22, 0x33].repeat(4).as_slice());
207 }
208
209 #[test]
210 fn rejects_extended_container_without_bitstream() {
211 let header = gamut_riff::Vp8xHeader {
213 canvas_width: 4,
214 canvas_height: 4,
215 ..Default::default()
216 };
217 let file = gamut_riff::write_extended(&header, &[]);
218 let got: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(&file);
219 assert!(matches!(got, Err(Error::InvalidInput(_))));
220 }
221
222 #[test]
223 fn skips_leading_metadata_then_decodes_bitstream() {
224 let vp8l = {
226 let full = solid_lossless_webp(1, 1, 9, 8, 7);
227 RiffReader::new(&full)
229 .unwrap()
230 .next()
231 .unwrap()
232 .unwrap()
233 .payload
234 .to_vec()
235 };
236 let mut w = RiffWriter::new();
237 w.write_chunk(FourCc::ICCP, &[1, 2, 3, 4]);
238 w.write_chunk(FourCc::VP8L, &vp8l);
239 let file = w.finish();
240 let got: ImageBuf<Rgb8> = WebpDecoder::new().decode_image(&file).unwrap();
241 assert_eq!(
242 got.dimensions(),
243 Dimensions {
244 width: 1,
245 height: 1
246 }
247 );
248 assert_eq!(got.as_samples(), [9, 8, 7].as_slice());
249 }
250
251 #[test]
252 fn errors_when_no_bitstream_chunk() {
253 let mut w = RiffWriter::new();
254 w.write_chunk(FourCc::EXIF, &[0xee; 6]);
255 let file = w.finish();
256 let err: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(&file);
257 assert!(matches!(err, Err(Error::InvalidInput(_))));
258 }
259
260 #[test]
261 fn rejects_non_riff_data() {
262 let err: Result<ImageBuf<Rgb8>> = WebpDecoder::new().decode_image(b"not a webp");
263 assert!(matches!(err, Err(Error::InvalidInput(_))));
264 }
265}