bevy_image/
image_texture_conversion.rs

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