Skip to main content

webp_animation/
encoder.rs

1use std::{mem, pin::Pin, ptr};
2
3use libwebp_sys as webp;
4
5use crate::{ColorMode, ConfigContainer, EncoderOptions, EncodingConfig, Error, WebPData};
6
7#[allow(unused_imports)]
8use crate::LossyEncodingConfig; // for docs
9
10/// An encoder for creating webp animation
11///
12/// Will take `n` frames as an input. WebP binary data is output at the end
13/// (wrapped into [`WebPData`] which acts as a `&[u8]`)
14///
15/// # Example without special configuration
16///
17/// ```rust
18/// use webp_animation::prelude::*;
19///
20/// // setup
21/// let dimensions = (64, 32);
22/// let bright_frame = [255, 255, 255, 255].repeat(64 * 32);
23/// let dark_frame = [0, 0, 0, 255].repeat(64 * 32);
24///
25/// // init encoder
26/// let mut encoder = Encoder::new(dimensions).unwrap();
27///
28/// // insert frames to specific (increasing) timestamps
29/// for i in 0..5 {
30///   let rgba_data = if i % 2 == 0 { &bright_frame } else { &dark_frame };
31///   let frame_timestamp_ms = i * 170;
32///
33///   encoder.add_frame(rgba_data, frame_timestamp_ms).unwrap();
34/// }
35///
36/// // get encoded webp data
37/// let final_timestamp_ms = 1_000;
38/// let webp_data = encoder.finalize(final_timestamp_ms).unwrap();
39/// // std::fs::write("my_animation.webp", webp_data);
40/// ```
41///
42/// # Example with configuration
43///
44/// See [`EncodingConfig`] and [`LossyEncodingConfig`] for per-field explanations.
45/// ```rust
46/// use webp_animation::prelude::*;
47///
48/// let mut encoder = Encoder::new_with_options((640, 480), EncoderOptions {
49///     kmin: 3,
50///     kmax: 5,
51///     encoding_config: Some(EncodingConfig {
52///         quality: 75.,
53///         encoding_type: EncodingType::Lossy(LossyEncodingConfig {
54///             segments: 2,
55///             alpha_compression: true,
56///             ..Default::default()
57///         }),
58///         ..Default::default()
59///     }),
60///     ..Default::default()
61/// }).unwrap();
62/// ```
63///
64/// # Example with per-frame configuration
65///
66/// ```rust
67/// use webp_animation::prelude::*;
68///
69/// let mut encoder = Encoder::new_with_options((640, 480), EncoderOptions {
70///     kmin: 3,
71///     kmax: 5,
72///     ..Default::default()
73/// }).unwrap();
74///
75/// encoder.add_frame_with_config(&[0u8; 640 * 480 * 4], 0, &EncodingConfig {
76///     quality: 75.,
77///     encoding_type: EncodingType::Lossy(LossyEncodingConfig {
78///         segments: 2,
79///         alpha_compression: true,
80///         ..Default::default()
81///     }),
82///     ..Default::default()
83/// }).unwrap();
84/// ```
85pub struct Encoder {
86    encoder_wr: EncoderWrapper,
87    frame: PictureWrapper,
88    options: EncoderOptions,
89    previous_timestamp: i32,
90    encoding_config: Option<ConfigContainer>,
91}
92
93impl Encoder {
94    /// Construct a new encoder with default options for dimensions (`width`, `height`)
95    pub fn new(dimensions: (u32, u32)) -> Result<Self, Error> {
96        Encoder::new_with_options(dimensions, Default::default())
97    }
98
99    /// Construct a new encoder with custom options for dimensions (`width`, `height`)
100    pub fn new_with_options(
101        dimensions: (u32, u32),
102        options: EncoderOptions,
103    ) -> Result<Self, Error> {
104        if dimensions.0 == 0 || dimensions.1 == 0 {
105            return Err(Error::DimensionsMustbePositive);
106        }
107
108        let enc_options = convert_options(&options)?;
109        let encoder_wr = EncoderWrapper::new(dimensions, enc_options)?;
110
111        log::trace!("Encoder initialized with dimensions {:?}", dimensions);
112
113        let mut encoder = Self {
114            encoder_wr,
115            options: options.clone(),
116            frame: PictureWrapper::new(dimensions)?,
117            previous_timestamp: -1,
118            encoding_config: None,
119        };
120
121        if let Some(config) = options.encoding_config {
122            encoder.set_default_encoding_config(config)?;
123        }
124
125        Ok(encoder)
126    }
127
128    /// Add a new frame to be encoded
129    ///
130    /// Inputs
131    /// * `data` is an array of pixels in [`ColorMode`] format set by [`EncoderOptions`]
132    ///   ([`ColorMode::Rgba`] by default)
133    /// * `timestamp_ms` of this frame in milliseconds. Duration of a frame would be
134    ///   calculated as "timestamp of next frame - timestamp of this frame".
135    ///   Hence, timestamps should be in non-decreasing order.
136    pub fn add_frame(&mut self, data: &[u8], timestamp_ms: i32) -> Result<(), Error> {
137        self.add_frame_internal(data, timestamp_ms, None)
138    }
139
140    /// Add a new frame to be encoded with special per-frame configuration ([`EncodingConfig`])
141    ///
142    /// See [`Encoder::add_frame`] for `data` and `timestamp` explanations
143    pub fn add_frame_with_config(
144        &mut self,
145        data: &[u8],
146        timestamp_ms: i32,
147        config: &EncodingConfig,
148    ) -> Result<(), Error> {
149        self.add_frame_internal(data, timestamp_ms, Some(config))
150    }
151
152    fn add_frame_internal(
153        &mut self,
154        data: &[u8],
155        timestamp: i32,
156        config: Option<&EncodingConfig>,
157    ) -> Result<(), Error> {
158        if timestamp <= self.previous_timestamp {
159            return Err(Error::TimestampMustBeHigherThanPrevious(
160                timestamp,
161                self.previous_timestamp,
162            ));
163        }
164
165        self.frame.set_data(data, self.options.color_mode)?;
166
167        if unsafe {
168            webp::WebPAnimEncoderAdd(
169                self.encoder_wr.encoder,
170                self.frame.as_webp_picture_ref(),
171                timestamp,
172                match config {
173                    Some(config) => {
174                        let config = config.to_config_container()?;
175                        config.as_ptr()
176                    }
177                    None => match &self.encoding_config {
178                        Some(config) => config.as_ptr(),
179                        None => std::ptr::null(),
180                    },
181                },
182            )
183        } == 0
184        {
185            return Err(Error::EncoderAddFailed);
186        }
187
188        self.previous_timestamp = timestamp;
189
190        log::trace!(
191            "Add a frame at timestamp {}ms, {} bytes",
192            timestamp,
193            data.len()
194        );
195
196        Ok(())
197    }
198
199    /// Sets the default encoding config
200    ///
201    /// Usually set in [`EncderOptions`] at constructor ([`Encoder::new_with_options`])
202    pub fn set_default_encoding_config(&mut self, config: EncodingConfig) -> Result<(), Error> {
203        self.encoding_config = Some(config.to_config_container()?);
204        self.options.encoding_config = Some(config);
205        Ok(())
206    }
207
208    /// Will encode the stream and return encoded bytes in a [`WebPData`] upon success
209    ///
210    /// `timestamp_ms` behaves as in [`Encoder::add_frame`], and determines the duration of the last frame
211    pub fn finalize(self, timestamp_ms: i32) -> Result<WebPData, Error> {
212        if self.previous_timestamp == -1 {
213            // -1 = no frames added
214            return Err(Error::NoFramesAdded);
215        }
216
217        if timestamp_ms < self.previous_timestamp {
218            return Err(Error::TimestampMustBeEqualOrHigherThanPrevious(
219                timestamp_ms,
220                self.previous_timestamp,
221            ));
222        }
223
224        if unsafe {
225            webp::WebPAnimEncoderAdd(
226                self.encoder_wr.encoder,
227                ptr::null_mut(),
228                timestamp_ms,
229                ptr::null_mut(),
230            )
231        } == 0
232        {
233            return Err(Error::EncoderAddFailed);
234        }
235
236        let mut data = WebPData::new();
237
238        if unsafe { webp::WebPAnimEncoderAssemble(self.encoder_wr.encoder, data.inner_ref()) } == 0
239        {
240            return Err(Error::EncoderAssmebleFailed);
241        }
242
243        log::trace!(
244            "Finalize encoding at timestamp {}ms, output binary size {} bytes",
245            timestamp_ms,
246            data.len()
247        );
248
249        Ok(data)
250    }
251}
252
253fn convert_options(
254    options: &EncoderOptions,
255) -> Result<Pin<Box<webp::WebPAnimEncoderOptions>>, Error> {
256    let mut enc_options = Box::pin(unsafe {
257        let mut enc_options = mem::zeroed();
258        if webp::WebPAnimEncoderOptionsInit(&mut enc_options) != 1 {
259            return Err(Error::OptionsInitFailed);
260        }
261        enc_options
262    });
263
264    enc_options.anim_params.loop_count = options.anim_params.loop_count;
265
266    enc_options.minimize_size = if options.minimize_size { 1 } else { 0 };
267    enc_options.kmin = options.kmin as i32;
268    enc_options.kmax = options.kmax as i32;
269    enc_options.allow_mixed = if options.allow_mixed { 1 } else { 0 };
270    enc_options.verbose = if options.verbose { 1 } else { 0 };
271
272    Ok(enc_options)
273}
274
275struct EncoderWrapper {
276    encoder: *mut webp::WebPAnimEncoder,
277
278    #[allow(dead_code)]
279    options: Pin<Box<webp::WebPAnimEncoderOptions>>,
280}
281
282impl EncoderWrapper {
283    pub fn new(
284        dimensions: (u32, u32),
285        options: Pin<Box<webp::WebPAnimEncoderOptions>>,
286    ) -> Result<Self, Error> {
287        let (width, height) = dimensions;
288
289        let encoder = unsafe { webp::WebPAnimEncoderNew(width as i32, height as i32, &*options) };
290        if encoder.is_null() {
291            return Err(Error::EncoderCreateFailed);
292        }
293
294        Ok(Self { encoder, options })
295    }
296}
297
298impl Drop for EncoderWrapper {
299    fn drop(&mut self) {
300        unsafe { webp::WebPAnimEncoderDelete(self.encoder) };
301    }
302}
303
304struct PictureWrapper {
305    picture: webp::WebPPicture,
306}
307
308impl PictureWrapper {
309    pub fn new(dimensions: (u32, u32)) -> Result<Self, Error> {
310        let mut picture = unsafe {
311            let mut picture = mem::zeroed();
312            assert!(webp::WebPPictureInit(&mut picture) != 0);
313            picture
314        };
315
316        picture.width = dimensions.0 as i32;
317        picture.height = dimensions.1 as i32;
318        picture.use_argb = 1;
319
320        Ok(Self { picture })
321    }
322
323    pub fn as_webp_picture_ref(&mut self) -> &mut webp::WebPPicture {
324        &mut self.picture
325    }
326
327    pub fn set_data(&mut self, data: &[u8], color_mode: ColorMode) -> Result<(), Error> {
328        let received_len = data.len();
329        let expected_len = self.data_size(color_mode);
330        if received_len != expected_len {
331            return Err(Error::BufferSizeFailed(expected_len, received_len));
332        }
333
334        if unsafe {
335            let size = color_mode.size() as i32;
336
337            match color_mode {
338                ColorMode::Rgba => webp::WebPPictureImportRGBA(
339                    &mut self.picture,
340                    data.as_ptr(),
341                    self.picture.width * size,
342                ),
343                ColorMode::Bgra => webp::WebPPictureImportBGRA(
344                    &mut self.picture,
345                    data.as_ptr(),
346                    self.picture.width * size,
347                ),
348                ColorMode::Rgb => webp::WebPPictureImportRGB(
349                    &mut self.picture,
350                    data.as_ptr(),
351                    self.picture.width * size,
352                ),
353                ColorMode::Bgr => webp::WebPPictureImportBGR(
354                    &mut self.picture,
355                    data.as_ptr(),
356                    self.picture.width * size,
357                ),
358            }
359        } == 0
360        {
361            return Err(Error::PictureImportFailed);
362        }
363
364        Ok(())
365    }
366
367    fn data_size(&self, color_mode: ColorMode) -> usize {
368        self.picture.width as usize * self.picture.height as usize * color_mode.size()
369    }
370}
371
372impl Drop for PictureWrapper {
373    fn drop(&mut self) {
374        unsafe { webp::WebPPictureFree(&mut self.picture) };
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::{Decoder, EncodingType, Frame, LossyEncodingConfig};
382    use std::fs::File;
383    use std::io::prelude::*;
384
385    #[test]
386    fn test_encoder() {
387        // read frames to be encoded
388        let mut frames = read_frames();
389
390        // encode data
391        let mut encoder = Encoder::new((400, 400)).unwrap();
392        for frame in &mut frames {
393            encoder.add_frame(frame.data(), frame.timestamp()).unwrap();
394        }
395        let webp_data = encoder.finalize(440).unwrap();
396        assert!(webp_data.len() > 0);
397        assert_eq!(&webp_data[..5], &[82, 73, 70, 70, 18]);
398
399        // decode previously encoded data
400        let decoder = Decoder::new(&webp_data).unwrap();
401        let decoded_frames: Vec<_> = decoder.into_iter().collect();
402
403        // assert that re-encoded matches the decoded frames
404        assert_eq!(frames.len(), decoded_frames.len());
405        for (f1, f2) in decoded_frames.iter().zip(frames) {
406            assert_eq!(f1.dimensions(), f2.dimensions());
407            assert_eq!(f1.color_mode(), f2.color_mode());
408            assert_eq!(f1.timestamp(), f2.timestamp());
409            assert_eq!(f1.data(), f2.data());
410        }
411    }
412
413    fn read_frames() -> Vec<Frame> {
414        let mut buf = Vec::new();
415        File::open("./data/animated.webp")
416            .unwrap()
417            .read_to_end(&mut buf)
418            .unwrap();
419
420        let decoder = Decoder::new(&buf).unwrap();
421        let frames: Vec<_> = decoder.into_iter().collect();
422        frames
423    }
424
425    #[test]
426    fn test_enc_options() {
427        let mut encoder = Encoder::new((400, 400)).unwrap();
428        encoder.add_frame(&[0u8; 400 * 400 * 4], 0).unwrap();
429        encoder
430            .add_frame_with_config(&[0u8; 400 * 400 * 4], 100, &EncodingConfig::default())
431            .unwrap();
432
433        let buf = encoder.finalize(200).unwrap();
434
435        let decoder = Decoder::new(&buf).unwrap();
436        let frames: Vec<_> = decoder.into_iter().collect();
437        assert_eq!(frames[0].dimensions(), (400, 400));
438        assert_eq!(frames[0].data(), &[0u8; 400 * 400 * 4]);
439    }
440
441    #[test]
442    fn test_failures() {
443        let mut encoder = Encoder::new((400, 400)).unwrap();
444        assert_eq!(
445            encoder.add_frame(&[0u8; 450 * 450 * 4], 0).unwrap_err(),
446            Error::BufferSizeFailed(640_000, 810_000)
447        );
448
449        assert_eq!(
450            encoder.add_frame(&[0u8; 50 * 50 * 4], 0).unwrap_err(),
451            Error::BufferSizeFailed(640_000, 10_000)
452        );
453
454        encoder.add_frame(&[0u8; 400 * 400 * 4], 0).unwrap();
455
456        assert_eq!(
457            encoder.add_frame(&[0u8; 400 * 400 * 4], -1).unwrap_err(),
458            Error::TimestampMustBeHigherThanPrevious(-1, 0)
459        );
460
461        assert_eq!(
462            encoder.add_frame(&[0u8; 400 * 400 * 4], 0).unwrap_err(),
463            Error::TimestampMustBeHigherThanPrevious(0, 0)
464        );
465
466        encoder.add_frame(&[0u8; 400 * 400 * 4], 10).unwrap();
467
468        assert_eq!(
469            encoder.finalize(0).unwrap_err(),
470            Error::TimestampMustBeEqualOrHigherThanPrevious(0, 10)
471        );
472    }
473
474    #[test]
475    fn test_wrong_encoding_config() {
476        let mut encoder = Encoder::new((4, 4)).unwrap();
477        assert!(encoder
478            .add_frame_with_config(
479                &[0u8; 4 * 4 * 4],
480                0,
481                &EncodingConfig {
482                    quality: 100.,
483                    ..Default::default()
484                },
485            )
486            .is_ok());
487
488        assert_eq!(
489            encoder
490                .add_frame_with_config(
491                    &[0u8; 4 * 4 * 4],
492                    5,
493                    &EncodingConfig {
494                        quality: 101.,
495                        ..Default::default()
496                    },
497                )
498                .unwrap_err(),
499            Error::InvalidEncodingConfig
500        );
501    }
502
503    #[test]
504    fn test_wrong_lossy_config() {
505        assert_eq!(
506            add_lossy_frame(LossyEncodingConfig {
507                segments: 9999,
508                ..Default::default()
509            })
510            .unwrap_err(),
511            Error::InvalidEncodingConfig
512        );
513
514        assert_eq!(
515            add_lossy_frame(LossyEncodingConfig {
516                pass: 11,
517                ..Default::default()
518            })
519            .unwrap_err(),
520            Error::InvalidEncodingConfig
521        );
522
523        assert_eq!(
524            add_lossy_frame(LossyEncodingConfig {
525                filter_sharpness: 8,
526                ..Default::default()
527            })
528            .unwrap_err(),
529            Error::InvalidEncodingConfig
530        );
531
532        assert!(add_lossy_frame(LossyEncodingConfig {
533            filter_sharpness: 7,
534            ..Default::default()
535        })
536        .is_ok());
537    }
538
539    fn add_lossy_frame(lossy_config: LossyEncodingConfig) -> Result<(), Error> {
540        let mut encoder = Encoder::new((4, 4)).unwrap();
541        encoder.add_frame_with_config(
542            &[0u8; 4 * 4 * 4],
543            0,
544            &EncodingConfig {
545                encoding_type: EncodingType::Lossy(lossy_config),
546                ..Default::default()
547            },
548        )
549    }
550}