Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,23 @@ jobs:
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Run tests without default features
run: cargo test --no-default-features --verbose
- name: Format check
run: cargo fmt --check

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
- uses: codecov/codecov-action@v5
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ img = [ "image" ]

[dev-dependencies]
image = "0.25"

[[example]]
name = "convert"
required-features = ["img"]

[[example]]
name = "convert_by_args"
required-features = ["img"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
![Build Status](https://github.com/jaredforth/webp/actions/workflows/rust.yml/badge.svg)
[![codecov](https://codecov.io/gh/jaredforth/webp/graph/badge.svg)](https://codecov.io/gh/jaredforth/webp)
[![Crate](https://img.shields.io/crates/v/webp.svg)](https://crates.io/crates/webp)
[![API](https://docs.rs/webp/badge.svg)](https://docs.rs/webp)
![Crates.io](https://img.shields.io/crates/d/webp)
Expand Down
93 changes: 55 additions & 38 deletions src/animation_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,16 @@ impl<'a> AnimDecoder<'a> {
Self { data }
}
pub fn decode(&self) -> Result<DecodeAnimImage, String> {
unsafe { self.decode_internal(true) }
unsafe { self.decode_internal() }
}
unsafe fn decode_internal(&self, mut has_alpha: bool) -> Result<DecodeAnimImage, String> {
// libwebp's animation decoder only outputs RGBA-family modes, so frames are always RGBA.
unsafe fn decode_internal(&self) -> Result<DecodeAnimImage, String> {
let mut dec_options: WebPAnimDecoderOptions = std::mem::zeroed();
dec_options.color_mode = if has_alpha {
WEBP_CSP_MODE::MODE_RGBA
} else {
WEBP_CSP_MODE::MODE_RGB
};
let ok = WebPAnimDecoderOptionsInitInternal(&mut dec_options, WebPGetDemuxABIVersion());
if ok == 0 {
return Err(String::from("option init error"));
}
match dec_options.color_mode {
WEBP_CSP_MODE::MODE_RGBA | WEBP_CSP_MODE::MODE_RGB => {}
_ => return Err(String::from("unsupport color mode")),
}
has_alpha = dec_options.color_mode == WEBP_CSP_MODE::MODE_RGBA;
dec_options.color_mode = WEBP_CSP_MODE::MODE_RGBA;
let webp_data = WebPData {
bytes: self.data.as_ptr(),
size: self.data.len(),
Expand All @@ -40,6 +32,7 @@ impl<'a> AnimDecoder<'a> {
let mut anim_info: WebPAnimInfo = std::mem::zeroed();
let ok = WebPAnimDecoderGetInfo(dec, &mut anim_info);
if ok == 0 {
WebPAnimDecoderDelete(dec);
return Err(String::from("null info"));
}
let width = anim_info.canvas_width;
Expand All @@ -49,25 +42,21 @@ impl<'a> AnimDecoder<'a> {
let mut buf: *mut u8 = std::ptr::null_mut();
let mut timestamp: std::os::raw::c_int = 0;
let ok = WebPAnimDecoderGetNext(dec, &mut buf, &mut timestamp);
if ok != 0 {
let len = (if has_alpha { 4 } else { 3 } * width * height) as usize;
let mut img = Vec::with_capacity(len);
buf.copy_to(img.spare_capacity_mut().as_mut_ptr().cast(), len);
img.set_len(len);
let layout = if has_alpha {
PixelLayout::Rgba
} else {
PixelLayout::Rgb
};
let frame = DecodeAnimFrame {
img,
width,
height,
layout,
timestamp,
};
list.push(frame);
if ok == 0 {
WebPAnimDecoderDelete(dec);
return Err(String::from("frame decode error"));
}
let len = (4 * width * height) as usize;
let mut img = Vec::with_capacity(len);
buf.copy_to(img.spare_capacity_mut().as_mut_ptr().cast(), len);
img.set_len(len);
list.push(DecodeAnimFrame {
img,
width,
height,
layout: PixelLayout::Rgba,
timestamp,
});
}
WebPAnimDecoderReset(dec);
//let demuxer:WebPDemuxer=WebPAnimDecoderGetDemuxer(dec);
Expand Down Expand Up @@ -102,7 +91,7 @@ impl From<Vec<DecodeAnimFrame>> for DecodeAnimImage {
}
impl DecodeAnimImage {
#[inline]
pub fn get_frame(&self, index: usize) -> Option<AnimFrame> {
pub fn get_frame(&self, index: usize) -> Option<AnimFrame<'_>> {
let f = self.frames.get(index)?;
Some(AnimFrame::new(
&f.img,
Expand All @@ -114,7 +103,7 @@ impl DecodeAnimImage {
))
}
#[inline]
pub fn get_frames(&self, index: core::ops::Range<usize>) -> Option<Vec<AnimFrame>> {
pub fn get_frames(&self, index: core::ops::Range<usize>) -> Option<Vec<AnimFrame<'_>>> {
let dec_frames = self.frames.get(index)?;
let mut frames = Vec::with_capacity(dec_frames.len());
for f in dec_frames {
Expand Down Expand Up @@ -144,12 +133,9 @@ impl<'a> IntoIterator for &'a DecodeAnimImage {
type IntoIter = std::vec::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
let fs = self.get_frames(0..self.frames.len());
if let Some(v) = fs {
v.into_iter()
} else {
vec![].into_iter()
}
self.get_frames(0..self.frames.len())
.unwrap_or_default()
.into_iter()
}
}

Expand Down Expand Up @@ -231,6 +217,37 @@ mod tests {
assert_eq!(count, anim.len());
}

#[test]
fn test_decode_corrupt_frame_is_error() {
let mut config = WebPConfig::new().unwrap();
config.lossless = 1;
let bufs: Vec<Vec<u8>> = (0..3)
.map(|t| {
(0..16 * 16 * 4)
.map(|i| ((i * 7 + t * 31) % 256) as u8)
.collect()
})
.collect();
let mut encoder = crate::AnimEncoder::new(16, 16, &config);
for (t, px) in bufs.iter().enumerate() {
encoder.add_frame(AnimFrame::from_rgba(px, 16, 16, t as i32 * 100));
}
let mut data = encoder.encode().to_vec();

// Corrupt the last frame's VP8L payload, leaving chunk headers intact.
let pos = data.windows(4).rposition(|w| w == b"VP8L").unwrap();
let size = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
for b in &mut data[pos + 8 + 6..pos + 8 + size] {
*b ^= 0xA5;
}

// Used to loop forever; must now fail instead of returning partial frames.
assert_eq!(
AnimDecoder::new(&data).decode().err().as_deref(),
Some("frame decode error")
);
}

#[test]
fn test_decode_failure_on_invalid_data() {
let data = vec![0u8; 10];
Expand Down
61 changes: 61 additions & 0 deletions src/animation_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,67 @@ mod tests {
);
}

#[cfg(feature = "img")]
#[test]
fn test_animframe_from_image() {
use image::{ImageBuffer, Luma, LumaA, Rgb, Rgb32FImage};

let luma = DynamicImage::ImageLuma8(ImageBuffer::from_pixel(1, 1, Luma([0])));
let luma_a = DynamicImage::ImageLumaA8(ImageBuffer::from_pixel(1, 1, LumaA([0, 0])));
let rgb32f = DynamicImage::ImageRgb32F(Rgb32FImage::new(1, 1));
assert!(AnimFrame::from_image(&luma, 0).is_err());
assert!(AnimFrame::from_image(&luma_a, 0).is_err());
assert!(AnimFrame::from_image(&rgb32f, 0).is_err());

let rgb = DynamicImage::ImageRgb8(ImageBuffer::from_pixel(2, 1, Rgb([1, 2, 3])));
let frame = AnimFrame::from_image(&rgb, 7).unwrap();
assert_eq!(frame.get_layout(), PixelLayout::Rgb);
assert_eq!(frame.get_time_ms(), 7);

// round-trip back into a DynamicImage via the RGB branch
assert_eq!(DynamicImage::from(&frame), rgb);
}

#[test]
fn test_encoder_from_animframe() {
let rgb = [1u8, 2, 3, 4, 5, 6];
let frame = AnimFrame::from_rgb(&rgb, 2, 1, 0);
let mem = Encoder::from(&frame).encode_lossless();
assert!(!mem.is_empty());
}

#[test]
fn test_animencoder_invalid_config() {
let config = default_config();
let mut bad = default_config();
bad.quality = 200.0;
let mut encoder = AnimEncoder::new(2, 1, &config);
encoder.add_frame(AnimFrame::new(
&[1, 2, 3, 4, 5, 6],
PixelLayout::Rgb,
2,
1,
0,
Some(&bad),
));
assert!(matches!(
encoder.try_encode(),
Err(AnimEncodeError::WebPEncodingError(_))
));
}

#[test]
fn test_animencoder_invalid_loop_count() {
let config = default_config();
let mut encoder = AnimEncoder::new(2, 1, &config);
encoder.set_loop_count(-1);
encoder.add_frame(AnimFrame::from_rgb(&[1, 2, 3, 4, 5, 6], 2, 1, 0));
assert!(matches!(
encoder.try_encode(),
Err(AnimEncodeError::WebPMuxError(_))
));
}

#[test]
fn test_animdecoder_decode_failure_on_invalid_data() {
let data = vec![0u8; 10];
Expand Down
17 changes: 14 additions & 3 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,21 @@ mod tests {

#[test]
fn test_decoder_rejects_animation() {
let config = WebPConfig::new().unwrap();
let mut encoder = crate::AnimEncoder::new(1, 1, &config);
encoder.add_frame(crate::AnimFrame::from_rgb(&[0, 0, 0], 1, 1, 0));
encoder.add_frame(crate::AnimFrame::from_rgb(&[255, 255, 255], 1, 1, 100));
let data = encoder.encode();
assert!(BitstreamFeatures::new(&data).unwrap().has_animation());
assert!(Decoder::new(&data).decode().is_none());
}

#[test]
fn test_decoder_truncated_data() {
let data = minimal_webp_rgb();
let decoder = Decoder::new(&data);
let image = decoder.decode();
assert!(image.is_some());
let truncated = &data[..data.len() - 8];
assert!(BitstreamFeatures::new(truncated).is_some());
assert!(Decoder::new(truncated).decode().is_none());
}

#[test]
Expand Down
28 changes: 28 additions & 0 deletions src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ mod tests {
let luma_a = DynamicImage::ImageLumaA8(ImageBuffer::from_pixel(1, 1, image::LumaA([0, 0])));
assert!(Encoder::from_image(&luma).is_err());
assert!(Encoder::from_image(&luma_a).is_err());
let rgb32f = DynamicImage::ImageRgb32F(image::Rgb32FImage::new(1, 1));
assert!(Encoder::from_image(&rgb32f).is_err());

let rgb = DynamicImage::ImageRgb8(ImageBuffer::from_pixel(2, 2, image::Rgb([1, 2, 3])));
let rgba =
Expand All @@ -278,4 +280,30 @@ mod tests {
let mem = encoder.encode(75.0);
assert!(!mem.is_empty());
}

#[test]
fn test_oversized_buffer_is_accepted() {
let image = [0u8; 16];
let mem = Encoder::from_rgb(&image, 2, 2).encode_lossless();
assert!(!mem.is_empty());
}

#[test]
fn test_encode_invalid_config() {
let image = [0u8; 12];
let res = Encoder::from_rgb(&image, 2, 2).encode_simple(false, 200.0);
assert!(matches!(
res,
Err(WebPEncodingError::VP8_ENC_ERROR_INVALID_CONFIGURATION)
));
}

#[test]
fn test_encode_zero_dimensions() {
let res = Encoder::from_rgb(&[], 0, 0).encode_simple(false, 75.0);
assert!(matches!(
res,
Err(WebPEncodingError::VP8_ENC_ERROR_BAD_DIMENSION)
));
}
}
13 changes: 3 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub use shared::*;

pub use libwebp_sys::WebPConfig;

#[cfg(test)]
#[cfg(all(test, feature = "img"))]
mod tests {
use std::ops::Deref;

Expand Down Expand Up @@ -181,15 +181,8 @@ mod tests {
t += 250;
}
let webp = encoder.encode();
let mut decode_images: Vec<DynamicImage> = vec![];
match AnimDecoder::new(&webp).decode() {
Ok(frames) => {
decode_images.extend((&frames).into_iter().map(|a| (&a).into()));
}
Err(mes) => {
println!("{}", mes);
}
}
let frames = AnimDecoder::new(&webp).decode().unwrap();
let decode_images: Vec<DynamicImage> = frames.into_iter().map(|a| (&a).into()).collect();
let mut encode_rgba = vec![];
for v in encode_images.into_iter() {
let value = DynamicImage::ImageRgba8(v.to_rgba8());
Expand Down
Loading