mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Image Trace: Flat Logo counts only the outlines it keeps toward the anchor limit
Flat Logo counted the anchors of every fitted outline and hole before it left out the ones with no area, so a trace whose kept paths fit the anchor limit could still be refused as too complex. The limit now counts the anchors of the subpaths that go into the result. The limit on outlines per color also counted holes, which are cut only with Ignore White on. Holes now count toward it only when they are cut.
This commit is contained in:
@@ -296,6 +296,28 @@ fn shape_subpaths(s: &Shape, map: &ToSource) -> Vec<SubPath> {
|
||||
subs
|
||||
}
|
||||
|
||||
/// A shape's subpaths in source pixels, with no outline or hole that paints nothing.
|
||||
struct Kept {
|
||||
class: usize,
|
||||
area: f64,
|
||||
subs: Vec<SubPath>,
|
||||
}
|
||||
|
||||
/// Adds `s` to `kept` unless it paints nothing, and counts the anchors of the subpaths it keeps
|
||||
/// in `anchors_made`: the trace is too complex once they come to more than `max_anchors`.
|
||||
fn keep_shape(s: &Shape, map: &ToSource, kept: &mut Vec<Kept>, anchors_made: &mut usize, max_anchors: usize) -> Result<(), TraceError> {
|
||||
let subs = shape_subpaths(s, map);
|
||||
if subs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
*anchors_made = anchors_made.saturating_add(subs.iter().map(|sp| sp.anchors.len()).sum::<usize>());
|
||||
if *anchors_made > max_anchors {
|
||||
return Err(TraceError::TooComplex { max: max_anchors });
|
||||
}
|
||||
kept.push(Kept { class: s.class, area: s.area, subs });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A hard-edged source (a GIF, say) has no anti-aliasing to read an edge position from, and its
|
||||
/// outlines would follow the pixel staircase. Blurring the coverage by under a pixel gives the
|
||||
/// outline a smooth edge to find. Sources with plenty of blended edge pixels are left alone.
|
||||
@@ -449,7 +471,8 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
// `noise` is in source pixels (areas under noise/10 are ignored), whatever the working size.
|
||||
let min_area = ((f64::from(params.noise) / 10.0) / (factor * factor) as f64).max(0.3);
|
||||
let mut stats = Stats::default();
|
||||
let mut shapes: Vec<Shape> = Vec::new();
|
||||
let to_source = ToSource { factor: factor as f64, work: (w, h), source: (w0, h0) };
|
||||
let mut shapes: Vec<Kept> = Vec::new();
|
||||
let mut anchors_made = 0usize;
|
||||
for class in 0..=white {
|
||||
if class == white && params.ignore_white {
|
||||
@@ -465,6 +488,10 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
if a2.abs() / 2.0 < min_area {
|
||||
continue;
|
||||
}
|
||||
// Holes are cut only under Ignore White; otherwise white and the other colors are traced as their own shapes.
|
||||
if a2 >= 0.0 && !params.ignore_white {
|
||||
continue;
|
||||
}
|
||||
// More real outlines than we will fit is refused, not silently truncated.
|
||||
if outers.len() + holes.len() >= MAX_LOOPS_PER_CLASS {
|
||||
return Err(TraceError::TooComplex { max: max_anchors });
|
||||
@@ -489,28 +516,19 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
let Some(outer) = fit_outline(&pts, &style, &mut stats) else { continue };
|
||||
let hole_fits: Vec<Vec<Cubic>> =
|
||||
holes.iter().zip(&owner).filter(|(_, o)| **o == Some(idx)).filter_map(|(hole, _)| fit_outline(hole, &style, &mut stats)).collect();
|
||||
anchors_made += outer.len() + hole_fits.iter().map(Vec::len).sum::<usize>();
|
||||
if anchors_made > max_anchors {
|
||||
return Err(TraceError::TooComplex { max: max_anchors });
|
||||
}
|
||||
shapes.push(Shape { class, area, outer, holes: hole_fits });
|
||||
keep_shape(&Shape { class, area, outer, holes: hole_fits }, &to_source, &mut shapes, &mut anchors_made, max_anchors)?;
|
||||
}
|
||||
}
|
||||
// Painter's order: biggest shapes first, so smaller ones sit on top of what they belong to.
|
||||
shapes.sort_by(|a, b| b.area.total_cmp(&a.area).then(a.class.cmp(&b.class)));
|
||||
let to_source = ToSource { factor: factor as f64, work: (w, h), source: (w0, h0) };
|
||||
let mut out = TraceResult { worked_at: (factor > 1).then_some((w as u32, h as u32)), ..TraceResult::default() };
|
||||
for s in shapes {
|
||||
let color = palette.get(s.class).copied().unwrap_or([255, 255, 255]);
|
||||
let subs = shape_subpaths(&s, &to_source);
|
||||
if subs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !out.palette.contains(&color) {
|
||||
out.palette.push(color);
|
||||
}
|
||||
out.paths.push(TracedPath {
|
||||
path: PathData::new(subs),
|
||||
path: PathData::new(s.subs),
|
||||
color,
|
||||
pixels: (s.area * (factor * factor) as f64) as usize,
|
||||
stroke: None,
|
||||
@@ -563,4 +581,17 @@ mod tests {
|
||||
};
|
||||
assert!(shape_subpaths(&shape, &map).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_anchor_limit_counts_only_the_subpaths_kept() {
|
||||
let map = ToSource { factor: 1.0, work: (40, 40), source: (40, 40) };
|
||||
let square = || polygon(&[(5.0, 5.0), (35.0, 5.0), (35.0, 35.0), (5.0, 35.0)]);
|
||||
let with_flat_hole = Shape { class: 0, area: 900.0, outer: square(), holes: vec![polygon(&[(10.0, 20.0), (20.0, 20.0)])] };
|
||||
let flat_outline = Shape { class: 0, area: 1.0, outer: polygon(&[(5.0, 5.0), (35.0, 5.0)]), holes: vec![square()] };
|
||||
let (mut kept, mut anchors_made) = (Vec::new(), 0usize);
|
||||
assert!(keep_shape(&with_flat_hole, &map, &mut kept, &mut anchors_made, 4).is_ok());
|
||||
assert!(keep_shape(&flat_outline, &map, &mut kept, &mut anchors_made, 4).is_ok());
|
||||
assert_eq!((kept.len(), anchors_made), (1, 4));
|
||||
assert!(matches!(keep_shape(&with_flat_hole, &map, &mut kept, &mut anchors_made, 4), Err(TraceError::TooComplex { max: 4 })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,24 @@ fn too_many_outlines_are_refused_not_silently_truncated() {
|
||||
assert!(res.paths.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn holes_that_are_not_cut_do_not_count_towards_the_outline_limit() {
|
||||
// A blue field with 625 holes, each filled with red, white or green: more holes than the limit, fewer of each fill.
|
||||
const GREEN: [u8; 3] = [40, 170, 60];
|
||||
let img = render(125, 125, 0.0, |x, y| {
|
||||
let (cx, cy) = (x.rem_euclid(5.0), y.rem_euclid(5.0));
|
||||
if (1.0..4.0).contains(&cx) && (1.0..4.0).contains(&cy) {
|
||||
Some([RED, [255, 255, 255], GREEN][((x / 5.0) as usize + (y / 5.0) as usize) % 3])
|
||||
} else {
|
||||
Some(BLUE)
|
||||
}
|
||||
});
|
||||
let res = trace_within(&img, &logo_params(), 1_000_000).expect("trace");
|
||||
assert!(res.paths.iter().any(|p| near(p.color, BLUE, 3)), "the blue field is traced");
|
||||
// Under Ignore White the holes are cut, so they count.
|
||||
assert!(matches!(trace_within(&img, &TraceParams { ignore_white: true, ..logo_params() }, 1_000_000), Err(TraceError::TooComplex { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_odd_sized_image_never_overshoots_its_edges() {
|
||||
// 1025 wide: the last working pixel is narrower than the others.
|
||||
|
||||
Reference in New Issue
Block a user