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#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct AnimationEncoderOptions {
15 pub loop_count: LoopCount,
17 pub background_color: BackgroundColor,
19 pub config: EncoderConfigOverrides,
22 pub animation: AnimationMuxOverrides,
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
32pub struct EncoderConfigOverrides {
33 pub quality: Option<f32>,
35 pub lossless: Option<bool>,
37 pub method: Option<u8>,
39 pub use_sharp_yuv: Option<bool>,
41 pub autofilter: Option<bool>,
43 pub alpha_quality: Option<u8>,
45 pub preprocessing: Option<u8>,
48 pub thread_level: Option<bool>,
50 pub filter_strength: Option<i32>,
52 pub filter_sharpness: Option<i32>,
54 pub filter_type: Option<i32>,
56}
57
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66pub struct AnimationMuxOverrides {
67 pub minimize_size: Option<bool>,
69 pub allow_mixed: Option<bool>,
71 pub kmin: Option<i32>,
73 pub kmax: Option<i32>,
75}
76
77impl AnimationEncoderOptions {
78 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 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 Self::new(LoopCount::Infinite, BackgroundColor { raw: 0xffff_ffff })
117 }
118}
119
120pub 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 unsafe {
138 if !self.encoder.is_null() {
139 WebPAnimEncoderDelete(self.encoder);
140 }
141 }
142 }
143}
144
145impl AnimationEncoder {
146 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 if unsafe { WebPValidateConfig(&config) } == 0 {
165 return Err(EncodeError::LibwebpConfigRejected);
166 }
167
168 let mux_abi = WebPGetMuxABIVersion();
169 let mut encoder_options: WebPAnimEncoderOptions = unsafe { std::mem::zeroed() };
171 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 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 pub const fn canvas(&self) -> CanvasSize {
200 self.canvas
201 }
202
203 pub const fn frame_count(&self) -> u32 {
205 self.frame_count
206 }
207
208 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 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 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 pub fn finish(self) -> Result<Vec<u8>, EncodeError> {
261 if self.frame_count == 0 {
262 return Err(EncodeError::NoFrames);
263 }
264 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 if unsafe { WebPAnimEncoderAssemble(self.encoder, &mut encoded) } == 0 {
279 return Err(EncodeError::Libwebp(encoder_error(self.encoder)));
280 }
281 let output = unsafe { std::slice::from_raw_parts(encoded.bytes, encoded.size) }.to_vec();
283 unsafe { WebPDataClear(&mut encoded) };
285 Ok(output)
286 }
287}
288
289#[derive(Clone, Debug, PartialEq)]
291pub enum EncodeError {
292 InvalidCanvasSize(CanvasSize),
294 ConfigInitialization,
296 InvalidQuality {
298 value: f32,
300 },
301 InvalidMethod {
303 value: u8,
305 },
306 InvalidAlphaQuality {
308 value: u8,
310 },
311 InvalidPreprocessing {
313 value: u8,
315 },
316 InvalidFilterStrength {
318 value: i32,
320 },
321 InvalidFilterSharpness {
323 value: i32,
325 },
326 InvalidFilterType {
328 value: i32,
330 },
331 IncompleteKeyframeInterval {
333 kmin: Option<i32>,
335 kmax: Option<i32>,
337 },
338 InvalidKeyframeInterval {
340 kmin: i32,
342 kmax: i32,
344 },
345 NonCanonicalKeyframeMode {
347 kmin: i32,
349 kmax: i32,
351 },
352 KeyframeIntervalBelowMinimum {
354 kmin: i32,
356 minimum: i32,
358 kmax: i32,
360 },
361 KeyframeIntervalTooWide {
363 kmin: i32,
365 kmax: i32,
367 maximum_span: i32,
369 },
370 KeyframeIntervalIgnoredByMinimizeSize,
372 LibwebpConfigRejected,
374 EncoderOptionsInitialization,
376 EncoderCreation,
378 NoFrames,
380 UnexpectedFrameCanvas {
382 actual: CanvasSize,
384 expected: CanvasSize,
386 },
387 InvalidFrameBufferLength {
389 actual: usize,
391 expected: usize,
393 },
394 NonMillisecondDuration(Duration),
396 TimestampOverflow,
398 FrameCountOverflow,
400 PictureInitialization,
402 PictureImport,
404 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 if unsafe { WebPPictureImportRGBA(&mut picture, rgba.as_ptr(), stride) } == 0 {
645 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 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 let error = unsafe { WebPAnimEncoderGetError(encoder) };
671 if error.is_null() {
672 "unknown error".to_owned()
673 } else {
674 unsafe { CStr::from_ptr(error) }
676 .to_string_lossy()
677 .into_owned()
678 }
679}