Support loading images with weird urls and improve error message (#5431)
* Closes #5341 * [x] I have followed the instructions in the PR template
This commit is contained in:
parent
f687b27efc
commit
291b83b7be
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<String> },
|
||||
|
||||
/// 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<String> },
|
||||
|
||||
/// 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::<Self>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub struct RetainedImage {
|
|||
}
|
||||
|
||||
impl RetainedImage {
|
||||
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self {
|
||||
pub fn from_color_image(debug_name: impl Into<String>, image: egui::ColorImage) -> Self {
|
||||
Self {
|
||||
debug_name: debug_name.into(),
|
||||
size: image.size,
|
||||
|
|
@ -54,7 +54,7 @@ impl RetainedImage {
|
|||
) -> Result<Self, String> {
|
||||
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<egui::ColorImage, String> {
|
||||
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::load::LoadError> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use egui::{
|
|||
use image::ImageFormat;
|
||||
use std::{mem::size_of, path::Path, sync::Arc};
|
||||
|
||||
type Entry = Result<Arc<ColorImage>, String>;
|
||||
type Entry = Result<Arc<ColorImage>, 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::<egui::Color32>(),
|
||||
Err(err) => err.len(),
|
||||
Err(err) => err.byte_size(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue