From 4d42507bbcc180df831435c8ef2c29740b5216aa Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 23:00:51 -0500 Subject: [PATCH] security(raster, segment): resample tall narrow images columns first (#3678) --- crates/raster/src/resample.rs | 30 +++++++++++++++++++++++++-- crates/segment/src/mask.rs | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/crates/raster/src/resample.rs b/crates/raster/src/resample.rs index afe0c10d76..4664dfa4b3 100644 --- a/crates/raster/src/resample.rs +++ b/crates/raster/src/resample.rs @@ -115,12 +115,22 @@ impl Pixel for [f32; 4] { } } -/// Resize to `w × h` (separable; horizontal pass then vertical pass). +/// Resize to `w × h` (separable; horizontal pass then vertical pass, or the other way round when +/// that intermediate would be far larger: widening a tall, narrow strip kept every source row at +/// the new width, gigabytes from a small source). pub fn resize(img: &Image, w: usize, h: usize, filter: Filter) -> Image { let (w, h) = (w.max(1), h.max(1)); if img.width == w && img.height == h { return img.clone(); } + if w.saturating_mul(img.height) > img.width.saturating_mul(h).saturating_mul(4) { + return horizontal(&vertical(img, h, filter), w, filter); + } + vertical(&horizontal(img, w, filter), h, filter) +} + +/// Each row resampled to `w` wide. +fn horizontal(img: &Image, w: usize, filter: Filter) -> Image { let wx = weights(img.width, w, filter); let mut tmp = Image::::new(w, img.height); par_rows(&mut tmp.data, w, |y, row| { @@ -134,12 +144,18 @@ pub fn resize(img: &Image, w: usize, h: usize, filter: Filter) -> I *o = acc; } }); + tmp +} + +/// Each column resampled to `h` tall. +fn vertical(img: &Image, h: usize, filter: Filter) -> Image { + let w = img.width; let wy = weights(img.height, h, filter); let mut out = Image::::new(w, h); par_rows(&mut out.data, w, |y, row| { let (lo, ws) = &wy[y]; for (k, wt) in ws.iter().enumerate() { - let src = tmp.row(lo + k); + let src = img.row(lo + k); for (o, s) in row.iter_mut().zip(src) { *o = o.madd(*s, *wt); } @@ -178,6 +194,16 @@ pub fn half(img: &Image) -> Image { mod tests { use super::*; + #[test] + fn widening_a_tall_strip_resamples_its_columns_first() { + // 1 × 400 000 to 4000 × 8: horizontal-first kept 400 000 rows of 4000 (25 GB of RGBA f32) + let mut strip = Image::<[f32; 4]>::new(1, 400_000); + strip.data.iter_mut().for_each(|p| *p = [0.25, 0.5, 0.75, 1.0]); + let out = resize(&strip, 4000, 8, Filter::Mitchell); + assert_eq!((out.width, out.height), (4000, 8)); + assert!(out.data.iter().all(|p| (p[1] - 0.5).abs() < 1e-3), "a flat strip stays flat"); + } + #[test] fn constant_image_stays_constant() { let img = Image::::filled(37, 21, 0.42); diff --git a/crates/segment/src/mask.rs b/crates/segment/src/mask.rs index af349a2625..d884659aab 100644 --- a/crates/segment/src/mask.rs +++ b/crates/segment/src/mask.rs @@ -34,6 +34,31 @@ pub fn preprocess(rgb: &[u8], w: usize, h: usize, side: usize) -> Result w.saturating_mul(4) { + let mut t = vec![0u8; n]; + for y in 0..h { + for x in 0..w { + if let (Some(d), Some(s)) = (t.get_mut((x * h + y) * 3..(x * h + y) * 3 + 3), rgb.get((y * w + x) * 3..(y * w + x) * 3 + 3)) { + d.copy_from_slice(s); + } + } + } + let o = preprocess(&t, h, w, side)?; + let mut out = vec![0f32; o.len()]; + for c in 0..3 { + for oy in 0..side { + for ox in 0..side { + if let (Some(d), Some(v)) = (out.get_mut((c * side + oy) * side + ox), o.get((c * side + ox) * side + oy)) { + *d = *v; + } + } + } + } + return Ok(out); + } let (wx, wy) = (weights(w, side), weights(h, side)); // horizontal pass: h rows × side columns × 3 let mut tmp = vec![0f32; h * side * 3]; @@ -117,6 +142,19 @@ mod tests { #[test] fn short_buffers_are_errors() { assert!(preprocess(&[0; 5], 2, 2, 4).is_err()); + // a strip 1 pixel wide and 200 000 tall: resampled transposed, so the intermediate is + // side × 1 rather than 200 000 × side; it equals the resample of its transposed twin + let h = 200_000usize; + let strip: Vec = (0..h * 3).map(|i| (i / 3 % 251) as u8).collect(); + let a = preprocess(&strip, 1, h, 8).unwrap(); + let b = preprocess(&strip, h, 1, 8).unwrap(); + for c in 0..3 { + for y in 0..8 { + for x in 0..8 { + assert_eq!(a[(c * 8 + y) * 8 + x], b[(c * 8 + x) * 8 + y]); + } + } + } assert!(preprocess(&[], 0, 0, 4).is_err()); }