Skip to content

Commit

Permalink
Expand/improve tests and fix animation decoding (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
fintelia authored Dec 17, 2023
1 parent f98b744 commit 944a249
Show file tree
Hide file tree
Showing 42 changed files with 139 additions and 25 deletions.
37 changes: 29 additions & 8 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,16 @@ impl<R: Read + Seek> WebPDecoder<R> {
// 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;
}
}
Expand Down Expand Up @@ -383,7 +388,7 @@ impl<R: Read + Seek> WebPDecoder<R> {
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) => {
Expand All @@ -397,9 +402,8 @@ impl<R: Read + Seek> WebPDecoder<R> {
// 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)?;
Expand Down Expand Up @@ -516,7 +520,14 @@ impl<R: Read + Seek> WebPDecoder<R> {
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 {
Expand Down Expand Up @@ -732,11 +743,21 @@ impl<R: Read + Seek> WebPDecoder<R> {
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))
}
Expand Down
49 changes: 49 additions & 0 deletions tests/CREDITS.md
Original file line number Diff line number Diff line change
@@ -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
```
78 changes: 61 additions & 17 deletions tests/decode.rs
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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);
Expand Down Expand Up @@ -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 [<reftest_ $name>]() {
refence_test(stringify!($name));
fn [<reftest_ $basename _ $name>]() {
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);
Binary file added tests/images/animated/random_lossless.webp
Binary file not shown.
Binary file added tests/images/animated/random_lossy.webp
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file added tests/reference/animated/random_lossless-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/reference/animated/random_lossless-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/reference/animated/random_lossless-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/reference/animated/random_lossy-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/reference/animated/random_lossy-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/reference/animated/random_lossy-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/reference/animated/random_lossy-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes

0 comments on commit 944a249

Please sign in to comment.