mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Merge pull request #4066 from dotCooCoo/fix/flat-logo-anchor-limit-counts-kept-outlines
Image Trace: Flat Logo counts only the outlines it keeps toward the anchor limit
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