Skip to content

Commit

Permalink
Improve animation decoding API (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
fintelia authored Jan 1, 2024
1 parent 5e680cc commit ce38503
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 55 deletions.
140 changes: 91 additions & 49 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::HashMap;

use std::io::{self, BufReader, Cursor, Read, Seek};

use std::num::NonZeroU16;
use std::ops::Range;
use thiserror::Error;

Expand Down Expand Up @@ -195,8 +196,7 @@ enum ImageKind {
}

struct AnimationState {
next_frame: usize,
loops_before_done: Option<u16>,
next_frame: u32,
next_frame_start: u64,
dispose_next_frame: bool,
canvas: Option<Vec<u8>>,
Expand All @@ -205,14 +205,22 @@ impl Default for AnimationState {
fn default() -> Self {
Self {
next_frame: 0,
loops_before_done: None,
next_frame_start: 0,
dispose_next_frame: true,
canvas: None,
}
}
}

/// Number of times that an animation loops.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum LoopCount {
/// The animation loops forever.
Forever,
/// Each frame of the animation is displayed the specified number of times.
Times(NonZeroU16),
}

/// WebP image format decoder.
pub struct WebPDecoder<R> {
r: R,
Expand All @@ -221,12 +229,14 @@ pub struct WebPDecoder<R> {
width: u32,
height: u32,

num_frames: usize,
kind: ImageKind,
animation: AnimationState,

kind: ImageKind,
is_lossy: bool,
has_alpha: bool,
num_frames: u32,
loop_count: LoopCount,
loop_duration: u64,

chunks: HashMap<WebPRiffChunk, Range<u64>>,
}
Expand All @@ -246,6 +256,8 @@ impl<R: Read + Seek> WebPDecoder<R> {
memory_limit: usize::MAX,
is_lossy: false,
has_alpha: false,
loop_count: LoopCount::Times(NonZeroU16::new(1).unwrap()),
loop_duration: 0,
};
decoder.read_data()?;
Ok(decoder)
Expand Down Expand Up @@ -343,25 +355,31 @@ impl<R: Read + Seek> WebPDecoder<R> {

if let WebPRiffChunk::ANMF = chunk {
self.num_frames += 1;
if chunk_size < 24 {
return Err(DecodingError::InvalidChunkSize);
}

reader.seek_relative(12)?;
let duration = reader.read_u32::<LittleEndian>()? & 0xffffff;
self.loop_duration =
self.loop_duration.wrapping_add(u64::from(duration));

// If the image is animated, the image data chunk will be inside the
// ANMF chunks, so we must inspect them to determine whether the
// image contains any lossy image data. VP8 chunks store lossy data
// 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) - 24)?;
continue;
} else {
reader.seek_relative(i64::from(chunk_size_rounded) - 16)?;
}

continue;
}

reader.seek_relative(i64::from(chunk_size_rounded))?;
Expand Down Expand Up @@ -395,10 +413,10 @@ impl<R: Read + Seek> WebPDecoder<R> {
Ok(Some(chunk)) => {
let mut cursor = Cursor::new(chunk);
cursor.read_exact(&mut info.background_color)?;
match cursor.read_u16::<LittleEndian>()? {
0 => self.animation.loops_before_done = None,
n => self.animation.loops_before_done = Some(n),
}
self.loop_count = match cursor.read_u16::<LittleEndian>()? {
0 => LoopCount::Forever,
n => LoopCount::Times(NonZeroU16::new(n).unwrap()),
};
self.animation.next_frame_start =
self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
}
Expand Down Expand Up @@ -445,40 +463,59 @@ impl<R: Read + Seek> WebPDecoder<R> {
self.memory_limit = limit;
}

/// Returns true if the image is animated.
pub fn has_animation(&self) -> bool {
match &self.kind {
ImageKind::Lossy | ImageKind::Lossless => false,
ImageKind::Extended(extended) => extended.animation,
/// Sets the background color if the image is an extended and animated webp.
pub fn set_background_color(&mut self, color: [u8; 4]) -> Result<(), DecodingError> {
if let ImageKind::Extended(info) = &mut self.kind {
info.background_color = color;
Ok(())
} else {
Err(DecodingError::InvalidParameter(
"Background color can only be set on animated webp".to_owned(),
))
}
}

/// Returns the (width, height) of the image in pixels.
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}

/// Returns whether the image has an alpha channel. If so, the pixel format is Rgba8 and
/// otherwise Rgb8.
pub fn has_alpha(&self) -> bool {
self.has_alpha
}

/// Returns true if the image is animated.
pub fn is_animated(&self) -> bool {
match &self.kind {
ImageKind::Lossy | ImageKind::Lossless => false,
ImageKind::Extended(extended) => extended.animation,
}
}

/// Returns whether the image is lossy. For animated images, this is true if any frame is lossy.
pub fn is_lossy(&mut self) -> bool {
self.is_lossy
}

/// Sets the background color if the image is an extended and animated webp.
pub fn set_background_color(&mut self, color: [u8; 4]) -> Result<(), DecodingError> {
if let ImageKind::Extended(info) = &mut self.kind {
info.background_color = color;
Ok(())
} else {
Err(DecodingError::InvalidParameter(
"Background color can only be set on animated webp".to_owned(),
))
}
/// Returns the number of frames of a single loop of the animation, or zero if the image is not
/// animated.
pub fn num_frames(&self) -> u32 {
self.num_frames
}

/// Returns the (width, height) of the image in pixels.
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
/// Returns the number of times the animation should loop.
pub fn loop_count(&self) -> LoopCount {
self.loop_count
}

/// Returns the total duration of one loop through the animation in milliseconds, or zero if the
/// image is not animated.
///
/// This is the sum of the durations of all individual frames of the image.
pub fn loop_duration(&self) -> u64 {
self.loop_duration
}

fn read_chunk(
Expand Down Expand Up @@ -529,7 +566,7 @@ 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 self.has_animation() {
if self.is_animated() {
let saved = std::mem::take(&mut self.animation);
self.animation.next_frame_start =
self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
Expand Down Expand Up @@ -602,16 +639,18 @@ impl<R: Read + Seek> WebPDecoder<R> {

/// Reads the next frame of the animation.
///
/// The frame contents are written into `buf` and the method returns the delay of the frame in
/// milliseconds. If there are no more frames, the method returns `None` and `buf` is left
/// The frame contents are written into `buf` and the method returns the duration of the frame
/// in milliseconds. If there are no more frames, the method returns `None` and `buf` is left
/// unchanged.
///
/// # Panics
///
/// Panics if the image is not animated.
pub fn read_frame(&mut self, buf: &mut [u8]) -> Result<Option<u32>, DecodingError> {
assert!(self.has_animation());
assert!(self.is_animated());
assert_eq!(Some(buf.len()), self.output_buffer_size());

if self.animation.loops_before_done == Some(0) {
if self.animation.next_frame == self.num_frames {
return Ok(None);
}

Expand Down Expand Up @@ -653,7 +692,7 @@ impl<R: Read + Seek> WebPDecoder<R> {
None
};

//read normal bitstream now
// Read normal bitstream now
let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
if chunk_size_rounded + 32 < anmf_size {
return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
Expand Down Expand Up @@ -752,16 +791,6 @@ impl<R: Read + Seek> WebPDecoder<R> {
self.animation.next_frame_start += anmf_size as u64 + 8;
self.animation.next_frame += 1;

if self.animation.next_frame >= self.num_frames {
self.animation.next_frame = 0;
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 - 8;
self.animation.dispose_next_frame = true;
}

if self.has_alpha() {
buf.copy_from_slice(self.animation.canvas.as_ref().unwrap());
} else {
Expand All @@ -775,6 +804,19 @@ impl<R: Read + Seek> WebPDecoder<R> {

Ok(Some(duration))
}

/// Resets the animation to the first frame.
///
/// # Panics
///
/// Panics if the image is not animated.
pub fn reset_animation(&mut self) {
assert!(self.is_animated());

self.animation.next_frame = 0;
self.animation.next_frame_start = self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
self.animation.dispose_next_frame = true;
}
}

pub(crate) fn range_reader<R: Read + Seek>(
Expand Down
10 changes: 4 additions & 6 deletions tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ fn reference_test(file: &str) {
let (width, height) = decoder.dimensions();

// Decode reference PNG
let reference_path = if decoder.has_animation() {
let reference_path = if decoder.is_animated() {
format!("tests/reference/{file}-1.png")
} else {
format!("tests/reference/{file}.png")
Expand Down Expand Up @@ -69,12 +69,10 @@ fn reference_test(file: &str) {
}

// If the file is animated, then check all frames.
if decoder.has_animation() {
for i in 1.. {
if decoder.is_animated() {
for i in 1..=decoder.num_frames() {
let reference_path = PathBuf::from(format!("tests/reference/{file}-{i}.png"));
if !reference_path.exists() {
break;
}
assert!(reference_path.exists());

let reference_contents = std::fs::read(reference_path).unwrap();
let mut reference_decoder = png::Decoder::new(Cursor::new(reference_contents))
Expand Down

0 comments on commit ce38503

Please sign in to comment.