Deep EXR: open cropped renders on their full display-window stage (#4907)

A renderer's data window is its crop of the frame; the composition lives
on the whole display window (our MY27 renders: data 1782x1267 at
(1109,1100) inside a 4000x3000 stage). decode_deep now validates the
display window against the limits and reports it as stage + origin on
DeepImage; flatten pads the composed region to the stage at its offset
(transparent elsewhere) and tags the image with a DisplayWindowStage
warning. Files without a usable display window open as before.
Includes a cropped-deep generator and a stage/offset oracle test.
This commit is contained in:
Elvis33LE authored and GitHub committed 2026-10-13 13:24:46 -04:00
1 parent 2571423bb3
commit 1bcb662828
3 files changed
+187 -2

No files matched your search

+44 -2
View File
@@ -227,6 +227,27 @@ pub(crate) fn decode_deep(meta: &MetaData, bytes: &[u8], limits: &Limits) -> Res
let npixels = w.checked_mul(h).ok_or_else(|| err("the data window is too large"))?;
limits.check_bytes(w as u32, h as u32, (header.channels.list.len().max(1) as u64) * 4)?;
// The display window is the full stage the renderer composed onto (a render pass's data
// window is often its crop). Open on the stage when it plausibly holds the data region:
// sane size for the limits, and the data region inside it. Otherwise fall back to the
// data window alone, as before.
let display = header.shared_attributes.display_window;
let display_size = (display.size.0.max(1) as u32, display.size.1.max(1) as u32);
let (stage, origin) = match display_size {
(sw, sh)
if sw <= 1 << 15
&& sh <= 1 << 15
&& limits.check_bytes(sw, sh, 4).is_ok()
&& dw.position.0 >= display.position.0
&& dw.position.1 >= display.position.1
&& dw.position.0.saturating_add(w as i32) <= display.position.0.saturating_add(sw as i32)
&& dw.position.1.saturating_add(h as i32) <= display.position.1.saturating_add(sh as i32) =>
{
(Some((sw, sh)), (dw.position.0 - display.position.0, dw.position.1 - display.position.1))
}
_ => (None, (0, 0)),
};
let exr_types: Vec<ExrSample> = header.channels.list.iter().map(|c| c.sample_type).collect();
let bytes_per_sample: usize = exr_types.iter().map(|t| sample_bytes(*t)).sum();
let multipart = meta.requirements.has_multiple_layers;
@@ -440,7 +461,7 @@ pub(crate) fn decode_deep(meta: &MetaData, bytes: &[u8], limits: &Limits) -> Res
}
}
Ok(DeepImage { width: w as u32, height: h as u32, channels, counts: deep_counts })
Ok(DeepImage { width: w as u32, height: h as u32, stage, origin, channels, counts: deep_counts })
}
/// Where the headers end and the offset tables begin. [`MetaData::read_from_buffered`]
@@ -565,7 +586,28 @@ pub(crate) fn flatten(deep: &DeepImage) -> Result<Image, CodecError> {
}
}
}
let mut img = Image::from_f32(deep.width, deep.height, layout, &out)?;
// Open on the full display-window stage when the file declares one: the renderer's data
// window is its crop, and the composition lives on the whole stage. The composed region
// pads to the stage at its offset, transparent elsewhere (limits validated the size).
let mut img = match deep.stage {
Some((sw, sh)) => {
let mut padded = vec![0f32; sw as usize * sh as usize * nc];
let row_len = deep.width as usize * nc;
let dx = deep.origin.0.max(0) as usize;
for row in 0..deep.height as usize {
let src = row * row_len;
let dst = (row + deep.origin.1.max(0) as usize) * sw as usize + dx;
if let (Some(d), Some(r)) = (padded.get_mut(dst * nc..dst * nc + row_len), out.get(src..src + row_len)) {
d.copy_from_slice(r);
}
}
Image::from_f32(sw, sh, layout, &padded)?
}
None => Image::from_f32(deep.width, deep.height, layout, &out)?,
};
if let Some((sw, sh)) = deep.stage {
img.warnings.push(DecodeWarning::DisplayWindowStage { stage: (sw, sh), origin: deep.origin });
}
img.warnings.push(DecodeWarning::DeepFlattened { format: F, max_samples_per_pixel: max_samples });
Ok(img)
}
+12
View File
@@ -150,6 +150,8 @@ pub enum DecodeWarning {
MorePages { total: Option<u32> },
/// Only one part of a multi-part file was decoded; `total` counts every part when known.
MoreParts { total: Option<u32> },
/// The image opened on the full `display_window` stage; the data region sits at `origin`.
DisplayWindowStage { stage: (u32, u32), origin: (i32, i32) },
/// The image data ends early (truncated or damaged file); the decoder filled in the rest
/// (a baseline JPEG's missing rows come out grey).
Truncated { format: Format },
@@ -193,6 +195,9 @@ impl fmt::Display for DecodeWarning {
DecodeWarning::MorePages { total: None } => write!(f, "only the first page of the file was imported"),
DecodeWarning::MoreParts { total: Some(n) } => write!(f, "only one of {n} EXR parts was imported"),
DecodeWarning::MoreParts { total: None } => write!(f, "only one EXR part of the file was imported"),
DecodeWarning::DisplayWindowStage { stage, origin } => {
write!(f, "opened on the full display window stage ({}x{}); the data sits at ({}, {})", stage.0, stage.1, origin.0, origin.1)
}
DecodeWarning::Truncated { format } => {
write!(f, "{} data ends early (the file is truncated or damaged); part of the image is missing", format.name())
}
@@ -232,6 +237,13 @@ pub struct DeepChannel {
pub struct DeepImage {
pub width: u32,
pub height: u32,
/// The full stage the image opens on: the EXR `display_window` size, when the file declares
/// one the data region fits into and the limits allow. `None` when absent, absurd or not
/// containing the data region — then the image opens on the data window alone, as before.
pub stage: Option<(u32, u32)>,
/// Where this image's (0, 0) sits on the stage: `data_window.position - display_window.position`.
/// `(0, 0)` when [`Self::stage`] is `None`.
pub origin: (i32, i32),
/// The sample channels in file order.
pub channels: Vec<DeepChannel>,
/// Cumulative sample counts, `len == width as usize * height as usize + 1`: pixel `i`
+131
View File
@@ -254,6 +254,9 @@ fn gen_deep(
out
}
/// RGBA float channels in chlist (alphabetical) order, no Z (everything composites).
const RGBA_NO_Z: [Chan; 4] = [Chan { name: "A", ty: 2 }, Chan { name: "B", ty: 2 }, Chan { name: "G", ty: 2 }, Chan { name: "R", ty: 2 }];
/// RGBA(Z) float channels in chlist (alphabetical) order.
const RGBAZ: [Chan; 5] =
[Chan { name: "A", ty: 2 }, Chan { name: "B", ty: 2 }, Chan { name: "G", ty: 2 }, Chan { name: "R", ty: 2 }, Chan { name: "Z", ty: 2 }];
@@ -709,3 +712,131 @@ fn real_deep_files_decode() {
assert!(img.warnings.iter().any(|w| matches!(w, photocraft_codecs::DecodeWarning::DeepFlattened { .. })), "{name}");
}
}
/// `gen_deep` for a cropped render: the data window sits at `(dwx, dwy)` inside a larger
/// display window of `sw × sh` (the stage the renderer composed onto). Scanline chunk Y
/// coordinates are absolute display rows, so they carry the data-window offset.
#[allow(clippy::too_many_arguments)]
fn gen_deep_cropped(
w: usize,
h: usize,
dwx: i32,
dwy: i32,
sw: usize,
sh: usize,
channels: &[Chan],
counts: &[u32],
sample: &dyn Fn(usize, usize) -> Vec<f32>,
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0x0131_2f76u32.to_le_bytes());
out.extend_from_slice(&(2u32 | 0x800).to_le_bytes());
let mut chlist = Vec::new();
for c in channels {
chlist.extend_from_slice(c.name.as_bytes());
chlist.push(0);
chlist.extend_from_slice(&c.ty.to_le_bytes());
chlist.push(0);
chlist.extend_from_slice(&[0u8; 3]);
chlist.extend_from_slice(&1i32.to_le_bytes());
chlist.extend_from_slice(&1i32.to_le_bytes());
}
chlist.push(0);
attr(&mut out, "channels", "chlist", &chlist);
attr(&mut out, "compression", "compression", &[0u8]);
// box2i is inclusive: the windows end at position + size - 1.
let dw: Vec<u8> = [dwx, dwy, dwx + w as i32 - 1, dwy + h as i32 - 1].iter().flat_map(|v| v.to_le_bytes()).collect();
attr(&mut out, "dataWindow", "box2i", &dw);
let disp: Vec<u8> = [0i32, 0, sw as i32 - 1, sh as i32 - 1].iter().flat_map(|v| v.to_le_bytes()).collect();
attr(&mut out, "displayWindow", "box2i", &disp);
attr(&mut out, "lineOrder", "lineOrder", &[0]);
attr(&mut out, "pixelAspectRatio", "float", &1f32.to_le_bytes());
attr(&mut out, "screenWindowCenter", "v2f", &[0f32.to_le_bytes(), 0f32.to_le_bytes()].concat());
attr(&mut out, "screenWindowWidth", "float", &1f32.to_le_bytes());
attr(&mut out, "type", "string", b"deepscanline");
attr(&mut out, "version", "int", &1i32.to_le_bytes());
out.push(0);
// One scanline block per data row; the pixel indexing stays data-relative.
let mut chunks: Vec<Vec<u8>> = Vec::new();
for y in 0..h {
let pixels: Vec<usize> = (y * w..y * w + w).collect();
let mut c = Vec::new();
c.extend_from_slice(&(dwy + y as i32).to_le_bytes());
let mut table = Vec::with_capacity(pixels.len() * 4);
let mut acc = 0u32;
for &p in &pixels {
acc += counts[p];
table.extend_from_slice(&acc.to_le_bytes());
}
let mut data = Vec::new();
for (ci, _) in channels.iter().enumerate() {
for &p in &pixels {
for v in sample(p, ci) {
data.extend_from_slice(&v.to_le_bytes());
}
}
}
c.extend_from_slice(&(table.len() as u64).to_le_bytes());
c.extend_from_slice(&(data.len() as u64).to_le_bytes());
c.extend_from_slice(&(data.len() as u64).to_le_bytes());
c.extend_from_slice(&table);
c.extend_from_slice(&data);
chunks.push(c);
}
let mut pos = out.len() + chunks.len() * 8;
let mut table = Vec::new();
for c in &chunks {
table.extend_from_slice(&(pos as u64).to_le_bytes());
pos += c.len();
}
out.extend_from_slice(&table);
for c in chunks {
out.extend_from_slice(&c);
}
out
}
/// A cropped render opens on the full display-window stage: MY27-style renders carry a
/// `display_window` of the full frame (e.g. 4000×3000) while the deep data window is the
/// render pass's crop inside it. The image must open at stage size with the data at its
/// crop offset, transparent elsewhere — not cropped to the data window.
#[test]
fn cropped_deep_data_opens_on_the_display_stage() {
let (w, h, dwx, dwy, sw, sh) = (24usize, 16usize, 8i32, 6i32, 48usize, 32usize);
let counts = vec![1u32; w * h];
let sample = |p: usize, ci: usize| -> Vec<f32> {
// chlist order (RGBA_NO_Z): A, B, G, R.
let x = p % w;
let y = p / w;
match ci {
0 => vec![1.0], // A
1 => vec![0.5], // B
2 => vec![y as f32 / h as f32], // G
_ => vec![x as f32 / w as f32], // R
}
};
let bytes = gen_deep_cropped(w, h, dwx, dwy, sw, sh, &RGBA_NO_Z, &counts, &sample);
let img = decode(&bytes).expect("cropped deep EXR decodes");
assert_eq!((img.width(), img.height()), (sw as u32, sh as u32), "opens on the display window stage");
assert!(img.warnings.iter().any(|w| matches!(w, photocraft_codecs::DecodeWarning::DisplayWindowStage { origin, .. } if *origin == (dwx, dwy))));
// The data sits at its crop offset: the first data pixel's green channel carries its
// data-row fraction, everything outside the data region stays transparent black.
let s = img.to_f32_samples().expect("f32");
let nc = s.len() / (sw * sh);
let px = |x: usize, y: usize| -> Vec<f32> { s[y * sw * nc + x * nc..y * sw * nc + x * nc + nc].to_vec() };
let first = px(dwx as usize, dwy as usize);
assert!(
(first[0] - 0.0).abs() < 1e-5 && (first[1] - 0.0).abs() < 1e-5 && (first[2] - 0.5).abs() < 1e-5 && (first[3] - 1.0).abs() < 1e-5,
"first data pixel at the offset: {first:?}"
);
let last = px(dwx as usize + w - 1, dwy as usize + h - 1);
assert!(
(last[0] - (w - 1) as f32 / w as f32).abs() < 1e-5 && (last[1] - (h - 1) as f32 / h as f32).abs() < 1e-5,
"last data pixel keeps its coordinates: {last:?}"
);
let outside = px(0, 0);
assert_eq!(outside, vec![0.0, 0.0, 0.0, 0.0], "outside the data region: transparent");
}