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