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 return true;
25 };
26
27 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 if mime == "application/octet-stream" {
38 return true;
39 }
40 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 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 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 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}