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