From 2541fd8efd95ebe6e9605b446b34da9a3fef2ca8 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 21:52:11 -0400 Subject: [PATCH] Audit: Image Trace bounds (mosaic duplicate tiles and short buffers, overflow-safe raster length, grouping and centerline work caps) (#3878) --- crates/trace/src/centerline/mod.rs | 5 ++ crates/trace/src/group.rs | 7 +++ crates/trace/src/lib.rs | 2 +- crates/trace/src/logo/pipeline.rs | 2 +- crates/trace/src/mosaic.rs | 76 +++++++++++++++++++++++------- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/crates/trace/src/centerline/mod.rs b/crates/trace/src/centerline/mod.rs index 7999f655f..332c4499c 100644 --- a/crates/trace/src/centerline/mod.rs +++ b/crates/trace/src/centerline/mod.rs @@ -65,6 +65,9 @@ pub(crate) fn lines( let mask = |i: usize| labels.get(i) == Some(&label); let mut seen = vec![false; w * h]; let mut stack = vec![]; + // Thin shapes nested in one another have overlapping boxes: the cells of all of them together + // are bounded, so the work is too (a component past it stays as it is, not a line). + let mut cells_left = n.saturating_mul(8).max(1 << 22); for start in 0..w * h { if !mask(start) || seen.get(start).copied().unwrap_or(false) { continue; @@ -86,6 +89,8 @@ pub(crate) fn lines( } } let Some(g) = Grid::from_component(&comp, w) else { continue }; + let Some(left) = cells_left.checked_sub(g.w.saturating_mul(g.h)) else { break }; + cells_left = left; let Some(dt) = distance::squared(&g.on, g.w, g.h) else { continue }; let widest = 2.0 * f64::from(dt.iter().copied().fold(0.0f32, f32::max)).sqrt() - 1.0; if widest > max_width { diff --git a/crates/trace/src/group.rs b/crates/trace/src/group.rs index d5b615de6..50b165f1a 100644 --- a/crates/trace/src/group.rs +++ b/crates/trace/src/group.rs @@ -73,6 +73,9 @@ fn inside_or_near(p: Point, o: &Outer) -> bool { inside } +/// The most paths grouped. +const MAX_GROUPED_PATHS: usize = 20_000; + /// For each of `paths` (bottom-most first), the path it lies inside, if any: the smallest filled /// path whose outer contour holds all of its own. The path that covers the whole `w` × `h` image /// (the paper) is nobody's parent: everything would be inside it. @@ -81,6 +84,10 @@ fn inside_or_near(p: Point, o: &Outer) -> bool { /// `paths` paints in its own order: where the paths can overlap (`stacked`), a path only gets a parent below it, and only /// if no path that would then come after it instead of before it shares any of its box. pub(crate) fn parents(paths: &[TracedPath], w: usize, h: usize, stacked: bool) -> Vec> { + // Each path is tested against the others (quadratic): past this many, they stay ungrouped. + if paths.len() > MAX_GROUPED_PATHS { + return vec![None; paths.len()]; + } let outers: Vec> = paths.iter().map(outer).collect(); let paper = Rect::new(1.0, 1.0, w as f64 - 1.0, h as f64 - 1.0); let mut parent: Vec> = vec![None; paths.len()]; diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs index d9dfeab21..1e3849bf4 100644 --- a/crates/trace/src/lib.rs +++ b/crates/trace/src/lib.rs @@ -516,7 +516,7 @@ pub fn trace(img: &Raster, params: &TraceParams) -> TraceResult { /// colour layers are traced in parallel where threads are available. pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result { let (w, h) = (img.width as usize, img.height as usize); - if w == 0 || h == 0 || img.rgba.len() != w * h * 4 { + if w == 0 || h == 0 || w.checked_mul(h).and_then(|n| n.checked_mul(4)) != Some(img.rgba.len()) { return Ok(TraceResult::default()); } let mut out = trace_paths(img, params, max_anchors)?; diff --git a/crates/trace/src/logo/pipeline.rs b/crates/trace/src/logo/pipeline.rs index 887989b5d..6004af7f4 100644 --- a/crates/trace/src/logo/pipeline.rs +++ b/crates/trace/src/logo/pipeline.rs @@ -342,7 +342,7 @@ fn choose_palette(params: &TraceParams, pix: &[Px], w: usize, h: usize) -> Resul /// Trace a flat-colour logo. pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result { let (w0, h0) = (img.width as usize, img.height as usize); - if w0 == 0 || h0 == 0 || img.rgba.len() != w0 * h0 * 4 { + if w0 == 0 || h0 == 0 || w0.checked_mul(h0).and_then(|n| n.checked_mul(4)) != Some(img.rgba.len()) { return Ok(TraceResult::default()); } let (mut pix, mut w, mut h, mut factor) = working_copy(img, 1); diff --git a/crates/trace/src/mosaic.rs b/crates/trace/src/mosaic.rs index ac2ae3051..b8bd46a37 100644 --- a/crates/trace/src/mosaic.rs +++ b/crates/trace/src/mosaic.rs @@ -2,38 +2,72 @@ use crate::Raster; +/// The most tiles along a side. +const MAX_SIDE: u32 = 4096; + /// Average colour (straight RGBA) of each of `cols` × `rows` tiles covering `img`, row by row. -/// Transparent pixels count by their alpha, so a tile over transparency fades out. +/// Transparent pixels count by their alpha, so a tile over transparency fades out. Tiles over the +/// same pixels (more tiles than pixels) are averaged once, and a raster whose bytes are not +/// `width * height * 4` gives transparent tiles. pub fn mosaic(img: &Raster, cols: u32, rows: u32) -> Vec<[u8; 4]> { - let (cols, rows) = (cols.max(1), rows.max(1)); - let mut out = Vec::with_capacity((cols * rows) as usize); + let (cols, rows) = (cols.clamp(1, MAX_SIDE), rows.clamp(1, MAX_SIDE)); + let tiles = cols as usize * rows as usize; + let (w, h) = (img.width as usize, img.height as usize); + if w.checked_mul(h).and_then(|n| n.checked_mul(4)) != Some(img.rgba.len()) { + return vec![[0; 4]; tiles]; + } + let mut out = Vec::with_capacity(tiles); + // The pixel rows of the last row of tiles, and where its tiles start in `out`. + let mut last_row: Option<((u32, u32), usize)> = None; for r in 0..rows { let y0 = (r as u64 * img.height as u64 / rows as u64) as u32; let y1 = (((r + 1) as u64 * img.height as u64 / rows as u64) as u32).max(y0 + 1).min(img.height); + if let Some((range, start)) = last_row + && range == (y0, y1) + { + out.extend_from_within(start..start + cols as usize); + continue; + } + let start = out.len(); + let mut last: Option<((u32, u32), [u8; 4])> = None; for c in 0..cols { let x0 = (c as u64 * img.width as u64 / cols as u64) as u32; let x1 = (((c + 1) as u64 * img.width as u64 / cols as u64) as u32).max(x0 + 1).min(img.width); - let (mut sr, mut sg, mut sb, mut sa, mut n) = (0u64, 0u64, 0u64, 0u64, 0u64); - for y in y0..y1.max(y0 + 1).min(img.height) { - for x in x0..x1.max(x0 + 1).min(img.width) { - let [pr, pg, pb, pa] = img.pixel(x, y); - let a = pa as u64; - sr += pr as u64 * a; - sg += pg as u64 * a; - sb += pb as u64 * a; - sa += a; - n += 1; - } + if let Some((range, tile)) = last + && range == (x0, x1) + { + out.push(tile); + continue; } - out.push(match (sr.checked_div(sa), sg.checked_div(sa), sb.checked_div(sa)) { - (Some(r), Some(g), Some(b)) => [r as u8, g as u8, b as u8, (sa / n.max(1)) as u8], - _ => [0, 0, 0, 0], - }); + let tile = average(img, (x0, x1), (y0, y1)); + out.push(tile); + last = Some(((x0, x1), tile)); } + last_row = Some(((y0, y1), start)); } out } +/// The average colour of pixels `xs` × `ys` (half-open). +fn average(img: &Raster, (x0, x1): (u32, u32), (y0, y1): (u32, u32)) -> [u8; 4] { + let (mut sr, mut sg, mut sb, mut sa, mut n) = (0u64, 0u64, 0u64, 0u64, 0u64); + for y in y0..y1.max(y0 + 1).min(img.height) { + for x in x0..x1.max(x0 + 1).min(img.width) { + let [pr, pg, pb, pa] = img.pixel(x, y); + let a = pa as u64; + sr += pr as u64 * a; + sg += pg as u64 * a; + sb += pb as u64 * a; + sa += a; + n += 1; + } + } + match (sr.checked_div(sa), sg.checked_div(sa), sb.checked_div(sa)) { + (Some(r), Some(g), Some(b)) => [r as u8, g as u8, b as u8, (sa / n.max(1)) as u8], + _ => [0, 0, 0, 0], + } +} + #[cfg(test)] mod tests { use super::*; @@ -49,6 +83,12 @@ mod tests { assert!((m[0][0] as i32 - 127).abs() <= 1 && (m[0][2] as i32 - 127).abs() <= 1); // More tiles than pixels still works. assert_eq!(mosaic(&img, 20, 8).len(), 160); + // Tiles over the same pixels average the same. + let m = mosaic(&img, 20, 8); + assert!(m.chunks(20).all(|row| row == &m[..20])); + // Bytes that are not the raster's size: transparent. + let short = Raster { width: 4, height: 4, rgba: vec![] }; + assert_eq!(mosaic(&short, 2, 2), vec![[0; 4]; 4]); // Transparency. let clear = Raster::from_fn(2, 2, |_, _| [9, 9, 9, 0]); assert_eq!(mosaic(&clear, 1, 1), vec![[0, 0, 0, 0]]);