egui/load.rs
1//! # Image loading
2//!
3//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
4//! will get you up and running quickly with its reasonable default implementations of the traits described below.
5//!
6//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature (`cargo add egui_extras -F all_loaders`).
7//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html)
8//! in your app's setup code.
9//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
10//!
11//! ## Loading process
12//!
13//! There are three kinds of loaders:
14//! - [`BytesLoader`]: load the raw bytes of an image
15//! - [`ImageLoader`]: decode the bytes into an array of colors
16//! - [`TextureLoader`]: ask the backend to put an image onto the GPU
17//!
18//! The different kinds of loaders represent different layers in the loading process:
19//!
20//! ```text,ignore
21//! ui.image("file://image.png")
22//! └► Context::try_load_texture
23//! └► TextureLoader::load
24//! └► Context::try_load_image
25//! └► ImageLoader::load
26//! └► Context::try_load_bytes
27//! └► BytesLoader::load
28//! ```
29//!
30//! As each layer attempts to load the URI, it first asks the layer below it
31//! for the data it needs to do its job. But this is not a strict requirement,
32//! an implementation could instead generate the data it needs!
33//!
34//! Loader trait implementations may be registered on a context with:
35//! - [`Context::add_bytes_loader`]
36//! - [`Context::add_image_loader`]
37//! - [`Context::add_texture_loader`]
38//!
39//! There may be multiple loaders of the same kind registered at the same time.
40//! The `try_load` methods on [`Context`] will attempt to call each loader one by one,
41//! until one of them returns something other than [`LoadError::NotSupported`].
42//!
43//! The loaders are stored in the context. This means they may hold state across frames,
44//! which they can (and _should_) use to cache the results of the operations they perform.
45//!
46//! For example, a [`BytesLoader`] that loads file URIs (`file://image.png`)
47//! would cache each file read. A [`TextureLoader`] would cache each combination
48//! of `(URI, TextureOptions)`, and so on.
49//!
50//! Each URI will be passed through the loaders as a plain `&str`.
51//! The loaders are free to derive as much meaning from the URI as they wish to.
52//! For example, a loader may determine that it doesn't support loading a specific URI
53//! if the protocol does not match what it expects.
54
55mod bytes_loader;
56mod texture_loader;
57
58use core::{
59 fmt::{Debug, Display},
60 ops::Deref,
61};
62use std::{borrow::Cow, sync::Arc};
63
64use ahash::HashMap;
65
66use emath::{Float as _, OrderedFloat};
67use epaint::{ColorImage, TextureHandle, TextureId, Vec2, mutex::Mutex, textures::TextureOptions};
68
69use crate::Context;
70
71pub use self::{bytes_loader::DefaultBytesLoader, texture_loader::DefaultTextureLoader};
72
73/// Represents a failed attempt at loading an image.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum LoadError {
76 /// Programmer error: There are no image loaders installed.
77 NoImageLoaders,
78
79 /// A specific loader does not support this scheme or protocol.
80 NotSupported,
81
82 /// A specific loader does not support the format of the image.
83 FormatNotSupported { detected_format: Option<String> },
84
85 /// Programmer error: Failed to find the bytes for this image because
86 /// there was no [`BytesLoader`] supporting the scheme.
87 NoMatchingBytesLoader,
88
89 /// Programmer error: Failed to parse the bytes as an image because
90 /// there was no [`ImageLoader`] supporting the format.
91 NoMatchingImageLoader { detected_format: Option<String> },
92
93 /// Programmer error: no matching [`TextureLoader`].
94 /// Because of the [`DefaultTextureLoader`], this error should never happen.
95 NoMatchingTextureLoader,
96
97 /// Runtime error: Loading was attempted, but failed (e.g. "File not found").
98 Loading(String),
99}
100
101impl LoadError {
102 /// Returns the (approximate) size of the error message in bytes.
103 pub fn byte_size(&self) -> usize {
104 match self {
105 Self::FormatNotSupported { detected_format }
106 | Self::NoMatchingImageLoader { detected_format } => {
107 detected_format.as_ref().map_or(0, |s| s.len())
108 }
109 Self::Loading(message) => message.len(),
110 _ => core::mem::size_of::<Self>(),
111 }
112 }
113}
114
115impl Display for LoadError {
116 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117 match self {
118 Self::NoImageLoaders => f.write_str(
119 "No image loaders are installed. If you're trying to load some images \
120 for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"),
121
122 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."),
123
124 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."),
125 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."),
126
127 Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"),
128
129 Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"),
130
131 Self::FormatNotSupported { detected_format } => write!(f, "Image format not supported by this loader: {detected_format:?}"),
132
133 Self::Loading(message) => f.write_str(message),
134 }
135 }
136}
137
138impl core::error::Error for LoadError {}
139
140pub type Result<T, E = LoadError> = core::result::Result<T, E>;
141
142/// Given as a hint for image loading requests.
143///
144/// Used mostly for rendering SVG:s to a good size.
145/// The [`SizeHint`] determines at what resolution the image should be rasterized.
146#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
147pub enum SizeHint {
148 /// Scale original size by some factor, keeping the original aspect ratio.
149 ///
150 /// The original size of the image is usually its texel resolution,
151 /// but for an SVG it's the point size of the SVG.
152 ///
153 /// For instance, setting `Scale(2.0)` will rasterize SVG:s to twice their original size,
154 /// which is useful for high-DPI displays.
155 Scale(OrderedFloat<f32>),
156
157 /// Scale to exactly this pixel width, keeping the original aspect ratio.
158 Width(u32),
159
160 /// Scale to exactly this pixel height, keeping the original aspect ratio.
161 Height(u32),
162
163 /// Scale to this pixel size.
164 Size {
165 width: u32,
166 height: u32,
167
168 /// If true, the image will be as large as possible
169 /// while still fitting within the given width/height.
170 maintain_aspect_ratio: bool,
171 },
172}
173
174impl SizeHint {
175 /// Multiply size hint by a factor.
176 pub fn scale_by(self, factor: f32) -> Self {
177 match self {
178 Self::Scale(scale) => Self::Scale(OrderedFloat(factor * scale.0)),
179 Self::Width(width) => Self::Width((factor * width as f32).round() as _),
180 Self::Height(height) => Self::Height((factor * height as f32).round() as _),
181 Self::Size {
182 width,
183 height,
184 maintain_aspect_ratio,
185 } => Self::Size {
186 width: (factor * width as f32).round() as _,
187 height: (factor * height as f32).round() as _,
188 maintain_aspect_ratio,
189 },
190 }
191 }
192}
193
194impl Default for SizeHint {
195 #[inline]
196 fn default() -> Self {
197 Self::Scale(1.0.ord())
198 }
199}
200
201/// Represents a byte buffer.
202///
203/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
204#[derive(Clone)]
205pub enum Bytes {
206 Static(&'static [u8]),
207 Shared(Arc<[u8]>),
208}
209
210impl Debug for Bytes {
211 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212 match self {
213 Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
214 Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
215 }
216 }
217}
218
219impl From<&'static [u8]> for Bytes {
220 #[inline]
221 fn from(value: &'static [u8]) -> Self {
222 Self::Static(value)
223 }
224}
225
226impl<const N: usize> From<&'static [u8; N]> for Bytes {
227 #[inline]
228 fn from(value: &'static [u8; N]) -> Self {
229 Self::Static(value)
230 }
231}
232
233impl From<Arc<[u8]>> for Bytes {
234 #[inline]
235 fn from(value: Arc<[u8]>) -> Self {
236 Self::Shared(value)
237 }
238}
239
240impl From<Vec<u8>> for Bytes {
241 #[inline]
242 fn from(value: Vec<u8>) -> Self {
243 Self::Shared(value.into())
244 }
245}
246
247impl AsRef<[u8]> for Bytes {
248 #[inline]
249 fn as_ref(&self) -> &[u8] {
250 match self {
251 Self::Static(bytes) => bytes,
252 Self::Shared(bytes) => bytes,
253 }
254 }
255}
256
257impl Deref for Bytes {
258 type Target = [u8];
259
260 #[inline]
261 fn deref(&self) -> &Self::Target {
262 self.as_ref()
263 }
264}
265
266/// Represents bytes which are currently being loaded.
267///
268/// This is similar to [`std::task::Poll`], but the `Pending` variant
269/// contains an optional `size`, which may be used during layout to
270/// pre-allocate space the image.
271#[derive(Clone)]
272pub enum BytesPoll {
273 /// Bytes are being loaded.
274 Pending {
275 /// Point size of the image.
276 ///
277 /// Set if known (e.g. from a HTTP header, or by parsing the image file header).
278 size: Option<Vec2>,
279 },
280
281 /// Bytes are loaded.
282 Ready {
283 /// Point size of the image.
284 ///
285 /// Set if known (e.g. from a HTTP header, or by parsing the image file header).
286 size: Option<Vec2>,
287
288 /// File contents, e.g. the contents of a `.png`.
289 bytes: Bytes,
290
291 /// Mime type of the content, e.g. `image/png`.
292 ///
293 /// Set if known (e.g. from `Content-Type` HTTP header).
294 mime: Option<String>,
295 },
296}
297
298/// Used to get a unique ID when implementing one of the loader traits: [`BytesLoader::id`], [`ImageLoader::id`], and [`TextureLoader::id`].
299///
300/// This just expands to `module_path!()` concatenated with the given type name.
301#[macro_export]
302macro_rules! generate_loader_id {
303 ($ty:ident) => {
304 concat!(module_path!(), "::", stringify!($ty))
305 };
306}
307pub use crate::generate_loader_id;
308
309/// Does the given URI end with the given file extension?
310///
311/// The comparison ignores ASCII case and any `#fragment` at the end of the URI,
312/// so `has_extension("cat.GIF#frame=2", "gif")` is `true`.
313///
314/// This is useful when implementing an [`ImageLoader`].
315pub fn has_extension(uri: &str, extension: &str) -> bool {
316 let path = uri.split('#').next().unwrap_or(uri);
317 std::path::Path::new(path)
318 .extension()
319 .is_some_and(|found| found.eq_ignore_ascii_case(extension))
320}
321
322pub type BytesLoadResult = Result<BytesPoll>;
323
324/// Represents a loader capable of loading raw unstructured bytes from somewhere,
325/// e.g. from disk or network.
326///
327/// It should also provide any subsequent loaders a hint for what the bytes may
328/// represent using [`BytesPoll::Ready::mime`], if it can be inferred.
329///
330/// Implementations are expected to cache at least each `URI`.
331pub trait BytesLoader {
332 /// Unique ID of this loader.
333 ///
334 /// To reduce the chance of collisions, use [`generate_loader_id`] for this.
335 fn id(&self) -> &str;
336
337 /// Try loading the bytes from the given uri.
338 ///
339 /// Implementations should call `ctx.request_repaint` to wake up the ui
340 /// once the data is ready.
341 ///
342 /// The implementation should cache any result, so that calling this
343 /// is immediate-mode safe.
344 ///
345 /// # Errors
346 /// This may fail with:
347 /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
348 /// - [`LoadError::Loading`] if the loading process failed.
349 fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
350
351 /// Forget the given `uri`.
352 ///
353 /// If `uri` is cached, it should be evicted from cache,
354 /// so that it may be fully reloaded.
355 fn forget(&self, uri: &str);
356
357 /// Forget all URIs ever given to this loader.
358 ///
359 /// If the loader caches any URIs, the entire cache should be cleared,
360 /// so that all of them may be fully reloaded.
361 fn forget_all(&self);
362
363 /// Implementations may use this to perform work at the end of a frame,
364 /// such as evicting unused entries from a cache.
365 fn end_pass(&self, pass_index: u64) {
366 let _ = pass_index;
367 }
368
369 /// If the loader caches any data, this should return the size of that cache.
370 fn byte_size(&self) -> usize;
371
372 /// Returns `true` if some data is currently being loaded.
373 fn has_pending(&self) -> bool {
374 false
375 }
376}
377
378/// Represents an image which is currently being loaded.
379///
380/// This is similar to [`std::task::Poll`], but the `Pending` variant
381/// contains an optional `size`, which may be used during layout to
382/// pre-allocate space the image.
383#[derive(Clone)]
384pub enum ImagePoll {
385 /// Image is loading.
386 Pending {
387 /// Point size of the image.
388 ///
389 /// Set if known (e.g. from a HTTP header, or by parsing the image file header).
390 size: Option<Vec2>,
391 },
392
393 /// Image is loaded.
394 Ready { image: Arc<ColorImage> },
395}
396
397pub type ImageLoadResult = Result<ImagePoll>;
398
399/// An `ImageLoader` decodes raw bytes into a [`ColorImage`].
400///
401/// Implementations are expected to cache at least each `URI`.
402pub trait ImageLoader: core::any::Any {
403 /// Unique ID of this loader.
404 ///
405 /// To reduce the chance of collisions, include `module_path!()` as part of this ID.
406 ///
407 /// For example: `concat!(module_path!(), "::MyLoader")`
408 /// for `my_crate::my_loader::MyLoader`.
409 fn id(&self) -> &str;
410
411 /// Try loading the image from the given uri.
412 ///
413 /// Implementations should call `ctx.request_repaint` to wake up the ui
414 /// once the image is ready.
415 ///
416 /// The implementation should cache any result, so that calling this
417 /// is immediate-mode safe.
418 ///
419 /// # Errors
420 /// This may fail with:
421 /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
422 /// - [`LoadError::Loading`] if the loading process failed.
423 fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
424
425 /// Forget the given `uri`.
426 ///
427 /// If `uri` is cached, it should be evicted from cache,
428 /// so that it may be fully reloaded.
429 fn forget(&self, uri: &str);
430
431 /// Forget all URIs ever given to this loader.
432 ///
433 /// If the loader caches any URIs, the entire cache should be cleared,
434 /// so that all of them may be fully reloaded.
435 fn forget_all(&self);
436
437 /// Implementations may use this to perform work at the end of a pass,
438 /// such as evicting unused entries from a cache.
439 fn end_pass(&self, pass_index: u64) {
440 let _ = pass_index;
441 }
442
443 /// If the loader caches any data, this should return the size of that cache.
444 fn byte_size(&self) -> usize;
445
446 /// Returns `true` if some image is currently being loaded.
447 ///
448 /// NOTE: You probably also want to check [`BytesLoader::has_pending`].
449 fn has_pending(&self) -> bool {
450 false
451 }
452}
453
454/// A texture with a known size.
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub struct SizedTexture {
457 pub id: TextureId,
458
459 /// Point size of the original SVG, or the size of the image in texels.
460 pub size: Vec2,
461}
462
463impl SizedTexture {
464 /// Create a [`SizedTexture`] from a texture `id` with a specific `size`.
465 pub fn new(id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
466 Self {
467 id: id.into(),
468 size: size.into(),
469 }
470 }
471
472 /// Fetch the [id][`SizedTexture::id`] and [size][`SizedTexture::size`] from a [`TextureHandle`].
473 pub fn from_handle(handle: &TextureHandle) -> Self {
474 let size = handle.size();
475 Self {
476 id: handle.id(),
477 size: Vec2::new(size[0] as f32, size[1] as f32),
478 }
479 }
480}
481
482impl From<(TextureId, Vec2)> for SizedTexture {
483 #[inline]
484 fn from((id, size): (TextureId, Vec2)) -> Self {
485 Self { id, size }
486 }
487}
488
489impl<'a> From<&'a TextureHandle> for SizedTexture {
490 #[inline]
491 fn from(handle: &'a TextureHandle) -> Self {
492 Self::from_handle(handle)
493 }
494}
495
496/// Represents a texture is currently being loaded.
497///
498/// This is similar to [`std::task::Poll`], but the `Pending` variant
499/// contains an optional `size`, which may be used during layout to
500/// pre-allocate space the image.
501#[derive(Clone, Copy)]
502pub enum TexturePoll {
503 /// Texture is loading.
504 Pending {
505 /// Point size of the image.
506 ///
507 /// Set if known (e.g. from a HTTP header, or by parsing the image file header).
508 size: Option<Vec2>,
509 },
510
511 /// Texture is loaded.
512 Ready { texture: SizedTexture },
513}
514
515impl TexturePoll {
516 /// Point size of the original SVG, or the size of the image in texels.
517 #[inline]
518 pub fn size(&self) -> Option<Vec2> {
519 match self {
520 Self::Pending { size } => *size,
521 Self::Ready { texture } => Some(texture.size),
522 }
523 }
524
525 #[inline]
526 pub fn texture_id(&self) -> Option<TextureId> {
527 match self {
528 Self::Pending { .. } => None,
529 Self::Ready { texture } => Some(texture.id),
530 }
531 }
532
533 #[inline]
534 pub fn is_pending(&self) -> bool {
535 matches!(self, Self::Pending { .. })
536 }
537
538 #[inline]
539 pub fn is_ready(&self) -> bool {
540 matches!(self, Self::Ready { .. })
541 }
542}
543
544pub type TextureLoadResult = Result<TexturePoll>;
545
546/// A `TextureLoader` uploads a [`ColorImage`] to the GPU, returning a [`SizedTexture`].
547///
548/// `egui` comes with an implementation that uses [`Context::load_texture`],
549/// which just asks the egui backend to upload the image to the GPU.
550///
551/// You can implement this trait if you do your own uploading of images to the GPU.
552/// For instance, you can use this to refer to textures in a game engine that egui
553/// doesn't otherwise know about.
554///
555/// Implementations are expected to cache each combination of `(URI, TextureOptions)`.
556pub trait TextureLoader {
557 /// Unique ID of this loader.
558 ///
559 /// To reduce the chance of collisions, include `module_path!()` as part of this ID.
560 ///
561 /// For example: `concat!(module_path!(), "::MyLoader")`
562 /// for `my_crate::my_loader::MyLoader`.
563 fn id(&self) -> &str;
564
565 /// Try loading the texture from the given uri.
566 ///
567 /// Implementations should call `ctx.request_repaint` to wake up the ui
568 /// once the texture is ready.
569 ///
570 /// The implementation should cache any result, so that calling this
571 /// is immediate-mode safe.
572 ///
573 /// # Errors
574 /// This may fail with:
575 /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
576 /// - [`LoadError::Loading`] if the loading process failed.
577 fn load(
578 &self,
579 ctx: &Context,
580 uri: &str,
581 texture_options: TextureOptions,
582 size_hint: SizeHint,
583 ) -> TextureLoadResult;
584
585 /// Forget the given `uri`.
586 ///
587 /// If `uri` is cached, it should be evicted from cache,
588 /// so that it may be fully reloaded.
589 fn forget(&self, uri: &str);
590
591 /// Forget all URIs ever given to this loader.
592 ///
593 /// If the loader caches any URIs, the entire cache should be cleared,
594 /// so that all of them may be fully reloaded.
595 fn forget_all(&self);
596
597 /// Implementations may use this to perform work at the end of a pass,
598 /// such as evicting unused entries from a cache.
599 fn end_pass(&self, pass_index: u64) {
600 let _ = pass_index;
601 }
602
603 /// If the loader caches any data, this should return the size of that cache.
604 fn byte_size(&self) -> usize;
605}
606
607type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
608type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
609type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
610
611#[derive(Clone)]
612/// The loaders of bytes, images, and textures.
613pub struct Loaders {
614 pub include: Arc<DefaultBytesLoader>,
615 pub bytes: Mutex<Vec<BytesLoaderImpl>>,
616 pub image: Mutex<Vec<ImageLoaderImpl>>,
617 pub texture: Mutex<Vec<TextureLoaderImpl>>,
618}
619
620impl Default for Loaders {
621 fn default() -> Self {
622 let include = Arc::new(DefaultBytesLoader::default());
623 Self {
624 bytes: Mutex::new(vec![Arc::clone(&include) as _]),
625 image: Mutex::new(Vec::new()),
626 // By default we only include `DefaultTextureLoader`.
627 texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]),
628 include,
629 }
630 }
631}
632
633impl Loaders {
634 /// The given pass has just ended.
635 pub fn end_pass(&self, pass_index: u64) {
636 let Self {
637 include,
638 bytes,
639 image,
640 texture,
641 } = self;
642
643 include.end_pass(pass_index);
644 for loader in bytes.lock().iter() {
645 loader.end_pass(pass_index);
646 }
647 for loader in image.lock().iter() {
648 loader.end_pass(pass_index);
649 }
650 for loader in texture.lock().iter() {
651 loader.end_pass(pass_index);
652 }
653 }
654}
655
656#[test]
657fn test_has_extension() {
658 assert!(has_extension("cat.svg", "svg"));
659 assert!(has_extension("cat.SVG", "svg"));
660 assert!(has_extension("http://example.com/cat.gif#frame=2", "gif"));
661 assert!(!has_extension("cat.svg.png", "svg"));
662 assert!(!has_extension("svg", "svg"));
663 assert!(!has_extension("cat.jpeg", "jpg"));
664 assert!(!has_extension("cat.svg?v=1", "svg"));
665}