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

@ -29,6 +29,7 @@
use crate::deps::*;
use crate::decoder::tiling::FrameBufferExt;
use crate::decoder::{self, tiling, Decoder, DecoderUpdate};
use crate::i18n::i18n;
use crate::image_metadata::LpImageMetadata;
@ -84,7 +85,7 @@ mod imp {
pub(super) is_deleted: Cell<bool>,
/// Track changes to this image
pub(super) file_monitor: RefCell<Option<gio::FileMonitor>>,
pub(super) tiles: Arc<ArcSwap<tiling::TilingStore>>,
pub(super) frame_buffer: Arc<ArcSwap<tiling::FrameBuffer>>,
pub(super) decoder: RefCell<Option<Arc<Decoder>>>,
pub(super) format: RefCell<Option<decoder::ImageFormat>>,
@ -139,6 +140,9 @@ mod imp {
/// Required for calculating delta while moving window around
pub(super) last_drag_value: Cell<Option<(f64, f64)>>,
pub(super) tick_callback: RefCell<Option<gtk::TickCallbackId>>,
pub(super) last_animated_frame: Cell<i64>,
widget_dimensions: Cell<(i32, i32)>,
}
@ -549,7 +553,7 @@ mod imp {
widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored());
// Add texture(s)
self.tiles
self.frame_buffer
.load()
.add_to_snapshot(snapshot, applicable_zoom, &render_options);
@ -655,7 +659,7 @@ impl LpImage {
log::debug!("Loading file {path:?}");
let tiles = &self.imp().tiles;
let tiles = &self.imp().frame_buffer;
let (decoder, mut decoder_update) = match Decoder::new(file.clone(), tiles.clone()).await {
Ok(x) => x,
@ -705,7 +709,7 @@ impl LpImage {
}
self.queue_draw();
imp.tiles.rcu(|tiles| {
imp.frame_buffer.rcu(|tiles| {
let mut new_tiles = (**tiles).clone();
// TODO: Use an area larger than the viewport
new_tiles.cleanup(self.imp().zoom_target.get(), self.viewport());
@ -718,10 +722,43 @@ impl LpImage {
DecoderUpdate::Format(format) => {
imp.format.replace(Some(format));
self.notify("format-name");
// Do the animation part for animated formats
if format.is_animated() {
let callback_id = self
.add_tick_callback(glib::clone!(@weak self as obj => @default-return glib::Continue(false), move |_, clock| obj.tick_callback(clock)));
imp.tick_callback.replace(Some(callback_id));
}
}
}
}
/// Roughly called for every frame if image is visible
///
/// We handle advancing to the next frame for animated GIFs etc here.
fn tick_callback(&self, clock: &gdk::FrameClock) -> glib::Continue {
// Do not animate if not visible
if !self.is_mapped() {
return glib::Continue(true);
}
let elapsed = clock.frame_time() - self.imp().last_animated_frame.get();
let duration = std::time::Duration::from_micros(elapsed as u64);
// Check if it's time to show the next frame
if self.imp().frame_buffer.frame_timeout(duration) {
// Just draw since frame_timeout updated to new frame
self.queue_draw();
self.imp().last_animated_frame.set(clock.frame_time());
if let Some(decoder) = self.imp().decoder.borrow().as_ref() {
// Decode new frame and load it into buffer
decoder.fill_frame_buffer();
}
}
glib::Continue(true)
}
pub fn is_loaded(&self) -> bool {
self.imp().is_loaded.get()
}
@ -851,7 +888,7 @@ impl LpImage {
);
self.imp()
.tiles
.frame_buffer
.load()
.add_to_snapshot(&snapshot, scale as f64, &render_options);
@ -1222,7 +1259,7 @@ impl LpImage {
}
fn original_dimensions(&self) -> (i32, i32) {
if let Some((width, height)) = self.imp().tiles.load().original_dimensions {
if let Some((width, height)) = self.imp().frame_buffer.load().original_dimensions() {
(width as i32, height as i32)
} else {
(0, 0)

View file

@ -1,8 +1,6 @@
//! A sliding view widget for images
//!
//! This widget it similar to `AdwCarousel`.
//! Currently, there is no swipe-gesture support since it's not working with
//! `GtkScrolledWindow` anyways.
use crate::deps::*;
use crate::widgets::LpImagePage;
@ -152,15 +150,26 @@ mod imp {
let position_shift = self.position_shift.get() as f32;
for (page_index, page) in self.pages.borrow().iter().enumerate() {
let page_position = page_index as f32;
// reverse page order for RTL languages
let direction_sign = if self.is_rtl() { -1. } else { 1. };
// This positions the pages within the carousel and shifts them
// according to the position that should currently be shown.
let x = direction_sign
* (page_index as f32 - scroll_position - position_shift)
* (page_position - scroll_position - position_shift)
* (width as f32 + self.page_spacing(width));
// Only show visible images
if page_position == (scroll_position + position_shift).floor()
|| page_position == (scroll_position + position_shift).ceil()
{
page.set_visible(true);
} else {
page.set_visible(false);
}
let transform = gsk::Transform::new().translate(&graphene::Point::new(x, 0.));
page.allocate(width, height, 0, Some(transform));
page.image().allocate(width, height, 0, None);