mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Merge remote-tracking branch 'origin/main' into fix/dxf-spline-paths
This commit is contained in:
commit
361b565670
2 files changed
+263
-1
No files matched your search
+223
-1
@@ -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<f32>,
|
||||
}
|
||||
|
||||
/// A decoded part whose channels are placed in the display window.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DisplayPart {
|
||||
pub info: PartInfo,
|
||||
pub channels: Vec<DisplayChannel>,
|
||||
}
|
||||
|
||||
/// 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<DisplayPart> {
|
||||
// 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<usize>,
|
||||
block_limit: &'a std::cell::Cell<Option<String>>,
|
||||
}
|
||||
|
||||
struct DisplaySelectReader {
|
||||
planes: Vec<Option<(String, Vec<f32>)>>,
|
||||
offset: (i64, i64),
|
||||
layer: (usize, usize),
|
||||
display: (usize, usize),
|
||||
f16s: Vec<f16>,
|
||||
f32s: Vec<f32>,
|
||||
u32s: Vec<u32>,
|
||||
}
|
||||
|
||||
impl<'s> ReadChannels<'s> for DisplaySelect<'_> {
|
||||
type Reader = DisplaySelectReader;
|
||||
|
||||
fn create_channels_reader(&'s self, header: &Header) -> exr::error::Result<DisplaySelectReader> {
|
||||
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::<f32>()))
|
||||
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<Option<(String, Vec<f32>)>>;
|
||||
|
||||
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`
|
||||
|
||||
@@ -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<u8> {
|
||||
let mut attrs = LayerAttributes::named("rgb");
|
||||
|
||||
Reference in new issue
Block a user