bevy_render/texture/
image_texture_conversion.rs

1use crate::{
2    render_asset::RenderAssetUsages,
3    texture::{Image, TextureFormatPixelInfo},
4};
5use image::{DynamicImage, ImageBuffer};
6use thiserror::Error;
7use wgpu::{Extent3d, TextureDimension, TextureFormat};
8
9impl Image {
10    /// Converts a [`DynamicImage`] to an [`Image`].
11    pub fn from_dynamic(
12        dyn_img: DynamicImage,
13        is_srgb: bool,
14        asset_usage: RenderAssetUsages,
15    ) -> Image {
16        use bytemuck::cast_slice;
17        let width;
18        let height;
19
20        let data: Vec<u8>;
21        let format: TextureFormat;
22
23        match dyn_img {
24            DynamicImage::ImageLuma8(image) => {
25                let i = DynamicImage::ImageLuma8(image).into_rgba8();
26                width = i.width();
27                height = i.height();
28                format = if is_srgb {
29                    TextureFormat::Rgba8UnormSrgb
30                } else {
31                    TextureFormat::Rgba8Unorm
32                };
33
34                data = i.into_raw();
35            }
36            DynamicImage::ImageLumaA8(image) => {
37                let i = DynamicImage::ImageLumaA8(image).into_rgba8();
38                width = i.width();
39                height = i.height();
40                format = if is_srgb {
41                    TextureFormat::Rgba8UnormSrgb
42                } else {
43                    TextureFormat::Rgba8Unorm
44                };
45
46                data = i.into_raw();
47            }
48            DynamicImage::ImageRgb8(image) => {
49                let i = DynamicImage::ImageRgb8(image).into_rgba8();
50                width = i.width();
51                height = i.height();
52                format = if is_srgb {
53                    TextureFormat::Rgba8UnormSrgb
54                } else {
55                    TextureFormat::Rgba8Unorm
56                };
57
58                data = i.into_raw();
59            }
60            DynamicImage::ImageRgba8(image) => {
61                width = image.width();
62                height = image.height();
63                format = if is_srgb {
64                    TextureFormat::Rgba8UnormSrgb
65                } else {
66                    TextureFormat::Rgba8Unorm
67                };
68
69                data = image.into_raw();
70            }
71            DynamicImage::ImageLuma16(image) => {
72                width = image.width();
73                height = image.height();
74                format = TextureFormat::R16Uint;
75
76                let raw_data = image.into_raw();
77
78                data = cast_slice(&raw_data).to_owned();
79            }
80            DynamicImage::ImageLumaA16(image) => {
81                width = image.width();
82                height = image.height();
83                format = TextureFormat::Rg16Uint;
84
85                let raw_data = image.into_raw();
86
87                data = cast_slice(&raw_data).to_owned();
88            }
89            DynamicImage::ImageRgb16(image) => {
90                let i = DynamicImage::ImageRgb16(image).into_rgba16();
91                width = i.width();
92                height = i.height();
93                format = TextureFormat::Rgba16Unorm;
94
95                let raw_data = i.into_raw();
96
97                data = cast_slice(&raw_data).to_owned();
98            }
99            DynamicImage::ImageRgba16(image) => {
100                width = image.width();
101                height = image.height();
102                format = TextureFormat::Rgba16Unorm;
103
104                let raw_data = image.into_raw();
105
106                data = cast_slice(&raw_data).to_owned();
107            }
108            DynamicImage::ImageRgb32F(image) => {
109                width = image.width();
110                height = image.height();
111                format = TextureFormat::Rgba32Float;
112
113                let mut local_data =
114                    Vec::with_capacity(width as usize * height as usize * format.pixel_size());
115
116                for pixel in image.into_raw().chunks_exact(3) {
117                    // TODO: use the array_chunks method once stabilised
118                    // https://github.com/rust-lang/rust/issues/74985
119                    let r = pixel[0];
120                    let g = pixel[1];
121                    let b = pixel[2];
122                    let a = 1f32;
123
124                    local_data.extend_from_slice(&r.to_ne_bytes());
125                    local_data.extend_from_slice(&g.to_ne_bytes());
126                    local_data.extend_from_slice(&b.to_ne_bytes());
127                    local_data.extend_from_slice(&a.to_ne_bytes());
128                }
129
130                data = local_data;
131            }
132            DynamicImage::ImageRgba32F(image) => {
133                width = image.width();
134                height = image.height();
135                format = TextureFormat::Rgba32Float;
136
137                let raw_data = image.into_raw();
138
139                data = cast_slice(&raw_data).to_owned();
140            }
141            // DynamicImage is now non exhaustive, catch future variants and convert them
142            _ => {
143                let image = dyn_img.into_rgba8();
144                width = image.width();
145                height = image.height();
146                format = TextureFormat::Rgba8UnormSrgb;
147
148                data = image.into_raw();
149            }
150        }
151
152        Image::new(
153            Extent3d {
154                width,
155                height,
156                depth_or_array_layers: 1,
157            },
158            TextureDimension::D2,
159            data,
160            format,
161            asset_usage,
162        )
163    }
164
165    /// Convert a [`Image`] to a [`DynamicImage`]. Useful for editing image
166    /// data. Not all [`TextureFormat`] are covered, therefore it will return an
167    /// error if the format is unsupported. Supported formats are:
168    /// - `TextureFormat::R8Unorm`
169    /// - `TextureFormat::Rg8Unorm`
170    /// - `TextureFormat::Rgba8UnormSrgb`
171    /// - `TextureFormat::Bgra8UnormSrgb`
172    ///
173    /// To convert [`Image`] to a different format see: [`Image::convert`].
174    pub fn try_into_dynamic(self) -> Result<DynamicImage, IntoDynamicImageError> {
175        match self.texture_descriptor.format {
176            TextureFormat::R8Unorm => ImageBuffer::from_raw(self.width(), self.height(), self.data)
177                .map(DynamicImage::ImageLuma8),
178            TextureFormat::Rg8Unorm => {
179                ImageBuffer::from_raw(self.width(), self.height(), self.data)
180                    .map(DynamicImage::ImageLumaA8)
181            }
182            TextureFormat::Rgba8UnormSrgb => {
183                ImageBuffer::from_raw(self.width(), self.height(), self.data)
184                    .map(DynamicImage::ImageRgba8)
185            }
186            // This format is commonly used as the format for the swapchain texture
187            // This conversion is added here to support screenshots
188            TextureFormat::Bgra8UnormSrgb | TextureFormat::Bgra8Unorm => {
189                ImageBuffer::from_raw(self.width(), self.height(), {
190                    let mut data = self.data;
191                    for bgra in data.chunks_exact_mut(4) {
192                        bgra.swap(0, 2);
193                    }
194                    data
195                })
196                .map(DynamicImage::ImageRgba8)
197            }
198            // Throw and error if conversion isn't supported
199            texture_format => return Err(IntoDynamicImageError::UnsupportedFormat(texture_format)),
200        }
201        .ok_or(IntoDynamicImageError::UnknownConversionError(
202            self.texture_descriptor.format,
203        ))
204    }
205}
206
207/// Errors that occur while converting an [`Image`] into a [`DynamicImage`]
208#[non_exhaustive]
209#[derive(Error, Debug)]
210pub enum IntoDynamicImageError {
211    /// Conversion into dynamic image not supported for source format.
212    #[error("Conversion into dynamic image not supported for {0:?}.")]
213    UnsupportedFormat(TextureFormat),
214
215    /// Encountered an unknown error during conversion.
216    #[error("Failed to convert into {0:?}.")]
217    UnknownConversionError(TextureFormat),
218}
219
220#[cfg(test)]
221mod test {
222    use image::{GenericImage, Rgba};
223
224    use super::*;
225
226    #[test]
227    fn two_way_conversion() {
228        // Check to see if color is preserved through an rgba8 conversion and back.
229        let mut initial = DynamicImage::new_rgba8(1, 1);
230        initial.put_pixel(0, 0, Rgba::from([132, 3, 7, 200]));
231
232        let image = Image::from_dynamic(initial.clone(), true, RenderAssetUsages::RENDER_WORLD);
233
234        // NOTE: Fails if `is_srbg = false` or the dynamic image is of the type rgb8.
235        assert_eq!(initial, image.try_into_dynamic().unwrap());
236    }
237}