Skip to main content

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