Skip to main content

webp_anim/codec/
encode.rs

1use std::{error::Error, ffi::CStr, fmt, time::Duration};
2
3use libwebp_sys::{
4    WebPAnimEncoder, WebPAnimEncoderAdd, WebPAnimEncoderAssemble, WebPAnimEncoderDelete,
5    WebPAnimEncoderGetError, WebPAnimEncoderNewInternal, WebPAnimEncoderOptions,
6    WebPAnimEncoderOptionsInitInternal, WebPConfig, WebPData, WebPDataClear, WebPGetMuxABIVersion,
7    WebPPicture, WebPPictureFree, WebPPictureImportRGBA, WebPValidateConfig,
8};
9
10use crate::model::{AnimationFrame, AnimationInfo, BackgroundColor, CanvasSize, LoopCount};
11
12/// Compression and animation metadata for an [`AnimationEncoder`].
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct AnimationEncoderOptions {
15    /// Loop policy written to the output animation.
16    pub loop_count: LoopCount,
17    /// Raw ANIM background color written to the output animation.
18    pub background_color: BackgroundColor,
19    /// Explicit overrides for libwebp's per-frame encoding configuration.
20    /// Unspecified fields retain libwebp's initialized defaults.
21    pub config: EncoderConfigOverrides,
22    /// Explicit overrides for libwebp's animation-mux configuration.
23    /// Unspecified fields retain libwebp's initialized defaults.
24    pub animation: AnimationMuxOverrides,
25}
26
27/// Optional per-frame configuration passed to libwebp.
28///
29/// Applications should choose their product policy explicitly. `None` means to
30/// retain the corresponding value supplied by `WebPConfig::new()`.
31#[derive(Clone, Copy, Debug, Default, PartialEq)]
32pub struct EncoderConfigOverrides {
33    /// Lossy quality in libwebp's inclusive `0.0..=100.0` range.
34    pub quality: Option<f32>,
35    /// Encode frames losslessly when `Some(true)` or lossy when `Some(false)`.
36    pub lossless: Option<bool>,
37    /// libwebp encoding method in the inclusive `0..=6` range.
38    pub method: Option<u8>,
39    /// Use libwebp's high-quality (and slower) RGB-to-YUV conversion.
40    pub use_sharp_yuv: Option<bool>,
41    /// Let libwebp select the in-loop filter strength per frame.
42    pub autofilter: Option<bool>,
43    /// Alpha compression quality in the inclusive `0..=100` range.
44    pub alpha_quality: Option<u8>,
45    /// libwebp preprocessing mode (`0` disables it). Mode 2 introduces
46    /// dithering, which can create temporal noise in animation.
47    pub preprocessing: Option<u8>,
48    /// Ask libwebp to use its internal encoder threading when available.
49    pub thread_level: Option<bool>,
50    /// Fixed in-loop filtering settings.
51    pub filter_strength: Option<i32>,
52    /// Fixed in-loop filter sharpness in libwebp's `0..=7` range.
53    pub filter_sharpness: Option<i32>,
54    /// In-loop filter type in libwebp's `0..=1` range.
55    pub filter_type: Option<i32>,
56}
57
58/// Optional animation-mux configuration passed to libwebp.
59///
60/// `kmin` and `kmax` form one setting: provide both to choose a key-frame
61/// policy, or neither to retain libwebp's initialized behavior. The canonical
62/// `(0, 0)` disables inserted key frames and `(0, 1)` makes every frame a key
63/// frame. For `kmax >= 2`, libwebp requires `kmin < kmax`,
64/// `kmin >= kmax / 2 + 1`, and a range no wider than 30 frames.
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66pub struct AnimationMuxOverrides {
67    /// Let libwebp favor a smaller animation over encoding speed.
68    pub minimize_size: Option<bool>,
69    /// Allow libwebp to choose lossless or lossy encoding for each frame.
70    pub allow_mixed: Option<bool>,
71    /// Minimum and maximum distance between animation key frames.
72    pub kmin: Option<i32>,
73    /// Maximum distance between animation key frames.
74    pub kmax: Option<i32>,
75}
76
77impl AnimationEncoderOptions {
78    /// Creates options with explicit animation metadata and no encoding overrides.
79    pub const fn new(loop_count: LoopCount, background_color: BackgroundColor) -> Self {
80        Self {
81            loop_count,
82            background_color,
83            config: EncoderConfigOverrides {
84                quality: None,
85                lossless: None,
86                method: None,
87                use_sharp_yuv: None,
88                autofilter: None,
89                alpha_quality: None,
90                preprocessing: None,
91                thread_level: None,
92                filter_strength: None,
93                filter_sharpness: None,
94                filter_type: None,
95            },
96            animation: AnimationMuxOverrides {
97                minimize_size: None,
98                allow_mixed: None,
99                kmin: None,
100                kmax: None,
101            },
102        }
103    }
104
105    /// Starts with animation metadata copied from a decoded source sequence.
106    pub const fn from_animation_info(info: AnimationInfo) -> Self {
107        Self::new(info.loop_count, info.background_color)
108    }
109}
110
111impl Default for AnimationEncoderOptions {
112    fn default() -> Self {
113        // libwebp 0.9.6 initializes these mux metadata values to loop_count =
114        // 0 and bgcolor = 0xffffffff. No product compression or key-frame
115        // policy is selected here.
116        Self::new(LoopCount::Infinite, BackgroundColor { raw: 0xffff_ffff })
117    }
118}
119
120/// Sequential animated-WebP encoder for full-canvas RGBA frames.
121///
122/// [`Self::add_frame`] accepts frames in presentation order. It converts each
123/// frame's duration to the cumulative millisecond timestamp required by WebP,
124/// then [`Self::finish`] writes the final timestamp needed to retain the last
125/// frame's duration.
126pub struct AnimationEncoder {
127    encoder: *mut WebPAnimEncoder,
128    canvas: CanvasSize,
129    config: WebPConfig,
130    next_timestamp_ms: i32,
131    frame_count: u32,
132}
133
134impl Drop for AnimationEncoder {
135    fn drop(&mut self) {
136        // SAFETY: `encoder` is created only by libwebp and released exactly once here.
137        unsafe {
138            if !self.encoder.is_null() {
139                WebPAnimEncoderDelete(self.encoder);
140            }
141        }
142    }
143}
144
145impl AnimationEncoder {
146    /// Creates an encoder for full-canvas RGBA frames of the given dimensions.
147    ///
148    /// Frames must be added in presentation order and must use exactly
149    /// `canvas.width * canvas.height * 4` bytes. At least one frame is required
150    /// before [`Self::finish`] can produce output.
151    pub fn new(canvas: CanvasSize, options: AnimationEncoderOptions) -> Result<Self, EncodeError> {
152        let width =
153            i32::try_from(canvas.width).map_err(|_| EncodeError::InvalidCanvasSize(canvas))?;
154        let height =
155            i32::try_from(canvas.height).map_err(|_| EncodeError::InvalidCanvasSize(canvas))?;
156        if width == 0 || height == 0 || canvas.rgba_bytes().is_none() {
157            return Err(EncodeError::InvalidCanvasSize(canvas));
158        }
159
160        validate_options(&options)?;
161        let mut config = WebPConfig::new().map_err(|_| EncodeError::ConfigInitialization)?;
162        apply_config_overrides(&mut config, options.config);
163        // SAFETY: `config` was initialized by libwebp and remains valid for the call.
164        if unsafe { WebPValidateConfig(&config) } == 0 {
165            return Err(EncodeError::LibwebpConfigRejected);
166        }
167
168        let mux_abi = WebPGetMuxABIVersion();
169        // SAFETY: libwebp initializes every field before the options are read.
170        let mut encoder_options: WebPAnimEncoderOptions = unsafe { std::mem::zeroed() };
171        // SAFETY: the options pointer is writable and the ABI comes from libwebp.
172        if unsafe { WebPAnimEncoderOptionsInitInternal(&mut encoder_options, mux_abi) } == 0 {
173            return Err(EncodeError::EncoderOptionsInitialization);
174        }
175        encoder_options.anim_params.loop_count = match options.loop_count {
176            LoopCount::Infinite => 0,
177            LoopCount::Finite(count) => i32::from(count.get()),
178        };
179        encoder_options.anim_params.bgcolor = options.background_color.raw;
180        apply_mux_overrides(&mut encoder_options, options.animation);
181
182        // SAFETY: dimensions and initialized options remain valid for this construction call.
183        let encoder =
184            unsafe { WebPAnimEncoderNewInternal(width, height, &encoder_options, mux_abi) };
185        if encoder.is_null() {
186            return Err(EncodeError::EncoderCreation);
187        }
188
189        Ok(Self {
190            encoder,
191            canvas,
192            config,
193            next_timestamp_ms: 0,
194            frame_count: 0,
195        })
196    }
197
198    /// Returns the canvas dimensions required by every frame.
199    pub const fn canvas(&self) -> CanvasSize {
200        self.canvas
201    }
202
203    /// Returns the number of frames accepted so far.
204    pub const fn frame_count(&self) -> u32 {
205        self.frame_count
206    }
207
208    /// Adds one full-canvas frame in presentation order.
209    pub fn add_frame(&mut self, frame: &AnimationFrame) -> Result<(), EncodeError> {
210        if frame.canvas != self.canvas {
211            return Err(EncodeError::UnexpectedFrameCanvas {
212                actual: frame.canvas,
213                expected: self.canvas,
214            });
215        }
216        self.add_rgba(&frame.rgba, frame.duration)
217    }
218
219    /// Adds one full-canvas RGBA frame without allocating an [`AnimationFrame`].
220    pub fn add_rgba(&mut self, rgba: &[u8], duration: Duration) -> Result<(), EncodeError> {
221        let expected = self
222            .canvas
223            .rgba_bytes()
224            .ok_or(EncodeError::InvalidCanvasSize(self.canvas))?;
225        if rgba.len() != expected {
226            return Err(EncodeError::InvalidFrameBufferLength {
227                actual: rgba.len(),
228                expected,
229            });
230        }
231        let duration_ms = duration_to_millis(duration)?;
232        let end_timestamp_ms = self
233            .next_timestamp_ms
234            .checked_add(duration_ms)
235            .ok_or(EncodeError::TimestampOverflow)?;
236
237        let mut picture = Picture::from_rgba(self.canvas, rgba)?;
238        // SAFETY: encoder, picture, and config are valid; libwebp consumes the frame during this call.
239        if unsafe {
240            WebPAnimEncoderAdd(
241                self.encoder,
242                &mut picture.0,
243                self.next_timestamp_ms,
244                &self.config,
245            )
246        } == 0
247        {
248            return Err(EncodeError::Libwebp(encoder_error(self.encoder)));
249        }
250
251        self.next_timestamp_ms = end_timestamp_ms;
252        self.frame_count = self
253            .frame_count
254            .checked_add(1)
255            .ok_or(EncodeError::FrameCountOverflow)?;
256        Ok(())
257    }
258
259    /// Flushes the final frame duration and returns the encoded WebP bytes.
260    pub fn finish(self) -> Result<Vec<u8>, EncodeError> {
261        if self.frame_count == 0 {
262            return Err(EncodeError::NoFrames);
263        }
264        // SAFETY: the null picture with the final timestamp signals end-of-stream to libwebp.
265        if unsafe {
266            WebPAnimEncoderAdd(
267                self.encoder,
268                std::ptr::null_mut(),
269                self.next_timestamp_ms,
270                std::ptr::null(),
271            )
272        } == 0
273        {
274            return Err(EncodeError::Libwebp(encoder_error(self.encoder)));
275        }
276        let mut encoded = WebPData::default();
277        // SAFETY: libwebp initializes `encoded` on success for this valid encoder.
278        if unsafe { WebPAnimEncoderAssemble(self.encoder, &mut encoded) } == 0 {
279            return Err(EncodeError::Libwebp(encoder_error(self.encoder)));
280        }
281        // SAFETY: libwebp allocated exactly `encoded.size` bytes and ownership is released below.
282        let output = unsafe { std::slice::from_raw_parts(encoded.bytes, encoded.size) }.to_vec();
283        // SAFETY: `encoded` is initialized by libwebp and this frees its output allocation exactly once.
284        unsafe { WebPDataClear(&mut encoded) };
285        Ok(output)
286    }
287}
288
289/// Failure to create or use an [`AnimationEncoder`].
290#[derive(Clone, Debug, PartialEq)]
291pub enum EncodeError {
292    /// The encoder canvas has a zero dimension or cannot be represented by libwebp.
293    InvalidCanvasSize(CanvasSize),
294    /// libwebp could not initialize its encoding configuration.
295    ConfigInitialization,
296    /// The lossy quality override is outside `0.0..=100.0` or is not finite.
297    InvalidQuality {
298        /// Rejected quality value.
299        value: f32,
300    },
301    /// The encoding method is outside libwebp's `0..=6` range.
302    InvalidMethod {
303        /// Rejected method value.
304        value: u8,
305    },
306    /// The alpha quality is outside libwebp's `0..=100` range.
307    InvalidAlphaQuality {
308        /// Rejected alpha quality value.
309        value: u8,
310    },
311    /// The preprocessing mode is outside libwebp's `0..=2` range.
312    InvalidPreprocessing {
313        /// Rejected preprocessing mode.
314        value: u8,
315    },
316    /// The filter strength is outside libwebp's `0..=100` range.
317    InvalidFilterStrength {
318        /// Rejected filter strength.
319        value: i32,
320    },
321    /// The filter sharpness is outside libwebp's `0..=7` range.
322    InvalidFilterSharpness {
323        /// Rejected filter sharpness.
324        value: i32,
325    },
326    /// The filter type is outside libwebp's `0..=1` range.
327    InvalidFilterType {
328        /// Rejected filter type.
329        value: i32,
330    },
331    /// Only one of `kmin` and `kmax` was supplied.
332    IncompleteKeyframeInterval {
333        /// Supplied minimum key-frame interval.
334        kmin: Option<i32>,
335        /// Supplied maximum key-frame interval.
336        kmax: Option<i32>,
337    },
338    /// `kmin` is negative or is not smaller than `kmax`.
339    InvalidKeyframeInterval {
340        /// Supplied minimum key-frame interval.
341        kmin: i32,
342        /// Supplied maximum key-frame interval.
343        kmax: i32,
344    },
345    /// A special key-frame mode was not expressed as `(0, 0)` or `(0, 1)`.
346    NonCanonicalKeyframeMode {
347        /// Supplied minimum key-frame interval.
348        kmin: i32,
349        /// Supplied maximum key-frame interval.
350        kmax: i32,
351    },
352    /// `kmin` is below libwebp's required minimum for the selected `kmax`.
353    KeyframeIntervalBelowMinimum {
354        /// Supplied minimum key-frame interval.
355        kmin: i32,
356        /// Required minimum key-frame interval.
357        minimum: i32,
358        /// Supplied maximum key-frame interval.
359        kmax: i32,
360    },
361    /// The key-frame interval is wider than libwebp permits.
362    KeyframeIntervalTooWide {
363        /// Supplied minimum key-frame interval.
364        kmin: i32,
365        /// Supplied maximum key-frame interval.
366        kmax: i32,
367        /// Maximum permitted `kmax - kmin` span.
368        maximum_span: i32,
369    },
370    /// Key-frame intervals cannot be combined with `minimize_size = Some(true)`.
371    KeyframeIntervalIgnoredByMinimizeSize,
372    /// libwebp rejected an otherwise locally valid configuration.
373    LibwebpConfigRejected,
374    /// libwebp could not initialize animation encoder options.
375    EncoderOptionsInitialization,
376    /// libwebp could not create the animation encoder.
377    EncoderCreation,
378    /// [`AnimationEncoder::finish`] was called before adding a frame.
379    NoFrames,
380    /// A frame canvas differs from the encoder canvas.
381    UnexpectedFrameCanvas {
382        /// Canvas supplied by the caller.
383        actual: CanvasSize,
384        /// Canvas required by the encoder.
385        expected: CanvasSize,
386    },
387    /// A frame buffer is not tightly packed RGBA8 for the encoder canvas.
388    InvalidFrameBufferLength {
389        /// Actual frame buffer size in bytes.
390        actual: usize,
391        /// Required frame buffer size in bytes.
392        expected: usize,
393    },
394    /// A frame duration is not an exact whole number of milliseconds.
395    NonMillisecondDuration(Duration),
396    /// Cumulative frame duration exceeds libwebp's timestamp range.
397    TimestampOverflow,
398    /// The number of added frames exceeds `u32`.
399    FrameCountOverflow,
400    /// libwebp could not initialize a frame picture.
401    PictureInitialization,
402    /// libwebp could not import RGBA pixels into a frame picture.
403    PictureImport,
404    /// libwebp reported an encoding error message.
405    Libwebp(String),
406}
407
408impl fmt::Display for EncodeError {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self {
411            Self::InvalidCanvasSize(size) => write!(
412                f,
413                "canvas has invalid dimensions {}x{}",
414                size.width, size.height
415            ),
416            Self::ConfigInitialization => {
417                f.write_str("failed to initialize WebP encoder configuration")
418            }
419            Self::InvalidQuality { value } => {
420                write!(f, "quality {value} is outside libwebp's 0.0..=100.0 range")
421            }
422            Self::InvalidMethod { value } => {
423                write!(f, "method {value} is outside libwebp's 0..=6 range")
424            }
425            Self::InvalidAlphaQuality { value } => {
426                write!(f, "alpha quality {value} is outside libwebp's 0..=100 range")
427            }
428            Self::InvalidPreprocessing { value } => {
429                write!(f, "preprocessing mode {value} is outside libwebp's 0..=2 range")
430            }
431            Self::InvalidFilterStrength { value } => {
432                write!(f, "filter strength {value} is outside libwebp's 0..=100 range")
433            }
434            Self::InvalidFilterSharpness { value } => {
435                write!(f, "filter sharpness {value} is outside libwebp's 0..=7 range")
436            }
437            Self::InvalidFilterType { value } => {
438                write!(f, "filter type {value} is outside libwebp's 0..=1 range")
439            }
440            Self::IncompleteKeyframeInterval { kmin, kmax } => write!(
441                f,
442                "key-frame interval requires both kmin and kmax (got kmin={kmin:?}, kmax={kmax:?})"
443            ),
444            Self::InvalidKeyframeInterval { kmin, kmax } => write!(
445                f,
446                "key-frame interval requires 0 <= kmin < kmax (got kmin={kmin}, kmax={kmax})"
447            ),
448            Self::NonCanonicalKeyframeMode { kmin, kmax } => write!(
449                f,
450                "key-frame special modes must use (kmin, kmax) = (0, 0) to disable insertion or (0, 1) for every frame (got {kmin}, {kmax})"
451            ),
452            Self::KeyframeIntervalBelowMinimum {
453                kmin,
454                minimum,
455                kmax,
456            } => write!(
457                f,
458                "key-frame interval requires kmin >= kmax / 2 + 1; got kmin={kmin}, kmax={kmax}, minimum={minimum}"
459            ),
460            Self::KeyframeIntervalTooWide {
461                kmin,
462                kmax,
463                maximum_span,
464            } => write!(
465                f,
466                "key-frame interval span kmax - kmin must not exceed {maximum_span}; got kmin={kmin}, kmax={kmax}"
467            ),
468            Self::KeyframeIntervalIgnoredByMinimizeSize => f.write_str(
469                "key-frame interval cannot be set when minimize_size is enabled because libwebp disables key-frame insertion",
470            ),
471            Self::LibwebpConfigRejected => {
472                f.write_str("libwebp rejected the validated encoder configuration")
473            }
474            Self::EncoderOptionsInitialization => {
475                f.write_str("failed to initialize WebP animation encoder options")
476            }
477            Self::EncoderCreation => f.write_str("failed to create WebP animation encoder"),
478            Self::NoFrames => f.write_str("an animated WebP requires at least one frame"),
479            Self::UnexpectedFrameCanvas { actual, expected } => write!(
480                f,
481                "frame canvas {}x{} does not match encoder canvas {}x{}",
482                actual.width, actual.height, expected.width, expected.height
483            ),
484            Self::InvalidFrameBufferLength { actual, expected } => {
485                write!(f, "frame buffer is {actual} bytes; expected {expected}")
486            }
487            Self::NonMillisecondDuration(duration) => write!(
488                f,
489                "frame duration {duration:?} is not an exact number of milliseconds"
490            ),
491            Self::TimestampOverflow => {
492                f.write_str("cumulative frame duration exceeds libwebp's timestamp range")
493            }
494            Self::FrameCountOverflow => f.write_str("animation frame count overflows u32"),
495            Self::PictureInitialization => f.write_str("failed to initialize a WebP frame picture"),
496            Self::PictureImport => f.write_str("failed to import RGBA frame pixels into libwebp"),
497            Self::Libwebp(error) => write!(f, "libwebp animation encoding failed: {error}"),
498        }
499    }
500}
501
502impl Error for EncodeError {}
503
504fn validate_options(options: &AnimationEncoderOptions) -> Result<(), EncodeError> {
505    let config = options.config;
506    if let Some(value) = config.quality {
507        if !value.is_finite() || !(0.0..=100.0).contains(&value) {
508            return Err(EncodeError::InvalidQuality { value });
509        }
510    }
511    if let Some(value) = config.method {
512        if value > 6 {
513            return Err(EncodeError::InvalidMethod { value });
514        }
515    }
516    if let Some(value) = config.alpha_quality {
517        if value > 100 {
518            return Err(EncodeError::InvalidAlphaQuality { value });
519        }
520    }
521    if let Some(value) = config.preprocessing {
522        if value > 2 {
523            return Err(EncodeError::InvalidPreprocessing { value });
524        }
525    }
526    if let Some(value) = config.filter_strength {
527        if !(0..=100).contains(&value) {
528            return Err(EncodeError::InvalidFilterStrength { value });
529        }
530    }
531    if let Some(value) = config.filter_sharpness {
532        if !(0..=7).contains(&value) {
533            return Err(EncodeError::InvalidFilterSharpness { value });
534        }
535    }
536    if let Some(value) = config.filter_type {
537        if !(0..=1).contains(&value) {
538            return Err(EncodeError::InvalidFilterType { value });
539        }
540    }
541
542    let animation = options.animation;
543    match (animation.kmin, animation.kmax) {
544        (None, None) => Ok(()),
545        (Some(_), Some(_)) if animation.minimize_size == Some(true) => {
546            Err(EncodeError::KeyframeIntervalIgnoredByMinimizeSize)
547        }
548        (Some(0), Some(0) | Some(1)) => Ok(()),
549        (Some(kmin), Some(kmax)) if kmax <= 1 => {
550            Err(EncodeError::NonCanonicalKeyframeMode { kmin, kmax })
551        }
552        (Some(kmin), Some(kmax)) if kmin < 0 || kmin >= kmax => {
553            Err(EncodeError::InvalidKeyframeInterval { kmin, kmax })
554        }
555        (Some(kmin), Some(kmax)) => {
556            let minimum = kmax / 2 + 1;
557            if kmin < minimum {
558                Err(EncodeError::KeyframeIntervalBelowMinimum {
559                    kmin,
560                    minimum,
561                    kmax,
562                })
563            } else if kmax - kmin > 30 {
564                Err(EncodeError::KeyframeIntervalTooWide {
565                    kmin,
566                    kmax,
567                    maximum_span: 30,
568                })
569            } else {
570                Ok(())
571            }
572        }
573        (kmin, kmax) => Err(EncodeError::IncompleteKeyframeInterval { kmin, kmax }),
574    }
575}
576
577fn apply_config_overrides(config: &mut WebPConfig, overrides: EncoderConfigOverrides) {
578    if let Some(value) = overrides.quality {
579        config.quality = value;
580    }
581    if let Some(value) = overrides.lossless {
582        config.lossless = i32::from(value);
583    }
584    if let Some(value) = overrides.method {
585        config.method = i32::from(value);
586    }
587    if let Some(value) = overrides.use_sharp_yuv {
588        config.use_sharp_yuv = i32::from(value);
589    }
590    if let Some(value) = overrides.autofilter {
591        config.autofilter = i32::from(value);
592    }
593    if let Some(value) = overrides.alpha_quality {
594        config.alpha_quality = i32::from(value);
595    }
596    if let Some(value) = overrides.preprocessing {
597        config.preprocessing = i32::from(value);
598    }
599    if let Some(value) = overrides.thread_level {
600        config.thread_level = i32::from(value);
601    }
602    if let Some(value) = overrides.filter_strength {
603        config.filter_strength = value;
604    }
605    if let Some(value) = overrides.filter_sharpness {
606        config.filter_sharpness = value;
607    }
608    if let Some(value) = overrides.filter_type {
609        config.filter_type = value;
610    }
611}
612
613fn apply_mux_overrides(options: &mut WebPAnimEncoderOptions, overrides: AnimationMuxOverrides) {
614    if let Some(value) = overrides.minimize_size {
615        options.minimize_size = i32::from(value);
616    }
617    if let Some(value) = overrides.allow_mixed {
618        options.allow_mixed = i32::from(value);
619    }
620    if let Some(value) = overrides.kmin {
621        options.kmin = value;
622    }
623    if let Some(value) = overrides.kmax {
624        options.kmax = value;
625    }
626}
627
628struct Picture(WebPPicture);
629
630impl Picture {
631    fn from_rgba(canvas: CanvasSize, rgba: &[u8]) -> Result<Self, EncodeError> {
632        let width =
633            i32::try_from(canvas.width).map_err(|_| EncodeError::InvalidCanvasSize(canvas))?;
634        let height =
635            i32::try_from(canvas.height).map_err(|_| EncodeError::InvalidCanvasSize(canvas))?;
636        let stride = width
637            .checked_mul(4)
638            .ok_or(EncodeError::InvalidCanvasSize(canvas))?;
639        let mut picture = WebPPicture::new().map_err(|_| EncodeError::PictureInitialization)?;
640        picture.use_argb = 1;
641        picture.width = width;
642        picture.height = height;
643        // SAFETY: `rgba` has the validated full-canvas length, and libwebp copies it before returning.
644        if unsafe { WebPPictureImportRGBA(&mut picture, rgba.as_ptr(), stride) } == 0 {
645            // SAFETY: libwebp may have allocated picture data before reporting failure.
646            unsafe { WebPPictureFree(&mut picture) };
647            return Err(EncodeError::PictureImport);
648        }
649        Ok(Self(picture))
650    }
651}
652
653impl Drop for Picture {
654    fn drop(&mut self) {
655        // SAFETY: libwebp initialized this picture; freeing is idempotent for its allocated members.
656        unsafe { WebPPictureFree(&mut self.0) };
657    }
658}
659
660fn duration_to_millis(duration: Duration) -> Result<i32, EncodeError> {
661    let milliseconds = duration.as_millis();
662    if Duration::from_millis(u64::try_from(milliseconds).unwrap_or(u64::MAX)) != duration {
663        return Err(EncodeError::NonMillisecondDuration(duration));
664    }
665    i32::try_from(milliseconds).map_err(|_| EncodeError::TimestampOverflow)
666}
667
668fn encoder_error(encoder: *mut WebPAnimEncoder) -> String {
669    // SAFETY: the error pointer, if non-null, is owned by the live encoder.
670    let error = unsafe { WebPAnimEncoderGetError(encoder) };
671    if error.is_null() {
672        "unknown error".to_owned()
673    } else {
674        // SAFETY: libwebp returns a NUL-terminated error message for this encoder.
675        unsafe { CStr::from_ptr(error) }
676            .to_string_lossy()
677            .into_owned()
678    }
679}