diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index aef212100c21b..3654af26699de 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -24,7 +24,7 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor}; pub const TEXTURE_ASSET_INDEX: u64 = 0; pub const SAMPLER_ASSET_INDEX: u64 = 1; -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] pub enum ImageFormat { Avif, Basis, @@ -45,6 +45,9 @@ pub enum ImageFormat { } impl ImageFormat { + /// Returns the image format for the given mime type. + /// + /// Unsupported formats will become [`None`]. pub fn from_mime_type(mime_type: &str) -> Option { Some(match mime_type.to_ascii_lowercase().as_str() { "image/bmp" | "image/x-bmp" => ImageFormat::Bmp, @@ -58,6 +61,9 @@ impl ImageFormat { }) } + /// Returns the image format for the given extension. + /// + /// Unsupported formats will become [`None`]. pub fn from_extension(extension: &str) -> Option { Some(match extension.to_ascii_lowercase().as_str() { "avif" => ImageFormat::Avif, @@ -79,9 +85,13 @@ impl ImageFormat { _ => return None, }) } +} + +impl TryFrom for image::ImageFormat { + type Error = (); - pub fn as_image_crate_format(&self) -> Option { - Some(match self { + fn try_from(value: ImageFormat) -> Result { + Ok(match value { ImageFormat::Avif => image::ImageFormat::Avif, ImageFormat::Bmp => image::ImageFormat::Bmp, ImageFormat::Dds => image::ImageFormat::Dds, @@ -96,7 +106,31 @@ impl ImageFormat { ImageFormat::Tga => image::ImageFormat::Tga, ImageFormat::Tiff => image::ImageFormat::Tiff, ImageFormat::WebP => image::ImageFormat::WebP, - ImageFormat::Basis | ImageFormat::Ktx2 => return None, + ImageFormat::Basis | ImageFormat::Ktx2 => return Err(()), + }) + } +} + +impl TryFrom for ImageFormat { + type Error = (); + + fn try_from(value: image::ImageFormat) -> Result { + Ok(match value { + image::ImageFormat::Avif => ImageFormat::Avif, + image::ImageFormat::Bmp => ImageFormat::Bmp, + image::ImageFormat::Dds => ImageFormat::Dds, + image::ImageFormat::Farbfeld => ImageFormat::Farbfeld, + image::ImageFormat::Gif => ImageFormat::Gif, + image::ImageFormat::OpenExr => ImageFormat::OpenExr, + image::ImageFormat::Hdr => ImageFormat::Hdr, + image::ImageFormat::Ico => ImageFormat::Ico, + image::ImageFormat::Jpeg => ImageFormat::Jpeg, + image::ImageFormat::Png => ImageFormat::Png, + image::ImageFormat::Pnm => ImageFormat::Pnm, + image::ImageFormat::Tga => ImageFormat::Tga, + image::ImageFormat::Tiff => ImageFormat::Tiff, + image::ImageFormat::WebP => ImageFormat::WebP, + _ => return Err(()), }) } } @@ -678,8 +712,8 @@ impl Image { } _ => { let image_crate_format = format - .as_image_crate_format() - .ok_or_else(|| TextureError::UnsupportedTextureFormat(format!("{format:?}")))?; + .try_into() + .map_err(|_| TextureError::UnsupportedTextureFormat(format!("{format:?}")))?; let mut reader = image::io::Reader::new(std::io::Cursor::new(buffer)); reader.set_format(image_crate_format); reader.no_limits(); @@ -734,6 +768,8 @@ pub enum TextureError { InvalidImageMimeType(String), #[error("invalid image extension: {0}")] InvalidImageExtension(String), + #[error("invalid image content type: {0:?}")] + InvalidImageContentType(image::ImageFormat), #[error("failed to load an image: {0}")] ImageError(#[from] image::ImageError), #[error("unsupported texture format: {0}")] diff --git a/crates/bevy_render/src/texture/image_loader.rs b/crates/bevy_render/src/texture/image_loader.rs index 44a4fdb9251cf..da46cbe195972 100644 --- a/crates/bevy_render/src/texture/image_loader.rs +++ b/crates/bevy_render/src/texture/image_loader.rs @@ -1,5 +1,6 @@ use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; use bevy_ecs::prelude::{FromWorld, World}; +use bevy_log::warn; use thiserror::Error; use crate::{ @@ -18,6 +19,8 @@ pub struct ImageLoader { } pub(crate) const IMG_FILE_EXTENSIONS: &[&str] = &[ + // special extension that doesn't map to any particular format. useful for loading assets of unknown format with ImageFormatSetting::FromContent + "image", #[cfg(feature = "basis-universal")] "basis", #[cfg(feature = "bmp")] @@ -46,10 +49,17 @@ pub(crate) const IMG_FILE_EXTENSIONS: &[&str] = &[ "ppm", ]; +/// The method for determining the [`ImageFormat`] of the loaded image. +/// +/// By default we attempt to determine the format automatically from the header block of the binary data. #[derive(Serialize, Deserialize, Default, Debug)] pub enum ImageFormatSetting { + /// Determine the image format by inspecting the header block of the image binary data. #[default] + FromContent, + /// Determine the image format from the filename extension. FromExtension, + /// Specify an explicit format for the image. Format(ImageFormat), } @@ -98,6 +108,29 @@ impl AssetLoader for ImageLoader { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; let image_type = match settings.format { + ImageFormatSetting::FromContent => { + let image_crate_format = + image::guess_format(&bytes).map_err(|err| FileTextureError { + error: TextureError::ImageError(err), + path: load_context.path().display().to_string(), + })?; + + let format = image_crate_format + .try_into() + .map_err(|_| FileTextureError { + error: TextureError::InvalidImageContentType(image_crate_format), + path: load_context.path().display().to_string(), + })?; + + // validate that the file format matches the content + if let Some(ext_format) = ImageFormat::from_extension(ext) { + if ext_format != format { + warn!("mismatched format for {}, filename extension `{}` has content of type {:?}", load_context.path().display(), ext, format); + } + } + + ImageType::Format(format) + } ImageFormatSetting::FromExtension => ImageType::Extension(ext), ImageFormatSetting::Format(format) => ImageType::Format(format), }; @@ -113,7 +146,7 @@ impl AssetLoader for ImageLoader { ) .map_err(|err| FileTextureError { error: err, - path: format!("{}", load_context.path().display()), + path: load_context.path().display().to_string(), })?) }) }