Skip to main content

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    ///
10    /// Grayscale images are expanded to RGBA.
11    pub fn from_dynamic(
12        dyn_img: DynamicImage,
13        is_srgb: bool,
14        asset_usage: RenderAssetUsages,
15    ) -> Image {
16        Self::from_dynamic_inner(dyn_img, is_srgb, true, asset_usage)
17    }
18
19    pub(crate) fn from_dynamic_inner(
20        dyn_img: DynamicImage,
21        is_srgb: bool,
22        expand_grayscale: bool,
23        asset_usage: RenderAssetUsages,
24    ) -> Image {
25        use bytemuck::cast_slice;
26        let dyn_img = match dyn_img {
27            DynamicImage::ImageLuma8(_) | DynamicImage::ImageLumaA8(_) if expand_grayscale => {
28                DynamicImage::ImageRgba8(dyn_img.into_rgba8())
29            }
30            DynamicImage::ImageLuma16(_) | DynamicImage::ImageLumaA16(_) if expand_grayscale => {
31                DynamicImage::ImageRgba16(dyn_img.into_rgba16())
32            }
33            other => other,
34        };
35        let width;
36        let height;
37
38        let data: Vec<u8>;
39        let format: TextureFormat;
40
41        match dyn_img {
42            DynamicImage::ImageLuma8(image) => {
43                width = image.width();
44                height = image.height();
45                format = TextureFormat::R8Unorm;
46
47                data = image.into_raw();
48            }
49            DynamicImage::ImageLumaA8(image) => {
50                width = image.width();
51                height = image.height();
52                format = TextureFormat::Rg8Unorm;
53
54                data = image.into_raw();
55            }
56            DynamicImage::ImageRgb8(image) => {
57                let i = DynamicImage::ImageRgb8(image).into_rgba8();
58                width = i.width();
59                height = i.height();
60                format = if is_srgb {
61                    TextureFormat::Rgba8UnormSrgb
62                } else {
63                    TextureFormat::Rgba8Unorm
64                };
65
66                data = i.into_raw();
67            }
68            DynamicImage::ImageRgba8(image) => {
69                width = image.width();
70                height = image.height();
71                format = if is_srgb {
72                    TextureFormat::Rgba8UnormSrgb
73                } else {
74                    TextureFormat::Rgba8Unorm
75                };
76
77                data = image.into_raw();
78            }
79            DynamicImage::ImageLuma16(image) => {
80                width = image.width();
81                height = image.height();
82                format = TextureFormat::R16Unorm;
83
84                let raw_data = image.into_raw();
85
86                data = cast_slice(&raw_data).to_owned();
87            }
88            DynamicImage::ImageLumaA16(image) => {
89                width = image.width();
90                height = image.height();
91                format = TextureFormat::Rg16Unorm;
92
93                let raw_data = image.into_raw();
94
95                data = cast_slice(&raw_data).to_owned();
96            }
97            DynamicImage::ImageRgb16(image) => {
98                let i = DynamicImage::ImageRgb16(image).into_rgba16();
99                width = i.width();
100                height = i.height();
101                format = TextureFormat::Rgba16Unorm;
102
103                let raw_data = i.into_raw();
104
105                data = cast_slice(&raw_data).to_owned();
106            }
107            DynamicImage::ImageRgba16(image) => {
108                width = image.width();
109                height = image.height();
110                format = TextureFormat::Rgba16Unorm;
111
112                let raw_data = image.into_raw();
113
114                data = cast_slice(&raw_data).to_owned();
115            }
116            DynamicImage::ImageRgb32F(image) => {
117                width = image.width();
118                height = image.height();
119                format = TextureFormat::Rgba32Float;
120
121                let mut local_data = Vec::with_capacity(
122                    width as usize * height as usize * format.pixel_size().unwrap_or(0),
123                );
124
125                for [r, g, b] in image.into_raw().as_chunks().0 {
126                    let a = 1f32;
127
128                    local_data.extend_from_slice(&r.to_le_bytes());
129                    local_data.extend_from_slice(&g.to_le_bytes());
130                    local_data.extend_from_slice(&b.to_le_bytes());
131                    local_data.extend_from_slice(&a.to_le_bytes());
132                }
133
134                data = local_data;
135            }
136            DynamicImage::ImageRgba32F(image) => {
137                width = image.width();
138                height = image.height();
139                format = TextureFormat::Rgba32Float;
140
141                let raw_data = image.into_raw();
142
143                data = cast_slice(&raw_data).to_owned();
144            }
145            // DynamicImage is now non exhaustive, catch future variants and convert them
146            _ => {
147                let image = dyn_img.into_rgba8();
148                width = image.width();
149                height = image.height();
150                format = TextureFormat::Rgba8UnormSrgb;
151
152                data = image.into_raw();
153            }
154        }
155
156        Image::new(
157            Extent3d {
158                width,
159                height,
160                depth_or_array_layers: 1,
161            },
162            TextureDimension::D2,
163            data,
164            format,
165            asset_usage,
166        )
167    }
168
169    /// Convert a [`Image`] to a [`DynamicImage`]. Useful for editing image
170    /// data. Not all [`TextureFormat`] are covered, therefore it will return an
171    /// error if the format is unsupported. Supported formats are:
172    /// - `TextureFormat::R8Unorm`
173    /// - `TextureFormat::Rg8Unorm`
174    /// - `TextureFormat::Rgba8UnormSrgb`
175    /// - `TextureFormat::Bgra8UnormSrgb`
176    /// - `TextureFormat::Rgba16Float`, converted to [`DynamicImage::ImageRgba32F`]
177    /// - `TextureFormat::Rgba32Float`
178    ///
179    /// To convert [`Image`] to a different format see: [`Image::convert`].
180    pub fn try_into_dynamic(self) -> Result<DynamicImage, IntoDynamicImageError> {
181        let width = self.width();
182        let height = self.height();
183        let Some(data) = self.data else {
184            return Err(IntoDynamicImageError::UninitializedImage);
185        };
186        match self.texture_descriptor.format {
187            TextureFormat::R8Unorm => {
188                ImageBuffer::from_raw(width, height, data).map(DynamicImage::ImageLuma8)
189            }
190            TextureFormat::Rg8Unorm => {
191                ImageBuffer::from_raw(width, height, data).map(DynamicImage::ImageLumaA8)
192            }
193            TextureFormat::Rgba8UnormSrgb => {
194                ImageBuffer::from_raw(width, height, data).map(DynamicImage::ImageRgba8)
195            }
196            // This format is commonly used as the format for the swapchain texture
197            // This conversion is added here to support screenshots
198            TextureFormat::Bgra8UnormSrgb | TextureFormat::Bgra8Unorm => {
199                ImageBuffer::from_raw(width, height, {
200                    let mut data = data;
201                    for [b, _, r, _] in data.as_chunks_mut().0 {
202                        core::mem::swap(b, r);
203                    }
204                    data
205                })
206                .map(DynamicImage::ImageRgba8)
207            }
208            // `DynamicImage` has no f16 pixel type, so convert to f32.
209            TextureFormat::Rgba16Float => {
210                let pixels: Vec<f32> = data
211                    .as_chunks()
212                    .0
213                    .iter()
214                    .map(|&bytes| half::f16::from_le_bytes(bytes).to_f32())
215                    .collect();
216                ImageBuffer::from_raw(width, height, pixels).map(DynamicImage::ImageRgba32F)
217            }
218            TextureFormat::Rgba32Float => {
219                let pixels: Vec<f32> = data
220                    .as_chunks()
221                    .0
222                    .iter()
223                    .map(|&bytes| f32::from_le_bytes(bytes))
224                    .collect();
225                ImageBuffer::from_raw(width, height, pixels).map(DynamicImage::ImageRgba32F)
226            }
227            // Throw and error if conversion isn't supported
228            texture_format => return Err(IntoDynamicImageError::UnsupportedFormat(texture_format)),
229        }
230        .ok_or(IntoDynamicImageError::UnknownConversionError(
231            self.texture_descriptor.format,
232        ))
233    }
234}
235
236/// Errors that occur while converting an [`Image`] into a [`DynamicImage`]
237#[non_exhaustive]
238#[derive(Error, Debug)]
239pub enum IntoDynamicImageError {
240    /// Conversion into dynamic image not supported for source format.
241    #[error("Conversion into dynamic image not supported for {0:?}.")]
242    UnsupportedFormat(TextureFormat),
243
244    /// Encountered an unknown error during conversion.
245    #[error("Failed to convert into {0:?}.")]
246    UnknownConversionError(TextureFormat),
247
248    /// Tried to convert an image that has no texture data
249    #[error("Image has no texture data")]
250    UninitializedImage,
251}
252
253#[cfg(test)]
254mod test {
255    use image::{GenericImage, Rgba};
256
257    use super::*;
258
259    #[test]
260    fn two_way_conversion() {
261        // Check to see if color is preserved through an rgba8 conversion and back.
262        let mut initial = DynamicImage::new_rgba8(1, 1);
263        initial.put_pixel(0, 0, Rgba::from([132, 3, 7, 200]));
264
265        let image = Image::from_dynamic(initial.clone(), true, RenderAssetUsages::RENDER_WORLD);
266
267        // NOTE: Fails if `is_srgb = false` or the dynamic image is of the type rgb8.
268        assert_eq!(initial, image.try_into_dynamic().unwrap());
269
270        let luma_a8 = Image::from_dynamic(
271            DynamicImage::new_luma_a8(1, 1),
272            false,
273            RenderAssetUsages::RENDER_WORLD,
274        );
275        assert_eq!(luma_a8.texture_descriptor.format, TextureFormat::Rgba8Unorm);
276
277        let luma16 = Image::from_dynamic(
278            DynamicImage::new_luma16(1, 1),
279            false,
280            RenderAssetUsages::RENDER_WORLD,
281        );
282        assert_eq!(luma16.texture_descriptor.format, TextureFormat::Rgba16Unorm);
283
284        let luma_a16 = Image::from_dynamic(
285            DynamicImage::new_luma_a16(1, 1),
286            false,
287            RenderAssetUsages::RENDER_WORLD,
288        );
289        assert_eq!(
290            luma_a16.texture_descriptor.format,
291            TextureFormat::Rgba16Unorm
292        );
293    }
294
295    #[test]
296    fn grayscale_expands_to_rgba() {
297        let luma8 = DynamicImage::ImageLuma8(ImageBuffer::from_raw(1, 1, vec![77]).unwrap());
298        let luma_a8 =
299            DynamicImage::ImageLumaA8(ImageBuffer::from_raw(1, 1, vec![100, 50]).unwrap());
300        let usage = RenderAssetUsages::RENDER_WORLD;
301
302        let image = Image::from_dynamic(luma8.clone(), true, usage);
303        assert_eq!(
304            image.texture_descriptor.format,
305            TextureFormat::Rgba8UnormSrgb
306        );
307        assert_eq!(image.data.as_deref(), Some(&[77, 77, 77, 255][..]));
308
309        let image = Image::from_dynamic(luma8, false, usage);
310        assert_eq!(image.texture_descriptor.format, TextureFormat::Rgba8Unorm);
311
312        let image = Image::from_dynamic(luma_a8.clone(), false, usage);
313        assert_eq!(image.texture_descriptor.format, TextureFormat::Rgba8Unorm);
314        assert_eq!(image.data.as_deref(), Some(&[100, 100, 100, 50][..]));
315
316        let image = Image::from_dynamic(luma_a8, true, usage);
317        assert_eq!(
318            image.texture_descriptor.format,
319            TextureFormat::Rgba8UnormSrgb
320        );
321
322        let luma16 = DynamicImage::ImageLuma16(ImageBuffer::from_raw(1, 1, vec![1000u16]).unwrap());
323        let image = Image::from_dynamic(luma16, false, usage);
324        assert_eq!(image.texture_descriptor.format, TextureFormat::Rgba16Unorm);
325        assert_eq!(image.data.as_ref().unwrap().len(), 8);
326
327        let luma_a16 =
328            DynamicImage::ImageLumaA16(ImageBuffer::from_raw(1, 1, vec![1000u16, 2000]).unwrap());
329        let image = Image::from_dynamic(luma_a16, false, usage);
330        assert_eq!(image.texture_descriptor.format, TextureFormat::Rgba16Unorm);
331        let channels: &[u16] = bytemuck::cast_slice(image.data.as_deref().unwrap());
332        assert_eq!(channels, &[1000, 1000, 1000, 2000]);
333    }
334
335    #[test]
336    fn grayscale_unexpanded() {
337        let usage = RenderAssetUsages::RENDER_WORLD;
338        let cases = [
339            (
340                DynamicImage::ImageLuma8(ImageBuffer::from_raw(1, 1, vec![77]).unwrap()),
341                TextureFormat::R8Unorm,
342                vec![77],
343            ),
344            (
345                DynamicImage::ImageLumaA8(ImageBuffer::from_raw(1, 1, vec![100, 50]).unwrap()),
346                TextureFormat::Rg8Unorm,
347                vec![100, 50],
348            ),
349            (
350                DynamicImage::ImageLuma16(ImageBuffer::from_raw(1, 1, vec![0x0102u16]).unwrap()),
351                TextureFormat::R16Unorm,
352                0x0102u16.to_ne_bytes().to_vec(),
353            ),
354            (
355                DynamicImage::ImageLumaA16(
356                    ImageBuffer::from_raw(1, 1, vec![0x0102u16, 0x0304]).unwrap(),
357                ),
358                TextureFormat::Rg16Unorm,
359                [0x0102u16.to_ne_bytes(), 0x0304u16.to_ne_bytes()].concat(),
360            ),
361        ];
362        for (dyn_img, format, data) in cases {
363            let image = Image::from_dynamic_inner(dyn_img, true, false, usage);
364            assert_eq!(image.texture_descriptor.format, format);
365            assert_eq!(image.data, Some(data));
366        }
367    }
368
369    #[test]
370    fn rgba16float_to_dynamic_keeps_hdr_range() {
371        // Includes a value above 1.0 and a negative one.
372        let pixels: [f32; 8] = [2.5, 1.0, -0.25, 1.0, 0.5, 0.0, 1.0, 0.25];
373        let data: Vec<u8> = pixels
374            .iter()
375            .flat_map(|&v| half::f16::from_f32(v).to_le_bytes())
376            .collect();
377        let image = Image::new(
378            Extent3d {
379                width: 2,
380                height: 1,
381                depth_or_array_layers: 1,
382            },
383            TextureDimension::D2,
384            data,
385            TextureFormat::Rgba16Float,
386            RenderAssetUsages::MAIN_WORLD,
387        );
388        let DynamicImage::ImageRgba32F(converted) = image.try_into_dynamic().unwrap() else {
389            panic!("expected DynamicImage::ImageRgba32F");
390        };
391        assert_eq!(converted.as_raw().as_slice(), &pixels);
392    }
393
394    #[test]
395    fn rgba32float_to_dynamic_is_lossless() {
396        let pixels: [f32; 4] = [3.75, 0.125, -1.5, 1.0];
397        let data: Vec<u8> = pixels.iter().flat_map(|v| v.to_le_bytes()).collect();
398        let image = Image::new(
399            Extent3d {
400                width: 1,
401                height: 1,
402                depth_or_array_layers: 1,
403            },
404            TextureDimension::D2,
405            data,
406            TextureFormat::Rgba32Float,
407            RenderAssetUsages::MAIN_WORLD,
408        );
409        let DynamicImage::ImageRgba32F(converted) = image.try_into_dynamic().unwrap() else {
410            panic!("expected DynamicImage::ImageRgba32F");
411        };
412        assert_eq!(converted.as_raw().as_slice(), &pixels);
413    }
414}