Coverage Report

Created: 2025-12-11 07:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/codecs/openexr.rs
Line
Count
Source
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
23
use exr::prelude::*;
24
25
use crate::error::{DecodingError, ImageFormatHint, UnsupportedError, UnsupportedErrorKind};
26
use crate::{
27
    ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult,
28
};
29
30
use std::io::{BufRead, Seek, Write};
31
32
/// An OpenEXR decoder. Immediately reads the meta data from the file.
33
#[derive(Debug)]
34
pub 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
48
impl<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
4.19k
    pub fn new(source: R) -> ImageResult<Self> {
55
4.19k
        Self::with_alpha_preference(source, None)
56
4.19k
    }
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
6.21k
    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
6.21k
        let exr_reader = exr::block::read(source, false).map_err(to_image_err)?;
67
68
2.32k
        let header_index = exr_reader
69
2.32k
            .headers()
70
2.32k
            .iter()
71
9.69k
            .position(|header| {
72
                // check if r/g/b exists in the channels
73
9.69k
                let has_rgb = ["R", "G", "B"]
74
9.69k
                    .iter()
75
9.69k
                    .all(|&required|  // alpha will be optional
76
21.0k
                    header.channels.find_index_of_channel(&Text::from(required)).is_some());
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference::{closure#0}::{closure#0}
Line
Count
Source
76
20.8k
                    header.channels.find_index_of_channel(&Text::from(required)).is_some());
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference::{closure#0}::{closure#0}
Line
Count
Source
76
240
                    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
9.69k
                !header.deep && has_rgb
80
9.69k
            })
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference::{closure#0}
Line
Count
Source
71
9.61k
            .position(|header| {
72
                // check if r/g/b exists in the channels
73
9.61k
                let has_rgb = ["R", "G", "B"]
74
9.61k
                    .iter()
75
9.61k
                    .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
9.61k
                !header.deep && has_rgb
80
9.61k
            })
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference::{closure#0}
Line
Count
Source
71
80
            .position(|header| {
72
                // check if r/g/b exists in the channels
73
80
                let has_rgb = ["R", "G", "B"]
74
80
                    .iter()
75
80
                    .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
80
                !header.deep && has_rgb
80
80
            })
81
2.32k
            .ok_or_else(|| {
82
64
                ImageError::Decoding(DecodingError::new(
83
64
                    ImageFormatHint::Exact(ImageFormat::OpenExr),
84
64
                    "image does not contain non-deep rgb channels",
85
64
                ))
86
64
            })?;
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference::{closure#1}
Line
Count
Source
81
64
            .ok_or_else(|| {
82
64
                ImageError::Decoding(DecodingError::new(
83
64
                    ImageFormatHint::Exact(ImageFormat::OpenExr),
84
64
                    "image does not contain non-deep rgb channels",
85
64
                ))
86
64
            })?;
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference::{closure#1}
87
88
2.26k
        let has_alpha = exr_reader.headers()[header_index]
89
2.26k
            .channels
90
2.26k
            .find_index_of_channel(&Text::from("A"))
91
2.26k
            .is_some();
92
93
2.26k
        Ok(Self {
94
2.26k
            alpha_preference,
95
2.26k
            exr_reader,
96
2.26k
            header_index,
97
2.26k
            alpha_present_in_file: has_alpha,
98
2.26k
        })
99
6.21k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference
Line
Count
Source
64
6.13k
    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
6.13k
        let exr_reader = exr::block::read(source, false).map_err(to_image_err)?;
67
68
2.24k
        let header_index = exr_reader
69
2.24k
            .headers()
70
2.24k
            .iter()
71
2.24k
            .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
2.24k
            .ok_or_else(|| {
82
                ImageError::Decoding(DecodingError::new(
83
                    ImageFormatHint::Exact(ImageFormat::OpenExr),
84
                    "image does not contain non-deep rgb channels",
85
                ))
86
64
            })?;
87
88
2.18k
        let has_alpha = exr_reader.headers()[header_index]
89
2.18k
            .channels
90
2.18k
            .find_index_of_channel(&Text::from("A"))
91
2.18k
            .is_some();
92
93
2.18k
        Ok(Self {
94
2.18k
            alpha_preference,
95
2.18k
            exr_reader,
96
2.18k
            header_index,
97
2.18k
            alpha_present_in_file: has_alpha,
98
2.18k
        })
99
6.13k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference
Line
Count
Source
64
80
    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
80
        let exr_reader = exr::block::read(source, false).map_err(to_image_err)?;
67
68
80
        let header_index = exr_reader
69
80
            .headers()
70
80
            .iter()
71
80
            .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
80
            .ok_or_else(|| {
82
                ImageError::Decoding(DecodingError::new(
83
                    ImageFormatHint::Exact(ImageFormat::OpenExr),
84
                    "image does not contain non-deep rgb channels",
85
                ))
86
0
            })?;
87
88
80
        let has_alpha = exr_reader.headers()[header_index]
89
80
            .channels
90
80
            .find_index_of_channel(&Text::from("A"))
91
80
            .is_some();
92
93
80
        Ok(Self {
94
80
            alpha_preference,
95
80
            exr_reader,
96
80
            header_index,
97
80
            alpha_present_in_file: has_alpha,
98
80
        })
99
80
    }
100
101
    // does not leak exrs-specific meta data into public api, just does it for this module
102
17.0k
    fn selected_exr_header(&self) -> &exr::meta::header::Header {
103
17.0k
        &self.exr_reader.meta_data().headers[self.header_index]
104
17.0k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::selected_exr_header
Line
Count
Source
102
16.4k
    fn selected_exr_header(&self) -> &exr::meta::header::Header {
103
16.4k
        &self.exr_reader.meta_data().headers[self.header_index]
104
16.4k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::selected_exr_header
Line
Count
Source
102
560
    fn selected_exr_header(&self) -> &exr::meta::header::Header {
103
560
        &self.exr_reader.meta_data().headers[self.header_index]
104
560
    }
105
}
106
107
impl<R: BufRead + Seek> ImageDecoder for OpenExrDecoder<R> {
108
10.3k
    fn dimensions(&self) -> (u32, u32) {
109
10.3k
        let size = self
110
10.3k
            .selected_exr_header()
111
10.3k
            .shared_attributes
112
10.3k
            .display_window
113
10.3k
            .size;
114
10.3k
        (size.width() as u32, size.height() as u32)
115
10.3k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::dimensions
Line
Count
Source
108
10.0k
    fn dimensions(&self) -> (u32, u32) {
109
10.0k
        let size = self
110
10.0k
            .selected_exr_header()
111
10.0k
            .shared_attributes
112
10.0k
            .display_window
113
10.0k
            .size;
114
10.0k
        (size.width() as u32, size.height() as u32)
115
10.0k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::dimensions
Line
Count
Source
108
320
    fn dimensions(&self) -> (u32, u32) {
109
320
        let size = self
110
320
            .selected_exr_header()
111
320
            .shared_attributes
112
320
            .display_window
113
320
            .size;
114
320
        (size.width() as u32, size.height() as u32)
115
320
    }
116
117
9.58k
    fn color_type(&self) -> ColorType {
118
9.58k
        let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file);
119
9.58k
        if returns_alpha {
120
2.35k
            ColorType::Rgba32F
121
        } else {
122
7.22k
            ColorType::Rgb32F
123
        }
124
9.58k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::color_type
Line
Count
Source
117
9.34k
    fn color_type(&self) -> ColorType {
118
9.34k
        let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file);
119
9.34k
        if returns_alpha {
120
2.11k
            ColorType::Rgba32F
121
        } else {
122
7.22k
            ColorType::Rgb32F
123
        }
124
9.34k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::color_type
Line
Count
Source
117
240
    fn color_type(&self) -> ColorType {
118
240
        let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file);
119
240
        if returns_alpha {
120
240
            ColorType::Rgba32F
121
        } else {
122
0
            ColorType::Rgb32F
123
        }
124
240
    }
125
126
0
    fn original_color_type(&self) -> ExtendedColorType {
127
0
        if self.alpha_present_in_file {
128
0
            ExtendedColorType::Rgba32F
129
        } else {
130
0
            ExtendedColorType::Rgb32F
131
        }
132
0
    }
133
134
    // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file`
135
2.22k
    fn read_image(self, unaligned_bytes: &mut [u8]) -> ImageResult<()> {
136
2.22k
        let _blocks_in_header = self.selected_exr_header().chunk_count as u64;
137
2.22k
        let channel_count = self.color_type().channel_count() as usize;
138
139
2.22k
        let display_window = self.selected_exr_header().shared_attributes.display_window;
140
2.22k
        let data_window_offset =
141
2.22k
            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
2.22k
            let (width, height) = self.dimensions();
146
2.22k
            let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
147
2.22k
            let expected_byte_count = (width as usize)
148
2.22k
                .checked_mul(height as usize)
149
2.22k
                .and_then(|size| size.checked_mul(bytes_per_pixel));
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#0}
Line
Count
Source
149
2.14k
                .and_then(|size| size.checked_mul(bytes_per_pixel));
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#0}
Line
Count
Source
149
80
                .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
2.22k
            let has_invalid_size_or_overflowed = expected_byte_count
153
2.22k
                .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#1}
Line
Count
Source
153
2.14k
                .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#1}
Line
Count
Source
153
80
                .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
2.22k
                .unwrap_or(true);
157
158
2.22k
            assert!(
159
2.22k
                !has_invalid_size_or_overflowed,
160
0
                "byte buffer not large enough for the specified dimensions and f32 pixels"
161
            );
162
        }
163
164
2.22k
        let result = read()
165
2.22k
            .no_deep_data()
166
2.22k
            .largest_resolution_level()
167
2.22k
            .rgba_channels(
168
2.22k
                move |_size, _channels| vec![0_f32; display_window.size.area() * channel_count],
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#2}
Line
Count
Source
168
2.14k
                move |_size, _channels| vec![0_f32; display_window.size.area() * channel_count],
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#2}
Line
Count
Source
168
80
                move |_size, _channels| vec![0_f32; display_window.size.area() * channel_count],
169
43.4M
                move |buffer, index_in_data_window, (r, g, b, a_or_1): (f32, f32, f32, f32)| {
170
43.4M
                    let index_in_display_window =
171
43.4M
                        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
43.4M
                    if index_in_display_window.x() >= 0
176
43.3M
                        && index_in_display_window.y() >= 0
177
43.2M
                        && index_in_display_window.x() < display_window.size.width() as i32
178
43.2M
                        && index_in_display_window.y() < display_window.size.height() as i32
179
43.1M
                    {
180
43.1M
                        let index_in_display_window =
181
43.1M
                            index_in_display_window.to_usize("index bug").unwrap();
182
43.1M
                        let first_f32_index =
183
43.1M
                            index_in_display_window.flat_index_for_size(display_window.size);
184
43.1M
185
43.1M
                        buffer[first_f32_index * channel_count
186
43.1M
                            ..(first_f32_index + 1) * channel_count]
187
43.1M
                            .copy_from_slice(&[r, g, b, a_or_1][0..channel_count]);
188
43.1M
189
43.1M
                        // TODO white point chromaticities + srgb/linear conversion?
190
43.1M
                    }
191
43.4M
                },
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#3}
Line
Count
Source
169
10.8M
                move |buffer, index_in_data_window, (r, g, b, a_or_1): (f32, f32, f32, f32)| {
170
10.8M
                    let index_in_display_window =
171
10.8M
                        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
10.8M
                    if index_in_display_window.x() >= 0
176
10.7M
                        && index_in_display_window.y() >= 0
177
10.6M
                        && index_in_display_window.x() < display_window.size.width() as i32
178
10.5M
                        && index_in_display_window.y() < display_window.size.height() as i32
179
10.5M
                    {
180
10.5M
                        let index_in_display_window =
181
10.5M
                            index_in_display_window.to_usize("index bug").unwrap();
182
10.5M
                        let first_f32_index =
183
10.5M
                            index_in_display_window.flat_index_for_size(display_window.size);
184
10.5M
185
10.5M
                        buffer[first_f32_index * channel_count
186
10.5M
                            ..(first_f32_index + 1) * channel_count]
187
10.5M
                            .copy_from_slice(&[r, g, b, a_or_1][0..channel_count]);
188
10.5M
189
10.5M
                        // TODO white point chromaticities + srgb/linear conversion?
190
10.5M
                    }
191
10.8M
                },
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#3}
Line
Count
Source
169
32.6M
                move |buffer, index_in_data_window, (r, g, b, a_or_1): (f32, f32, f32, f32)| {
170
32.6M
                    let index_in_display_window =
171
32.6M
                        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
32.6M
                    if index_in_display_window.x() >= 0
176
32.6M
                        && index_in_display_window.y() >= 0
177
32.6M
                        && index_in_display_window.x() < display_window.size.width() as i32
178
32.6M
                        && index_in_display_window.y() < display_window.size.height() as i32
179
32.6M
                    {
180
32.6M
                        let index_in_display_window =
181
32.6M
                            index_in_display_window.to_usize("index bug").unwrap();
182
32.6M
                        let first_f32_index =
183
32.6M
                            index_in_display_window.flat_index_for_size(display_window.size);
184
32.6M
185
32.6M
                        buffer[first_f32_index * channel_count
186
32.6M
                            ..(first_f32_index + 1) * channel_count]
187
32.6M
                            .copy_from_slice(&[r, g, b, a_or_1][0..channel_count]);
188
32.6M
189
32.6M
                        // TODO white point chromaticities + srgb/linear conversion?
190
32.6M
                    }
191
32.6M
                },
192
            )
193
2.22k
            .first_valid_layer() // TODO select exact layer by self.header_index?
194
2.22k
            .all_attributes()
195
2.22k
            .from_chunks(self.exr_reader)
196
2.22k
            .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
160
        unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
203
160
            result.layer_data.channel_data.pixels.as_slice(),
204
160
        ));
205
160
        Ok(())
206
2.22k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image
Line
Count
Source
135
2.14k
    fn read_image(self, unaligned_bytes: &mut [u8]) -> ImageResult<()> {
136
2.14k
        let _blocks_in_header = self.selected_exr_header().chunk_count as u64;
137
2.14k
        let channel_count = self.color_type().channel_count() as usize;
138
139
2.14k
        let display_window = self.selected_exr_header().shared_attributes.display_window;
140
2.14k
        let data_window_offset =
141
2.14k
            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
2.14k
            let (width, height) = self.dimensions();
146
2.14k
            let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
147
2.14k
            let expected_byte_count = (width as usize)
148
2.14k
                .checked_mul(height as usize)
149
2.14k
                .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
2.14k
            let has_invalid_size_or_overflowed = expected_byte_count
153
2.14k
                .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
2.14k
                .unwrap_or(true);
157
158
2.14k
            assert!(
159
2.14k
                !has_invalid_size_or_overflowed,
160
0
                "byte buffer not large enough for the specified dimensions and f32 pixels"
161
            );
162
        }
163
164
2.14k
        let result = read()
165
2.14k
            .no_deep_data()
166
2.14k
            .largest_resolution_level()
167
2.14k
            .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
2.14k
            .first_valid_layer() // TODO select exact layer by self.header_index?
194
2.14k
            .all_attributes()
195
2.14k
            .from_chunks(self.exr_reader)
196
2.14k
            .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
80
        unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
203
80
            result.layer_data.channel_data.pixels.as_slice(),
204
80
        ));
205
80
        Ok(())
206
2.14k
    }
<image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image
Line
Count
Source
135
80
    fn read_image(self, unaligned_bytes: &mut [u8]) -> ImageResult<()> {
136
80
        let _blocks_in_header = self.selected_exr_header().chunk_count as u64;
137
80
        let channel_count = self.color_type().channel_count() as usize;
138
139
80
        let display_window = self.selected_exr_header().shared_attributes.display_window;
140
80
        let data_window_offset =
141
80
            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
80
            let (width, height) = self.dimensions();
146
80
            let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
147
80
            let expected_byte_count = (width as usize)
148
80
                .checked_mul(height as usize)
149
80
                .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
80
            let has_invalid_size_or_overflowed = expected_byte_count
153
80
                .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
80
                .unwrap_or(true);
157
158
80
            assert!(
159
80
                !has_invalid_size_or_overflowed,
160
0
                "byte buffer not large enough for the specified dimensions and f32 pixels"
161
            );
162
        }
163
164
80
        let result = read()
165
80
            .no_deep_data()
166
80
            .largest_resolution_level()
167
80
            .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
80
            .first_valid_layer() // TODO select exact layer by self.header_index?
194
80
            .all_attributes()
195
80
            .from_chunks(self.exr_reader)
196
80
            .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
80
        unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
203
80
            result.layer_data.channel_data.pixels.as_slice(),
204
80
        ));
