decoder: Cleanup and better image format detection

Closes #97

Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/140>
This commit is contained in:
Sophie Herold 2023-03-14 00:10:14 +01:00
parent 42d558d22f
commit b8d5f833f3
3 changed files with 88 additions and 42 deletions

View file

@ -2,37 +2,50 @@
use super::*; use super::*;
use crate::decoder::tiling::{self, TilingStoreExt}; use crate::decoder::tiling::{self, TilingStoreExt};
use crate::deps::*; use crate::deps::*;
use crate::i18n::*;
use anyhow::Context; use anyhow::Context;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use gtk::prelude::*; use gtk::prelude::*;
use std::path::PathBuf; use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug)] #[derive(Debug)]
pub struct ImageRsOther; pub struct ImageRsOther;
impl ImageRsOther { impl ImageRsOther {
fn reader(
path: &Path,
format: image_rs::ImageFormat,
) -> anyhow::Result<image_rs::io::Reader<BufReader<File>>> {
let mut reader = image_rs::io::Reader::open(path).context(i18n("Failed to open image"))?;
reader.set_format(format);
Ok(reader)
}
pub fn new( pub fn new(
path: PathBuf, path: PathBuf,
format: image_rs::ImageFormat,
updater: UpdateSender, updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::TilingStore>>, tiles: Arc<ArcSwap<tiling::TilingStore>>,
) -> Self { ) -> Self {
updater.spawn_error_handled(move || { 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); 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? // TODO: Set something huge?
reader.limits(image_rs::io::Limits::no_limits()); reader.limits(image_rs::io::Limits::no_limits());
let reader = reader let dynamic_image = reader.decode().context(i18n("Failed to decode image"))?;
.with_guessed_format()
.context("Failed to open image")?;
let dynamic_image = reader.decode().context("Failed to decode image")?;
let decoded_image = Decoded { dynamic_image }; let decoded_image = Decoded { dynamic_image };

View file

@ -4,17 +4,19 @@ pub mod tiling;
pub use formats::ImageDimensionDetails; pub use formats::ImageDimensionDetails;
use crate::deps::*;
use crate::i18n::*;
use crate::image_metadata::ImageMetadata; use crate::image_metadata::ImageMetadata;
use formats::*; use formats::*;
use tiling::TilingStoreExt; use tiling::TilingStoreExt;
use anyhow::Context; use anyhow::{anyhow, bail, Context};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use futures::channel::mpsc; use futures::channel::mpsc;
use gio::prelude::*; use gio::prelude::*;
use gtk::graphene;
use std::io::Read; use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
pub use formats::{ImageFormat, RSVG_MAX_SIZE}; pub use formats::{ImageFormat, RSVG_MAX_SIZE};
@ -35,7 +37,7 @@ pub struct UpdateSender {
} }
#[derive(Debug)] #[derive(Debug)]
/// Signals for renderer /// Signals for renderer (LpImage)
pub enum DecoderUpdate { pub enum DecoderUpdate {
/// Dimensions of image in `TilingSore` available/updated /// Dimensions of image in `TilingSore` available/updated
Dimensions(ImageDimensionDetails), Dimensions(ImageDimensionDetails),
@ -89,7 +91,7 @@ impl Decoder {
/// ///
/// The textures will be stored in the passed `TilingStore`. /// The textures will be stored in the passed `TilingStore`.
/// The renderer should listen to updates from the returned receiver. /// The renderer should listen to updates from the returned receiver.
pub fn new( pub async fn new(
file: gio::File, file: gio::File,
tiles: Arc<ArcSwap<tiling::TilingStore>>, tiles: Arc<ArcSwap<tiling::TilingStore>>,
) -> anyhow::Result<(Self, mpsc::UnboundedReceiver<DecoderUpdate>)> { ) -> anyhow::Result<(Self, mpsc::UnboundedReceiver<DecoderUpdate>)> {
@ -99,38 +101,72 @@ impl Decoder {
let update_sender = UpdateSender { sender }; let update_sender = UpdateSender { sender };
tiles.set_update_sender(update_sender.clone()); 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<ArcSwap<tiling::TilingStore>>,
) -> anyhow::Result<FormatDecoder> {
update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path))); update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path)));
let mut buf = Vec::new(); let mut buf = Vec::new();
let file = std::fs::File::open(&path)?; let file = std::fs::File::open(&path).context(i18n("Could not open image"))?;
file.take(64).read_to_end(&mut buf)?; file.take(64)
let format = image_rs::guess_format(&buf); .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 { match format {
image_rs::ImageFormat::Avif => { image_rs::ImageFormat::Avif => {
update_sender.send(DecoderUpdate::Format(ImageFormat::Heif)); 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 => { format => {
update_sender.send(DecoderUpdate::Format(ImageFormat::ImageRs(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 /// Request missing tiles

View file

@ -32,7 +32,6 @@ use crate::deps::*;
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;
use crate::util;
use adw::{prelude::*, subclass::prelude::*}; use adw::{prelude::*, subclass::prelude::*};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
@ -311,8 +310,8 @@ mod imp {
return gtk::Inhibit(false); return gtk::Inhibit(false);
} }
// Use exponential scaling since zoom is always multiplicative with the existing value // 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) // 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() { let (zoom_factor, animated) = match event.unit() {
gdk::ScrollUnit::Wheel => (f64::exp( - y * f64::ln(ZOOM_FACTOR_SCROLL_WHEEL)), y.abs() >= 1.), 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), gdk::ScrollUnit::Surface => (f64::exp( - y * f64::ln(ZOOM_FACTOR_SCROLL_SURFACE)), false),
@ -657,16 +656,14 @@ impl LpImage {
log::debug!("Loading file {path:?}"); log::debug!("Loading file {path:?}");
let tiles = &self.imp().tiles; let tiles = &self.imp().tiles;
// TODO: Fix two unwraps
let (decoder, mut decoder_update) = util::spawn( let (decoder, mut decoder_update) = match Decoder::new(file.clone(), tiles.clone()).await {
"image-init-decoder", Ok(x) => x,
glib::clone!(@strong file, @strong tiles => move || { Err(err) => {
Decoder::new(file.clone(), tiles.clone()) self.set_error(err);
}), return;
) }
.await };
.unwrap()
.unwrap();
let weak_obj = self.downgrade(); let weak_obj = self.downgrade();
spawn!(async move { spawn!(async move {