1use exr::prelude::*;
24
25use crate::error::{DecodingError, EncodingError, ImageFormatHint};
26use crate::image::decoder_to_vec;
27use crate::{
28 ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult,
29 Progress,
30};
31use std::io::{Cursor, Read, 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: Read + 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<'a, R: 'a + Read + Seek> ImageDecoder<'a> for OpenExrDecoder<R> {
109 type Reader = Cursor<Vec<u8>>;
110
111 fn dimensions(&self) -> (u32, u32) {
112 let size = self
113 .selected_exr_header()
114 .shared_attributes
115 .display_window
116 .size;
117 (size.width() as u32, size.height() as u32)
118 }
119
120 fn color_type(&self) -> ColorType {
121 let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file);
122 if returns_alpha {
123 ColorType::Rgba32F
124 } else {
125 ColorType::Rgb32F
126 }
127 }
128
129 fn original_color_type(&self) -> ExtendedColorType {
130 if self.alpha_present_in_file {
131 ExtendedColorType::Rgba32F
132 } else {
133 ExtendedColorType::Rgb32F
134 }
135 }
136
137 fn into_reader(self) -> ImageResult<Self::Reader> {
140 Ok(Cursor::new(decoder_to_vec(self)?))
141 }
142
143 fn scanline_bytes(&self) -> u64 {
144 self.total_bytes()
149 }
150
151 fn read_image_with_progress<F: Fn(Progress)>(
153 self,
154 unaligned_bytes: &mut [u8],
155 progress_callback: F,
156 ) -> ImageResult<()> {
157 let blocks_in_header = self.selected_exr_header().chunk_count as u64;
158 let channel_count = self.color_type().channel_count() as usize;
159
160 let display_window = self.selected_exr_header().shared_attributes.display_window;
161 let data_window_offset =
162 self.selected_exr_header().own_attributes.layer_position - display_window.position;
163
164 {
165 let (width, height) = self.dimensions();
167 let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
168 let expected_byte_count = (width as usize)
169 .checked_mul(height as usize)
170 .and_then(|size| size.checked_mul(bytes_per_pixel));
171
172 let has_invalid_size_or_overflowed = expected_byte_count
174 .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
175 .unwrap_or(true);
178
179 if has_invalid_size_or_overflowed {
180 panic!("byte buffer not large enough for the specified dimensions and f32 pixels");
181 }
182 }
183
184 let result = read()
185 .no_deep_data()
186 .largest_resolution_level()
187 .rgba_channels(
188 move |_size, _channels| vec![0_f32; display_window.size.area() * channel_count],
189 move |buffer, index_in_data_window, (r, g, b, a_or_1): (f32, f32, f32, f32)| {
190 let index_in_display_window =
191 index_in_data_window.to_i32() + data_window_offset;
192
193 if index_in_display_window.x() >= 0
196 && index_in_display_window.y() >= 0
197 && index_in_display_window.x() < display_window.size.width() as i32
198 && index_in_display_window.y() < display_window.size.height() as i32
199 {
200 let index_in_display_window =
201 index_in_display_window.to_usize("index bug").unwrap();
202 let first_f32_index =
203 index_in_display_window.flat_index_for_size(display_window.size);
204
205 buffer[first_f32_index * channel_count
206 ..(first_f32_index + 1) * channel_count]
207 .copy_from_slice(&[r, g, b, a_or_1][0..channel_count]);
208
209 }
211 },
212 )
213 .first_valid_layer() .all_attributes()
215 .on_progress(|progress| {
216 progress_callback(
217 Progress::new(
218 (progress * blocks_in_header as f64) as u64,
219 blocks_in_header,
220 ), );
222 })
223 .from_chunks(self.exr_reader)
224 .map_err(to_image_err)?;
225
226 unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
231 result.layer_data.channel_data.pixels.as_slice(),
232 ));
233 Ok(())
234 }
235}
236
237fn write_buffer(
244 mut buffered_write: impl Write + Seek,
245 unaligned_bytes: &[u8],
246 width: u32,
247 height: u32,
248 color_type: ColorType,
249) -> ImageResult<()> {
250 let width = width as usize;
251 let height = height as usize;
252
253 {
254 let expected_byte_count = width
256 .checked_mul(height)
257 .and_then(|size| size.checked_mul(color_type.bytes_per_pixel() as usize));
258
259 let has_invalid_size_or_overflowed = expected_byte_count
261 .map(|expected_byte_count| unaligned_bytes.len() < expected_byte_count)
262 .unwrap_or(true);
265
266 if has_invalid_size_or_overflowed {
267 return Err(ImageError::Encoding(EncodingError::new(
268 ImageFormatHint::Exact(ImageFormat::OpenExr),
269 "byte buffer not large enough for the specified dimensions and f32 pixels",
270 )));
271 }
272 }
273
274 let bytes_per_pixel = color_type.bytes_per_pixel() as usize;
275
276 match color_type {
277 ColorType::Rgb32F => {
278 exr::prelude::Image ::from_channels(
280 (width, height),
281 SpecificChannels::rgb(|pixel: Vec2<usize>| {
282 let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
283 let start_byte = pixel_index * bytes_per_pixel;
284
285 let [r, g, b]: [f32; 3] = bytemuck::pod_read_unaligned(
286 &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
287 );
288
289 (r, g, b)
290 }),
291 )
292 .write()
293 .to_buffered(&mut buffered_write)
295 .map_err(to_image_err)?;
296 }
297
298 ColorType::Rgba32F => {
299 exr::prelude::Image ::from_channels(
301 (width, height),
302 SpecificChannels::rgba(|pixel: Vec2<usize>| {
303 let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
304 let start_byte = pixel_index * bytes_per_pixel;
305
306 let [r, g, b, a]: [f32; 4] = bytemuck::pod_read_unaligned(
307 &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
308 );
309
310 (r, g, b, a)
311 }),
312 )
313 .write()
314 .to_buffered(&mut buffered_write)
316 .map_err(to_image_err)?;
317 }
318
319 unsupported_color_type => {
321 return Err(ImageError::Encoding(EncodingError::new(
322 ImageFormatHint::Exact(ImageFormat::OpenExr),
323 format!(
324 "writing color type {:?} not yet supported",
325 unsupported_color_type
326 ),
327 )))
328 }
329 }
330
331 Ok(())
332}
333
334#[derive(Debug)]
337pub struct OpenExrEncoder<W>(W);
338
339impl<W> OpenExrEncoder<W> {
340 pub fn new(write: W) -> Self {
343 Self(write)
344 }
345}
346
347impl<W> ImageEncoder for OpenExrEncoder<W>
348where
349 W: Write + Seek,
350{
351 #[track_caller]
356 fn write_image(
357 self,
358 buf: &[u8],
359 width: u32,
360 height: u32,
361 color_type: ColorType,
362 ) -> ImageResult<()> {
363 let expected_buffer_len =
364 (width as u64 * height as u64).saturating_mul(color_type.bytes_per_pixel() as u64);
365 assert_eq!(
366 expected_buffer_len,
367 buf.len() as u64,
368 "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
369 buf.len(),
370 );
371
372 write_buffer(self.0, buf, width, height, color_type)
373 }
374}
375
376fn to_image_err(exr_error: Error) -> ImageError {
377 ImageError::Decoding(DecodingError::new(
378 ImageFormatHint::Exact(ImageFormat::OpenExr),
379 exr_error.to_string(),
380 ))
381}
382
383#[cfg(test)]
384mod test {
385 use super::*;
386
387 use std::io::BufReader;
388 use std::path::{Path, PathBuf};
389
390 use crate::buffer_::{Rgb32FImage, Rgba32FImage};
391 use crate::error::{LimitError, LimitErrorKind};
392 use crate::{ImageBuffer, Rgb, Rgba};
393
394 const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"];
395
396 fn write_rgb_image(write: impl Write + Seek, image: &Rgb32FImage) -> ImageResult<()> {
400 write_buffer(
401 write,
402 bytemuck::cast_slice(image.as_raw().as_slice()),
403 image.width(),
404 image.height(),
405 ColorType::Rgb32F,
406 )
407 }
408
409 fn write_rgba_image(write: impl Write + Seek, image: &Rgba32FImage) -> ImageResult<()> {
413 write_buffer(
414 write,
415 bytemuck::cast_slice(image.as_raw().as_slice()),
416 image.width(),
417 image.height(),
418 ColorType::Rgba32F,
419 )
420 }
421
422 fn read_as_rgba_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgba32FImage> {
424 read_as_rgba_image(BufReader::new(std::fs::File::open(path)?))
425 }
426
427 fn read_as_rgb_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgb32FImage> {
429 read_as_rgb_image(BufReader::new(std::fs::File::open(path)?))
430 }
431
432 fn read_as_rgb_image(read: impl Read + Seek) -> ImageResult<Rgb32FImage> {
434 let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?;
435 let (width, height) = decoder.dimensions();
436 let buffer: Vec<f32> = decoder_to_vec(decoder)?;
437
438 ImageBuffer::from_raw(width, height, buffer)
439 .ok_or_else(|| {
442 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
443 })
444 }
445
446 fn read_as_rgba_image(read: impl Read + Seek) -> ImageResult<Rgba32FImage> {
448 let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?;
449 let (width, height) = decoder.dimensions();
450 let buffer: Vec<f32> = decoder_to_vec(decoder)?;
451
452 ImageBuffer::from_raw(width, height, buffer)
453 .ok_or_else(|| {
456 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
457 })
458 }
459
460 #[test]
461 fn compare_exr_hdr() {
462 if cfg!(not(feature = "hdr")) {
463 eprintln!("warning: to run all the openexr tests, activate the hdr feature flag");
464 }
465
466 #[cfg(feature = "hdr")]
467 {
468 let folder = BASE_PATH.iter().collect::<PathBuf>();
469 let reference_path = folder.clone().join("overexposed gradient.hdr");
470 let exr_path = folder
471 .clone()
472 .join("overexposed gradient - data window equals display window.exr");
473
474 let hdr: Vec<Rgb<f32>> = crate::codecs::hdr::HdrDecoder::new(std::io::BufReader::new(
475 std::fs::File::open(reference_path).unwrap(),
476 ))
477 .unwrap()
478 .read_image_hdr()
479 .unwrap();
480
481 let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap();
482 assert_eq!(
483 exr_pixels.dimensions().0 * exr_pixels.dimensions().1,
484 hdr.len() as u32
485 );
486
487 for (expected, found) in hdr.iter().zip(exr_pixels.pixels()) {
488 for (expected, found) in expected.0.iter().zip(found.0.iter()) {
489 assert!(
492 (expected - found).abs() < 0.1,
493 "expected {}, found {}",
494 expected,
495 found
496 );
497 }
498 }
499 }
500 }
501
502 #[test]
503 fn roundtrip_rgba() {
504 let mut next_random = vec![1.0, 0.0, -1.0, -3.15, 27.0, 11.0, 31.0]
505 .into_iter()
506 .cycle();
507 let mut next_random = move || next_random.next().unwrap();
508
509 let generated_image: Rgba32FImage = ImageBuffer::from_fn(9, 31, |_x, _y| {
510 Rgba([next_random(), next_random(), next_random(), next_random()])
511 });
512
513 let mut bytes = vec![];
514 write_rgba_image(Cursor::new(&mut bytes), &generated_image).unwrap();
515 let decoded_image = read_as_rgba_image(Cursor::new(bytes)).unwrap();
516
517 debug_assert_eq!(generated_image, decoded_image);
518 }
519
520 #[test]
521 fn roundtrip_rgb() {
522 let mut next_random = vec![1.0, 0.0, -1.0, -3.15, 27.0, 11.0, 31.0]
523 .into_iter()
524 .cycle();
525 let mut next_random = move || next_random.next().unwrap();
526
527 let generated_image: Rgb32FImage = ImageBuffer::from_fn(9, 31, |_x, _y| {
528 Rgb([next_random(), next_random(), next_random()])
529 });
530
531 let mut bytes = vec![];
532 write_rgb_image(Cursor::new(&mut bytes), &generated_image).unwrap();
533 let decoded_image = read_as_rgb_image(Cursor::new(bytes)).unwrap();
534
535 debug_assert_eq!(generated_image, decoded_image);
536 }
537
538 #[test]
539 fn compare_rgba_rgb() {
540 let exr_path = BASE_PATH
541 .iter()
542 .collect::<PathBuf>()
543 .join("overexposed gradient - data window equals display window.exr");
544
545 let rgb: Rgb32FImage = read_as_rgb_image_from_file(&exr_path).unwrap();
546 let rgba: Rgba32FImage = read_as_rgba_image_from_file(&exr_path).unwrap();
547
548 assert_eq!(rgba.dimensions(), rgb.dimensions());
549
550 for (Rgb(rgb), Rgba(rgba)) in rgb.pixels().zip(rgba.pixels()) {
551 assert_eq!(rgb, &rgba[..3]);
552 }
553 }
554
555 #[test]
556 fn compare_cropped() {
557 let exr_path = BASE_PATH.iter().collect::<PathBuf>();
567 let original = exr_path.clone().join("cropping - uncropped original.exr");
568 let cropped = exr_path
569 .clone()
570 .join("cropping - data window differs display window.exr");
571
572 {
574 let original_exr = read_first_flat_layer_from_file(&original).unwrap();
575 let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap();
576 assert_eq!(
577 original_exr.attributes.display_window,
578 cropped_exr.attributes.display_window
579 );
580 assert_ne!(
581 original_exr.layer_data.attributes.layer_position,
582 cropped_exr.layer_data.attributes.layer_position
583 );
584 assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size);
585 }
586
587 let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap();
589 let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap();
590 assert_eq!(original.dimensions(), cropped.dimensions());
591
592 assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b));
595 }
596}