image/codecs/
openexr.rs

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 = 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    /// 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}