diff --git a/src/decoder.rs b/src/decoder.rs index b0bc88c..5cc6417 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -338,11 +338,16 @@ impl WebPDecoder { // and the spec says that lossless images SHOULD NOT contain ALPH // chunks, so we treat both as indicators of lossy images. if !self.is_lossy { + if chunk_size < 24 { + return Err(DecodingError::InvalidChunkSize); + } + + reader.seek_relative(16)?; let (subchunk, ..) = read_chunk_header(&mut reader)?; if let WebPRiffChunk::VP8 | WebPRiffChunk::ALPH = subchunk { self.is_lossy = true; } - reader.seek_relative(i64::from(chunk_size_rounded) - 8)?; + reader.seek_relative(i64::from(chunk_size_rounded) - 24)?; continue; } } @@ -383,7 +388,7 @@ impl WebPDecoder { n => self.animation.loops_before_done = Some(n), } self.animation.next_frame_start = - self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start; + self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8; } Ok(None) => return Err(DecodingError::ChunkMissing), Err(DecodingError::MemoryLimitExceeded) => { @@ -397,9 +402,8 @@ impl WebPDecoder { // store the ALPH, VP8, and VP8L chunks (as applicable) of the first frame in the // hashmap so that we can read them later. if let Some(range) = self.chunks.get(&WebPRiffChunk::ANMF).cloned() { - self.r.seek(io::SeekFrom::Start(range.start))?; - let mut position = range.start; - + let mut position = range.start + 16; + self.r.seek(io::SeekFrom::Start(position))?; for _ in 0..2 { let (subchunk, subchunk_size, subchunk_size_rounded) = read_chunk_header(&mut self.r)?; @@ -516,7 +520,14 @@ impl WebPDecoder { pub fn read_image(&mut self, buf: &mut [u8]) -> Result<(), DecodingError> { assert_eq!(Some(buf.len()), self.output_buffer_size()); - if let Some(range) = self.chunks.get(&WebPRiffChunk::VP8L) { + if self.has_animation() { + let saved = std::mem::take(&mut self.animation); + self.animation.next_frame_start = + self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8; + let result = self.read_frame(buf); + self.animation = saved; + result?; + } else if let Some(range) = self.chunks.get(&WebPRiffChunk::VP8L) { let mut frame = LosslessDecoder::new(range_reader(&mut self.r, range.clone())?); let frame = frame.decode_frame()?; if u32::from(frame.width) != self.width || u32::from(frame.height) != self.height { @@ -732,11 +743,21 @@ impl WebPDecoder { if self.animation.loops_before_done.is_some() { *self.animation.loops_before_done.as_mut().unwrap() -= 1; } - self.animation.next_frame_start = self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start; + self.animation.next_frame_start = + self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8; self.animation.dispose_next_frame = true; } - buf.copy_from_slice(self.animation.canvas.as_ref().unwrap()); + if self.has_alpha() { + buf.copy_from_slice(self.animation.canvas.as_ref().unwrap()); + } else { + for (b, c) in buf + .chunks_exact_mut(3) + .zip(self.animation.canvas.as_ref().unwrap().chunks_exact(4)) + { + b.copy_from_slice(&c[..3]); + } + } Ok(Some(duration)) } diff --git a/tests/CREDITS.md b/tests/CREDITS.md new file mode 100644 index 0000000..454ce0e --- /dev/null +++ b/tests/CREDITS.md @@ -0,0 +1,49 @@ +This document describes how the images in the tests directory were obtained. + +# Source images + +The files in the _images_ directory are WebP images used to test the decoder. + +## images/gallery1 + +Downloaded from https://developers.google.com/speed/webp/gallery1. + +## images/gallery2 + +Downloaded from https://developers.google.com/speed/webp/gallery2 + +## images/animated + +Manually created using imagemagick... + +random.webp: +``` +convert -delay 10 -size 64x63 xc: xc: xc: +noise Random -define webp:lossless=true random_lossless.webp +``` + +random2.webp: +``` +convert -delay 15 -size 99x87 xc: xc: xc: xc: +noise Random -define webp:lossless=false random_lossy.webp +``` + +# Reference images + +These files are all PNGs with contents that should exactly match the associated WebP file in the _images_ directory. + +## reference/gallery1 and reference/gallery2 + +These files were all produced by running dwebp with the `-nofancy` option. + +## reference/animated + +random-lossless-N.png: + +``` +for i in {1..3}; do webpmux -get frame ${i} ../../images/animated/random_lossless.webp -o random_lossless-${i}.png && convert random_lossless-${i}.png random_lossless-${i}.png; done +``` + +random-lossy-N.png: + +``` +for i in {1..4}; do webpmux -get frame ${i} ../../images/animated/random_lossy.webp -o random_lossy-${i}.png && dwebp random_lossy-${i}.png -nofancy -o random_lossy-${i}.png; done +``` diff --git a/tests/decode.rs b/tests/decode.rs index cf5928f..38790e3 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -1,8 +1,17 @@ -use std::io::Cursor; +use std::{io::Cursor, path::PathBuf}; + +fn reference_test(file: &str) { + // Prepare WebP decoder + let contents = std::fs::read(format!("tests/images/{file}.webp")).unwrap(); + let mut decoder = webp::WebPDecoder::new(Cursor::new(contents)).unwrap(); + let (width, height) = decoder.dimensions(); -fn refence_test(file: &str) { // Decode reference PNG - let reference_path = format!("tests/reference/{file}.png",); + let reference_path = if decoder.has_animation() { + format!("tests/reference/{file}-1.png") + } else { + format!("tests/reference/{file}.png") + }; let reference_contents = std::fs::read(reference_path).unwrap(); let mut reference_decoder = png::Decoder::new(Cursor::new(reference_contents)) .read_info() @@ -11,11 +20,6 @@ fn refence_test(file: &str) { let mut reference_data = vec![0; reference_decoder.output_buffer_size()]; reference_decoder.next_frame(&mut reference_data).unwrap(); - // Prepare WebP decoder - let contents = std::fs::read(format!("tests/images/{file}.webp")).unwrap(); - let mut decoder = webp::WebPDecoder::new(Cursor::new(contents)).unwrap(); - let (width, height) = decoder.dimensions(); - // Compare metadata assert_eq!(width, reference_decoder.info().width); assert_eq!(height, reference_decoder.info().height); @@ -63,23 +67,63 @@ fn refence_test(file: &str) { "More than 10% of pixels differ" ); } + + // If the file is animated, then check all frames. + if decoder.has_animation() { + for i in 1.. { + let reference_path = PathBuf::from(format!("tests/reference/{file}-{i}.png")); + if !reference_path.exists() { + break; + } + + let reference_contents = std::fs::read(reference_path).unwrap(); + let mut reference_decoder = png::Decoder::new(Cursor::new(reference_contents)) + .read_info() + .unwrap(); + assert_eq!(reference_decoder.info().bit_depth, png::BitDepth::Eight); + let mut reference_data = vec![0; reference_decoder.output_buffer_size()]; + reference_decoder.next_frame(&mut reference_data).unwrap(); + + // Decode WebP + let bytes_per_pixel = if decoder.has_alpha() { 4 } else { 3 }; + let mut data = vec![0; width as usize * height as usize * bytes_per_pixel]; + decoder.read_frame(&mut data).unwrap(); + + if !decoder.is_lossy() { + if data != reference_data { + panic!("Pixel mismatch") + } + } else { + let num_bytes_different = data + .iter() + .zip(reference_data.iter()) + .filter(|(a, b)| a != b) + .count(); + assert!( + 100 * num_bytes_different / data.len() < 10, + "More than 10% of pixels differ" + ); + } + } + } } macro_rules! reftest { - ($name:expr) => { + ($basename:expr, $name:expr) => { paste::paste! { #[test] - fn []() { - refence_test(stringify!($name)); + fn []() { + reference_test(concat!(stringify!($basename), "/", stringify!($name))); } } }; - ($name:expr, $($tail:expr),+) => { - reftest!( $name ); - reftest!( $($tail),+ ); + ($basename:expr, $name:expr, $($tail:expr),+) => { + reftest!( $basename, $name ); + reftest!( $basename, $($tail),+ ); } } -reftest!(1, 2, 3, 4, 5); -reftest!(1_webp_ll, 2_webp_ll, 3_webp_ll, 4_webp_ll, 5_webp_ll); -reftest!(1_webp_a, 2_webp_a, 3_webp_a, 4_webp_a, 5_webp_a); +reftest!(gallery1, 1, 2, 3, 4, 5); +reftest!(gallery2, 1_webp_ll, 2_webp_ll, 3_webp_ll, 4_webp_ll, 5_webp_ll); +reftest!(gallery2, 1_webp_a, 2_webp_a, 3_webp_a, 4_webp_a, 5_webp_a); +reftest!(animated, random_lossless, random_lossy); diff --git a/tests/images/animated/random_lossless.webp b/tests/images/animated/random_lossless.webp new file mode 100644 index 0000000..944aab0 Binary files /dev/null and b/tests/images/animated/random_lossless.webp differ diff --git a/tests/images/animated/random_lossy.webp b/tests/images/animated/random_lossy.webp new file mode 100644 index 0000000..e06d60e Binary files /dev/null and b/tests/images/animated/random_lossy.webp differ diff --git a/tests/images/1.webp b/tests/images/gallery1/1.webp similarity index 100% rename from tests/images/1.webp rename to tests/images/gallery1/1.webp diff --git a/tests/images/2.webp b/tests/images/gallery1/2.webp similarity index 100% rename from tests/images/2.webp rename to tests/images/gallery1/2.webp diff --git a/tests/images/3.webp b/tests/images/gallery1/3.webp similarity index 100% rename from tests/images/3.webp rename to tests/images/gallery1/3.webp diff --git a/tests/images/4.webp b/tests/images/gallery1/4.webp similarity index 100% rename from tests/images/4.webp rename to tests/images/gallery1/4.webp diff --git a/tests/images/5.webp b/tests/images/gallery1/5.webp similarity index 100% rename from tests/images/5.webp rename to tests/images/gallery1/5.webp diff --git a/tests/images/1_webp_a.webp b/tests/images/gallery2/1_webp_a.webp similarity index 100% rename from tests/images/1_webp_a.webp rename to tests/images/gallery2/1_webp_a.webp diff --git a/tests/images/1_webp_ll.webp b/tests/images/gallery2/1_webp_ll.webp similarity index 100% rename from tests/images/1_webp_ll.webp rename to tests/images/gallery2/1_webp_ll.webp diff --git a/tests/images/2_webp_a.webp b/tests/images/gallery2/2_webp_a.webp similarity index 100% rename from tests/images/2_webp_a.webp rename to tests/images/gallery2/2_webp_a.webp diff --git a/tests/images/2_webp_ll.webp b/tests/images/gallery2/2_webp_ll.webp similarity index 100% rename from tests/images/2_webp_ll.webp rename to tests/images/gallery2/2_webp_ll.webp diff --git a/tests/images/3_webp_a.webp b/tests/images/gallery2/3_webp_a.webp similarity index 100% rename from tests/images/3_webp_a.webp rename to tests/images/gallery2/3_webp_a.webp diff --git a/tests/images/3_webp_ll.webp b/tests/images/gallery2/3_webp_ll.webp similarity index 100% rename from tests/images/3_webp_ll.webp rename to tests/images/gallery2/3_webp_ll.webp diff --git a/tests/images/4_webp_a.webp b/tests/images/gallery2/4_webp_a.webp similarity index 100% rename from tests/images/4_webp_a.webp rename to tests/images/gallery2/4_webp_a.webp diff --git a/tests/images/4_webp_ll.webp b/tests/images/gallery2/4_webp_ll.webp similarity index 100% rename from tests/images/4_webp_ll.webp rename to tests/images/gallery2/4_webp_ll.webp diff --git a/tests/images/5_webp_a.webp b/tests/images/gallery2/5_webp_a.webp similarity index 100% rename from tests/images/5_webp_a.webp rename to tests/images/gallery2/5_webp_a.webp diff --git a/tests/images/5_webp_ll.webp b/tests/images/gallery2/5_webp_ll.webp similarity index 100% rename from tests/images/5_webp_ll.webp rename to tests/images/gallery2/5_webp_ll.webp diff --git a/tests/reference/animated/random_lossless-1.png b/tests/reference/animated/random_lossless-1.png new file mode 100644 index 0000000..3ed2230 Binary files /dev/null and b/tests/reference/animated/random_lossless-1.png differ diff --git a/tests/reference/animated/random_lossless-2.png b/tests/reference/animated/random_lossless-2.png new file mode 100644 index 0000000..b200211 Binary files /dev/null and b/tests/reference/animated/random_lossless-2.png differ diff --git a/tests/reference/animated/random_lossless-3.png b/tests/reference/animated/random_lossless-3.png new file mode 100644 index 0000000..c1d77ef Binary files /dev/null and b/tests/reference/animated/random_lossless-3.png differ diff --git a/tests/reference/animated/random_lossy-1.png b/tests/reference/animated/random_lossy-1.png new file mode 100644 index 0000000..d555097 Binary files /dev/null and b/tests/reference/animated/random_lossy-1.png differ diff --git a/tests/reference/animated/random_lossy-2.png b/tests/reference/animated/random_lossy-2.png new file mode 100644 index 0000000..41ca056 Binary files /dev/null and b/tests/reference/animated/random_lossy-2.png differ diff --git a/tests/reference/animated/random_lossy-3.png b/tests/reference/animated/random_lossy-3.png new file mode 100644 index 0000000..bee4cbf Binary files /dev/null and b/tests/reference/animated/random_lossy-3.png differ diff --git a/tests/reference/animated/random_lossy-4.png b/tests/reference/animated/random_lossy-4.png new file mode 100644 index 0000000..ca504e0 Binary files /dev/null and b/tests/reference/animated/random_lossy-4.png differ diff --git a/tests/reference/1.png b/tests/reference/gallery1/1.png similarity index 100% rename from tests/reference/1.png rename to tests/reference/gallery1/1.png diff --git a/tests/reference/2.png b/tests/reference/gallery1/2.png similarity index 100% rename from tests/reference/2.png rename to tests/reference/gallery1/2.png diff --git a/tests/reference/3.png b/tests/reference/gallery1/3.png similarity index 100% rename from tests/reference/3.png rename to tests/reference/gallery1/3.png diff --git a/tests/reference/4.png b/tests/reference/gallery1/4.png similarity index 100% rename from tests/reference/4.png rename to tests/reference/gallery1/4.png diff --git a/tests/reference/5.png b/tests/reference/gallery1/5.png similarity index 100% rename from tests/reference/5.png rename to tests/reference/gallery1/5.png diff --git a/tests/reference/1_webp_a.png b/tests/reference/gallery2/1_webp_a.png similarity index 100% rename from tests/reference/1_webp_a.png rename to tests/reference/gallery2/1_webp_a.png diff --git a/tests/reference/1_webp_ll.png b/tests/reference/gallery2/1_webp_ll.png similarity index 100% rename from tests/reference/1_webp_ll.png rename to tests/reference/gallery2/1_webp_ll.png diff --git a/tests/reference/2_webp_a.png b/tests/reference/gallery2/2_webp_a.png similarity index 100% rename from tests/reference/2_webp_a.png rename to tests/reference/gallery2/2_webp_a.png diff --git a/tests/reference/2_webp_ll.png b/tests/reference/gallery2/2_webp_ll.png similarity index 100% rename from tests/reference/2_webp_ll.png rename to tests/reference/gallery2/2_webp_ll.png diff --git a/tests/reference/3_webp_a.png b/tests/reference/gallery2/3_webp_a.png similarity index 100% rename from tests/reference/3_webp_a.png rename to tests/reference/gallery2/3_webp_a.png diff --git a/tests/reference/3_webp_ll.png b/tests/reference/gallery2/3_webp_ll.png similarity index 100% rename from tests/reference/3_webp_ll.png rename to tests/reference/gallery2/3_webp_ll.png diff --git a/tests/reference/4_webp_a.png b/tests/reference/gallery2/4_webp_a.png similarity index 100% rename from tests/reference/4_webp_a.png rename to tests/reference/gallery2/4_webp_a.png diff --git a/tests/reference/4_webp_ll.png b/tests/reference/gallery2/4_webp_ll.png similarity index 100% rename from tests/reference/4_webp_ll.png rename to tests/reference/gallery2/4_webp_ll.png diff --git a/tests/reference/5_webp_a.png b/tests/reference/gallery2/5_webp_a.png similarity index 100% rename from tests/reference/5_webp_a.png rename to tests/reference/gallery2/5_webp_a.png diff --git a/tests/reference/5_webp_ll.png b/tests/reference/gallery2/5_webp_ll.png similarity index 100% rename from tests/reference/5_webp_ll.png rename to tests/reference/gallery2/5_webp_ll.png