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
17 changes: 9 additions & 8 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ name: Rust

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
CARGO_TERM_COLOR: always

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Format check
run: cargo fmt --check
5 changes: 1 addition & 4 deletions examples/convert_by_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ use image::*;
use std::{env::args, fmt::format, path::Path};
use webp::*;


/// cargo run --example convert_by_args lake.jpg.
fn main() {

//Add a get args functions
let arg: Vec<String> = args().collect();
if arg.len() != 2 {
Expand All @@ -17,7 +15,6 @@ fn main() {
let path = format(format_args!("assets/{}", arg[1]));
let path = Path::new(&path);


// Using `image` crate, open the included .jpg file
let img = image::open(path).unwrap();
let (w, h) = img.dimensions();
Expand Down Expand Up @@ -62,4 +59,4 @@ fn test_convert() {
// Define and write the WebP-encoded file to a given path
let output_path = Path::new("assets").join("lake").with_extension("webp");
std::fs::write(&output_path, &*webp).unwrap();
}
}
87 changes: 87 additions & 0 deletions src/animation_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,90 @@ impl<'a> IntoIterator for &'a DecodeAnimImage {
}
}
}

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

fn minimal_webp_animation() -> Vec<u8> {
vec![
0x52, 0x49, 0x46, 0x46, 0x84, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50,
0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x41, 0x4e, 0x49, 0x4d, 0x06, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x41, 0x4e, 0x4d, 0x46, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x02, 0x56, 0x50,
0x38, 0x4c, 0x0f, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x10, 0xfd,
0x8f, 0xfe, 0x07, 0x22, 0xa2, 0xff, 0x01, 0x00, 0x41, 0x4e, 0x4d, 0x46, 0x28, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x64, 0x00, 0x00, 0x00, 0x56, 0x50, 0x38, 0x4c, 0x0f, 0x00, 0x00, 0x00, 0x2f, 0x00,
0x00, 0x00, 0x00, 0x07, 0x10, 0xd1, 0xff, 0xfe, 0x07, 0x22, 0xa2, 0xff, 0x01, 0x00,
]
}

#[test]
fn test_decoder_creation() {
let data = minimal_webp_animation();
let decoder = AnimDecoder::new(&data);
assert_eq!(decoder.data, &data[..]);
}

#[test]
fn test_decode_success_and_metadata() {
let data = minimal_webp_animation();
let decoder = AnimDecoder::new(&data);
let result = decoder.decode();
assert!(result.is_ok(), "Decoding should succeed for valid data");
let anim = result.unwrap();
assert!(anim.len() > 0, "Animation should have at least one frame");
let _ = anim.loop_count;
let _ = anim.bg_color;
}

#[test]
fn test_get_frame_and_get_frames() {
let data = minimal_webp_animation();
let decoder = AnimDecoder::new(&data);
let anim = decoder.decode().unwrap();
let frame = anim.get_frame(0);
assert!(frame.is_some(), "Should retrieve first frame");
let frames = anim.get_frames(0..1);
assert!(frames.is_some(), "Should retrieve frame range");
assert_eq!(frames.unwrap().len(), 1);
}

#[test]
fn test_has_animation_and_len() {
let data = minimal_webp_animation();
let decoder = AnimDecoder::new(&data);
let anim = decoder.decode().unwrap();
assert_eq!(anim.has_animation(), anim.len() > 1);
}

#[test]
fn test_sort_by_time_stamp() {
let data = minimal_webp_animation();
let decoder = AnimDecoder::new(&data);
let mut anim = decoder.decode().unwrap();
anim.frames.reverse();
anim.sort_by_time_stamp();
let timestamps: Vec<_> = anim.frames.iter().map(|f| f.timestamp).collect();
assert!(timestamps.windows(2).all(|w| w[0] <= w[1]));
}

#[test]
fn test_iteration() {
let data = minimal_webp_animation();
let decoder = AnimDecoder::new(&data);
let anim = decoder.decode().unwrap();
let count = anim.into_iter().count();
assert_eq!(count, anim.len());
}

