bevy_image/
image_loader.rs

1use crate::image::{Image, ImageFormat, ImageType, TextureError};
2use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages};
3use thiserror::Error;
4
5use super::{CompressedImageFormats, ImageSampler};
6use serde::{Deserialize, Serialize};
7
8/// Loader for images that can be read by the `image` crate.
9#[derive(Clone)]
10pub struct ImageLoader {
11    supported_compressed_formats: CompressedImageFormats,
12}
13
14impl ImageLoader {
15    /// Full list of supported formats.
16    pub const SUPPORTED_FORMATS: &'static [ImageFormat] = &[
17        #[cfg(feature = "basis-universal")]
18        ImageFormat::Basis,
19        #[cfg(feature = "bmp")]
20        ImageFormat::Bmp,
21        #[cfg(feature = "dds")]
22        ImageFormat::Dds,
23        #[cfg(feature = "ff")]
24        ImageFormat::Farbfeld,
25        #[cfg(feature = "gif")]
26        ImageFormat::Gif,
27        #[cfg(feature = "ico")]
28        ImageFormat::Ico,
29        #[cfg(feature = "jpeg")]
30        ImageFormat::Jpeg,
31        #[cfg(feature = "ktx2")]
32        ImageFormat::Ktx2,
33        #[cfg(feature = "png")]
34        ImageFormat::Png,
35        #[cfg(feature = "pnm")]
36        ImageFormat::Pnm,
37        #[cfg(feature = "qoi")]
38        ImageFormat::Qoi,
39        #[cfg(feature = "tga")]
40        ImageFormat::Tga,
41        #[cfg(feature = "tiff")]
42        ImageFormat::Tiff,
43        #[cfg(feature = "webp")]
44        ImageFormat::WebP,
45    ];
46
47    /// Total count of file extensions, for computing supported file extensions list.
48    const COUNT_FILE_EXTENSIONS: usize = {
49        let mut count = 0;
50        let mut idx = 0;
51        while idx < Self::SUPPORTED_FORMATS.len() {
52            count += Self::SUPPORTED_FORMATS[idx].to_file_extensions().len();
53            idx += 1;
54        }
55        count
56    };
57
58    /// Gets the list of file extensions for all formats.
59    pub const SUPPORTED_FILE_EXTENSIONS: &'static [&'static str] = &{
60        let mut exts = [""; Self::COUNT_FILE_EXTENSIONS];
61        let mut ext_idx = 0;
62        let mut fmt_idx = 0;
63        while fmt_idx < Self::SUPPORTED_FORMATS.len() {
64            let mut off = 0;
65            let fmt_exts = Self::SUPPORTED_FORMATS[fmt_idx].to_file_extensions();
66            while off < fmt_exts.len() {
67                exts[ext_idx] = fmt_exts[off];
68                off += 1;
69                ext_idx += 1;
70            }
71            fmt_idx += 1;
72        }
73        exts
74    };
75
76    /// Creates a new image loader that supports the provided formats.
77    pub fn new(supported_compressed_formats: CompressedImageFormats) -> Self {
78        Self {
79            supported_compressed_formats,
80        }
81    }
82}
83
84/// How to determine an image's format when loading.
85#[derive(Serialize, Deserialize, Default, Debug, Clone)]
86pub enum ImageFormatSetting {
87    /// Determine the image format from its file extension.
88    ///
89    /// This is the default.
90    #[default]
91    FromExtension,
92    /// Declare the image format explicitly.
93    Format(ImageFormat),
94    /// Guess the image format by looking for magic bytes at the
95    /// beginning of its data.
96    Guess,
97}
98
99/// Settings for loading an [`Image`] using an [`ImageLoader`].
100#[derive(Serialize, Deserialize, Debug, Clone)]
101pub struct ImageLoaderSettings {
102    /// How to determine the image's container format.
103    pub format: ImageFormatSetting,
104    /// Forcibly use a specific [`wgpu_types::TextureFormat`].
105    /// Useful to control how data is handled when used
106    /// in a shader.
107    /// Ex: data that would be `R16Uint` that needs to
108    /// be sampled as a float using `R16Snorm`.
109    #[serde(skip)]
110    pub texture_format: Option<wgpu_types::TextureFormat>,
111    /// Specifies whether image data is linear
112    /// or in sRGB space when this is not determined by
113    /// the image format.
114    pub is_srgb: bool,
115    /// [`ImageSampler`] to use when rendering - this does
116    /// not affect the loading of the image data.
117    pub sampler: ImageSampler,
118    /// Where the asset will be used - see the docs on
119    /// [`RenderAssetUsages`] for details.
120    pub asset_usage: RenderAssetUsages,
121}
122
123impl Default for ImageLoaderSettings {
124    fn default() -> Self {
125        Self {
126            format: ImageFormatSetting::default(),
127            texture_format: None,
128            is_srgb: true,
129            sampler: ImageSampler::Default,
130            asset_usage: RenderAssetUsages::default(),
131        }
132    }
133}
134
135/// An error when loading an image using [`ImageLoader`].
136#[non_exhaustive]
137#[derive(Debug, Error)]
138pub enum ImageLoaderError {
139    /// An error occurred while trying to load the image bytes.
140    #[error("Failed to load image bytes: {0}")]
141    Io(#[from] std::io::Error),
142    /// An error occurred while trying to decode the image bytes.
143    #[error("Could not load texture file: {0}")]
144    FileTexture(#[from] FileTextureError),
145}
146
147impl AssetLoader for ImageLoader {
148    type Asset = Image;
149    type Settings = ImageLoaderSettings;
150    type Error = ImageLoaderError;
151    async fn load(
152        &self,
153        reader: &mut dyn Reader,
154        settings: &ImageLoaderSettings,
155        load_context: &mut LoadContext<'_>,
156    ) -> Result<Image, Self::Error> {
157        let mut bytes = Vec::new();
158        reader.read_to_end(&mut bytes).await?;
159        let image_type = match settings.format {
160            ImageFormatSetting::FromExtension => {
161                // use the file extension for the image type
162                let ext = load_context.path().extension().unwrap().to_str().unwrap();
163                ImageType::Extension(ext)
164            }
165            ImageFormatSetting::Format(format) => ImageType::Format(format),
166            ImageFormatSetting::Guess => {
167                let format = image::guess_format(&bytes).map_err(|err| FileTextureError {
168                    error: err.into(),
169                    path: format!("{}", load_context.path().display()),
170                })?;
171                ImageType::Format(ImageFormat::from_image_crate_format(format).ok_or_else(
172                    || FileTextureError {
173                        error: TextureError::UnsupportedTextureFormat(format!("{format:?}")),
174                        path: format!("{}", load_context.path().display()),
175                    },
176                )?)
177            }
178        };
179        Ok(Image::from_buffer(
180            &bytes,
181            image_type,
182            self.supported_compressed_formats,
183            settings.is_srgb,
184            settings.sampler.clone(),
185            settings.asset_usage,
186        )
187        .map(|mut image| {
188            if let Some(format) = settings.texture_format {
189                image.texture_descriptor.format = format;
190            }
191            image
192        })
193        .map_err(|err| FileTextureError {
194            error: err,
195            path: format!("{}", load_context.path().display()),
196        })?)
197    }
198
199    fn extensions(&self) -> &[&str] {
200        Self::SUPPORTED_FILE_EXTENSIONS
201    }
202}
203
204/// An error that occurs when loading a texture from a file.
205#[derive(Error, Debug)]
206#[error("Error reading image file {path}: {error}.")]
207pub struct FileTextureError {
208    error: TextureError,
209    path: String,
210}