decoder: Support animated formats (GIF, WEBP, APNG)

Closes #27
This commit is contained in:
Sophie Herold 2023-03-15 22:35:59 +01:00 • committed by Christopher Davis
parent ea9063de0f
commit f33c4bd470
9 changed files with 368 additions and 80 deletions

View file

@ -1,6 +1,6 @@
///! Decode using libheif-rs
use super::*;
use crate::decoder::tiling::{self, TilingStoreExt};
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::i18n::*;
use gtk::prelude::*;
@ -23,7 +23,7 @@ impl Heif {
pub fn new(
path: PathBuf,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::TilingStore>>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> Self {
log::debug!("Loading HEIF");
updater.spawn_error_handled(move || {

View file

@ -1,12 +1,13 @@
///! Decode using image-rs
use super::*;
use crate::decoder::tiling::{self, TilingStoreExt};
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::i18n::*;
use anyhow::Context;
use arc_swap::ArcSwap;
use gtk::prelude::*;
use image_rs::AnimationDecoder;
use std::fs::File;
use std::io::BufReader;
@ -14,7 +15,20 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug)]
pub struct ImageRsOther;
pub struct ImageRsOther {
thread_handle: Option<std::thread::JoinHandle<()>>,
abort: Arc<std::sync::RwLock<bool>>,
}
impl Drop for ImageRsOther {
fn drop(&mut self) {
if let Some(handle) = &self.thread_handle {
let mut abort = self.abort.write().unwrap();
*abort = true;
handle.thread().unpark();
}
}
}
impl ImageRsOther {
fn reader(
@ -30,8 +44,38 @@ impl ImageRsOther {
path: PathBuf,
format: image_rs::ImageFormat,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::TilingStore>>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> Self {
let abort: Arc<std::sync::RwLock<bool>> = Default::default();
let thread_handle = match format {
image_rs::ImageFormat::Gif
| image_rs::ImageFormat::WebP
| image_rs::ImageFormat::Png => Some(Self::new_animated(
path,
format,
updater,
tiles,
abort.clone(),
)),
_ => {
Self::new_static(path, format, updater, tiles);
None
}
};
ImageRsOther {
thread_handle,
abort,
}
}
fn new_static(
path: PathBuf,
format: image_rs::ImageFormat,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) {
updater.spawn_error_handled(move || {
let reader = Self::reader(&path, format)?;
let dimensions = reader
@ -42,7 +86,6 @@ impl ImageRsOther {
let mut reader = Self::reader(&path, format)?;
// TODO: Set something huge?
reader.limits(image_rs::io::Limits::no_limits());
let dynamic_image = reader.decode().context(i18n("Failed to decode image"))?;
@ -60,8 +103,109 @@ impl ImageRsOther {
Ok(())
});
}
ImageRsOther
fn new_animated(
path: PathBuf,
format: image_rs::ImageFormat,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
abort: Arc<std::sync::RwLock<bool>>,
) -> std::thread::JoinHandle<()> {
updater.clone().spawn_error_handled(move || {
let mut nth_frame = 1;
// We are currently decoding for each repetition of the animation
// TODO: Check if/how that can be solved differently
loop {
let file = std::fs::File::open(&path)?;
let (frames, animated_format) = match format {
image_rs::ImageFormat::Gif => (
image_rs::codecs::gif::GifDecoder::new(file)?.into_frames(),
ImageFormat::AnimatedGif,
),
image_rs::ImageFormat::WebP => (
image_rs::codecs::webp::WebPDecoder::new(file)?.into_frames(),
ImageFormat::AnimatedWebP,
),
image_rs::ImageFormat::Png => {
let decoder = image_rs::codecs::png::PngDecoder::new(file)?;
if decoder.is_apng() {
(decoder.apng().into_frames(), ImageFormat::AnimatedPng)
} else {
// Static PNG images need a different decoder
Self::new_static(path, format, updater, tiles);
return Ok(());
}
}
_ => todo!(),
};
for frame in frames {
match frame {
Err(err) => {
log::warn!("Skipping frame: {err}");
}
Ok(frame) => {
if nth_frame == 2 {
// We have at least two frames so this is animated
updater.send(DecoderUpdate::Format(animated_format));
nth_frame = 3;
}
let (delay_num, delay_den) = frame.delay().numer_denom_ms();
let delay = std::time::Duration::from_micros(f64::round(
delay_num as f64 * 1000. / delay_den as f64,
)
as u64);
let position = (frame.left(), frame.top());
let rgb_image = frame.into_buffer();
let dimensions = (
rgb_image.width() + position.0,
rgb_image.height() + position.1,
);
let dynamic_image = image_rs::DynamicImage::from(rgb_image);
let decoded_image = Decoded { dynamic_image };
let tile = tiling::Tile {
position,
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: decoded_image.into_texture(),
};
tiles.push_frame(tile, dimensions, delay);
if nth_frame == 1 {
updater.send(DecoderUpdate::Dimensions(Default::default()));
updater.send(DecoderUpdate::Redraw);
nth_frame = 2;
}
}
}
if tiles.n_frames() >= 3 {
std::thread::park();
}
if *abort.read().unwrap() {
log::debug!("Terminating decoder thread.");
return Ok(());
}
}
}
})
}
pub fn fill_frame_buffer(&self) {
if let Some(handle) = &self.thread_handle {
handle.thread().unpark();
} else {
log::error!("Trying to wake up one-shot decoder");
}
}
}

View file

@ -6,16 +6,29 @@ pub use heif::Heif;
pub use image_rs_other::ImageRsOther;
pub use svg::{Svg, RSVG_MAX_SIZE};
use super::{Decoder, UpdateSender};
use super::{Decoder, DecoderUpdate, UpdateSender};
use crate::i18n::*;
#[derive(Clone, Copy, Debug)]
pub enum ImageFormat {
ImageRs(image_rs::ImageFormat),
AnimatedGif,
AnimatedWebP,
AnimatedPng,
// TODO: Add details about contained format
Heif,
Svg,
}
impl ImageFormat {
pub fn is_animated(&self) -> bool {
matches!(
self,
ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng
)
}
}
impl std::fmt::Display for ImageFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
@ -28,6 +41,9 @@ impl std::fmt::Display for ImageFormat {
write!(f, "{}", debug)
}
}
Self::AnimatedGif => write!(f, "{}", i18n("Animated GIF")),
Self::AnimatedWebP => write!(f, "{}", i18n("Animated WebP")),
Self::AnimatedPng => write!(f, "{}", i18n("Animated PNG")),
Self::Heif => write!(f, "HEIF Container"),
Self::Svg => write!(f, "SVG"),
}

View file

@ -1,6 +1,6 @@
///! Decode using librsvg
use super::*;
use crate::decoder::tiling::{self, TilingStoreExt};
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::decoder::TileRequest;
use crate::deps::*;
@ -47,7 +47,7 @@ impl Svg {
pub fn new(
path: PathBuf,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::TilingStore>>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> Self {
let current_request: Arc<std::sync::RwLock<Request>> = Default::default();
let request_store = current_request.clone();

View file

@ -8,7 +8,7 @@ use crate::deps::*;
use crate::i18n::*;
use crate::image_metadata::ImageMetadata;
use formats::*;
use tiling::TilingStoreExt;
use tiling::FrameBufferExt;
use anyhow::{anyhow, bail, Context};
use arc_swap::ArcSwap;
@ -89,11 +89,11 @@ enum FormatDecoder {
impl Decoder {
/// Get new image decoder
///
/// The textures will be stored in the passed `TilingStore`.
/// The textures will be stored in the passed `TiledImage`.
/// The renderer should listen to updates from the returned receiver.
pub async fn new(
file: gio::File,
tiles: Arc<ArcSwap<tiling::TilingStore>>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> anyhow::Result<(Self, mpsc::UnboundedReceiver<DecoderUpdate>)> {
let path = file.path().context("Need a file path")?;
let (sender, receiver) = mpsc::unbounded();
@ -111,7 +111,7 @@ impl Decoder {
fn format_decoder(
update_sender: UpdateSender,
path: PathBuf,
tiles: Arc<ArcSwap<tiling::TilingStore>>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> anyhow::Result<FormatDecoder> {
update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path)));
@ -177,4 +177,12 @@ impl Decoder {
_ => {}
};
}
pub fn fill_frame_buffer(&self) {
if let FormatDecoder::ImageRsOther(decoder) = &self.decoder {
decoder.fill_frame_buffer();
} else {
log::error!("Trying to fill frame buffer for decoder without animation support");
}
}
}

View file

@ -1,7 +1,7 @@
///! Tiled renderer
///!
///! It is not always feasible or desiareble to store the complete decoded
///! image in the VRAM. [`TilingStore`] allows to compose the parts of
///! image in the VRAM. [`TiledImage`] allows to compose the parts of
///! the image currently viewed, out of smaller [`Tiles`].
///!
///! This is especially important for SVGs where every the image has to
@ -15,7 +15,9 @@ use arc_swap::ArcSwap;
use gtk::prelude::*;
use std::collections::BTreeMap;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
const ZOOM_SIGNIFICANT_DIGETS: i32 = 6;
pub const TILE_SIZE: u16 = 4000;
@ -77,18 +79,72 @@ impl Tile {
}
}
/// Buffers multiple images for animations
///
/// If the image is not animated, this only contains one image
#[derive(Clone, Debug, Default)]
pub struct FrameBuffer {
/// The first entry is always the currently shown frame
pub images: VecDeque<TiledImage>,
pub update_sender: Option<UpdateSender>,
}
impl FrameBuffer {
/// Returns mutable reference to the current image
///
/// This is a convenience functions for the case of non-animated images.
/// If no image exists yet, a new one is inserted an returned.
pub fn current(&mut self) -> &mut TiledImage {
if self.images.is_empty() {
self.images.push_back(TiledImage::default())
}
self.images.front_mut().unwrap()
}
pub fn original_dimensions(&self) -> Option<Coordinates> {
self.images.front()?.original_dimensions
}
pub fn set_update_sender(&mut self, sender: UpdateSender) {
self.update_sender = Some(sender);
}
/// Render the current image into this snapshot
pub fn add_to_snapshot(&self, snapshot: &gtk::Snapshot, zoom: f64, options: &RenderOptions) {
if let Some(tiling) = self.images.front() {
tiling.add_to_snapshot(snapshot, zoom, options);
} else {
log::error!("Trying to snapshot empty tiling queue");
}
}
pub fn cleanup(&mut self, zoom: f64, viewport: graphene::Rect) {
self.current().cleanup(zoom, viewport);
}
pub fn contains(&self, zoom: f64, coordinates: Coordinates) -> bool {
self.images
.front()
.map_or(false, |x| x.contains(zoom, coordinates))
}
}
#[derive(Clone, Debug)]
/// Store tiles for image
pub struct TilingStore {
///
/// This is the common representation for image textures.
/// I many cases there might only be one tile.
pub struct TiledImage {
pub tiles: BTreeMap<ZoomLevel, BTreeMap<Coordinates, Tile>>,
/// Tile size without bleed
pub tile_size: u16,
/// Complete image size
pub original_dimensions: Option<Coordinates>,
pub update_sender: Option<UpdateSender>,
/// Delay until to show this frame if animated
pub delay: std::time::Duration,
}
impl Default for TilingStore {
impl Default for TiledImage {
fn default() -> Self {
Self::new(TILE_SIZE)
}
@ -99,13 +155,13 @@ pub struct RenderOptions {
pub scaling_filter: gsk::ScalingFilter,
}
impl TilingStore {
impl TiledImage {
pub fn new(tile_size: u16) -> Self {
TilingStore {
TiledImage {
tiles: Default::default(),
tile_size,
original_dimensions: None,
update_sender: None,
delay: Default::default(),
}
}
@ -128,28 +184,6 @@ impl TilingStore {
pub fn push(&mut self, tile: Tile) {
let map = self.tiles.entry(tile.zoom_level).or_default();
map.insert(tile.coordinates(), tile);
if let Some(updater) = &self.update_sender {
updater.send(DecoderUpdate::Redraw);
}
}
pub fn set_original_dimensions(&mut self, dimensions: Coordinates) {
self.set_original_dimensions_full(dimensions, Default::default());
}
pub fn set_original_dimensions_full(
&mut self,
dimensions: Coordinates,
dimension_details: ImageDimensionDetails,
) {
self.original_dimensions = Some(dimensions);
if let Some(updater) = &self.update_sender {
updater.send(DecoderUpdate::Dimensions(dimension_details));
}
}
pub fn set_update_sender(&mut self, sender: UpdateSender) {
self.update_sender = Some(sender);
}
pub fn contains(&self, zoom: f64, coordinates: Coordinates) -> bool {
@ -340,8 +374,13 @@ impl RectExt for graphene::Rect {
}
}
pub trait TilingStoreExt {
pub trait FrameBufferExt {
fn push(&self, tile: Tile);
fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration);
/// Return true if the next frame should be shown and removes the outdated frame
fn frame_timeout(&self, elapsed: Duration) -> bool;
/// Returns the number of currently buffered frames
fn n_frames(&self) -> usize;
fn set_original_dimensions(&self, size: Coordinates);
fn set_original_dimensions_full(
&self,
@ -351,21 +390,52 @@ pub trait TilingStoreExt {
fn set_update_sender(&self, sender: UpdateSender);
}
impl TilingStoreExt for ArcSwap<TilingStore> {
impl FrameBufferExt for ArcSwap<FrameBuffer> {
fn push(&self, tile: Tile) {
self.rcu(|tiling_store| {
let mut new_store = (**tiling_store).clone();
new_store.push(tile.clone());
new_store.current().push(tile.clone());
Arc::new(new_store)
});
if let Some(updater) = &self.load().update_sender {
updater.send(DecoderUpdate::Redraw);
}
}
fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration) {
let mut store = TiledImage::default();
store.push(tile);
store.original_dimensions = Some(dimensions);
store.delay = delay;
self.rcu(|tiling_store| {
let mut new_store = (**tiling_store).clone();
new_store.images.push_back(store.clone());
Arc::new(new_store)
});
}
fn frame_timeout(&self, elapsed: Duration) -> bool {
if let Some(next_frame) = self.load().images.get(1) {
if elapsed >= next_frame.delay {
self.rcu(|tiling_store| {
let mut new_store = (**tiling_store).clone();
new_store.images.pop_front();
Arc::new(new_store)
});
return true;
}
}
false
}
fn n_frames(&self) -> usize {
self.load().images.len()
}
fn set_original_dimensions(&self, size: Coordinates) {
self.rcu(|tiling_store| {
let mut new_store = (**tiling_store).clone();
new_store.set_original_dimensions(size);
Arc::new(new_store)
});
self.set_original_dimensions_full(size, Default::default());
}
fn set_original_dimensions_full(
@ -375,9 +445,13 @@ impl TilingStoreExt for ArcSwap<TilingStore> {
) {
self.rcu(|tiling_store| {
let mut new_store = (**tiling_store).clone();
new_store.set_original_dimensions_full(size, dimension_details.clone());
new_store.current().original_dimensions = Some(size);
Arc::new(new_store)
});
if let Some(updater) = &self.load().update_sender {
updater.send(DecoderUpdate::Dimensions(dimension_details));
}
}
fn set_update_sender(&self, sender: UpdateSender) {
@ -395,7 +469,7 @@ mod test {
#[test]
fn test_tiling_cleanup() {
let mut store = TilingStore::new(2);
let mut store = TiledImage::new(2);
let toplevel = Tile {
position: (0, 0),
@ -468,7 +542,7 @@ mod test {
#[test]
fn test_tiling_cleanup_simple() {
let mut store = TilingStore::new(2);
let mut store = TiledImage::new(2);
let toplevel = Tile {
position: (0, 0),