Skip to main content

webp_rust/
compat.rs

1//! Compatibility wrapper for callback-based decode flows used by `wml2`.
2//!
3//! This module intentionally mirrors the shape of the `wml2` draw-side API so
4//! the WebP codec core can stay in `webp-rust` while callers keep a thin
5//! adapter around their own image buffer and metadata model.
6
7use crate::decoder::lossless::decode_lossless_webp_to_rgba;
8use crate::decoder::lossy::{decode_lossy_vp8_frame_to_rgba, decode_lossy_webp_to_rgba};
9use crate::decoder::{
10    get_features, parse_animation_webp, DecodedImage, DecoderError, ParsedAnimationFrame,
11    WebpFormat,
12};
13use bin_rs::io::read_u32_le;
14use bin_rs::reader::BinaryReader;
15use std::collections::HashMap;
16
17type Error = Box<dyn std::error::Error>;
18
19/// Metadata map used by the compatibility wrapper.
20pub type Metadata = HashMap<String, DataMap>;
21
22/// Minimal metadata value type used by the compatibility wrapper.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum DataMap {
25    UInt(u64),
26    UIntAllay(Vec<u64>),
27    Raw(Vec<u8>),
28    Ascii(String),
29    None,
30}
31
32/// RGBA color value used by animation initialization.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct RGBA {
35    pub red: u8,
36    pub green: u8,
37    pub blue: u8,
38    pub alpha: u8,
39}
40
41/// Callback response command.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum ResponseCommand {
44    Abort,
45    Continue,
46}
47
48/// Response returned by compatibility callbacks.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct CallbackResponse {
51    pub response: ResponseCommand,
52}
53
54impl CallbackResponse {
55    pub fn abort() -> Self {
56        Self {
57            response: ResponseCommand::Abort,
58        }
59    }
60
61    pub fn cont() -> Self {
62        Self {
63            response: ResponseCommand::Continue,
64        }
65    }
66}
67
68/// Receives decoded image data from compatibility decode entry points.
69pub trait DrawCallback: Sync + Send {
70    fn init(
71        &mut self,
72        width: usize,
73        height: usize,
74        option: Option<InitOptions>,
75    ) -> Result<Option<CallbackResponse>, Error>;
76    fn draw(
77        &mut self,
78        start_x: usize,
79        start_y: usize,
80        width: usize,
81        height: usize,
82        data: &[u8],
83        option: Option<DrawOptions>,
84    ) -> Result<Option<CallbackResponse>, Error>;
85    fn terminate(
86        &mut self,
87        term: Option<TerminateOptions>,
88    ) -> Result<Option<CallbackResponse>, Error>;
89    fn next(&mut self, next: Option<NextOptions>) -> Result<Option<CallbackResponse>, Error>;
90    fn verbose(
91        &mut self,
92        verbose: &str,
93        option: Option<VerboseOptions>,
94    ) -> Result<Option<CallbackResponse>, Error>;
95    fn set_metadata(
96        &mut self,
97        key: &str,
98        value: DataMap,
99    ) -> Result<Option<CallbackResponse>, Error>;
100}
101
102/// Decoder initialization options.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct InitOptions {
105    pub loop_count: u32,
106    pub background: Option<RGBA>,
107    pub animation: bool,
108}
109
110/// Draw options placeholder kept for shape compatibility.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub struct DrawOptions {}
113
114/// Termination options placeholder kept for shape compatibility.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
116pub struct TerminateOptions {}
117
118/// Verbose options placeholder kept for shape compatibility.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
120pub struct VerboseOptions {}
121
122/// Frame transition commands.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum NextOption {
125    Continue,
126    Next,
127    Dispose,
128    ClearAbort,
129    Terminate,
130}
131
132/// Disposal mode for an animation frame.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum NextDispose {
135    None,
136    Override,
137    Background,
138    Previous,
139}
140
141/// Blend mode for an animation frame.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum NextBlend {
144    Source,
145    Override,
146}
147
148/// Destination rectangle for a frame.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct ImageRect {
151    pub start_x: i32,
152    pub start_y: i32,
153    pub width: usize,
154    pub height: usize,
155}
156
157/// Per-frame transition options.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct NextOptions {
160    pub flag: NextOption,
161    pub await_time: u64,
162    pub image_rect: Option<ImageRect>,
163    pub dispose_option: Option<NextDispose>,
164    pub blend: Option<NextBlend>,
165}
166
167/// Decoder call options.
168pub struct DecodeOptions<'a> {
169    pub debug_flag: usize,
170    pub drawer: &'a mut dyn DrawCallback,
171    pub options: Option<Metadata>,
172}
173
174impl<'a> DecodeOptions<'a> {
175    pub fn new(drawer: &'a mut dyn DrawCallback) -> Self {
176        Self {
177            debug_flag: 0,
178            drawer,
179            options: None,
180        }
181    }
182}
183
184fn argb_to_rgba(argb: u32) -> RGBA {
185    RGBA {
186        red: ((argb >> 16) & 0xff) as u8,
187        green: ((argb >> 8) & 0xff) as u8,
188        blue: (argb & 0xff) as u8,
189        alpha: (argb >> 24) as u8,
190    }
191}
192
193fn map_error(error: DecoderError) -> Error {
194    Box::new(error)
195}
196
197fn read_container<B: BinaryReader>(reader: &mut B) -> Result<Vec<u8>, Error> {
198    let header = reader.read_bytes_no_move(12)?;
199    if header.len() < 12 || &header[0..4] != b"RIFF" || &header[8..12] != b"WEBP" {
200        return Err(Box::new(std::io::Error::new(
201            std::io::ErrorKind::InvalidData,
202            "not a WebP RIFF container",
203        )));
204    }
205
206    let riff_size = read_u32_le(&header, 4) as usize;
207    let total_size = riff_size + 8;
208    if total_size < 12 {
209        return Err(Box::new(std::io::Error::new(
210            std::io::ErrorKind::InvalidData,
211            "invalid WebP container length",
212        )));
213    }
214
215    Ok(reader.read_bytes_as_vec(total_size)?)
216}
217
218fn next_options(frame: &ParsedAnimationFrame<'_>) -> NextOptions {
219    NextOptions {
220        flag: NextOption::Continue,
221        await_time: frame.duration as u64,
222        image_rect: Some(ImageRect {
223            start_x: frame.x_offset as i32,
224            start_y: frame.y_offset as i32,
225            width: frame.width,
226            height: frame.height,
227        }),
228        dispose_option: Some(if frame.dispose_to_background {
229            NextDispose::Background
230        } else {
231            NextDispose::None
232        }),
233        blend: Some(if frame.blend {
234            NextBlend::Source
235        } else {
236            NextBlend::Override
237        }),
238    }
239}
240
241fn decode_frame_rgba(frame: &ParsedAnimationFrame<'_>) -> Result<DecodedImage, DecoderError> {
242    let image = match &frame.image_chunk.fourcc {
243        b"VP8L" => {
244            if frame.alpha_chunk.is_some() {
245                return Err(DecoderError::Bitstream(
246                    "VP8L animation frame must not carry ALPH chunk",
247                ));
248            }
249            crate::decoder::decode_lossless_vp8l_to_rgba(frame.image_data)?
250        }
251        b"VP8 " => decode_lossy_vp8_frame_to_rgba(frame.image_data, frame.alpha_data)?,
252        _ => return Err(DecoderError::Bitstream("unsupported animation frame chunk")),
253    };
254
255    if image.width != frame.width || image.height != frame.height {
256        return Err(DecoderError::Bitstream(
257            "animation frame dimensions do not match bitstream",
258        ));
259    }
260    Ok(image)
261}
262
263fn read_le32(bytes: &[u8]) -> usize {
264    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize
265}
266
267type ScannedChunk<'a> = ([u8; 4], &'a [u8]);
268
269fn scan_chunks(data: &[u8]) -> Result<Vec<ScannedChunk<'_>>, DecoderError> {
270    if data.len() < 12 {
271        return Err(DecoderError::NotEnoughData("RIFF header"));
272    }
273    if &data[0..4] != b"RIFF" || &data[8..12] != b"WEBP" {
274        return Err(DecoderError::Bitstream("wrong RIFF WEBP signature"));
275    }
276
277    let riff_size = read_le32(&data[4..8]);
278    let limit = riff_size + 8;
279    if limit > data.len() {
280        return Err(DecoderError::NotEnoughData("truncated RIFF payload"));
281    }
282
283    let mut offset = 12;
284    let mut chunks = Vec::new();
285    while offset + 8 <= limit {
286        let size = read_le32(&data[offset + 4..offset + 8]);
287        let padded_size = size + (size & 1);
288        let chunk_end = offset + 8 + padded_size;
289        if chunk_end > limit {
290            return Err(DecoderError::NotEnoughData("chunk payload"));
291        }
292
293        let fourcc: [u8; 4] = data[offset..offset + 4]
294            .try_into()
295            .expect("valid fourcc slice");
296        let payload = &data[offset + 8..offset + 8 + size];
297        chunks.push((fourcc, payload));
298        offset = chunk_end;
299    }
300
301    Ok(chunks)
302}
303
304fn webp_codec_name(format: WebpFormat, animated: bool) -> &'static str {
305    if animated {
306        "Animated"
307    } else {
308        match format {
309            WebpFormat::Lossy => "Lossy",
310            WebpFormat::Lossless => "Lossless",
311            WebpFormat::Undefined => "Undefined",
312        }
313    }
314}
315
316fn make_metadata(data: &[u8]) -> Result<Metadata, DecoderError> {
317    let features = get_features(data)?;
318    let chunks = scan_chunks(data)?;
319    let mut map = HashMap::new();
320
321    map.insert("Format".to_string(), DataMap::Ascii("WEBP".to_string()));
322    map.insert("width".to_string(), DataMap::UInt(features.width as u64));
323    map.insert("height".to_string(), DataMap::UInt(features.height as u64));
324    map.insert(
325        "WebP codec".to_string(),
326        DataMap::Ascii(webp_codec_name(features.format, features.has_animation).to_string()),
327    );
328    map.insert(
329        "WebP has alpha".to_string(),
330        DataMap::Ascii(features.has_alpha.to_string()),
331    );
332    map.insert(
333        "WebP animated".to_string(),
334        DataMap::Ascii(features.has_animation.to_string()),
335    );
336
337    if let Some(vp8x) = features.vp8x {
338        map.insert(
339            "canvas width".to_string(),
340            DataMap::UInt(vp8x.canvas_width as u64),
341        );
342        map.insert(
343            "canvas height".to_string(),
344            DataMap::UInt(vp8x.canvas_height as u64),
345        );
346    }
347
348    if features.has_animation {
349        let parsed = parse_animation_webp(data)?;
350        map.insert(
351            "Animation frames".to_string(),
352            DataMap::UInt(parsed.frames.len() as u64),
353        );
354        map.insert(
355            "Animation loop count".to_string(),
356            DataMap::UInt(parsed.animation.loop_count as u64),
357        );
358        map.insert(
359            "Animation background color".to_string(),
360            DataMap::UInt(parsed.animation.background_color as u64),
361        );
362        map.insert(
363            "Animation frame durations".to_string(),
364            DataMap::UIntAllay(
365                parsed
366                    .frames
367                    .iter()
368                    .map(|frame| frame.duration as u64)
369                    .collect(),
370            ),
371        );
372    }
373
374    for (fourcc, payload) in chunks {
375        match &fourcc {
376            b"ICCP" => {
377                map.insert("ICC Profile".to_string(), DataMap::Raw(payload.to_vec()));
378            }
379            b"EXIF" => {
380                map.insert("EXIF Raw".to_string(), DataMap::Raw(payload.to_vec()));
381            }
382            b"XMP " => match String::from_utf8(payload.to_vec()) {
383                Ok(xmp) => {
384                    map.insert("XMP".to_string(), DataMap::Ascii(xmp));
385                }
386                Err(_) => {
387                    map.insert("XMP Raw".to_string(), DataMap::Raw(payload.to_vec()));
388                }
389            },
390            _ => {}
391        }
392    }
393
394    Ok(map)
395}
396
397/// Decodes a WebP image using a callback-based interface compatible with
398/// `wml2`'s draw-side flow.
399pub fn decode<B: BinaryReader>(
400    reader: &mut B,
401    option: &mut DecodeOptions<'_>,
402) -> Result<(), Error> {
403    let data = read_container(reader)?;
404    let metadata = make_metadata(&data).map_err(map_error)?;
405    let features = get_features(&data).map_err(map_error)?;
406
407    if features.has_animation {
408        let parsed = parse_animation_webp(&data).map_err(map_error)?;
409        let init = InitOptions {
410            loop_count: parsed.animation.loop_count as u32,
411            background: Some(argb_to_rgba(parsed.animation.background_color)),
412            animation: true,
413        };
414        option
415            .drawer
416            .init(parsed.features.width, parsed.features.height, Some(init))?;
417
418        let mut allow_multi_image = false;
419        for (index, frame) in parsed.frames.iter().enumerate() {
420            let decoded = decode_frame_rgba(frame).map_err(map_error)?;
421            if index == 0 {
422                option.drawer.draw(
423                    frame.x_offset,
424                    frame.y_offset,
425                    frame.width,
426                    frame.height,
427                    &decoded.rgba,
428                    None,
429                )?;
430
431                let result = option.drawer.next(Some(next_options(frame)))?;
432                if let Some(response) = result {
433                    if response.response == ResponseCommand::Continue {
434                        allow_multi_image = true;
435                        option
436                            .drawer
437                            .draw(0, 0, frame.width, frame.height, &decoded.rgba, None)?;
438                    }
439                }
440                continue;
441            }
442
443            if !allow_multi_image {
444                continue;
445            }
446
447            let result = option.drawer.next(Some(next_options(frame)))?;
448            if let Some(response) = result {
449                if response.response == ResponseCommand::Abort {
450                    break;
451                }
452            }
453
454            option
455                .drawer
456                .draw(0, 0, frame.width, frame.height, &decoded.rgba, None)?;
457        }
458    } else {
459        let init = InitOptions {
460            loop_count: 0,
461            background: None,
462            animation: false,
463        };
464        option
465            .drawer
466            .init(features.width, features.height, Some(init))?;
467
468        let decoded = match features.format {
469            WebpFormat::Lossy => decode_lossy_webp_to_rgba(&data).map_err(map_error)?,
470            WebpFormat::Lossless => decode_lossless_webp_to_rgba(&data).map_err(map_error)?,
471            WebpFormat::Undefined => {
472                return Err(Box::new(std::io::Error::new(
473                    std::io::ErrorKind::Unsupported,
474                    "unsupported WebP format",
475                )));
476            }
477        };
478
479        option
480            .drawer
481            .draw(0, 0, decoded.width, decoded.height, &decoded.rgba, None)?;
482    }
483
484    for (key, value) in metadata {
485        option.drawer.set_metadata(&key, value)?;
486    }
487    option.drawer.terminate(None)?;
488
489    Ok(())
490}