Skip to main content

maple_render_core/
webp_anim.rs

1//! Animated WebP output.
2//!
3//! Unlike GIF, animated WebP supports full 24-bit color with alpha per frame,
4//! so it does **not** need color quantization and exhibits no gradient banding.
5//!
6//! Encoding is delegated to [`libwebp`](https://developers.google.com/speed/webp)
7//! via the [`webp-animation`] crate (statically linked when the `static` feature
8//! is enabled, which is the default on native targets).
9
10#[cfg(not(target_arch = "wasm32"))]
11use std::{fs::File, io::Write, path::Path};
12
13#[cfg(not(target_arch = "wasm32"))]
14use webp_animation::Encoder as WebPEncoder;
15
16#[cfg(not(target_arch = "wasm32"))]
17use crate::{
18    error::{Error, Result},
19    renders::Renders,
20};
21
22/// Quality (0..=100) used for lossy WebP encoding.
23pub const DEFAULT_WEBP_QUALITY: f32 = 95.0;
24
25/// Lossy method (0=fastest … 6=slowest-best). libwebp default is 4.
26pub const DEFAULT_WEBP_METHOD: usize = 4;
27
28#[cfg(not(target_arch = "wasm32"))]
29pub struct WebpOptions {
30    /// 0..=100. 100 = best quality (largest).
31    pub quality: f32,
32    /// Lossless encoding when true (quality acts as compression effort 0..=100).
33    pub lossless: bool,
34    /// Method / speed: 0 = fastest (larger/slightly lower quality), 6 = slowest best.
35    pub method: usize,
36}
37
38impl Default for WebpOptions {
39    fn default() -> Self {
40        WebpOptions { quality: DEFAULT_WEBP_QUALITY, lossless: false, method: DEFAULT_WEBP_METHOD }
41    }
42}
43
44#[cfg(not(target_arch = "wasm32"))]
45pub struct WebpAnim {
46    renders: Renders,
47    period: f64,
48    hold: f64,
49    first_frame: i32,
50    options: WebpOptions,
51}
52
53#[cfg(not(target_arch = "wasm32"))]
54impl WebpAnim {
55    pub fn new(renders: Renders) -> Self {
56        WebpAnim {
57            renders,
58            period: 0.1,
59            hold: 5.0,
60            first_frame: -1,
61            options: WebpOptions::default(),
62        }
63    }
64
65    pub fn set_first_frame(&mut self, index: i32) {
66        self.first_frame = index;
67    }
68
69    pub fn set_timing(&mut self, period: f64, hold: f64) {
70        self.period = period;
71        self.hold = hold;
72    }
73
74    pub fn set_options(&mut self, options: WebpOptions) {
75        self.options = WebpOptions {
76            quality: options.quality.clamp(0.0, 100.0),
77            lossless: options.lossless,
78            method: options.method.min(6),
79        };
80    }
81
82    /// Encode a single RGBA frame to a WebP byte buffer (still image, not animation).
83    ///
84    /// This is a convenience for `--webp_single`. It is only available on native
85    /// targets (libwebp is not built for wasm).
86    pub fn encode_single(img: &image::RgbaImage, options: &WebpOptions) -> Result<Vec<u8>> {
87        let (width, height) = (img.width(), img.height());
88        let rgba = img.as_raw().to_vec();
89
90        let enc_options = if options.lossless {
91            webp_animation::EncoderOptions {
92                encoding_config: Some(webp_animation::EncodingConfig {
93                    encoding_type: webp_animation::EncodingType::Lossless,
94                    quality: options.quality,
95                    method: options.method,
96                    ..Default::default()
97                }),
98                color_mode: webp_animation::ColorMode::Rgba,
99                ..Default::default()
100            }
101        } else {
102            let mut cfg = webp_animation::EncodingConfig::new_lossy(options.quality);
103            cfg.method = options.method;
104            webp_animation::EncoderOptions {
105                encoding_config: Some(cfg),
106                color_mode: webp_animation::ColorMode::Rgba,
107                ..Default::default()
108            }
109        };
110
111        let mut enc = WebPEncoder::new_with_options((width, height), enc_options)
112            .map_err(|e| Error::VideoEncode(format!("WebP encoder init: {}", e)))?;
113        enc.add_frame(&rgba, 0)
114            .map_err(|e| Error::VideoEncode(format!("WebP add_frame: {}", e)))?;
115        let data =
116            enc.finalize(1).map_err(|e| Error::VideoEncode(format!("WebP finalize: {}", e)))?;
117        Ok(data.as_ref().to_vec())
118    }
119
120    /// Encode all frames to a WebP byte buffer.
121    pub fn encode(&mut self) -> Result<Vec<u8>> {
122        let frames = self.renders.length() as i32;
123        if frames == 0 {
124            return Err(Error::VideoEncode("No frames to encode".to_string()));
125        }
126
127        // Dimensions come from the first render.
128        let first = self.renders.get_render(0)?;
129        let (width, height) = (first.get().width(), first.get().height());
130
131        let encoding_config = if self.options.lossless {
132            webp_animation::EncodingConfig {
133                encoding_type: webp_animation::EncodingType::Lossless,
134                quality: self.options.quality,
135                method: self.options.method,
136                ..Default::default()
137            }
138        } else {
139            let mut cfg = webp_animation::EncodingConfig::new_lossy(self.options.quality);
140            cfg.method = self.options.method;
141            cfg
142        };
143
144        let enc_options = webp_animation::EncoderOptions {
145            encoding_config: Some(encoding_config),
146            color_mode: webp_animation::ColorMode::Rgba,
147            kmin: 3,
148            kmax: 5,
149            ..Default::default()
150        };
151
152        let mut enc = WebPEncoder::new_with_options((width, height), enc_options)
153            .map_err(|e| Error::VideoEncode(format!("WebP encoder init: {}", e)))?;
154
155        let mut curr_ms: f64 = 0.0;
156        let mut prev_ts: i32 = -1;
157
158        for base in 0..frames {
159            let i = if self.first_frame >= 0 { (base + self.first_frame) % frames } else { base };
160
161            let step = if i == frames - 1 { self.period + self.hold } else { self.period };
162
163            let render = self.renders.get_render(i)?;
164            let img = render.get();
165            // libwebp expects raw RGBA bytes (one plane, no stride padding).
166            let rgba: Vec<u8> = img.as_raw().to_vec();
167
168            let mut ts = (curr_ms).round() as i32;
169            if ts <= prev_ts {
170                ts = prev_ts + 1;
171            }
172            enc.add_frame(&rgba, ts)
173                .map_err(|e| Error::VideoEncode(format!("WebP add_frame: {}", e)))?;
174            prev_ts = ts;
175
176            curr_ms += step * 1000.0;
177            self.renders.remove_render(i);
178        }
179
180        // Finalize: timestamp marks when the final frame's display ends.
181        let mut final_ts = curr_ms.round() as i32;
182        if final_ts <= prev_ts {
183            final_ts = prev_ts + 1;
184        }
185
186        let webp_data = enc
187            .finalize(final_ts)
188            .map_err(|e| Error::VideoEncode(format!("WebP finalize: {}", e)))?;
189
190        Ok(webp_data.as_ref().to_vec())
191    }
192
193    /// Encode all frames and write the result to `path`.
194    pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
195        let data = self.encode()?;
196        let file = File::create(path.as_ref()).map_err(|e| Error::Io(e))?;
197        let mut writer = std::io::BufWriter::new(file);
198        writer.write_all(&data).map_err(|e| Error::Io(e))?;
199        Ok(())
200    }
201}