Skip to content

Commit

Permalink
webp: Decode animations frame by frame
Browse files Browse the repository at this point in the history
Previously, animated WebPs could take some time
until they started playing and would consume more memory.
  • Loading branch information
sophie-h committed Nov 18, 2023
1 parent 032c3e2 commit ec857a1
Showing 1 changed file with 57 additions and 22 deletions.
79 changes: 57 additions & 22 deletions src/codecs/webp/extended.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::convert::TryInto;
use std::io::{self, Cursor, Error, Read};
use std::io::{self, Cursor, Error, Read, Seek};
use std::{error, fmt};

use super::decoder::{
Expand Down Expand Up @@ -68,7 +68,8 @@ pub(crate) struct WebPExtendedInfo {
#[derive(Debug)]
enum ExtendedImageData {
Animation {
frames: Vec<AnimatedFrame>,
frames: Vec<Vec<u8>>,
first_frame: AnimatedFrame,
anim_info: WebPAnimatedInfo,
},
Static(WebPStatic),
Expand All @@ -95,7 +96,7 @@ impl ExtendedImage {

pub(crate) fn color_type(&self) -> ColorType {
match &self.image {
ExtendedImageData::Animation { frames, .. } => &frames[0].image,
ExtendedImageData::Animation { first_frame, .. } => &first_frame.image,
ExtendedImageData::Static(image) => image,
}
.color_type()
Expand All @@ -112,14 +113,35 @@ impl ExtendedImage {
type Item = ImageResult<Frame>;

fn next(&mut self) -> Option<Self::Item> {
if let ExtendedImageData::Animation { frames, anim_info } = &self.image.image {
if let ExtendedImageData::Animation {
frames,
anim_info,
first_frame,
} = &self.image.image
{
let frame = frames.get(self.index);
match frame {
Some(anim_image) => {
Some(anim_frame_data) => {
let anim_frame;
let frame;

if self.index == 0 {
// Use already decoded first frame
anim_frame = first_frame;
} else {
frame = read_anim_frame(
&mut Cursor::new(anim_frame_data),
self.image.info.canvas_width,
self.image.info.canvas_height,
)
.ok()?;
anim_frame = &frame;
};

self.index += 1;
ExtendedImage::draw_subimage(
&mut self.canvas,
anim_image,
anim_frame,
anim_info.background_color,
)
}
Expand Down Expand Up @@ -154,7 +176,8 @@ impl ExtendedImage {
mut info: WebPExtendedInfo,
) -> ImageResult<ExtendedImage> {
let mut anim_info: Option<WebPAnimatedInfo> = None;
let mut anim_frames: Vec<AnimatedFrame> = Vec::new();
let mut anim_frames: Vec<Vec<u8>> = Vec::new();
let mut anim_first_frame: Option<AnimatedFrame> = None;
let mut static_frame: Option<WebPStatic> = None;
//go until end of file and while chunk headers are valid
while let Some((mut cursor, chunk)) = read_extended_chunk(reader)? {
Expand All @@ -168,8 +191,21 @@ impl ExtendedImage {
}
}
WebPRiffChunk::ANMF => {
let frame = read_anim_frame(cursor, info.canvas_width, info.canvas_height)?;
anim_frames.push(frame);
let mut frame_data = Vec::new();

// Store first frame decoded to avoid decoding it for certain function calls
if anim_first_frame.is_none() {
anim_first_frame = Some(read_anim_frame(
&mut cursor,
info.canvas_width,
info.canvas_height,
)?);

cursor.rewind().unwrap();
}

cursor.read_to_end(&mut frame_data)?;
anim_frames.push(frame_data);
}
WebPRiffChunk::ALPH => {
if static_frame.is_none() {
Expand Down Expand Up @@ -210,15 +246,11 @@ impl ExtendedImage {
}
}

let image = if let Some(info) = anim_info {
if anim_frames.is_empty() {
return Err(ImageError::IoError(Error::from(
io::ErrorKind::UnexpectedEof,
)));
}
let image = if let (Some(anim_info), Some(first_frame)) = (anim_info, anim_first_frame) {
ExtendedImageData::Animation {
frames: anim_frames,
anim_info: info,
first_frame,
anim_info,
}
} else if let Some(frame) = static_frame {
ExtendedImageData::Static(frame)
Expand Down Expand Up @@ -335,8 +367,11 @@ impl ExtendedImage {
pub(crate) fn fill_buf(&self, buf: &mut [u8]) {
match &self.image {
// will always have at least one frame
ExtendedImageData::Animation { frames, anim_info } => {
let first_frame = &frames[0];
ExtendedImageData::Animation {
anim_info,
first_frame,
..
} => {
let (canvas_width, canvas_height) = self.dimensions();
if canvas_width == first_frame.width && canvas_height == first_frame.height {
first_frame.image.fill_buf(buf);
Expand All @@ -361,7 +396,7 @@ impl ExtendedImage {
pub(crate) fn get_buf_size(&self) -> usize {
match &self.image {
// will always have at least one frame
ExtendedImageData::Animation { frames, .. } => &frames[0].image,
ExtendedImageData::Animation { first_frame, .. } => &first_frame.image,
ExtendedImageData::Static(image) => image,
}
.get_buf_size()
Expand All @@ -382,7 +417,7 @@ impl ExtendedImage {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
enum WebPStatic {
LossyWithAlpha(RgbaImage),
LossyWithoutAlpha(RgbImage),
Expand Down Expand Up @@ -552,7 +587,7 @@ struct WebPAnimatedInfo {
_loop_count: u16,
}

#[derive(Debug)]
#[derive(Debug, Clone)]
struct AnimatedFrame {
offset_x: u32,
offset_y: u32,
Expand Down Expand Up @@ -633,7 +668,7 @@ pub(crate) fn read_extended_header<R: Read>(reader: &mut R) -> ImageResult<WebPE
}

fn read_anim_frame<R: Read>(
mut reader: R,
mut reader: &mut R,
canvas_width: u32,
canvas_height: u32,
) -> ImageResult<AnimatedFrame> {
Expand Down

0 comments on commit ec857a1

Please sign in to comment.