From 179db957f712be3773b5beee92d9a0f45b6fb20f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 11:01:24 -0700 Subject: [PATCH] Image Trace: a color with no traced paths is left out of the palette A filled path that straightens to no area is left out of the trace (#3949). When that removed every path of a color, the color stayed in the trace's palette, so the Image Trace panel's Colors count and the `colors` that `imageTrace.make` returns included it. The palette now lists only the colors that have a path. --- crates/trace/src/lib.rs | 7 ++++++- crates/trace/src/tests.rs | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs index 185da7558..0cf283f6d 100644 --- a/crates/trace/src/lib.rs +++ b/crates/trace/src/lib.rs @@ -642,7 +642,12 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result let largest = job.boxes.iter().flatten().map(|b| (b.2 - b.0) * (b.3 - b.1)).max().unwrap_or(1).max(1); let at_a_time = ((512 << 20) / (largest * 10)).max(1); for (paths, &(_, ci)) in parallel_map_at_most(at_a_time, &layers, |&(r, ci)| job.layer(r, ci)).into_iter().zip(&layers) { - out.paths.extend(paths.ok_or(TraceError::TooComplex { max: max_anchors })?); + let paths = paths.ok_or(TraceError::TooComplex { max: max_anchors })?; + // A layer with no paths adds no color to the palette. + if paths.is_empty() { + continue; + } + out.paths.extend(paths); let color = q.palette[ci]; if !out.palette.contains(&color) { out.palette.push(color); diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs index fd727d266..82e178b6f 100644 --- a/crates/trace/src/tests.rs +++ b/crates/trace/src/tests.rs @@ -1355,6 +1355,46 @@ fn a_traced_pixel_that_straightens_to_no_area_is_left_out_of_fills_and_kept_as_a } } +#[test] +fn a_color_whose_every_path_straightens_to_no_area_is_left_out_of_the_palette() { + // A light rectangle with a black block on it and a one-pixel row of a pale blue along its lower + // edge, on a transparent background. Traced with a library of the light, the black and a + // blue, the row is the only part in the blue. + const BLUE: [u8; 3] = [40, 120, 240]; + let img = Raster::from_fn(200, 120, |x, y| { + if (60..140).contains(&x) && (70..90).contains(&y) { + BLACK + } else if (20..180).contains(&x) && (20..100).contains(&y) { + [240, 240, 220, 255] + } else if y == 100 && (30..170).contains(&x) { + [140, 180, 230, 255] + } else { + [0, 0, 0, 0] + } + }); + let params = TraceParams { + mode: Mode::Color, + palette: Palette::DocumentLibrary, + swatches: vec![[240, 240, 220], [0, 0, 0], BLUE], + colors: 3, + paths: 90.0, + corners: 60.0, + noise: 25, + method: Method::Abutting, + ..TraceParams::default() + }; + let used = |res: &TraceResult| res.palette.iter().all(|c| res.paths.iter().any(|p| p.color == *c)); + // Left alone, the row is a path in the blue, and the blue is in the palette. + let res = trace(&img, ¶ms); + assert!(res.paths.iter().any(|p| p.color == BLUE), "{:?}", res.paths.iter().map(|p| (p.color, p.path.bounds())).collect::>()); + assert!(res.palette.contains(&BLUE) && used(&res), "{:?}", res.palette); + // Snapped, both sides of the row straighten onto one line and its path is left out: no path is + // in the blue, and the palette leaves it out too. + let res = trace(&img, &TraceParams { snap_curves_to_lines: true, ..params }); + assert!(res.paths.iter().all(|p| p.color != BLUE), "{:?}", res.paths.iter().map(|p| (p.color, p.path.bounds())).collect::>()); + assert!(!res.palette.contains(&BLUE) && used(&res), "{:?}", res.palette); +} + #[test] fn a_straight_anti_aliased_line_is_two_anchors_in_its_middle_as_wide_as_its_ink() { // 2.6 px wide, from (20.3, 30.7) to (170.6, 52.2): a little off the pixel rows.