Skip to main content

rff_codec_webp/
lib.rs

1//! WebP still-image codec, backed by the pure-Rust [`image_webp`].
2//!
3//! Decode handles both VP8 (lossy) and VP8L (lossless), yielding packed
4//! [`Rgb24`](PixelFormat::Rgb24) or [`Rgba`](PixelFormat::Rgba). Encode is
5//! **lossless** (image-webp's encoder). Bridge to the YUV codecs with
6//! `-vf format=...`.
7
8use std::io::Cursor;
9
10use image_webp::{ColorType, WebPDecoder, WebPEncoder};
11use rff_codec::{Codec, CodecRegistry, Decoder, Encoder};
12use rff_core::{Error, Frame, MediaType, Packet, PixelFormat, Result, VideoFrame};
13
14/// Register the WebP codec into a [`CodecRegistry`].
15pub fn register(registry: &mut CodecRegistry) {
16    registry.register(Codec {
17        id: rff_core::CodecId::Webp,
18        name: "webp",
19        long_name: "WebP image (VP8 / VP8L)",
20        media_type: MediaType::Video,
21        decoder: Some(|| Box::new(WebpDecoder::default())),
22        encoder: Some(|| Box::new(WebpEncoder::default())),
23    });
24}
25
26#[derive(Default)]
27struct WebpDecoder {
28    frame: Option<Frame>,
29    eof: bool,
30}
31
32impl Decoder for WebpDecoder {
33    fn send_packet(&mut self, packet: &Packet) -> Result<()> {
34        self.frame = Some(decode_webp(&packet.data)?);
35        Ok(())
36    }
37
38    fn receive_frame(&mut self) -> Result<Frame> {
39        if let Some(frame) = self.frame.take() {
40            return Ok(frame);
41        }
42        if self.eof {
43            Err(Error::Eof)
44        } else {
45            Err(Error::Again)
46        }
47    }
48
49    fn flush(&mut self) {
50        self.eof = true;
51    }
52}
53
54fn decode_webp(data: &[u8]) -> Result<Frame> {
55    let mut decoder = WebPDecoder::new(Cursor::new(data))
56        .map_err(|e| Error::invalid(format!("webp decode: {e}")))?;
57    let (w, h) = decoder.dimensions();
58    let has_alpha = decoder.has_alpha();
59    let channels = if has_alpha { 4 } else { 3 };
60
61    let mut buf = vec![0u8; w as usize * h as usize * channels];
62    decoder
63        .read_image(&mut buf)
64        .map_err(|e| Error::invalid(format!("webp decode: {e}")))?;
65
66    let (format, stride) = if has_alpha {
67        (PixelFormat::Rgba, w as usize * 4)
68    } else {
69        (PixelFormat::Rgb24, w as usize * 3)
70    };
71    Ok(Frame::Video(VideoFrame {
72        width: w,
73        height: h,
74        format,
75        planes: vec![buf],
76        strides: vec![stride],
77        pts: None,
78    }))
79}
80
81#[derive(Default)]
82struct WebpEncoder {
83    packet: Option<Packet>,
84    eof: bool,
85}
86
87impl Encoder for WebpEncoder {
88    /// Packed RGB only — this encoder has no Y'CbCr path, so the pipeline
89    /// converts planar input (e.g. straight from the JPEG decoder) for us.
90    fn accepted_pixel_formats(&self) -> Option<Vec<PixelFormat>> {
91        Some(vec![PixelFormat::Rgb24, PixelFormat::Rgba])
92    }
93
94    fn send_frame(&mut self, frame: &Frame) -> Result<()> {
95        let vf = match frame {
96            Frame::Video(v) => v,
97            Frame::Audio(_) => {
98                return Err(Error::unsupported(
99                    "webp encode: audio frame on an image codec",
100                ))
101            }
102        };
103        self.packet = Some(Packet::from_data(0, encode_webp(vf)?));
104        Ok(())
105    }
106
107    fn receive_packet(&mut self) -> Result<Packet> {
108        if let Some(packet) = self.packet.take() {
109            return Ok(packet);
110        }
111        if self.eof {
112            Err(Error::Eof)
113        } else {
114            Err(Error::Again)
115        }
116    }
117
118    fn flush(&mut self) {
119        self.eof = true;
120    }
121}
122
123fn encode_webp(vf: &VideoFrame) -> Result<Vec<u8>> {
124    let (color, channels) = match vf.format {
125        PixelFormat::Rgb24 => (ColorType::Rgb8, 3usize),
126        PixelFormat::Rgba => (ColorType::Rgba8, 4usize),
127        other => {
128            return Err(Error::unsupported(format!(
129                "webp encode: needs rgb24/rgba, got `{}` (convert with -vf format=rgb24)",
130                other.name()
131            )))
132        }
133    };
134    let (w, h) = (vf.width as usize, vf.height as usize);
135    let row = w * channels;
136    let stride = vf.strides[0];
137    let packed: Vec<u8> = if stride == row {
138        vf.planes[0].clone()
139    } else {
140        let mut p = Vec::with_capacity(row * h);
141        for j in 0..h {
142            p.extend_from_slice(&vf.planes[0][j * stride..j * stride + row]);
143        }
144        p
145    };
146
147    let mut out = Vec::new();
148    WebPEncoder::new(&mut out)
149        .encode(&packed, vf.width, vf.height, color)
150        .map_err(|e| Error::invalid(format!("webp encode: {e}")))?;
151    Ok(out)
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn webp_lossless_encode_decode_roundtrips() {
160        let (w, h) = (32u32, 24u32);
161        let (wi, hi) = (w as usize, h as usize);
162        let mut rgb = vec![0u8; wi * hi * 3];
163        for j in 0..hi {
164            for i in 0..wi {
165                let o = (j * wi + i) * 3;
166                rgb[o] = (i * 255 / (wi - 1)) as u8;
167                rgb[o + 1] = (j * 255 / (hi - 1)) as u8;
168                rgb[o + 2] = 64;
169            }
170        }
171        let src = VideoFrame {
172            width: w,
173            height: h,
174            format: PixelFormat::Rgb24,
175            planes: vec![rgb.clone()],
176            strides: vec![wi * 3],
177            pts: None,
178        };
179
180        let bytes = encode_webp(&src).unwrap();
181        assert_eq!(&bytes[0..4], b"RIFF");
182        assert_eq!(&bytes[8..12], b"WEBP");
183
184        let Frame::Video(decoded) = decode_webp(&bytes).unwrap() else {
185            unreachable!()
186        };
187        assert_eq!((decoded.width, decoded.height), (w, h));
188        // Lossless: an RGB source round-trips exactly (decoded may carry alpha).
189        let got_rgb: Vec<u8> = match decoded.format {
190            PixelFormat::Rgb24 => decoded.planes[0].clone(),
191            PixelFormat::Rgba => decoded.planes[0]
192                .chunks_exact(4)
193                .flat_map(|p| p[0..3].to_vec())
194                .collect(),
195            other => panic!("unexpected format {other:?}"),
196        };
197        assert_eq!(got_rgb, rgb);
198    }
199}