#[test]
fn test_decode_failure_on_invalid_data() {
let data = vec![0u8; 10];
let decoder = AnimDecoder::new(&data);
let result = decoder.decode();
assert!(result.is_err(), "Decoding should fail for invalid data");
}
}
98 changes: 91 additions & 7 deletions src/animation_encoder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::ffi::CString;

#[cfg(feature = "img")]
use image::DynamicImage;
use libwebp_sys::*;
Expand Down Expand Up @@ -36,7 +34,7 @@ impl<'a> AnimFrame<'a> {
}
}
#[cfg(feature = "img")]
pub fn from_image(image: &'a DynamicImage, timestamp: i32) -> Result<Self, &str> {
pub fn from_image(image: &'a DynamicImage, timestamp: i32) -> Result<Self, &'a str> {
match image {
DynamicImage::ImageLuma8(_) => Err("Unimplemented"),
DynamicImage::ImageLumaA8(_) => Err("Unimplemented"),
Expand Down Expand Up @@ -182,10 +180,14 @@ unsafe fn anim_encode(all_frame: &AnimEncoder) -> Result<WebPMemory, AnimEncodeE
let mut webp_data = std::mem::MaybeUninit::<WebPData>::uninit();
let ok = WebPAnimEncoderAssemble(encoder, webp_data.as_mut_ptr());
if ok == 0 {
//ok == false
let cstring = WebPAnimEncoderGetError(encoder);
let cstring = CString::from_raw(cstring as *mut _);
let string = cstring.to_string_lossy().to_string();
let err_ptr = WebPAnimEncoderGetError(encoder);
let string = if !err_ptr.is_null() {
unsafe { std::ffi::CStr::from_ptr(err_ptr) }
.to_string_lossy()
.into_owned()
} else {
String::from("Unknown error")
};
WebPAnimEncoderDelete(encoder);
return Err(AnimEncodeError::WebPAnimEncoderGetError(string));
}
Expand All @@ -203,3 +205,85 @@ unsafe fn anim_encode(all_frame: &AnimEncoder) -> Result<WebPMemory, AnimEncodeE
let raw_data: WebPData = webp_data.assume_init();
Ok(WebPMemory(raw_data.bytes as *mut u8, raw_data.size))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::shared::PixelLayout;
use crate::AnimDecoder;

fn default_config() -> WebPConfig {
let mut config = unsafe { std::mem::zeroed() };
let ok = unsafe {
WebPConfigInitInternal(
&mut config,
WebPPreset::WEBP_PRESET_DEFAULT,
75.0,
WEBP_ENCODER_ABI_VERSION as i32,
)
};
assert_ne!(ok, 0, "WebPConfigInitInternal failed");
config
}

#[test]
fn test_animframe_new_and_accessors() {
let img = [255u8, 0, 0, 255, 0, 255, 0, 255];
let frame = AnimFrame::new(&img, PixelLayout::Rgba, 2, 1, 42, None);
assert_eq!(frame.get_image(), &img);
assert_eq!(frame.get_layout(), PixelLayout::Rgba);
assert_eq!(frame.width(), 2);
assert_eq!(frame.height(), 1);
assert_eq!(frame.get_time_ms(), 42);
}

#[test]
fn test_animframe_from_rgb_and_rgba() {
let rgb = [1u8, 2, 3, 4, 5, 6];
let rgba = [1u8, 2, 3, 4, 5, 6, 7, 8];
let f_rgb = AnimFrame::from_rgb(&rgb, 2, 1, 100);
let f_rgba = AnimFrame::from_rgba(&rgba, 2, 1, 200);
assert_eq!(f_rgb.get_layout(), PixelLayout::Rgb);
assert_eq!(f_rgba.get_layout(), PixelLayout::Rgba);
assert_eq!(f_rgb.get_time_ms(), 100);
assert_eq!(f_rgba.get_time_ms(), 200);
}

#[test]
fn test_animencoder_add_and_configure() {
let config = default_config();
let mut encoder = AnimEncoder::new(2, 1, &config);
encoder.set_bgcolor([1, 2, 3, 4]);
encoder.set_loop_count(3);

let frame = AnimFrame::from_rgb(&[1, 2, 3, 4, 5, 6], 2, 1, 0);
encoder.add_frame(frame);

assert_eq!(encoder.frames.len(), 1);
assert_eq!(encoder.width, 2);
assert_eq!(encoder.height, 1);
assert_eq!(encoder.muxparams.loop_count, 3);

let expected_bg = (4u32 << 24) | (3u32 << 16) | (2u32 << 8) | 1u32;
assert_eq!(encoder.muxparams.bgcolor, expected_bg);
}

#[test]
fn test_animencoder_encode_error_on_empty() {
let config = default_config();
let encoder = AnimEncoder::new(2, 1, &config);
let result = encoder.try_encode();
assert!(
result.is_err(),
"Encoding with no frames should fail or error"
);
}

#[test]
fn test_animdecoder_decode_failure_on_invalid_data() {
let data = vec![0u8; 10];
let decoder = AnimDecoder::new(&data);
let result = decoder.decode();
assert!(result.is_err(), "Decoding should fail for invalid data");
}
}
106 changes: 106 additions & 0 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,109 @@ pub enum BitstreamFormat {
Lossy = 1,
Lossless = 2,
}

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

fn minimal_webp_rgb() -> Vec<u8> {
vec![
0x52, 0x49, 0x46, 0x46, 0x24, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50,
0x38, 0x20, 0x18, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x01, 0x00,
0x01, 0x00, 0x02, 0x00, 0x34, 0x25, 0xa4, 0x00, 0x03, 0x70, 0x00, 0xfe, 0xfb, 0x94,
0x00, 0x00,
]
}

#[test]
fn test_bitstream_features_basic() {
let data = minimal_webp_rgb();
let features = BitstreamFeatures::new(&data).expect("Should parse features");
assert_eq!(features.width(), 1);
assert_eq!(features.height(), 1);
assert!(!features.has_alpha());
assert!(!features.has_animation());
assert!(matches!(
features.format(),
Some(BitstreamFormat::Lossy)
| Some(BitstreamFormat::Lossless)
| Some(BitstreamFormat::Undefined)
));
}

#[test]
fn test_decoder_decode_success() {
let mut data = minimal_webp_rgb();
data.extend_from_slice(&[0u8; 32]); // Add padding
let decoder = Decoder::new(&data);
let image = decoder.decode();
assert!(image.is_some(), "Should decode minimal WebP");
let image = image.unwrap();
assert_eq!(image.width(), 1);
assert_eq!(image.height(), 1);
assert_eq!(image.layout(), PixelLayout::Rgb);
}

#[test]
fn test_decoder_rejects_animation() {
let data = minimal_webp_rgb();
let decoder = Decoder::new(&data);
let image = decoder.decode();
assert!(image.is_some());
}

#[test]
fn test_bitstream_features_invalid_data() {
let data = vec![0u8; 8];
let features = BitstreamFeatures::new(&data);
assert!(features.is_none(), "Should not parse invalid WebP");
}

#[test]
fn test_decoder_invalid_data() {
let data = vec![0u8; 8];
let decoder = Decoder::new(&data);
assert!(decoder.decode().is_none(), "Should not decode invalid WebP");
}

#[test]
fn test_bitstreamfeatures_debug_output() {
fn make_features(
width: i32,
height: i32,
has_alpha: i32,
has_animation: i32,
format: i32,
) -> BitstreamFeatures {
BitstreamFeatures(WebPBitstreamFeatures {
width,
height,
has_alpha,
has_animation,
format,
pad: [0; 5],
})
}

let cases = [
(make_features(1, 2, 1, 0, 1), "format: \"Lossy\""),
(make_features(3, 4, 0, 1, 2), "format: \"Lossless\""),
(make_features(5, 6, 0, 0, 0), "format: \"Undefined\""),
(make_features(7, 8, 1, 1, 42), "format: \"Error\""),
];

for (features, format_str) in &cases {
let dbg = format!("{features:?}");
assert!(dbg.contains("BitstreamFeatures"));
assert!(dbg.contains(&format!("width: {}", features.width())));
assert!(dbg.contains(&format!("height: {}", features.height())));
assert!(dbg.contains(&format!("has_alpha: {}", features.has_alpha())));
assert!(dbg.contains(&format!("has_animation: {}", features.has_animation())));
assert!(
dbg.contains(format_str),
"Debug output missing expected format string: {}",
format_str
);
}
}
}
Loading