205
80
        Ok(())
206
80
    }
207
208
1.44k
    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
209
1.44k
        (*self).read_image(buf)
210
1.44k
    }
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`
219
80
fn write_buffer(
220
80
    mut buffered_write: impl Write + Seek,
221
80
    unaligned_bytes: &[u8],
222
80
    width: u32,
223
80
    height: u32,
224
80
    color_type: ExtendedColorType,
225
80
) -> ImageResult<()> {
226
80
    let width = width as usize;
227
80
    let height = height as usize;
228
80
    let bytes_per_pixel = color_type.bits_per_pixel() as usize / 8;
229
230
80
    match color_type {
231
        ExtendedColorType::Rgb32F => {
232
0
            Image // TODO compression method zip??
233
0
                ::from_channels(
234
0
                (width, height),
235
0
                SpecificChannels::rgb(|pixel: Vec2<usize>| {
236
0
                    let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
237
0
                    let start_byte = pixel_index * bytes_per_pixel;
238
239
0
                    let [r, g, b]: [f32; 3] = bytemuck::pod_read_unaligned(
240
0
                        &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
241
0
                    );
242
243
0
                    (r, g, b)
244
0
                }),
Unexecuted instantiation: image::codecs::openexr::write_buffer::<_>::{closure#0}
Unexecuted instantiation: image::codecs::openexr::write_buffer::<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>::{closure#0}
Unexecuted instantiation: image::codecs::openexr::write_buffer::<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>::{closure#0}
245
            )
246
0
            .write()
247
            // .on_progress(|progress| todo!())
248
0
            .to_buffered(&mut buffered_write)
249
0
            .map_err(to_image_err)?;
250
        }
251
252
        ExtendedColorType::Rgba32F => {
253
80
            Image // TODO compression method zip??
254
80
                ::from_channels(
255
80
                (width, height),
256
32.6M
                SpecificChannels::rgba(|pixel: Vec2<usize>| {
257
32.6M
                    let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
258
32.6M
                    let start_byte = pixel_index * bytes_per_pixel;
259
260
32.6M
                    let [r, g, b, a]: [f32; 4] = bytemuck::pod_read_unaligned(
261
32.6M
                        &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
262
32.6M
                    );
263
264
32.6M
                    (r, g, b, a)
265
32.6M
                }),
Unexecuted instantiation: image::codecs::openexr::write_buffer::<_>::{closure#1}
Unexecuted instantiation: image::codecs::openexr::write_buffer::<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>::{closure#1}
image::codecs::openexr::write_buffer::<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>::{closure#1}
Line
Count
Source
256
32.6M
                SpecificChannels::rgba(|pixel: Vec2<usize>| {
257
32.6M
                    let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
258
32.6M
                    let start_byte = pixel_index * bytes_per_pixel;
259
260
32.6M
                    let [r, g, b, a]: [f32; 4] = bytemuck::pod_read_unaligned(
261
32.6M
                        &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
262
32.6M
                    );
263
264
32.6M
                    (r, g, b, a)
265
32.6M
                }),
266
            )
267
80
            .write()
268
            // .on_progress(|progress| todo!())
269
80
            .to_buffered(&mut buffered_write)
270
80
            .map_err(to_image_err)?;
271
        }
272
273
        // TODO other color types and channel types
274
0
        unsupported_color_type => {
275
0
            return Err(ImageError::Unsupported(
276
0
                UnsupportedError::from_format_and_kind(
277
0
                    ImageFormat::OpenExr.into(),
278
0
                    UnsupportedErrorKind::Color(unsupported_color_type),
279
0
                ),
280
0
            ))
281
        }
282
    }
283
284
80
    Ok(())
285
80
}
Unexecuted instantiation: image::codecs::openexr::write_buffer::<_>
Unexecuted instantiation: image::codecs::openexr::write_buffer::<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>
image::codecs::openexr::write_buffer::<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>
Line
Count
Source
219
80
fn write_buffer(
220
80
    mut buffered_write: impl Write + Seek,
221
80
    unaligned_bytes: &[u8],
222
80
    width: u32,
223
80
    height: u32,
224
80
    color_type: ExtendedColorType,
225
80
) -> ImageResult<()> {
226
80
    let width = width as usize;
227
80
    let height = height as usize;
228
80
    let bytes_per_pixel = color_type.bits_per_pixel() as usize / 8;
229
230
80
    match color_type {
231
        ExtendedColorType::Rgb32F => {
232
0
            Image // TODO compression method zip??
233
0
                ::from_channels(
234
0
                (width, height),
235
0
                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
0
            .write()
247
            // .on_progress(|progress| todo!())
248
0
            .to_buffered(&mut buffered_write)
249
0
            .map_err(to_image_err)?;
250
        }
251
252
        ExtendedColorType::Rgba32F => {
253
80
            Image // TODO compression method zip??
254
80
                ::from_channels(
255
80
                (width, height),
256
80
                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
80
            .write()
268
            // .on_progress(|progress| todo!())
269
80
            .to_buffered(&mut buffered_write)
270
80
            .map_err(to_image_err)?;
271
        }
272
273
        // TODO other color types and channel types
274
0
        unsupported_color_type => {
275
0
            return Err(ImageError::Unsupported(
276
0
                UnsupportedError::from_format_and_kind(
277
0
                    ImageFormat::OpenExr.into(),
278
0
                    UnsupportedErrorKind::Color(unsupported_color_type),
279
0
                ),
280
0
            ))
281
        }
282
    }
283
284
80
    Ok(())
285
80
}
286
287
// TODO is this struct and trait actually used anywhere?
288
/// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`.
289
#[derive(Debug)]
290
pub struct OpenExrEncoder<W>(W);
291
292
impl<W> OpenExrEncoder<W> {
293
    /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`.
294
    // use constructor, not public field, for future backwards-compatibility
295
80
    pub fn new(write: W) -> Self {
296
80
        Self(write)
297
80
    }
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<_>>::new
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new
<image::codecs::openexr::OpenExrEncoder<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>>::new
Line
Count
Source
295
80
    pub fn new(write: W) -> Self {
296
80
        Self(write)
297
80
    }
298
}
299
300
impl<W> ImageEncoder for OpenExrEncoder<W>
301
where
302
    W: Write + Seek,
303
{
304
    /// Writes the complete image.
305
    ///
306
    /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter`
307
    /// for best performance.
308
    #[track_caller]
309
80
    fn write_image(
310
80
        self,
311
80
        buf: &[u8],
312
80
        width: u32,
313
80
        height: u32,
314
80
        color_type: ExtendedColorType,
315
80
    ) -> ImageResult<()> {
316
80
        let expected_buffer_len = color_type.buffer_size(width, height);
317
80
        assert_eq!(
318
            expected_buffer_len,
319
80
            buf.len() as u64,
320
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
321
0
            buf.len(),
322
        );
323
324
80
        write_buffer(self.0, buf, width, height, color_type)
325
80
    }
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<_> as image::io::encoder::ImageEncoder>::write_image
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image
<image::codecs::openexr::OpenExrEncoder<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image
Line
Count
Source
309
80
    fn write_image(
310
80
        self,
311
80
        buf: &[u8],
312
80
        width: u32,
313
80
        height: u32,
314
80
        color_type: ExtendedColorType,
315
80
    ) -> ImageResult<()> {
316
80
        let expected_buffer_len = color_type.buffer_size(width, height);
317
80
        assert_eq!(
318
            expected_buffer_len,
319
80
            buf.len() as u64,
320
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
321
0
            buf.len(),
322
        );
323
324
80
        write_buffer(self.0, buf, width, height, color_type)
325
80
    }
326
}
327
328
5.94k
fn to_image_err(exr_error: Error) -> ImageError {
329
5.94k
    ImageError::Decoding(DecodingError::new(
330
5.94k
        ImageFormatHint::Exact(ImageFormat::OpenExr),
331
5.94k
        exr_error.to_string(),
332
5.94k
    ))
333
5.94k
}
334
335
#[cfg(test)]
336
mod test {
337
    use super::*;
338
339
    use std::fs::File;
340
    use std::io::{BufReader, Cursor};
341
    use std::path::{Path, PathBuf};
342
343
    use crate::error::{LimitError, LimitErrorKind};
344
    use crate::images::buffer::{Rgb32FImage, Rgba32FImage};
345
    use crate::io::free_functions::decoder_to_vec;
346
    use crate::{DynamicImage, ImageBuffer, Rgb, Rgba};
347
348
    const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"];
349
350
    /// Write an `Rgb32FImage`.
351
    /// Assumes the writer is buffered. In most cases,
352
    /// you should wrap your writer in a `BufWriter` for best performance.
353
    fn write_rgb_image(write: impl Write + Seek, image: &Rgb32FImage) -> ImageResult<()> {
354
        write_buffer(
355
            write,
356
            bytemuck::cast_slice(image.as_raw().as_slice()),
357
            image.width(),
358
            image.height(),
359
            ExtendedColorType::Rgb32F,
360
        )
361
    }
362
363
    /// Write an `Rgba32FImage`.
364
    /// Assumes the writer is buffered. In most cases,
365
    /// you should wrap your writer in a `BufWriter` for best performance.
366
    fn write_rgba_image(write: impl Write + Seek, image: &Rgba32FImage) -> ImageResult<()> {
367
        write_buffer(
368
            write,
369
            bytemuck::cast_slice(image.as_raw().as_slice()),
370
            image.width(),
371
            image.height(),
372
            ExtendedColorType::Rgba32F,
373
        )
374
    }
375
376
    /// Read the file from the specified path into an `Rgba32FImage`.
377
    fn read_as_rgba_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgba32FImage> {
378
        read_as_rgba_image(BufReader::new(File::open(path)?))
379
    }
380
381
    /// Read the file from the specified path into an `Rgb32FImage`.
382
    fn read_as_rgb_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgb32FImage> {
383
        read_as_rgb_image(BufReader::new(File::open(path)?))
384
    }
385
386
    /// Read the file from the specified path into an `Rgb32FImage`.
387
    fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult<Rgb32FImage> {
388
        let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?;
389
        let (width, height) = decoder.dimensions();
390
        let buffer: Vec<f32> = decoder_to_vec(decoder)?;
391
392
        ImageBuffer::from_raw(width, height, buffer)
393
            // this should be the only reason for the "from raw" call to fail,
394
            // even though such a large allocation would probably cause an error much earlier
395
            .ok_or_else(|| {
396
                ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
397
            })
398
    }
399
400
    /// Read the file from the specified path into an `Rgba32FImage`.
401
    fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult<Rgba32FImage> {
402
        let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?;
403
        let (width, height) = decoder.dimensions();
404
        let buffer: Vec<f32> = decoder_to_vec(decoder)?;
405
406
        ImageBuffer::from_raw(width, height, buffer)
407
            // this should be the only reason for the "from raw" call to fail,
408
            // even though such a large allocation would probably cause an error much earlier
409
            .ok_or_else(|| {
410
                ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
411
            })
412
    }
413
414
    #[test]
415
    fn compare_exr_hdr() {
416
        if cfg!(not(feature = "hdr")) {
417
            eprintln!("warning: to run all the openexr tests, activate the hdr feature flag");
418
        }
419
420
        #[cfg(feature = "hdr")]
421
        {
422
            use crate::codecs::hdr::HdrDecoder;
423
424
            let folder = BASE_PATH.iter().collect::<PathBuf>();
425
            let reference_path = folder.join("overexposed gradient.hdr");
426
            let exr_path =
427
                folder.join("overexposed gradient - data window equals display window.exr");
428
429
            let hdr_decoder =
430
                HdrDecoder::new(BufReader::new(File::open(reference_path).unwrap())).unwrap();
431
            let hdr: Rgb32FImage = match DynamicImage::from_decoder(hdr_decoder).unwrap() {
432
                DynamicImage::ImageRgb32F(image) => image,
433
                _ => panic!("expected rgb32f image"),
434
            };
435
436
            let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap();
437
            assert_eq!(exr_pixels.dimensions(), hdr.dimensions());
438
439
            for (expected, found) in hdr.pixels().zip(exr_pixels.pixels()) {
440
                for (expected, found) in expected.0.iter().zip(found.0.iter()) {
441
                    // the large tolerance seems to be caused by
442
                    // the RGBE u8x4 pixel quantization of the hdr image format
443
                    assert!(
444
                        (expected - found).abs() < 0.1,
445
                        "expected {expected}, found {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.join("cropping - uncropped original.exr");
518
        let cropped = exr_path.join("cropping - data window differs display window.exr");
519
520
        // smoke-check that the exr files are actually not the same
521
        {
522
            let original_exr = read_first_flat_layer_from_file(&original).unwrap();
523
            let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap();
524
            assert_eq!(
525
                original_exr.attributes.display_window,
526
                cropped_exr.attributes.display_window
527
            );
528
            assert_ne!(
529
                original_exr.layer_data.attributes.layer_position,
530
                cropped_exr.layer_data.attributes.layer_position
531
            );
532
            assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size);
533
        }
534
535
        // check that they result in the same image
536
        let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap();
537
        let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap();
538
        assert_eq!(original.dimensions(), cropped.dimensions());
539
540
        // the following is not a simple assert_eq, as in case of an error,
541
        // the whole image would be printed to the console, which takes forever
542
        assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b));
543
    }
544
}