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::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/// An OpenEXR decoder. Immediately reads the meta data from the file.
34#[derive(Debug)]
35pub struct OpenExrDecoder<R> {
36    exr_reader: exr::block::reader::Reader<R>,
37
38    // select a header that is rgb and not deep
39    header_index: usize,
40
41    // decode either rgb or rgba.
42    // can be specified to include or discard alpha channels.
43    // if none, the alpha channel will only be allocated where the file contains data for it.
44    alpha_preference: Option<bool>,
45
46    alpha_present_in_file: bool,
47}
48
49impl<R: Read + Seek> OpenExrDecoder<R> {
50    /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
51    /// Assumes the reader is buffered. In most cases,
52    /// you should wrap your reader in a `BufReader` for best performance.
53    /// Loads an alpha channel if the file has alpha samples.
54    /// Use `with_alpha_preference` if you want to load or not load alpha unconditionally.
55    pub fn new(source: R) -> ImageResult<Self> {
56        Self::with_alpha_preference(source, None)
57    }
58
59    /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
60    /// Assumes the reader is buffered. In most cases,
61    /// you should wrap your reader in a `BufReader` for best performance.
62    /// If alpha preference is specified, an alpha channel will
63    /// always be present or always be not present in the returned image.
64    /// If alpha preference is none, the alpha channel will only be returned if it is found in the file.
65    pub fn with_alpha_preference(source: R, alpha_preference: Option<bool>) -> ImageResult<Self> {
66        // read meta data, then wait for further instructions, keeping the file open and ready
67        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                // check if r/g/b exists in the channels
74                let has_rgb = ["R", "G", "B"]
75                    .iter()
76                    .all(|&required|  // alpha will be optional
77                    header.channels.find_index_of_channel(&Text::from(required)).is_some());
78
79                // we currently dont support deep images, or images with other color spaces than rgb
80                !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    // does not leak exrs-specific meta data into public api, just does it for this module
103    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    /// Use `read_image` instead if possible,
138    /// as this method creates a whole new buffer just to contain the entire image.
139    fn into_reader(self) -> ImageResult<Self::Reader> {
140        Ok(Cursor::new(decoder_to_vec(self)?))
141    }
142
143    fn scanline_bytes(&self) -> u64 {
144        // we cannot always read individual scan lines for every file,
145        // as the tiles or lines in the file could be in random or reversed order.
146        // therefore we currently read all lines at once
147        // Todo: optimize for specific exr.line_order?
148        self.total_bytes()
149    }
150
151    // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file`
152    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            // check whether the buffer is large enough for the dimensions of the file
166            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            // if the width and height does not match the length of the bytes, the arguments are invalid
173            let has_invalid_size_or_overflowed = expected_byte_count
174                .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
175                // otherwise, size calculation overflowed, is bigger than memory,
176                // therefore data is too small, so it is invalid.
177                .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                    // only keep pixels inside the data window
194                    // TODO filter chunks based on this
195                    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                        // TODO white point chromaticities + srgb/linear conversion?
210                    }
211                },
212            )
213            .first_valid_layer() // TODO select exact layer by self.header_index?
214            .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                    ), // TODO precision errors?
221                );
222            })
223            .from_chunks(self.exr_reader)
224            .map_err(to_image_err)?;
225
226        // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice
227
228        // this cast is safe and works with any alignment, as bytes are copied, and not f32 values.
229        // note: buffer slice length is checked in the beginning of this function and will be correct at this point
230        unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
231            result.layer_data.channel_data.pixels.as_slice(),
232        ));
233        Ok(())
234    }
235}
236
237/// Write a raw byte buffer of pixels,
238/// returning an Error if it has an invalid length.
239///
240/// Assumes the writer is buffered. In most cases,
241/// you should wrap your writer in a `BufWriter` for best performance.
242// private. access via `OpenExrEncoder`
243fn 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        // check whether the buffer is large enough for the specified dimensions
255        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        // if the width and height does not match the length of the bytes, the arguments are invalid
260        let has_invalid_size_or_overflowed = expected_byte_count
261            .map(|expected_byte_count| unaligned_bytes.len() < expected_byte_count)
262            // otherwise, size calculation overflowed, is bigger than memory,
263            // therefore data is too small, so it is invalid.
264            .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 // TODO compression method zip??
279                ::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            // .on_progress(|progress| todo!())
294            .to_buffered(&mut buffered_write)
295            .map_err(to_image_err)?;
296        }
297
298        ColorType::Rgba32F => {
299            exr::prelude::Image // TODO compression method zip??
300                ::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            // .on_progress(|progress| todo!())
315            .to_buffered(&mut buffered_write)
316            .map_err(to_image_err)?;
317        }
318
319        // TODO other color types and channel types
320        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// TODO is this struct and trait actually used anywhere?
335/// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`.
336#[derive(Debug)]
337pub struct OpenExrEncoder<W>(W);
338
339impl<W> OpenExrEncoder<W> {
340    /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`.
341    // use constructor, not public field, for future backwards-compatibility
342    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    /// Writes the complete image.
352    ///
353    /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter`
354    /// for best performance.
355    #[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    /// Write an `Rgb32FImage`.
397    /// Assumes the writer is buffered. In most cases,
398    /// you should wrap your writer in a `BufWriter` for best performance.
399    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    /// Write an `Rgba32FImage`.
410    /// Assumes the writer is buffered. In most cases,
411    /// you should wrap your writer in a `BufWriter` for best performance.
412    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    /// Read the file from the specified path into an `Rgba32FImage`.
423    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    /// Read the file from the specified path into an `Rgb32FImage`.
428    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    /// Read the file from the specified path into an `Rgb32FImage`.
433    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            // this should be the only reason for the "from raw" call to fail,
440            // even though such a large allocation would probably cause an error much earlier
441            .ok_or_else(|| {
442                ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
443            })
444    }
445
446    /// Read the file from the specified path into an `Rgba32FImage`.
447    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            // this should be the only reason for the "from raw" call to fail,
454            // even though such a large allocation would probably cause an error much earlier
455            .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                    // the large tolerance seems to be caused by
490                    // the RGBE u8x4 pixel quantization of the hdr image format
491                    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        // like in photoshop, exr images may have layers placed anywhere in a canvas.
558        // we don't want to load the pixels from the layer, but we want to load the pixels from the canvas.
559        // a layer might be smaller than the canvas, in that case the canvas should be transparent black
560        // where no layer was covering it. a layer might also be larger than the canvas,
561        // these pixels should be discarded.
562        //
563        // in this test we want to make sure that an
564        // auto-cropped image will be reproduced to the original.
565
566        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        // smoke-check that the exr files are actually not the same
573        {
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        // check that they result in the same image
588        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        // the following is not a simple assert_eq, as in case of an error,
593        // the whole image would be printed to the console, which takes forever
594        assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b));
595    }
596}