Skip to main content

pito_codecs/webp/
mod.rs

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}