diff --git a/crates/exr/src/decode.rs b/crates/exr/src/decode.rs index 2b47f42eb..ab21064ce 100644 --- a/crates/exr/src/decode.rs +++ b/crates/exr/src/decode.rs @@ -29,7 +29,7 @@ use exr::prelude::{ }; pub use half::f16; -use crate::channels::color_channels; +use crate::channels::{color_channels, part_channel_name}; /// Why a file could not be read or written. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] @@ -402,6 +402,228 @@ impl Part { } } +/// A channel placed in its part's display window. Samples are `f32`; samples outside the data +/// window are filled with the caller's per-channel value. +#[derive(Debug, Clone, PartialEq)] +pub struct DisplayChannel { + pub name: String, + pub samples: Vec, +} + +/// A decoded part whose channels are placed in the display window. +#[derive(Debug, Clone, PartialEq)] +pub struct DisplayPart { + pub info: PartInfo, + pub channels: Vec, +} + +/// Decodes the kept flat channels of one part and places them in its display window. +/// +/// Channels are decoded in their stored sample type, converted to `f32`, and clipped or padded +/// according to their data and display windows. `keep` and `fill` receive the part-qualified name +/// from [`part_channel_name`], which is also returned in [`DisplayChannel::name`]. Both decoded +/// block data and display-window buffers are bounded by `limits` before allocation. +pub fn decode_display_part( + bytes: &[u8], + index: usize, + keep: &dyn Fn(&str) -> bool, + fill: &dyn Fn(&str) -> f32, + limits: &Limits, +) -> Result { + // Only the caller-selected display planes are allocated here. Keep the shared header and + // expansion-ratio checks, but do not charge ignored channel data against the output budget. + let mut header_limits = *limits; + header_limits.max_width = u32::MAX; + header_limits.max_height = u32::MAX; + header_limits.max_pixels = u64::MAX; + header_limits.max_bytes = u64::MAX; + let meta = read_meta(bytes, &header_limits)?; + let info = meta.headers.get(index).map(|header| part_info(index, header)).ok_or_else(|| malformed(format!("part {index}")))?; + if info.deep { + return Err(Error::Unsupported("deep data (use the deep reader)".into())); + } + let display = info.display_window; + let pixels = u64::from(display.width) + .checked_mul(u64::from(display.height)) + .ok_or_else(|| Error::LimitExceeded("display window dimensions overflow".into()))?; + if pixels > limits.max_pixels { + return Err(Error::LimitExceeded(format!("display window of {pixels} pixels exceeds {}", limits.max_pixels))); + } + let kept = info.channels.iter().filter(|channel| keep(&part_channel_name(info.name.as_deref(), &channel.name))).count(); + let bytes_needed = pixels + .checked_mul(4) + .and_then(|bytes_per_channel| bytes_per_channel.checked_mul(kept as u64)) + .ok_or_else(|| Error::LimitExceeded("display window byte size overflow".into()))?; + if bytes_needed > limits.max_bytes || usize::try_from(bytes_needed).is_err() { + return Err(Error::LimitExceeded(format!("{bytes_needed} display-window bytes exceed {}", limits.max_bytes))); + } + let block_limit = std::cell::Cell::new(None); + let select = DisplaySelect { index, keep, fill, limits, seen: std::cell::Cell::new(0), block_limit: &block_limit }; + let image = ReadImage::new(ReadAllLayers { read_channels: select }, |_| {}) + .from_buffered(Cursor::new(bytes)) + .map_err(|error| block_limit.take().map(Error::LimitExceeded).unwrap_or_else(|| map_exr(error)))?; + let channels = image + .layer_data + .into_iter() + .nth(index) + .ok_or_else(|| malformed("the part was not read"))? + .channel_data + .into_iter() + .flatten() + .map(|(name, samples)| DisplayChannel { name, samples }) + .collect(); + Ok(DisplayPart { info, channels }) +} + +/// Filtered channel reader that writes each decoded block directly into a bounded display plane. +struct DisplaySelect<'a> { + index: usize, + keep: &'a dyn Fn(&str) -> bool, + fill: &'a dyn Fn(&str) -> f32, + limits: &'a Limits, + seen: std::cell::Cell, + block_limit: &'a std::cell::Cell>, +} + +struct DisplaySelectReader { + planes: Vec)>>, + offset: (i64, i64), + layer: (usize, usize), + display: (usize, usize), + f16s: Vec, + f32s: Vec, + u32s: Vec, +} + +impl<'s> ReadChannels<'s> for DisplaySelect<'_> { + type Reader = DisplaySelectReader; + + fn create_channels_reader(&'s self, header: &Header) -> exr::error::Result { + let part = self.seen.get(); + self.seen.set(part.saturating_add(1)); + let (layer_w, layer_h) = (header.layer_size.0, header.layer_size.1); + let display_window = header.shared_attributes.display_window; + let (display_w, display_h) = (display_window.size.0, display_window.size.1); + if part != self.index { + return Ok(DisplaySelectReader { + planes: header.channels.list.iter().map(|_| None).collect(), + offset: (0, 0), + layer: (layer_w, layer_h), + display: (display_w, display_h), + f16s: vec![], + f32s: vec![], + u32s: vec![], + }); + } + // The EXR crate decompresses each accepted block before `read_block` can discard ignored + // channels. Bound that all-channel input separately from the selected output planes. + let Some(block_bytes) = layer_w + .checked_mul(layer_h) + .and_then(|pixels| pixels.checked_mul(header.channels.list.len().max(1))) + .and_then(|samples| samples.checked_mul(std::mem::size_of::())) + else { + self.block_limit.set(Some(format!("data-window block size exceeds {}", self.limits.max_bytes))); + return Err(exr::error::Error::Invalid("data-window block size overflow".into())); + }; + if u64::try_from(block_bytes).map_or(true, |bytes| bytes > self.limits.max_bytes) || block_bytes > isize::MAX as usize { + self.block_limit.set(Some(format!("{block_bytes} data-window bytes exceed {}", self.limits.max_bytes))); + return Err(exr::error::Error::Invalid("data-window blocks exceed limit".into())); + } + let pixels = display_w.checked_mul(display_h).filter(|n| *n > 0).ok_or(exr::error::Error::Invalid("display window".into()))?; + let prefix = header.own_attributes.layer_name.as_ref().map(|name| name.to_string()).filter(|name| !name.is_empty()); + let kept = + header.channels.list.iter().filter(|channel| (self.keep)(&part_channel_name(prefix.as_deref(), &channel.name.to_string()))).count(); + let bytes = pixels + .checked_mul(4) + .and_then(|n| n.checked_mul(kept)) + .ok_or_else(|| exr::error::Error::Invalid("display planes exceed limit".into()))?; + if pixels as u64 > self.limits.max_pixels || bytes as u64 > self.limits.max_bytes || bytes > isize::MAX as usize { + return Err(exr::error::Error::Invalid("display planes exceed limit".into())); + } + let mut planes = Vec::with_capacity(header.channels.list.len()); + for channel in header.channels.list.iter() { + let name = part_channel_name(prefix.as_deref(), &channel.name.to_string()); + let plane = if (self.keep)(&name) { + if channel.sampling.0 != 1 || channel.sampling.1 != 1 { + return Err(exr::error::Error::NotSupported("subsampled channels".into())); + } + Some((name.clone(), vec![(self.fill)(&name); pixels])) + } else { + None + }; + planes.push(plane); + } + let position = header.own_attributes.layer_position; + Ok(DisplaySelectReader { + planes, + offset: (i64::from(position.0) - i64::from(display_window.position.0), i64::from(position.1) - i64::from(display_window.position.1)), + layer: (layer_w, layer_h), + display: (display_w, display_h), + f16s: vec![], + f32s: vec![], + u32s: vec![], + }) + } +} + +impl ChannelsReader for DisplaySelectReader { + type Channels = Vec)>>; + + fn filter_block(&self, tile: TileCoordinates) -> bool { + tile.is_largest_resolution_level() && self.planes.iter().any(Option::is_some) + } + + fn read_block(&mut self, header: &Header, block: UncompressedBlock) -> exr::error::UnitResult { + let ((layer_w, layer_h), (display_w, display_h)) = (self.layer, self.display); + for line in block.lines(&header.channels) { + let Some(Some((_, dst_plane))) = self.planes.get_mut(line.location.channel) else { continue }; + let (x, y, count) = (line.location.position.0, line.location.position.1, line.location.sample_count); + if y >= layer_h || x.checked_add(count).is_none_or(|end| end > layer_w) { + continue; + } + let Some(ty) = usize::try_from(y as i64 + self.offset.1).ok().filter(|ty| *ty < display_h) else { continue }; + let tx0 = x as i64 + self.offset.0; + let skip = usize::try_from(-tx0).unwrap_or(0).min(count); + let tx = usize::try_from(tx0.max(0)).unwrap_or(usize::MAX); + let take = (count - skip).min(display_w.saturating_sub(tx)); + if take == 0 { + continue; + } + let start = ty.checked_mul(display_w).and_then(|row| row.checked_add(tx)); + let Some(dst) = start.and_then(|s| dst_plane.get_mut(s..s.checked_add(take)?)) else { continue }; + match header.channels.list.get(line.location.channel).map(|c| c.sample_type) { + Some(ExrSample::F16) => { + self.f16s.resize(count, f16::ZERO); + line.read_samples_into_slice(&mut self.f16s)?; + for (d, s) in dst.iter_mut().zip(self.f16s.iter().skip(skip)) { + *d = s.to_f32(); + } + } + Some(ExrSample::F32) => { + self.f32s.resize(count, 0.0); + line.read_samples_into_slice(&mut self.f32s)?; + for (d, s) in dst.iter_mut().zip(self.f32s.iter().skip(skip)) { + *d = *s; + } + } + Some(ExrSample::U32) => { + self.u32s.resize(count, 0); + line.read_samples_into_slice(&mut self.u32s)?; + for (d, s) in dst.iter_mut().zip(self.u32s.iter().skip(skip)) { + *d = *s as f32; + } + } + None => continue, + } + } + Ok(()) + } + + fn into_channels(self) -> Self::Channels { + self.planes + } +} + /// Places a data-window plane in a display window: samples outside the display window are cut, /// the display window's pixels the data window does not cover are `fill`. `None` when the plane /// is not `data.width × data.height` samples or the display window has more than `max_pixels` diff --git a/crates/exr/src/decode_tests.rs b/crates/exr/src/decode_tests.rs index 69d8d07ac..54c28d9aa 100644 --- a/crates/exr/src/decode_tests.rs +++ b/crates/exr/src/decode_tests.rs @@ -118,6 +118,46 @@ fn display_window_placement() { assert_eq!(to_display_window(&[1.0, 2.0, 3.0, 4.0], far, display, 0.0, 100).unwrap(), [0.0; 6]); } +#[test] +fn decoded_display_part_places_selected_channels_and_checks_total_budget() { + let bytes = file(vec![layer(None, 2, 2, &["R", "G"], false, exr::prelude::Compression::ZIP1)], (3, 2)); + let part = decode_display_part(&bytes, 0, &|name| name == "R", &|_| 9.0, &Limits::default()).unwrap(); + assert_eq!(part.channels.len(), 1); + assert_eq!(part.channels[0].name, "R"); + assert_eq!(part.channels[0].samples, [0.0, 1.0, 9.0, 2.0, 3.0, 9.0]); + + let limits = Limits { max_bytes: 6 * 4 - 1, ..Limits::default() }; + assert!(matches!(decode_display_part(&bytes, 0, &all, &|_| 0.0, &limits), Err(Error::LimitExceeded(_)))); + + // One selected display plane fits exactly, but the EXR decoder must decompress both data + // channels in its input block before discarding the ignored one. + let block_limits = Limits { max_bytes: 6 * 4, ..Limits::default() }; + assert!(matches!(decode_display_part(&bytes, 0, &|name| name == "R", &|_| 0.0, &block_limits), Err(Error::LimitExceeded(_)))); +} + +#[test] +fn decoded_display_part_clips_a_wide_data_window_to_the_small_display() { + let width = (Limits::default().max_width as usize) + 1; + let bytes = file(vec![layer(None, width, 1, &["R"], false, exr::prelude::Compression::ZIP1)], (1, 1)); + let part = decode_display_part(&bytes, 0, &all, &|_| 9.0, &Limits::default()).unwrap(); + assert_eq!(part.channels.len(), 1); + assert_eq!(part.channels[0].samples, [0.0]); +} + +#[test] +fn decoded_display_part_rejects_kept_subsampled_channels() { + let mut bytes = file(vec![layer(None, 4, 1, &["R"], false, exr::prelude::Compression::Uncompressed)], (4, 1)); + // exr's writer doesn't emit subsampling yet, so patch the standard chlist sampling field in a + // valid uncompressed file. The reader must reject it before looking at the block payload. + let tag = b"channels\0chlist\0"; + let attr = bytes.windows(tag.len()).position(|window| window == tag).unwrap(); + let payload = attr + tag.len() + 4; + let name_end = payload + bytes[payload..].iter().position(|byte| *byte == 0).unwrap(); + let x_sampling = name_end + 1 + 4 + 1 + 3; + bytes[x_sampling..x_sampling + 4].copy_from_slice(&2u32.to_le_bytes()); + assert!(matches!(decode_display_part(&bytes, 0, &|name| name == "R", &|_| 0.0, &Limits::default()), Err(Error::Unsupported(_)))); +} + /// A 2x2 single-part file whose layer carries a 2x1 preview. fn with_preview() -> Vec { let mut attrs = LayerAttributes::named("rgb");