Skip to main content

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}