security(raster, segment): resample tall narrow images columns first (#3678)

This commit is contained in:
Dylan M. Taylor
2026-11-03 23:00:51 -05:00
committed by GitHub
parent 1c5cbce835
commit 4d42507bbc
2 changed files with 66 additions and 2 deletions
+28 -2
View File
@@ -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<T: Pixel>(img: &Image<T>, w: usize, h: usize, filter: Filter) -> Image<T> {
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<T: Pixel>(img: &Image<T>, w: usize, filter: Filter) -> Image<T> {
let wx = weights(img.width, w, filter);
let mut tmp = Image::<T>::new(w, img.height);
par_rows(&mut tmp.data, w, |y, row| {
@@ -134,12 +144,18 @@ pub fn resize<T: Pixel>(img: &Image<T>, w: usize, h: usize, filter: Filter) -> I
*o = acc;
}
});
tmp
}
/// Each column resampled to `h` tall.
fn vertical<T: Pixel>(img: &Image<T>, h: usize, filter: Filter) -> Image<T> {
let w = img.width;
let wy = weights(img.height, h, filter);
let mut out = Image::<T>::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<T: Pixel>(img: &Image<T>) -> Image<T> {
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::<f32>::filled(37, 21, 0.42);
+38
View File
@@ -34,6 +34,31 @@ pub fn preprocess(rgb: &[u8], w: usize, h: usize, side: usize) -> Result<Vec<f32
if w == 0 || h == 0 || rgb.len() < n {
return Err(Error::Model(format!("image buffer of {} bytes for {w} × {h}", rgb.len())));
}
// The horizontal pass keeps every source row (h × side samples): for a strip far taller than
// it is wide that is many times the image itself, so such an image is resampled transposed
// (its rows become columns) and the result transposed back.
if h > 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<u8> = (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());
}