From fad8bb793992502dc81f7ea00c3a2183d5109c6c Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Wed, 12 Apr 2023 04:13:30 +0200 Subject: [PATCH] decoder: Use GFile instead of PathBuf This allows to show images from locations that don't have a representation with path. This also works around not being able to read files in trash:/// Closes #56 Part-of: --- data/gtk/window.ui | 2 +- src/application.rs | 2 +- src/decoder/formats/heif.rs | 20 +++++++--- src/decoder/formats/image_rs_other.rs | 38 +++++++++---------- src/decoder/formats/svg.rs | 7 ++-- src/decoder/mod.rs | 54 ++++++++++++++++----------- src/file_model.rs | 16 ++++---- src/image_metadata/mod.rs | 14 +++---- src/image_metadata/obj.rs | 5 +-- src/util/mod.rs | 19 ++++++++++ src/widgets/image.rs | 25 ++----------- src/widgets/image_page.rs | 4 +- src/widgets/image_view.rs | 15 ++++---- src/widgets/sliding_view.rs | 1 - src/window.rs | 4 +- 15 files changed, 119 insertions(+), 107 deletions(-) diff --git a/data/gtk/window.ui b/data/gtk/window.ui index f172102..7e42abc 100644 --- a/data/gtk/window.ui +++ b/data/gtk/window.ui @@ -59,7 +59,7 @@ Loupe - + image_view diff --git a/src/application.rs b/src/application.rs index 3624dbc..f64d93a 100644 --- a/src/application.rs +++ b/src/application.rs @@ -73,7 +73,7 @@ mod imp { let application = self.obj(); for file in files { let win = LpWindow::new(&*application); - win.set_image_from_file(&file); + win.set_image_from_file(file); } } } diff --git a/src/decoder/formats/heif.rs b/src/decoder/formats/heif.rs index eca2d86..d9bea2f 100644 --- a/src/decoder/formats/heif.rs +++ b/src/decoder/formats/heif.rs @@ -3,14 +3,14 @@ use super::*; use crate::decoder::tiling::{self, FrameBufferExt}; use crate::deps::*; use crate::util::gettext::*; -use gtk::prelude::*; +use crate::util::ToBufRead; use anyhow::Context; use arc_swap::ArcSwap; +use gtk::prelude::*; use libheif_rs::{ColorSpace, HeifContext, LibHeif, Plane, RgbChroma}; use once_cell::sync::Lazy; -use std::path::PathBuf; use std::sync::Arc; #[derive(Debug)] @@ -21,14 +21,24 @@ static LIBHEIF: Lazy = Lazy::new(LibHeif::new); impl Heif { pub fn new( - path: PathBuf, + file: gio::File, updater: UpdateSender, tiles: Arc>, ) -> Self { log::debug!("Loading HEIF"); updater.spawn_error_handled(move || { - let ctx = HeifContext::read_from_file(&path.display().to_string()) - .context(gettext("Failed to read image"))?; + let file_size = file + .query_info( + gio::FILE_ATTRIBUTE_STANDARD_SIZE, + gio::FileQueryInfoFlags::NONE, + gio::Cancellable::NONE, + ) + .context(gettext("Failed to read image file information"))? + .size(); + let buf_reader = file.to_buf_read()?; + let stream_reader = libheif_rs::StreamReader::new(buf_reader, file_size as u64); + let ctx = HeifContext::read_from_reader(Box::new(stream_reader)) + .context(gettext("Failed to decode image"))?; let handle = ctx.primary_image_handle()?; tiles.set_original_dimensions((handle.width(), handle.height())); diff --git a/src/decoder/formats/image_rs_other.rs b/src/decoder/formats/image_rs_other.rs index b81349f..5abe86d 100644 --- a/src/decoder/formats/image_rs_other.rs +++ b/src/decoder/formats/image_rs_other.rs @@ -3,15 +3,13 @@ use super::*; use crate::decoder::tiling::{self, FrameBufferExt}; use crate::deps::*; use crate::util::gettext::*; +use crate::util::{BufReadSeek, ToBufRead}; use anyhow::Context; use arc_swap::ArcSwap; use gtk::prelude::*; use image_rs::AnimationDecoder; -use std::fs::File; -use std::io::BufReader; -use std::path::{Path, PathBuf}; use std::sync::Arc; #[derive(Debug)] @@ -32,17 +30,17 @@ impl Drop for ImageRsOther { impl ImageRsOther { fn reader( - path: &Path, + file: &gio::File, format: image_rs::ImageFormat, - ) -> anyhow::Result>> { - let mut reader = - image_rs::io::Reader::open(path).context(gettext("Failed to open image"))?; + ) -> anyhow::Result>> { + let buf_read = file.to_buf_read()?; + let mut reader = image_rs::io::Reader::new(buf_read); reader.set_format(format); Ok(reader) } pub fn new( - path: PathBuf, + file: gio::File, format: image_rs::ImageFormat, updater: UpdateSender, tiles: Arc>, @@ -53,14 +51,14 @@ impl ImageRsOther { image_rs::ImageFormat::Gif | image_rs::ImageFormat::WebP | image_rs::ImageFormat::Png => Some(Self::new_animated( - path, + file, format, updater, tiles, abort.clone(), )), _ => { - Self::new_static(path, format, updater, tiles); + Self::new_static(file, format, updater, tiles); None } }; @@ -72,20 +70,20 @@ impl ImageRsOther { } fn new_static( - path: PathBuf, + file: gio::File, format: image_rs::ImageFormat, updater: UpdateSender, tiles: Arc>, ) { updater.spawn_error_handled(move || { - let reader = Self::reader(&path, format)?; + let reader = Self::reader(&file, format)?; let dimensions = reader .into_dimensions() .context("Failed to read image dimensions")?; tiles.set_original_dimensions(dimensions); - let mut reader = Self::reader(&path, format)?; + let mut reader = Self::reader(&file, format)?; reader.limits(image_rs::io::Limits::no_limits()); @@ -107,7 +105,7 @@ impl ImageRsOther { } fn new_animated( - path: PathBuf, + file: gio::File, format: image_rs::ImageFormat, updater: UpdateSender, tiles: Arc>, @@ -119,30 +117,30 @@ impl ImageRsOther { // 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 buf_reader = file.to_buf_read()?; let (frames, animated_format) = match format { image_rs::ImageFormat::Gif => ( - image_rs::codecs::gif::GifDecoder::new(file)?.into_frames(), + image_rs::codecs::gif::GifDecoder::new(buf_reader)?.into_frames(), ImageFormat::AnimatedGif, ), image_rs::ImageFormat::WebP => { - let decoder = image_rs::codecs::webp::WebPDecoder::new(file)?; + let decoder = image_rs::codecs::webp::WebPDecoder::new(buf_reader)?; if decoder.has_animation() { (decoder.into_frames(), ImageFormat::AnimatedWebP) } else { // Static WebP images need a different decoder - Self::new_static(path, format, updater, tiles); + Self::new_static(file, format, updater, tiles); return Ok(()); } } image_rs::ImageFormat::Png => { - let decoder = image_rs::codecs::png::PngDecoder::new(file)?; + let decoder = image_rs::codecs::png::PngDecoder::new(buf_reader)?; 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); + Self::new_static(file, format, updater, tiles); return Ok(()); } } diff --git a/src/decoder/formats/svg.rs b/src/decoder/formats/svg.rs index 9288b45..e11502c 100644 --- a/src/decoder/formats/svg.rs +++ b/src/decoder/formats/svg.rs @@ -8,7 +8,6 @@ use anyhow::Context; use arc_swap::ArcSwap; use gtk::prelude::*; -use std::path::PathBuf; use std::sync::Arc; /// Current librsvg limit on maximum dimensions. See @@ -45,7 +44,7 @@ impl Drop for Svg { impl Svg { pub fn new( - path: PathBuf, + file: gio::File, updater: UpdateSender, tiles: Arc>, ) -> Self { @@ -53,7 +52,7 @@ impl Svg { let request_store = current_request.clone(); let thread_handle = updater.spawn_error_handled(move || { - let handle = rsvg::Loader::new().read_path(path)?; + let handle = rsvg::Loader::new().read_file(&file, gio::Cancellable::NONE)?; let renderer = rsvg::CairoRenderer::new(&handle); let (original_width, original_height) = svg_dimensions(&renderer); @@ -85,7 +84,7 @@ impl Svg { tile_size: tiling::TILE_SIZE, original_dimensions: (original_width, original_height), zoom: tile_request.zoom, - bleed: 2, + bleed: 0, }; let relevant_tiles = tiling.relevant_tiles(&tile_request.viewport); diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8f9e23f..82fa480 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -7,16 +7,16 @@ pub use formats::ImageDimensionDetails; use crate::deps::*; use crate::image_metadata::ImageMetadata; use crate::util::gettext::*; +use crate::util::ToBufRead; use formats::*; use tiling::FrameBufferExt; -use anyhow::{anyhow, bail, Context}; +use anyhow::{anyhow, bail, Context, Result}; use arc_swap::ArcSwap; use futures::channel::mpsc; use gio::prelude::*; use std::io::Read; -use std::path::PathBuf; use std::sync::Arc; pub use formats::{ImageFormat, RSVG_MAX_SIZE}; @@ -95,13 +95,12 @@ impl Decoder { file: gio::File, tiles: Arc>, ) -> anyhow::Result<(Self, mpsc::UnboundedReceiver)> { - let path = file.path().context("Need a file path")?; let (sender, receiver) = mpsc::unbounded(); let update_sender = UpdateSender { sender }; tiles.set_update_sender(update_sender.clone()); - let decoder = gio::spawn_blocking(move || Self::format_decoder(update_sender, path, tiles)) + let decoder = gio::spawn_blocking(move || Self::format_decoder(update_sender, file, tiles)) .await .map_err(|_| anyhow!("Constructing the FormatDecoder failed unexpectedly"))??; @@ -110,31 +109,23 @@ impl Decoder { fn format_decoder( update_sender: UpdateSender, - path: PathBuf, + file: gio::File, tiles: Arc>, ) -> anyhow::Result { - update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path))); + update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&file))); - let mut buf = Vec::new(); - let file = std::fs::File::open(&path).context(gettext("Could not open image"))?; - file.take(64) - .read_to_end(&mut buf) - .context(gettext("Could not read image"))?; + let format = Self::guess_format(&file)?; - // Try magic bytes first and than file name extension - let format = - image_rs::guess_format(&buf).or_else(|_| image_rs::ImageFormat::from_path(&path)); - - if let Ok(format) = format { + if let Some(format) = format { match format { image_rs::ImageFormat::Avif => { update_sender.send(DecoderUpdate::Format(ImageFormat::Heif)); - return Ok(FormatDecoder::Heif(Heif::new(path, update_sender, tiles))); + return Ok(FormatDecoder::Heif(Heif::new(file, update_sender, tiles))); } format => { update_sender.send(DecoderUpdate::Format(ImageFormat::ImageRs(format))); return Ok(FormatDecoder::ImageRsOther(ImageRsOther::new( - path, + file, format, update_sender, tiles, @@ -142,7 +133,7 @@ impl Decoder { } } } else { - let file_info = gio::File::for_path(&path) + let file_info = file .query_info( gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, gio::FileQueryInfoFlags::NONE, @@ -159,12 +150,12 @@ impl Decoder { // - image/svg+xml-compressed if content_type.split('+').next() == Some("image/svg") { update_sender.send(DecoderUpdate::Format(ImageFormat::Svg)); - return Ok(FormatDecoder::Svg(Svg::new(path, update_sender, tiles))); + return Ok(FormatDecoder::Svg(Svg::new(file, update_sender, tiles))); } else if ["image/avif", "image/heif", "image/heic"] .contains(&content_type.as_str()) { update_sender.send(DecoderUpdate::Format(ImageFormat::Heif)); - return Ok(FormatDecoder::Heif(Heif::new(path, update_sender, tiles))); + return Ok(FormatDecoder::Heif(Heif::new(file, update_sender, tiles))); } else { bail!(gettext_f( "Unknown image format: {}", @@ -193,4 +184,25 @@ impl Decoder { log::error!("Trying to fill frame buffer for decoder without animation support"); } } + + fn guess_format(file: &gio::File) -> anyhow::Result> { + let mut buf = Vec::new(); + let fs_file = file + .to_buf_read() + .context(gettext("Could not open image"))?; + fs_file + .take(64) + .read_to_end(&mut buf) + .context(gettext("Could not read image"))?; + + // Try magic bytes first and than file name extension + + if let Ok(format) = image_rs::guess_format(&buf) { + Ok(Some(format)) + } else if let Some(basename) = file.basename() { + Ok(image_rs::ImageFormat::from_path(basename).ok()) + } else { + Ok(None) + } + } } diff --git a/src/file_model.rs b/src/file_model.rs index d770c97..57b083a 100644 --- a/src/file_model.rs +++ b/src/file_model.rs @@ -25,7 +25,7 @@ use anyhow::Context; use gio::prelude::*; use gio::subclass::prelude::*; use glib::GString; -use indexmap::{IndexMap, IndexSet}; +use indexmap::IndexMap; use once_cell::sync::{Lazy, OnceCell}; use std::cell::RefCell; @@ -35,8 +35,8 @@ mod imp { #[derive(Debug, Default)] pub struct LpFileModel { - /// Use and IndexSet such that we can put the elements into an arbitrary order - /// while still having fast hash access via `PathBuf`. + /// Use and IndexMap such that we can put the elements into an arbitrary order + /// while still having fast hash access via file URI. pub(super) files: RefCell>, pub(super) directory: OnceCell, /// Track file changes. @@ -167,7 +167,7 @@ impl LpFileModel { } pub fn before(&self, file: &gio::File) -> Option { - let index = self.index_of(&file)?; + let index = self.index_of(file)?; self.imp() .files @@ -188,7 +188,7 @@ impl LpFileModel { .cloned() } - /// Returns `n` elements before and after given path + /// Returns `n` elements before and after given file pub fn files_around(&self, file: &gio::File, n: usize) -> IndexMap { let Some(index) = self.index_of(file) else { log::error!("URI not in model: {}", file.uri()); @@ -207,12 +207,12 @@ impl LpFileModel { .collect() } - /// Return first path + /// Return first file pub fn first(&self) -> Option { self.imp().files.borrow().first().map(|x| x.1).cloned() } - /// Returns last path + /// Returns last file pub fn last(&self) -> Option { self.imp().files.borrow().last().map(|x| x.1).cloned() } @@ -232,7 +232,7 @@ impl LpFileModel { fn is_image_file(file: &gio::File) -> bool { let Ok(info) = util::query_attributes(file, vec![gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE]) else { - log::warn!("Could not query file info: {:?}", file.path()); + log::warn!("Could not query file info: {}", file.uri()); return false; }; diff --git a/src/image_metadata/mod.rs b/src/image_metadata/mod.rs index 983ca09..b175c3f 100644 --- a/src/image_metadata/mod.rs +++ b/src/image_metadata/mod.rs @@ -9,12 +9,11 @@ mod obj; pub use gps::GPSLocation; pub use obj::LpImageMetadata; -use crate::util; use crate::util::gettext::*; +use crate::util::{self, ToBufRead}; -use gtk::glib; - -use std::path::Path; +use crate::deps::*; +use gio::prelude::*; #[derive(Default)] pub enum ImageMetadata { @@ -45,10 +44,9 @@ impl std::fmt::Debug for ImageMetadata { } impl ImageMetadata { - pub fn load(path: &Path) -> Self { - log::debug!("Loading metadata for {:?}", path); - if let Ok(file) = std::fs::File::open(path) { - let mut bufreader = std::io::BufReader::new(&file); + pub fn load(file: &gio::File) -> Self { + log::debug!("Loading metadata for {}", file.uri()); + if let Ok(mut bufreader) = file.to_buf_read() { let exifreader = exif::Reader::new(); if let Ok(exif) = exifreader.read_from_container(&mut bufreader) { diff --git a/src/image_metadata/obj.rs b/src/image_metadata/obj.rs index 07f7e0b..7aca868 100644 --- a/src/image_metadata/obj.rs +++ b/src/image_metadata/obj.rs @@ -7,17 +7,16 @@ use gtk::glib; use once_cell::sync::Lazy; use std::cell::RefCell; -use std::path::Path; glib::wrapper! { pub struct LpImageMetadata(ObjectSubclass); } impl LpImageMetadata { - pub fn load(path: &Path) -> Self { + pub fn load(file: &gio::File) -> Self { let obj = glib::Object::new::(); - let metadata = ImageMetadata::load(path); + let metadata = ImageMetadata::load(file); obj.imp().metadata.replace(metadata); obj diff --git a/src/util/mod.rs b/src/util/mod.rs index b6e73d0..c142e13 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -154,6 +154,25 @@ pub async fn untrash(path: &Path) -> anyhow::Result<()> { error } +pub trait BufReadSeek: std::io::BufRead + std::io::Seek {} + +impl BufReadSeek for std::io::BufReader> {} + +pub trait ToBufRead { + fn to_buf_read(&self) -> anyhow::Result>; +} + +impl ToBufRead for gio::File { + fn to_buf_read(&self) -> anyhow::Result> { + let read = self + .read(gio::Cancellable::NONE) + .context(gettext("Failed to open image"))? + .into_read(); + + Ok(Box::new(std::io::BufReader::new(read))) + } +} + #[derive(Debug, Clone, Copy)] pub enum Position { First, diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 1dfccf3..f1e1dcf 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -32,7 +32,6 @@ use crate::deps::*; use crate::decoder::tiling::FrameBufferExt; use crate::decoder::{self, tiling, Decoder, DecoderUpdate}; use crate::image_metadata::LpImageMetadata; -use crate::util::gettext::*; use crate::util::Gesture; use crate::util::spawn; @@ -43,7 +42,6 @@ use once_cell::sync::Lazy; use once_cell::unsync::OnceCell; use std::cell::{Cell, RefCell}; -use std::path::PathBuf; use std::sync::Arc; /// Default background color around images and behind transparent images @@ -107,7 +105,6 @@ mod imp { #[derive(Debug, Default)] pub struct LpImage { pub(super) file: RefCell>, - pub(super) path: RefCell>, pub(super) is_deleted: Cell, /// Track changes to this image pub(super) file_monitor: RefCell>, @@ -194,9 +191,6 @@ mod imp { glib::ParamSpecObject::builder::("file") .read_only() .build(), - glib::ParamSpecVariant::builder("path", glib::VariantTy::BYTE_STRING) - .read_only() - .build(), glib::ParamSpecBoolean::builder("is-deleted") .read_only() .build(), @@ -242,7 +236,6 @@ mod imp { let obj = self.obj(); match pspec.name() { "file" => obj.file().to_value(), - "path" => obj.path().to_variant().to_value(), "is-deleted" => obj.is_deleted().to_value(), "is-loaded" => obj.is_loaded().to_value(), "error" => obj.error().to_value(), @@ -743,15 +736,9 @@ glib::wrapper! { impl LpImage { pub async fn load(&self, file: &gio::File) { - let path = file.path().unwrap(); - self.set_file(&file); + log::debug!("Loading file {}", file.uri()); - if !path.is_file() { - self.set_error(anyhow::Error::msg(gettext("File does not exist"))); - return; - } - - log::debug!("Loading file {path:?}"); + self.set_file(file); let tiles = &self.imp().frame_buffer; @@ -920,16 +907,10 @@ impl LpImage { self.imp().file.borrow().clone() } - pub fn path(&self) -> Option { - self.imp().path.borrow().clone() - } - pub(super) fn set_file(&self, file: &gio::File) { let imp = self.imp(); imp.file.replace(Some(file.clone())); - imp.path.replace(file.path()); - self.notify("path"); let monitor = file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE); if let Ok(m) = &monitor { @@ -953,7 +934,7 @@ impl LpImage { match event { gio::FileMonitorEvent::Renamed => { if let Some(file) = file_b { - log::debug!("Moved to {:?}", file.path()); + log::debug!("Moved to {}", file.uri()); self.set_file(file); } } diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index 3aa18c8..f0412fc 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -31,8 +31,6 @@ use gtk::prelude::*; use gtk::CompositeTemplate; use once_cell::sync::Lazy; -use std::path::PathBuf; - mod imp { use super::*; @@ -151,7 +149,7 @@ impl LpImagePage { pub fn from_file(file: &gio::File) -> Self { let obj = glib::Object::new::(); - obj.imp().image.set_file(&file); + obj.imp().image.set_file(file); spawn(clone!(@weak obj, @strong file => async move { let imp = obj.imp(); diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 1b163d0..b90c651 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -44,7 +44,6 @@ use gtk::CompositeTemplate; use once_cell::sync::Lazy; use std::cell::{Cell, RefCell}; -use std::path::{Path, PathBuf}; // The number of pages we want to buffer // on either side of the current page. @@ -139,7 +138,7 @@ mod imp { let view = &*obj; signal_group.connect_closure( - "notify::path", + "notify::file", true, glib::closure_local!(@watch view => move |_: LpImage, _: glib::ParamSpec| { view.current_image_path_changed(); @@ -333,7 +332,7 @@ impl LpImageView { .and_then(|x| self.model().index_of(&x)) .unwrap_or_default(); - let page = if let Some(page) = sliding_view.get(&new_file) { + let page = if let Some(page) = sliding_view.get(new_file) { page } else { let page = LpImagePage::from_file(new_file); @@ -362,22 +361,22 @@ impl LpImageView { self.imp().preserve_content.set(false); - let existing = dbg!(sliding_view.pages()); - let target = dbg!(self.model().files_around(current_file, BUFFER)); + let existing = sliding_view.pages(); + let target = self.model().files_around(current_file, BUFFER); // remove old pages for (uri, page) in &existing { if !target.contains_key(uri) { - sliding_view.remove(dbg!(page)); + sliding_view.remove(page); } } // add missing pages or put in correct position for (position, (_, file)) in target.iter().enumerate() { if let Some(page) = existing.get(&file.uri()) { - sliding_view.move_to(page, dbg!(position)); + sliding_view.move_to(page, position); } else { - sliding_view.insert(&LpImagePage::from_file(file), dbg!(position)); + sliding_view.insert(&LpImagePage::from_file(file), position); } } diff --git a/src/widgets/sliding_view.rs b/src/widgets/sliding_view.rs index f1dd837..1562914 100644 --- a/src/widgets/sliding_view.rs +++ b/src/widgets/sliding_view.rs @@ -11,7 +11,6 @@ use indexmap::IndexMap; use once_cell::sync::{Lazy, OnceCell}; use std::cell::{Cell, RefCell}; -//use std::path::{Path, PathBuf}; static SCROLL_DAMPING_RATIO: f64 = 1.; static SCROLL_MASS: f64 = 0.5; diff --git a/src/window.rs b/src/window.rs index ea843f0..97c08cd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -678,9 +678,9 @@ impl LpWindow { // have multiple `match` or `if let` branches, and without needing // to unwrap. #[template_callback] - fn window_title(&self, path: glib::variant::Variant) -> String { + fn window_title(&self, file: Option<&gio::File>) -> String { // ensure that templates are initialized - if path.as_maybe().is_none() { + if file.is_none() { gettext("Loupe") } else { self.imp()