Coverage Report

Created: 2026-06-18 07:57

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::{
26
    DecodingError, ImageFormatHint, ParameterError, ParameterErrorKind, UnsupportedError,
27
    UnsupportedErrorKind,
28
};
29
use crate::io::{DecodedImageAttributes, DecoderPreparedImage};
30
use crate::{
31
    ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult,
32
};
33
34
use std::io::{BufRead, Seek, Write};
35
36
/// An OpenEXR decoder. Immediately reads the meta data from the file.
37
#[derive(Debug)]
38
pub struct OpenExrDecoder<R> {
39
    exr_reader: Option<exr::block::reader::Reader<R>>,
40
41
    // select a header that is rgb and not deep
42
    header_index: usize,
43
44
    // decode either rgb or rgba.
45
    // can be specified to include or discard alpha channels.
46
    // if none, the alpha channel will only be allocated where the file contains data for it.
47
    alpha_preference: Option<bool>,
48
49
    alpha_present_in_file: bool,
50
}
51
52
impl<R: BufRead + Seek> OpenExrDecoder<R> {
53
    /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
54
    /// Assumes the reader is buffered. In most cases,
55
    /// you should wrap your reader in a `BufReader` for best performance.
56
    /// Loads an alpha channel if the file has alpha samples.
57
    /// Use `with_alpha_preference` if you want to load or not load alpha unconditionally.
58
0
    pub fn new(source: R) -> ImageResult<Self> {
59
0
        Self::with_alpha_preference(source, None)
60
0
    }
61
62
    /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
63
    /// Assumes the reader is buffered. In most cases,
64
    /// you should wrap your reader in a `BufReader` for best performance.
65
    /// If alpha preference is specified, an alpha channel will
66
    /// always be present or always be not present in the returned image.
67
    /// If alpha preference is none, the alpha channel will only be returned if it is found in the file.
68
0
    pub fn with_alpha_preference(source: R, alpha_preference: Option<bool>) -> ImageResult<Self> {
69
        // read meta data, then wait for further instructions, keeping the file open and ready
70
0
        let exr_reader = exr::block::read(source, false).map_err(to_image_err)?;
71
72
0
        let header_index = exr_reader
73
0
            .headers()
74
0
            .iter()
75
0
            .position(|header| {
76
                // check if r/g/b exists in the channels
77
0
                let has_rgb = ["R", "G", "B"]
78
0
                    .iter()
79
0
                    .all(|&required|  // alpha will be optional
80
0
                    header.channels.find_index_of_channel(&Text::from(required)).is_some());
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference::{closure#0}::{closure#0}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference::{closure#0}::{closure#0}
81
82
                // we currently dont support deep images, or images with other color spaces than rgb
83
0
                !header.deep && has_rgb
84
0
            })
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference::{closure#0}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference::{closure#0}
85
0
            .ok_or_else(|| {
86
0
                ImageError::Decoding(DecodingError::new(
87
0
                    ImageFormatHint::Exact(ImageFormat::OpenExr),
88
0
                    "image does not contain non-deep rgb channels",
89
0
                ))
90
0
            })?;
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference::{closure#1}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference::{closure#1}
91
92
0
        let has_alpha = exr_reader.headers()[header_index]
93
0
            .channels
94
0
            .find_index_of_channel(&Text::from("A"))
95
0
            .is_some();
96
97
0
        Ok(Self {
98
0
            alpha_preference,
99
0
            exr_reader: Some(exr_reader),
100
0
            header_index,
101
0
            alpha_present_in_file: has_alpha,
102
0
        })
103
0
    }
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>>>::with_alpha_preference
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::with_alpha_preference
104
}
105
106
impl<R: BufRead + Seek> ImageDecoder for OpenExrDecoder<R> {
107
0
    fn prepare_image(&mut self) -> ImageResult<DecoderPreparedImage> {
108
0
        let (width, height) = match &self.exr_reader {
109
0
            Some(exr) => {
110
0
                let header = &exr.meta_data().headers[self.header_index];
111
0
                let size = header.shared_attributes.display_window.size;
112
0
                (size.width() as u32, size.height() as u32)
113
            }
114
            // We have already ended..
115
            None => {
116
0
                return Err(ImageError::Parameter(ParameterError::from_kind(
117
0
                    ParameterErrorKind::NoMoreData,
118
0
                )))
119
            }
120
        };
121
122
0
        let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file);
123
0
        let color = if returns_alpha {
124
0
            ColorType::Rgba32F
125
        } else {
126
0
            ColorType::Rgb32F
127
        };
128
129
        // We may have discarded the alpha channel.
130
0
        Ok(DecoderPreparedImage::new(width, height, color))
131
0
    }
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::prepare_image
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::prepare_image
132
133
    // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file`
134
0
    fn read_image(&mut self, unaligned_bytes: &mut [u8]) -> ImageResult<DecodedImageAttributes> {
135
0
        let layout = self.prepare_image()?;
136
0
        let (width, height) = layout.layout.dimensions();
137
138
0
        let original = if self.alpha_present_in_file {
139
0
            ExtendedColorType::Rgba32F
140
        } else {
141
0
            ExtendedColorType::Rgb32F
142
        };
143
144
0
        let reader = self.exr_reader.take().ok_or_else(|| {
145
0
            ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData))
146
0
        })?;
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#0}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#0}
147
148
0
        let _blocks_in_header = reader.headers()[self.header_index].chunk_count as u64;
149
0
        let channel_count = layout.layout.color.channel_count() as usize;
150
151
0
        let display_window = reader.headers()[self.header_index]
152
0
            .shared_attributes
153
0
            .display_window;
154
155
0
        let data_window_offset = reader.headers()[self.header_index]
156
0
            .own_attributes
157
0
            .layer_position
158
0
            - display_window.position;
159
160
        {
161
            // check whether the buffer is large enough for the dimensions of the file
162
0
            let bytes_per_pixel = usize::from(layout.layout.color.bytes_per_pixel());
163
0
            let expected_byte_count = (width as usize)
164
0
                .checked_mul(height as usize)
165
0
                .and_then(|size| size.checked_mul(bytes_per_pixel));
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#1}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#1}
166
167
            // if the width and height does not match the length of the bytes, the arguments are invalid
168
0
            let has_invalid_size_or_overflowed = expected_byte_count
169
0
                .map(|expected_byte_count| unaligned_bytes.len() != expected_byte_count)
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#2}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#2}
170
                // otherwise, size calculation overflowed, is bigger than memory,
171
                // therefore data is too small, so it is invalid.
172
0
                .unwrap_or(true);
173
174
0
            assert!(
175
0
                !has_invalid_size_or_overflowed,
176
0
                "byte buffer not large enough for the specified dimensions and f32 pixels"
177
            );
178
        }
179
180
0
        let result = read()
181
0
            .no_deep_data()
182
0
            .largest_resolution_level()
183
0
            .rgba_channels(
184
0
                move |_size, _channels| vec![0_f32; display_window.size.area() * channel_count],
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#3}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#3}
185
0
                move |buffer, index_in_data_window, (r, g, b, a_or_1): (f32, f32, f32, f32)| {
186
0
                    let index_in_display_window =
187
0
                        index_in_data_window.to_i32() + data_window_offset;
188
189
                    // only keep pixels inside the data window
190
                    // TODO filter chunks based on this
191
0
                    if index_in_display_window.x() >= 0
192
0
                        && index_in_display_window.y() >= 0
193
0
                        && index_in_display_window.x() < display_window.size.width() as i32
194
0
                        && index_in_display_window.y() < display_window.size.height() as i32
195
0
                    {
196
0
                        let index_in_display_window =
197
0
                            index_in_display_window.to_usize("index bug").unwrap();
198
0
                        let first_f32_index =
199
0
                            index_in_display_window.flat_index_for_size(display_window.size);
200
0
201
0
                        buffer[first_f32_index * channel_count
202
0
                            ..(first_f32_index + 1) * channel_count]
203
0
                            .copy_from_slice(&[r, g, b, a_or_1][0..channel_count]);
204
0
205
0
                        // TODO white point chromaticities + srgb/linear conversion?
206
0
                    }
207
0
                },
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image::{closure#4}
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image::{closure#4}
208
            )
209
0
            .first_valid_layer() // TODO select exact layer by self.header_index?
210
0
            .all_attributes()
211
0
            .from_chunks(reader)
212
0
            .map_err(to_image_err)?;
213
214
        // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice
215
216
        // this cast is safe and works with any alignment, as bytes are copied, and not f32 values.
217
        // note: buffer slice length is checked in the beginning of this function and will be correct at this point
218
0
        unaligned_bytes.copy_from_slice(bytemuck::cast_slice(
219
0
            result.layer_data.channel_data.pixels.as_slice(),
220
0
        ));
221
222
0
        Ok(DecodedImageAttributes {
223
0
            original_color_type: Some(original),
224
0
            ..DecodedImageAttributes::default()
225
0
        })
226
0
    }
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<&[u8]>> as image::io::decoder::ImageDecoder>::read_image
Unexecuted instantiation: <image::codecs::openexr::OpenExrDecoder<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::decoder::ImageDecoder>::read_image
227
}
228
229
/// Write a raw byte buffer of pixels,
230
/// returning an Error if it has an invalid length.
231
///
232
/// Assumes the writer is buffered. In most cases,
233
/// you should wrap your writer in a `BufWriter` for best performance.
234
// private. access via `OpenExrEncoder`
235
0
fn write_buffer(
236
0
    mut buffered_write: impl Write + Seek,
237
0
    unaligned_bytes: &[u8],
238
0
    width: u32,
239
0
    height: u32,
240
0
    color_type: ExtendedColorType,
241
0
) -> ImageResult<()> {
242
0
    let width = width as usize;
243
0
    let height = height as usize;
244
0
    let bytes_per_pixel = color_type.bits_per_pixel() as usize / 8;
245
246
0
    match color_type {
247
        ExtendedColorType::Rgb32F => {
248
0
            Image // TODO compression method zip??
249
0
                ::from_channels(
250
0
                (width, height),
251
0
                SpecificChannels::rgb(|pixel: Vec2<usize>| {
252
0
                    let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
253
0
                    let start_byte = pixel_index * bytes_per_pixel;
254
255
0
                    let [r, g, b]: [f32; 3] = bytemuck::pod_read_unaligned(
256
0
                        &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
257
0
                    );
258
259
0
                    (r, g, b)
260
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}
261
            )
262
0
            .write()
263
            // .on_progress(|progress| todo!())
264
0
            .to_buffered(&mut buffered_write)
265
0
            .map_err(to_image_err)?;
266
        }
267
268
        ExtendedColorType::Rgba32F => {
269
0
            Image // TODO compression method zip??
270
0
                ::from_channels(
271
0
                (width, height),
272
0
                SpecificChannels::rgba(|pixel: Vec2<usize>| {
273
0
                    let pixel_index = pixel.flat_index_for_size(Vec2(width, height));
274
0
                    let start_byte = pixel_index * bytes_per_pixel;
275
276
0
                    let [r, g, b, a]: [f32; 4] = bytemuck::pod_read_unaligned(
277
0
                        &unaligned_bytes[start_byte..start_byte + bytes_per_pixel],
278
0
                    );
279
280
0
                    (r, g, b, a)
281
0
                }),
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}
Unexecuted instantiation: image::codecs::openexr::write_buffer::<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>::{closure#1}
282
            )
283
0
            .write()
284
            // .on_progress(|progress| todo!())
285
0
            .to_buffered(&mut buffered_write)
286
0
            .map_err(to_image_err)?;
287
        }
288
289
        // TODO other color types and channel types
290
0
        unsupported_color_type => {
291
0
            return Err(ImageError::Unsupported(
292
0
                UnsupportedError::from_format_and_kind(
293
0
                    ImageFormat::OpenExr.into(),
294
0
                    UnsupportedErrorKind::Color(unsupported_color_type),
295
0
                ),
296
0
            ))
297
        }
298
    }
299
300
0
    Ok(())
301
0
}
Unexecuted instantiation: image::codecs::openexr::write_buffer::<_>
Unexecuted instantiation: image::codecs::openexr::write_buffer::<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>
Unexecuted instantiation: image::codecs::openexr::write_buffer::<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>
302
303
// TODO is this struct and trait actually used anywhere?
304
/// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`.
305
#[derive(Debug)]
306
pub struct OpenExrEncoder<W>(W);
307
308
impl<W> OpenExrEncoder<W> {
309
    /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`.
310
    // use constructor, not public field, for future backwards-compatibility
311
0
    pub fn new(write: W) -> Self {
312
0
        Self(write)
313
0
    }
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<_>>::new
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>>>::new
314
}
315
316
impl<W> ImageEncoder for OpenExrEncoder<W>
317
where
318
    W: Write + Seek,
319
{
320
    /// Writes the complete image.
321
    ///
322
    /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter`
323
    /// for best performance.
324
    #[track_caller]
325
0
    fn write_image(
326
0
        self,
327
0
        buf: &[u8],
328
0
        width: u32,
329
0
        height: u32,
330
0
        color_type: ExtendedColorType,
331
0
    ) -> ImageResult<()> {
332
0
        let expected_buffer_len = color_type.buffer_size(width, height);
333
0
        assert_eq!(
334
            expected_buffer_len,
335
0
            buf.len() as u64,
336
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
337
0
            buf.len(),
338
        );
339
340
0
        write_buffer(self.0, buf, width, height, color_type)
341
0
    }
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
Unexecuted instantiation: <image::codecs::openexr::OpenExrEncoder<std::io::cursor::Cursor<&mut alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image
342
}
343
344
0
fn to_image_err(exr_error: Error) -> ImageError {
345
0
    ImageError::Decoding(DecodingError::new(
346
0
        ImageFormatHint::Exact(ImageFormat::OpenExr),
347
0
        exr_error.to_string(),
348
0
    ))
349
0
}
350
351
#[cfg(test)]
352
mod test {
353
    use super::*;
354
355
    use std::fs::File;
356
    use std::io::{BufReader, Cursor};
357
    use std::path::{Path, PathBuf};
358
359
    use crate::error::{LimitError, LimitErrorKind};
360
    use crate::images::buffer::{Rgb32FImage, Rgba32FImage};
361
    use crate::io::free_functions::decoder_to_vec;
362
    use crate::{DynamicImage, ImageBuffer, Rgb, Rgba};
363
364
    const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"];
365
366
    /// Write an `Rgb32FImage`.
367
    /// Assumes the writer is buffered. In most cases,
368
    /// you should wrap your writer in a `BufWriter` for best performance.
369
    fn write_rgb_image(write: impl Write + Seek, image: &Rgb32FImage) -> ImageResult<()> {
370
        write_buffer(
371
            write,
372
            bytemuck::cast_slice(image.subpixels()),
373
            image.width(),
374
            image.height(),
375
            ExtendedColorType::Rgb32F,
376
        )
377
    }
378
379
    /// Write an `Rgba32FImage`.
380
    /// Assumes the writer is buffered. In most cases,
381
    /// you should wrap your writer in a `BufWriter` for best performance.
382
    fn write_rgba_image(write: impl Write + Seek, image: &Rgba32FImage) -> ImageResult<()> {
383
        write_buffer(
384
            write,
385
            bytemuck::cast_slice(image.subpixels()),
386
            image.width(),
387
            image.height(),
388
            ExtendedColorType::Rgba32F,
389
        )
390
    }
391
392
    /// Read the file from the specified path into an `Rgba32FImage`.
393
    fn read_as_rgba_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgba32FImage> {
394
        read_as_rgba_image(BufReader::new(File::open(path)?))
395
    }
396
397
    /// Read the file from the specified path into an `Rgb32FImage`.
398
    fn read_as_rgb_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgb32FImage> {
399
        read_as_rgb_image(BufReader::new(File::open(path)?))
400
    }
401
402
    /// Read the file from the specified path into an `Rgb32FImage`.
403
    fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult<Rgb32FImage> {
404
        let mut decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?;
405
        let (width, height) = decoder.prepare_image()?.layout.dimensions();
406
        let (buffer, _): (Vec<f32>, _) = decoder_to_vec(&mut decoder)?;
407
408
        ImageBuffer::from_raw(width, height, buffer)
409
            // this should be the only reason for the "from raw" call to fail,
410
            // even though such a large allocation would probably cause an error much earlier
411
            .ok_or_else(|| {
412
                ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
413
            })
414
    }
415
416
    /// Read the file from the specified path into an `Rgba32FImage`.
417
    fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult<Rgba32FImage> {
418
        let mut decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?;
419
        let (width, height) = decoder.prepare_image()?.layout.dimensions();
420
        let (buffer, _): (Vec<f32>, _) = decoder_to_vec(&mut decoder)?;
421
422
        ImageBuffer::from_raw(width, height, buffer)
423
            // this should be the only reason for the "from raw" call to fail,
424
            // even though such a large allocation would probably cause an error much earlier
425
            .ok_or_else(|| {
426
                ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
427
            })
428
    }
429
430
    #[test]
431
    fn compare_exr_hdr() {
432
        if cfg!(not(feature = "hdr")) {
433
            eprintln!("warning: to run all the openexr tests, activate the hdr feature flag");
434
        }
435
436
        #[cfg(feature = "hdr")]
437
        {
438
            use crate::codecs::hdr::HdrDecoder;
439
440
            let folder = BASE_PATH.iter().collect::<PathBuf>();
441
            let reference_path = folder.join("overexposed gradient.hdr");
442
            let exr_path =
443
                folder.join("overexposed gradient - data window equals display window.exr");
444
445
            let hdr_decoder =
446
                HdrDecoder::new(BufReader::new(File::open(reference_path).unwrap())).unwrap();
447
            let hdr: Rgb32FImage = match DynamicImage::from_decoder(hdr_decoder).unwrap() {
448
                DynamicImage::ImageRgb32F(image) => image,
449
                _ => panic!("expected rgb32f image"),
450
            };
451
452
            let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap();
453
            assert_eq!(exr_pixels.dimensions(), hdr.dimensions());
454
455
            for (expected, found) in hdr.pixels().iter().zip(exr_pixels.pixels().iter()) {
456
                for (expected, found) in expected.0.iter().zip(found.0.iter()) {
457
                    // the large tolerance seems to be caused by
458
                    // the RGBE u8x4 pixel quantization of the hdr image format
459
                    assert!(
460
                        (expected - found).abs() < 0.1,
461
                        "expected {expected}, found {found}"
462
                    );
463
                }
464
            }
465
        }
466
    }
467
468
    #[test]
469
    fn roundtrip_rgba() {
470
        let mut next_random = vec![1.0, 0.0, -1.0, -3.15, 27.0, 11.0, 31.0]
471
            .into_iter()
472
            .cycle();
473
        let mut next_random = move || next_random.next().unwrap();
474
475
        let generated_image: Rgba32FImage = ImageBuffer::from_fn(9, 31, |_x, _y| {
476
            Rgba([next_random(), next_random(), next_random(), next_random()])
477
        });
478
479
        let mut bytes = vec![];
480
        write_rgba_image(Cursor::new(&mut bytes), &generated_image).unwrap();
481
        let decoded_image = read_as_rgba_image(Cursor::new(bytes)).unwrap();
482
483
        debug_assert_eq!(generated_image, decoded_image);
484
    }
485
486
    #[test]
487
    fn roundtrip_rgb() {
488
        let mut next_random = vec![1.0, 0.0, -1.0, -3.15, 27.0, 11.0, 31.0]
489
            .into_iter()
490
            .cycle();
491
        let mut next_random = move || next_random.next().unwrap();
492
493
        let generated_image: Rgb32FImage = ImageBuffer::from_fn(9, 31, |_x, _y| {
494
            Rgb([next_random(), next_random(), next_random()])
495
        });
496
497
        let mut bytes = vec![];
498
        write_rgb_image(Cursor::new(&mut bytes), &generated_image).unwrap();
499
        let decoded_image = read_as_rgb_image(Cursor::new(bytes)).unwrap();
500
501
        debug_assert_eq!(generated_image, decoded_image);
502
    }
503
504
    #[test]
505
    fn compare_rgba_rgb() {
506
        let exr_path = BASE_PATH
507
            .iter()
508
            .collect::<PathBuf>()
509
            .join("overexposed gradient - data window equals display window.exr");
510
511
        let rgb: Rgb32FImage = read_as_rgb_image_from_file(&exr_path).unwrap();
512
        let rgba: Rgba32FImage = read_as_rgba_image_from_file(&exr_path).unwrap();
513
514
        assert_eq!(rgba.dimensions(), rgb.dimensions());
515
516
        for (Rgb(rgb), Rgba(rgba)) in rgb.pixels().iter().zip(rgba.pixels().iter()) {
517
            assert_eq!(rgb, &rgba[..3]);
518
        }
519
    }
520
521
    #[test]
522
    fn compare_cropped() {
523
        // like in photoshop, exr images may have layers placed anywhere in a canvas.
524
        // we don't want to load the pixels from the layer, but we want to load the pixels from the canvas.
525
        // a layer might be smaller than the canvas, in that case the canvas should be transparent black
526
        // where no layer was covering it. a layer might also be larger than the canvas,
527
        // these pixels should be discarded.
528
        //
529
        // in this test we want to make sure that an
530
        // auto-cropped image will be reproduced to the original.
531
532
        let exr_path = BASE_PATH.iter().collect::<PathBuf>();
533
        let original = exr_path.join("cropping - uncropped original.exr");
534
        let cropped = exr_path.join("cropping - data window differs display window.exr");
535
536
        // smoke-check that the exr files are actually not the same
537
        {
538
            let original_exr = read_first_flat_layer_from_file(&original).unwrap();
539
            let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap();
540
            assert_eq!(
541
                original_exr.attributes.display_window,
542
                cropped_exr.attributes.display_window
543
            );
544
            assert_ne!(
545
                original_exr.layer_data.attributes.layer_position,
546
                cropped_exr.layer_data.attributes.layer_position
547
            );
548
            assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size);
549
        }
550
551
        // check that they result in the same image
552
        let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap();
553
        let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap();
554
        assert_eq!(original.dimensions(), cropped.dimensions());
555
556
        // the following is not a simple assert_eq, as in case of an error,
557
        // the whole image would be printed to the console, which takes forever
558
        assert!(original.pixels() == cropped.pixels());
559
    }
560
}