Audit: Image Trace bounds (mosaic duplicate tiles and short buffers, overflow-safe raster length, grouping and centerline work caps) (#3878)

This commit is contained in:
Dylan M. Taylor
2026-10-23 21:52:11 -04:00
committed by GitHub
parent 8cc69ed45c
commit 2541fd8efd
5 changed files with 72 additions and 20 deletions
+5
View File
@@ -65,6 +65,9 @@ pub(crate) fn lines(
let mask = |i: usize| labels.get(i) == Some(&label); let mask = |i: usize| labels.get(i) == Some(&label);
let mut seen = vec![false; w * h]; let mut seen = vec![false; w * h];
let mut stack = vec![]; 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 { for start in 0..w * h {
if !mask(start) || seen.get(start).copied().unwrap_or(false) { if !mask(start) || seen.get(start).copied().unwrap_or(false) {
continue; continue;
@@ -86,6 +89,8 @@ pub(crate) fn lines(
} }
} }
let Some(g) = Grid::from_component(&comp, w) else { continue }; 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 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; let widest = 2.0 * f64::from(dt.iter().copied().fold(0.0f32, f32::max)).sqrt() - 1.0;
if widest > max_width { if widest > max_width {
+7
View File
@@ -73,6 +73,9 @@ fn inside_or_near(p: Point, o: &Outer) -> bool {
inside 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 /// 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 /// 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. /// (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 /// `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. /// 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<Option<usize>> { pub(crate) fn parents(paths: &[TracedPath], w: usize, h: usize, stacked: bool) -> Vec<Option<usize>> {
// 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<Option<Outer>> = paths.iter().map(outer).collect(); let outers: Vec<Option<Outer>> = paths.iter().map(outer).collect();
let paper = Rect::new(1.0, 1.0, w as f64 - 1.0, h as f64 - 1.0); let paper = Rect::new(1.0, 1.0, w as f64 - 1.0, h as f64 - 1.0);
let mut parent: Vec<Option<usize>> = vec![None; paths.len()]; let mut parent: Vec<Option<usize>> = vec![None; paths.len()];
+1 -1
View File
@@ -516,7 +516,7 @@ pub fn trace(img: &Raster, params: &TraceParams) -> TraceResult {
/// colour layers are traced in parallel where threads are available. /// colour layers are traced in parallel where threads are available.
pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> { pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> {
let (w, h) = (img.width as usize, img.height as usize); 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()); return Ok(TraceResult::default());
} }
let mut out = trace_paths(img, params, max_anchors)?; let mut out = trace_paths(img, params, max_anchors)?;
+1 -1
View File
@@ -342,7 +342,7 @@ fn choose_palette(params: &TraceParams, pix: &[Px], w: usize, h: usize) -> Resul
/// Trace a flat-colour logo. /// Trace a flat-colour logo.
pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> { pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> {
let (w0, h0) = (img.width as usize, img.height as usize); 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()); return Ok(TraceResult::default());
} }
let (mut pix, mut w, mut h, mut factor) = working_copy(img, 1); let (mut pix, mut w, mut h, mut factor) = working_copy(img, 1);
+58 -18
View File
@@ -2,38 +2,72 @@
use crate::Raster; 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. /// 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]> { pub fn mosaic(img: &Raster, cols: u32, rows: u32) -> Vec<[u8; 4]> {
let (cols, rows) = (cols.max(1), rows.max(1)); let (cols, rows) = (cols.clamp(1, MAX_SIDE), rows.clamp(1, MAX_SIDE));
let mut out = Vec::with_capacity((cols * rows) as usize); 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 { for r in 0..rows {
let y0 = (r as u64 * img.height as u64 / rows as u64) as u32; 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); 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 { for c in 0..cols {
let x0 = (c as u64 * img.width as u64 / cols as u64) as u32; 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 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); if let Some((range, tile)) = last
for y in y0..y1.max(y0 + 1).min(img.height) { && range == (x0, x1)
for x in x0..x1.max(x0 + 1).min(img.width) { {
let [pr, pg, pb, pa] = img.pixel(x, y); out.push(tile);
let a = pa as u64; continue;
sr += pr as u64 * a;
sg += pg as u64 * a;
sb += pb as u64 * a;
sa += a;
n += 1;
}
} }
out.push(match (sr.checked_div(sa), sg.checked_div(sa), sb.checked_div(sa)) { let tile = average(img, (x0, x1), (y0, y1));
(Some(r), Some(g), Some(b)) => [r as u8, g as u8, b as u8, (sa / n.max(1)) as u8], out.push(tile);
_ => [0, 0, 0, 0], last = Some(((x0, x1), tile));
});
} }
last_row = Some(((y0, y1), start));
} }
out 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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); assert!((m[0][0] as i32 - 127).abs() <= 1 && (m[0][2] as i32 - 127).abs() <= 1);
// More tiles than pixels still works. // More tiles than pixels still works.
assert_eq!(mosaic(&img, 20, 8).len(), 160); 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. // Transparency.
let clear = Raster::from_fn(2, 2, |_, _| [9, 9, 9, 0]); let clear = Raster::from_fn(2, 2, |_, _| [9, 9, 9, 0]);
assert_eq!(mosaic(&clear, 1, 1), vec![[0, 0, 0, 0]]); assert_eq!(mosaic(&clear, 1, 1), vec![[0, 0, 0, 0]]);