1mod alpha;
2mod anim;
3mod lossless;
4#[cfg(all(test, target_os = "linux"))]
5#[allow(dead_code)]
6mod oracle;
7mod riff;
8#[cfg(all(test, target_os = "linux"))]
9mod tests;
10mod vp8;
11
12#[deprecated(note = "use pito_codecs::media::Rgba")]
13pub type Rgba = crate::media::Rgba;
14pub use anim::{AnimDecoder, AnimEncoder, Animation, Frame, FrameRef, decode_animation};
15
16use crate::media::{self, Result};
17use riff::{Chunks, invalid, u24};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Info {
21 pub width: u32,
22 pub height: u32,
23 pub alpha: bool,
24 pub animated: bool,
25 pub lossless: bool,
26 pub frames: u32,
27 pub loop_count: u16,
28 pub background: [u8; 4],
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct Options {
33 pub lossless: bool,
34 pub quality: f32,
35 pub effort: u8,
36 pub exact: bool,
37}
38
39impl Default for Options {
40 fn default() -> Self {
41 Options {
42 lossless: false,
43 quality: 75.0,
44 effort: 4,
45 exact: false,
46 }
47 }
48}
49
50impl Options {
51 pub fn lossless() -> Self {
52 Options {
53 lossless: true,
54 ..Options::default()
55 }
56 }
57
58 pub fn lossy(quality: f32) -> Self {
59 Options {
60 quality,
61 ..Options::default()
62 }
63 }
64}
65
66#[derive(Clone, Copy)]
67pub(crate) enum Coded<'a> {
68 Lossy {
69 data: &'a [u8],
70 alpha: Option<&'a [u8]>,
71 },
72 Lossless(&'a [u8]),
73}
74
75#[derive(Clone, Copy)]
76pub(crate) struct Piece<'a> {
77 pub x: u32,
78 pub y: u32,
79 pub width: u32,
80 pub height: u32,
81 pub duration: u32,
82 pub blend: bool,
83 pub dispose: bool,
84 pub alpha: bool,
85 pub coded: Coded<'a>,
86}
87
88pub(crate) struct Parsed<'a> {
89 pub info: Info,
90 pub pieces: Vec<Piece<'a>>,
91}
92
93pub fn info(data: &[u8]) -> Result<Info> {
94 Ok(parse(data)?.info)
95}
96
97pub fn decode(data: &[u8]) -> Result<media::Rgba> {
98 let parsed = parse(data)?;
99 if parsed.info.animated {
100 let mut dec = AnimDecoder::from_parsed(parsed);
101 return match dec.next_frame()? {
102 Some(f) => Ok(f.image.clone()),
103 None => Err(invalid("animation without frames")),
104 };
105 }
106 let piece = parsed.pieces[0];
107 let mut out = media::Rgba::new(piece.width, piece.height);
108 decode_piece(&piece, &mut out.data)?;
109 Ok(out)
110}
111
112pub fn encode(image: &media::Rgba, options: &Options) -> Result<Vec<u8>> {
113 check_image(image)?;
114 let (body, alpha) = encode_piece(image, options)?;
115 let mut w = riff::Writer::new();
116 if alpha && !options.lossless {
117 let mut x = Vec::with_capacity(10);
118 x.extend_from_slice(&[0x10, 0, 0, 0]);
119 riff::put_u24(&mut x, image.width - 1);
120 riff::put_u24(&mut x, image.height - 1);
121 w.chunk(b"VP8X", &x);
122 }
123 w.out.extend_from_slice(&body);
124 w.finish()
125}
126
127pub(crate) fn encode_piece(image: &media::Rgba, options: &Options) -> Result<(Vec<u8>, bool)> {
128 let alpha = image.data.chunks_exact(4).any(|p| p[3] != 255);
129 let mut out = Vec::new();
130 if options.lossless {
131 riff::put_chunk(&mut out, b"VP8L", &lossless::encode(image, options)?);
132 } else {
133 let data = vp8::encode(&image.data, image.width, image.height, options)?;
134 if alpha {
135 riff::put_chunk(&mut out, b"ALPH", &alpha::encode(image, options)?);
136 }
137 riff::put_chunk(&mut out, b"VP8 ", &data);
138 }
139 Ok((out, alpha))
140}
141
142pub(crate) fn check_image(image: &media::Rgba) -> Result<()> {
143 if image.width == 0 || image.height == 0 || image.width > 16383 || image.height > 16383 {
144 return Err(crate::media::Error::Unsupported(format!(
145 "webp: {}x{} (each side 1 to 16383)",
146 image.width, image.height
147 )));
148 }
149 if image.data.len() != image.width as usize * image.height as usize * 4 {
150 return Err(invalid("RGBA buffer size"));
151 }
152 Ok(())
153}
154
155pub(crate) fn decode_piece(piece: &Piece, out: &mut [u8]) -> Result<()> {
156 match piece.coded {
157 Coded::Lossless(data) => {
158 let (w, h, _) = lossless::header(data)?;
159 if w != piece.width || h != piece.height {
160 return Err(invalid("VP8L size differs from its frame"));
161 }
162 lossless::decode_rgba(data, out)
163 }
164 Coded::Lossy { data, alpha } => {
165 let (w, h) = vp8::info(data)?;
166 if w != piece.width || h != piece.height {
167 return Err(invalid("VP8 size differs from its frame"));
168 }
169 vp8::decode_into(data, out)?;
170 if let Some(a) = alpha {
171 alpha::decode(a, w, h, out)?;
172 }
173 Ok(())
174 }
175 }
176}
177
178pub(crate) fn parse(file: &[u8]) -> Result<Parsed<'_>> {
179 let body = riff::body(file)?;
180 let mut chunks = Chunks::new(body);
181 let first = chunks.next().ok_or_else(|| invalid("no chunks"))??;
182 match &first.id {
183 b"VP8 " => {
184 let (w, h) = vp8::info(first.data)?;
185 Ok(still(
186 Coded::Lossy {
187 data: first.data,
188 alpha: None,
189 },
190 w,
191 h,
192 false,
193 false,
194 ))
195 }
196 b"VP8L" => {
197 let (w, h, a) = lossless::header(first.data)?;
198 Ok(still(Coded::Lossless(first.data), w, h, a, true))
199 }
200 b"VP8X" => parse_extended(first.data, chunks),
201 _ => Err(invalid("unknown first chunk")),
202 }
203}
204
205fn still(coded: Coded<'_>, width: u32, height: u32, alpha: bool, lossless: bool) -> Parsed<'_> {
206 let piece = Piece {
207 x: 0,
208 y: 0,
209 width,
210 height,
211 duration: 0,
212 blend: false,
213 dispose: false,
214 alpha,
215 coded,
216 };
217 Parsed {
218 info: Info {
219 width,
220 height,
221 alpha,
222 animated: false,
223 lossless,
224 frames: 1,
225 loop_count: 0,
226 background: [0; 4],
227 },
228 pieces: vec![piece],
229 }
230}
231
232fn parse_extended<'a>(x: &[u8], chunks: Chunks<'a>) -> Result<Parsed<'a>> {
233 if x.len() < 10 {
234 return Err(invalid("VP8X chunk too short"));
235 }
236 let flags = x[0];
237 let width = u24(&x[4..]) + 1;
238 let height = u24(&x[7..]) + 1;
239 if width as u64 * height as u64 > 1 << 28 {
240 return Err(crate::media::Error::Unsupported(format!(
241 "webp: {width}x{height} canvas is over 268 megapixels"
242 )));
243 }
244 let animated = flags & 0x02 != 0;
245 let mut info = Info {
246 width,
247 height,
248 alpha: flags & 0x10 != 0,
249 animated,
250 lossless: false,
251 frames: 0,
252 loop_count: 0,
253 background: [0; 4],
254 };
255 let mut pieces = Vec::new();
256 let mut alph = None;
257 for chunk in chunks {
258 let chunk = chunk?;
259 match &chunk.id {
260 b"ANIM" if animated => {
261 if chunk.data.len() < 6 {
262 return Err(invalid("ANIM chunk too short"));
263 }
264 let b = chunk.data;
265 info.background = [b[2], b[1], b[0], b[3]];
266 info.loop_count = u16::from_le_bytes([b[4], b[5]]);
267 }
268 b"ANMF" if animated => pieces.push(parse_anmf(chunk.data)?),
269 b"ALPH" if !animated => alph = Some(chunk.data),
270 b"VP8 " if !animated && pieces.is_empty() => {
271 let (w, h) = vp8::info(chunk.data)?;
272 pieces.push(Piece {
273 x: 0,
274 y: 0,
275 width: w,
276 height: h,
277 duration: 0,
278 blend: false,
279 dispose: false,
280 alpha: alph.is_some(),
281 coded: Coded::Lossy {
282 data: chunk.data,
283 alpha: alph,
284 },
285 });
286 }
287 b"VP8L" if !animated && pieces.is_empty() => {
288 let (w, h, a) = lossless::header(chunk.data)?;
289 pieces.push(Piece {
290 x: 0,
291 y: 0,
292 width: w,
293 height: h,
294 duration: 0,
295 blend: false,
296 dispose: false,
297 alpha: a,
298 coded: Coded::Lossless(chunk.data),
299 });
300 }
301 _ => {}
302 }
303 }
304 if pieces.is_empty() {
305 return Err(invalid("no image data"));
306 }
307 for p in &pieces {
308 if p.x + p.width > width || p.y + p.height > height {
309 return Err(invalid("frame outside the canvas"));
310 }
311 }
312 if !animated && (pieces[0].width != width || pieces[0].height != height) {
313 return Err(invalid("image size differs from the canvas"));
314 }
315 info.lossless = pieces.iter().all(|p| matches!(p.coded, Coded::Lossless(_)));
316 info.frames = pieces.len() as u32;
317 Ok(Parsed { info, pieces })
318}
319
320fn parse_anmf(d: &[u8]) -> Result<Piece<'_>> {
321 if d.len() < 16 {
322 return Err(invalid("ANMF chunk too short"));
323 }
324 let x = u24(&d[0..]) * 2;
325 let y = u24(&d[3..]) * 2;
326 let width = u24(&d[6..]) + 1;
327 let height = u24(&d[9..]) + 1;
328 let duration = u24(&d[12..]);
329 let flags = d[15];
330 let mut alph = None;
331 let mut coded = None;
332 for chunk in Chunks::new(&d[16..]) {
333 let chunk = chunk?;
334 match &chunk.id {
335 b"ALPH" => alph = Some(chunk.data),
336 b"VP8 " => {
337 coded = Some((
338 Coded::Lossy {
339 data: chunk.data,
340 alpha: alph,
341 },
342 alph.is_some(),
343 ));
344 break;
345 }
346 b"VP8L" => {
347 let (_, _, a) = lossless::header(chunk.data)?;
348 coded = Some((Coded::Lossless(chunk.data), a));
349 break;
350 }
351 _ => {}
352 }
353 }
354 let (coded, alpha) = coded.ok_or_else(|| invalid("ANMF without image data"))?;
355 Ok(Piece {
356 x,
357 y,
358 width,
359 height,
360 duration,
361 blend: flags & 0x02 == 0,
362 dispose: flags & 0x01 != 0,
363 alpha,
364 coded,
365 })
366}