1//! Decoding of OpenEXR (.exr) Images
2//!
3//! OpenEXR is an image format that is widely used, especially in VFX,
4//! because it supports lossless and lossy compression for float data.
5//!
6//! This decoder only supports RGB and RGBA images.
7//! If an image does not contain alpha information,
8//! it is defaulted to `1.0` (no transparency).
9//!
10//! # Related Links
11//! * <https://www.openexr.com/documentation.html> - The OpenEXR reference.
12//!
13//!
14//! Current limitations (July 2021):
15//! - only pixel type `Rgba32F` and `Rgba16F` are supported
16//! - only non-deep rgb/rgba files supported, no conversion from/to YCbCr or similar
17//! - only the first non-deep rgb layer is used
18//! - only the largest mip map level is used
19//! - pixels outside display window are lost
20//! - meta data is lost
21//! - dwaa/dwab compressed images not supported yet by the exr library
22//! - (chroma) subsampling not supported yet by the exr library
23use 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/// An OpenEXR decoder. Immediately reads the meta data from the file.
33#[derive(Debug)]
34pub struct OpenExrDecoder<R> {
35 exr_reader: exr::block::reader::Reader<R>,
36
37 // select a header that is rgb and not deep
38 header_index: usize,
39
40 // decode either rgb or rgba.
41 // can be specified to include or discard alpha channels.
42 // if none, the alpha channel will only be allocated where the file contains data for it.
43 alpha_preference: Option<bool>,
44
45 alpha_present_in_file: bool,
46}
47
48impl<R: BufRead + Seek> OpenExrDecoder<R> {
49 /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
50 /// Assumes the reader is buffered. In most cases,
51 /// you should wrap your reader in a `BufReader` for best performance.
52 /// Loads an alpha channel if the file has alpha samples.
53 /// Use `with_alpha_preference` if you want to load or not load alpha unconditionally.
54 pub fn new(source: R) -> ImageResult<Self> {
55 Self::with_alpha_preference(source, None)
56 }
57
58 /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
59 /// Assumes the reader is buffered. In most cases,
60 /// you should wrap your reader in a `BufReader` for best performance.
61 /// If alpha preference is specified, an alpha channel will
62 /// always be present or always be not present in the returned image.
63 /// If alpha preference is none, the alpha channel will only be returned if it is found in the file.
64 pub fn with_alpha_preference(source: R, alpha_preference: Option<bool>) -> ImageResult<Self> {
65 // read meta data, then wait for further instructions, keeping the file open and ready
66 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 // check if r/g/b exists in the channels
73 let has_rgb = ["R", "G", "B"]
74 .iter()
75 .all(|&required| // alpha will be optional
76 header.channels.find_index_of_channel(&Text::from(required)).is_some());
77
78 // we currently dont support deep images, or images with other color spaces than rgb
79 !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 // does not leak exrs-specific meta data into public api, just does it for this module
102 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 // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file`
135 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 // check whether the buffer is large enough for the dimensions of the file
145 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 // if the width and height does not match the length of the bytes, the arguments are invalid
152 let has_invalid_size_or_overflowed = expected_byte_count
153 .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
154 // otherwise, size calculation overflowed, is bigger than memory,
155 // therefore data is too small, so it is invalid.
156 .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 // only keep pixels inside the data window
174 // TODO filter chunks based on this
175 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 // TODO white point chromaticities + srgb/linear conversion?
190 }
191 },
192 )
193 .first_valid_layer() // TODO select exact layer by self.header_index?
194 .all_attributes()
195 .from_chunks(self.exr_reader)
196 .map_err(to_image_err)?;
197
198 // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice
199
200 // this cast is safe and works with any alignment, as bytes are copied, and not f32 values.
201 // note: buffer slice length is checked in the beginning of this function and will be correct at this point
202 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
213/// Write a raw byte buffer of pixels,
214/// returning an Error if it has an invalid length.
215///
216/// Assumes the writer is buffered. In most cases,
217/// you should wrap your writer in a `BufWriter` for best performance.
218// private. access via `OpenExrEncoder`
219fn 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 // TODO compression method zip??
233 ::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 // .on_progress(|progress| todo!())
248 .to_buffered(&mut buffered_write)
249 .map_err(to_image_err)?;
250 }
251
252 ExtendedColorType::Rgba32F => {
253 Image // TODO compression method zip??
254 ::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 // .on_progress(|progress| todo!())
269 .to_buffered(&mut buffered_write)
270 .map_err(to_image_err)?;
271 }
272
273 // TODO other color types and channel types
274 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// TODO is this struct and trait actually used anywhere?
286/// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`.
287#[derive(Debug)]
288pub struct OpenExrEncoder<W>(W);
289
290impl<W> OpenExrEncoder<W> {
291 /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`.
292 // use constructor, not public field, for future backwards-compatibility
293 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 /// Writes the complete image.
303 ///
304 /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter`
305 /// for best performance.
306 #[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: u64 = 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, unaligned_bytes:buf, width, height, color_type)
323 }
324}
325
326fn to_image_err(exr_error: Error) -> ImageError {
327 ImageError::Decoding(DecodingError::new(
328 format:ImageFormatHint::Exact(ImageFormat::OpenExr),
329 err: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 /// Write an `Rgb32FImage`.
348 /// Assumes the writer is buffered. In most cases,
349 /// you should wrap your writer in a `BufWriter` for best performance.
350 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 /// Write an `Rgba32FImage`.
361 /// Assumes the writer is buffered. In most cases,
362 /// you should wrap your writer in a `BufWriter` for best performance.
363 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 /// Read the file from the specified path into an `Rgba32FImage`.
374 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 /// Read the file from the specified path into an `Rgb32FImage`.
379 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 /// Read the file from the specified path into an `Rgb32FImage`.
384 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 // this should be the only reason for the "from raw" call to fail,
391 // even though such a large allocation would probably cause an error much earlier
392 .ok_or_else(|| {
393 ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
394 })
395 }
396
397 /// Read the file from the specified path into an `Rgba32FImage`.
398 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 // this should be the only reason for the "from raw" call to fail,
405 // even though such a large allocation would probably cause an error much earlier
406 .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 // the large tolerance seems to be caused by
440 // the RGBE u8x4 pixel quantization of the hdr image format
441 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 // like in photoshop, exr images may have layers placed anywhere in a canvas.
508 // we don't want to load the pixels from the layer, but we want to load the pixels from the canvas.
509 // a layer might be smaller than the canvas, in that case the canvas should be transparent black
510 // where no layer was covering it. a layer might also be larger than the canvas,
511 // these pixels should be discarded.
512 //
513 // in this test we want to make sure that an
514 // auto-cropped image will be reproduced to the original.
515
516 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 // smoke-check that the exr files are actually not the same
523 {
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 // check that they result in the same image
538 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 // the following is not a simple assert_eq, as in case of an error,
543 // the whole image would be printed to the console, which takes forever
544 assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b));
545 }
546}
547