| 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::{ |
| 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 | pub fn new(source: R) -> ImageResult<Self> { |
| 55 | Self::with_alpha_preference(source, None) |
| 56 | } |
| 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 | 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 | let exr_reader = exr::block::read(source, false).map_err(to_image_err)?; |
| 67 | |
| 68 | let header_index = exr_reader |
| 69 | .headers() |
| 70 | .iter() |
| 71 | .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 | .ok_or_else(|| { |
| 82 | ImageError::Decoding(DecodingError::new( |
| 83 | ImageFormatHint::Exact(ImageFormat::OpenExr), |
| 84 | "image does not contain non-deep rgb channels" , |
| 85 | )) |
| 86 | })?; |
| 87 | |
| 88 | let has_alpha = exr_reader.headers()[header_index] |
| 89 | .channels |
| 90 | .find_index_of_channel(&Text::from("A" )) |
| 91 | .is_some(); |
| 92 | |
| 93 | Ok(Self { |
| 94 | alpha_preference, |
| 95 | exr_reader, |
| 96 | header_index, |
| 97 | alpha_present_in_file: has_alpha, |
| 98 | }) |
| 99 | } |
| 100 | |
| 101 | // does not leak exrs-specific meta data into public api, just does it for this module |
| 102 | fn selected_exr_header(&self) -> &exr::meta::header::Header { |
| 103 | &self.exr_reader.meta_data().headers[self.header_index] |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | impl<R: BufRead + Seek> ImageDecoder for OpenExrDecoder<R> { |
| 108 | fn dimensions(&self) -> (u32, u32) { |
| 109 | let size = self |
| 110 | .selected_exr_header() |
| 111 | .shared_attributes |
| 112 | .display_window |
| 113 | .size; |
| 114 | (size.width() as u32, size.height() as u32) |
| 115 | } |
| 116 | |
| 117 | fn color_type(&self) -> ColorType { |
| 118 | let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file); |
| 119 | if returns_alpha { |
| 120 | ColorType::Rgba32F |
| 121 | } else { |
| 122 | ColorType::Rgb32F |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | fn original_color_type(&self) -> ExtendedColorType { |
| 127 | if self.alpha_present_in_file { |
| 128 | ExtendedColorType::Rgba32F |
| 129 | } else { |
| 130 | ExtendedColorType::Rgb32F |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file` |
| 135 | fn read_image(self, unaligned_bytes: &mut [u8]) -> ImageResult<()> { |
| 136 | let _blocks_in_header = self.selected_exr_header().chunk_count as u64; |
| 137 | let channel_count = self.color_type().channel_count() as usize; |
| 138 | |
| 139 | let display_window = self.selected_exr_header().shared_attributes.display_window; |
| 140 | let data_window_offset = |
| 141 | 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 | let (width, height) = self.dimensions(); |
| 146 | let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize; |
| 147 | let expected_byte_count = (width as usize) |
| 148 | .checked_mul(height as usize) |
| 149 | .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 | let has_invalid_size_or_overflowed = expected_byte_count |
| 153 | .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 | .unwrap_or(true); |
| 157 | |
| 158 | assert!( |
| 159 | !has_invalid_size_or_overflowed, |
| 160 | "byte buffer not large enough for the specified dimensions and f32 pixels" |
| 161 | ); |
| 162 | } |
| 163 | |
| 164 | let result = read() |
| 165 | .no_deep_data() |
| 166 | .largest_resolution_level() |
| 167 | .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 | .first_valid_layer() // TODO select exact layer by self.header_index? |
| 194 | .all_attributes() |
| 195 | .from_chunks(self.exr_reader) |
| 196 | .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 | unaligned_bytes.copy_from_slice(bytemuck::cast_slice( |
| 203 | result.layer_data.channel_data.pixels.as_slice(), |
| 204 | )); |
| 205 | Ok(()) |
| 206 | } |
| 207 | |
| 208 | fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> { |
| 209 | (*self).read_image(buf) |
| 210 | } |
| 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 | fn write_buffer( |
| 220 | mut buffered_write: impl Write + Seek, |
| 221 | unaligned_bytes: &[u8], |
| 222 | width: u32, |
| 223 | height: u32, |
| 224 | color_type: ExtendedColorType, |
| 225 | ) -> ImageResult<()> { |
| 226 | let width = width as usize; |
| 227 | let height = height as usize; |
| 228 | let bytes_per_pixel = color_type.bits_per_pixel() as usize / 8; |
| 229 | |
| 230 | match color_type { |
| 231 | ExtendedColorType::Rgb32F => { |
| 232 | Image // TODO compression method zip?? |
| 233 | ::from_channels( |
| 234 | (width, height), |
| 235 | 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 | .write() |
| 247 | // .on_progress(|progress| todo!()) |
| 248 | .to_buffered(&mut buffered_write) |
| 249 | .map_err(to_image_err)?; |
| 250 | } |
| 251 | |
| 252 | ExtendedColorType::Rgba32F => { |
| 253 | Image // TODO compression method zip?? |
| 254 | ::from_channels( |
| 255 | (width, height), |
| 256 | 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 | .write() |
| 268 | // .on_progress(|progress| todo!()) |
| 269 | .to_buffered(&mut buffered_write) |
| 270 | .map_err(to_image_err)?; |
| 271 | } |
| 272 | |
| 273 | // TODO other color types and channel types |
| 274 | unsupported_color_type => { |
| 275 | return Err(ImageError::Encoding(EncodingError::new( |
| 276 | ImageFormatHint::Exact(ImageFormat::OpenExr), |
| 277 | format!("writing color type {unsupported_color_type:?} not yet supported" ), |
| 278 | ))) |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | Ok(()) |
| 283 | } |
| 284 | |
| 285 | // TODO is this struct and trait actually used anywhere? |
| 286 | /// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`. |
| 287 | #[derive (Debug)] |
| 288 | pub struct OpenExrEncoder<W>(W); |
| 289 | |
| 290 | impl<W> OpenExrEncoder<W> { |
| 291 | /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`. |
| 292 | // use constructor, not public field, for future backwards-compatibility |
| 293 | pub fn new(write: W) -> Self { |
| 294 | Self(write) |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | impl<W> ImageEncoder for OpenExrEncoder<W> |
| 299 | where |
| 300 | W: Write + Seek, |
| 301 | { |
| 302 | /// Writes the complete image. |
| 303 | /// |
| 304 | /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter` |
| 305 | /// for best performance. |
| 306 | #[track_caller ] |
| 307 | fn write_image( |
| 308 | self, |
| 309 | buf: &[u8], |
| 310 | width: u32, |
| 311 | height: u32, |
| 312 | color_type: ExtendedColorType, |
| 313 | ) -> ImageResult<()> { |
| 314 | let expected_buffer_len: u64 = color_type.buffer_size(width, height); |
| 315 | assert_eq!( |
| 316 | expected_buffer_len, |
| 317 | buf.len() as u64, |
| 318 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x {height} image" , |
| 319 | buf.len(), |
| 320 | ); |
| 321 | |
| 322 | write_buffer(self.0, unaligned_bytes:buf, width, height, color_type) |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | fn to_image_err(exr_error: Error) -> ImageError { |
| 327 | ImageError::Decoding(DecodingError::new( |
| 328 | format:ImageFormatHint::Exact(ImageFormat::OpenExr), |
| 329 | err:exr_error.to_string(), |
| 330 | )) |
| 331 | } |
| 332 | |
| 333 | #[cfg (test)] |
| 334 | mod test { |
| 335 | use super::*; |
| 336 | |
| 337 | use std::fs::File; |
| 338 | use std::io::{BufReader, Cursor}; |
| 339 | use std::path::{Path, PathBuf}; |
| 340 | |
| 341 | use crate::buffer_::{Rgb32FImage, Rgba32FImage}; |
| 342 | use crate::error::{LimitError, LimitErrorKind}; |
| 343 | use crate::{DynamicImage, ImageBuffer, Rgb, Rgba}; |
| 344 | |
| 345 | const BASE_PATH: &[&str] = &["." , "tests" , "images" , "exr" ]; |
| 346 | |
| 347 | /// Write an `Rgb32FImage`. |
| 348 | /// Assumes the writer is buffered. In most cases, |
| 349 | /// you should wrap your writer in a `BufWriter` for best performance. |
| 350 | fn write_rgb_image(write: impl Write + Seek, image: &Rgb32FImage) -> ImageResult<()> { |
| 351 | write_buffer( |
| 352 | write, |
| 353 | bytemuck::cast_slice(image.as_raw().as_slice()), |
| 354 | image.width(), |
| 355 | image.height(), |
| 356 | ExtendedColorType::Rgb32F, |
| 357 | ) |
| 358 | } |
| 359 | |
| 360 | /// Write an `Rgba32FImage`. |
| 361 | /// Assumes the writer is buffered. In most cases, |
| 362 | /// you should wrap your writer in a `BufWriter` for best performance. |
| 363 | fn write_rgba_image(write: impl Write + Seek, image: &Rgba32FImage) -> ImageResult<()> { |
| 364 | write_buffer( |
| 365 | write, |
| 366 | bytemuck::cast_slice(image.as_raw().as_slice()), |
| 367 | image.width(), |
| 368 | image.height(), |
| 369 | ExtendedColorType::Rgba32F, |
| 370 | ) |
| 371 | } |
| 372 | |
| 373 | /// Read the file from the specified path into an `Rgba32FImage`. |
| 374 | fn read_as_rgba_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgba32FImage> { |
| 375 | read_as_rgba_image(BufReader::new(File::open(path)?)) |
| 376 | } |
| 377 | |
| 378 | /// Read the file from the specified path into an `Rgb32FImage`. |
| 379 | fn read_as_rgb_image_from_file(path: impl AsRef<Path>) -> ImageResult<Rgb32FImage> { |
| 380 | read_as_rgb_image(BufReader::new(File::open(path)?)) |
| 381 | } |
| 382 | |
| 383 | /// Read the file from the specified path into an `Rgb32FImage`. |
| 384 | fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult<Rgb32FImage> { |
| 385 | let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?; |
| 386 | let (width, height) = decoder.dimensions(); |
| 387 | let buffer: Vec<f32> = crate::image::decoder_to_vec(decoder)?; |
| 388 | |
| 389 | ImageBuffer::from_raw(width, height, buffer) |
| 390 | // this should be the only reason for the "from raw" call to fail, |
| 391 | // even though such a large allocation would probably cause an error much earlier |
| 392 | .ok_or_else(|| { |
| 393 | ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory)) |
| 394 | }) |
| 395 | } |
| 396 | |
| 397 | /// Read the file from the specified path into an `Rgba32FImage`. |
| 398 | fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult<Rgba32FImage> { |
| 399 | let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; |
| 400 | let (width, height) = decoder.dimensions(); |
| 401 | let buffer: Vec<f32> = crate::image::decoder_to_vec(decoder)?; |
| 402 | |
| 403 | ImageBuffer::from_raw(width, height, buffer) |
| 404 | // this should be the only reason for the "from raw" call to fail, |
| 405 | // even though such a large allocation would probably cause an error much earlier |
| 406 | .ok_or_else(|| { |
| 407 | ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory)) |
| 408 | }) |
| 409 | } |
| 410 | |
| 411 | #[test ] |
| 412 | fn compare_exr_hdr() { |
| 413 | if cfg!(not(feature = "hdr" )) { |
| 414 | eprintln!("warning: to run all the openexr tests, activate the hdr feature flag" ); |
| 415 | } |
| 416 | |
| 417 | #[cfg (feature = "hdr" )] |
| 418 | { |
| 419 | use crate::codecs::hdr::HdrDecoder; |
| 420 | |
| 421 | let folder = BASE_PATH.iter().collect::<PathBuf>(); |
| 422 | let reference_path = folder.clone().join("overexposed gradient.hdr" ); |
| 423 | let exr_path = folder |
| 424 | .clone() |
| 425 | .join("overexposed gradient - data window equals display window.exr" ); |
| 426 | |
| 427 | let hdr_decoder = |
| 428 | HdrDecoder::new(BufReader::new(File::open(reference_path).unwrap())).unwrap(); |
| 429 | let hdr: Rgb32FImage = match DynamicImage::from_decoder(hdr_decoder).unwrap() { |
| 430 | DynamicImage::ImageRgb32F(image) => image, |
| 431 | _ => panic!("expected rgb32f image" ), |
| 432 | }; |
| 433 | |
| 434 | let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap(); |
| 435 | assert_eq!(exr_pixels.dimensions(), hdr.dimensions()); |
| 436 | |
| 437 | for (expected, found) in hdr.pixels().zip(exr_pixels.pixels()) { |
| 438 | for (expected, found) in expected.0.iter().zip(found.0.iter()) { |
| 439 | // the large tolerance seems to be caused by |
| 440 | // the RGBE u8x4 pixel quantization of the hdr image format |
| 441 | assert!( |
| 442 | (expected - found).abs() < 0.1, |
| 443 | "expected {}, found {}" , |
| 444 | expected, |
| 445 | 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.clone().join("cropping - uncropped original.exr" ); |
| 518 | let cropped = exr_path |
| 519 | .clone() |
| 520 | .join("cropping - data window differs display window.exr" ); |
| 521 | |
| 522 | // smoke-check that the exr files are actually not the same |
| 523 | { |
| 524 | let original_exr = read_first_flat_layer_from_file(&original).unwrap(); |
| 525 | let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap(); |
| 526 | assert_eq!( |
| 527 | original_exr.attributes.display_window, |
| 528 | cropped_exr.attributes.display_window |
| 529 | ); |
| 530 | assert_ne!( |
| 531 | original_exr.layer_data.attributes.layer_position, |
| 532 | cropped_exr.layer_data.attributes.layer_position |
| 533 | ); |
| 534 | assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size); |
| 535 | } |
| 536 | |
| 537 | // check that they result in the same image |
| 538 | let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap(); |
| 539 | let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap(); |
| 540 | assert_eq!(original.dimensions(), cropped.dimensions()); |
| 541 | |
| 542 | // the following is not a simple assert_eq, as in case of an error, |
| 543 | // the whole image would be printed to the console, which takes forever |
| 544 | assert!(original.pixels().zip(cropped.pixels()).all(|(a, b)| a == b)); |
| 545 | } |
| 546 | } |
| 547 | |