Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,22 @@ A high-level Rust wrapper for decoding and encoding WebP animations
"""
keywords = ["webp", "webp-animation", "decoder", "encoder"]
categories = ["multimedia::images", "multimedia", "api-bindings"]
edition = "2018"
rust-version = "1.47"
edition = "2024"
rust-version = "1.93"

[dependencies]
image = { version = "0.24.1", default_features = false, optional = true }
image = { version = "0.25.9", default-features = false, optional = true }
log = "0.4.14"

[dependencies.libwebp-sys2]
version = "0.1.9"
version = "0.2.0"
features = ["0_5", "0_6", "1_2", "demux", "mux"]

[dev-dependencies]
image = { version = "0.24.1", default_features = false, features = ["png"] }
imageproc = "0.23.0"
env_logger = "0.10.0"
image = { version = "0.25.9", default-features = false, features = ["png"] }
imageproc = "0.26.0"
env_logger = "0.11.8"

[features]
static = ["libwebp-sys2/static"]
image = ["dep:image"]
20 changes: 11 additions & 9 deletions examples/decode_animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ fn main() {
let decoder = Decoder::new(&buffer).unwrap();

for frame in decoder.into_iter() {
assert_eq!(frame.dimensions(), (400, 400));
assert_eq!(frame.data().len(), 400 * 400 * 4); // w * h * rgba
#[cfg(feature = "image")]
let (dimensions, data_len) = {
let dims = frame.dimensions();
let len = frame.data().len();
let image = frame.into_image().unwrap();
assert_eq!(image.dimensions(), (400, 400));
(dims, len)
};

#[cfg(features = "image")]
assert_eq!(frame.into_image().unwrap().dimensions(), (400, 400));
#[cfg(not(feature = "image"))]
let (dimensions, data_len) = { (frame.dimensions(), frame.data().len()) };

info!(
"Frame, dimensions={:?}, data_len={}",
frame.dimensions(),
frame.data().len()
);
info!("Frame, dimensions={:?}, data_len={}", dimensions, data_len);
}
}
4 changes: 2 additions & 2 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
name = "fuzz"
version = "0.1.0"
authors = ["Mika Vatanen <blaind@blaind.net>"]
edition = "2018"
edition = "2024"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.3.0"
libfuzzer-sys = "0.4.12"
webp-animation = { path = ".." }

[[bin]]
Expand Down
26 changes: 20 additions & 6 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,16 @@ impl<'a> Debug for Decoder<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let info = &self.info;

write!(f, "Decoder {{ buffer: {}b, info: {{ w: {}, h: {}, loop_cnt: {}, bgcolor: 0x{:x}, frame_count: {} }} }}", self.buffer.len(), info.canvas_width, info.canvas_height, info.loop_count, info.bgcolor, info.frame_count)
write!(
f,
"Decoder {{ buffer: {}b, info: {{ w: {}, h: {}, loop_cnt: {}, bgcolor: 0x{:x}, frame_count: {} }} }}",
self.buffer.len(),
info.canvas_width,
info.canvas_height,
info.loop_count,
info.bgcolor,
info.frame_count
)
}
}

Expand Down Expand Up @@ -250,12 +259,16 @@ impl<'a> Iterator for DecoderIterator<'a> {
} != 1
{
// "False if any of the arguments are NULL, or if there is a parsing or decoding error, or if there are no more frames. Otherwise, returns true."
log::warn!("webp::WebPAnimDecoderGetNext did not return success - frame parsing failed, parsing/decoding error?");
log::warn!(
"webp::WebPAnimDecoderGetNext did not return success - frame parsing failed, parsing/decoding error?"
);
return None;
}

if output_buffer.is_null() {
log::error!("webp::WebPAnimDecoderGetNext returned null output ptr, can not decode a frame. This should not happen");
log::error!(
"webp::WebPAnimDecoderGetNext returned null output ptr, can not decode a frame. This should not happen"
);
return None;
}

Expand Down Expand Up @@ -321,7 +334,7 @@ mod tests {
fn test_decode_to_image() {
use std::io::Cursor;

use image::{codecs::png::PngDecoder, DynamicImage, ImageDecoder as _, ImageOutputFormat};
use image::{DynamicImage, ImageDecoder as _, ImageFormat, codecs::png::PngDecoder};

let buffer = get_animated_buffer();
let decoder = Decoder::new(&buffer).unwrap();
Expand All @@ -332,12 +345,13 @@ mod tests {

let mut buf = Cursor::new(Vec::new());
DynamicImage::ImageRgba8(image)
.write_to(&mut buf, ImageOutputFormat::Png)
.write_to(&mut buf, ImageFormat::Png)
.unwrap();

let buf = buf.into_inner();
let cursor = Cursor::new(&buf);

let png_decoder = PngDecoder::new(&buf[..]).unwrap();
let png_decoder = PngDecoder::new(cursor).unwrap();
assert_eq!(png_decoder.dimensions(), (400, 400));
}

Expand Down
59 changes: 33 additions & 26 deletions src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,29 +164,32 @@ impl Encoder {

self.frame.set_data(data, self.options.color_mode)?;

// keep config alive
let tmp_config;
let config_ptr = match config {
Some(cfg) => {
tmp_config = cfg.to_config_container()?;
&*tmp_config // Deref or explicit borrow
}
None => match &self.encoding_config {
Some(cfg) => &**cfg,
None => ptr::null(),
},
};

if unsafe {
webp::WebPAnimEncoderAdd(
self.encoder_wr.encoder,
self.frame.as_webp_picture_ref(),
timestamp,
match config {
Some(config) => {
let config = config.to_config_container()?;
config.as_ptr()
}
None => match &self.encoding_config {
Some(config) => config.as_ptr(),
None => std::ptr::null(),
},
},
config_ptr,
)
} == 0
{
return Err(Error::EncoderAddFailed);
}

self.previous_timestamp = timestamp;

log::trace!(
"Add a frame at timestamp {}ms, {} bytes",
timestamp,
Expand Down Expand Up @@ -474,16 +477,18 @@ mod tests {
#[test]
fn test_wrong_encoding_config() {
let mut encoder = Encoder::new((4, 4)).unwrap();
assert!(encoder
.add_frame_with_config(
&[0u8; 4 * 4 * 4],
0,
&EncodingConfig {
quality: 100.,
..Default::default()
},
)
.is_ok());
assert!(
encoder
.add_frame_with_config(
&[0u8; 4 * 4 * 4],
0,
&EncodingConfig {
quality: 100.,
..Default::default()
},
)
.is_ok()
);

assert_eq!(
encoder
Expand Down Expand Up @@ -529,11 +534,13 @@ mod tests {
Error::InvalidEncodingConfig
);

assert!(add_lossy_frame(LossyEncodingConfig {
filter_sharpness: 7,
..Default::default()
})
.is_ok());
assert!(
add_lossy_frame(LossyEncodingConfig {
filter_sharpness: 7,
..Default::default()
})
.is_ok()
);
}

fn add_lossy_frame(lossy_config: LossyEncodingConfig) -> Result<(), Error> {
Expand Down
22 changes: 14 additions & 8 deletions src/encoder_config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::mem;
use std::{mem, ops::Deref};

use crate::{ColorMode, Error};

Expand Down Expand Up @@ -314,9 +314,12 @@ pub(crate) struct ConfigContainer {
impl ConfigContainer {
pub fn new(config: &EncodingConfig) -> Result<Self, Error> {
let mut webp_config = unsafe {
let mut config = mem::zeroed();
webp::WebPConfigInit(&mut config);
config
let mut cfg = mem::zeroed();
let ok = webp::WebPConfigInit(&mut cfg);
if ok == 0 {
return Err(Error::InvalidEncodingConfig);
}
cfg
};

config.apply_to(&mut webp_config);
Expand All @@ -329,27 +332,30 @@ impl ConfigContainer {
config: webp_config,
})
}
}

impl Deref for ConfigContainer {
type Target = webp::WebPConfig;

pub fn as_ptr(&self) -> &webp::WebPConfig {
fn deref(&self) -> &Self::Target {
&self.config
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_config_defaults() {
let default_webp_config = unsafe {
let mut config = mem::zeroed();
webp::WebPConfigInit(&mut config);
assert_ne!(webp::WebPConfigInit(&mut config), 0);
config
};

let config = ConfigContainer::new(&EncodingConfig::default()).unwrap();

let left = config.as_ptr();
let left = &*config;
let def = &default_webp_config;

// custom-set
Expand Down
77 changes: 63 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,71 @@ pub enum Error {
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::OptionsInitFailed => write!(f, "OptionsInitFailed: Initializing webp options failed, internal (memory allocation?) failure"),
Error::DecodeFailed => write!(f, "DecodeFailed: Could not decode input bytes, possibly malformed data"),
Error::DecoderGetInfoFailed => write!(f, "DecoderGetInfoFailed: Decoder could not get metadata of webp stream. Corrupt data?"),
Error::TooLargeCanvas(width, height, max_size) => write!(f, "TooLargeCanvas: Decodable canvas is too large ({} x {} = {} pixels). For now, size is limited to 3840 * 2160 = {} pixels", width, height, width * height, max_size),
Error::EncoderCreateFailed => write!(f, "EncoderCreateFailed: Encoder create failed. Wrong options combination?"),
Error::BufferSizeFailed(expected, received) => write!(f, "BufferSizeFailed: Expected (width * height * 4 = {}) bytes as input buffer, got {} bytes", expected, received),
Error::PictureImportFailed => write!(f, "PictureImportFailed: Raw data could not be converted into webp frame by underlying libwebp library"),
Error::EncoderAddFailed => write!(f, "EncoderAddFailed: Frame could not be added to webp stream by underlying libwebp library"),
Error::WrongColorMode(requested, expected) => write!(f, "WrongColorMode: Requested image in {:?} format but underlying is stored as {:?}", expected, requested),
Error::TimestampMustBeHigherThanPrevious(requested, previous) => write!(f, "TimestampMustBeHigherThanPrevious: Supplied timestamp (got {}) must be higher than {}", requested, previous),
Error::TimestampMustBeEqualOrHigherThanPrevious(requested, previous) => write!(f, "TimestampMustBeEqualOrHigherThanPrevious: Supplied timestamp (got {}) must be higher or equal to {}", requested, previous),
Error::EncoderAssmebleFailed => write!(f, "EncoderAssmebleFailed: Encoder webp assembly failed"),
Error::DimensionsMustbePositive => write!(f, "DimensionsMustbePositive: Supplied dimensions must be positive"),
Error::OptionsInitFailed => write!(
f,
"OptionsInitFailed: Initializing webp options failed, internal (memory allocation?) failure"
),
Error::DecodeFailed => write!(
f,
"DecodeFailed: Could not decode input bytes, possibly malformed data"
),
Error::DecoderGetInfoFailed => write!(
f,
"DecoderGetInfoFailed: Decoder could not get metadata of webp stream. Corrupt data?"
),
Error::TooLargeCanvas(width, height, max_size) => write!(
f,
"TooLargeCanvas: Decodable canvas is too large ({} x {} = {} pixels). For now, size is limited to 3840 * 2160 = {} pixels",
width,
height,
width * height,
max_size
),
Error::EncoderCreateFailed => write!(
f,
"EncoderCreateFailed: Encoder create failed. Wrong options combination?"
),
Error::BufferSizeFailed(expected, received) => write!(
f,
"BufferSizeFailed: Expected (width * height * 4 = {}) bytes as input buffer, got {} bytes",
expected, received
),
Error::PictureImportFailed => write!(
f,
"PictureImportFailed: Raw data could not be converted into webp frame by underlying libwebp library"
),
Error::EncoderAddFailed => write!(
f,
"EncoderAddFailed: Frame could not be added to webp stream by underlying libwebp library"
),
Error::WrongColorMode(requested, expected) => write!(
f,
"WrongColorMode: Requested image in {:?} format but underlying is stored as {:?}",
expected, requested
),
Error::TimestampMustBeHigherThanPrevious(requested, previous) => write!(
f,
"TimestampMustBeHigherThanPrevious: Supplied timestamp (got {}) must be higher than {}",
requested, previous
),
Error::TimestampMustBeEqualOrHigherThanPrevious(requested, previous) => write!(
f,
"TimestampMustBeEqualOrHigherThanPrevious: Supplied timestamp (got {}) must be higher or equal to {}",
requested, previous
),
Error::EncoderAssmebleFailed => {
write!(f, "EncoderAssmebleFailed: Encoder webp assembly failed")
}
Error::DimensionsMustbePositive => write!(
f,
"DimensionsMustbePositive: Supplied dimensions must be positive"
),
Error::NoFramesAdded => write!(f, "NoFramesAdded: No frames have been added yet"),
Error::ZeroSizeBuffer => write!(f, "ZeroSizeBuffer: Buffer contains no data"),
Error::InvalidEncodingConfig => write!(f, "InvalidEncodingConfig: encoding configuration validation failed")
Error::InvalidEncodingConfig => write!(
f,
"InvalidEncodingConfig: encoding configuration validation failed"
),
}
}
}
Expand Down