Skip to main content

slimg_core/codec/
webp.rs

1use crate::error::{Error, Result};
2use crate::format::Format;
3
4use super::{Codec, EncodeOptions, ImageData};
5
6/// WebP codec backed by libwebp.
7pub struct WebPCodec;
8
9impl Codec for WebPCodec {
10    fn format(&self) -> Format {
11        Format::WebP
12    }
13
14    fn decode(&self, data: &[u8]) -> Result<ImageData> {
15        let img = image::load_from_memory_with_format(data, image::ImageFormat::WebP)
16            .map_err(|e| Error::Decode(format!("webp decode: {e}")))?;
17
18        let rgba = img.to_rgba8();
19        let width = rgba.width();
20        let height = rgba.height();
21
22        Ok(ImageData::new(width, height, rgba.into_raw()))
23    }
24
25    fn encode(&self, image: &ImageData, options: &EncodeOptions) -> Result<Vec<u8>> {
26        let encoder = webp::Encoder::from_rgba(&image.data, image.width, image.height);
27        let encoded = encoder.encode(options.quality as f32);
28
29        Ok(encoded.to_vec())
30    }
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    fn create_test_image(width: u32, height: u32) -> ImageData {
38        let size = (width * height * 4) as usize;
39        let mut data = vec![0u8; size];
40        for y in 0..height {
41            for x in 0..width {
42                let i = ((y * width + x) * 4) as usize;
43                data[i] = (x * 255 / width) as u8; // R
44                data[i + 1] = (y * 255 / height) as u8; // G
45                data[i + 2] = 128; // B
46                data[i + 3] = 255; // A
47            }
48        }
49        ImageData::new(width, height, data)
50    }
51
52    #[test]
53    fn encode_and_decode_roundtrip() {
54        let codec = WebPCodec;
55        let original = create_test_image(64, 48);
56        let options = EncodeOptions { quality: 90 };
57
58        let encoded = codec.encode(&original, &options).expect("encode failed");
59
60        // Decode back and verify dimensions
61        let decoded = codec.decode(&encoded).expect("decode failed");
62        assert_eq!(decoded.width, original.width);
63        assert_eq!(decoded.height, original.height);
64        assert_eq!(
65            decoded.data.len(),
66            (decoded.width * decoded.height * 4) as usize
67        );
68    }
69
70    #[test]
71    fn lower_quality_produces_smaller_file() {
72        let codec = WebPCodec;
73        let image = create_test_image(128, 96);
74
75        let high = codec
76            .encode(&image, &EncodeOptions { quality: 95 })
77            .expect("encode q95 failed");
78        let low = codec
79            .encode(&image, &EncodeOptions { quality: 20 })
80            .expect("encode q20 failed");
81
82        assert!(
83            low.len() < high.len(),
84            "low quality ({} bytes) should be smaller than high quality ({} bytes)",
85            low.len(),
86            high.len(),
87        );
88    }
89}