1use exr::prelude::*;
24
25use crate::error::{DecodingError, EncodingError, ImageFormatHint};
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::Encoding(EncodingError::new(
276 ImageFormatHint::Exact(ImageFormat::OpenExr),
277 format!("writing color type {unsupported_color_type:?} not yet supported"),
278 )))
279 }
280 }
281
282 Ok(())
283}
284
285#[derive(Debug)]
288pub struct OpenExrEncoder<W>(W);
289
290impl<W> OpenExrEncoder<W> {
291 pub fn new(write: W) -> Self {
294 Self(write)
295 }
296}
297
298impl<W> ImageEncoder for OpenExrEncoder<W>
299where
300 W: Write + Seek,
301{
302 #[track_caller]
307 fn write_image(
308 self,
309 buf: &[u8],
310 width: u32,
311 height: u32,
312 color_type: ExtendedColorType,
313 ) -> ImageResult<()> {
314 let expected_buffer_len = color_type.buffer_size(width, height);
315 assert_eq!(
316 expected_buffer_len,
317 buf.len() as u64,
318 "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
319 buf.len(),
320 );
321
322 write_buffer(self.0, buf, width, height, color_type)
323 }
324}
325
326fn to_image_err(exr_error: Error) -> ImageError {
327 ImageError::Decoding(DecodingError::new(
328 ImageFormatHint::Exact(ImageFormat::OpenExr),
329 exr_error.to_string(),
330 ))
331}
332
333#[cfg(test)]
334mod test {
335 use super::*;
336
337 use std::fs::File;
338 use std::io::{BufReader, Cursor};
339 use std::path::{Path, PathBuf};
340
341 use crate::buffer_::{Rgb32FImage, Rgba32FImage};
342 use crate::error::{LimitError, LimitErrorKind};
343 use crate::{DynamicImage, ImageBuffer, Rgb, Rgba};
344
345 const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"];
346
347 fn write_rgb_image(write: impl Write + Seek, image: &Rgb32FImage) -> ImageResult<()> {
351 write_buffer(
352 write,
353 bytemuck::cast_slice(image.as_raw().as_slice()),
354 image.width(),
355 image.height(),
356 ExtendedColorType::Rgb32F,
357 )
358 }
359
360 fn write_rgba_image(write: impl Write + Seek, image: &Rgba32FImage) -> ImageResult<()> {
364 write_buffer(
365 write,
366 bytemuck::cast_slice(image.as_raw().as_slice()),
367 image.width(),
368 image.height(),
369 ExtendedColorType::Rgba32F,
370 )
371 }
372
373 fn read_as_rgba_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgba32FImage> {
375 read_as_rgba_image(BufReader::new(File::open(path)?))
376 }
377
378 fn read_as_rgb_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgb32FImage> {
380 read_as_rgb_image(BufReader::new(File::open(path)?))
381 }
382
383 fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult<Rgb32FImage> {
385 let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?;
386 let (width, height) = decoder.dimensions();
387 let buffer: Vec<f32> = crate::image::decoder_to_vec(decoder)?;
388
389 ImageBuffer::from_raw(width, height, buffer)
390 .ok_or_else(|| {
393 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
394 })
395 }
396
397 fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult<Rgba32FImage> {
399 let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?;
400 let (width, height) = decoder.dimensions();
401 let buffer: Vec<f32> = crate::image::decoder_to_vec(decoder)?;
402
403 ImageBuffer::from_raw(width, height, buffer)
404 .ok_or_else(|| {
407 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
408 })
409 }
410
411 #[test]
412 fn compare_exr_hdr() {
413 if cfg!(not(feature = "hdr")) {
414 eprintln!("warning: to run all the openexr tests, activate the hdr feature flag");
415 }
416
417 #[cfg(feature = "hdr")]
418 {
419 use crate::codecs::hdr::HdrDecoder;
420
421 let folder = BASE_PATH.iter().collect::<PathBuf>();
422 let reference_path = folder.clone().join("overexposed gradient.hdr");
423 let exr_path = folder
424 .clone()
425 .join("overexposed gradient - data window equals display window.exr");
426
427 let hdr_decoder =
428 HdrDecoder::new(BufReader::new(File::open(reference_path).unwrap())).unwrap();
429 let hdr: Rgb32FImage = match DynamicImage::from_decoder(hdr_decoder).unwrap() {
430 DynamicImage::ImageRgb32F(image) => image,
431 _ => panic!("expected rgb32f image"),
432 };
433
434 let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap();
435 assert_eq!(exr_pixels.dimensions(), hdr.dimensions());
436
437 for (expected, found) in hdr.pixels().zip(exr_pixels.pixels()) {
438 for (expected, found) in expected.0.iter().zip(found.0.iter()) {
439 assert!(
442 (expected - found).abs() < 0.1,
443 "expected {}, found {}",
444 expected,
445 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.clone().join("cropping - uncropped original.exr");
518 let cropped = exr_path
519 .clone()
520 .join("cropping - data window differs display window.exr");
521
522 {
524 let original_exr = read_first_flat_layer_from_file(&original).unwrap();
525 let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap();
526 assert_eq!(
527 original_exr.attributes.display_window,
528 cropped_exr.attributes.display_window
529 );
530 assert_ne!(
531 original_exr.layer_data.attributes.layer_position,
532 cropped_exr.layer_data.attributes.layer_position
533 );
534 assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size);
535 }
536
537 let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap();
539 let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap();
540 assert_eq!(original.dimensions(), cropped.dimensions());
541
542 assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b));
545 }
546}