egui_extras/loaders/
image_loader.rs

1use ahash::HashMap;
2use egui::{
3    load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
4    mutex::Mutex,
5    ColorImage,
6};
7use image::ImageFormat;
8use std::{mem::size_of, path::Path, sync::Arc};
9
10type Entry = Result<Arc<ColorImage>, LoadError>;
11
12#[derive(Default)]
13pub struct ImageCrateLoader {
14    cache: Mutex<HashMap<String, Entry>>,
15}
16
17impl ImageCrateLoader {
18    pub const ID: &'static str = egui::generate_loader_id!(ImageCrateLoader);
19}
20
21fn is_supported_uri(uri: &str) -> bool {
22    let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else {
23        // `true` because if there's no extension, assume that we support it
24        return true;
25    };
26
27    // Uses only the enabled image crate features
28    ImageFormat::all()
29        .filter(ImageFormat::reading_enabled)
30        .flat_map(ImageFormat::extensions_str)
31        .any(|format_ext| ext == *format_ext)
32}
33
34fn is_supported_mime(mime: &str) -> bool {
35    // This is the default mime type for binary files, so this might actually be a valid image,
36    // let's relay on image's format guessing
37    if mime == "application/octet-stream" {
38        return true;
39    }
40    // Uses only the enabled image crate features
41    ImageFormat::all()
42        .filter(ImageFormat::reading_enabled)
43        .map(|fmt| fmt.to_mime_type())
44        .any(|format_mime| mime == format_mime)
45}
46
47impl ImageLoader for ImageCrateLoader {
48    fn id(&self) -> &str {
49        Self::ID
50    }
51
52    fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
53        // three stages of guessing if we support loading the image:
54        // 1. URI extension (only done for files)
55        // 2. Mime from `BytesPoll::Ready`
56        // 3. image::guess_format (used internally by image::load_from_memory)
57
58        // (1)
59        if uri.starts_with("file://") && !is_supported_uri(uri) {
60            return Err(LoadError::NotSupported);
61        }
62
63        let mut cache = self.cache.lock();
64        if let Some(entry) = cache.get(uri).cloned() {
65            match entry {
66                Ok(image) => Ok(ImagePoll::Ready { image }),
67                Err(err) => Err(err),
68            }
69        } else {
70            match ctx.try_load_bytes(uri) {
71                Ok(BytesPoll::Ready { bytes, mime, .. }) => {
72                    // (2)
73                    if let Some(mime) = mime {
74                        if !is_supported_mime(&mime) {
75                            return Err(LoadError::FormatNotSupported {
76                                detected_format: Some(mime),
77                            });
78                        }
79                    }
80
81                    if bytes.starts_with(b"version https://git-lfs") {
82                        return Err(LoadError::FormatNotSupported {
83                            detected_format: Some("git-lfs".to_owned()),
84                        });
85                    }
86
87                    // (3)
88                    log::trace!("started loading {uri:?}");
89                    let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
90                    log::trace!("finished loading {uri:?}");
91                    cache.insert(uri.into(), result.clone());
92                    result.map(|image| ImagePoll::Ready { image })
93                }
94                Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
95                Err(err) => Err(err),
96            }
97        }
98    }
99
100    fn forget(&self, uri: &str) {
101        let _ = self.cache.lock().remove(uri);
102    }
103
104    fn forget_all(&self) {
105        self.cache.lock().clear();
106    }
107
108    fn byte_size(&self) -> usize {
109        self.cache
110            .lock()
111            .values()
112            .map(|result| match result {
113                Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
114                Err(err) => err.byte_size(),
115            })
116            .sum()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn check_support() {
126        assert!(is_supported_uri("https://test.png"));
127        assert!(is_supported_uri("test.jpeg"));
128        assert!(is_supported_uri("http://test.gif"));
129        assert!(is_supported_uri("file://test"));
130        assert!(!is_supported_uri("test.svg"));
131    }
132}