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 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 _ => {
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 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 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 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 texture_format => return Err(IntoDynamicImageError::UnsupportedFormat(texture_format)),
229 }
230 .ok_or(IntoDynamicImageError::UnknownConversionError(
231 self.texture_descriptor.format,
232 ))
233 }
234}
235
236#[non_exhaustive]
238#[derive(Error, Debug)]
239pub enum IntoDynamicImageError {
240 #[error("Conversion into dynamic image not supported for {0:?}.")]
242 UnsupportedFormat(TextureFormat),
243
244 #[error("Failed to convert into {0:?}.")]
246 UnknownConversionError(TextureFormat),
247
248 #[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 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 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 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}