diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 31000a2..1baef98 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/examples/convert_by_args.rs b/examples/convert_by_args.rs index d979dae..8e9d529 100644 --- a/examples/convert_by_args.rs +++ b/examples/convert_by_args.rs @@ -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 = args().collect(); if arg.len() != 2 { @@ -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(); @@ -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(); -} \ No newline at end of file +} diff --git a/src/animation_decoder.rs b/src/animation_decoder.rs index 86e4afc..6cc8601 100644 --- a/src/animation_decoder.rs +++ b/src/animation_decoder.rs @@ -152,3 +152,90 @@ impl<'a> IntoIterator for &'a DecodeAnimImage { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn minimal_webp_animation() -> Vec { + 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"); + } +} diff --git a/src/animation_encoder.rs b/src/animation_encoder.rs index 6547c8c..049eccb 100644 --- a/src/animation_encoder.rs +++ b/src/animation_encoder.rs @@ -1,5 +1,3 @@ -use std::ffi::CString; - #[cfg(feature = "img")] use image::DynamicImage; use libwebp_sys::*; @@ -36,7 +34,7 @@ impl<'a> AnimFrame<'a> { } } #[cfg(feature = "img")] - pub fn from_image(image: &'a DynamicImage, timestamp: i32) -> Result { + pub fn from_image(image: &'a DynamicImage, timestamp: i32) -> Result { match image { DynamicImage::ImageLuma8(_) => Err("Unimplemented"), DynamicImage::ImageLumaA8(_) => Err("Unimplemented"), @@ -182,10 +180,14 @@ unsafe fn anim_encode(all_frame: &AnimEncoder) -> Result::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)); } @@ -203,3 +205,85 @@ unsafe fn anim_encode(all_frame: &AnimEncoder) -> Result 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"); + } +} diff --git a/src/decoder.rs b/src/decoder.rs index cd459e7..38066c2 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -144,3 +144,109 @@ pub enum BitstreamFormat { Lossy = 1, Lossless = 2, } + +#[cfg(test)] +mod tests { + use super::*; + + fn minimal_webp_rgb() -> Vec { + 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 + ); + } + } +} diff --git a/src/encoder.rs b/src/encoder.rs index ee1f915..d44bee5 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -26,7 +26,7 @@ impl<'a> Encoder<'a> { #[cfg(feature = "img")] /// Creates a new encoder from the given image. - pub fn from_image(image: &'a DynamicImage) -> Result { + pub fn from_image(image: &'a DynamicImage) -> Result { match image { DynamicImage::ImageLuma8(_) => Err("Unimplemented"), DynamicImage::ImageLumaA8(_) => Err("Unimplemented"), @@ -136,3 +136,61 @@ unsafe fn encode( Err(picture.error_code) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::shared; + + #[test] + fn test_encoder_new_assigns_fields() { + let data = [1, 2, 3, 4, 5, 6]; + let enc = Encoder::new(&data, shared::PixelLayout::Rgb, 2, 3); + assert_eq!(enc.image, &data); + assert_eq!(enc.layout, shared::PixelLayout::Rgb); + assert_eq!(enc.width, 2); + assert_eq!(enc.height, 3); + } + + #[test] + fn test_encoder_from_rgb_and_rgba() { + let rgb = [10, 20, 30, 40, 50, 60]; + let rgba = [1, 2, 3, 4, 5, 6, 7, 8]; + let enc_rgb = Encoder::from_rgb(&rgb, 2, 1); + let enc_rgba = Encoder::from_rgba(&rgba, 2, 1); + assert_eq!(enc_rgb.layout, shared::PixelLayout::Rgb); + assert_eq!(enc_rgba.layout, shared::PixelLayout::Rgba); + assert_eq!(enc_rgb.image, &rgb); + assert_eq!(enc_rgba.image, &rgba); + assert_eq!(enc_rgb.width, 2); + assert_eq!(enc_rgba.height, 1); + } + + #[cfg(feature = "img")] + #[test] + fn test_encoder_from_image_error_branches() { + use image::{DynamicImage, ImageBuffer}; + + let luma = DynamicImage::ImageLuma8(ImageBuffer::from_pixel(1, 1, image::Luma([0]))); + 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 rgb = DynamicImage::ImageRgb8(ImageBuffer::from_pixel(2, 2, image::Rgb([1, 2, 3]))); + let rgba = + DynamicImage::ImageRgba8(ImageBuffer::from_pixel(2, 2, image::Rgba([1, 2, 3, 4]))); + assert!(Encoder::from_image(&rgb).is_ok()); + assert!(Encoder::from_image(&rgba).is_ok()); + } + + #[test] + fn test_encode_runs_without_panic() { + let width = 2; + let height = 2; + let image = [255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0]; + let encoder = Encoder::new(&image, PixelLayout::Rgb, width, height); + + let mem = encoder.encode(75.0); + assert!(!mem.is_empty()); + } +} diff --git a/src/shared.rs b/src/shared.rs index 39c3082..232fd48 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -107,6 +107,10 @@ impl WebPImage { pub fn is_alpha(&self) -> bool { self.layout.is_alpha() } + + pub fn layout(&self) -> PixelLayout { + self.layout + } } impl Deref for WebPImage { @@ -136,3 +140,92 @@ impl PixelLayout { self == PixelLayout::Rgba } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pixel_layout_is_alpha() { + assert!(!PixelLayout::Rgb.is_alpha()); + assert!(PixelLayout::Rgba.is_alpha()); + } + + #[test] + fn test_webpimage_accessors() { + let data = vec![10, 20, 30, 40, 50, 60, 70, 80]; + + let mut boxed = data.clone().into_boxed_slice(); + let ptr = boxed.as_mut_ptr(); + let len = boxed.len(); + std::mem::forget(boxed); + + let mem = WebPMemory(ptr, len); + let img = WebPImage::new(mem, PixelLayout::Rgba, 2, 1); + + assert_eq!(img.width(), 2); + assert_eq!(img.height(), 1); + assert!(img.is_alpha()); + assert_eq!(img.layout(), PixelLayout::Rgba); + + assert_eq!(&img[..], &data[..]); + } + + #[test] + fn test_webpimage_deref_mut() { + let data = vec![1, 2, 3, 4]; + let mut boxed = data.clone().into_boxed_slice(); + let ptr = boxed.as_mut_ptr(); + let len = boxed.len(); + std::mem::forget(boxed); + + let mem = WebPMemory(ptr, len); + let mut img = WebPImage::new(mem, PixelLayout::Rgb, 2, 1); + + img.deref_mut()[0] = 42; + assert_eq!(img[0], 42); + } + + #[test] + fn test_webpmemory_drop_calls_webpfree() { + let data = vec![1, 2, 3, 4]; + let mut boxed = data.clone().into_boxed_slice(); + let ptr = boxed.as_mut_ptr(); + let len = boxed.len(); + std::mem::forget(boxed); + + let _mem = WebPMemory(ptr, len); + } + + #[test] + fn test_pixel_layout_equality() { + assert_eq!(PixelLayout::Rgb, PixelLayout::Rgb); + assert_ne!(PixelLayout::Rgb, PixelLayout::Rgba); + } + + #[test] + fn test_webpmemory_debug_exact() { + let data = vec![1u8, 2, 3]; + let mut boxed = data.into_boxed_slice(); + let ptr = boxed.as_mut_ptr(); + let len = boxed.len(); + std::mem::forget(boxed); + + let mem = WebPMemory(ptr, len); + + let dbg_str = format!("{:?}", mem); + + assert_eq!(dbg_str, "WebpMemory"); + } + + #[test] + fn test_manageedpicture_deref() { + let pic = unsafe { std::mem::zeroed::() }; + let managed = ManageedPicture(pic); + + let inner_ref: &WebPPicture = &*managed; + let orig_ptr = &managed.0 as *const WebPPicture; + let deref_ptr = inner_ref as *const WebPPicture; + assert_eq!(orig_ptr, deref_ptr); + } +}