mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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()];
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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
@@ -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]]);
|
||||||
|
|||||||
Reference in New Issue
Block a user