image_page: Specific error for file loading

Give a specific error message if loading the source file fails.

Closes #487
This commit is contained in:
Sophie Herold 2025-12-22 01:36:28 +01:00
parent b9668c7262
commit 7151cf5d1d
8 changed files with 123 additions and 122 deletions

9
Cargo.lock generated
View file

@ -881,8 +881,7 @@ dependencies = [
[[package]] [[package]]
name = "glycin" name = "glycin"
version = "3.0.7" version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.gnome.org/GNOME/glycin#f34faa00c480b7809ceb8503ec85282446206277"
checksum = "502ac59dc785b95521e7deea20e292f3501a11752600c4f37cf34676f794b4e3"
dependencies = [ dependencies = [
"async-fs", "async-fs",
"async-global-executor", "async-global-executor",
@ -918,8 +917,7 @@ dependencies = [
[[package]] [[package]]
name = "glycin-common" name = "glycin-common"
version = "1.0.3" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.gnome.org/GNOME/glycin#f34faa00c480b7809ceb8503ec85282446206277"
checksum = "6e9b31c7fa5ccb9d91317c956e767daef59be9fee3ae9df30468470368678482"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"gufo-common", "gufo-common",
@ -937,8 +935,7 @@ dependencies = [
[[package]] [[package]]
name = "glycin-utils" name = "glycin-utils"
version = "4.0.4" version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.gnome.org/GNOME/glycin#f34faa00c480b7809ceb8503ec85282446206277"
checksum = "7dfec0cd29d5ecfb1dd723868e22e2c49441020f35d5f14307018ff0a1679e2f"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"bitflags", "bitflags",

View file

@ -27,7 +27,10 @@ chrono = { version = "0.4.38", default-features = false, features = ["std"] }
gettext-rs = { version = "0.7.0", features = ["gettext-system"] } gettext-rs = { version = "0.7.0", features = ["gettext-system"] }
gettext-sys = { version = "0.26.0", features = ["gettext-system"] } gettext-sys = { version = "0.26.0", features = ["gettext-system"] }
gio = { version = "0.21.0", features = ["v2_74"] } gio = { version = "0.21.0", features = ["v2_74"] }
glycin = { version = "3.0.4", features = ["gdk4", "unstable-config"] } glycin = { version = "3.0.4", features = [
"gdk4",
"unstable-config",
], git = "https://gitlab.gnome.org/GNOME/glycin" }
gtk = { package = "gtk4", version = "0.10.0", features = [ gtk = { package = "gtk4", version = "0.10.0", features = [
"v4_14", "v4_14",
"xml_validation", "xml_validation",

View file

@ -0,0 +1 @@
Add a specific error message when loading the image file fails. Usually when the file does not exists or there are no rights to access the file.

View file

@ -59,32 +59,21 @@ pub enum DecoderUpdate {
Redraw, Redraw,
/// Start animation /// Start animation
Animated, Animated,
Error(DecoderError), Error(Option<DecoderError>),
} }
#[derive(Debug, Clone, Default, PartialEq, Eq)] #[derive(Debug, Clone)]
pub enum DecoderError { pub enum DecoderError {
/// Image format not supported or unknown /// Image format not supported or unknown
UnsupportedFormat, UnsupportedFormat(String, glycin::ErrorCtx),
/// No glycin-loaders installed /// No glycin-loaders installed
NoLoadersConfigured, NoLoadersConfigured(glycin::ErrorCtx),
/// Memory limit exceeded /// Memory limit exceeded
OutOfMemory, OutOfMemory(glycin::ErrorCtx),
/// Failed to load image
ImageSource(glib::Error, glycin::ErrorCtx),
Generic(String), Generic(glycin::ErrorCtx),
//ImageSource(String),
#[default]
None,
}
impl DecoderError {
pub fn is_err(&self) -> bool {
matches!(self, Self::None)
}
pub fn is_ok(&self) -> bool {
!matches!(self, Self::None)
}
} }
impl UpdateSender { impl UpdateSender {
@ -116,27 +105,34 @@ impl UpdateSender {
} }
pub fn send_error(&self, err: glycin::ErrorCtx) { pub fn send_error(&self, err: glycin::ErrorCtx) {
dbg!(&err);
if let Some(mime_type) = err.unsupported_format() { if let Some(mime_type) = err.unsupported_format() {
let mut metadata = Metadata::default(); let mut metadata = Metadata::default();
metadata.set_mime_type(mime_type); metadata.set_mime_type(mime_type.clone());
self.send(DecoderUpdate::Metadata(Box::new(metadata))); self.send(DecoderUpdate::Metadata(Box::new(metadata)));
self.send(DecoderUpdate::Error(DecoderError::UnsupportedFormat)); self.send(DecoderUpdate::Error(Some(DecoderError::UnsupportedFormat(
} mime_type, err,
if err.is_out_of_memory() { ))));
self.send(DecoderUpdate::Error(DecoderError::OutOfMemory)); } else if err.is_out_of_memory() {
self.send(DecoderUpdate::Error(Some(DecoderError::OutOfMemory(err))));
} else if matches!(err.error(), glycin::Error::NoLoadersConfigured(_)) { } else if matches!(err.error(), glycin::Error::NoLoadersConfigured(_)) {
self.send(DecoderUpdate::Error(DecoderError::NoLoadersConfigured)); self.send(DecoderUpdate::Error(Some(
DecoderError::NoLoadersConfigured(err),
)));
} else if let glycin::Error::ImageSource(glib_err) = err.error() {
self.send(DecoderUpdate::Error(Some(DecoderError::ImageSource(
glib_err.clone(),
err,
))));
} else {
self.send(DecoderUpdate::Error(Some(DecoderError::Generic(err))));
} }
self.send(DecoderUpdate::Error(DecoderError::Generic(err.to_string())));
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Decoder { pub struct Decoder {
decoder: FormatDecoder, decoder: FormatDecoder,
update_sender: UpdateSender,
} }
#[derive(Debug)] #[derive(Debug)]
@ -171,7 +167,6 @@ impl Decoder {
return ( return (
Self { Self {
decoder: FormatDecoder::Failed, decoder: FormatDecoder::Failed,
update_sender,
}, },
receiver, receiver,
); );
@ -182,7 +177,6 @@ impl Decoder {
let decoder = Self { let decoder = Self {
decoder: format_decoder, decoder: format_decoder,
update_sender,
}; };
(decoder, receiver) (decoder, receiver)
@ -209,8 +203,7 @@ impl Decoder {
pub fn request(&self, tile_request: TileRequest) { pub fn request(&self, tile_request: TileRequest) {
if let FormatDecoder::Svg(svg) = &self.decoder { if let FormatDecoder::Svg(svg) = &self.decoder {
if let Err(err) = svg.request(tile_request) { if let Err(err) = svg.request(tile_request) {
self.update_sender tracing::error!("Failed to request new SVG tiles: {err}");
.send(DecoderUpdate::Error(DecoderError::Generic(err.to_string())));
} }
} }
} }

View file

@ -54,6 +54,7 @@ use glib::subclass::Signal;
use glib::{Properties, SignalGroup}; use glib::{Properties, SignalGroup};
use gufo_common::orientation::Orientation; use gufo_common::orientation::Orientation;
use crate::decoder::tiling::SharedFrameBuffer;
use crate::decoder::{self, Decoder, DecoderUpdate, tiling}; use crate::decoder::{self, Decoder, DecoderUpdate, tiling};
use crate::deps::*; use crate::deps::*;
use crate::metadata::Metadata; use crate::metadata::Metadata;
@ -137,13 +138,11 @@ pub enum FitMode {
ExactVolatile, ExactVolatile,
} }
mod imp { use decoder::DecoderError;
use decoder::DecoderError; use glycin::Operations;
use glycin::Operations;
mod imp {
use super::*; use super::*;
use crate::decoder::tiling::SharedFrameBuffer;
use crate::editing;
#[derive(Debug, Default, Properties)] #[derive(Debug, Default, Properties)]
#[properties(wrapper_type = super::LpImage)] #[properties(wrapper_type = super::LpImage)]
@ -155,9 +154,9 @@ mod imp {
#[property(get)] #[property(get)]
pub(super) is_deleted: Cell<bool>, pub(super) is_deleted: Cell<bool>,
/// Set if an error has occurred, shown on error_page /// Set if an error has occurred, shown on error_page
pub(super) specific_error: RefCell<DecoderError>, pub(super) specific_error: RefCell<Option<DecoderError>>,
#[property(get=Self::is_error)] #[property(get=Self::is_error)]
pub(super) is_error: Cell<bool>, pub(super) is_error: PhantomData<bool>,
/// Set to true when image is ready for displaying /// Set to true when image is ready for displaying
#[property(get)] #[property(get)]
pub(super) is_loaded: Cell<bool>, pub(super) is_loaded: Cell<bool>,
@ -257,7 +256,7 @@ mod imp {
pub(super) nth_snapshot: Cell<u8>, pub(super) nth_snapshot: Cell<u8>,
/// Editing queue /// Editing queue
pub(super) editing_queue: editing::Queue, pub(super) editing_queue: crate::editing::Queue,
pub(super) operations: RefCell<Option<Arc<Operations>>>, pub(super) operations: RefCell<Option<Arc<Operations>>>,
} }

View file

@ -268,7 +268,7 @@ impl imp::LpImage {
} }
pub(super) fn is_error(&self) -> bool { pub(super) fn is_error(&self) -> bool {
self.specific_error.borrow().is_err() self.specific_error.borrow().is_some()
} }
fn set_loaded(&self, is_loaded: bool) { fn set_loaded(&self, is_loaded: bool) {
@ -279,7 +279,7 @@ impl imp::LpImage {
obj.notify_is_loaded(); obj.notify_is_loaded();
if is_loaded { if is_loaded {
obj.set_specific_error(DecoderError::None); obj.set_specific_error(None);
} }
} }
} }
@ -357,26 +357,24 @@ impl LpImage {
imp.decoder.replace(Some(Arc::new(decoder))); imp.decoder.replace(Some(Arc::new(decoder)));
} }
pub fn specific_error(&self) -> DecoderError { pub fn specific_error(&self) -> Option<DecoderError> {
self.imp().specific_error.borrow().clone() self.imp().specific_error.borrow().clone()
} }
pub fn set_specific_error(&self, error: DecoderError) { pub fn set_specific_error(&self, error: Option<DecoderError>) {
let imp = self.imp(); let imp = self.imp();
// Keeping first error instead of replacing with new error // Keeping first error instead of replacing with new error or return
if error.is_err() && self.is_error() { if error.is_some() == self.is_error() {
return; return;
} }
if self.specific_error() != error { let is_err = error.is_some();
let is_err = error.is_err(); imp.specific_error.replace(error);
imp.specific_error.replace(error); self.notify_is_error();
self.notify_is_error();
if is_err { if is_err {
imp.set_loaded(false); imp.set_loaded(false);
}
} }
} }
} }

View file

@ -214,72 +214,79 @@ impl LpImagePage {
let imp = self.imp(); let imp = self.imp();
let image = self.image(); let image = self.image();
match image.specific_error() { if let Some(error) = image.specific_error() {
DecoderError::NoLoadersConfigured => { let details;
// Translators: {} is replaced with a version number let description;
let message = gettext_f(
"No image loaders available. Maybe the “glycin-loaders” package with compatibility version “{}” is not installed.",
[format!("{}+", glycin::COMPAT_VERSION)],
);
imp.error_page.set_description(Some(&message)); match error {
imp.stack.set_visible_child(&*imp.error_page); DecoderError::NoLoadersConfigured(err) => {
imp.error_more_info.set_visible(false); // Translators: {} is replaced with a version number
} description = gettext_f(
DecoderError::UnsupportedFormat => { "No image loaders available. Maybe the “glycin-loaders” package with compatibility version “{}” is not installed.",
let mime_type = image.metadata().mime_type().unwrap_or_default(); [format!("{}+", glycin::COMPAT_VERSION)],
let content_type = gio::content_type_from_mime_type(&mime_type).unwrap_or_default(); );
let description = gio::content_type_get_description(&content_type).to_string();
let message = if glycin::Loader::DEFAULT_MIME_TYPES.contains(&mime_type.as_str()) { details = err.to_string();
// Translators: The first occurance of {} is replace with a description of the
// format and the second with an id (mime-type) of the format.
gettext_f(
"The image format “{} ({})” is known but not installed.",
[&description, &mime_type],
)
} else {
// Translators: The first occurance of {} is replace with a description of the
// format and the second with an id (mime-type) of the format.
gettext_f(
"Unknown image format “{} ({}).”",
[&description, &mime_type],
)
};
imp.error_more_info.connect_clicked(glib::clone!(
#[weak(rename_to = obj)]
self,
move |_| {
LpErrorDetails::new(&obj.root().unwrap(), &err, ErrorType::Loader);
}
));
imp.error_page.set_description(Some(&message));
imp.stack.set_visible_child(&*imp.error_page);
}
DecoderError::Generic(err) => {
imp.error_more_info.connect_clicked(glib::clone!(
#[weak(rename_to = obj)]
self,
move |_| {
LpErrorDetails::new(&obj.root().unwrap(), &err, ErrorType::Loader);
}
));
if matches!(image.specific_error(), DecoderError::OutOfMemory) {
imp.error_page.set_description(Some(&gettext(
"There is not enough free memory available to load this image.",
)));
} else {
imp.error_page.set_description(Some(&gettext(
"Either the image file is corrupted or it contains unsupported elements.",
)));
} }
DecoderError::UnsupportedFormat(mime_type, err) => {
let content_type =
gio::content_type_from_mime_type(&mime_type).unwrap_or_default();
let mime_type_description =
gio::content_type_get_description(&content_type).to_string();
imp.error_more_info.set_visible(true); description =
imp.stack.set_visible_child(&*imp.error_page); if glycin::Loader::DEFAULT_MIME_TYPES.contains(&mime_type.as_str()) {
// Translators: The first occurance of {} is replace with a description
// of the format and the second with an id
// (mime-type) of the format.
gettext_f(
"The image format “{} ({})” is known but not installed.",
[&mime_type_description, &mime_type],
)
} else {
// Translators: The first occurance of {} is replace with a description
// of the format and the second with an id
// (mime-type) of the format.
gettext_f(
"Unknown image format “{} ({}).”",
[&mime_type_description, &mime_type],
)
};
details = err.to_string();
}
DecoderError::OutOfMemory(err) => {
description =
gettext("There is not enough free memory available to load this image.");
details = err.to_string();
}
DecoderError::ImageSource(glib_err, err) => {
// Translators: The first occurance of {} is replace with a
// translated, more detailled error message.
description =
gettext_f("Failed to load image file: {}", [&glib_err.to_string()]);
details = err.to_string();
}
DecoderError::Generic(err) => {
description = gettext(
"Either the image file is corrupted or it contains unsupported elements.",
);
details = err.to_string();
}
} }
imp.error_more_info.connect_clicked(glib::clone!(
#[weak(rename_to = obj)]
self,
move |_| {
LpErrorDetails::new(&obj.root().unwrap(), &details, ErrorType::Loader);
}
));
imp.error_page.set_description(Some(&description));
imp.error_more_info.set_visible(true);
imp.stack.set_visible_child(&*imp.error_page);
} }
} }
} }

View file

@ -650,7 +650,10 @@ impl LpImageView {
#[weak(rename_to = obj)] #[weak(rename_to = obj)]
self, self,
move |image| { move |image| {
if matches!(image.specific_error(), DecoderError::UnsupportedFormat) { if matches!(
image.specific_error(),
Some(DecoderError::UnsupportedFormat(_, _))
) {
if obj.current_image().as_ref() == Some(image) { if obj.current_image().as_ref() == Some(image) {
tracing::debug!( tracing::debug!(
"Image format unsupported but not removing since current image" "Image format unsupported but not removing since current image"