1use alloc::{boxed::Box, string::ToString, vec};
24use exr::prelude::*;
25
26use crate::error::{DecodingError, ImageFormatHint, UnsupportedError, UnsupportedErrorKind};
27use crate::{
28 ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult,
29};
30
31use no_std_io::io::{BufRead, Seek, Write};
32
33#[derive(Debug)]
35pub struct OpenExrDecoder<R> {
36 exr_reader: exr::block::reader::Reader<R>,
37
38 header_index: usize,
40
41 alpha_preference: Option<bool>,
45
46 alpha_present_in_file: bool,
47}
48
49impl<R: BufRead + Seek> OpenExrDecoder<R> {
50 pub fn new(source: R) -> ImageResult<Self> {
56 Self::with_alpha_preference(source, None)
57 }
58
59 pub fn with_alpha_preference(source: R, alpha_preference: Option<bool>) -> ImageResult<Self> {
66 let exr_reader = exr::block::read(source, false).map_err(to_image_err)?;
68
69 let header_index = exr_reader
70 .headers()
71 .iter()
72 .position(|header| {
73 let has_rgb = ["R", "G", "B"]
75 .iter()
76 .all(|&required| header.channels.find_index_of_channel(&Text::from(required)).is_some());
78
79 !header.deep && has_rgb
81 })
82 .ok_or_else(|| {
83 ImageError::Decoding(DecodingError::new(
84 ImageFormatHint::Exact(ImageFormat::OpenExr),
85 "image does not contain non-deep rgb channels",
86 ))
87 })?;
88
89 let has_alpha = exr_reader.headers()[header_index]
90 .channels
91 .find_index_of_channel(&Text::from("A"))
92 .is_some();
93
94 Ok(Self {
95 alpha_preference,
96 exr_reader,
97 header_index,
98 alpha_present_in_file: has_alpha,
99 })
100 }
101
102 fn selected_exr_header(&self) -> &exr::meta::header::Header {
104 &self.exr_reader.meta_data().headers[self.header_index]
105 }
106}
107
108impl<R: BufRead + Seek> ImageDecoder for OpenExrDecoder<R> {
109 fn dimensions(&self) -> (u32, u32) {
110 let size = self
111 .selected_exr_header()
112 .shared_attributes
113 .display_window
114 .size;
115 (size.width() as u32, size.height() as u32)
116 }
117
118 fn color_type(&self) -> ColorType {
119 let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file);
120 if returns_alpha {
121 ColorType::Rgba32F
122 } else {
123 ColorType::Rgb32F
124 }
125 }
126
127 fn original_color_type(&self) -> ExtendedColorType {
128 if self.alpha_present_in_file {
129 ExtendedColorType::Rgba32F
130 } else {
131 ExtendedColorType::Rgb32F
132 }
133 }
134
135 fn read_image(self, unaligned_bytes: &mut [u8]) -> ImageResult<()> {
137 let _blocks_in_header = self.selected_exr_header().chunk_count as u64;
138 let channel_count = self.color_type().channel_count() as usize;
139
140 let display_window = self.selected_exr_header().shared_attributes.display_window;
141 let data_window_offset =
142 self.selected_exr_header().own_attributes.layer_position - display_window.position;
143
144 {
145 let (width, height) = self.dimensions();
147 let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
148 let expected_byte_count = (width as usize)
149 .checked_mul(height as usize)
150 .and_then(|size| size.checked_mul(bytes_per_pixel));
151
152 let has_invalid_size_or_overflowed = expected_byte_count
154 .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
155 .unwrap_or(true);
158
159 assert!(
160 !has_invalid_size_or_overflowed,
161 "byte buffer not large enough for the specified dimensions and f32 pixels"
162 );
163 }
164
165 let result = read()
166 .no_deep_data()
167 .largest_resolution_level()
168 .rgba_channels(
169 move |_size, _channels| vec![0_f32; display_window.size.area() * channel_count],
170 move |buffer, index_in_data_window, (r, g, b, a_or_1): (f32, f32, f32, f32)| {
171 let index_in_display_window =
172 index_in_data_window.to_i32() + data_window_offset;
173
174 if index_in_display_window.x() >= 0
177 && index_in_display_window.y() >= 0
178 && index_in_display_window.x() < display_window.size.width() as i32
179 && index_in_display_window.y() < display_window.size.height() as i32
180 {
181 let index_in_display_window =
182 index_in_display_window.to_usize("index bug").unwrap();
183 let first_f32_index =
184 index_in_display_window.flat_index_for_size(display_window.size);
185
186 buffer[first_f32_index * channel_count
187 ..(first_f32_index + 1) * channel_count]
188 .copy_from_slice(&[r, g, b, a_or_1][0..channel_count]);
189
190 }
192 },
193 )
194 .first_valid_layer() .all_attributes()
196 .from_chunks(self.exr_reader)
197 .map_err(to_image_err)?;
198
199 unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
204 result.layer_data.channel_data.pixels.as_slice(),
205 ));
206 Ok(())
207 }
208
209 fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
210 (*self).read_image(buf)
211 }
212}
213
214fn write_buffer(
221 mut buffered_write: impl Write + Seek,
222 unaligned_bytes: &[u8],
223 width: u32,
224 height: u32,
225 color_type: ExtendedColorType,
226) -> ImageResult<()> {
227 let width = width as usize;
228 let height = height as usize;
229 let bytes_per_pixel = color_type.bits_per_pixel() as usize / 8;
230
231 match color_type {
232 ExtendedColorType::Rgb32F => {
233 Image ::from_channels(
235 (width, height),
236 SpecificChannels::rgb(|pixel: Vec2<usize>| {
237 let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
238 let start_byte = pixel_index * bytes_per_pixel;
239
240 let [r, g, b]: [f32; 3] = bytemuck::pod_read_unaligned(
241 &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
242 );
243
244 (r, g, b)
245 }),
246 )
247 .write()
248 .to_buffered(&mut buffered_write)
250 .map_err(to_image_err)?;
251 }
252
253 ExtendedColorType::Rgba32F => {
254 Image ::from_channels(
256 (width, height),
257 SpecificChannels::rgba(|pixel: Vec2<usize>| {
258 let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
259 let start_byte = pixel_index * bytes_per_pixel;
260
261 let [r, g, b, a]: [f32; 4] = bytemuck::pod_read_unaligned(
262 &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
263 );
264
265 (r, g, b, a)
266 }),
267 )
268 .write()
269 .to_buffered(&mut buffered_write)
271 .map_err(to_image_err)?;
272 }
273
274 unsupported_color_type => {
276 return Err(ImageError::Unsupported(
277 UnsupportedError::from_format_and_kind(
278 ImageFormat::OpenExr.into(),
279 UnsupportedErrorKind::Color(unsupported_color_type),
280 ),
281 ))
282 }
283 }
284
285 Ok(())
286}
287
288#[derive(Debug)]
291pub struct OpenExrEncoder<W>(W);
292
293impl<W> OpenExrEncoder<W> {
294 pub fn new(write: W) -> Self {
297 Self(write)
298 }
299}
300
301impl<W> ImageEncoder for OpenExrEncoder<W>
302where
303 W: Write + Seek,
304{
305 #[track_caller]
310 fn write_image(
311 self,
312 buf: &[u8],
313 width: u32,
314 height: u32,
315 color_type: ExtendedColorType,
316 ) -> ImageResult<()> {
317 let expected_buffer_len = color_type.buffer_size(width, height);
318 assert_eq!(
319 expected_buffer_len,
320 buf.len() as u64,
321 "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
322 buf.len(),
323 );
324
325 write_buffer(self.0, buf, width, height, color_type)
326 }
327}
328
329fn to_image_err(exr_error: Error) -> ImageError {
330 ImageError::Decoding(DecodingError::new(
331 ImageFormatHint::Exact(ImageFormat::OpenExr),
332 exr_error.to_string(),
333 ))
334}
335
336#[cfg(test)]
337mod test {
338 use super::*;
339
340 use no_std_io::io::{BufReader, Cursor};
341 use std::fs::File;
342 use std::path::{Path, PathBuf};
343
344 use crate::error::{LimitError, LimitErrorKind};
345 use crate::images::buffer::{Rgb32FImage, Rgba32FImage};
346 use crate::io::free_functions::decoder_to_vec;
347 use crate::{DynamicImage, ImageBuffer, Rgb, Rgba};
348
349 const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"];
350
351 fn write_rgb_image(write: impl Write + Seek, image: &Rgb32FImage) -> ImageResult<()> {
355 write_buffer(
356 write,
357 bytemuck::cast_slice(image.as_raw().as_slice()),
358 image.width(),
359 image.height(),
360 ExtendedColorType::Rgb32F,
361 )
362 }
363
364 fn write_rgba_image(write: impl Write + Seek, image: &Rgba32FImage) -> ImageResult<()> {
368 write_buffer(
369 write,
370 bytemuck::cast_slice(image.as_raw().as_slice()),
371 image.width(),
372 image.height(),
373 ExtendedColorType::Rgba32F,
374 )
375 }
376
377 fn read_as_rgba_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgba32FImage> {
379 read_as_rgba_image(BufReader::new(File::open(path)?))
380 }
381
382 fn read_as_rgb_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgb32FImage> {
384 read_as_rgb_image(BufReader::new(File::open(path)?))
385 }
386
387 fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult<Rgb32FImage> {
389 let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?;
390 let (width, height) = decoder.dimensions();
391 let buffer: Vec<f32> = decoder_to_vec(decoder)?;
392
393 ImageBuffer::from_raw(width, height, buffer)
394 .ok_or_else(|| {
397 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
398 })
399 }
400
401 fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult<Rgba32FImage> {
403 let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?;
404 let (width, height) = decoder.dimensions();
405 let buffer: Vec<f32> = decoder_to_vec(decoder)?;
406
407 ImageBuffer::from_raw(width, height, buffer)
408 .ok_or_else(|| {
411 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
412 })
413 }
414
415 #[test]
416 fn compare_exr_hdr() {
417 if cfg!(not(feature = "hdr")) {
418 eprintln!("warning: to run all the openexr tests, activate the hdr feature flag");
419 }
420
421 #[cfg(feature = "hdr")]
422 {
423 use crate::codecs::hdr::HdrDecoder;
424
425 let folder = BASE_PATH.iter().collect::<PathBuf>();
426 let reference_path = folder.join("overexposed gradient.hdr");
427 let exr_path =
428 folder.join("overexposed gradient - data window equals display window.exr");
429
430 let hdr_decoder =
431 HdrDecoder::new(BufReader::new(File::open(reference_path).unwrap())).unwrap();
432 let hdr: Rgb32FImage = match DynamicImage::from_decoder(hdr_decoder).unwrap() {
433 DynamicImage::ImageRgb32F(image) => image,
434 _ => panic!("expected rgb32f image"),
435 };
436
437 let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap();
438 assert_eq!(exr_pixels.dimensions(), hdr.dimensions());
439
440 for (expected, found) in hdr.pixels().zip(exr_pixels.pixels()) {
441 for (expected, found) in expected.0.iter().zip(found.0.iter()) {
442 assert!(
445 (expected - found).abs() < 0.1,
446 "expected {expected}, found {found}"
447 );
448 }
449 }
450 }
451 }
452
453 #[test]
454 fn roundtrip_rgba() {
455 let mut next_random = vec![1.0, 0.0, -1.0, -3.15, 27.0, 11.0, 31.0]
456 .into_iter()
457 .cycle();
458 let mut next_random = move || next_random.next().unwrap();
459
460 let generated_image: Rgba32FImage = ImageBuffer::from_fn(9, 31, |_x, _y| {
461 Rgba([next_random(), next_random(), next_random(), next_random()])
462 });
463
464 let mut bytes = vec![];
465 write_rgba_image(Cursor::new(&mut bytes), &generated_image).unwrap();
466 let decoded_image = read_as_rgba_image(Cursor::new(bytes)).unwrap();
467
468 debug_assert_eq!(generated_image, decoded_image);
469 }
470
471 #[test]
472 fn roundtrip_rgb() {
473 let mut next_random = vec![1.0, 0.0, -1.0, -3.15, 27.0, 11.0, 31.0]
474 .into_iter()
475 .cycle();
476 let mut next_random = move || next_random.next().unwrap();
477
478 let generated_image: Rgb32FImage = ImageBuffer::from_fn(9, 31, |_x, _y| {
479 Rgb([next_random(), next_random(), next_random()])
480 });
481
482 let mut bytes = vec![];
483 write_rgb_image(Cursor::new(&mut bytes), &generated_image).unwrap();
484 let decoded_image = read_as_rgb_image(Cursor::new(bytes)).unwrap();
485
486 debug_assert_eq!(generated_image, decoded_image);
487 }
488
489 #[test]
490 fn compare_rgba_rgb() {
491 let exr_path = BASE_PATH
492 .iter()
493 .collect::<PathBuf>()
494 .join("overexposed gradient - data window equals display window.exr");
495
496 let rgb: Rgb32FImage = read_as_rgb_image_from_file(&exr_path).unwrap();
497 let rgba: Rgba32FImage = read_as_rgba_image_from_file(&exr_path).unwrap();
498
499 assert_eq!(rgba.dimensions(), rgb.dimensions());
500
501 for (Rgb(rgb), Rgba(rgba)) in rgb.pixels().zip(rgba.pixels()) {
502 assert_eq!(rgb, &rgba[..3]);
503 }
504 }
505
506 #[test]
507 fn compare_cropped() {
508 let exr_path = BASE_PATH.iter().collect::<PathBuf>();
518 let original = exr_path.join("cropping - uncropped original.exr");
519 let cropped = exr_path.join("cropping - data window differs display window.exr");
520
521 {
523 let original_exr = read_first_flat_layer_from_file(&original).unwrap();
524 let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap();
525 assert_eq!(
526 original_exr.attributes.display_window,
527 cropped_exr.attributes.display_window
528 );
529 assert_ne!(
530 original_exr.layer_data.attributes.layer_position,
531 cropped_exr.layer_data.attributes.layer_position
532 );
533 assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size);
534 }
535
536 let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap();
538 let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap();
539 assert_eq!(original.dimensions(), cropped.dimensions());
540
541 assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b));
544 }
545}