diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index bb7138bddf022e..132bcceea45f90 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -5,7 +5,6 @@ use super::dds::*; #[cfg(feature = "ktx2")] use super::ktx2::*; -use super::image_texture_conversion::image_to_texture; use crate::{ render_asset::{PrepareAssetError, RenderAsset}, render_resource::{Sampler, Texture, TextureView}, @@ -342,13 +341,18 @@ impl Image { }); } - /// Convert a texture from a format to another - /// Only a few formats are supported as input and output: + /// Convert a texture from a format to another. Only a few formats are + /// supported as input and output: /// - `TextureFormat::R8Unorm` /// - `TextureFormat::Rg8Unorm` /// - `TextureFormat::Rgba8UnormSrgb` + /// + /// To get [`Image`] as a [`image::DynamicImage`] see: + /// [`Image::try_into_dynamic`]. pub fn convert(&self, new_format: TextureFormat) -> Option { - super::image_texture_conversion::texture_to_image(self) + self.clone() + .try_into_dynamic() + .ok() .and_then(|img| match new_format { TextureFormat::R8Unorm => { Some((image::DynamicImage::ImageLuma8(img.into_luma8()), false)) @@ -362,9 +366,7 @@ impl Image { } _ => None, }) - .map(|(dyn_img, is_srgb)| { - super::image_texture_conversion::image_to_texture(dyn_img, is_srgb) - }) + .map(|(dyn_img, is_srgb)| Self::from_dynamic(dyn_img, is_srgb)) } /// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image` @@ -402,7 +404,7 @@ impl Image { reader.set_format(image_crate_format); reader.no_limits(); let dyn_img = reader.decode()?; - Ok(image_to_texture(dyn_img, is_srgb)) + Ok(Self::from_dynamic(dyn_img, is_srgb)) } } } diff --git a/crates/bevy_render/src/texture/image_texture_conversion.rs b/crates/bevy_render/src/texture/image_texture_conversion.rs index 44611faafcddb0..45ae5bbc225820 100644 --- a/crates/bevy_render/src/texture/image_texture_conversion.rs +++ b/crates/bevy_render/src/texture/image_texture_conversion.rs @@ -1,194 +1,233 @@ use crate::texture::{Image, TextureFormatPixelInfo}; +use anyhow::anyhow; use image::{DynamicImage, ImageBuffer}; use wgpu::{Extent3d, TextureDimension, TextureFormat}; -// TODO: fix name? -/// Converts a [`DynamicImage`] to an [`Image`]. -pub(crate) fn image_to_texture(dyn_img: DynamicImage, is_srgb: bool) -> Image { - use bevy_core::cast_slice; - let width; - let height; - - let data: Vec; - let format: TextureFormat; - - match dyn_img { - DynamicImage::ImageLuma8(i) => { - let i = DynamicImage::ImageLuma8(i).into_rgba8(); - width = i.width(); - height = i.height(); - format = if is_srgb { - TextureFormat::Rgba8UnormSrgb - } else { - TextureFormat::Rgba8Unorm - }; - - data = i.into_raw(); - } - DynamicImage::ImageLumaA8(i) => { - let i = DynamicImage::ImageLumaA8(i).into_rgba8(); - width = i.width(); - height = i.height(); - format = if is_srgb { - TextureFormat::Rgba8UnormSrgb - } else { - TextureFormat::Rgba8Unorm - }; - - data = i.into_raw(); - } - DynamicImage::ImageRgb8(i) => { - let i = DynamicImage::ImageRgb8(i).into_rgba8(); - width = i.width(); - height = i.height(); - format = if is_srgb { - TextureFormat::Rgba8UnormSrgb - } else { - TextureFormat::Rgba8Unorm - }; - - data = i.into_raw(); - } - DynamicImage::ImageRgba8(i) => { - width = i.width(); - height = i.height(); - format = if is_srgb { - TextureFormat::Rgba8UnormSrgb - } else { - TextureFormat::Rgba8Unorm - }; - - data = i.into_raw(); - } - DynamicImage::ImageLuma16(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::R16Uint; +impl Image { + /// Converts a [`DynamicImage`] to an [`Image`]. + pub fn from_dynamic(dyn_img: DynamicImage, is_srgb: bool) -> Image { + use bevy_core::cast_slice; + let width; + let height; + + let data: Vec; + let format: TextureFormat; + + match dyn_img { + DynamicImage::ImageLuma8(i) => { + let i = DynamicImage::ImageLuma8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; + + data = i.into_raw(); + } + DynamicImage::ImageLumaA8(i) => { + let i = DynamicImage::ImageLumaA8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; + + data = i.into_raw(); + } + DynamicImage::ImageRgb8(i) => { + let i = DynamicImage::ImageRgb8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; + + data = i.into_raw(); + } + DynamicImage::ImageRgba8(i) => { + width = i.width(); + height = i.height(); + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; + + data = i.into_raw(); + } + DynamicImage::ImageLuma16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::R16Uint; - let raw_data = i.into_raw(); + let raw_data = i.into_raw(); - data = cast_slice(&raw_data).to_owned(); - } - DynamicImage::ImageLumaA16(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Rg16Uint; + data = cast_slice(&raw_data).to_owned(); + } + DynamicImage::ImageLumaA16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rg16Uint; - let raw_data = i.into_raw(); + let raw_data = i.into_raw(); - data = cast_slice(&raw_data).to_owned(); - } - DynamicImage::ImageRgb16(image) => { - width = image.width(); - height = image.height(); - format = TextureFormat::Rgba16Uint; - - let mut local_data = - Vec::with_capacity(width as usize * height as usize * format.pixel_size()); - - for pixel in image.into_raw().chunks_exact(3) { - // TODO: use the array_chunks method once stabilised - // https://github.com/rust-lang/rust/issues/74985 - let r = pixel[0]; - let g = pixel[1]; - let b = pixel[2]; - let a = u16::max_value(); - - local_data.extend_from_slice(&r.to_ne_bytes()); - local_data.extend_from_slice(&g.to_ne_bytes()); - local_data.extend_from_slice(&b.to_ne_bytes()); - local_data.extend_from_slice(&a.to_ne_bytes()); + data = cast_slice(&raw_data).to_owned(); } + DynamicImage::ImageRgb16(image) => { + width = image.width(); + height = image.height(); + format = TextureFormat::Rgba16Uint; + + let mut local_data = + Vec::with_capacity(width as usize * height as usize * format.pixel_size()); + + for pixel in image.into_raw().chunks_exact(3) { + // TODO: use the array_chunks method once stabilised + // https://github.com/rust-lang/rust/issues/74985 + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + let a = u16::max_value(); + + local_data.extend_from_slice(&r.to_ne_bytes()); + local_data.extend_from_slice(&g.to_ne_bytes()); + local_data.extend_from_slice(&b.to_ne_bytes()); + local_data.extend_from_slice(&a.to_ne_bytes()); + } + + data = local_data; + } + DynamicImage::ImageRgba16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba16Uint; - data = local_data; - } - DynamicImage::ImageRgba16(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Rgba16Uint; - - let raw_data = i.into_raw(); + let raw_data = i.into_raw(); - data = cast_slice(&raw_data).to_owned(); - } - DynamicImage::ImageRgb32F(image) => { - width = image.width(); - height = image.height(); - format = TextureFormat::Rgba32Float; - - let mut local_data = - Vec::with_capacity(width as usize * height as usize * format.pixel_size()); - - for pixel in image.into_raw().chunks_exact(3) { - // TODO: use the array_chunks method once stabilised - // https://github.com/rust-lang/rust/issues/74985 - let r = pixel[0]; - let g = pixel[1]; - let b = pixel[2]; - let a = u16::max_value(); - - local_data.extend_from_slice(&r.to_ne_bytes()); - local_data.extend_from_slice(&g.to_ne_bytes()); - local_data.extend_from_slice(&b.to_ne_bytes()); - local_data.extend_from_slice(&a.to_ne_bytes()); + data = cast_slice(&raw_data).to_owned(); + } + DynamicImage::ImageRgb32F(image) => { + width = image.width(); + height = image.height(); + format = TextureFormat::Rgba32Float; + + let mut local_data = + Vec::with_capacity(width as usize * height as usize * format.pixel_size()); + + for pixel in image.into_raw().chunks_exact(3) { + // TODO: use the array_chunks method once stabilised + // https://github.com/rust-lang/rust/issues/74985 + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + let a = u16::max_value(); + + local_data.extend_from_slice(&r.to_ne_bytes()); + local_data.extend_from_slice(&g.to_ne_bytes()); + local_data.extend_from_slice(&b.to_ne_bytes()); + local_data.extend_from_slice(&a.to_ne_bytes()); + } + + data = local_data; } + DynamicImage::ImageRgba32F(image) => { + width = image.width(); + height = image.height(); + format = TextureFormat::Rgba32Float; - data = local_data; + let raw_data = image.into_raw(); + + data = cast_slice(&raw_data).to_owned(); + } + // DynamicImage is now non exhaustive, catch future variants and convert them + _ => { + let image = dyn_img.into_rgba8(); + width = image.width(); + height = image.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = image.into_raw(); + } } - DynamicImage::ImageRgba32F(image) => { - width = image.width(); - height = image.height(); - format = TextureFormat::Rgba32Float; - let raw_data = image.into_raw(); + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + data, + format, + ) + } - data = cast_slice(&raw_data).to_owned(); - } - // DynamicImage is now non exhaustive, catch future variants and convert them - _ => { - let image = dyn_img.into_rgba8(); - width = image.width(); - height = image.height(); - format = TextureFormat::Rgba8UnormSrgb; - - data = image.into_raw(); + /// Convert a [`Image`] to a [`DynamicImage`]. Usefull for editing image + /// data. Not all [`TextureFormat`] are covered, therefore it will return an + /// error if the format is unsupported. Supported formats are: + /// - `TextureFormat::R8Unorm` + /// - `TextureFormat::Rg8Unorm` + /// - `TextureFormat::Rgba8UnormSrgb` + /// + /// To convert [`Image`] to a different format see: [`Image::convert`]. + pub fn try_into_dynamic(self) -> anyhow::Result { + match self.texture_descriptor.format { + TextureFormat::R8Unorm => ImageBuffer::from_raw( + self.texture_descriptor.size.width, + self.texture_descriptor.size.height, + self.data, + ) + .map(DynamicImage::ImageLuma8), + TextureFormat::Rg8Unorm => ImageBuffer::from_raw( + self.texture_descriptor.size.width, + self.texture_descriptor.size.height, + self.data, + ) + .map(DynamicImage::ImageLumaA8), + TextureFormat::Rgba8UnormSrgb => ImageBuffer::from_raw( + self.texture_descriptor.size.width, + self.texture_descriptor.size.height, + self.data, + ) + .map(DynamicImage::ImageRgba8), + // Throw and error if conversion isn't supported + texture_format => { + return Err(anyhow!( + "Conversion into dynamic image not supported for {:?}.", + texture_format + )) + } } + .ok_or_else(|| { + anyhow!( + "Failed to convert into {:?}.", + self.texture_descriptor.format + ) + }) } - - Image::new( - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - data, - format, - ) } -/// Converts an [`Image`] to a [`DynamicImage`]. Not all [`TextureFormat`] are -/// covered, therefore it will return `None` if the format is unsupported. -pub(crate) fn texture_to_image(texture: &Image) -> Option { - match texture.texture_descriptor.format { - TextureFormat::R8Unorm => ImageBuffer::from_raw( - texture.texture_descriptor.size.width, - texture.texture_descriptor.size.height, - texture.data.clone(), - ) - .map(DynamicImage::ImageLuma8), - TextureFormat::Rg8Unorm => ImageBuffer::from_raw( - texture.texture_descriptor.size.width, - texture.texture_descriptor.size.height, - texture.data.clone(), - ) - .map(DynamicImage::ImageLumaA8), - TextureFormat::Rgba8UnormSrgb => ImageBuffer::from_raw( - texture.texture_descriptor.size.width, - texture.texture_descriptor.size.height, - texture.data.clone(), - ) - .map(DynamicImage::ImageRgba8), - _ => None, +#[cfg(test)] +mod test { + use image::{GenericImage, Rgba}; + + use super::*; + + #[test] + fn two_way_conversion() { + // Check to see if color is preserved through an rgba8 conversion and back. + let mut initial = DynamicImage::new_rgba8(1, 1); + initial.put_pixel(0, 0, Rgba::from([132, 3, 7, 200])); + + let image = Image::from_dynamic(initial.clone(), true); + + // NOTE: Fails if `is_srbg = false` or the dynamic image is of the type rgb8. + assert_eq!(initial, image.try_into_dynamic().unwrap()); } }