Skip to main content

bevy_image/
image_loader.rs

1use crate::{
2    image::{Image, ImageFormat, ImageType, TextureError},
3    TextureReinterpretationError,
4};
5use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages};
6use bevy_reflect::TypePath;
7use thiserror::Error;
8
9use super::{CompressedImageFormats, ImageSampler};
10use serde::{Deserialize, Serialize};
11
12/// Loader for images that can be read by the `image` crate.
13#[derive(Clone, TypePath)]
14pub struct ImageLoader {
15    supported_compressed_formats: CompressedImageFormats,
16}
17
18impl ImageLoader {
19    /// Full list of supported formats.
20    pub const SUPPORTED_FORMATS: &'static [ImageFormat] = &[
21        #[cfg(feature = "basis-universal")]
22        ImageFormat::Basis,
23        #[cfg(feature = "bmp")]
24        ImageFormat::Bmp,
25        #[cfg(feature = "dds")]
26        ImageFormat::Dds,
27        #[cfg(feature = "ff")]
28        ImageFormat::Farbfeld,
29        #[cfg(feature = "gif")]
30        ImageFormat::Gif,
31        #[cfg(feature = "ico")]
32        ImageFormat::Ico,
33        #[cfg(feature = "jpeg")]
34        ImageFormat::Jpeg,
35        #[cfg(feature = "ktx2")]
36        ImageFormat::Ktx2,
37        #[cfg(feature = "png")]
38        ImageFormat::Png,
39        #[cfg(feature = "pnm")]
40        ImageFormat::Pnm,
41        #[cfg(feature = "qoi")]
42        ImageFormat::Qoi,
43        #[cfg(feature = "tga")]
44        ImageFormat::Tga,
45        #[cfg(feature = "tiff")]
46        ImageFormat::Tiff,
47        #[cfg(feature = "webp")]
48        ImageFormat::WebP,
49    ];
50
51    /// Total count of file extensions, for computing supported file extensions list.
52    const COUNT_FILE_EXTENSIONS: usize = {
53        let mut count = 0;
54        let mut idx = 0;
55        while idx < Self::SUPPORTED_FORMATS.len() {
56            count += Self::SUPPORTED_FORMATS[idx].to_file_extensions().len();
57            idx += 1;
58        }
59        count
60    };
61
62    /// Gets the list of file extensions for all formats.
63    pub const SUPPORTED_FILE_EXTENSIONS: &'static [&'static str] = &{
64        let mut exts = [""; Self::COUNT_FILE_EXTENSIONS];
65        let mut ext_idx = 0;
66        let mut fmt_idx = 0;
67        while fmt_idx < Self::SUPPORTED_FORMATS.len() {
68            let mut off = 0;
69            let fmt_exts = Self::SUPPORTED_FORMATS[fmt_idx].to_file_extensions();
70            while off < fmt_exts.len() {
71                exts[ext_idx] = fmt_exts[off];
72                off += 1;
73                ext_idx += 1;
74            }
75            fmt_idx += 1;
76        }
77        exts
78    };
79
80    /// Creates a new image loader that supports the provided formats.
81    pub fn new(supported_compressed_formats: CompressedImageFormats) -> Self {
82        Self {
83            supported_compressed_formats,
84        }
85    }
86}
87
88/// How to determine an image's format when loading.
89#[derive(Serialize, Deserialize, Default, Debug, Clone)]
90pub enum ImageFormatSetting {
91    /// Determine the image format from its file extension.
92    ///
93    /// This is the default.
94    #[default]
95    FromExtension,
96    /// Declare the image format explicitly.
97    Format(ImageFormat),
98    /// Guess the image format by looking for magic bytes at the
99    /// beginning of its data.
100    Guess,
101}
102
103/// How to interpret the image as an array of textures.
104#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
105pub enum ImageArrayLayout {
106    /// Interpret the image as a vertical stack of *n* images.
107    RowCount {
108        /// The total number of images in the stack.
109        rows: u32,
110    },
111    /// Interpret the image as a vertical stack of images, each *n* pixels tall.
112    RowHeight {
113        /// The height of a single image in the stack.
114        pixels: u32,
115    },
116    /// Interpret the image as a grid of images, with **n** columns and rows.
117    GridCount {
118        /// The number of columns in the grid.
119        columns: u32,
120        /// The number of rows in the grid.
121        rows: u32,
122    },
123    /// Interpret the image as a grid of images where each tile is **n** pixels wide/tall.
124    GridSize {
125        /// The width of a single tile in pixels.
126        tile_width_pixels: u32,
127        /// The height of a single tile in pixels.
128        tile_height_pixels: u32,
129    },
130}
131
132/// Settings for loading an [`Image`] using an [`ImageLoader`].
133#[derive(Serialize, Deserialize, Debug, Clone)]
134pub struct ImageLoaderSettings {
135    /// How to determine the image's container format.
136    pub format: ImageFormatSetting,
137    /// Forcibly use a specific [`wgpu_types::TextureFormat`].
138    /// Useful to control how data is handled when used
139    /// in a shader.
140    /// Ex: data that would be `R16Uint` that needs to
141    /// be sampled as a float using `R16Snorm`.
142    /// Grayscale images are not expanded to RGBA when this format has one or two channels.
143    #[serde(default)]
144    pub texture_format: Option<wgpu_types::TextureFormat>,
145    /// Specifies whether image data is linear
146    /// or in sRGB space when this is not determined by
147    /// the image format.
148    pub is_srgb: bool,
149    /// [`ImageSampler`] to use when rendering - this does
150    /// not affect the loading of the image data.
151    pub sampler: ImageSampler,
152    /// Where the asset will be used - see the docs on
153    /// [`RenderAssetUsages`] for details.
154    pub asset_usage: RenderAssetUsages,
155    /// Interpret the image as an array of images. This is
156    /// primarily for use with the `texture2DArray` shader
157    /// uniform type.
158    #[serde(default)]
159    pub array_layout: Option<ImageArrayLayout>,
160    /// Whether to expand grayscale images to RGBA.
161    /// When `false`, grayscale images load as `R8Unorm`, `Rg8Unorm`, `R16Unorm` or `Rg16Unorm`
162    /// and `is_srgb` is ignored.
163    #[serde(default = "default_expand_grayscale")]
164    pub expand_grayscale: bool,
165}
166
167fn default_expand_grayscale() -> bool {
168    true
169}
170
171impl Default for ImageLoaderSettings {
172    fn default() -> Self {
173        Self {
174            format: ImageFormatSetting::default(),
175            texture_format: None,
176            is_srgb: true,
177            sampler: ImageSampler::Default,
178            asset_usage: RenderAssetUsages::default(),
179            array_layout: None,
180            expand_grayscale: true,
181        }
182    }
183}
184
185/// An error when loading an image using [`ImageLoader`].
186#[non_exhaustive]
187#[derive(Debug, Error)]
188pub enum ImageLoaderError {
189    /// An error occurred while trying to load the image bytes.
190    #[error("Failed to load image bytes: {0}")]
191    Io(#[from] std::io::Error),
192    /// An error occurred while trying to decode the image bytes.
193    #[error("Could not load texture file: {0}")]
194    FileTexture(#[from] FileTextureError),
195    /// An error occurred while trying to interpret the image bytes as an array texture.
196    #[error("Invalid array layout: {0}")]
197    ArrayLayout(#[from] TextureReinterpretationError),
198}
199
200impl AssetLoader for ImageLoader {
201    type Asset = Image;
202    type Settings = ImageLoaderSettings;
203    type Error = ImageLoaderError;
204    async fn load(
205        &self,
206        reader: &mut dyn Reader,
207        settings: &ImageLoaderSettings,
208        load_context: &mut LoadContext<'_>,
209    ) -> Result<Image, Self::Error> {
210        let mut bytes = Vec::new();
211        reader.read_to_end(&mut bytes).await?;
212        let image_type = match settings.format {
213            ImageFormatSetting::FromExtension => {
214                // use the file extension for the image type
215                let ext = load_context
216                    .path()
217                    .path()
218                    .extension()
219                    .unwrap()
220                    .to_str()
221                    .unwrap();
222                ImageType::Extension(ext)
223            }
224            ImageFormatSetting::Format(format) => ImageType::Format(format),
225            ImageFormatSetting::Guess => {
226                let format = image::guess_format(&bytes).map_err(|err| FileTextureError {
227                    error: err.into(),
228                    path: format!("{}", load_context.path().path().display()),
229                })?;
230                ImageType::Format(ImageFormat::from_image_crate_format(format).ok_or_else(
231                    || FileTextureError {
232                        error: TextureError::UnsupportedTextureFormat(format!("{format:?}")),
233                        path: format!("{}", load_context.path().path().display()),
234                    },
235                )?)
236            }
237        };
238
239        let expand_grayscale = settings.expand_grayscale
240            && !settings.texture_format.is_some_and(|f| f.components() <= 2);
241        let mut image = Image::from_buffer_inner(
242            &bytes,
243            image_type,
244            self.supported_compressed_formats,
245            settings.is_srgb,
246            expand_grayscale,
247            settings.sampler.clone(),
248            settings.asset_usage,
249        )
250        .map_err(|err| FileTextureError {
251            error: err,
252            path: format!("{}", load_context.path().path().display()),
253        })?;
254
255        if let Some(format) = settings.texture_format {
256            image.texture_descriptor.format = format;
257        }
258
259        if let Some(array_layout) = settings.array_layout {
260            let image = match array_layout {
261                ImageArrayLayout::RowCount { rows } => {
262                    image.reinterpret_stacked_2d_as_array(rows)?;
263                    image
264                }
265                ImageArrayLayout::RowHeight { pixels } => {
266                    image.reinterpret_stacked_2d_as_array(image.height() / pixels)?;
267                    image
268                }
269                ImageArrayLayout::GridCount { columns, rows } => {
270                    image.create_stacked_array_from_2d_grid(rows, columns)?
271                }
272                ImageArrayLayout::GridSize {
273                    tile_width_pixels,
274                    tile_height_pixels,
275                } => image.create_stacked_array_from_2d_grid(
276                    image.height() / tile_height_pixels,
277                    image.width() / tile_width_pixels,
278                )?,
279            };
280            return Ok(image);
281        }
282        Ok(image)
283    }
284
285    fn extensions(&self) -> &[&str] {
286        Self::SUPPORTED_FILE_EXTENSIONS
287    }
288}
289
290/// An error that occurs when loading a texture from a file.
291#[derive(Error, Debug)]
292#[error("Error reading image file {path}: {error}.")]
293pub struct FileTextureError {
294    error: TextureError,
295    path: String,
296}
297
298#[cfg(test)]
299mod tests {
300    use super::ImageLoaderSettings;
301
302    #[test]
303    fn missing_expand_grayscale_defaults_to_true() {
304        let mut value = serde_json::to_value(ImageLoaderSettings::default()).unwrap();
305        value.as_object_mut().unwrap().remove("expand_grayscale");
306        let settings: ImageLoaderSettings = serde_json::from_value(value).unwrap();
307        assert!(settings.expand_grayscale);
308    }
309}