diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index cffc37c3293..aa449849e32 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3455,15 +3455,23 @@ impl Context { return Err(load::LoadError::NoImageLoaders); } + let mut format = None; + // Try most recently added loaders first (hence `.rev()`) for loader in image_loaders.iter().rev() { match loader.load(self, uri, size_hint) { Err(load::LoadError::NotSupported) => continue, + Err(load::LoadError::FormatNotSupported { detected_format }) => { + format = format.or(detected_format); + continue; + } result => return result, } } - Err(load::LoadError::NoMatchingImageLoader) + Err(load::LoadError::NoMatchingImageLoader { + detected_format: format, + }) } /// Try loading the texture from the given uri using any available texture loaders. diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index b6711de3c52..26950eb2add 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -77,16 +77,19 @@ pub enum LoadError { /// Programmer error: There are no image loaders installed. NoImageLoaders, - /// A specific loader does not support this scheme, protocol or image format. + /// A specific loader does not support this scheme or protocol. NotSupported, + /// A specific loader does not support the format of the image. + FormatNotSupported { detected_format: Option }, + /// Programmer error: Failed to find the bytes for this image because /// there was no [`BytesLoader`] supporting the scheme. NoMatchingBytesLoader, /// Programmer error: Failed to parse the bytes as an image because - /// there was no [`ImageLoader`] supporting the scheme. - NoMatchingImageLoader, + /// there was no [`ImageLoader`] supporting the format. + NoMatchingImageLoader { detected_format: Option }, /// Programmer error: no matching [`TextureLoader`]. /// Because of the [`DefaultTextureLoader`], this error should never happen. @@ -96,6 +99,20 @@ pub enum LoadError { Loading(String), } +impl LoadError { + /// Returns the (approximate) size of the error message in bytes. + pub fn byte_size(&self) -> usize { + match self { + Self::FormatNotSupported { detected_format } + | Self::NoMatchingImageLoader { detected_format } => { + detected_format.as_ref().map_or(0, |s| s.len()) + } + Self::Loading(message) => message.len(), + _ => std::mem::size_of::(), + } + } +} + impl Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -105,12 +122,15 @@ impl Display for LoadError { Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), - Self::NoMatchingImageLoader => f.write_str("No matching ImageLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), + Self::NoMatchingImageLoader { detected_format: None } => f.write_str("No matching ImageLoader. Either no ImageLoader is installed or the image is corrupted / has an unsupported format."), + Self::NoMatchingImageLoader { detected_format: Some(detected_format) } => write!(f, "No matching ImageLoader for format: {detected_format:?}. Make sure you enabled the necessary features on the image crate."), Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"), Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"), + Self::FormatNotSupported { detected_format } => write!(f, "Image format not supported by this loader: {detected_format:?}"), + Self::Loading(message) => f.write_str(message), } } diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index f6301aec823..1d2f6afa480 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -28,7 +28,7 @@ pub struct RetainedImage { } impl RetainedImage { - pub fn from_color_image(debug_name: impl Into, image: ColorImage) -> Self { + pub fn from_color_image(debug_name: impl Into, image: egui::ColorImage) -> Self { Self { debug_name: debug_name.into(), size: image.size, @@ -54,7 +54,7 @@ impl RetainedImage { ) -> Result { Ok(Self::from_color_image( debug_name, - load_image_bytes(image_bytes)?, + load_image_bytes(image_bytes).map_err(|err| err.to_string())?, )) } @@ -154,7 +154,7 @@ impl RetainedImage { self.texture .lock() .get_or_insert_with(|| { - let image: &mut ColorImage = &mut self.image.lock(); + let image: &mut egui::ColorImage = &mut self.image.lock(); let image = std::mem::take(image); ctx.load_texture(&self.debug_name, image, self.options) }) @@ -190,8 +190,6 @@ impl RetainedImage { // ---------------------------------------------------------------------------- -use egui::ColorImage; - /// Load a (non-svg) image. /// /// Requires the "image" feature. You must also opt-in to the image formats you need @@ -200,9 +198,19 @@ use egui::ColorImage; /// # Errors /// On invalid image or unsupported image format. #[cfg(feature = "image")] -pub fn load_image_bytes(image_bytes: &[u8]) -> Result { +pub fn load_image_bytes(image_bytes: &[u8]) -> Result { crate::profile_function!(); - let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?; + let image = image::load_from_memory(image_bytes).map_err(|err| match err { + image::ImageError::Unsupported(err) => match err.kind() { + image::error::UnsupportedErrorKind::Format(format) => { + egui::load::LoadError::FormatNotSupported { + detected_format: Some(format.to_string()), + } + } + _ => egui::load::LoadError::Loading(err.to_string()), + }, + err => egui::load::LoadError::Loading(err.to_string()), + })?; let size = [image.width() as _, image.height() as _]; let image_buffer = image.to_rgba8(); let pixels = image_buffer.as_flat_samples(); diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 8c2c497058a..088ef5e4587 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -7,7 +7,7 @@ use egui::{ use image::ImageFormat; use std::{mem::size_of, path::Path, sync::Arc}; -type Entry = Result, String>; +type Entry = Result, LoadError>; #[derive(Default)] pub struct ImageCrateLoader { @@ -31,9 +31,14 @@ fn is_supported_uri(uri: &str) -> bool { .any(|format_ext| ext == *format_ext) } -fn is_unsupported_mime(mime: &str) -> bool { +fn is_supported_mime(mime: &str) -> bool { + // This is the default mime type for binary files, so this might actually be a valid image, + // let's relay on image's format guessing + if mime == "application/octet-stream" { + return true; + } // Uses only the enabled image crate features - !ImageFormat::all() + ImageFormat::all() .filter(ImageFormat::reading_enabled) .map(|fmt| fmt.to_mime_type()) .any(|format_mime| mime == format_mime) @@ -46,12 +51,12 @@ impl ImageLoader for ImageCrateLoader { fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult { // three stages of guessing if we support loading the image: - // 1. URI extension + // 1. URI extension (only done for files) // 2. Mime from `BytesPoll::Ready` - // 3. image::guess_format + // 3. image::guess_format (used internally by image::load_from_memory) // (1) - if !is_supported_uri(uri) { + if uri.starts_with("file://") && !is_supported_uri(uri) { return Err(LoadError::NotSupported); } @@ -59,26 +64,26 @@ impl ImageLoader for ImageCrateLoader { if let Some(entry) = cache.get(uri).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Loading(err)), + Err(err) => Err(err), } } else { match ctx.try_load_bytes(uri) { Ok(BytesPoll::Ready { bytes, mime, .. }) => { - // (2 and 3) - if mime.as_deref().is_some_and(is_unsupported_mime) - || image::guess_format(&bytes).is_err() - { - return Err(LoadError::NotSupported); + // (2) + if let Some(mime) = mime { + if !is_supported_mime(&mime) { + return Err(LoadError::FormatNotSupported { + detected_format: Some(mime), + }); + } } + // (3) log::trace!("started loading {uri:?}"); let result = crate::image::load_image_bytes(&bytes).map(Arc::new); log::trace!("finished loading {uri:?}"); cache.insert(uri.into(), result.clone()); - match result { - Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Loading(err)), - } + result.map(|image| ImagePoll::Ready { image }) } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), Err(err) => Err(err), @@ -100,7 +105,7 @@ impl ImageLoader for ImageCrateLoader { .values() .map(|result| match result { Ok(image) => image.pixels.len() * size_of::(), - Err(err) => err.len(), + Err(err) => err.byte_size(), }) .sum() }