diff --git a/src/decoder/formats/image_rs_other.rs b/src/decoder/formats/image_rs_other.rs index 7c4bcf8..2971e55 100644 --- a/src/decoder/formats/image_rs_other.rs +++ b/src/decoder/formats/image_rs_other.rs @@ -2,37 +2,50 @@ use super::*; use crate::decoder::tiling::{self, TilingStoreExt}; use crate::deps::*; +use crate::i18n::*; use anyhow::Context; use arc_swap::ArcSwap; use gtk::prelude::*; -use std::path::PathBuf; +use std::fs::File; +use std::io::BufReader; +use std::path::{Path, PathBuf}; use std::sync::Arc; #[derive(Debug)] pub struct ImageRsOther; impl ImageRsOther { + fn reader( + path: &Path, + format: image_rs::ImageFormat, + ) -> anyhow::Result>> { + let mut reader = image_rs::io::Reader::open(path).context(i18n("Failed to open image"))?; + reader.set_format(format); + Ok(reader) + } + pub fn new( path: PathBuf, + format: image_rs::ImageFormat, updater: UpdateSender, tiles: Arc>, ) -> Self { updater.spawn_error_handled(move || { - let dimensions = image_rs::image_dimensions(&path).context("Failed to read image")?; + let reader = Self::reader(&path, format)?; + let dimensions = reader + .into_dimensions() + .context("Failed to read image dimensions")?; tiles.set_original_dimensions(dimensions); - let mut reader = image_rs::io::Reader::open(&path).context("Failed to open image")?; + let mut reader = Self::reader(&path, format)?; // TODO: Set something huge? reader.limits(image_rs::io::Limits::no_limits()); - let reader = reader - .with_guessed_format() - .context("Failed to open image")?; - let dynamic_image = reader.decode().context("Failed to decode image")?; + let dynamic_image = reader.decode().context(i18n("Failed to decode image"))?; let decoded_image = Decoded { dynamic_image }; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index b4a4df3..5c00a82 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -4,17 +4,19 @@ pub mod tiling; pub use formats::ImageDimensionDetails; +use crate::deps::*; +use crate::i18n::*; use crate::image_metadata::ImageMetadata; use formats::*; use tiling::TilingStoreExt; -use anyhow::Context; +use anyhow::{anyhow, bail, Context}; use arc_swap::ArcSwap; use futures::channel::mpsc; use gio::prelude::*; -use gtk::graphene; use std::io::Read; +use std::path::PathBuf; use std::sync::Arc; pub use formats::{ImageFormat, RSVG_MAX_SIZE}; @@ -35,7 +37,7 @@ pub struct UpdateSender { } #[derive(Debug)] -/// Signals for renderer +/// Signals for renderer (LpImage) pub enum DecoderUpdate { /// Dimensions of image in `TilingSore` available/updated Dimensions(ImageDimensionDetails), @@ -89,7 +91,7 @@ impl Decoder { /// /// The textures will be stored in the passed `TilingStore`. /// The renderer should listen to updates from the returned receiver. - pub fn new( + pub async fn new( file: gio::File, tiles: Arc>, ) -> anyhow::Result<(Self, mpsc::UnboundedReceiver)> { @@ -99,38 +101,72 @@ impl Decoder { 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)) + .await + .map_err(|_| anyhow!("Constructing the FormatDecoder failed unexpectedly"))??; + + Ok((Self { decoder }, receiver)) + } + + fn format_decoder( + update_sender: UpdateSender, + path: PathBuf, + tiles: Arc>, + ) -> anyhow::Result { update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path))); let mut buf = Vec::new(); - let file = std::fs::File::open(&path)?; - file.take(64).read_to_end(&mut buf)?; - let format = image_rs::guess_format(&buf); + let file = std::fs::File::open(&path).context(i18n("Could not open image"))?; + file.take(64) + .read_to_end(&mut buf) + .context(i18n("Could not read image"))?; - let decoder = if let Ok(format) = format { + // 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 { match format { image_rs::ImageFormat::Avif => { update_sender.send(DecoderUpdate::Format(ImageFormat::Heif)); - FormatDecoder::Heif(Heif::new(path, update_sender, tiles)) + return Ok(FormatDecoder::Heif(Heif::new(path, update_sender, tiles))); } format => { update_sender.send(DecoderUpdate::Format(ImageFormat::ImageRs(format))); - FormatDecoder::ImageRsOther(ImageRsOther::new(path, update_sender, tiles)) + return Ok(FormatDecoder::ImageRsOther(ImageRsOther::new( + path, + format, + update_sender, + tiles, + ))); + } + } + } else { + let file_info = gio::File::for_path(&path) + .query_info( + gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + gio::FileQueryInfoFlags::NONE, + gio::Cancellable::NONE, + ) + .context("Could not read content type information")?; + if let Some(content_type) = file_info + .content_type() + .as_ref() + .and_then(|x| gio::content_type_get_mime_type(x)) + { + // Known things we want to match here are + // - image/svg+xml + // - 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))); + } else { + bail!(i18n_f("Unknown image format: {}", &[content_type.as_str()])); } } } - // TODO: Use mime glib mime type detection - else if [Some("svg"), Some("svgz")].contains(&path.extension().and_then(|x| x.to_str())) { - update_sender.send(DecoderUpdate::Format(ImageFormat::Svg)); - FormatDecoder::Svg(Svg::new(path, update_sender, tiles)) - } else if [Some("heic"), Some("avif")].contains(&path.extension().and_then(|x| x.to_str())) - { - update_sender.send(DecoderUpdate::Format(ImageFormat::Heif)); - FormatDecoder::Heif(Heif::new(path, update_sender, tiles)) - } else { - return None.context("unknown image format"); - }; - Ok((Self { decoder }, receiver)) + bail!(i18n("Unknown image format")) } /// Request missing tiles diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 90b5abf..c547e19 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -32,7 +32,6 @@ use crate::deps::*; use crate::decoder::{self, tiling, Decoder, DecoderUpdate}; use crate::i18n::i18n; use crate::image_metadata::LpImageMetadata; -use crate::util; use adw::{prelude::*, subclass::prelude::*}; use arc_swap::ArcSwap; @@ -311,8 +310,8 @@ mod imp { return gtk::Inhibit(false); } - // Use exponential scaling since zoom is always multiplicative with the existing value - // This is the right thing since `exp(n/2)^2 == exp(n)` (two small steps are the same as one larger step) + // Use exponential scaling since zoom is always multiplicative with the existing value + // This is the right thing since `exp(n/2)^2 == exp(n)` (two small steps are the same as one larger step) let (zoom_factor, animated) = match event.unit() { gdk::ScrollUnit::Wheel => (f64::exp( - y * f64::ln(ZOOM_FACTOR_SCROLL_WHEEL)), y.abs() >= 1.), gdk::ScrollUnit::Surface => (f64::exp( - y * f64::ln(ZOOM_FACTOR_SCROLL_SURFACE)), false), @@ -657,16 +656,14 @@ impl LpImage { log::debug!("Loading file {path:?}"); let tiles = &self.imp().tiles; - // TODO: Fix two unwraps - let (decoder, mut decoder_update) = util::spawn( - "image-init-decoder", - glib::clone!(@strong file, @strong tiles => move || { - Decoder::new(file.clone(), tiles.clone()) - }), - ) - .await - .unwrap() - .unwrap(); + + let (decoder, mut decoder_update) = match Decoder::new(file.clone(), tiles.clone()).await { + Ok(x) => x, + Err(err) => { + self.set_error(err); + return; + } + }; let weak_obj = self.downgrade(); spawn!(async move {