use crate::config::{EncoderConfig, Preset};
use crate::error::{Error, Result};
use crate::types::{ColorMode, EncodePixel, PixelLayout};
use alloc::vec::Vec;
use core::ptr;
use whereat::*;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Frame {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
pub timestamp_ms: i32,
pub duration_ms: u32,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AnimationInfo {
pub width: u32,
pub height: u32,
pub frame_count: u32,
pub loop_count: u32,
pub bgcolor: u32,
}
pub struct AnimationDecoder {
decoder: *mut libwebp_sys::WebPAnimDecoder,
info: AnimationInfo,
bpp: usize,
limits: crate::Limits,
_data: Vec<u8>, }
unsafe impl Send for AnimationDecoder {}
impl AnimationDecoder {
pub fn new(data: &[u8]) -> Result<Self> {
Self::with_options(data, ColorMode::Rgba, true)
}
pub fn get_limits(&self) -> &crate::Limits {
&self.limits
}
pub fn with_options(data: &[u8], color_mode: ColorMode, use_threads: bool) -> Result<Self> {
Self::with_options_limits(data, color_mode, use_threads, &crate::Limits::none())
}
pub fn with_options_limits(
data: &[u8],
color_mode: ColorMode,
use_threads: bool,
limits: &crate::Limits,
) -> Result<Self> {
limits
.check_input_size(data.len() as u64)
.map_err(|e| at!(Error::LimitExceeded(e)))?;
let (csp_mode, bpp) = match color_mode {
ColorMode::Rgba => (libwebp_sys::WEBP_CSP_MODE::MODE_RGBA, 4),
ColorMode::Bgra => (libwebp_sys::WEBP_CSP_MODE::MODE_BGRA, 4),
_ => {
return Err(at!(Error::InvalidInput(
"animation decoder only supports ColorMode::Rgba or ColorMode::Bgra".into(),
)));
}
};
let data_copy = data.to_vec();
let mut options = core::mem::MaybeUninit::<libwebp_sys::WebPAnimDecoderOptions>::uninit();
let ok = unsafe { libwebp_sys::WebPAnimDecoderOptionsInit(options.as_mut_ptr()) };
if ok == 0 {
return Err(at!(Error::InvalidConfig(
"failed to init decoder options".into(),
)));
}
let mut options = unsafe { options.assume_init() };
options.color_mode = csp_mode;
options.use_threads = use_threads as i32;
let webp_data = libwebp_sys::WebPData {
bytes: data_copy.as_ptr(),
size: data_copy.len(),
};
let decoder = unsafe { libwebp_sys::WebPAnimDecoderNew(&webp_data, &options) };
if decoder.is_null() {
return Err(at!(Error::InvalidWebP));
}
let mut anim_info = libwebp_sys::WebPAnimInfo::default();
let ok = unsafe { libwebp_sys::WebPAnimDecoderGetInfo(decoder, &mut anim_info) };
if ok == 0 {
unsafe { libwebp_sys::WebPAnimDecoderDelete(decoder) };
return Err(at!(Error::InvalidWebP));
}
if let Err(e) = limits.check_animation(
anim_info.canvas_width,
anim_info.canvas_height,
anim_info.frame_count,
) {
unsafe { libwebp_sys::WebPAnimDecoderDelete(decoder) };
return Err(at!(Error::LimitExceeded(e)));
}
Ok(Self {
decoder,
info: AnimationInfo {
width: anim_info.canvas_width,
height: anim_info.canvas_height,
frame_count: anim_info.frame_count,
loop_count: anim_info.loop_count,
bgcolor: anim_info.bgcolor,
},
bpp,
limits: *limits,
_data: data_copy,
})
}
pub fn info(&self) -> &AnimationInfo {
&self.info
}
pub fn has_more_frames(&self) -> bool {
unsafe { libwebp_sys::WebPAnimDecoderHasMoreFrames(self.decoder) != 0 }
}
pub fn next_frame(&mut self) -> Result<Option<Frame>> {
if !self.has_more_frames() {
return Ok(None);
}
let mut buf: *mut u8 = ptr::null_mut();
let mut timestamp: i32 = 0;
let ok =
unsafe { libwebp_sys::WebPAnimDecoderGetNext(self.decoder, &mut buf, &mut timestamp) };
if ok == 0 {
return Err(at!(Error::DecodeFailed(
crate::error::DecodingError::BitstreamError,
)));
}
let size = (self.info.width as usize)
.saturating_mul(self.info.height as usize)
.saturating_mul(self.bpp);
let data = unsafe { core::slice::from_raw_parts(buf, size).to_vec() };
let duration_ms = 0;
Ok(Some(Frame {
data,
width: self.info.width,
height: self.info.height,
timestamp_ms: timestamp,
duration_ms,
}))
}
pub fn reset(&mut self) {
unsafe {
libwebp_sys::WebPAnimDecoderReset(self.decoder);
}
}
pub fn decode_all(&mut self) -> Result<Vec<Frame>> {
self.reset();
const MAX_FRAME_HINT: u32 = 4096;
let cap = (self.info.frame_count as usize).min(MAX_FRAME_HINT as usize);
let mut frames = Vec::with_capacity(cap);
let mut prev_timestamp = 0i32;
while let Some(mut frame) = self.next_frame()? {
frame.duration_ms = frame.timestamp_ms.saturating_sub(prev_timestamp).max(0) as u32;
prev_timestamp = frame.timestamp_ms;
self.limits
.check_animation_ms(frame.timestamp_ms.max(0) as u64)
.map_err(|e| at!(Error::LimitExceeded(e)))?;
frames.push(frame);
}
let len = frames.len();
if len > 0 {
let prev_duration = if len > 1 {
frames[len - 2].duration_ms
} else {
100 };
frames[len - 1].duration_ms = prev_duration;
}
Ok(frames)
}
}
impl Drop for AnimationDecoder {
fn drop(&mut self) {
if !self.decoder.is_null() {
unsafe {
libwebp_sys::WebPAnimDecoderDelete(self.decoder);
}
}
}
}
pub struct AnimationEncoder {
encoder: *mut libwebp_sys::WebPAnimEncoder,
width: u32,
height: u32,
config: EncoderConfig,
#[cfg(feature = "icc")]
icc_profile: Option<Vec<u8>>,
}
unsafe impl Send for AnimationEncoder {}
impl AnimationEncoder {
pub fn new(width: u32, height: u32) -> Result<Self> {
Self::with_options(width, height, true, 0)
}
pub fn with_options(
width: u32,
height: u32,
allow_mixed: bool,
loop_count: u32,
) -> Result<Self> {
if width == 0 || height == 0 || width > 16383 || height > 16383 {
return Err(at!(Error::InvalidInput("invalid dimensions".into())));
}
let mut options = core::mem::MaybeUninit::<libwebp_sys::WebPAnimEncoderOptions>::uninit();
let ok = unsafe {
libwebp_sys::WebPAnimEncoderOptionsInitInternal(
options.as_mut_ptr(),
libwebp_sys::WEBP_MUX_ABI_VERSION as i32,
)
};
if ok == 0 {
return Err(at!(Error::InvalidConfig(
"failed to init encoder options".into(),
)));
}
let mut options = unsafe { options.assume_init() };
options.allow_mixed = allow_mixed as i32;
options.anim_params.loop_count = loop_count as i32;
let encoder = unsafe {
libwebp_sys::WebPAnimEncoderNewInternal(
width as i32,
height as i32,
&options,
libwebp_sys::WEBP_MUX_ABI_VERSION as i32,
)
};
if encoder.is_null() {
return Err(at!(Error::OutOfMemory));
}
Ok(Self {
encoder,
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
})
}
pub fn set_quality(&mut self, quality: f32) {
self.config.quality = quality;
}
pub fn set_preset(&mut self, preset: Preset) {
self.config.preset = preset;
}
pub fn set_lossless(&mut self, lossless: bool) {
self.config.lossless = lossless;
}
#[cfg(feature = "icc")]
pub fn set_icc_profile(&mut self, profile: Vec<u8>) {
self.icc_profile = Some(profile);
}
pub fn add_frame<P: EncodePixel>(&mut self, pixels: &[P], timestamp_ms: i32) -> Result<()> {
let bpp = P::LAYOUT.bytes_per_pixel();
let data = unsafe {
core::slice::from_raw_parts(
pixels.as_ptr() as *const u8,
pixels.len().saturating_mul(bpp),
)
};
self.add_frame_internal(data, timestamp_ms, P::LAYOUT)
}
pub fn add_frame_rgba(&mut self, data: &[u8], timestamp_ms: i32) -> Result<()> {
self.add_frame_internal(data, timestamp_ms, PixelLayout::Rgba)
}
pub fn add_frame_rgb(&mut self, data: &[u8], timestamp_ms: i32) -> Result<()> {
self.add_frame_internal(data, timestamp_ms, PixelLayout::Rgb)
}
pub fn add_frame_bgra(&mut self, data: &[u8], timestamp_ms: i32) -> Result<()> {
self.add_frame_internal(data, timestamp_ms, PixelLayout::Bgra)
}
pub fn add_frame_bgr(&mut self, data: &[u8], timestamp_ms: i32) -> Result<()> {
self.add_frame_internal(data, timestamp_ms, PixelLayout::Bgr)
}
fn add_frame_internal(
&mut self,
data: &[u8],
timestamp_ms: i32,
layout: PixelLayout,
) -> Result<()> {
let bpp = layout.bytes_per_pixel();
let expected = (self.width as usize)
.saturating_mul(self.height as usize)
.saturating_mul(bpp);
if data.len() < expected {
return Err(at!(Error::InvalidInput("buffer too small".into())));
}
let webp_config = self.config.to_libwebp()?;
let mut picture = libwebp_sys::WebPPicture::new()
.map_err(|_| at!(Error::InvalidConfig("failed to init picture".into())))?;
picture.width = self.width as i32;
picture.height = self.height as i32;
picture.use_argb = 1;
let stride = (self.width as usize * bpp) as i32;
let import_ok = unsafe {
match layout {
PixelLayout::Rgba => {
libwebp_sys::WebPPictureImportRGBA(&mut picture, data.as_ptr(), stride)
}
PixelLayout::Rgb => {
libwebp_sys::WebPPictureImportRGB(&mut picture, data.as_ptr(), stride)
}
PixelLayout::Bgra => {
libwebp_sys::WebPPictureImportBGRA(&mut picture, data.as_ptr(), stride)
}
PixelLayout::Bgr => {
libwebp_sys::WebPPictureImportBGR(&mut picture, data.as_ptr(), stride)
}
}
};
if import_ok == 0 {
unsafe { libwebp_sys::WebPPictureFree(&mut picture) };
return Err(at!(Error::OutOfMemory));
}
let ok = unsafe {
libwebp_sys::WebPAnimEncoderAdd(self.encoder, &mut picture, timestamp_ms, &webp_config)
};
unsafe { libwebp_sys::WebPPictureFree(&mut picture) };
if ok == 0 {
let error_msg = unsafe {
let ptr = libwebp_sys::WebPAnimEncoderGetError(self.encoder);
if ptr.is_null() {
"unknown error"
} else {
core::ffi::CStr::from_ptr(ptr)
.to_str()
.unwrap_or("unknown error")
}
};
return Err(at!(Error::AnimationError(error_msg.into())));
}
Ok(())
}
pub fn finish(self, end_timestamp_ms: i32) -> Result<Vec<u8>> {
let ok = unsafe {
libwebp_sys::WebPAnimEncoderAdd(
self.encoder,
ptr::null_mut(),
end_timestamp_ms,
ptr::null(),
)
};
if ok == 0 {
return Err(at!(Error::AnimationError(
"failed to finalize animation".into()
)));
}
let mut webp_data = libwebp_sys::WebPData::default();
let ok = unsafe { libwebp_sys::WebPAnimEncoderAssemble(self.encoder, &mut webp_data) };
if ok == 0 {
return Err(at!(Error::AnimationError(
"failed to assemble animation".into()
)));
}
let result = unsafe {
if webp_data.bytes.is_null() || webp_data.size == 0 {
return Err(at!(Error::AnimationError("empty output".into())));
}
let slice = core::slice::from_raw_parts(webp_data.bytes, webp_data.size);
let vec = slice.to_vec();
libwebp_sys::WebPDataClear(&mut webp_data);
vec
};
#[cfg(feature = "icc")]
if let Some(ref icc) = self.icc_profile {
return crate::mux::embed_icc(&result, icc);
}
Ok(result)
}
}
impl Drop for AnimationEncoder {
fn drop(&mut self) {
if !self.encoder.is_null() {
unsafe {
libwebp_sys::WebPAnimEncoderDelete(self.encoder);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_animation_encoder_creation() {
let encoder = AnimationEncoder::new(100, 100);
assert!(encoder.is_ok());
}
#[test]
fn test_animation_encoder_invalid_dimensions() {
assert!(AnimationEncoder::new(0, 100).is_err());
assert!(AnimationEncoder::new(100, 0).is_err());
assert!(AnimationEncoder::new(20000, 100).is_err());
}
}