diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs index 6ea7b689e..9e8157083 100644 --- a/crates/trace/src/lib.rs +++ b/crates/trace/src/lib.rs @@ -291,6 +291,7 @@ pub struct TraceParams { pub auto_grouping: bool, /// Gradients (Grayscale and Color): an area whose colour changes evenly along one direction /// is one path filled with a linear gradient ([`TracedPath::gradient`]), not bands of colour. + /// Grayscale finds them in the image's grays, and their stops are gray. pub gradients: bool, /// Create: trace areas as filled shapes. pub fills: bool, @@ -527,7 +528,17 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result let smoothed = (params.mode != Mode::BlackAndWhite && quantize::continuous_tone(img)).then(|| smooth::smoothed(img)); let img = smoothed.as_ref().unwrap_or(img); // Gradients are found first and kept out of the palette: no colour is spent on bands of them. - let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(img)).filter(|f| !f.1.is_empty()); + // Grayscale finds them in the image's grays (as its flat colours are), so their stops are gray. + let grays = (params.gradients && params.mode == Mode::Grayscale).then(|| { + let mut grays = img.clone(); + for p in grays.rgba.as_chunks_mut::<4>().0 { + let g = quantize::luma(*p); + p[..3].fill(g); + } + grays + }); + let traced_as = grays.as_ref().unwrap_or(img); + let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(traced_as)).filter(|f| !f.1.is_empty()); let without = found.as_ref().map(|(of, _)| { let mut rest = img.clone(); for (p, &g) in rest.rgba.as_chunks_mut::<4>().0.iter_mut().zip(of) { @@ -542,7 +553,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result // Each gradient is a palette entry of its own (its colour: the gradient's middle). let mut gradients: Vec> = vec![None; q.palette.len()]; if let Some((mut of, found)) = found { - gradient::settle_edges(img, &mut of, &found, &mut q.labels); + gradient::settle_edges(traced_as, &mut of, &found, &mut q.labels); let first = q.palette.len(); for (l, &g) in q.labels.iter_mut().zip(&of) { if g != u32::MAX { @@ -552,6 +563,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result q.palette.extend(found.iter().map(|g| g.color_at(g.start.midpoint(g.end)).map(|v| v.round().clamp(0.0, 255.0) as u8))); gradients.extend(found.into_iter().map(Some)); } + drop(grays); denoise(&mut q.labels, w, h, params.noise as usize); // Pixel count per palette entry, and the box its pixels lie in. let mut counts = vec![0usize; q.palette.len()]; diff --git a/crates/trace/src/quantize.rs b/crates/trace/src/quantize.rs index 476a212eb..7c14e3dde 100644 --- a/crates/trace/src/quantize.rs +++ b/crates/trace/src/quantize.rs @@ -14,7 +14,8 @@ pub struct Quantized { pub palette: Vec<[u8; 3]>, } -fn luma(p: [u8; 4]) -> u8 { +/// A pixel's gray (Rec. 601 luma): what Grayscale traces it as. +pub(crate) fn luma(p: [u8; 4]) -> u8 { ((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114 + 500) / 1000) as u8 } diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs index 3f8fb0f5c..59e49bd8c 100644 --- a/crates/trace/src/tests.rs +++ b/crates/trace/src/tests.rs @@ -1295,3 +1295,36 @@ fn stroke_caps_are_square_or_round_as_the_ink_is() { assert!(line(true)); assert!(!line(false)); } + +/// A blue-to-orange sky over flat colours, and a green-to-magenta ramp in a rectangle on them. +fn sky_and_ramp() -> Raster { + Raster::from_fn(240, 160, |x, y| { + let (fx, fy) = (f64::from(x), f64::from(y)); + let mix = |a: [f64; 3], b: [f64; 3], t: f64| [0, 1, 2].map(|k| (a[k] + (b[k] - a[k]) * t).round() as u8); + if (20..100).contains(&x) && (100..150).contains(&y) { + let [r, g, b] = mix([40.0, 200.0, 60.0], [220.0, 40.0, 200.0], (fx - 20.0) / 80.0); + [r, g, b, 255] + } else if y >= 90 { + if x >= 140 { [200, 30, 40, 255] } else { [30, 120, 50, 255] } + } else { + let [r, g, b] = mix([30.0, 60.0, 200.0], [250.0, 140.0, 30.0], fy / 90.0); + [r, g, b, 255] + } + }) +} + +#[test] +fn grayscale_gradients_are_grey() { + let img = sky_and_ramp(); + let params = TraceParams { mode: Mode::Grayscale, colors: 4, gradients: true, ..TraceParams::default() }; + let res = trace(&img, ¶ms); + let grey = |c: [u8; 3]| c[0] == c[1] && c[1] == c[2]; + assert!(res.paths.iter().any(|p| p.gradient.is_some()), "a gradient is still found"); + for p in &res.paths { + assert!(grey(p.color), "{:?}", p.color); + for stop in p.gradient.iter().flat_map(|g| &g.stops) { + assert!(grey(stop.1), "{:?}", p.gradient); + } + } + assert!(res.palette.iter().all(|&c| grey(c)), "{:?}", res.palette); +}