parent
ea9063de0f
commit
f33c4bd470
9 changed files with 368 additions and 80 deletions
34
README.md
34
README.md
|
|
@ -17,23 +17,23 @@ Bundles will not be automatically updated after installation.
|
||||||
|
|
||||||
## Supported Formats
|
## Supported Formats
|
||||||
|
|
||||||
| Format | ICC | CICP | EXIF | XMP | Animation | Library |
|
| Format | View | Optional | ICC | CICP | EXIF | XMP | Animation | Library |
|
||||||
|----------|-----|------|------|-----|-----------|-------------------------------|
|
|-----------|------|----------|-----|------|------|-----|-----------|----------------------------|
|
||||||
| JPEG | ✔ | — | ✔ | ✘ | — | image-rs |
|
| AVIF | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) |
|
||||||
| PNG | ✔ | ✘ | ✔ | ✘ | ✘ | image-rs |
|
| BMP | ✔ | | ✘ | — | — | — | — | image-rs |
|
||||||
| GIF | ✘ * | — | — | ✘ | ✘ | image-rs |
|
| DDS | ✔ | | — | — | — | — | — | image-rs |
|
||||||
| WEBP | — | — | ✔ | ✘ | ✘ | image-rs |
|
| farbfeld | ✔ | | — | — | — | — | — | image-rs |
|
||||||
| AVIF | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) |
|
| GIF | ✔ | | ✘ * | — | — | ✘ | ✔ | image-rs |
|
||||||
| HEIC | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) ** |
|
| HEIC | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) |
|
||||||
| TIFF | ✘ | — | ✔ | ✘ | — | image-rs |
|
| ICO | ✔ | | — | — | — | — | — | image-rs |
|
||||||
| SVG | ✘ | — | — | ✘ * | — | librsvg |
|
| JPEG | ✔ | | ✔ | — | ✔ | ✘ | — | image-rs |
|
||||||
| BMP | ✘ | — | — | — | — | image-rs |
|
| OpenEXR | ✔ | | — | — | — | — | — | image-rs |
|
||||||
| ICO | — | — | — | — | — | image-rs |
|
| PNG | ✔ | | ✔ | ✘ | ✔ | ✘ | ✔ | image-rs |
|
||||||
| PNM | — | — | — | — | — | image-rs |
|
| PNM | ✔ | | — | — | — | — | — | image-rs |
|
||||||
| DDS | — | — | — | — | — | image-rs |
|
| SVG | ✔ | | ✘ | — | — | ✘ * | — | librsvg |
|
||||||
| TGA | — | — | — | — | — | image-rs |
|
| TGA | ✔ | | — | — | — | — | — | image-rs |
|
||||||
| OpenEXR | — | — | — | — | — | image-rs |
|
| TIFF | ✔ | | ✘ | — | ✔ | ✘ | — | image-rs |
|
||||||
| farbfeld | — | — | — | — | — | image-rs |
|
| WEBP | ✔ | | — | — | ✔ | ✘ | ✔ | image-rs |
|
||||||
|
|
||||||
| Symbol | Meaning |
|
| Symbol | Meaning |
|
||||||
|--------|------------------------------------------------|
|
|--------|------------------------------------------------|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
///! Decode using libheif-rs
|
///! Decode using libheif-rs
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, TilingStoreExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
@ -23,7 +23,7 @@ impl Heif {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
updater: UpdateSender,
|
updater: UpdateSender,
|
||||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
log::debug!("Loading HEIF");
|
log::debug!("Loading HEIF");
|
||||||
updater.spawn_error_handled(move || {
|
updater.spawn_error_handled(move || {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
///! Decode using image-rs
|
///! Decode using image-rs
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, TilingStoreExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
use image_rs::AnimationDecoder;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
|
@ -14,7 +15,20 @@ use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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 {
|
impl ImageRsOther {
|
||||||
fn reader(
|
fn reader(
|
||||||
|
|
@ -30,8 +44,38 @@ impl ImageRsOther {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
format: image_rs::ImageFormat,
|
format: image_rs::ImageFormat,
|
||||||
updater: UpdateSender,
|
updater: UpdateSender,
|
||||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> Self {
|
) -> 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 || {
|
updater.spawn_error_handled(move || {
|
||||||
let reader = Self::reader(&path, format)?;
|
let reader = Self::reader(&path, format)?;
|
||||||
let dimensions = reader
|
let dimensions = reader
|
||||||
|
|
@ -42,7 +86,6 @@ impl ImageRsOther {
|
||||||
|
|
||||||
let mut reader = Self::reader(&path, format)?;
|
let mut reader = Self::reader(&path, format)?;
|
||||||
|
|
||||||
// TODO: Set something huge?
|
|
||||||
reader.limits(image_rs::io::Limits::no_limits());
|
reader.limits(image_rs::io::Limits::no_limits());
|
||||||
|
|
||||||
let dynamic_image = reader.decode().context(i18n("Failed to decode image"))?;
|
let dynamic_image = reader.decode().context(i18n("Failed to decode image"))?;
|
||||||
|
|
@ -60,8 +103,109 @@ impl ImageRsOther {
|
||||||
|
|
||||||
Ok(())
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,29 @@ pub use heif::Heif;
|
||||||
pub use image_rs_other::ImageRsOther;
|
pub use image_rs_other::ImageRsOther;
|
||||||
pub use svg::{Svg, RSVG_MAX_SIZE};
|
pub use svg::{Svg, RSVG_MAX_SIZE};
|
||||||
|
|
||||||
use super::{Decoder, UpdateSender};
|
use super::{Decoder, DecoderUpdate, UpdateSender};
|
||||||
|
use crate::i18n::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum ImageFormat {
|
pub enum ImageFormat {
|
||||||
ImageRs(image_rs::ImageFormat),
|
ImageRs(image_rs::ImageFormat),
|
||||||
|
AnimatedGif,
|
||||||
|
AnimatedWebP,
|
||||||
|
AnimatedPng,
|
||||||
// TODO: Add details about contained format
|
// TODO: Add details about contained format
|
||||||
Heif,
|
Heif,
|
||||||
Svg,
|
Svg,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ImageFormat {
|
||||||
|
pub fn is_animated(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ImageFormat {
|
impl std::fmt::Display for ImageFormat {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -28,6 +41,9 @@ impl std::fmt::Display for ImageFormat {
|
||||||
write!(f, "{}", debug)
|
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::Heif => write!(f, "HEIF Container"),
|
||||||
Self::Svg => write!(f, "SVG"),
|
Self::Svg => write!(f, "SVG"),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
///! Decode using librsvg
|
///! Decode using librsvg
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, TilingStoreExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::decoder::TileRequest;
|
use crate::decoder::TileRequest;
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ impl Svg {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
updater: UpdateSender,
|
updater: UpdateSender,
|
||||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let current_request: Arc<std::sync::RwLock<Request>> = Default::default();
|
let current_request: Arc<std::sync::RwLock<Request>> = Default::default();
|
||||||
let request_store = current_request.clone();
|
let request_store = current_request.clone();
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::deps::*;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
use crate::image_metadata::ImageMetadata;
|
use crate::image_metadata::ImageMetadata;
|
||||||
use formats::*;
|
use formats::*;
|
||||||
use tiling::TilingStoreExt;
|
use tiling::FrameBufferExt;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
|
|
@ -89,11 +89,11 @@ enum FormatDecoder {
|
||||||
impl Decoder {
|
impl Decoder {
|
||||||
/// Get new image 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.
|
/// The renderer should listen to updates from the returned receiver.
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
file: gio::File,
|
file: gio::File,
|
||||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> anyhow::Result<(Self, mpsc::UnboundedReceiver<DecoderUpdate>)> {
|
) -> anyhow::Result<(Self, mpsc::UnboundedReceiver<DecoderUpdate>)> {
|
||||||
let path = file.path().context("Need a file path")?;
|
let path = file.path().context("Need a file path")?;
|
||||||
let (sender, receiver) = mpsc::unbounded();
|
let (sender, receiver) = mpsc::unbounded();
|
||||||
|
|
@ -111,7 +111,7 @@ impl Decoder {
|
||||||
fn format_decoder(
|
fn format_decoder(
|
||||||
update_sender: UpdateSender,
|
update_sender: UpdateSender,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> anyhow::Result<FormatDecoder> {
|
) -> anyhow::Result<FormatDecoder> {
|
||||||
update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path)));
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
///! Tiled renderer
|
///! Tiled renderer
|
||||||
///!
|
///!
|
||||||
///! It is not always feasible or desiareble to store the complete decoded
|
///! 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`].
|
///! the image currently viewed, out of smaller [`Tiles`].
|
||||||
///!
|
///!
|
||||||
///! This is especially important for SVGs where every the image has to
|
///! This is especially important for SVGs where every the image has to
|
||||||
|
|
@ -15,7 +15,9 @@ use arc_swap::ArcSwap;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
const ZOOM_SIGNIFICANT_DIGETS: i32 = 6;
|
const ZOOM_SIGNIFICANT_DIGETS: i32 = 6;
|
||||||
pub const TILE_SIZE: u16 = 4000;
|
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: >k::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)]
|
#[derive(Clone, Debug)]
|
||||||
/// Store tiles for image
|
/// 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>>,
|
pub tiles: BTreeMap<ZoomLevel, BTreeMap<Coordinates, Tile>>,
|
||||||
/// Tile size without bleed
|
/// Tile size without bleed
|
||||||
pub tile_size: u16,
|
pub tile_size: u16,
|
||||||
/// Complete image size
|
/// Complete image size
|
||||||
pub original_dimensions: Option<Coordinates>,
|
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 {
|
fn default() -> Self {
|
||||||
Self::new(TILE_SIZE)
|
Self::new(TILE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
@ -99,13 +155,13 @@ pub struct RenderOptions {
|
||||||
pub scaling_filter: gsk::ScalingFilter,
|
pub scaling_filter: gsk::ScalingFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TilingStore {
|
impl TiledImage {
|
||||||
pub fn new(tile_size: u16) -> Self {
|
pub fn new(tile_size: u16) -> Self {
|
||||||
TilingStore {
|
TiledImage {
|
||||||
tiles: Default::default(),
|
tiles: Default::default(),
|
||||||
tile_size,
|
tile_size,
|
||||||
original_dimensions: None,
|
original_dimensions: None,
|
||||||
update_sender: None,
|
delay: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,28 +184,6 @@ impl TilingStore {
|
||||||
pub fn push(&mut self, tile: Tile) {
|
pub fn push(&mut self, tile: Tile) {
|
||||||
let map = self.tiles.entry(tile.zoom_level).or_default();
|
let map = self.tiles.entry(tile.zoom_level).or_default();
|
||||||
map.insert(tile.coordinates(), tile);
|
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 {
|
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(&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(&self, size: Coordinates);
|
||||||
fn set_original_dimensions_full(
|
fn set_original_dimensions_full(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -351,21 +390,52 @@ pub trait TilingStoreExt {
|
||||||
fn set_update_sender(&self, sender: UpdateSender);
|
fn set_update_sender(&self, sender: UpdateSender);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TilingStoreExt for ArcSwap<TilingStore> {
|
impl FrameBufferExt for ArcSwap<FrameBuffer> {
|
||||||
fn push(&self, tile: Tile) {
|
fn push(&self, tile: Tile) {
|
||||||
self.rcu(|tiling_store| {
|
self.rcu(|tiling_store| {
|
||||||
let mut new_store = (**tiling_store).clone();
|
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)
|
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) {
|
fn set_original_dimensions(&self, size: Coordinates) {
|
||||||
self.rcu(|tiling_store| {
|
self.set_original_dimensions_full(size, Default::default());
|
||||||
let mut new_store = (**tiling_store).clone();
|
|
||||||
new_store.set_original_dimensions(size);
|
|
||||||
Arc::new(new_store)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_original_dimensions_full(
|
fn set_original_dimensions_full(
|
||||||
|
|
@ -375,9 +445,13 @@ impl TilingStoreExt for ArcSwap<TilingStore> {
|
||||||
) {
|
) {
|
||||||
self.rcu(|tiling_store| {
|
self.rcu(|tiling_store| {
|
||||||
let mut new_store = (**tiling_store).clone();
|
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)
|
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) {
|
fn set_update_sender(&self, sender: UpdateSender) {
|
||||||
|
|
@ -395,7 +469,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tiling_cleanup() {
|
fn test_tiling_cleanup() {
|
||||||
let mut store = TilingStore::new(2);
|
let mut store = TiledImage::new(2);
|
||||||
|
|
||||||
let toplevel = Tile {
|
let toplevel = Tile {
|
||||||
position: (0, 0),
|
position: (0, 0),
|
||||||
|
|
@ -468,7 +542,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tiling_cleanup_simple() {
|
fn test_tiling_cleanup_simple() {
|
||||||
let mut store = TilingStore::new(2);
|
let mut store = TiledImage::new(2);
|
||||||
|
|
||||||
let toplevel = Tile {
|
let toplevel = Tile {
|
||||||
position: (0, 0),
|
position: (0, 0),
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
|
|
||||||
|
use crate::decoder::tiling::FrameBufferExt;
|
||||||
use crate::decoder::{self, tiling, Decoder, DecoderUpdate};
|
use crate::decoder::{self, tiling, Decoder, DecoderUpdate};
|
||||||
use crate::i18n::i18n;
|
use crate::i18n::i18n;
|
||||||
use crate::image_metadata::LpImageMetadata;
|
use crate::image_metadata::LpImageMetadata;
|
||||||
|
|
@ -84,7 +85,7 @@ mod imp {
|
||||||
pub(super) is_deleted: Cell<bool>,
|
pub(super) is_deleted: Cell<bool>,
|
||||||
/// Track changes to this image
|
/// Track changes to this image
|
||||||
pub(super) file_monitor: RefCell<Option<gio::FileMonitor>>,
|
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) decoder: RefCell<Option<Arc<Decoder>>>,
|
||||||
pub(super) format: RefCell<Option<decoder::ImageFormat>>,
|
pub(super) format: RefCell<Option<decoder::ImageFormat>>,
|
||||||
|
|
||||||
|
|
@ -139,6 +140,9 @@ mod imp {
|
||||||
/// Required for calculating delta while moving window around
|
/// Required for calculating delta while moving window around
|
||||||
pub(super) last_drag_value: Cell<Option<(f64, f64)>>,
|
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)>,
|
widget_dimensions: Cell<(i32, i32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,7 +553,7 @@ mod imp {
|
||||||
widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored());
|
widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored());
|
||||||
|
|
||||||
// Add texture(s)
|
// Add texture(s)
|
||||||
self.tiles
|
self.frame_buffer
|
||||||
.load()
|
.load()
|
||||||
.add_to_snapshot(snapshot, applicable_zoom, &render_options);
|
.add_to_snapshot(snapshot, applicable_zoom, &render_options);
|
||||||
|
|
||||||
|
|
@ -655,7 +659,7 @@ impl LpImage {
|
||||||
|
|
||||||
log::debug!("Loading file {path:?}");
|
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 {
|
let (decoder, mut decoder_update) = match Decoder::new(file.clone(), tiles.clone()).await {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
|
|
@ -705,7 +709,7 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.queue_draw();
|
self.queue_draw();
|
||||||
imp.tiles.rcu(|tiles| {
|
imp.frame_buffer.rcu(|tiles| {
|
||||||
let mut new_tiles = (**tiles).clone();
|
let mut new_tiles = (**tiles).clone();
|
||||||
// TODO: Use an area larger than the viewport
|
// TODO: Use an area larger than the viewport
|
||||||
new_tiles.cleanup(self.imp().zoom_target.get(), self.viewport());
|
new_tiles.cleanup(self.imp().zoom_target.get(), self.viewport());
|
||||||
|
|
@ -718,10 +722,43 @@ impl LpImage {
|
||||||
DecoderUpdate::Format(format) => {
|
DecoderUpdate::Format(format) => {
|
||||||
imp.format.replace(Some(format));
|
imp.format.replace(Some(format));
|
||||||
self.notify("format-name");
|
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 {
|
pub fn is_loaded(&self) -> bool {
|
||||||
self.imp().is_loaded.get()
|
self.imp().is_loaded.get()
|
||||||
}
|
}
|
||||||
|
|
@ -851,7 +888,7 @@ impl LpImage {
|
||||||
);
|
);
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.tiles
|
.frame_buffer
|
||||||
.load()
|
.load()
|
||||||
.add_to_snapshot(&snapshot, scale as f64, &render_options);
|
.add_to_snapshot(&snapshot, scale as f64, &render_options);
|
||||||
|
|
||||||
|
|
@ -1222,7 +1259,7 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn original_dimensions(&self) -> (i32, i32) {
|
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)
|
(width as i32, height as i32)
|
||||||
} else {
|
} else {
|
||||||
(0, 0)
|
(0, 0)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
//! A sliding view widget for images
|
//! A sliding view widget for images
|
||||||
//!
|
//!
|
||||||
//! This widget it similar to `AdwCarousel`.
|
//! 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::deps::*;
|
||||||
use crate::widgets::LpImagePage;
|
use crate::widgets::LpImagePage;
|
||||||
|
|
@ -152,15 +150,26 @@ mod imp {
|
||||||
let position_shift = self.position_shift.get() as f32;
|
let position_shift = self.position_shift.get() as f32;
|
||||||
|
|
||||||
for (page_index, page) in self.pages.borrow().iter().enumerate() {
|
for (page_index, page) in self.pages.borrow().iter().enumerate() {
|
||||||
|
let page_position = page_index as f32;
|
||||||
|
|
||||||
// reverse page order for RTL languages
|
// reverse page order for RTL languages
|
||||||
let direction_sign = if self.is_rtl() { -1. } else { 1. };
|
let direction_sign = if self.is_rtl() { -1. } else { 1. };
|
||||||
|
|
||||||
// This positions the pages within the carousel and shifts them
|
// This positions the pages within the carousel and shifts them
|
||||||
// according to the position that should currently be shown.
|
// according to the position that should currently be shown.
|
||||||
let x = direction_sign
|
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));
|
* (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.));
|
let transform = gsk::Transform::new().translate(&graphene::Point::new(x, 0.));
|
||||||
page.allocate(width, height, 0, Some(transform));
|
page.allocate(width, height, 0, Some(transform));
|
||||||
page.image().allocate(width, height, 0, None);
|
page.image().allocate(width, height, 0, None);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue