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