Compare commits

..
Author SHA1 Message Date
bflatastic 9c42a2942a Image Trace: Auto Grouping
Auto Grouping (params `autoGrouping`, a checkbox in the panel; off unless asked
for): each traced path that has others inside its outer contour comes out in a
group with them, the path first, nested as deep as the picture is. The path that
covers the whole image is nobody's group.

The grouped result paints the same picture: where paths can overlap (stacked
layers, Flat Logo, strokes) a path only joins a path below it, and only if that
moves it under nothing that shares any of its box.

The harness measures three presets with the option on ("grouped %"), painting
them in the grouped order.
2026-10-13 11:15:02 +07:00
BFlat 36286cc783 Merge pull request #1609 from storytold/u/trace-shapes
Image Trace: Shapes, and corners whose pixel is mostly outside the shape
2026-10-13 11:02:42 +07:00
BFlat 0573d3cdfb Merge pull request #1596 from storytold/fix/1561-symbol-percentages
SVG import: percentage lengths in a symbol with a viewBox are of the viewBox
2026-10-13 10:26:19 +07:00
bflatastic 9ca74ac011 Image Trace: Shapes, and corners whose pixel is mostly outside the shape
Shapes (params `shapes`, a checkbox in the panel; off unless asked for): a filled
outline that is a circle, an ellipse, a rectangle or a regular polygon comes out
as that live shape. A shape is only made when every point of the traced outline
lies within 0.5 px (0.2% of the shape's size, if more) of it; an ellipse whose
radii differ by less than twice that is a circle of its area.

The outline before fitting: a corner's own pixel no longer pulls the two edges
that meet there, and a one-pixel step between two long edges is the corner where
they meet. Sharp corners kept: Default 69% -> 76%, the colour presets 86% -> 91%.

The harness gains plain-shape inputs and "shapes %".
2026-10-13 10:25:05 +07:00
BFlat f0f3cb4eb1 Merge pull request #1595 from storytold/u/trace-strokes
Image Trace: stroked centre lines lie in the middle of their ink and are as wide as it
2026-10-13 10:14:01 +07:00
bflatastic 5fe916a425 SVG import: percentage lengths in a symbol with a viewBox are of the viewBox (#1561) 2026-10-13 09:58:14 +07:00
bflatastic 8e7b9ba436 Image Trace: stroked centre lines lie in the middle of their ink and are as wide as it
Create: Strokes (the Outline preset, or the option) traced lines along the pixel skeleton: right to a pixel,
with a width counted in pixels, and fitted by a separate, older fitter.

- Each skeleton point is moved to the middle of the line's ink, read from the image across the line: every
  pixel counts as much ink as its colour is the line's rather than the paper's, the ink's centroid is the
  middle and its sum the width (an anti-aliased or blurred line says both to a fraction of a pixel).
- The stroke width is the median of those widths, no longer a whole number of pixels.
- Each end goes where the ink ends, less the half width a round cap adds.
- Centre lines are fitted as outlines are: a straight line is two anchors, corners stay corners, Snap Curves To
  Lines levels them. The old Douglas-Peucker and simplify fitter, and the crate's pathops dependency, go.

Outline on the thin-line inputs, all sizes and variants (docs/image-trace-quality.md), before -> after:
- painted outline 0.310 -> 0.055 px off on average; 81 -> 14 anchors for five lines
- centre line 0.295 -> 0.052 px from the true centre line; stroke width 0.13 -> 0.02 px off; 18.4 -> 2.7
  anchors a stroke (blurred, noisy and scanned lines: 2.0 each; aliased: 4.6)
- largest distance 1.84 -> 2.94 px: the square ends of a 10 px line under a round cap
Presets without Create: Strokes are unchanged.
2026-10-13 09:36:47 +07:00
BFlat cbcee3bd54 Merge pull request #1593 from storytold/u/trace-upright
Image Trace: Snap Curves To Lines levels lines a degree or two askew
2026-10-13 09:36:14 +07:00
BFlat 0810a15f6e Merge pull request #1566 from marcoarnulfo/fix/new-symbol-options-dialog
Symbols panel: New Symbol asks for Symbol Options before making the symbol
2026-10-13 09:31:54 +07:00
BFlat db7e568809 Merge pull request #1567 from excited-raccoon/css-class-dedup-hashmap
Replace linear scan in CSS class dedup with a HashMap lookup
2026-10-13 09:31:36 +07:00
BFlat 9d72686d95 Merge pull request #1565 from switch87/fix/1557-css-transform-font
SVG import: CSS transforms in CSS syntax move objects; the font shorthand styles type
2026-10-13 09:31:20 +07:00
BFlat d79fb145c2 Merge pull request #1558 from marcoarnulfo/fix/zigzag-regular-waveform
Zig Zag: ridges are spaced evenly along each segment's length, so a straight line gets a regular wave
2026-10-13 09:31:04 +07:00
BFlat 6a16627de6 Merge pull request #1583 from storytold/fix/1534-guides
Fix #1534: guide paths take the guide colour and style, and a dragged selection snaps to ruler guides with Smart Guides on
2026-10-13 09:09:09 +07:00
BFlat acc1c9a85d Merge pull request #1582 from storytold/fix/1533-join-coincident
Fix #1533: Join merges the selected anchors that lie on one another (after Average) into one
2026-10-13 09:08:58 +07:00
BFlat 8fbf724cf6 Merge pull request #1580 from storytold/fix/1539-shortcut-menuless
Keyboard Shortcuts: the commands outside the menus that run without params are listed too (#1539)
2026-10-13 09:08:47 +07:00
BFlat 02ed637c68 Merge pull request #1544 from switch87/fix/1542-clip-covers-page
PDF/EPS import: a clip only counts as covering the page when it does
2026-10-13 09:08:21 +07:00
BFlat 9d2b074d12 Merge pull request #1543 from marcoarnulfo/fix/guide-objects-locked
Guides: Lock Guides locks the paths made into guides too
2026-10-13 09:08:05 +07:00
BFlat 632ea2a9f3 Merge pull request #1541 from switch87/fix/1536-effect-raster-paint-transform
Render: effect rasters drawn after a pattern fill keep their place and size
2026-10-13 09:07:48 +07:00
BFlat 523fd6e185 Merge pull request #1549 from marcoarnulfo/fix/duplicate-shortcut
Keyboard Shortcuts: Duplicate is listed, so it can be given a shortcut
2026-10-13 09:07:31 +07:00
bflatastic 612082e26e Image Trace: Snap Curves To Lines levels lines a degree or two askew
The documented option "replaces slightly curved lines with straight lines and snaps lines near 0 or 90 degrees
to absolute 0 or 90 degrees" (helpx, Image Trace panel options); ours did the first half only.

- With Snap Curves To Lines, a straight piece at least 2 px long and within 2.5 degrees of level or upright is
  made exactly so, by moving its ends across it to their mean (the page gives no angle: 2.5 degrees is our
  choice). The pieces next to a moved end follow it.
- A junction other regions share stays where it is, so abutting regions still have one curve along their edge;
  a line between a junction and a free end is levelled to the junction.
- The harness gains askew inputs (a rectangle, a plus sign and two lines, 1 to 2 degrees off level and upright)
  and an "upright %" column: the share of the outline length that is exactly level or upright.

On the clean askew inputs (docs/image-trace-quality.md): Black and White Logo and Technical Drawing, the presets
with the option on, go from 0% to 91% and 95% of their outline exactly level or upright. Their outlines are then
0.49 and 0.59 px from the art as drawn where they were 0.05 (4.2 px at the far end of a 300 px side turned 1.5
degrees): that distance is what the option is for. Presets without the option are unchanged.
2026-10-13 09:07:13 +07:00
bflatastic fea316911d Symbols panel: Alt-click on New Symbol skips Symbol Options 2026-10-13 08:53:06 +07:00
Ayanprogrammer11 3af08505f8 Replace linear scan in CSS class dedup with a HashMap lookup
Export::class() deduplicated declaration blocks with
classes.iter().position(), an O(n) scan per style, making exports with
many distinct styles (e.g. large palettes) quadratic overall. Track the
1-based class number per declaration in a HashMap alongside the
first-use-ordered Vec, so each lookup is O(1) while output stays
byte-identical.

Also adds a regression test covering 256 distinct declarations with
recurrence of early styles, for both InternalCss and StyleEntities
styling.
2026-10-13 08:53:06 +07:00
Gert Pellin 2c01a42b35 SVG import: CSS transforms in CSS syntax move objects; the font shorthand styles type
usvg takes a `transform` CSS property as a presentation attribute that
overrides the element's own, but reads its value as SVG: CSS units
(`translate(100px, 0)`, `rotate(45deg)`) don't parse, and the object
was left with no transform at all. Before usvg runs, transform
declarations in `style` attributes and `<style>` rules are now written
in SVG syntax (lengths in px, angles in deg/rad/grad/turn, translateX/Y,
scaleX/Y and skew as SVG's functions); values that can't be are left.

The text pass's own cascade now expands the `font` shorthand into its
longhands, as usvg does for the rest of the drawing, so text styled
with `font: italic bold 30px …` keeps its size, weight and style.

Refs #1557
2026-10-13 08:53:06 +07:00
bflatastic 45334df881 Zig Zag: tests of the ridge spacing on a circle and on an uneven curve 2026-10-13 08:53:06 +07:00
Marco Arnulfo ee97225c19 Symbols panel: New Symbol asks for Symbol Options before making the symbol (fixes #1560) 2026-10-13 08:53:06 +07:00
Marco Arnulfo 6085197bf4 Zig Zag: ridges are spaced evenly along each segment's length, so a straight line gets a regular wave (fixes #1554) 2026-10-13 08:53:05 +07:00
bflatastic 50911d8608 Fix #1534: a dragged selection snaps to ruler guides with Smart Guides on too 2026-10-13 08:51:43 +07:00
bflatastic 6939eff2f9 Fix #1534: paths made into guides take the guide colour and style of Preferences 2026-10-13 08:51:43 +07:00
Gert Pellin 9652d1d6b2 PDF/EPS import: a clip only counts as covering the page when it does
Both readers skipped a clip of at most six straight segments whose
bounding box contained the page. A diamond touching the page edges has
the page as its bounding box, so its clip was dropped and everything it
cuts away showed. geom::hit::polygon_covers_rect answers it properly
(one convex polygon containing the page's corners); any other path is
kept as a clip.

Fixes #1542
2026-10-13 08:51:40 +07:00
Marco Arnulfo 79b5d73ca7 Guides: Lock Guides locks the paths made into guides too (part of #1534) 2026-10-13 08:51:38 +07:00
Gert Pellin 05fdd15b45 Render: effect rasters drawn after a pattern fill keep their place and size
fx.rs draws blur, shadow and glow rasters as image paints that map 1:1
onto their rect, but only set the geometry transform: the paint
transform a pattern fill (or a radial or freeform gradient) left on the
context still applied, so in supersampled renders (raster exports) the
effect came out tiny and displaced. Both draws now reset the paint
transform first.

Fixes #1536
2026-10-13 08:51:37 +07:00
bflatastic 0a16b62a22 Fix #1533: Join merges the selected anchors that lie on one another (after Average) into one 2026-10-13 08:51:34 +07:00
bflatastic 20f5174710 Keyboard Shortcuts: the commands outside the menus that run without params are listed too (#1539) 2026-10-13 08:51:32 +07:00
Marco Arnulfo 00195e8d28 Keyboard Shortcuts: Duplicate is listed, so it can be given a shortcut (fixes #1539) 2026-10-13 08:51:30 +07:00
BFlat 05e321f4e0 Merge pull request #1574 from storytold/u/trace-options
Image Trace: Ignore Color, and the Auto Color and Outline presets
2026-10-13 08:47:04 +07:00
BFlat 85770d3afb Merge pull request #1553 from switch87/fix/1552-type42-em
EPS/PS import: Type 42 fonts set type at their size
2026-10-13 08:27:05 +07:00
BFlat 421443b8ac Merge pull request #1551 from switch87/fix/1550-pdf-alpha-softmask-images
PDF import: images in an alpha soft mask count by their alpha
2026-10-13 08:26:49 +07:00
Gert Pellin e2a63a8baf EPS/PS import: Type 42 fonts set type at their size
Interp::font took a font's FontMatrix times 1000 as the matrix of one
em, the Type 1 convention (glyphs in 1000-unit space). A Type 42 font's
glyph space is one unit an em, its FontMatrix usually the identity, so
type in Type 42 fonts (cairo, Inkscape, LibreOffice) came in 1000 times
too large and off the page. FontType 42 now takes the matrix as it is.

Fixes #1552
2026-10-13 08:11:27 +07:00
bflatastic 44d170f059 PDF import: pattern swatches read inside an alpha mask keep their images; a luminosity mask test 2026-10-13 08:11:27 +07:00
Gert Pellin fce90e3144 PDF import: images in an alpha soft mask count by their alpha
An alpha soft mask's vector art is whitened so its luminance is its
alpha, but images kept their colour: cairo (and so Inkscape) draws a
mask as an image whose alpha is in its own SMask, often black pixels,
so the mask came in empty. While reading an alpha mask's art the
importer now reads images white with their own alpha (under their own
cache key, without the JPEG and CMYK shortcuts), and stencil masks
white; a luminosity mask nested inside reads them as they are.

Fixes #1550
2026-10-13 08:11:27 +07:00
bflatastic 761cc2371a Image Trace: Ignore Color, and the Auto Color and Outline presets
Against the documented panel (https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/image-trace-panel-options.html):

- Ignore Color: "Ignores a specific color while tracing. Use the color picker to selectively ignore a color."
  The panel's Ignore White is now Ignore Color with a colour button beside it (white until another colour is
  picked); `imageTrace.make` takes `ignoreColor: "#rrggbb"` beside `ignoreWhite`, and leaves out the palette
  colours within about 30 levels of it. Documents saved before have no such setting and ignore white as they
  did. Flat Logo mode leaves out white only.
- Auto Color ("Creates a posterized image from a photo or artwork"): Color mode with the Automatic palette.
- Outline ("Simplifies the image to black outlines"): Black and White with Create: Strokes and no fills.
- The Outline preset's name is translated as a preset's name, not as the Outline view's.

Measured (docs/image-trace-quality.md): Auto Color on flat art is 0.140 px off on average with 4.0 paths (16
Colors: 0.141, 4.2). Outline on the line inputs, the first measure of Create: Strokes: 0.310 px off on average,
1.84 at worst, 81 anchors for 5 lines (Line Art's filled outlines: 0.055, 0.96, 46).
2026-10-13 08:04:32 +07:00
BFlat 3752b3ec31 Merge pull request #1563 from storytold/u/trace-thin
Image Trace: thin lines in blurred and scanned art keep smooth edges
2026-10-13 07:50:58 +07:00
72 changed files with 3013 additions and 489 deletions

No files matched your search

Generated
-1
View File
@@ -4916,7 +4916,6 @@ dependencies = [
"serde_json",
"thiserror 2.0.21",
"vectorcraft-geom",
"vectorcraft-pathops",
]
[[package]]
+8
View File
@@ -1465,6 +1465,14 @@ impl Document {
pub fn new_graphic_style_name(&self) -> String {
(self.graphic_styles.len() + 1..).map(|i| format!("Graphic Style {i}")).find(|n| self.graphic_style(n).is_none()).unwrap_or_default()
}
/// `base`, or the first free "`base` N" (N from 2) when a symbol already has that name.
pub fn unique_symbol_name(&self, base: &str) -> String {
let taken = |n: &str| self.symbols.iter().any(|s| s.name == n);
if !taken(base) {
return base.to_string();
}
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
}
}
fn default_graphic_styles() -> Vec<GraphicStyle> {
+10 -2
View File
@@ -1,6 +1,7 @@
//! Distort & Transform effects.
use serde_json::Value;
use vectorcraft_geom::kurbo::ParamCurveArclen;
use vectorcraft_geom::{Affine, Anchor, ParamCurve, PathData, Point, Rect, SubPath, Vec2};
use crate::util::*;
@@ -194,7 +195,10 @@ pub fn twist(path: &PathData, b: Rect, angle: f64) -> PathData {
map_nonlinear(path, pieces, f)
}
/// Zig Zag: `ridges` extra points per segment, alternately displaced along the normal.
const ZIGZAG_ARCLEN_ACCURACY: f64 = 1e-4;
/// Zig Zag: `ridges` extra points per segment, evenly spaced along its length and alternately
/// displaced along the normal.
pub fn zig_zag(path: &PathData, b: Rect, p: &Value) -> PathData {
let size = num(p, "size", 10.0).clamp(-1e4, 1e4);
let amp = if flag(p, "relative", false) { size / 100.0 * mean_size(b) } else { size };
@@ -207,8 +211,12 @@ pub fn zig_zag(path: &PathData, b: Rect, p: &Value) -> PathData {
let mut k = 0usize;
for i in 0..segs {
let c = sp.segment(i);
// Ridges are spaced by length, not by `t`: a line's retracted handles bunch `t` up
// near the anchors, and so does any uneven curve.
let len = c.arclen(ZIGZAG_ARCLEN_ACCURACY);
for j in 0..=ridges {
let t = j as f64 / (ridges + 1) as f64;
let s = j as f64 / (ridges + 1) as f64;
let t = if j > 0 && len > 1e-9 { c.inv_arclen(s * len, ZIGZAG_ARCLEN_ACCURACY).clamp(0.0, 1.0) } else { s };
let nrm = if j == 0 {
// Average the normals of the segments meeting at the anchor.
let prev = if i > 0 {
+56
View File
@@ -244,6 +244,62 @@ fn zigzag_ridge_count_and_amplitude() {
assert!(b.width() > 100.0 && b.width() <= 120.0 + 1e-6, "{b:?}");
}
#[test]
fn zigzag_spaces_ridges_evenly_along_a_straight_line() {
// #1554: on a straight line every ridge is the same distance from the line and as far from
// the next one, whether the points are corners or smooth.
let line = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(0.0, 500.0)], false));
for points in ["corner", "smooth"] {
let out = run("distort.zigZag", json!({"ridges": 4, "points": points, "size": 20}), &line);
let ps: Vec<Point> = out.subpaths[0].anchors.iter().map(|a| a.p).collect();
assert_eq!(ps.len(), 6, "{points}");
for (k, q) in ps.iter().enumerate() {
assert!(close(q.x.abs(), 20.0, 1e-6), "{points} {k}: {q:?}");
assert!(close(q.y, 100.0 * k as f64, 1e-3), "{points} {k}: {q:?}");
}
}
}
/// #1554 on curves: a circle's ridges sit at equal angles, at the radius plus and minus the size in
/// turn all the way round.
#[test]
fn zigzag_on_a_circle_is_regular() {
let circle = shapes::ellipse(Rect::new(-50.0, -50.0, 50.0, 50.0));
let out = run("distort.zigZag", json!({"ridges": 3, "points": "corner", "size": 10}), &circle);
let pts: Vec<Point> = out.subpaths[0].anchors.iter().map(|a| a.p).collect();
assert_eq!(pts.len(), 16);
let start = pts[0].y.atan2(pts[0].x);
let mut radii = vec![];
for (i, q) in pts.iter().enumerate() {
let turn = (q.y.atan2(q.x) - start).to_degrees().abs() / 22.5;
assert!(close(turn, turn.round(), 0.02), "point {i} at {turn} sixteenths");
radii.push(q.to_vec2().hypot().round());
}
assert!(radii.iter().all(|r| *r == 60.0 || *r == 40.0) && (0..16).all(|i| radii[i] != radii[(i + 1) % 16]), "{radii:?}");
}
/// #1554 on an uneven curve: the ridges divide its length, not its parameter, into equal parts.
#[test]
fn zigzag_spaces_ridges_by_length_on_an_uneven_curve() {
use vectorcraft_geom::kurbo::{ParamCurve, ParamCurveArclen, ParamCurveNearest};
let (a, b) = (Point::new(0.0, 0.0), Point::new(300.0, 0.0));
let curve = PathData::single(SubPath::new(
vec![
vectorcraft_geom::Anchor::with_handles(a, a, Point::new(10.0, 40.0)),
vectorcraft_geom::Anchor::with_handles(b, Point::new(100.0, 200.0), b),
],
false,
));
let c = curve.subpaths[0].segment(0);
let len = c.arclen(1e-6);
let out = run("distort.zigZag", json!({"ridges": 3, "points": "corner", "size": 0}), &curve);
let along: Vec<f64> = out.subpaths[0].anchors.iter().map(|q| c.subsegment(0.0..c.nearest(q.p, 1e-9).t).arclen(1e-6) / len).collect();
assert_eq!(along.len(), 5);
for (k, share) in along.iter().enumerate() {
assert!(close(*share, k as f64 / 4.0, 1e-3), "{along:?}");
}
}
#[test]
fn offset_path_grows_and_shrinks() {
let grow = run("path.offsetPath", json!({"offset": 10, "joins": "miter"}), &square()).bounds().unwrap();
+2 -10
View File
@@ -526,14 +526,6 @@ fn normalize(mut art: Node) -> Result<(Node, (f64, f64), Point)> {
Ok((art, (w, h), c))
}
fn unique_symbol_name(d: &Document, base: &str) -> String {
let taken = |n: &str| d.symbols.iter().any(|s| s.name == n);
if !taken(base) {
return base.to_string();
}
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
}
fn instances_of(d: &Document, name: Option<&str>) -> Vec<NodeId> {
let mut v = vec![];
d.walk(|n| {
@@ -587,7 +579,7 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
let ids = roots(s, p)?;
let art = selection_art(s, p)?.ok_or_else(|| EngineError::Other("nothing selected".into()))?;
let (art, size, c) = normalize(art)?;
let name = unique_symbol_name(&s.doc()?.doc, str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol"));
let name = s.doc()?.doc.unique_symbol_name(str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol"));
let id = s.edit("New Symbol", |d, sel| {
let art = d.reid(&art);
d.symbols.push(Symbol { name: name.clone(), art: Arc::new(art) });
@@ -764,7 +756,7 @@ fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let name = name_param(p, C)?.to_string();
let d = &s.doc()?.doc;
let sym = d.symbols.iter().find(|x| x.name == name).cloned().ok_or_else(|| bad(C, format!("no symbol named `{name}`")))?;
let new = unique_symbol_name(d, str_param(p, "newName").unwrap_or(&format!("{name} copy")));
let new = d.unique_symbol_name(str_param(p, "newName").unwrap_or(&format!("{name} copy")));
let size = natural_size(d, &name);
s.edit("Duplicate Symbol", |d, _| {
let art = d.reid(&sym.art);
+71 -3
View File
@@ -15,7 +15,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::appearance::{AppearanceItem, FillLayer, LineCap, LineJoin, StrokeLayer};
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection, TraceView};
use vectorcraft_doc::{Appearance, Document, LiveShape, Node, NodeId, NodeKind, Selection, TraceView};
use vectorcraft_geom::{FillRule, PathData, Point, Rect, SubPath};
use vectorcraft_pathops as po;
use vectorcraft_tools::builder::{
@@ -120,7 +120,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Make",
["Object", "Image Trace"],
None,
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite, snapCurvesToLines, fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors}",
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors}",
has_selection_or_ids,
|s, p| trace_make(s, p, false)
),
@@ -703,6 +703,66 @@ fn trace_library(s: &Session, params: &mut tr::TraceParams) -> Result<Vec<([u8;
/// refused with advice instead (#525).
pub const MAX_TRACE_ANCHORS: usize = 2_000_000;
/// The live shape for a shape the tracer found (in the image's pixels), placed in the document by `xf`.
fn traced_shape(shape: tr::TracedShape, xf: vectorcraft_geom::Affine) -> LiveShape {
use vectorcraft_geom::Affine;
let about = |c: Point, deg: f64| Affine::translate(c.to_vec2()) * Affine::rotate(deg.to_radians());
let mut live = match shape {
tr::TracedShape::Rectangle { center, width, height, radius, rotation } => LiveShape::Rectangle {
w: width,
h: height,
radii: [radius; 4],
kinds: Default::default(),
xf: about(center, rotation) * Affine::translate((-width / 2.0, -height / 2.0)),
},
tr::TracedShape::Ellipse { center, rx, ry, rotation } => {
LiveShape::Ellipse { w: 2.0 * rx, h: 2.0 * ry, pie: (0.0, 360.0), xf: about(center, rotation) * Affine::translate((-rx, -ry)) }
}
tr::TracedShape::Polygon { center, radius, sides, rotation } => {
LiveShape::Polygon { radius, sides, xf: about(center, rotation), radii: vec![], kinds: vec![] }
}
};
// (Whether the move changed the shape's own numbers does not matter: its path is made from them next.)
let _ = live.transform(xf);
live
}
/// Auto Grouping: `nodes` (one per traced path, bottom-most first) with each path that has others
/// inside it (`parents`, as the tracer gives them) in a group with them: the path, then what is
/// inside it. Without parents, the nodes as they are.
fn grouped(d: &mut Document, nodes: Vec<Node>, parents: &[Option<usize>]) -> Vec<Arc<Node>> {
let n = nodes.len();
let mut inside: Vec<Vec<usize>> = vec![vec![]; n];
let mut roots = vec![];
for i in 0..n {
match parents.get(i).copied().flatten().and_then(|p| inside.get_mut(p).filter(|_| p != i)) {
Some(of) => of.push(i),
None => roots.push(i),
}
}
// Outermost first, so that backwards each path comes after those inside it.
let mut order = roots.clone();
let mut at = 0;
while let Some(&i) = order.get(at) {
order.extend(inside.get(i).into_iter().flatten());
at += 1;
}
let mut nodes: Vec<Option<Node>> = nodes.into_iter().map(Some).collect();
let mut built: Vec<Option<Arc<Node>>> = vec![None; n];
for &i in order.iter().rev() {
let Some(node) = nodes.get_mut(i).and_then(Option::take) else { continue };
let within: Vec<Arc<Node>> = inside.get(i).into_iter().flatten().filter_map(|&c| built.get_mut(c)?.take()).collect();
let made = if within.is_empty() { node } else { Node::group(d.alloc_id(), std::iter::once(Arc::new(node)).chain(within).collect()) };
if let Some(slot) = built.get_mut(i) {
*slot = Some(Arc::new(made));
}
}
// (Paths that are inside each other in a ring are reached from no root: the tracer makes none,
// and they are kept all the same.)
let ring = nodes.into_iter().flatten().map(Arc::new).collect::<Vec<_>>();
roots.iter().filter_map(|&r| built.get_mut(r)?.take()).chain(ring).collect()
}
fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
const C: &str = "imageTrace.make";
let mut params = trace_params(p)?;
@@ -763,9 +823,16 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
hidden.visible = false;
children.push(Arc::new(hidden));
}
let mut traced = Vec::with_capacity(res.paths.len());
for tp in res.paths {
let path = tp.path.transformed(xf);
let mut n = shape_node(d, path, None);
// Shapes: a path the tracer found to be a shape is that live shape.
if let (Some(shape), NodeKind::Path { path, live, .. }) = (tp.shape, &mut n.kind) {
let shape = traced_shape(shape, xf);
*path = shape.to_path();
*live = Some(Box::new(shape));
}
// A library colour fills with the swatch's own colour (its model kept).
let color = library.iter().find(|(rgb, _)| *rgb == tp.color).map_or_else(|| Color::rgb8(tp.color[0], tp.color[1], tp.color[2]), |l| l.1);
let paint = Paint::solid(color);
@@ -782,8 +849,9 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
.into(),
None => fill_only(paint).into(),
};
children.push(Arc::new(n));
traced.push(n);
}
children.extend(grouped(d, traced, &res.parents));
let gid = d.alloc_id();
let mut g = Node::group(gid, children);
g.name = if expand { None } else { Some(TRACE_NAME.into()) };
+4 -2
View File
@@ -489,8 +489,10 @@ fn release_guides(s: &mut Session, _: &Value) -> Result<Value> {
fn lock_guides(s: &mut Session, p: &Value) -> Result<Value> {
let v = p.get("locked").and_then(Value::as_bool).unwrap_or(!s.menu.guides_locked);
s.menu.guides_locked = v;
// Locked guides can't stay selected.
if v && !s.doc()?.selection.guides.is_empty() {
// Locked guides can't stay selected, ruler guides or guide paths ([`Session::select`] lets
// those go).
let st = s.doc()?;
if v && (!st.selection.guides.is_empty() || st.selection.objects.iter().any(|id| crate::is_guide_path(&st.doc, *id))) {
s.select(|_, sel| sel.guides.clear())?;
}
Ok(json!({ "locked": v }))
+9 -1
View File
@@ -132,7 +132,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection_or_guides,
clear
),
cmd!("edit.duplicate", "Duplicate", [], None, "{dx?, dy?} duplicate the selection in place (offset optional)", has_selection, duplicate),
cmd!(
"edit.duplicate",
"Duplicate",
["Edit"],
None,
"{dx?, dy?} duplicate the selection in place (offset optional)",
has_selection,
duplicate
),
]
}
+58 -2
View File
@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
use serde_json::{Value, json};
use vectorcraft_doc::{NodeId, NodeKind, Scaling};
use vectorcraft_geom::{AnchorKind, PathData, Point, SubPath, Vec2};
use vectorcraft_geom::{Anchor, AnchorKind, PathData, Point, SubPath, Vec2};
use super::create::anchor_from_json;
use super::pathops::Beside;
@@ -64,7 +64,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Join",
["Object", "Path"],
Some("Cmd+J"),
"{ids?: [path, path] (default: the selected paths), ends?: [\"first\"|\"last\", \"first\"|\"last\"] (which end of each path to join, as the Pen joins them; default: the nearest pair)} join two open paths into one (the first keeps its id, selected), or close a single open path",
"{ids?: [path, path] (default: the selected paths), ends?: [\"first\"|\"last\", \"first\"|\"last\"] (which end of each path to join, as the Pen joins them; default: the nearest pair)} join two open paths into one (the first keeps its id, selected), or close a single open path; without ids, direct-selected anchors lying on one another along a path merge into one anchor instead",
has_selection,
join
),
@@ -300,6 +300,37 @@ fn end_points(sp: &SubPath) -> Option<(Point, Point)> {
Some((sp.anchors.first()?.p, sp.anchors.last()?.p))
}
/// Merge each run of `picked` neighbours of `sp` that lie on one another into one anchor, which
/// keeps the first one's incoming handle and the last one's outgoing handle. Returns how many
/// anchors went.
fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool) -> usize {
let same = |a: &Anchor, b: &Anchor| a.p.distance(b.p) < 1e-6;
let before = sp.anchors.len();
// The closing pair of a closed path first: taking the last anchor moves no other index.
if sp.closed
&& before > 2
&& picked(0)
&& picked(before - 1)
&& let [first, .., last] = sp.anchors.as_mut_slice()
&& same(first, last)
{
first.h_in = last.h_in;
sp.anchors.pop();
}
// Back to front, so the indices `picked` knows stay true for the anchors still to visit.
for i in (1..sp.anchors.len()).rev() {
if picked(i - 1)
&& picked(i)
&& let Some([a, b]) = sp.anchors.get_mut(i - 1..=i)
&& same(a, b)
{
a.h_out = b.h_out;
sp.anchors.remove(i);
}
}
before - sp.anchors.len()
}
fn join(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.join";
let ids = match ids_param(p, "ids") {
@@ -312,6 +343,31 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
}
None => selected_paths(s)?,
};
// Direct-selected anchors lying on one another along a path (two corners brought together by
// Average) become one anchor; the paths themselves are joined only when there are none.
if p.get("ids").is_none() {
let st = s.doc()?;
let mut merged = vec![];
for (id, picked) in &st.selection.anchors {
let Some(mut path) = st.doc.node(*id).and_then(|n| n.path_data()).cloned() else { continue };
let n: usize = path.subpaths.iter_mut().enumerate().map(|(si, sp)| merge_coincident(sp, |ai| picked.contains(&(si, ai)))).sum();
if n > 0 {
merged.push((*id, path));
}
}
if !merged.is_empty() {
let objects = st.selection.objects.clone();
s.edit("Join", |d, sel| {
for (id, path) in merged {
*path_mut(d, id)? = path;
}
// The anchor indices changed: the paths stay selected, whole.
sel.set(objects);
Ok(())
})?;
return ok();
}
}
// The ends to join, as (reverse the first path, reverse the second): the first path's end
// must come last, the second's first.
let ends = match p.get("ends") {
+13
View File
@@ -578,6 +578,11 @@ fn strip_replay_only(id: &str, p: &mut Value) {
}
}
/// Is `id` a path made into a guide (View → Guides → Make Guides)?
pub(crate) fn is_guide_path(d: &Document, id: NodeId) -> bool {
d.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Path { guide: true, .. }))
}
/// Cheap sanity check after an edit: artboards and the objects just touched (the selection) must
/// have finite, in-range geometry, so saved files always reload and renderers never see NaN/∞.
fn doc_sane(d: &Document, sel: &Selection) -> bool {
@@ -1482,10 +1487,18 @@ impl Session {
/// Change only the selection (not an undo step).
pub fn select(&mut self, f: impl FnOnce(&Document, &mut Selection)) -> Result<()> {
self.active_appearance_item = None;
let guides_locked = self.menu.guides_locked;
let st = self.doc_mut()?;
let before = st.selection.objects.clone();
f(&st.doc, &mut st.selection);
st.selection.prune(&st.doc);
// Lock Guides locks the paths made into guides too (#1534), whatever selects them.
if guides_locked {
let locked: Vec<NodeId> = st.selection.objects.iter().copied().filter(|id| is_guide_path(&st.doc, *id)).collect();
for id in locked {
st.selection.remove(id);
}
}
// Selecting art makes its layer (or sublayer) the current one, as in the Layers panel of
// the reference app: new art then goes beside it.
if st.selection.objects != before
+77 -1
View File
@@ -671,6 +671,81 @@ fn image_trace_make_places_paths_over_the_image() {
assert_eq!(paths[0].appearance.fill_paint(), Paint::solid(Color::rgb8(0, 0, 0)));
}
/// Shapes: the disc of the test image comes out as a live ellipse where the image lies, the same
/// circle as its traced path; without the option it is a plain path.
#[test]
fn image_trace_shapes_makes_live_shapes() {
let mut s = session();
image_doc(&mut s);
let r = s.execute("imageTrace.make", &json!({"params": {"ignoreWhite": true, "shapes": true}})).unwrap();
let n = node(&s, NodeId(r["id"].as_u64().unwrap()));
let paths = traced_paths(&n);
assert_eq!(paths.len(), 1);
let NodeKind::Path { path, live: Some(live), .. } = &paths[0].kind else { panic!("not a live shape: {:?}", paths[0].kind) };
let vectorcraft_doc::LiveShape::Ellipse { w, h, .. } = **live else { panic!("{live:?}") };
// A circle of r = 30 px, drawn twice the size.
assert!(w == h && (w - 60.0).abs() < 0.75, "{w} × {h}");
let bb = path.bounds().unwrap();
assert!((bb.center().x - 150.0).abs() < 1.5 && (bb.center().y - 150.0).abs() < 1.5 && (bb.width() - 120.0).abs() < 1.5, "{bb:?}");
assert_eq!(*path, live.to_path());
assert_eq!(paths[0].appearance.fill_paint(), Paint::solid(Color::rgb8(0, 0, 0)));
// Off, as documents saved before have it: a path.
let mut s = session();
image_doc(&mut s);
let r = s.execute("imageTrace.make", &json!({"params": {"ignoreWhite": true}})).unwrap();
let n = node(&s, NodeId(r["id"].as_u64().unwrap()));
assert!(matches!(traced_paths(&n)[0].kind, NodeKind::Path { live: None, .. }));
}
/// Auto Grouping: a ring and the dot in its hole are a group (the ring first), the square beside
/// them is not in it; the picture is the same as without, and the group traces again, expands
/// and releases as any other.
#[test]
fn image_trace_auto_grouping_groups_what_is_inside() {
let art = vectorcraft_trace::Raster::from_fn(120, 80, |x, y| {
let d = (x as f64 + 0.5 - 40.0).hypot(y as f64 + 0.5 - 40.0);
if d <= 6.0 || (90..110).contains(&x) && (30..50).contains(&y) {
[200, 0, 0, 255]
} else if (16.0..=30.0).contains(&d) {
[0, 0, 160, 255]
} else {
[255, 255, 255, 255]
}
});
let make = |grouping: bool| {
let mut s = session();
add_image(&mut s, &art, Affine::IDENTITY);
let r = s.execute("imageTrace.make", &json!({"preset": "6 Colors", "params": {"ignoreWhite": true, "autoGrouping": grouping}})).unwrap();
assert_eq!(r["paths"], 3);
let g = NodeId(r["id"].as_u64().unwrap());
(s, g)
};
let (mut s, g) = make(true);
let items = traced_paths(&node(&s, g));
assert_eq!(items.len(), 2);
let group = items.iter().find(|n| matches!(n.kind, NodeKind::Group { .. })).expect("a group");
let inside = group.children().unwrap();
assert_eq!(inside.len(), 2);
assert_eq!(inside[0].appearance.fill_paint(), Paint::solid(Color::rgb8(0, 0, 160)));
assert_eq!(inside[1].appearance.fill_paint(), Paint::solid(Color::rgb8(200, 0, 0)));
assert!(matches!(items.iter().find(|n| n.id != group.id).unwrap().kind, NodeKind::Path { .. }));
// The same picture as without.
let (flat, fg) = make(false);
assert_eq!(traced_paths(&node(&flat, fg)).len(), 3);
let shot = |s: &Session| {
let img = render_doc(s, true);
(0..120).flat_map(|x| (0..80).map(move |y| (x, y))).map(|(x, y)| img.pixel(x, y)).collect::<Vec<_>>()
};
assert_eq!(shot(&s), shot(&flat));
// Traced again without the option, it is flat again; expanded, the groups stay.
let r = s.execute("imageTrace.make", &json!({"preset": "6 Colors", "params": {"ignoreWhite": true}})).unwrap();
assert_eq!(traced_paths(&node(&s, NodeId(r["id"].as_u64().unwrap()))).len(), 3);
s.execute("imageTrace.make", &json!({"preset": "6 Colors", "params": {"ignoreWhite": true, "autoGrouping": true}})).unwrap();
let r = s.execute("imageTrace.expand", &json!({})).unwrap();
let expanded = node(&s, NodeId(r["ids"][0].as_u64().unwrap()));
assert_eq!(expanded.children().unwrap().len(), 2);
}
#[test]
fn image_trace_release_and_expand() {
let mut s = session();
@@ -795,7 +870,8 @@ fn image_trace_presets_query_and_errors() {
let mut s = session();
let r = s.execute("imageTrace.presets", &json!({})).unwrap();
let names: Vec<&str> = r["presets"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap()).collect();
assert_eq!(names.len(), 13);
assert_eq!(names.len(), 15);
assert!(names.contains(&"Auto Color") && names.contains(&"Outline"));
assert!(names.contains(&"Technical Drawing") && names.contains(&"Shades of Gray") && names.contains(&"Flat Logo"));
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
assert!(s.execute("imageTrace.make", &json!({"id": a.0})).is_err(), "not an image");
+25
View File
@@ -1164,6 +1164,31 @@ fn the_pen_continues_any_open_path_and_joins_another() {
assert_eq!(pts(&s, a), [(400.0, 200.0), (300.0, 200.0), (200.0, 100.0), (100.0, 100.0), (50.0, 50.0)]);
}
/// #1533: two corners of a rectangle brought together by Average become one anchor on Join, in
/// one undo step; anchors that aren't on one another leave Join as it was.
#[test]
fn join_merges_the_selected_anchors_average_brought_together() {
let mut s = session();
let r = rect(&mut s, 100.0, 100.0, 100.0, 80.0);
let pts = |s: &Session| path(s, r).subpaths[0].anchors.iter().map(|x| (x.p.x, x.p.y)).collect::<Vec<_>>();
s.execute("select.anchors", &json!({"id": r.0, "anchors": [[0, 0], [0, 1]], "mode": "set"})).unwrap();
assert!(s.execute("path.join", &json!({})).is_err(), "apart: nothing to merge, and the path is closed");
s.execute("path.average", &json!({"axis": "both"})).unwrap();
s.execute("path.join", &json!({})).unwrap();
assert_eq!(pts(&s), [(150.0, 100.0), (200.0, 180.0), (100.0, 180.0)]);
assert!(path(&s, r).subpaths[0].closed);
let sel = &s.doc().unwrap().selection;
assert!(sel.objects == [r] && sel.anchors.is_empty());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(pts(&s).len(), 4);
// The closing pair: the first and last anchors of a closed path.
s.execute("select.anchors", &json!({"id": r.0, "anchors": [[0, 0], [0, 3]], "mode": "set"})).unwrap();
s.execute("path.average", &json!({"axis": "both"})).unwrap();
let before = pts(&s);
s.execute("path.join", &json!({})).unwrap();
assert_eq!(pts(&s), before[..3]);
}
/// `path.join {ids, ends}` joins the ends asked for, not the nearest pair; bad requests are errors.
#[test]
fn join_takes_the_paths_and_the_ends_to_join() {
+20
View File
@@ -1249,6 +1249,26 @@ fn ruler_guides_and_lock() {
assert!(s.execute("guide.remove", &json!({"index": 0})).is_err());
}
/// #1534: Lock Guides locks the paths made into guides too: they can't be selected (so neither
/// moved, reshaped nor deleted), by a click, a marquee or Select All, and locking lets them go.
#[test]
fn locked_guides_lock_guide_paths() {
let mut s = session();
let (a, b) = (rect(&mut s, 0.0, 0.0, 10.0, 10.0), rect(&mut s, 20.0, 20.0, 10.0, 10.0));
sel(&mut s, &[a]);
s.execute("view.guides.make", &json!({})).unwrap();
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
sel(&mut s, &[a, b]);
assert_eq!(selected(&s), vec![b]);
s.execute("select.all", &json!({})).unwrap();
assert_eq!(selected(&s), vec![b]);
s.execute("view.guides.lock", &json!({"locked": false})).unwrap();
sel(&mut s, &[a, b]);
assert_eq!(selected(&s), vec![a, b]);
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
assert_eq!(selected(&s), vec![b]);
}
fn guides(s: &Session) -> Vec<(bool, f64)> {
s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos)).collect()
}
+3 -3
View File
@@ -366,9 +366,9 @@ impl Out {
/// A new clip of `path` inside `clips`; `None` for a rectangle around the whole page, which
/// clips nothing.
fn clip(&mut self, path: BezPath, rule: FillRule) -> Option<Arc<Clip>> {
let lines = path.elements().iter().all(|e| !matches!(e, PathEl::QuadTo(..) | PathEl::CurveTo(..)));
let around = path.bounding_box().inflate(0.5, 0.5);
if lines && path.elements().len() <= 6 && around.contains(self.frame.origin()) && around.contains(Point::new(self.frame.x1, self.frame.y1)) {
// Only a polygon that really covers the page: a diamond touching its edges has the page as
// its bounding box but cuts its corners away (#1542).
if path.elements().len() <= 6 && vectorcraft_geom::hit::polygon_covers_rect(&path, self.frame, 0.5) {
return None;
}
self.next_clip += 1;
+22
View File
@@ -248,6 +248,12 @@ fn clips_become_clipping_groups() {
// eoclip of a path.
let o = objects(&read("10 10 moveto 30 10 lineto 30 30 lineto closepath eoclip newpath 0 0 50 50 rectfill").document);
assert!(matches!(o[0].kind, NodeKind::Group { clip: true, .. }));
// #1542: a diamond touching the page edges has the page as its bounding box but still clips.
let o = objects(&read("50 0 moveto 100 50 lineto 50 100 lineto 0 50 lineto closepath clip newpath 0 0 100 100 rectfill").document);
assert!(matches!(o[0].kind, NodeKind::Group { clip: true, .. }), "{:?}", o[0].kind);
// A turned square around the whole page clips nothing.
let o = objects(&read("50 -60 moveto 160 50 lineto 50 160 lineto -60 50 lineto closepath clip newpath 0 0 100 100 rectfill").document);
assert!(matches!(o[0].kind, NodeKind::Path { .. }), "{:?}", o[0].kind);
}
#[test]
@@ -467,6 +473,22 @@ fn the_bounding_box_comes_from_the_header_or_the_trailer() {
assert!(r.warnings.iter().any(|w| w.contains("first of the file's 2 pages")), "{:?}", r.warnings);
}
/// #1552: a Type 42 font's glyph space is one unit an em, so its identity FontMatrix scaled to 12
/// sets 12 pt type (a Type 1's 0.001 matrix needs the 1000-unit space).
#[test]
fn type_42_fonts_set_type_at_their_size() {
for (kind, matrix) in [(42, "[1 0 0 1 0 0]"), (1, "[0.001 0 0 0.001 0 0]")] {
let body = format!(
"/T 10 dict begin /FontName /T def /FontType {kind} def /FontMatrix {matrix} def /Encoding StandardEncoding def currentdict end /T exch definefont pop \
/T findfont 12 scalefont setfont 10 20 moveto (Hi) show"
);
let r = read(&body);
let NodeKind::Text(t) = &objects(&r.document)[0].kind else { panic!("{:?}", r.warnings) };
let size = t.runs[0].style.size * t.xf.determinant().abs().sqrt();
assert!((size - 12.0).abs() < 1e-6, "FontType {kind}: {size} pt");
}
}
#[test]
fn embedded_font_programs_are_skipped() {
let body = "/MyFont 10 dict begin /FontName /MyFont def /FontMatrix [0.001 0 0 0.001 0 0] def currentdict end currentfile eexec\n\
+4 -1
View File
@@ -403,7 +403,10 @@ impl Interp<'_> {
let f = font.borrow();
let name = f.get(&Key::name("FontName")).and_then(Obj::text).map_or_else(|| DEFAULT_FONT.to_string(), |n| n.to_string());
let m = f.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
Ok((name, m * Affine::scale(1000.0)))
// Type 1 glyphs are in 1000-unit space; a Type 42 font's glyph space is one unit an em
// (its FontMatrix, usually the identity, is already the em's).
let units = if f.get(&Key::name("FontType")).and_then(Obj::as_num) == Some(42.0) { 1.0 } else { 1000.0 };
Ok((name, m * Affine::scale(units)))
}
/// `s` (read as Latin-1) set in the current font at user point `at`: point type placed there,
+67
View File
@@ -6,6 +6,47 @@ use kurbo::{BezPath, ParamCurveNearest, PathEl, Point, Rect, Shape};
use crate::path::{FillRule, PathData};
/// Does the filled area of `path` cover all of `r` shrunk by `tol` on every side? Answered for one
/// convex polygon (a single subpath of straight segments), which covers the rect when it contains
/// its four corners; any other path gives `false`, so a caller treats it as not covering.
pub fn polygon_covers_rect(path: &BezPath, r: Rect, tol: f64) -> bool {
let mut pts: Vec<Point> = Vec::new();
for (i, el) in path.elements().iter().enumerate() {
match *el {
PathEl::MoveTo(p) if i == 0 => pts.push(p),
PathEl::LineTo(p) if i > 0 => pts.push(p),
PathEl::ClosePath if i > 0 => {}
_ => return false,
}
}
if pts.len() > 1 && pts.first() == pts.last() {
pts.pop();
}
if pts.len() < 3 {
return false;
}
// Convex: every turn goes the same way (straight runs allowed).
let n = pts.len();
let mut sign = 0.0f64;
for i in 0..n {
let (a, b, c) = (pts[i], pts[(i + 1) % n], pts[(i + 2) % n]);
let cross = (b - a).cross(c - b);
if cross.abs() <= 1e-9 {
continue;
}
if sign == 0.0 {
sign = cross.signum();
} else if cross.signum() != sign {
return false;
}
}
let inner = r.inflate(-tol, -tol);
sign != 0.0
&& [Point::new(inner.x0, inner.y0), Point::new(inner.x1, inner.y0), Point::new(inner.x1, inner.y1), Point::new(inner.x0, inner.y1)]
.into_iter()
.all(|c| fill_contains(path, FillRule::NonZero, c))
}
/// Is `p` inside the filled area of `path` under `rule`? Open subpaths are implicitly closed (as when filled).
pub fn fill_contains(path: &BezPath, rule: FillRule, p: Point) -> bool {
let w = closed(path).winding(p);
@@ -94,6 +135,32 @@ mod tests {
use super::*;
use crate::shapes;
#[test]
fn polygon_covers_rect_only_for_convex_polygons_around_it() {
let page = Rect::new(0.0, 0.0, 100.0, 100.0);
let poly = |pts: &[(f64, f64)]| {
let mut b = BezPath::new();
b.move_to(pts[0]);
for &p in &pts[1..] {
b.line_to(p);
}
b.close_path();
b
};
assert!(polygon_covers_rect(&poly(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]), page, 0.01));
assert!(polygon_covers_rect(&poly(&[(50.0, -60.0), (160.0, 50.0), (50.0, 160.0), (-60.0, 50.0)]), page, 0.01), "turned, around it");
// The page's bounding box, but its corners are cut away.
assert!(!polygon_covers_rect(&poly(&[(50.0, 0.0), (100.0, 50.0), (50.0, 100.0), (0.0, 50.0)]), page, 0.01));
// Contains the corners but notches into the page: not convex.
assert!(!polygon_covers_rect(&poly(&[(-1.0, -1.0), (101.0, -1.0), (101.0, 101.0), (50.0, 40.0), (-1.0, 101.0)]), page, 0.01));
// Curves and several subpaths aren't judged.
assert!(!polygon_covers_rect(&kurbo::Circle::new((50.0, 50.0), 200.0).to_path(0.1), page, 0.01));
let mut two = poly(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
two.extend(poly(&[(10.0, 10.0), (20.0, 10.0), (20.0, 20.0)]));
assert!(!polygon_covers_rect(&two, page, 0.01));
assert!(!polygon_covers_rect(&BezPath::new(), page, 0.01));
}
#[test]
fn fill_and_stroke() {
let p = shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)).to_bezpath();
+33 -14
View File
@@ -26,6 +26,8 @@ use crate::import_shading::{clipped, extend_clip, fold_stop_opacity, gradient_sh
use crate::import_text::{Families, LineFacts, Look, Placement, TextLine, Upright};
use crate::{CropTo, ImportOptions, ImportReport, PdfError, TextAs};
/// Mixed into an image's cache key when it is read white inside an alpha soft mask.
const WHITE_KEY: u128 = 0x7717_e000_a1fa;
/// The name of the paths text imports as.
const TEXT_OUTLINES: &str = "<Text Outlines>";
/// A page whose overprint (ISO 32000-1 §8.6.7) is left out rather than given to the wrong objects.
@@ -788,6 +790,10 @@ struct Builder<'p> {
/// A transparency group was just pushed: a form's (whose flags come next) or an image's.
pending: bool,
nested: u32,
/// Reading an alpha soft mask's art: images are read white with their own alpha, so the
/// mask's luminance is their alpha, as whitened vector art's is (cairo, and so Inkscape,
/// rasterizes masks into such images).
white_images: bool,
/// The last filled path: its node and its outline (a stroke of the same outline joins it).
last_fill: Option<(NodeId, BezPath)>,
/// What each line of type made tells beyond its text object ([`crate::import_lines`]).
@@ -930,6 +936,7 @@ impl<'p> Builder<'p> {
marked: vec![],
pending: false,
nested: 0,
white_images: false,
last_fill: None,
lines: HashMap::new(),
}
@@ -1299,7 +1306,10 @@ impl<'p> Builder<'p> {
return s.clone();
}
let page = self.page;
let spec = match self.nested(page, |b| m.interpret(b)) {
let saved = std::mem::replace(&mut self.white_images, m.mask_type() == MaskType::Alpha);
let art = self.nested(page, |b| m.interpret(b));
self.white_images = saved;
let spec = match art {
Some(art) => {
let [r, g, b, _] = m.background_color().to_rgba().components();
let backdrop = luminance(&Color::rgb(r, g, b));
@@ -1330,10 +1340,15 @@ impl<'p> Builder<'p> {
{
return Some(Paint::Pattern { pattern: name.clone(), xf });
}
// A swatch is shared with uses outside an alpha mask, and pattern paints keep their
// colours there: its images are read as they are.
let white = std::mem::take(&mut self.white_images);
let art = self.nested(Rect::ZERO, |b| {
// A cell whose content can't be decoded is empty.
let _ = t.interpret(b, Affine::IDENTITY, stroke);
})?;
});
self.white_images = white;
let art = art?;
let sig = paints(&art);
let entry = self.pattern_keys.entry(key).or_default();
entry.0 += 1;
@@ -1754,14 +1769,12 @@ impl<'a> Device<'a> for Builder<'_> {
(*isolate, *knockout) = (i, k);
}
}
let bb = clip_path.path.bounding_box();
// Clips that contain the whole page do nothing visible; don't create groups for them.
// Clips that contain the whole page do nothing visible; don't create groups for them. Only
// a polygon that really covers it counts: a diamond touching the page edges has the page
// as its bounding box but cuts its corners away (#1542).
let redundant = clip_path.path.elements().len() <= 6
&& bb.x0 <= self.page.x0 + 0.01
&& bb.y0 <= self.page.y0 + 0.01
&& bb.x1 >= self.page.x1 - 0.01
&& bb.y1 >= self.page.y1 - 0.01
&& self.page.area() > 0.0;
&& self.page.area() > 0.0
&& vectorcraft_geom::hit::polygon_covers_rect(&clip_path.path, self.page, 0.01);
let kind = if redundant { FrameKind::Skip } else { FrameKind::Clip(Box::new(self.clip_node(&clip_path.path, fill_rule(clip_path.fill)))) };
self.stack.push(Frame::new(kind));
}
@@ -1834,12 +1847,14 @@ impl<'a> Device<'a> for Builder<'_> {
self.pending = false;
match image {
Image::Raster(r) => {
let key = hayro_interpret::CacheKey::cache_key(&r);
let white = self.white_images;
// Inside an alpha mask the same image is read again, white (its own cache key).
let key = hayro_interpret::CacheKey::cache_key(&r) ^ if white { WHITE_KEY } else { 0 };
let st = r.stream();
let (w, h) = (r.width(), r.height());
// CMYK images keep their samples (read once per image).
let known = self.cmyk_keys.contains(&key);
let cmyk = if known {
let known = !white && self.cmyk_keys.contains(&key);
let cmyk = if known || white {
None
} else {
crate::import_image::cmyk(st, w, h).unwrap_or_else(|why| {
@@ -1858,7 +1873,8 @@ impl<'a> Device<'a> for Builder<'_> {
let dict = st.dict();
let filters = st.filters();
let cs_ok = dict.get::<Name<'_>>(b"ColorSpace").is_some_and(|n| matches!(n.as_ref(), b"DeviceRGB" | b"DeviceGray"));
let jpeg = filters.len() == 1
let jpeg = !white
&& filters.len() == 1
&& matches!(filters[0], hayro_syntax::Filter::DctDecode)
&& cs_ok
&& !dict.contains_key(b"SMask")
@@ -1875,6 +1891,7 @@ impl<'a> Device<'a> for Builder<'_> {
let (w, h, sf) = (img.width(), img.height(), img.scale_factors());
let a = alpha.map(|a| resize_alpha(&a, w, h)).unwrap_or_else(|| vec![255; (w * h) as usize]);
let rgba: Vec<u8> = match img {
_ if white => a.into_iter().flat_map(|a| [255, 255, 255, a]).collect(),
ImageData::Rgb(d) => d.data.as_chunks::<3>().0.iter().zip(a).flat_map(|(c, a)| [c[0], c[1], c[2], a]).collect(),
ImageData::Luma(d) => d.data.iter().zip(a).flat_map(|(g, a)| [*g, *g, *g, a]).collect(),
};
@@ -1890,11 +1907,13 @@ impl<'a> Device<'a> for Builder<'_> {
self.add_image(key, || rgba_png(rgba, w, h).map(|png| (ImageBlob::new("image/png", png), w, h)), xf);
}
Image::Stencil(s) => {
let key = hayro_interpret::CacheKey::cache_key(&s);
let white = self.white_images;
let key = hayro_interpret::CacheKey::cache_key(&s) ^ if white { WHITE_KEY } else { 0 };
let mut decoded: Option<Decoded> = None;
s.with_stencil(
|luma, paint| {
let rgb = match paint {
_ if white => [255, 255, 255, 255],
hayro_interpret::Paint::Color(c) => c.to_rgba().to_rgba8(),
_ => [0, 0, 0, 255],
};
+65
View File
@@ -78,6 +78,58 @@ fn the_mask(d: &Document) -> (Node, OpacityMask) {
(n, m)
}
/// The two pixels of the image a soft mask of kind `s` draws: black, opaque on the left and clear
/// on the right (its own `/SMask`).
fn mask_image_pixels(s: &str) -> [[u8; 4]; 2] {
let g = first_extra(1);
let group = stream(
&format!(
"/Type /XObject /Subtype /Form /BBox [0 0 100 100] /Group << /S /Transparency /CS /DeviceRGB >> /Resources << /XObject << /Im0 {} 0 R >> >>",
g + 1
),
"q 100 0 0 100 0 0 cm /Im0 Do Q",
);
let image = stream(
&format!(
"/Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode /SMask {} 0 R",
g + 2
),
"000000000000>",
);
let smask =
stream("/Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /ASCIIHexDecode", "FF00>");
let bytes = one_page(
"/GS0 gs 1 0 0 rg 0 0 100 100 re f",
&format!("/ExtGState << /GS0 << /SMask << /Type /Mask /S /{s} /G {g} 0 R >> >> >>"),
&[&group, &image, &smask],
);
let d = open(&bytes);
let (_, m) = the_mask(&d);
let mut keys = vec![];
m.art.walk(&mut |n| {
if let NodeKind::Image(im) = &n.kind {
keys.push(im.key.clone());
}
});
let [key] = keys.as_slice() else { panic!("one image in the mask art: {keys:?}") };
let px = image::load_from_memory(&d.images[key].bytes).unwrap().to_rgba8();
[px.get_pixel(0, 0).0, px.get_pixel(1, 0).0]
}
/// An alpha soft mask whose art is an image with its own alpha (some PDF writers rasterize masks
/// so): the image is read white with that alpha, so the mask's luminance is the alpha, not the
/// image's colour (ISO 32000-1 11.5.2: an alpha mask takes the group's alpha, not its colour).
#[test]
fn an_alpha_soft_mask_of_an_image_uses_the_images_alpha() {
assert_eq!(mask_image_pixels("Alpha"), [[255, 255, 255, 255], [255, 255, 255, 0]]);
}
/// A luminosity soft mask takes the group's colour (ISO 32000-1 11.5.3): its image stays black.
#[test]
fn a_luminosity_soft_mask_of_an_image_keeps_the_images_colour() {
assert_eq!(mask_image_pixels("Luminosity"), [[0, 0, 0, 255], [0, 0, 0, 0]]);
}
#[test]
fn a_luminosity_soft_mask_becomes_an_opacity_mask() {
let d = open(&masked("/S /Luminosity", "1 1 1 rg 0 0 50 100 re f"));
@@ -544,3 +596,16 @@ fn glyphs_closed_up_by_half_an_em_stay_one_line() {
assert_eq!(t.len(), 1, "one line: {:?}", t.iter().map(|t| t.plain_text()).collect::<Vec<_>>());
assert_eq!(t[0].plain_text(), "Tight");
}
/// #1542: a clip only counts as covering the page when it does; a diamond whose bounding box is
/// the page still clips, a page rectangle or a turned square around it doesn't make a group.
#[test]
fn clips_with_the_page_as_bounding_box_still_clip() {
let clipped = |clip: &str| {
let d = open(&one_page(&format!("q {clip} W n 1 0.5 0 rg 0 0 100 100 re f Q"), "", &[]));
all(&d).iter().any(|n| matches!(n.kind, NodeKind::Group { clip: true, .. }))
};
assert!(clipped("50 0 m 100 50 l 50 100 l 0 50 l h"), "diamond");
assert!(!clipped("0 0 100 100 re"), "the page");
assert!(!clipped("50 -60 m 160 50 l 50 160 l -60 50 l h"), "a turned square around the page");
}
+4
View File
@@ -274,6 +274,9 @@ fn draw_scaled(ctx: &mut RenderContext, image: std::sync::Arc<vello_cpu::Pixmap>
// On whole pixels at full size the cheapest sampler is exact.
let quality = if k < 1.0 { peniko::ImageQuality::Medium } else { peniko::ImageQuality::Low };
let sampler = peniko::ImageSampler { quality, ..Default::default() };
// The raster maps 1:1 onto the rect: a paint transform left by an earlier fill (a pattern,
// radial or freeform gradient) would resample it (#1536).
ctx.reset_paint_transform();
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
ctx.set_transform(Affine::IDENTITY);
@@ -617,6 +620,7 @@ impl Renderer {
ctx.set_transform(Affine::translate((x, y)));
ctx.set_blend_mode(blend_mode(mode));
let sampler = peniko::ImageSampler { quality: peniko::ImageQuality::Low, ..Default::default() };
ctx.reset_paint_transform();
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
ctx.set_blend_mode(blend_mode(vectorcraft_doc::color::BlendMode::Normal));
+15 -1
View File
@@ -96,6 +96,9 @@ pub struct RenderOptions {
/// Pattern editing mode's tile edge and swatch bounds colour (Object → Pattern → Tile Edge
/// Color), RGB.
pub tile_edge: [u8; 3],
/// Colour and style of the paths made into guides (Preferences › Guides & Grid, as the ruler
/// guides the UI draws): RGB, and whether they are dotted.
pub guide: ([u8; 3], bool),
/// View Opacity Mask (Alt-click the mask thumbnail): instead of the artwork, show this object's
/// opacity mask alone as its coverage in greyscale (white = opaque, black = transparent).
pub mask_view: Option<NodeId>,
@@ -201,6 +204,7 @@ impl Default for RenderOptions {
trim: false,
skip_templates: false,
tile_edge: vectorcraft_doc::LAYER_COLORS[0].1,
guide: ([0x4a, 0xd8, 0xff], false),
mask_view: None,
highlight_substitutions: false,
anti_alias: AntiAlias::Art,
@@ -1158,7 +1162,17 @@ impl Renderer {
NodeKind::Path { path, rule, guide, .. } => {
let bp = path.to_bezpath();
if *guide {
self.hairline(ctx, f, &bp, [0x4a, 0xd8, 0xff, 255]);
let ([r, g, b], dots) = f.opts.guide;
if dots {
let mut screen = bp.clone();
screen.apply_affine(f.view);
ctx.set_transform(Affine::IDENTITY);
ctx.set_stroke(kurbo::Stroke::new(1.5).with_dashes(0.0, [1.5, 2.5]));
ctx.set_paint(f.ink.fixed([r, g, b, 255]));
ctx.stroke_path(&screen);
} else {
self.hairline(ctx, f, &bp, [r, g, b, 255]);
}
} else {
self.draw_shape(ctx, f, n, &bp, *rule);
}
+52
View File
@@ -274,6 +274,58 @@ mod tests {
p[0] > 200 && p[1] > 200 && p[2] > 200
}
/// #1536: a raster effect drawn after a pattern fill is the same as without the pattern (the
/// pattern's paint transform must not resample the effect's raster).
#[test]
fn effects_after_a_pattern_fill_keep_their_place_and_size() {
let fx = |id: &str, params: serde_json::Value| vectorcraft_doc::Effect { id: id.into(), params, visible: true };
for effect in [
fx("blur.gaussian", serde_json::json!({"radius": 4.0})),
fx("stylize.dropShadow", serde_json::json!({"x": 6, "y": 6, "blur": 3, "opacity": 100})),
] {
let make = |with_pattern: bool| {
let mut d = doc_with_pattern(TileType::Grid);
let l = d.layers[0].id;
let filled = d.children(Some(l)).unwrap()[0].id;
d.remove(filled).unwrap();
if with_pattern {
let id = d.alloc_id();
let n = Node::path(
id,
shapes::rectangle(Rect::new(0.0, 0.0, 80.0, 80.0)),
Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0),
);
d.insert(Some(l), 0, n).unwrap();
}
let id = d.alloc_id();
let mut n = Node::path(
id,
shapes::ellipse(Rect::new(120.0, 110.0, 180.0, 170.0)),
Appearance::basic(Paint::solid(Color::rgb(0.5, 0.5, 0.5)), Paint::None, 0.0),
);
n.appearance.effects.push(effect.clone());
let at = d.children(Some(l)).map_or(0, Vec::len);
d.insert(Some(l), at, n).unwrap();
d
};
// Supersampled, as raster exports draw.
let one = |d: &Document| {
let mut r = Renderer::new();
r.render(
d,
200,
200,
Affine::IDENTITY,
&RenderOptions { background: Some([255, 255, 255, 255]), supersample: true, ..Default::default() },
)
};
let (with, without) = (one(&make(true)), one(&make(false)));
for (x, y) in [(150, 140), (125, 140), (176, 160), (150, 172), (184, 176)] {
assert_eq!(with.pixel(x, y), without.pixel(x, y), "{} at ({x}, {y})", effect.id);
}
}
}
#[test]
fn pattern_fill_is_periodic() {
let d = doc_with_pattern(TileType::Grid);
+28
View File
@@ -67,3 +67,31 @@ fn swatch_bounds_outline_the_period_dashed() {
let off = render(&editing(tt, false, false), Some([0, 200, 0]));
assert!((51..70).all(|x| is_white(off.pixel(x, 90))));
}
/// #1534: a path made into a guide is drawn in the guide colour and style (Preferences › Guides &
/// Grid), as ruler guides are: a line, or dots with gaps between them.
#[test]
fn guide_paths_take_the_guide_color_and_style() {
let mut d = Document::new(200.0, 200.0);
let (layer, id) = (d.layers[0].id, d.alloc_id());
let mut n = Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 60.0, 60.0)), Appearance::basic(Paint::None, Paint::None, 0.0));
if let NodeKind::Path { guide, .. } = &mut n.kind {
*guide = true;
}
d.insert(Some(layer), 0, n).unwrap();
let draw = |guide| {
let opts = RenderOptions { background: Some([255; 4]), guide, ..Default::default() };
Renderer::new().render(&d, 120, 120, Affine::translate((50.5, 50.5)), &opts)
};
// Along the top edge of the square (y = 50), between its corners: the pixels the red covers.
let inked = |r: &Rendered| (55..105).filter(|x| r.pixel(*x, 50)[1] < 64).count();
let line = draw(([200, 0, 0], false));
let p = line.pixel(80, 50);
assert!(p[0] > 150 && p[1] < 100 && p[2] < 100, "{p:?}");
assert_eq!(inked(&line), 50);
let dotted = draw(([200, 0, 0], true));
let dots = inked(&dotted);
assert!((10..40).contains(&dots), "{dots}: {:?}", (45..70).map(|x| dotted.pixel(x, 50)[1]).collect::<Vec<_>>());
let default = draw(RenderOptions::default().guide);
assert!((55..105).all(|x| !is_white(default.pixel(x, 50))), "a line by default");
}
+10 -7
View File
@@ -50,6 +50,7 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
body: String::new(),
defs: String::new(),
classes: Vec::new(),
class_ids: HashMap::new(),
used_ids: HashSet::new(),
names: HashMap::new(),
depth: 1,
@@ -259,8 +260,10 @@ struct Writer<'a> {
xf: Affine,
body: String,
defs: String,
/// Unique CSS declaration blocks; class name is `cls-{index + 1}`.
/// Unique CSS declaration blocks, in first-use order (used for deterministic output).
classes: Vec<String>,
/// 1-based class/entity number by declaration; avoids a linear scan for each style.
class_ids: HashMap<String, usize>,
used_ids: HashSet<String>,
names: HashMap<NodeId, String>,
depth: usize,
@@ -657,13 +660,13 @@ impl Writer<'_> {
/// The 1-based number of a distinct declaration block (a CSS class or a style entity).
fn class(&mut self, decl: String) -> usize {
match self.classes.iter().position(|c| *c == decl) {
Some(i) => i + 1,
None => {
self.classes.push(decl);
self.classes.len()
}
if let Some(&i) = self.class_ids.get(decl.as_str()) {
return i;
}
let i = self.classes.len() + 1;
self.class_ids.insert(decl.clone(), i);
self.classes.push(decl);
i
}
/// `<metadata>`: the Dublin Core terms of File Info ([`ExportOptions::metadata`]) and the
+19 -2
View File
@@ -21,9 +21,11 @@ use text::TextSlots;
pub(crate) use fx::BLEND;
mod css;
mod css_transform;
mod files;
mod fx;
mod spread;
mod symbol_units;
mod text;
mod vector_effect;
@@ -464,11 +466,15 @@ impl Edits {
/// * undisplayed objects (`display: none`, as Save writes hidden layers) are shown and recorded,
/// to come back hidden; one something links to (a `<use>` template) stays as it is;
/// * shapes with a non-scaling stroke get an id when they have none, recorded ([`vector_effect`]);
/// * linked files are read ([`files`]).
/// * linked files are read ([`files`]);
/// * percentage lengths in a `<symbol>` with a `viewBox` become numbers ([`symbol_units`]).
fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
let mut found = Found::default();
// (`:` for prefixed elements such as `<svg:image>`.)
if !["<a", "display", "<use", ":use", "<image", ":image", vector_effect::NON_SCALING, "data-", "groupmode"].iter().any(|t| svg.contains(t)) {
let css_transform = has_css_transform(svg);
if !css_transform
&& !["<a", "display", "<use", ":use", "<image", ":image", vector_effect::NON_SCALING, "data-", "groupmode"].iter().any(|t| svg.contains(t))
{
return (svg.into(), found);
}
let Ok(xml) = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }) else {
@@ -507,9 +513,20 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
}
}
found.files = files::Files::read(svg, &xml, opts, &mut edits, &mut found.warnings);
if !symbols.is_empty() && svg.contains('%') {
edits.list.extend(symbol_units::rewrites(&xml));
}
if css_transform {
edits.list.extend(css_transform::rewrites(svg, &xml));
}
(edits.apply(svg), found)
}
/// Is there a `transform` CSS declaration in `svg` (`transform` followed by `:`)?
fn has_css_transform(svg: &str) -> bool {
svg.match_indices("transform").any(|(i, t)| svg.get(i + t.len()..).is_some_and(|s| s.trim_start().starts_with(':')))
}
/// The elements that are objects (what [`hidden_objects`] shows).
const OBJECTS: &[&str] = &["g", "a", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "image", "text", "use"];
+34 -1
View File
@@ -15,7 +15,8 @@ use usvg::roxmltree;
pub(super) type XNode<'a, 'i> = roxmltree::Node<'a, 'i>;
/// `name: value` declarations of a block or `style` attribute, with their `!important` flag.
/// `name: value` declarations of a block or `style` attribute, with their `!important` flag. The
/// `font` shorthand becomes its longhands, as usvg reads it for the rest of the drawing (#1557).
fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
s.split(';')
.filter_map(|d| {
@@ -27,9 +28,28 @@ fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
};
Some((k.trim().to_ascii_lowercase(), v.to_string(), important))
})
.flat_map(|(k, v, important)| match k.as_str() {
"font" => font_longhands(&v).into_iter().map(|(k, v)| (k.to_string(), v, important)).collect(),
_ => vec![(k, v, important)],
})
.collect()
}
/// The longhands of a `font` shorthand: those it leaves out back to their initial values. None
/// when it doesn't parse (it is then ignored, as in CSS).
fn font_longhands(v: &str) -> Vec<(&'static str, String)> {
let Ok(f) = svgtypes::FontShorthand::from_str(v) else { return Vec::new() };
let or = |o: Option<&str>| o.unwrap_or("normal").to_string();
vec![
("font-style", or(f.font_style)),
("font-variant", or(f.font_variant)),
("font-weight", or(f.font_weight)),
("font-stretch", or(f.font_stretch)),
("font-size", f.font_size.to_string()),
("font-family", f.font_family.to_string()),
]
}
/// One compound selector (`text.a.b#c[x=y]`).
#[derive(Debug, Default)]
struct Compound {
@@ -343,6 +363,19 @@ mod tests {
);
}
#[test]
fn the_font_shorthand_sets_its_longhands() {
let d = parse_decls("fill: red; font: italic bold 30px 'DejaVu Sans' ; font-weight: 300 !important; font: nonsense");
let get = |k: &str| d.iter().filter(|(n, ..)| n == k).map(|(_, v, i)| (v.as_str(), *i)).collect::<Vec<_>>();
assert_eq!(get("font-style"), [("italic", false)]);
assert_eq!(get("font-weight"), [("bold", false), ("300", true)]);
assert_eq!(get("font-size"), [("30px", false)]);
assert_eq!(get("font-family"), [("'DejaVu Sans'", false)]);
assert_eq!(get("font-variant"), [("normal", false)]);
assert_eq!(get("fill"), [("red", false)]);
assert!(get("font").is_empty(), "the shorthand itself is not kept");
}
#[test]
fn unsupported_selectors_are_dropped() {
for s in ["a + b", "a ~ b", "a:hover", "svg|text", "> a", "a >", ""] {
+151
View File
@@ -0,0 +1,151 @@
//! CSS `transform` declarations in SVG syntax, for usvg.
//!
//! usvg takes the `transform` property (from a `style` attribute or a `<style>` rule) as a
//! presentation attribute that overrides the element's `transform` attribute, but reads its value
//! as SVG, without CSS units: `translate(100px, 0)` or `rotate(45deg)` fails, and the element is
//! left with no transform at all (#1557). Before usvg runs, such values are rewritten in SVG
//! syntax: lengths in `px` (or unitless), angles in `deg`, `rad`, `grad` or `turn`, and the CSS
//! functions SVG lacks (`translateX`, `scaleY`, `skew`…) spelled with SVG's. A value that can't be
//! (percentages, `em`, 3D functions) is left as it is.
use std::f64::consts::PI;
use std::ops::Range;
use usvg::roxmltree;
use super::css::XNode;
/// The CSS transform `value` in SVG syntax; `None` when it has nothing to rewrite or can't be.
pub(super) fn to_svg(value: &str) -> Option<String> {
let v = value.trim();
if v.eq_ignore_ascii_case("none") {
return Some("matrix(1 0 0 1 0 0)".into());
}
let mut out = Vec::new();
let mut rest = v;
while !rest.trim_start().is_empty() {
rest = rest.trim_start();
let open = rest.find('(')?;
let close = rest.find(')')?;
let name = rest.get(..open)?.trim().to_ascii_lowercase();
let args: Vec<&str> = rest.get(open + 1..close)?.split(|c: char| c == ',' || c.is_whitespace()).filter(|a| !a.is_empty()).collect();
let len = |i: usize| args.get(i).and_then(|a| length(a));
let ang = |i: usize| args.get(i).and_then(|a| angle(a));
let num = |i: usize| args.get(i).and_then(|a| a.parse::<f64>().ok()).filter(|n| n.is_finite());
let f = match (name.as_str(), args.len()) {
("matrix", 6) => format!("matrix({} {} {} {} {} {})", num(0)?, num(1)?, num(2)?, num(3)?, len(4)?, len(5)?),
("translate", 1) => format!("translate({} 0)", len(0)?),
("translate", 2) => format!("translate({} {})", len(0)?, len(1)?),
("translatex", 1) => format!("translate({} 0)", len(0)?),
("translatey", 1) => format!("translate(0 {})", len(0)?),
("scale", 1) => format!("scale({})", num(0)?),
("scale", 2) => format!("scale({} {})", num(0)?, num(1)?),
("scalex", 1) => format!("scale({} 1)", num(0)?),
("scaley", 1) => format!("scale(1 {})", num(0)?),
("rotate", 1) => format!("rotate({})", ang(0)?),
("skewx", 1) => format!("skewX({})", ang(0)?),
("skewy", 1) => format!("skewY({})", ang(0)?),
("skew", 1) => format!("skewX({})", ang(0)?),
("skew", 2) => format!("matrix(1 {} {} 1 0 0)", ang(1)?.to_radians().tan(), ang(0)?.to_radians().tan()),
_ => return None,
};
out.push(f);
rest = rest.get(close + 1..)?;
}
let svg = out.join(" ");
(!out.is_empty() && svg != v).then_some(svg)
}
/// A length in user units: `px` or unitless.
fn length(a: &str) -> Option<f64> {
let n = a.strip_suffix("px").unwrap_or(a).parse::<f64>().ok()?;
n.is_finite().then_some(n)
}
/// An angle in degrees.
fn angle(a: &str) -> Option<f64> {
let a = a.to_ascii_lowercase();
let (n, k) = if let Some(n) = a.strip_suffix("deg") {
(n, 1.0)
} else if let Some(n) = a.strip_suffix("grad") {
(n, 0.9)
} else if let Some(n) = a.strip_suffix("rad") {
(n, 180.0 / PI)
} else if let Some(n) = a.strip_suffix("turn") {
(n, 360.0)
} else {
(a.as_str(), 1.0)
};
let d = n.parse::<f64>().ok()? * k;
d.is_finite().then_some(d)
}
/// Where CSS can be in `svg`: `style` attribute values and `<style>` contents.
fn css_ranges(xml: &roxmltree::Document) -> Vec<Range<usize>> {
let mut out = Vec::new();
for n in xml.descendants().filter(XNode::is_element) {
if let Some(a) = n.attributes().find(|a| a.name() == "style" && a.namespace().is_none()) {
out.push(a.range_value());
}
if n.tag_name().name() == "style" {
out.extend(n.children().filter(XNode::is_text).map(|t| t.range()));
}
}
out
}
/// The `transform` declarations in CSS in `svg`, each as (value range, value in SVG syntax).
pub(super) fn rewrites(svg: &str, xml: &roxmltree::Document) -> Vec<(Range<usize>, String)> {
let mut out = Vec::new();
for r in css_ranges(xml) {
let Some(css) = svg.get(r.clone()) else { continue };
let mut from = 0;
while let Some(i) = css.get(from..).and_then(|s| s.find("transform")).map(|i| i + from) {
from = i + "transform".len();
let before = css.get(..i).and_then(|s| s.chars().next_back());
if before.is_some_and(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
continue;
}
let after = css.get(from..).unwrap_or("");
let Some(colon) = after.find(|c: char| !c.is_whitespace()).filter(|&k| after.get(k..).is_some_and(|s| s.starts_with(':'))) else {
continue;
};
let start = from + colon + 1;
let len = css.get(start..).and_then(|s| s.find([';', '}', '!', '"', '\'', '<'])).unwrap_or(css.len() - start);
let Some(value) = css.get(start..start + len) else { continue };
if let Some(svg_value) = to_svg(value) {
out.push((r.start + start..r.start + start + len, format!(" {svg_value}")));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn css_transforms_become_svg() {
assert_eq!(to_svg("translate(100px, 100px)").as_deref(), Some("translate(100 100)"));
assert_eq!(to_svg(" translateX(5px) rotate(0.25turn) ").as_deref(), Some("translate(5 0) rotate(90)"));
assert_eq!(to_svg("rotate(1.5707963267948966rad) scaleY(2)").as_deref(), Some("rotate(90) scale(1 2)"));
assert_eq!(to_svg("skew(45deg)").as_deref(), Some("skewX(45)"));
assert_eq!(to_svg("matrix(1, 0, 0, 1, 10px, 20px)").as_deref(), Some("matrix(1 0 0 1 10 20)"));
assert_eq!(to_svg("none").as_deref(), Some("matrix(1 0 0 1 0 0)"));
// Already SVG, or not expressible: left alone.
assert_eq!(to_svg("translate(10 20)"), None);
assert_eq!(to_svg("translate(50%, 0)"), None);
assert_eq!(to_svg("rotate3d(1, 0, 0, 45deg)"), None);
assert_eq!(to_svg("translate(1px"), None);
}
#[test]
fn only_css_is_rewritten() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><style>.a { fill: red; transform: rotate(45deg) } .b{-webkit-transform:rotate(1deg)}</style><rect class="a" transform="translate(1 2)" style="transform:translate(3px,4px);fill:blue"/><text>transform: rotate(9deg)</text></svg>"#;
let xml = roxmltree::Document::parse(svg).unwrap();
let found = rewrites(svg, &xml);
let r: Vec<(&str, &str)> = found.iter().map(|(r, v)| (&svg[r.clone()], v.as_str())).collect();
assert_eq!(r, vec![(" rotate(45deg) ", " rotate(45)"), ("translate(3px,4px)", " translate(3 4)")]);
}
}
+72
View File
@@ -0,0 +1,72 @@
//! Percentage lengths inside a `<symbol>` with a `viewBox`, as numbers, for usvg.
//!
//! A percentage is of the nearest viewport's size in user units, and a symbol's `viewBox` sets
//! that size for its content as a nested `<svg>`'s does (SVG 2, 8.9 Units; 5.6 The `use` element).
//! usvg takes the percentages of the `<use>`'s width and height instead and then scales the art by
//! the `viewBox`, so it comes out too large and out of place (#1561). Before usvg runs, the
//! percentages of the shapes' geometry are rewritten in the `viewBox`'s units.
use std::ops::Range;
use std::str::FromStr;
use usvg::roxmltree;
/// The elements whose geometry attributes are rewritten.
const SHAPES: &[&str] = &["rect", "circle", "ellipse", "line", "image", "use", "text"];
/// The elements whose content has lengths of its own kind (or a viewport of its own).
const SCOPES: &[&str] = &["symbol", "svg", "pattern", "mask", "clipPath", "marker"];
/// What a percentage of attribute `name` is of, in a viewport of `w` × `h`: its width, its height,
/// or (a radius) its diagonal over √2.
fn basis(name: &str, w: f64, h: f64) -> Option<f64> {
match name {
"x" | "x1" | "x2" | "cx" | "width" | "rx" => Some(w),
"y" | "y1" | "y2" | "cy" | "height" | "ry" => Some(h),
"r" => Some(((w * w + h * h) / 2.0).sqrt()),
_ => None,
}
}
/// The percentage geometry of shapes in symbols with a `viewBox`, each as (value range, number).
pub(super) fn rewrites(xml: &roxmltree::Document) -> Vec<(Range<usize>, String)> {
let mut out = Vec::new();
for n in xml.descendants().filter(|n| n.is_element() && SHAPES.contains(&n.tag_name().name())) {
let Some(scope) = n.ancestors().skip(1).find(|a| SCOPES.contains(&a.tag_name().name())) else { continue };
let Some(vb) = Some(scope)
.filter(|s| s.tag_name().name() == "symbol")
.and_then(|s| s.attribute("viewBox"))
.and_then(|v| svgtypes::ViewBox::from_str(v).ok())
else {
continue;
};
if !(vb.w > 0.0 && vb.h > 0.0 && vb.w.is_finite() && vb.h.is_finite()) {
continue;
}
for a in n.attributes().filter(|a| a.namespace().is_none()) {
let percent = a.value().trim().strip_suffix('%').and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite());
if let (Some(p), Some(of)) = (percent, basis(a.name(), vb.w, vb.h)) {
out.push((a.range_value(), (p / 100.0 * of).to_string()));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentages_in_a_symbol_are_of_its_view_box() {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><rect width="50%"/>
<symbol id="a" viewBox="0 0 200 100"><rect x="10%" y="10%" width="100%" height="50%" rx="5"/><circle cx="50%" r="100%"/>
<linearGradient x2="100%"/><pattern><rect width="100%"/></pattern><g><line x2="25%" y2=" 25% "/></g></symbol>
<symbol id="b"><rect width="100%"/></symbol><use href="#a" width="30%"/></svg>"##;
let xml = roxmltree::Document::parse(svg).unwrap();
let found: Vec<(&str, String)> = rewrites(&xml).into_iter().map(|(r, v)| (&svg[r], v)).collect();
let r = (200.0f64 * 200.0 / 2.0 + 100.0 * 100.0 / 2.0).sqrt().to_string();
let expected =
[("10%", "20"), ("10%", "10"), ("100%", "200"), ("50%", "50"), ("50%", "100"), ("100%", r.as_str()), ("25%", "50"), (" 25% ", "25")];
assert_eq!(found.iter().map(|(a, b)| (*a, b.as_str())).collect::<Vec<_>>(), expected);
}
}
+35
View File
@@ -181,3 +181,38 @@ fn reflect_and_repeat_match_resvg() {
assert_similar(&render_artboard(&d), &resvg_render(&svg(&body), 200, 120), 24.0, 0.01);
}
}
/// #1557: `transform` as a CSS property, in CSS syntax, moves the object (and wins over the
/// attribute, as in CSS), from a `style` attribute or a `<style>` rule.
#[test]
fn css_transforms_move_objects() {
let d = import_with(
&svg(r#"<style>.r { transform: rotate(90deg) }</style>
<rect id="a" x="0" y="0" width="50" height="50" fill="red" style="transform: translate(100px, 100px)"/>
<rect id="b" x="0" y="0" width="50" height="50" fill="blue" transform="translate(0 100)" style="transform: translateX(100px)"/>
<rect id="c" class="r" x="0" y="0" width="50" height="20" fill="green"/>"#),
&ImportOptions::default(),
)
.unwrap()
.0;
let b: Vec<Rect> = art(&d).iter().map(|n| n.geometric_bounds().unwrap()).collect();
let near =
|r: Rect, x0: f64, y0: f64, x1: f64, y1: f64| [(r.x0, x0), (r.y0, y0), (r.x1, x1), (r.y1, y1)].iter().all(|(a, b)| (a - b).abs() < 1e-6);
assert!(near(b[0], 100.0, 100.0, 150.0, 150.0), "{:?}", b[0]);
assert!(near(b[1], 100.0, 0.0, 150.0, 50.0), "{:?}", b[1]);
assert!(near(b[2], -20.0, 0.0, 0.0, 50.0), "{:?}", b[2]);
}
/// #1561: percentages in a `<symbol>` with a `viewBox` are of the `viewBox`, so the symbol draws as
/// the same one written with numbers.
#[test]
fn percentages_in_a_symbol_are_of_its_view_box() {
let drawing = |w: &str, h: &str, cx: &str, cy: &str| {
let s = svg(&format!(
r##"<defs><symbol id="s" viewBox="0 0 100 50"><rect width="{w}" height="{h}" fill="red"/><circle cx="{cx}" cy="{cy}" r="20" fill="blue"/></symbol></defs>
<use xlink:href="#s" x="20" y="10" width="160" height="100"/>"##
));
render_artboard(&import_with(&s, &ImportOptions::default()).unwrap().0)
};
assert_similar(&drawing("100%", "100%", "25%", "50%"), &drawing("100", "50", "25", "25"), 24.0, 0.001);
}
+11
View File
@@ -583,3 +583,14 @@ fn text_path_to_an_empty_shape_still_falls_back_to_point_type() {
assert!(matches!(only_text(&d).kind, TextKind::Point));
assert!(w.iter().any(|w| w.contains("textPath")), "{w:?}");
}
/// #1557: the `font` shorthand sets the type's size, weight and style.
#[test]
fn the_font_shorthand_styles_type() {
let d = open(r#"<text x="10" y="40" style="font: italic bold 30px sans-serif">Deco</text>"#);
let t = only_text(&d);
let st = &t.runs[0].style;
assert!(close(st.size * t.xf.determinant().abs().sqrt(), 30.0, 1e-6), "{}", st.size);
let style = st.font_style.to_ascii_lowercase();
assert!(style.contains("bold") && style.contains("italic"), "{style}");
}
+27
View File
@@ -29,6 +29,33 @@ fn doc() -> Document {
d
}
/// A large palette should create one class per distinct declaration, preserving first-use
/// numbers even when earlier styles recur after hundreds of others.
#[test]
fn many_css_styles_preserve_first_use_numbers() {
let mut d = Document::new(300.0, 10.0);
let layer = d.layers[0].id;
let colors = (0u16..256).chain([0, 128, 255]);
for (x, color) in colors.enumerate() {
let x = x as f64;
let appearance = Appearance::basic(Paint::solid(Color::rgb8(color as u8, 31, 63)), Paint::None, 0.0);
let n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, 0.0, x + 0.5, 1.0)), appearance);
d.insert(Some(layer), usize::MAX, n).unwrap();
}
for (styling, definition_marker) in [(Styling::InternalCss, ".cls-"), (Styling::StyleEntities, "<!ENTITY st")] {
let svg = export(&d, &ExportOptions { styling, ..Default::default() });
let paths: Vec<_> = svg.lines().filter(|line| line.contains("<path ")).collect();
assert_eq!(paths.len(), 259, "{styling:?}");
for (i, path) in paths.iter().enumerate() {
let number = if i < 256 { i + 1 } else { [1, 129, 256][i - 256] };
let expected = if styling == Styling::InternalCss { format!("class=\"cls-{number}\"") } else { format!("style=\"&st{number};\"") };
assert!(path.contains(&expected), "path {i} should reference {expected}: {path}");
}
assert_eq!(svg.matches(definition_marker).count(), 256, "{styling:?}");
}
}
#[test]
fn every_combination_reads_back() {
let d = doc();
+6
View File
@@ -384,6 +384,12 @@ impl Targets {
self
}
/// The ruler guides alone ([`Self::with_rulers`]): what a dragged selection's grabbed point
/// still snaps to with View → Snap to Point when Smart Guides line the selection up.
pub(crate) fn rulers(cx: &ToolContext) -> Self {
Self::default().styled(cx).with_rulers(cx)
}
/// What a dragged selection's grabbed point snaps to with View → Snap to Point and Smart
/// Guides off: the anchors of the art but `exclude` and the ruler guides.
pub fn snap_to_point(cx: &ToolContext, exclude: &[NodeId]) -> Option<Self> {
+35
View File
@@ -90,6 +90,9 @@ pub(crate) struct MoveSnap {
/// original.
copy_targets: Option<Targets>,
copy_points: Option<Targets>,
/// With Smart Guides on, the ruler guides View › Snap to Point still pulls the grabbed point
/// onto (#1534); `points` holds them with Smart Guides off.
rulers: Option<Targets>,
}
impl MoveSnap {
@@ -102,6 +105,7 @@ impl MoveSnap {
points: points(&cx.selection.objects),
copy_targets: guides.then(|| Targets::for_move(cx, &[])),
copy_points: points(&[]),
rulers: (guides && cx.snap_to_point).then(|| Targets::rulers(cx)),
}
}
@@ -133,6 +137,14 @@ impl MoveSnap {
guides = ov;
}
}
// A ruler guide within reach of the grabbed point wins over lining the bounds up.
if let Some(t) = &self.rulers {
let (q, ov) = t.snap_point(start + d, cx.tol(cx.snap_tolerance));
if q != start + d {
d = q - start;
guides = ov;
}
}
if cx.snap_to_pixel
&& let Some(b) = self.bounds
{
@@ -733,6 +745,29 @@ mod tests {
assert_eq!(a, vec![Action::Commit]);
}
/// #1534: with Smart Guides on (the default), the point a selection is dragged by still snaps
/// to a ruler guide within the Snap to Point distance; not with Snap to Point off.
#[test]
fn a_move_snaps_the_grabbed_point_to_a_ruler_guide_with_smart_guides_on() {
let (mut d, id) = doc_with_rect();
d.guides = vec![vectorcraft_doc::Guide::new(true, 173.4)];
let mut s = Selection::default();
s.add(id);
let p = paint();
let dx = |c: &ToolContext, to: f64| {
let mut t = SelectionTool::default();
t.pointer(c, &ev(PointerKind::Down, 150.0, 150.0));
let a = t.pointer(c, &ev(PointerKind::Drag, to, 150.0));
let Some(Action::Preview(_, v)) = a.last() else { panic!("{a:?}") };
v["matrix"][4].as_f64().unwrap()
};
let c = cx(&d, &s, &p);
assert!(c.smart_guides && c.snap_to_point);
assert!((dx(&c, 172.0) - 23.4).abs() < 1e-9, "onto the guide");
assert_eq!(dx(&c, 165.0), 15.0, "out of reach");
assert_eq!(dx(&ToolContext { snap_to_point: false, ..cx(&d, &s, &p) }, 172.0), 22.0);
}
/// Alt pressed during a move makes a copy, which snaps to the original it leaves behind (as
/// gathered before the previews moved it); released, the move doesn't.
#[test]
-1
View File
@@ -10,7 +10,6 @@ workspace = true
[dependencies]
vectorcraft-geom = { workspace = true }
vectorcraft-pathops = { workspace = true }
kurbo = { workspace = true }
image = { workspace = true }
serde = { workspace = true }
+183 -8
View File
@@ -12,14 +12,23 @@
//! merged into one point) become polylines through the pixel centres; rings without ends
//! become closed ones. Short spurs that thinning leaves at junctions are pruned and the runs
//! on either side of a junction left with two of them are joined.
//! 5. **Curves**: each polyline is fitted like an outline ([`fit::fit_polyline`]); the stroke
//! width is the line's mean width (its pixel count over its length).
//! 5. **Ink**: the skeleton is right to a pixel only, so each of its points is moved to the middle
//! of the line's ink, read from the image: across the line (along the pixel column or row most
//! nearly square to it) each pixel is as much ink as its colour is the line's rather than the
//! paper's, and the ink's centroid is the line's middle there and its sum the line's width (an
//! anti-aliased or blurred line says both to a fraction of a pixel). The stroke width is the
//! median of those widths, and each end is put where the ink ends, less the half width a round
//! cap adds.
//! 6. **Curves**: each polyline is fitted as outlines are ([`fit::fit_piece`]): a straight line
//! is two anchors, corners stay corners, the rest is as few curves as Paths allows.
use std::collections::HashSet;
use vectorcraft_geom::{PathData, Point};
use crate::fit::{self, FitOptions};
use crate::Raster;
use crate::fit;
use crate::logo::Style;
/// One line of a colour layer: its centre lines and the width to stroke them with.
pub(crate) struct Line {
@@ -31,7 +40,19 @@ pub(crate) struct Line {
/// The lines of colour `label` in `labels` (`w` × `h`, row-major) no wider than `max_width`
/// pixels, and which pixels they cover (to leave out of the filled areas).
pub(crate) fn lines(labels: &[u16], label: u16, w: usize, h: usize, max_width: f64, o: &FitOptions) -> (Vec<bool>, Vec<Line>) {
/// `ink` is the colour of the layer's ink in `img` (its palette colour; in Black and White the
/// image's own black), which the lines' middles and widths are read against.
#[allow(clippy::too_many_arguments)]
pub(crate) fn lines(
labels: &[u16],
label: u16,
w: usize,
h: usize,
max_width: f64,
img: &Raster,
ink: [f64; 3],
style: &Style,
) -> (Vec<bool>, Vec<Line>) {
let mut thin = vec![false; labels.len()];
let mut out = vec![];
if w == 0 || h == 0 || labels.len() != w * h {
@@ -61,7 +82,7 @@ pub(crate) fn lines(labels: &[u16], label: u16, w: usize, h: usize, max_width: f
if widest > max_width {
continue;
}
let Some(line) = trace_line(&comp, w, max_width, o) else { continue };
let Some(line) = trace_line(&comp, w, max_width, img, ink, style) else { continue };
for &i in &comp {
thin[i] = true;
}
@@ -221,7 +242,7 @@ impl Run {
}
/// The centre lines of one line component (pixel indices of the `w`-wide image).
fn trace_line(comp: &[usize], w: usize, max_width: f64, o: &FitOptions) -> Option<Line> {
fn trace_line(comp: &[usize], w: usize, max_width: f64, img: &Raster, ink: [f64; 3], style: &Style) -> Option<Line> {
let xs = comp.iter().map(|&i| i % w);
let ys = comp.iter().map(|&i| i / w);
let (x0, x1, y0, y1) = (xs.clone().min()?, xs.max()?, ys.clone().min()?, ys.max()?);
@@ -312,14 +333,168 @@ fn trace_line(comp: &[usize], w: usize, max_width: f64, o: &FitOptions) -> Optio
};
let spur = width(&runs);
let runs = join(prune(runs, centres.len(), spur), centres.len());
let width = width(&runs);
let subs: Vec<_> = runs.iter().filter_map(|r| fit::fit_polyline(&r.pts, r.closed, o)).collect();
let mut runs = runs;
let coarse = width(&runs);
let ink = Ink { img, color: ink, reach: (max_width / 2.0).ceil().clamp(1.0, 64.0) as i64 + 2 };
// How many runs end at each node: one is an end of the line.
let mut meet = vec![0usize; centres.len()];
for e in runs.iter().flat_map(|r| r.ends.iter().flatten()) {
meet[*e] += 1;
}
let (mut widths, mut soft, mut read) = (vec![], 0usize, 0usize);
for r in &mut runs {
let n = r.pts.len();
let old = r.pts.clone();
for i in 0..n {
// A junction stays where its pixels put it.
let end = if i == 0 {
r.ends[0]
} else if i + 1 == n {
r.ends[1]
} else {
None
};
if end.is_some_and(|e| meet[e] > 1) {
continue;
}
let (a, b) = if r.closed { (old[(i + n - 2 % n) % n], old[(i + 2) % n]) } else { (old[i.saturating_sub(2)], old[(i + 2).min(n - 1)]) };
let d = b - a;
if d.hypot() < 1e-9 {
continue;
}
let upright = d.x.abs() >= d.y.abs();
let Some(across) = ink.across(old[i], upright) else { continue };
r.pts[i] = old[i] + if upright { kurbo::Vec2::new(0.0, across.middle) } else { kurbo::Vec2::new(across.middle, 0.0) };
widths.push(across.width * if upright { d.x.abs() } else { d.y.abs() } / d.hypot());
read += 1;
soft += usize::from(across.soft);
}
}
// The stroke is as wide as the ink at the line's middle point (where no ink could be read, as
// wide as its pixels make it).
widths.sort_by(f64::total_cmp);
let width = widths.get(widths.len() / 2).copied().unwrap_or(coarse).clamp(0.25, max_width.max(1.0));
// Each end goes where the ink ends, less the half width its round cap adds.
for r in runs.iter_mut().filter(|r| !r.closed && r.pts.len() >= 2) {
for k in 0..2 {
if r.ends[k].is_some_and(|e| meet[e] > 1) {
continue;
}
let n = r.pts.len();
let (tip, inner) = if k == 0 { (r.pts[0], r.pts[3.min(n - 1)]) } else { (r.pts[n - 1], r.pts[n - 1 - 3.min(n - 1)]) };
let d = tip - inner;
if d.hypot() < 1e-9 {
continue;
}
let more = ink.beyond(tip, d / d.hypot()) - width / 2.0;
if more.abs() > 0.05 && more > -1.0 {
let to = tip + d / d.hypot() * more;
if k == 0 { r.pts[0] = to } else { r.pts[n - 1] = to }
}
}
}
let mut style = *style;
if read > 0 && soft * 4 < read {
// Hard pixels: the middle is only known to about half a pixel (as for outlines).
style.tol *= 2.5;
style.line_tol *= 2.5;
style.sigma = 1.5;
}
let subs: Vec<_> = runs
.iter()
.filter_map(|r| {
if r.closed { fit::subpath(&fit::fit_closed(&r.pts, &style)?, true) } else { fit::subpath(&fit::fit_piece(&r.pts, &style, false), false) }
})
.collect();
if subs.is_empty() {
return None;
}
Some(Line { path: PathData::new(subs), width, pixels: comp.len() })
}
/// What the image says of a line's ink.
struct Ink<'a> {
img: &'a Raster,
/// The line's colour.
color: [f64; 3],
/// How far (pixels) from the skeleton the ink is looked for.
reach: i64,
}
/// The ink across a line at one point: where its middle lies from the point, how wide it is, and
/// whether its edges are soft (some pixel is part ink, part paper).
struct Across {
middle: f64,
width: f64,
soft: bool,
}
impl Ink<'_> {
/// How far the colour of pixel `(x, y)` is from the line's (`None` off the image).
fn far(&self, x: i64, y: i64) -> Option<f64> {
let (w, h) = (i64::from(self.img.width), i64::from(self.img.height));
if x < 0 || y < 0 || x >= w || y >= h {
return None;
}
let at = (y * w + x) as usize * 4;
let p = self.img.rgba.get(at..at + 3)?;
Some((0..3).map(|k| (f64::from(p[k]) - self.color[k]).powi(2)).sum::<f64>().sqrt())
}
/// How far the paper's colour is from the line's around pixel `(x, y)`: the farthest colour
/// within reach, along its row and its column.
fn paper(&self, x: i64, y: i64) -> f64 {
(-self.reach..=self.reach).flat_map(|k| [self.far(x + k, y), self.far(x, y + k)]).flatten().fold(0.0, f64::max)
}
/// The ink across the line through the pixel `p` lies in: along its column (`upright`) or row.
fn across(&self, p: Point, upright: bool) -> Option<Across> {
let (x, y) = (p.x.floor() as i64, p.y.floor() as i64);
let paper = self.paper(x, y);
// Paper too like the ink says nothing.
if paper < 24.0 {
return None;
}
let tone = |k: i64| (if upright { self.far(x, y + k) } else { self.far(x + k, y) }).map_or(0.0, |d| (1.0 - d / paper).clamp(0.0, 1.0));
// From the pixel with most ink near the skeleton's (thinning leaves it up to a pixel or two
// off the line's darkest row) out to where the ink ends, or rises again (another line).
let peak = [0, -1, 1, -2, 2].into_iter().fold(0, |best, k| if tone(k) > tone(best) + 1e-9 { k } else { best });
let here = tone(peak);
let (mut sum, mut moment, mut soft) = (here, here * peak as f64, here > 0.1 && here < 0.9);
for side in [-1, 1] {
let mut last = here;
for k in 1..=self.reach {
let t = tone(peak + side * k);
if t < 0.04 || t > last + 0.1 {
break;
}
sum += t;
moment += t * (peak + side * k) as f64;
soft |= t > 0.1 && t < 0.9;
last = t;
}
}
// The pixel's centre is at `p`'s own half-pixel; the middle is measured from `p`.
let centre = if upright { y as f64 + 0.5 - p.y } else { x as f64 + 0.5 - p.x };
(sum > 0.0).then(|| Across { middle: centre + moment / sum, width: sum, soft })
}
/// How much ink (pixels) lies beyond `tip`, going `dir` (a unit vector).
fn beyond(&self, tip: Point, dir: kurbo::Vec2) -> f64 {
let paper = self.paper(tip.x.floor() as i64, tip.y.floor() as i64);
if paper < 24.0 {
return 0.0;
}
let mut sum = 0.0;
for step in 1..=4 * (self.reach + 2) {
let q = tip + dir * (step as f64 * 0.25);
let t = self.far(q.x.floor() as i64, q.y.floor() as i64).map_or(0.0, |d| (1.0 - d / paper).clamp(0.0, 1.0));
if t < 0.04 {
break;
}
sum += 0.25 * t;
}
sum
}
}
/// The runs without the spurs thinning leaves: runs shorter than the line's `width` from a line end
/// to a junction that keeps at least two other runs.
fn prune(runs: Vec<Run>, nodes: usize, width: f64) -> Vec<Run> {
+87 -87
View File
@@ -1,20 +1,11 @@
//! Loop → Bézier curves, and centre line → Bézier curves.
//! Loop → Bézier curves, and the pieces of that which centre lines use too.
use vectorcraft_geom::{Anchor, AnchorKind, CubicBez, ParamCurve, PathData, Point, SubPath};
use vectorcraft_pathops::{SimplifyOptions, simplify_with};
use vectorcraft_geom::{Anchor, AnchorKind, Point, SubPath};
use crate::contour::Loop;
use crate::logo::{Cubic, Stats, Style, all_within, fit_open, fit_outline, resample_closed, resample_open, sample_cubics};
use crate::subpixel::{self, Crack};
#[derive(Clone, Copy, Debug)]
pub(crate) struct FitOptions {
pub polygon_tol: f64,
pub fit_tol: f64,
pub corner_angle: f64,
pub snap_lines: bool,
}
fn seg_dist(p: Point, a: Point, b: Point) -> f64 {
let ab = b - a;
let l2 = ab.hypot2();
@@ -60,10 +51,69 @@ fn before(a: Point, b: Point) -> std::cmp::Ordering {
a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y))
}
/// Lines within this of level or upright are made exactly so (the tangent of 2.5°).
const NEARLY_UPRIGHT: f64 = 0.0437;
/// Snap Curves To Lines: the straight pieces of `segs` (a chain; `closed` when its end joins its
/// start) that are within 2.5° of level or upright and at least 2 px long are made exactly level
/// or upright, by moving their ends across them: both to their mean, or, in an open chain, the
/// free end to the end that is fixed (with `fixed_ends`, a chain's own ends stay where they are:
/// they are junctions other regions share). The pieces next to a moved end follow it.
fn snap_upright(segs: &mut [Cubic], closed: bool, fixed_ends: bool) {
let n = segs.len();
let straight = |c: &Cubic| seg_dist(c[1], c[0], c[3]) < 1e-6 && seg_dist(c[2], c[0], c[3]) < 1e-6;
let lines: Vec<bool> = segs.iter().map(straight).collect();
// Anchor `i` starts piece `i`; an open chain has one more, at its end.
let fixed = |i: usize| !closed && fixed_ends && (i == 0 || i == n);
let at = |segs: &[Cubic], i: usize| if i < n { segs[i][0] } else { segs[n - 1][3] };
let put = |segs: &mut [Cubic], i: usize, p: Point| {
let i = if closed { i % n } else { i };
let d = p - at(segs, i);
if i < n {
segs[i][0] = p;
segs[i][1] += d;
}
if let Some(before) = if i > 0 { Some(i - 1) } else { closed.then_some(n - 1) } {
segs[before][3] = p;
segs[before][2] += d;
}
};
for (i, &line) in lines.iter().enumerate() {
let (ia, ib) = (i, if closed { (i + 1) % n } else { i + 1 });
let (a, b) = (at(segs, ia), at(segs, ib));
let d = b - a;
if !line || d.hypot() < 2.0 || ia == ib {
continue;
}
let level = d.y.abs() <= NEARLY_UPRIGHT * d.x.abs();
if !level && d.x.abs() > NEARLY_UPRIGHT * d.y.abs() {
continue;
}
let pick = |p: Point| if level { p.y } else { p.x };
let to = match (fixed(ia), fixed(ib)) {
(true, true) => continue,
(true, false) => pick(a),
(false, true) => pick(b),
(false, false) => (pick(a) + pick(b)) / 2.0,
};
let moved = |p: Point| if level { Point::new(p.x, to) } else { Point::new(to, p.y) };
if !fixed(ia) {
put(segs, ia, moved(a));
}
if !fixed(ib) {
put(segs, ib, moved(b));
}
}
// A straight piece has its handles on it.
for (c, _) in segs.iter_mut().zip(&lines).filter(|(_, line)| **line) {
*c = [c[0], c[0] + (c[3] - c[0]) / 3.0, c[0] + (c[3] - c[0]) * (2.0 / 3.0), c[3]];
}
}
/// A whole loop fitted. The same loop gives the same curves whichever way it runs and wherever it
/// starts (it is fitted clockwise from its leftmost point): the outline of a shape and the hole it
/// leaves in the shape around it are one curve.
fn fit_closed(pts: &[Point], style: &Style) -> Option<Vec<Cubic>> {
pub(crate) fn fit_closed(pts: &[Point], style: &Style) -> Option<Vec<Cubic>> {
let n = pts.len();
let area2: f64 = (0..n).map(|i| pts[i].x * pts[(i + 1) % n].y - pts[(i + 1) % n].x * pts[i].y).sum();
let flip = area2 < 0.0;
@@ -80,13 +130,17 @@ fn fit_closed(pts: &[Point], style: &Style) -> Option<Vec<Cubic>> {
if stats.reverted > 0 && !all_within(&sample_cubics(&segs, 0.25), &resample_closed(&pts, 0.5).map_or_else(|| pts.clone(), |(p, _)| p), 1.5) {
segs = polygon(&pts, style.tol);
}
if style.upright {
snap_upright(&mut segs, true, false);
}
Some(if flip { reversed(segs) } else { segs })
}
/// A piece of a loop between two junctions fitted, from its first point to its last. The same
/// piece gives the same curve whichever way it runs (it is fitted from its end further left): the
/// edge two regions share is one curve.
fn fit_piece(pts: &[Point], style: &Style) -> Vec<Cubic> {
/// edge two regions share is one curve. `fixed_ends`: its ends are junctions and stay put (a centre
/// line's ends are its own).
pub(crate) fn fit_piece(pts: &[Point], style: &Style, fixed_ends: bool) -> Vec<Cubic> {
let n = pts.len();
let (Some(&first), Some(&last)) = (pts.first(), pts.last()) else { return vec![] };
let flip = match before(first, last) {
@@ -107,6 +161,9 @@ fn fit_piece(pts: &[Point], style: &Style) -> Vec<Cubic> {
let kept: Vec<Point> = (0..n).filter(|&i| keep[i]).map(|i| pts[i]).collect();
segs = kept.windows(2).filter(|p| p[0] != p[1]).map(|p| [p[0], p[0], p[1], p[1]]).collect();
}
if style.upright {
snap_upright(&mut segs, false, fixed_ends);
}
if flip { reversed(segs) } else { segs }
}
@@ -137,22 +194,30 @@ pub(crate) fn fit_loop(
if piece.closed {
segs = fit_closed(&piece.pts, &style)?;
} else {
segs.extend(fit_piece(&piece.pts, &style));
segs.extend(fit_piece(&piece.pts, &style, true));
}
}
subpath(&segs, true)
}
/// A chain of cubics as a subpath: closed (its end joins its start) or open. A straight piece has
/// no handles; an anchor whose two handles line up is smooth.
pub(crate) fn subpath(segs: &[Cubic], closed: bool) -> Option<SubPath> {
let n = segs.len();
// A straight piece has no handles.
let straight = |c: &Cubic| seg_dist(c[1], c[0], c[3]) < 1e-6 && seg_dist(c[2], c[0], c[3]) < 1e-6;
let mut anchors = Vec::with_capacity(n);
for (i, cur) in segs.iter().enumerate() {
let before = segs.get((i + n - 1) % n)?;
let p = cur[0];
let (h_in, h_out) = (if straight(before) { p } else { before[2] }, if straight(cur) { p } else { cur[1] });
let mut anchors = Vec::with_capacity(n + 1);
for i in 0..if closed { n } else { n + 1 } {
// The piece that arrives at this anchor and the one that leaves it.
let before = if i > 0 { segs.get(i - 1) } else { closed.then(|| segs.last()).flatten() };
let after = segs.get(i);
let p = after.map(|c| c[0]).or(before.map(|c| c[3]))?;
let h_in = before.filter(|c| !straight(c)).map_or(p, |c| c[2]);
let h_out = after.filter(|c| !straight(c)).map_or(p, |c| c[1]);
let (vi, vo) = (p - h_in, h_out - p);
let smooth = vi.hypot() > 1e-9 && vo.hypot() > 1e-9 && vi.cross(vo).abs() < 0.07 * vi.hypot() * vo.hypot() && vi.dot(vo) > 0.0;
anchors.push(Anchor { p, h_in, h_out, kind: if smooth { AnchorKind::Smooth } else { AnchorKind::Corner } });
}
(n >= 2).then(|| SubPath::new(anchors, true))
(n >= if closed { 2 } else { 1 }).then(|| SubPath::new(anchors, closed))
}
/// The closed outline `pts` as straight pieces within `tol` of it (Douglas–Peucker).
@@ -170,68 +235,3 @@ fn polygon(pts: &[Point], tol: f64) -> Vec<Cubic> {
let kept: Vec<Point> = (0..n).filter(|&i| keep[i]).map(|i| pts[i]).collect();
(0..kept.len()).map(|i| [kept[i], kept[i], kept[(i + 1) % kept.len()], kept[(i + 1) % kept.len()]]).collect()
}
/// Fit a centre line through `pts` (pixel-centre points in order) into a Bézier subpath: the
/// points within `polygon_tol` of it are dropped (Douglas–Peucker), then curves are fitted. A
/// `closed` line runs back to its first point.
pub(crate) fn fit_polyline(pts: &[Point], closed: bool, o: &FitOptions) -> Option<SubPath> {
let n = pts.len();
let first = *pts.first()?;
if n < 2 {
return None;
}
let mut ext = pts.to_vec();
let mut keep = vec![false; n + 1];
keep[0] = true;
if closed {
// Split the ring at its first point and the point farthest from it.
ext.push(first);
let far = (1..n).max_by(|&i, &j| pts[i].distance_squared(first).total_cmp(&pts[j].distance_squared(first))).unwrap_or(n / 2);
keep[far] = true;
rdp(&ext, 0, far, o.polygon_tol, &mut keep);
rdp(&ext, far, n, o.polygon_tol, &mut keep);
} else {
keep[n - 1] = true;
rdp(&ext, 0, n - 1, o.polygon_tol, &mut keep);
}
let mut poly: Vec<Point> = (0..n).filter(|&i| keep[i]).map(|i| pts[i]).collect();
if closed && poly.len() < 3 {
poly = pts.to_vec();
}
if poly.len() < if closed { 3 } else { 2 } {
return None;
}
let sp = SubPath::polyline(&poly, closed);
let fitted =
simplify_with(&PathData::single(sp), &SimplifyOptions { tolerance: o.fit_tol, corner_angle_deg: o.corner_angle, straight_lines: false });
let mut sp = fitted.subpaths.into_iter().next()?;
if sp.anchors.len() < 2 {
return None;
}
if o.snap_lines {
snap_to_lines(&mut sp, (o.fit_tol * 2.0).max(1.5));
}
Some(sp)
}
/// Retract the handles of curves that stay within `tol` of their chord.
fn snap_to_lines(sp: &mut SubPath, tol: f64) {
let n = sp.anchors.len();
for i in 0..sp.segment_count() {
let j = (i + 1) % n;
let (a, b) = (sp.anchors[i], sp.anchors[j]);
if !a.has_out() && !b.has_in() {
continue;
}
let c = CubicBez::new(a.p, a.h_out, b.h_in, b.p);
if (1..8).all(|k| seg_dist(c.eval(k as f64 / 8.0), a.p, b.p) <= tol) {
sp.anchors[i].h_out = a.p;
sp.anchors[j].h_in = b.p;
}
}
for a in &mut sp.anchors {
if !a.has_in() || !a.has_out() {
a.kind = AnchorKind::Corner;
}
}
}
+188
View File
@@ -0,0 +1,188 @@
//! Auto Grouping: which traced path lies inside which, so that a result can be grouped the way it
//! is nested (a ring with what fills its hole, a face with its eyes).
use crate::TracedPath;
use kurbo::{PathEl, Point, Rect};
/// How far (px) outside an outline a point may lie and still count as inside it: two paths that
/// share an edge are each fitted on their own.
const NEAR: f64 = 0.5;
/// A path's outer contour as a polygon, and its box.
struct Outer {
pts: Vec<Point>,
bbox: Rect,
/// A closed, filled contour: something can lie inside it.
holds: bool,
/// Where something can: for each pixel row from `bbox.y0 - NEAR` down, the edges (each from
/// the point of that index to the next) that pass through it or within [`NEAR`] of it. A
/// point is tested against the edges of its row, not all of a long outline's.
rows: Vec<Vec<u32>>,
}
fn outer(tp: &TracedPath) -> Option<Outer> {
let (mut pts, mut subpaths, mut closed) = (vec![], 0, false);
kurbo::flatten(tp.path.to_bezpath(), 0.2, |el| match el {
PathEl::MoveTo(p) => {
subpaths += 1;
if subpaths == 1 {
pts.push(p);
}
}
PathEl::LineTo(p) if subpaths == 1 => pts.push(p),
PathEl::ClosePath if subpaths == 1 => closed = true,
_ => {}
});
let first = *pts.first()?;
let bbox = pts.iter().fold(Rect::from_points(first, first), |b, p| b.union_pt(*p));
if !bbox.is_finite() {
return None;
}
let holds = closed && tp.stroke.is_none();
// (Traced paths lie in the image: a box taller than any image is nothing to hold things.)
let holds = holds && bbox.height() < 1e6 && u32::try_from(pts.len()).is_ok();
let mut rows = vec![];
if holds {
rows = vec![vec![]; (bbox.height() + 2.0 * NEAR) as usize + 1];
for (i, (a, b)) in pts.iter().zip(pts.iter().cycle().skip(1)).enumerate() {
let (top, bottom) = (a.y.min(b.y) - bbox.y0, a.y.max(b.y) - bbox.y0 + 2.0 * NEAR);
for row in rows.iter_mut().take(bottom as usize + 1).skip(top.max(0.0) as usize) {
row.push(i as u32);
}
}
}
Some(Outer { pts, bbox, holds, rows })
}
/// Whether `p` lies inside the closed outline of `o`, or within [`NEAR`] of its edge.
fn inside_or_near(p: Point, o: &Outer) -> bool {
let row = p.y - (o.bbox.y0 - NEAR);
let Some(edges) = (row >= 0.0).then(|| o.rows.get(row as usize)).flatten() else { return false };
let mut inside = false;
for &i in edges {
let (Some(a), Some(b)) = (o.pts.get(i as usize), o.pts.get((i as usize + 1) % o.pts.len().max(1))) else { continue };
if (a.y > p.y) != (b.y > p.y) && p.x < a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x) {
inside = !inside;
}
let d = *b - *a;
let t = if d.hypot2() > 0.0 { ((p - *a).dot(d) / d.hypot2()).clamp(0.0, 1.0) } else { 0.0 };
if (p - (*a + d * t)).hypot2() <= NEAR * NEAR {
return true;
}
}
inside
}
/// 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
/// (the paper) is nobody's parent: everything would be inside it.
///
/// Drawing each path followed by those inside it paints the picture `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.
pub(crate) fn parents(paths: &[TracedPath], w: usize, h: usize, stacked: bool) -> Vec<Option<usize>> {
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 mut parent: Vec<Option<usize>> = vec![None; paths.len()];
// How many paths each is inside of.
let mut depth = vec![0usize; paths.len()];
// The paths something can lie inside, by the area of their boxes.
let mut by_area: Vec<usize> = (0..paths.len()).filter(|&p| outers.get(p).and_then(Option::as_ref).is_some_and(|o| o.holds)).collect();
let area = |p: &usize| outers.get(*p).and_then(Option::as_ref).map_or(0.0, |o| o.bbox.area());
by_area.sort_by(|a, b| area(a).total_cmp(&area(b)));
for (x, ox) in outers.iter().enumerate() {
let Some(ox) = ox else { continue };
// The paths whose box holds its own, smallest first.
let mut holders = by_area.iter().filter_map(|&p| Some((p, outers.get(p)?.as_ref()?))).filter(|&(p, op)| {
let (o, i) = (op.bbox, ox.bbox);
o.x0 - NEAR <= i.x0
&& o.y0 - NEAR <= i.y0
&& o.x1 + NEAR >= i.x1
&& o.y1 + NEAR >= i.y1
&& o.area() > i.area()
&& !(stacked && p > x)
&& !(o.x0 <= paper.x0 && o.y0 <= paper.y0 && o.x1 >= paper.x1 && o.y1 >= paper.y1)
});
// The paths under `x` that share some of its box: it may not come to lie under any of them.
// (Found only once some path holds its first point: most paths are inside none.)
let under = std::cell::OnceCell::new();
let under = || {
under.get_or_init(|| {
(0..x)
.filter(|&y| outers.get(y).and_then(Option::as_ref).is_some_and(|oy| oy.bbox.intersect(ox.bbox).area() > 0.0))
.collect::<Vec<_>>()
})
};
let found = holders.find(|&(p, op)| {
ox.pts.first().is_some_and(|pt| inside_or_near(*pt, op))
&& (!stacked || under().iter().all(|&y| !comes_after(&parent, &depth, y, p)))
&& ox.pts.iter().all(|pt| inside_or_near(*pt, op))
});
if let (Some((p, _)), Some(slot)) = (found, parent.get_mut(x)) {
*slot = Some(p);
let deep = depth.get(p).copied().unwrap_or(0) + 1;
if let Some(d) = depth.get_mut(x) {
*d = deep;
}
}
}
parent
}
/// Whether `y` is drawn after `p` and all that is inside `p`: it is in another branch of the
/// nesting, and that branch starts above the one `p` is in.
fn comes_after(parent: &[Option<usize>], depth: &[usize], y: usize, p: usize) -> bool {
let up = |i: usize| parent.get(i).copied().flatten();
let deep = |i: usize| depth.get(i).copied().unwrap_or(0);
let (mut a, mut b) = (y, p);
while deep(a) > deep(b) {
let Some(q) = up(a) else { break };
a = q;
}
while deep(b) > deep(a) {
let Some(q) = up(b) else { break };
b = q;
}
// (Equal: one is the other, or inside it.)
while a != b && up(a) != up(b) {
let (Some(qa), Some(qb)) = (up(a), up(b)) else { break };
(a, b) = (qa, qb);
}
a > b
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_geom::PathData;
fn disc(c: (f64, f64), r: f64, color: u8) -> TracedPath {
let path = PathData::from_bezpath(&kurbo::Shape::to_path(&kurbo::Circle::new(c, r), 0.01));
TracedPath { path, color: [color; 3], pixels: 1, stroke: None, shape: None }
}
#[test]
fn a_path_is_inside_the_smallest_path_that_holds_it() {
// The paper, a disc, a disc inside it, a dot inside that, and a disc beside them.
let paths = [
disc((50.0, 50.0), 80.0, 255),
disc((30.0, 50.0), 20.0, 0),
disc((30.0, 50.0), 10.0, 100),
disc((30.0, 50.0), 3.0, 0),
disc((75.0, 50.0), 10.0, 0),
];
for stacked in [false, true] {
assert_eq!(parents(&paths, 100, 100, stacked), [None, None, Some(1), Some(2), None]);
}
}
#[test]
fn stacked_paths_are_not_reordered_where_they_overlap() {
// A dot over a disc, with a wider disc between them in the stack that covers the dot's
// place without holding the first disc: grouped with the first disc, the dot would go under it.
let paths = [disc((30.0, 50.0), 20.0, 0), disc((60.0, 50.0), 24.0, 100), disc((35.0, 50.0), 3.0, 200)];
assert_eq!(parents(&paths, 100, 100, true), [None, None, None]);
// Where paths cannot overlap, either order paints the same.
assert_eq!(parents(&paths, 100, 100, false), [None, None, Some(0)]);
}
}
+79 -37
View File
@@ -34,8 +34,9 @@
//! Lines also straightens slightly curved runs and makes arcs of parts of circles.
//!
//! With **Create: Strokes**, the parts of a colour layer no wider than the stroke width are traced
//! as stroked centre lines instead (see `centerline`); without Fills, wider parts become stroked
//! outlines.
//! as stroked centre lines instead (see `centerline`): placed in the middle of their ink to a
//! fraction of a pixel, as wide as their ink, and fitted as outlines are. Without Fills, wider
//! parts become stroked outlines.
//!
//! Colour layers are traced either *abutting* (each colour's own area; shapes share edges) or
//! *overlapping* (stacked: each layer also covers every layer above it, so no hairline gaps).
@@ -44,9 +45,11 @@
mod centerline;
mod contour;
mod fit;
mod group;
mod logo;
mod mosaic;
mod quantize;
mod shapes;
mod smooth;
mod subpixel;
@@ -58,6 +61,7 @@ use vectorcraft_geom::PathData;
pub use contour::{Component, Loop, trace_mask};
pub use mosaic::mosaic;
pub use quantize::{FULL_TONE_MAX, Quantized, TRANSPARENT, denoise, quantize};
pub use shapes::TracedShape;
use subpixel::Crack;
/// Errors decoding a raster.
@@ -269,11 +273,20 @@ pub struct TraceParams {
/// `noise / 10` pixels of the source image.)
pub noise: u32,
pub method: Method,
/// Drop white areas (no white background shape).
/// Ignore Color: drop the areas of one colour (no background shape): white, unless
/// `ignore_color` names another.
pub ignore_white: bool,
/// The colour Ignore Color leaves out (`#rrggbb`): the palette colours within about 30 levels
/// of it. `None` is white. (Flat Logo mode leaves out white only.)
pub ignore_color: Option<String>,
/// Replace nearly-straight curves with straight lines. In Logo mode this also snaps tight arcs
/// to true circles.
pub snap_curves_to_lines: bool,
/// Shapes: a filled outline that is a circle, an ellipse, a rectangle (square or rounded
/// corners) or a regular polygon comes out as exactly that shape ([`TracedPath::shape`]).
pub shapes: bool,
/// Auto Grouping: say which path lies inside which ([`TraceResult::parents`]).
pub auto_grouping: bool,
/// Create: trace areas as filled shapes.
pub fills: bool,
/// Create: trace lines no wider than `stroke_width` as stroked centre lines.
@@ -302,7 +315,10 @@ impl Default for TraceParams {
noise: 25,
method: Method::Abutting,
ignore_white: false,
ignore_color: None,
snap_curves_to_lines: false,
shapes: false,
auto_grouping: false,
fills: true,
strokes: false,
stroke_width: 10.0,
@@ -312,20 +328,10 @@ impl Default for TraceParams {
}
impl TraceParams {
/// Maximum distance (px) of polygon vertices from the pixel boundary.
fn polygon_tolerance(&self) -> f64 {
let f = (self.paths / 100.0).clamp(0.0, 1.0);
0.55 + 0.75 * (1.0 - f)
}
/// Curve fitting tolerance (px).
fn fit_tolerance(&self) -> f64 {
let f = (self.paths / 100.0).clamp(0.0, 1.0);
0.2 + 1.6 * (1.0 - f)
}
/// How outlines are fitted: Paths sets how closely the curves follow the edge (0.08 px at 100,
/// 0.33 px at 50, 0.78 px at 0), Corners the turn that makes a corner (35° at 100, 90° at 0).
/// Straight edges are always lines; Snap Curves To Lines also straightens slightly curved ones
/// and makes parts of circles true arcs.
/// Straight edges are always lines; Snap Curves To Lines also straightens slightly curved ones,
/// makes parts of circles true arcs, and makes lines within 2.5° of level or upright exactly so.
fn outline_style(&self) -> logo::Style {
// Parameters come from users and agents: a NaN or infinity must not reach the geometry.
let unit = |v: f64, default: f64| if v.is_finite() { (v / 100.0).clamp(0.0, 1.0) } else { default };
@@ -340,19 +346,16 @@ impl TraceParams {
circle_tol: if snap { 0.3 } else { 0.15 },
restore: Some(logo::RestoreParams::default()),
check_step: 0.15,
upright: snap,
..logo::Style::default()
}
}
/// Turn angle (degrees) above which a polygon vertex stays a corner.
fn corner_angle(&self) -> f64 {
let c = (self.corners / 100.0).clamp(0.0, 1.0);
150.0 - 115.0 * c
}
}
/// Built-in preset names in panel order (our own parameter sets).
pub const PRESET_NAMES: &[&str] = &[
"Default",
"Auto Color",
"High Fidelity Photo",
"Low Fidelity Photo",
"3 Colors",
@@ -364,6 +367,7 @@ pub const PRESET_NAMES: &[&str] = &[
"Silhouettes",
"Line Art",
"Technical Drawing",
"Outline",
"Flat Logo",
];
@@ -392,6 +396,10 @@ pub fn preset(name: &str) -> Option<TraceParams> {
};
Some(match key.as_str() {
"default" => d,
// A posterised image of a photo or of artwork: its own colours where it is flat art, as
// many as its tones need where it is not.
// (Color detail 85: a colour on a hundredth of the picture is still one of its colours.)
"auto color" => TraceParams { palette: Palette::Automatic, color_detail: 85.0, ..color(6, 65.0, 60.0, 16, Method::Abutting) },
"high fidelity photo" => color(64, 90.0, 25.0, 4, Method::Overlapping),
"low fidelity photo" => color(20, 60.0, 50.0, 12, Method::Overlapping),
"3 colors" => color(3, 60.0, 60.0, 20, Method::Abutting),
@@ -403,6 +411,8 @@ pub fn preset(name: &str) -> Option<TraceParams> {
"silhouettes" => bw(200, 40.0, 60.0, 30, false),
"line art" => bw(128, 80.0, 70.0, 10, false),
"technical drawing" => bw(128, 95.0, 90.0, 4, true),
// Black outlines: lines as stroked centre lines, wider areas as stroked outlines.
"outline" => TraceParams { fills: false, strokes: true, ..bw(128, 80.0, 70.0, 10, false) },
// Flat-colour logos: colours found automatically, edges placed to a fraction of a pixel.
"flat logo" => TraceParams { mode: Mode::Logo, colors: 8, paths: 60.0, corners: 50.0, noise: 6, snap_curves_to_lines: true, ..d },
_ => return None,
@@ -426,6 +436,8 @@ pub struct TracedPath {
/// Create Strokes: the path is stroked this wide (px), not filled (centre lines, or the
/// outlines of wider areas without Fills).
pub stroke: Option<f64>,
/// Shapes: the shape the path is, exactly (`path` is that shape's outline).
pub shape: Option<TracedShape>,
}
/// The traced result, bottom-most path first.
@@ -434,6 +446,9 @@ pub struct TraceResult {
pub paths: Vec<TracedPath>,
/// Colours actually used.
pub palette: Vec<[u8; 3]>,
/// Auto Grouping: for each of `paths`, the path it lies inside, if any (empty without the
/// option). Each path drawn before those inside it paints what `paths` paints in its order.
pub parents: Vec<Option<usize>>,
}
impl TraceResult {
@@ -446,6 +461,14 @@ fn is_white(c: [u8; 3]) -> bool {
c.iter().all(|&v| v >= 245)
}
/// Whether Ignore Color leaves palette colour `c` out: `target` is the colour picked (`None`: white).
fn is_ignored(c: [u8; 3], target: Option<[u8; 3]>) -> bool {
match target {
None => is_white(c),
Some(t) => (0..3).map(|k| (f64::from(c[k]) - f64::from(t[k])).powi(2)).sum::<f64>() <= 30.0 * 30.0 || (is_white(t) && is_white(c)),
}
}
/// Trace `img` with `params`. Failures (an image Logo mode refuses, say) give an empty result: use
/// [`trace_within`] to see why.
pub fn trace(img: &Raster, params: &TraceParams) -> TraceResult {
@@ -461,9 +484,25 @@ pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> R
if w == 0 || h == 0 || img.rgba.len() != w * h * 4 {
return Ok(TraceResult::default());
}
let mut out = trace_paths(img, params, max_anchors)?;
if params.auto_grouping {
// Abutting areas cannot overlap; stacked layers, Logo's shapes and stroked lines can.
let stacked = params.method == Method::Overlapping || params.mode == Mode::Logo || params.strokes;
out.parents = group::parents(&out.paths, img.width as usize, img.height as usize, stacked);
}
Ok(out)
}
fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> {
let (w, h) = (img.width as usize, img.height as usize);
if params.mode == Mode::Logo {
return logo::trace_logo(img, params, max_anchors);
}
// The colour Ignore Color leaves out, if not white.
let ignore = match &params.ignore_color {
Some(name) if params.ignore_white => Some(logo::parse_hex(name).ok_or_else(|| TraceError::BadPalette(name.clone()))?),
_ => None,
};
// A photo is traced from a copy with its noise smoothed away (see `smooth`).
let smoothed = (params.mode != Mode::BlackAndWhite && quantize::continuous_tone(img)).then(|| smooth::smoothed(img));
let img = smoothed.as_ref().unwrap_or(img);
@@ -500,21 +539,16 @@ pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> R
rank[i] = r;
}
let layers: Vec<(usize, usize)> =
order.iter().copied().enumerate().filter(|&(_, ci)| !(params.ignore_white && is_white(q.palette[ci]))).collect();
order.iter().copied().enumerate().filter(|&(_, ci)| !(params.ignore_white && is_ignored(q.palette[ci], ignore))).collect();
let job = Layers {
q: &q,
rank: &rank,
params,
img,
ignore,
boxes,
bw_tones: bw_tones(img, &q),
style: params.outline_style(),
opts: fit::FitOptions {
polygon_tol: params.polygon_tolerance(),
fit_tol: params.fit_tolerance(),
corner_angle: params.corner_angle(),
snap_lines: params.snap_curves_to_lines,
},
w,
h,
anchors: AtomicUsize::new(0),
@@ -571,14 +605,14 @@ struct Layers<'a> {
rank: &'a [usize],
params: &'a TraceParams,
img: &'a Raster,
/// The colour Ignore Color leaves out, if not white.
ignore: Option<[u8; 3]>,
/// The box each palette entry's pixels lie in: `(x0, y0, x1, y1)`, the far sides excluded.
boxes: Vec<Option<(usize, usize, usize, usize)>>,
/// Black and White: the luminance of white and of black (see `bw_tones`).
bw_tones: [f64; 2],
/// How outlines are fitted.
/// How outlines and centre lines are fitted.
style: logo::Style,
/// How centre lines are fitted.
opts: fit::FitOptions,
w: usize,
h: usize,
/// Anchor points made so far, by every layer.
@@ -590,7 +624,7 @@ impl Layers<'_> {
/// Ignore White: white is no part of any layer (stacked layers reach under the layers above
/// them, and must not reach under white that is left out).
fn ignored(&self, label: u16) -> bool {
self.params.ignore_white && self.q.palette.get(label as usize).is_some_and(|c| is_white(*c))
self.params.ignore_white && self.q.palette.get(label as usize).is_some_and(|c| is_ignored(*c, self.ignore))
}
/// The region across a crack, for cutting outlines at their junctions: the outside pixel's
/// colour. Stacked layers end on many colours at once and share no edge: one neighbour.
@@ -736,13 +770,15 @@ impl Layers<'_> {
/// The paths of layer `r` (palette entry `ci`), or `None` once the anchors made pass the
/// maximum.
fn layer(&self, r: usize, ci: usize) -> Option<Vec<TracedPath>> {
let (params, opts) = (self.params, &self.opts);
let params = self.params;
let color = *self.q.palette.get(ci)?;
let overlapping = params.method == Method::Overlapping;
let (w, h) = (self.w, self.h);
// Create Strokes: this colour's lines, left out of its areas.
let (thin, lines) = if params.strokes {
centerline::lines(&self.q.labels, u16::try_from(ci).unwrap_or(TRANSPARENT), w, h, params.stroke_width, opts)
// The ink the lines are read against: the layer's colour; Black and White's own black.
let ink = if params.mode == Mode::BlackAndWhite { [self.bw_tones[usize::from(ci == 1)]; 3] } else { color.map(f64::from) };
centerline::lines(&self.q.labels, u16::try_from(ci).unwrap_or(TRANSPARENT), w, h, params.stroke_width, self.img, ink, &self.style)
} else {
(vec![], vec![])
};
@@ -767,12 +803,12 @@ impl Layers<'_> {
let area_stroke = (params.strokes && !params.fills).then_some(1.0);
let min_hole = params.noise.max(1) as i64;
let mut paths = vec![];
let mut add = |path: PathData, pixels: usize, stroke: Option<f64>| {
let mut add = |path: PathData, pixels: usize, stroke: Option<f64>, shape: Option<TracedShape>| {
let n = path.anchor_count();
if self.anchors.fetch_add(n, Ordering::Relaxed).saturating_add(n) > self.max_anchors {
return false;
}
paths.push(TracedPath { path, color, pixels, stroke });
paths.push(TracedPath { path, color, pixels, stroke, shape });
true
};
let mut comps = trace_mask(&mask, bw, bh);
@@ -794,12 +830,18 @@ impl Layers<'_> {
}
subs.extend(fit(hole));
}
if !add(PathData::new(subs), comp.pixels, area_stroke) {
// Shapes: a filled outline without holes that is a shape becomes that shape.
let shape = match subs.as_slice() {
[only] if params.shapes && area_stroke.is_none() => shapes::recognise(only),
_ => None,
};
let path = shape.map_or_else(|| PathData::new(subs), |s| s.path());
if !add(path, comp.pixels, area_stroke, shape) {
return None;
}
}
for line in lines {
if !add(line.path, line.pixels, Some(line.width)) {
if !add(line.path, line.pixels, Some(line.width), None) {
return None;
}
}
+1
View File
@@ -26,5 +26,6 @@ mod unmix;
pub(crate) use geom::{Cubic, all_within, resample_closed, resample_open, sample_cubics};
pub(crate) use outline::{Stats, Style, fit_open, fit_outline};
pub(crate) use palette::parse_hex;
pub(crate) use pipeline::trace_logo;
pub(crate) use restore::RestoreParams;
+4
View File
@@ -25,6 +25,9 @@ pub(crate) struct Style {
pub max_dev: f64,
/// Spacing of the points of the fit that this check looks at.
pub check_step: f64,
/// Straight pieces within a few degrees of level or upright are made exactly so (by the
/// tracer's other modes; Flat Logo leaves them).
pub upright: bool,
}
impl Default for Style {
@@ -42,6 +45,7 @@ impl Default for Style {
restore: Some(RestoreParams::default()),
max_dev: 1.4,
check_step: 0.03,
upright: false,
}
}
}
+7 -1
View File
@@ -478,7 +478,13 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
if !out.palette.contains(&color) {
out.palette.push(color);
}
out.paths.push(TracedPath { path: PathData::new(subs), color, pixels: (s.area * (factor * factor) as f64) as usize, stroke: None });
out.paths.push(TracedPath {
path: PathData::new(subs),
color,
pixels: (s.area * (factor * factor) as f64) as usize,
stroke: None,
shape: None,
});
}
Ok(out)
}
+240
View File
@@ -0,0 +1,240 @@
//! Shapes: traced outlines that are a circle, an ellipse, a rectangle (its corners square or
//! rounded) or a regular polygon are told so, and come out as exactly that shape.
//!
//! An outline is a shape when every point of it lies within a small tolerance of the shape that
//! its own area, middle and extents describe: nothing is taken for a shape that it merely
//! resembles.
use vectorcraft_geom::{Affine, ParamCurve, PathData, Point, Rect, SubPath, shapes};
/// A shape an outline was found to be (pixel coordinates; angles in degrees, clockwise on screen).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TracedShape {
/// A rectangle `width` × `height` about `center`, turned by `rotation`, its corners rounded
/// with `radius` (0: square corners).
Rectangle { center: Point, width: f64, height: f64, radius: f64, rotation: f64 },
/// An ellipse with radii `rx` and `ry` about `center`, turned by `rotation` (a circle: equal
/// radii, no rotation).
Ellipse { center: Point, rx: f64, ry: f64, rotation: f64 },
/// A regular polygon: its first corner `rotation` from straight up.
Polygon { center: Point, radius: f64, sides: u32, rotation: f64 },
}
impl TracedShape {
/// The shape's outline.
pub fn path(&self) -> PathData {
let turned = |p: PathData, c: Point, deg: f64| p.transformed(Affine::rotate_about(deg.to_radians(), c));
match *self {
TracedShape::Rectangle { center, width, height, radius, rotation } => {
let r = Rect::from_center_size(center, (width, height));
turned(if radius > 0.0 { shapes::rounded_rectangle(r, radius) } else { shapes::rectangle(r) }, center, rotation)
}
TracedShape::Ellipse { center, rx, ry, rotation } => {
turned(shapes::ellipse(Rect::from_center_size(center, (2.0 * rx, 2.0 * ry))), center, rotation)
}
TracedShape::Polygon { center, radius, sides, rotation } => shapes::polygon(center, radius, sides, rotation),
}
}
}
/// How far a point may lie from the shape: half a pixel, and for large shapes 0.2% of their size.
fn tolerance(size: f64) -> f64 {
(0.002 * size).max(0.5)
}
/// The shape the closed outline `sp` is, if it is one (and at least 6 px across).
pub(crate) fn recognise(sp: &SubPath) -> Option<TracedShape> {
if !sp.closed || sp.anchors.len() < 2 {
return None;
}
// Points all along the outline.
let pts: Vec<Point> = (0..sp.segment_count()).flat_map(|i| (0..24).map(move |k| sp.segment(i).eval(f64::from(k) / 24.0))).collect();
let n = pts.len();
if n < 8 || !pts.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
return None;
}
// Area, middle and second moments of the polygon through them.
let (mut area2, mut cx, mut cy) = (0.0, 0.0, 0.0);
for i in 0..n {
let (a, b) = (pts[i], pts[(i + 1) % n]);
let cross = a.x * b.y - b.x * a.y;
area2 += cross;
cx += (a.x + b.x) * cross;
cy += (a.y + b.y) * cross;
}
let area = area2.abs() / 2.0;
if area < 20.0 {
return None;
}
let c = Point::new(cx / (3.0 * area2), cy / (3.0 * area2));
let (mut ixx, mut iyy, mut ixy) = (0.0, 0.0, 0.0);
for i in 0..n {
let (a, b) = (pts[i] - c, pts[(i + 1) % n] - c);
let cross = a.x * b.y - b.x * a.y;
ixx += (a.x * a.x + a.x * b.x + b.x * b.x) * cross;
iyy += (a.y * a.y + a.y * b.y + b.y * b.y) * cross;
ixy += (a.x * b.y + 2.0 * a.x * a.y + 2.0 * b.x * b.y + b.x * a.y) * cross;
}
// Per unit of area: the variances of x and y and their covariance.
let (sxx, syy, sxy) = (ixx / (6.0 * area2), iyy / (6.0 * area2), ixy / (12.0 * area2));
let mean = (sxx + syy) / 2.0;
let spread = ((sxx - syy) / 2.0).hypot(sxy);
let (major, minor) = (mean + spread, (mean - spread).max(0.0));
// The direction of the longer axis (none when the shape is as wide every way).
let axis = if spread < 0.01 * mean { 0.0 } else { 0.5 * (2.0 * sxy).atan2(sxx - syy) };
let size = 4.0 * major.sqrt();
let tol = tolerance(size);
if size < 6.0 || !size.is_finite() {
return None;
}
let local = |p: Point, angle: f64| {
let d = p - c;
(d.x * angle.cos() + d.y * angle.sin(), d.y * angle.cos() - d.x * angle.sin())
};
// An ellipse: its radii are twice the roots of its variances.
let (rx, ry) = (2.0 * major.sqrt(), 2.0 * minor.sqrt());
if ry > 2.0 {
let off = pts.iter().map(|&p| {
let (x, y) = local(p, axis);
// Scaled to a circle: how far from it, in pixels of the larger radius (no nearer than
// that to the ellipse: a long thin bar is no flat ellipse).
((x / rx).hypot(y / ry) - 1.0).abs() * rx
});
if off.fold(0.0, f64::max) <= tol {
// Radii this close are a circle's, of the outline's area: a circle without anti-aliasing
// is fitted a little wider one way than the other.
let circle = (rx - ry).abs() <= 2.0 * tol;
let (rx, ry) = if circle { ((rx * ry).sqrt(), (rx * ry).sqrt()) } else { (rx, ry) };
return Some(TracedShape::Ellipse { center: c, rx, ry, rotation: if circle { 0.0 } else { axis.to_degrees() } });
}
}
// A rectangle, along its longest straight side (a square has no longer axis to go by).
let side = (0..sp.segment_count())
.filter(|&i| sp.segment_is_line(i))
.map(|i| sp.segment(i))
.max_by(|a, b| a.p0.distance(a.p3).total_cmp(&b.p0.distance(b.p3)));
if let Some(side) = side {
let d = side.p3 - side.p0;
// Turned by under 45° either way: a rectangle is the same a quarter turn on.
let quarter = std::f64::consts::FRAC_PI_2;
let angle = d.y.atan2(d.x);
let angle = angle - quarter * (angle / quarter).round();
let xy: Vec<(f64, f64)> = pts.iter().map(|&p| local(p, angle)).collect();
let (hw, hh) = xy.iter().fold((0.0f64, 0.0f64), |m, p| (m.0.max(p.0.abs()), m.1.max(p.1.abs())));
// Square corners first; else rounded ones, which take (4 − π) r² off the area.
let rounded = ((4.0 * hw * hh - area) / (4.0 - std::f64::consts::PI)).max(0.0).sqrt().min(hw.min(hh));
for radius in [0.0, rounded] {
let off = xy.iter().map(|&(x, y)| {
// Distance to the rounded rectangle's edge.
let (qx, qy) = (x.abs() - hw + radius, y.abs() - hh + radius);
(qx.max(0.0).hypot(qy.max(0.0)) + qx.max(qy).min(0.0) - radius).abs()
});
if hw > 2.0 && hh > 2.0 && (radius == 0.0 || radius >= 0.75) && off.fold(0.0, f64::max) <= tol {
return Some(TracedShape::Rectangle { center: c, width: 2.0 * hw, height: 2.0 * hh, radius, rotation: angle.to_degrees() });
}
}
}
// A regular polygon: its corners evenly round the middle, its sides straight between them.
let sides = sp.anchors.len();
if (3..=12).contains(&sides) {
let radius = sp.anchors.iter().map(|a| a.p.distance(c)).sum::<f64>() / sides as f64;
let first = sp.anchors.first()?.p - c;
let rotation = (first.y.atan2(first.x).to_degrees() + 90.0).rem_euclid(360.0 / sides as f64);
let shape = TracedShape::Polygon { center: c, radius, sides: sides as u32, rotation };
let ideal: Vec<Point> = shape.path().subpaths.first()?.anchors.iter().map(|a| a.p).collect();
// Every corner of the polygon has one of the outline's on it, and every point of the
// outline lies on one of the polygon's sides.
let cornered = ideal.iter().all(|q| sp.anchors.iter().any(|a| q.distance(a.p) <= 1.5 * tol));
let on_a_side = |p: Point| {
(0..ideal.len()).any(|i| {
let (a, b) = (ideal[i], ideal[(i + 1) % ideal.len()]);
let t = ((p - a).dot(b - a) / (b - a).hypot2()).clamp(0.0, 1.0);
p.distance(a + (b - a) * t) <= tol
})
};
if cornered && pts.iter().all(|&p| on_a_side(p)) {
// Four equal sides are a square: a rectangle, its sides half a right angle from its corners.
let side = radius * std::f64::consts::SQRT_2;
let turned = (rotation + 45.0).rem_euclid(90.0);
let square = TracedShape::Rectangle {
center: c,
width: side,
height: side,
radius: 0.0,
rotation: if turned > 45.0 { turned - 90.0 } else { turned },
};
return Some(if sides == 4 { square } else { shape });
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn is(shape: TracedShape) -> Option<TracedShape> {
recognise(shape.path().subpaths.first().unwrap())
}
#[test]
fn each_shape_is_told_from_its_own_outline() {
let c = Point::new(60.3, 45.6);
match is(TracedShape::Ellipse { center: c, rx: 20.0, ry: 20.0, rotation: 0.0 }) {
Some(TracedShape::Ellipse { center, rx, ry, rotation }) => {
assert!(center.distance(c) < 0.05 && (rx - 20.0).abs() < 0.05 && rx == ry && rotation == 0.0, "{center:?} {rx} {ry}")
}
other => panic!("{other:?}"),
}
match is(TracedShape::Ellipse { center: c, rx: 30.0, ry: 12.0, rotation: 25.0 }) {
Some(TracedShape::Ellipse { rx, ry, rotation, .. }) => {
assert!((rx - 30.0).abs() < 0.3 && (ry - 12.0).abs() < 0.3 && (rotation - 25.0).abs() < 0.5, "{rx} {ry} {rotation}")
}
other => panic!("{other:?}"),
}
match is(TracedShape::Rectangle { center: c, width: 50.0, height: 20.0, radius: 0.0, rotation: 10.0 }) {
Some(TracedShape::Rectangle { width, height, radius, rotation, .. }) => {
assert!((width - 50.0).abs() < 0.1 && (height - 20.0).abs() < 0.1 && radius == 0.0 && (rotation - 10.0).abs() < 0.1)
}
other => panic!("{other:?}"),
}
match is(TracedShape::Rectangle { center: c, width: 50.0, height: 30.0, radius: 8.0, rotation: 0.0 }) {
Some(TracedShape::Rectangle { width, radius, .. }) => {
assert!((width - 50.0).abs() < 0.2 && (radius - 8.0).abs() < 0.5, "{width} {radius}")
}
other => panic!("{other:?}"),
}
for sides in [3, 5, 6, 8] {
match is(TracedShape::Polygon { center: c, radius: 25.0, sides, rotation: 7.0 }) {
Some(TracedShape::Polygon { radius, sides: n, rotation, .. }) => {
assert!(n == sides && (radius - 25.0).abs() < 0.1 && (rotation - 7.0).abs() < 0.1, "{sides}: {radius} {rotation}")
}
other => panic!("{sides} sides: {other:?}"),
}
}
}
#[test]
fn what_only_resembles_a_shape_is_none() {
// A star, an L, a rectangle with one corner cut, an egg.
let star = shapes::star(Point::new(50.0, 50.0), 30.0, 12.0, 5, 0.0);
assert_eq!(recognise(&star.subpaths[0]), None);
let poly = |pts: &[(f64, f64)]| SubPath::polyline(&pts.iter().map(|&p| Point::from(p)).collect::<Vec<_>>(), true);
assert_eq!(recognise(&poly(&[(0.0, 0.0), (40.0, 0.0), (40.0, 15.0), (15.0, 15.0), (15.0, 40.0), (0.0, 40.0)])), None);
assert_eq!(recognise(&poly(&[(0.0, 0.0), (40.0, 0.0), (40.0, 22.0), (34.0, 30.0), (0.0, 30.0)])), None);
let mut egg = shapes::ellipse(Rect::new(10.0, 10.0, 70.0, 50.0));
egg.subpaths[0].anchors[2].p.x += 4.0;
assert_eq!(recognise(&egg.subpaths[0]), None);
// Too small to say, open, or not a number.
assert_eq!(is(TracedShape::Ellipse { center: Point::new(5.0, 5.0), rx: 2.0, ry: 2.0, rotation: 0.0 }), None);
let mut open = shapes::rectangle(Rect::new(0.0, 0.0, 30.0, 20.0)).subpaths[0].clone();
open.closed = false;
assert_eq!(recognise(&open), None);
let mut bad = shapes::rectangle(Rect::new(0.0, 0.0, 30.0, 20.0)).subpaths[0].clone();
bad.anchors[1].p.x = f64::NAN;
assert_eq!(recognise(&bad), None);
}
}
+54 -1
View File
@@ -173,6 +173,56 @@ pub(crate) fn outline(
};
let step: Vec<bool> = (0..n).map(|i| edges[i].len == 1 && turn(i) * turn(i + 1) < 0).collect();
let outward = |d: (i32, i32)| Vec2::new(f64::from(d.1), f64::from(-d.0));
// The pixel at a corner of the shape lies on both of the corner's edges, and what it lacks of
// the inside's colour is the doing of both: read as one edge's alone it dents the corner. The
// crack of each edge on it takes the shift of the next crack along that edge instead (edges
// of three pixels or more: a shorter one has no pixel clear of its corners).
let shifts = {
let mut shifts = shifts;
for i in 0..n {
let (b, e) = ((i + n - 1) % n, i);
if step[b] || step[e] || turn(i) == 0 {
continue;
}
let (before, edge) = (&edges[b], &edges[e]);
if before.len >= 3 {
shifts[before.first + before.len - 1] = shifts[before.first + before.len - 2];
}
if edge.len >= 3 {
shifts[edge.first] = shifts[edge.first + 1];
}
}
shifts
};
// A sharp corner whose own pixel is less than half the inside's colour goes to the outside, and
// the loop cuts the corner in two steps (the same, turned round, where the corner pixel of a
// notch goes to the inside). Between two edges of three pixels or more, those two steps are
// that corner: it lies where the two edges, each moved by its shift, meet. `notch[i]` is, for
// the first of the two steps, the corner (`Some(Some(..))`), for the second nothing
// (`Some(None)`), for any other edge `None`.
let mut notch: Vec<Option<Option<Point>>> = vec![None; n];
for i in 0..n {
let (a, s1, s2, b) = ((i + n - 1) % n, i, (i + 1) % n, (i + 2) % n);
let free = |e: usize| junctions[edges[e].first].is_none();
if n >= 6
&& step[s1]
&& step[s2]
&& edges[a].len >= 3
&& edges[b].len >= 3
&& edges[s1].dir == edges[b].dir
&& edges[s2].dir == edges[a].dir
&& free(s1)
&& free(s2)
&& free(b)
{
let (ea, eb) = (&edges[a], &edges[b]);
// Edge `a` carried on one pixel further meets edge `b` carried back one.
let meet = at(s1) + Vec2::new(f64::from(ea.dir.0), f64::from(ea.dir.1));
notch[s1] = Some(Some(meet + outward(ea.dir) * shifts[ea.first + ea.len - 1] + outward(eb.dir) * shifts[eb.first]));
notch[s2] = Some(None);
}
}
let mut pieces: Vec<Piece> = Vec::new();
let start = first.unwrap_or(0);
@@ -196,7 +246,10 @@ pub(crate) fn outline(
let before = &edges[(i + n - 1) % n];
pts.push(corner + outward(before.dir) * shifts[(crack + cracks - 1) % cracks] + outward(e.dir) * shifts[crack]);
}
if step[i] || !hard {
if !hard && let Some(sharp) = notch[i] {
// A corner whose own pixel went to the other side: the corner, not the two steps.
pts.extend(sharp);
} else if step[i] || !hard {
pts.push(corner + Vec2::new(f64::from(e.dir.0), f64::from(e.dir.1)) * 0.5 + outward(e.dir) * shifts[crack]);
}
}
+180 -2
View File
@@ -255,7 +255,7 @@ fn snap_curves_to_lines_straightens_near_lines() {
#[test]
fn presets_resolve() {
assert_eq!(presets().len(), 13);
assert_eq!(presets().len(), 15);
for n in PRESET_NAMES {
assert!(preset(n).is_some(), "{n}");
}
@@ -608,7 +608,7 @@ fn create_strokes_survives_noise_and_tiny_images() {
for noise in [0, 1, 4] {
let p = TraceParams { strokes: true, fills, stroke_width: width, noise, ..TraceParams::default() };
for t in trace(&img, &p).paths {
assert!(t.stroke.is_none_or(|w| w.is_finite() && w >= 1.0), "{:?}", t.stroke);
assert!(t.stroke.is_none_or(|w| w.is_finite() && w >= 0.25), "{:?}", t.stroke);
}
}
}
@@ -1072,3 +1072,181 @@ fn a_blurred_thin_line_on_unevenly_lit_paper_is_a_few_anchors() {
let off = along(path).iter().filter(|p| p.x > 14.0 && p.x < 146.0).map(|p| ((p.y - (17.3 + 0.06 * p.x)).abs() - 1.0).abs()).fold(0.0, f64::max);
assert!(off < 0.5, "edges {off} px from where the ink's width puts them");
}
#[test]
fn ignore_color_leaves_out_the_colour_picked() {
// Navy field, yellow disc, green bar.
let img = three_colour_art(0.0);
let params = TraceParams { colors: 3, ignore_white: true, ..sixteen_colors() };
// White by default: nothing here is white.
assert_eq!(trace(&img, &params).paths.len(), 3);
// The navy picked a little off: the field goes, the disc and the bar stay.
let res = trace(&img, &TraceParams { ignore_color: Some("#18263c".replace("3c", "5e")), ..params.clone() });
assert_eq!(res.paths.len(), 2);
assert!(res.paths.iter().all(|p| p.color != [20, 40, 90]));
// Not ticked, the colour is not left out; and a colour that is none is refused.
assert_eq!(trace(&img, &TraceParams { ignore_white: false, ignore_color: Some("#14285a".into()), ..params.clone() }).paths.len(), 3);
let bad = trace_within(&img, &TraceParams { ignore_color: Some("navy".into()), ..params.clone() }, usize::MAX);
assert!(matches!(bad, Err(TraceError::BadPalette(_))));
// Old documents have no such setting: white.
let old: TraceParams = serde_json::from_str(r#"{"mode":"color","ignoreWhite":true}"#).unwrap();
assert_eq!(old.ignore_color, None);
assert_eq!(serde_json::to_value(&params).unwrap()["ignoreColor"], serde_json::Value::Null);
}
#[test]
fn snap_curves_to_lines_levels_a_rectangle_a_degree_askew() {
// 120 × 60, turned 1.5° about its centre.
let (c, t) = (Point::new(100.3, 60.6), 1.5f64.to_radians());
let img = soft(200, 120, |x, y| {
let (dx, dy) = (x - c.x, y - c.y);
(dx * t.cos() + dy * t.sin()).abs() <= 60.0 && (dy * t.cos() - dx * t.sin()).abs() <= 30.0
});
let sides = |snap: bool| {
let res = trace(&img, &TraceParams { snap_curves_to_lines: snap, ..bw() });
let sp = res.paths[0].path.subpaths[0].clone();
assert_eq!(sp.anchors.len(), 4, "{sp:?}");
(0..4).map(|i| sp.segment(i)).map(|s| ((s.p3.x - s.p0.x).abs(), (s.p3.y - s.p0.y).abs())).collect::<Vec<_>>()
};
// Snapped, every side is exactly level or upright; left alone, it keeps its 1.5°.
assert!(sides(true).iter().all(|(dx, dy)| *dx == 0.0 || *dy == 0.0), "{:?}", sides(true));
assert!(sides(false).iter().all(|(dx, dy)| dx.min(*dy) > 0.5), "{:?}", sides(false));
// And it is still the rectangle: 120 by 60 about the same centre.
let b = trace(&img, &TraceParams { snap_curves_to_lines: true, ..bw() }).paths[0].path.bounds().unwrap();
assert!((b.width() - 120.0).abs() < 1.0 && (b.height() - 60.0).abs() < 1.0 && (b.center().x - c.x).abs() < 0.3, "{b:?}");
// A side 5° off is no slip of the scanner: it stays.
let t = 5f64.to_radians();
let img = soft(200, 120, |x, y| {
let (dx, dy) = (x - c.x, y - c.y);
(dx * t.cos() + dy * t.sin()).abs() <= 60.0 && (dy * t.cos() - dx * t.sin()).abs() <= 30.0
});
let sp = trace(&img, &TraceParams { snap_curves_to_lines: true, ..bw() }).paths[0].path.subpaths[0].clone();
assert!(
(0..sp.segment_count())
.all(|i| (sp.segment(i).p3.y - sp.segment(i).p0.y).abs() > 1.0 && (sp.segment(i).p3.x - sp.segment(i).p0.x).abs() > 1.0)
);
}
#[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.
let (a, b) = (Point::new(20.3, 30.7), Point::new(170.6, 52.2));
let along_line = |p: Point| ((p - a).dot(b - a) / (b - a).hypot2()).clamp(0.0, 1.0);
let img = soft(190, 80, |x, y| {
let p = Point::new(x, y);
p.distance(a + (b - a) * along_line(p)) <= 1.3
});
for noisy in [false, true] {
// (With grain of ±6 levels too: the line stays two anchors.)
let img = Raster::from_fn(190, 80, |x, y| {
let v = f64::from(img.pixel(x, y)[0]) + if noisy { 6.0 * jitter(x, y, 3) } else { 0.0 };
let v = v.round().clamp(0.0, 255.0) as u8;
[v, v, v, 255]
});
let res = trace(&img, &preset("Outline").unwrap());
assert_eq!(res.paths.len(), 1);
let (path, width) = (&res.paths[0].path, res.paths[0].stroke.unwrap());
assert_eq!(path.anchor_count(), 2, "noisy {noisy}: {path:?}");
assert!((width - 2.6).abs() < 0.15, "noisy {noisy}: {width} px wide");
for end in [path.subpaths[0].anchors[0].p, path.subpaths[0].anchors[1].p] {
// On the line through the true ends, to a tenth of a pixel.
let off = (end - a).cross(b - a).abs() / (b - a).hypot();
assert!(off < 0.1, "noisy {noisy}: an end {off} px off the line's middle");
// And where the ink ends, less the half width its round cap adds (the line here has round ends).
assert!(end.distance(a).min(end.distance(b)) < 0.6, "noisy {noisy}: an end at {end:?}");
}
}
}
#[test]
fn a_stroke_is_as_wide_as_its_ink_not_a_whole_number_of_pixels() {
// (Lines with a pixel that is all ink somewhere: a thinner one's ink is read against its own
// darkest pixel, and comes out a little wide.)
for want in [1.7, 2.4, 3.4] {
let img = soft(120, 40, |x, y| (10.0..110.0).contains(&x) && (y - 20.3).abs() <= want / 2.0);
let res = trace(&img, &preset("Outline").unwrap());
let width = res.paths[0].stroke.unwrap();
assert!((width - want).abs() < 0.08, "{width} for a line {want} px wide");
}
}
#[test]
fn an_anti_aliased_rectangle_along_the_pixel_grid_is_four_anchors() {
// Its sides lie between pixel columns and rows: its corner pixels are part of two sides each.
let img = soft(140, 90, |x, y| (20.4..120.7).contains(&x) && (15.3..70.6).contains(&y));
let res = trace(&img, &bw());
let sp = &res.paths[0].path.subpaths[0];
assert_eq!(sp.anchors.len(), 4, "{sp:?}");
assert!((0..4).all(|i| sp.segment_is_line(i)));
let b = res.paths[0].path.bounds().unwrap();
// (To the eighth of a pixel the test picture is drawn to.)
assert!((b.x0 - 20.4).abs() < 0.07 && (b.x1 - 120.7).abs() < 0.07 && (b.y0 - 15.3).abs() < 0.07 && (b.y1 - 70.6).abs() < 0.07, "{b:?}");
}
#[test]
fn shapes_come_out_as_shapes_when_asked_for() {
// A disc, a square along the pixel grid and a rectangle turned 20°, anti-aliased.
let turn = 20f64.to_radians();
let turned = |x: f64, y: f64| {
let (dx, dy) = (x - 180.4, y - 45.3);
(dx * turn.cos() + dy * turn.sin()).abs() <= 30.0 && (dy * turn.cos() - dx * turn.sin()).abs() <= 18.0
};
let img = soft(220, 90, |x, y| {
Point::new(x, y).distance(Point::new(40.3, 44.6)) <= 25.0 || ((80.4..130.7).contains(&x) && (20.3..70.6).contains(&y)) || turned(x, y)
});
let plain = trace(&img, &bw());
assert!(plain.paths.iter().all(|p| p.shape.is_none()));
let res = trace(&img, &TraceParams { shapes: true, ..bw() });
assert_eq!(res.paths.len(), 3);
let mut found = (false, false, false);
for p in &res.paths {
// The path is the shape's own outline.
assert_eq!(Some(&p.path), p.shape.map(|s| s.path()).as_ref());
match p.shape {
Some(TracedShape::Ellipse { center, rx, ry, .. }) => {
found.0 = center.distance(Point::new(40.3, 44.6)) < 0.15 && (rx - 25.0).abs() < 0.15 && rx == ry
}
Some(TracedShape::Rectangle { center, width, height, radius: 0.0, rotation }) if center.x < 150.0 => {
found.1 = center.distance(Point::new(105.55, 45.45)) < 0.15
&& (width - 50.3).abs() < 0.25
&& (height - 50.3).abs() < 0.25
&& rotation.abs() < 0.3
}
Some(TracedShape::Rectangle { center, width, height, radius: 0.0, rotation }) => {
found.2 = center.distance(Point::new(180.4, 45.3)) < 0.2
&& (width - 60.0).abs() < 0.3
&& (height - 36.0).abs() < 0.3
&& (rotation - 20.0).abs() < 0.3
}
other => panic!("{other:?}"),
}
}
assert_eq!(found, (true, true, true));
// A letter is no shape, and a ring is not a disc.
let ring = soft(100, 100, |x, y| (20.0..40.0).contains(&Point::new(x, y).distance(Point::new(50.0, 50.0))));
assert!(trace(&ring, &TraceParams { shapes: true, ..bw() }).paths.iter().all(|p| p.shape.is_none()));
}
#[test]
fn a_corner_whose_pixel_is_mostly_outside_is_still_a_corner() {
// Each corner pixel is under half covered (0.6 × 0.7 and less) and traces as outside.
let img = soft(140, 90, |x, y| (20.4..120.6).contains(&x) && (15.3..70.7).contains(&y));
let res = trace(&img, &bw());
let sp = &res.paths[0].path.subpaths[0];
assert_eq!(sp.anchors.len(), 4, "{sp:?}");
assert!((0..4).all(|i| sp.segment_is_line(i)));
let b = res.paths[0].path.bounds().unwrap();
assert!((b.x0 - 20.4).abs() < 0.07 && (b.x1 - 120.6).abs() < 0.07 && (b.y0 - 15.3).abs() < 0.07 && (b.y1 - 70.7).abs() < 0.07, "{b:?}");
}
/// A circle without anti-aliasing is still a circle, not an ellipse a little wider than tall.
#[test]
fn a_hard_edged_circle_is_a_circle() {
let img = Raster::from_fn(100, 100, |x, y| {
let (dx, dy) = (x as f64 + 0.5 - 50.0, y as f64 + 0.5 - 50.0);
if dx * dx + dy * dy <= 900.0 { [0, 0, 0, 255] } else { [255, 255, 255, 255] }
});
let res = trace(&img, &TraceParams { shapes: true, ignore_white: true, ..Default::default() });
let [TracedPath { shape: Some(TracedShape::Ellipse { center, rx, ry, .. }), .. }] = res.paths.as_slice() else { panic!("{:?}", res.paths) };
assert!(rx == ry && (rx - 30.0).abs() < 0.3 && center.distance(Point::new(50.0, 50.0)) < 0.1, "{center:?} {rx} {ry}");
}
+86 -6
View File
@@ -2,7 +2,7 @@
//! sizes) and two continuous-tone images computed per pixel.
use kurbo::{Affine, BezPath, Circle, Ellipse, Point, RoundedRect, Shape};
use vectorcraft_trace::Raster;
use vectorcraft_trace::{Raster, TracedShape};
use crate::raster::{self, noise};
@@ -15,13 +15,18 @@ pub enum Class {
Poly,
/// Thin lines, 0.5 to 5 units wide.
Lines,
/// Straight-edged shapes and lines a degree or two off level and upright, as a scan or a
/// screenshot taken a little askew has them.
Askew,
/// Plain shapes apart from each other: a circle, an ellipse, rectangles, a regular polygon.
Plain,
/// Flat art in several colours, with regions that touch.
Flat,
/// Continuous tone: a gradient and a photo-like image.
Tone,
}
pub const CLASSES: [Class; 5] = [Class::Shape, Class::Poly, Class::Lines, Class::Flat, Class::Tone];
pub const CLASSES: [Class; 7] = [Class::Shape, Class::Poly, Class::Lines, Class::Askew, Class::Plain, Class::Flat, Class::Tone];
impl Class {
pub fn name(self) -> &'static str {
@@ -29,6 +34,8 @@ impl Class {
Class::Shape => "curved shapes",
Class::Poly => "polygons",
Class::Lines => "thin lines",
Class::Askew => "askew shapes",
Class::Plain => "plain shapes",
Class::Flat => "flat colour art",
Class::Tone => "gradient, photo",
}
@@ -78,6 +85,10 @@ pub struct Input {
pub anchors: usize,
/// Its sharp corners.
pub corners: Vec<Point>,
/// The plain shapes it is made of, where it is made of them.
pub plain: Vec<TracedShape>,
/// Its lines, where it is lines: each one's two ends and its width.
pub strokes: Vec<(Point, Point, f64)>,
tone: Option<Tone>,
}
@@ -166,9 +177,21 @@ pub fn inputs() -> Vec<Input> {
bg: WHITE,
anchors,
corners,
strokes: vec![],
plain: vec![],
tone: None,
};
let tone = |name, f: Tone| Input { name, class: Class::Tone, shapes: vec![], bg: WHITE, anchors: 0, corners: vec![], tone: Some(f) };
let tone = |name, f: Tone| Input {
name,
class: Class::Tone,
shapes: vec![],
bg: WHITE,
anchors: 0,
corners: vec![],
strokes: vec![],
plain: vec![],
tone: Some(f),
};
let ink = |p: BezPath| vec![(p, INK)];
let mut ellipses = Ellipse::new((90.3, 99.6), (60.0, 30.0), 0.0).to_path(1e-3);
@@ -208,13 +231,51 @@ pub fn inputs() -> Vec<Input> {
// Five lines 200 long, 0.5 to 5 wide, at small angles to the pixel grid.
let mut lines = BezPath::new();
let mut strokes = vec![];
for (i, (deg, width)) in [(0.0f64, 0.5), (6.0, 1.0), (-9.0, 2.0), (12.0, 3.0), (-4.0, 5.0)].into_iter().enumerate() {
let (hw, hl) = (width / 2.0, 100.0);
let mut strip = poly(&[(-hl, -hw), (hl, -hw), (hl, hw), (-hl, hw)]);
strip.apply_affine(Affine::translate((128.3, 36.4 + 46.0 * i as f64)) * Affine::rotate(deg.to_radians()));
let place = Affine::translate((128.3, 36.4 + 46.0 * i as f64)) * Affine::rotate(deg.to_radians());
strip.apply_affine(place);
strokes.push((place * Point::new(-hl, 0.0), place * Point::new(hl, 0.0), width));
lines.extend(strip);
}
// A rectangle 1.5° off level, a plus sign 1° off the other way, and two lines 1° and 2° off.
let turned = |p: BezPath, deg: f64, about: (f64, f64)| Affine::rotate_about(deg.to_radians(), Point::new(about.0, about.1)) * p;
let plus = [
(150.0, 150.0),
(170.0, 150.0),
(170.0, 180.0),
(200.0, 180.0),
(200.0, 200.0),
(170.0, 200.0),
(170.0, 230.0),
(150.0, 230.0),
(150.0, 200.0),
(120.0, 200.0),
(120.0, 180.0),
(150.0, 180.0),
];
let mut askew = turned(poly(&[(40.3, 30.6), (200.7, 30.6), (200.7, 110.4), (40.3, 110.4)]), 1.5, (120.0, 70.0));
askew.extend(turned(poly(&plus), -1.0, (160.0, 190.0)));
askew.extend(turned(poly(&[(20.0, 150.3), (100.0, 150.3), (100.0, 153.3), (20.0, 153.3)]), 1.0, (60.0, 152.0)));
askew.extend(turned(poly(&[(60.4, 170.0), (62.4, 170.0), (62.4, 240.0), (60.4, 240.0)]), 2.0, (61.0, 205.0)));
// A circle, a rectangle, a square, a rounded rectangle, an ellipse and a hexagon.
let plain = vec![
TracedShape::Ellipse { center: Point::new(50.3, 50.6), rx: 30.0, ry: 30.0, rotation: 0.0 },
TracedShape::Rectangle { center: Point::new(170.4, 50.3), width: 110.0, height: 60.0, radius: 0.0, rotation: 0.0 },
TracedShape::Rectangle { center: Point::new(50.6, 140.4), width: 60.0, height: 60.0, radius: 0.0, rotation: 0.0 },
TracedShape::Rectangle { center: Point::new(170.3, 140.6), width: 100.0, height: 50.0, radius: 12.0, rotation: 0.0 },
TracedShape::Ellipse { center: Point::new(70.4, 220.3), rx: 45.0, ry: 18.0, rotation: 0.0 },
TracedShape::Polygon { center: Point::new(190.6, 215.4), radius: 30.0, sides: 6, rotation: 10.0 },
];
let mut plain_art = BezPath::new();
for shape in &plain {
plain_art.extend(shape.path().to_bezpath());
}
let centre = Point::new(128.4, 128.3);
let touching = vec![(half_disc(centre, 90.0, true), [200, 40, 50]), (half_disc(centre, 90.0, false), [30, 90, 200])];
let bar = [(60.3, 180.4), (196.6, 180.4), (196.6, 206.7), (60.3, 206.7)];
@@ -225,7 +286,7 @@ pub fn inputs() -> Vec<Input> {
(poly(&bar), [40, 160, 90]),
];
vec![
let mut all = vec![
art("circle", Class::Shape, ink(Circle::new((128.3, 127.6), 80.4).to_path(1e-3)), 4, vec![]),
art("ellipses", Class::Shape, ink(ellipses), 8, vec![]),
art("badge with a hole", Class::Shape, ink(badge), 12, vec![]),
@@ -234,11 +295,20 @@ pub fn inputs() -> Vec<Input> {
art("star", Class::Poly, ink(poly(&star)), 10, points(&star)),
art("letter A", Class::Poly, ink(with_holes(poly(&a_outer), &[poly(&a_counter)])), 10, points(&[&a_outer[..], &a_counter].concat())),
art("lines", Class::Lines, ink(lines), 20, vec![]),
art("askew shapes and lines", Class::Askew, ink(askew), 24, vec![]),
art("plain shapes", Class::Plain, ink(plain_art), 30, vec![]),
art("two touching halves", Class::Flat, touching, 6, vec![(centre.x, centre.y - 90.0).into(), (centre.x, centre.y + 90.0).into()]),
art("four-colour badge", Class::Flat, multicolor, 20, points(&bar)),
tone("gradient", gradient),
tone("photo-like", photo),
]
];
if let Some(input) = all.iter_mut().find(|i| i.name == "plain shapes") {
input.plain = plain;
}
if let Some(input) = all.iter_mut().find(|i| i.name == "lines") {
input.strokes = strokes;
}
all
}
impl Input {
@@ -268,6 +338,16 @@ impl Input {
let k = Affine::scale(f64::from(size) / 256.0);
self.shapes.iter().map(|(p, c)| (k * p.clone(), *c)).collect()
}
/// Its lines in the pixel coordinates of a `size`-pixel drawing.
pub fn scaled_strokes(&self, size: u32) -> Vec<(Point, Point, f64)> {
let k = f64::from(size) / 256.0;
self.strokes.iter().map(|(a, b, w)| ((a.to_vec2() * k).to_point(), (b.to_vec2() * k).to_point(), w * k)).collect()
}
/// Its plain shapes' outlines in the pixel coordinates of a `size`-pixel drawing.
pub fn scaled_plain(&self, size: u32) -> Vec<BezPath> {
let k = Affine::scale(f64::from(size) / 256.0);
self.plain.iter().map(|s| k * s.path().to_bezpath()).collect()
}
pub fn scaled_corners(&self, size: u32) -> Vec<Point> {
self.corners.iter().map(|p| (p.to_vec2() * (f64::from(size) / 256.0)).to_point()).collect()
}
+33 -11
View File
@@ -65,14 +65,19 @@ static ALLOC: Counting = Counting;
/// Whether `input` is what `preset` is meant for.
fn target(preset: &str, input: &Input) -> bool {
let classes: &[Class] = match preset {
"High Fidelity Photo" | "Low Fidelity Photo" | "Shades of Gray" => &[Class::Tone],
"High Fidelity Photo"
| "Low Fidelity Photo"
| "Shades of Gray"
| "Shades of Gray with Auto Grouping"
| "High Fidelity Photo with Auto Grouping" => &[Class::Tone],
// Three colours cannot show the badge's five.
"3 Colors" => return input.name == "two touching halves",
"6 Colors" | "16 Colors" | "Flat Logo" => &[Class::Flat],
"6 Colors" | "16 Colors" | "Flat Logo" | "Auto Color" | "6 Colors with Auto Grouping" => &[Class::Flat],
"Default with Shapes" | "6 Colors with Shapes" => &[Class::Plain],
"Sketched Art" => &[Class::Shape, Class::Lines],
"Silhouettes" => &[Class::Shape],
"Line Art" => &[Class::Lines],
"Technical Drawing" => &[Class::Lines, Class::Poly],
"Line Art" | "Outline" => &[Class::Lines],
"Technical Drawing" => &[Class::Lines, Class::Poly, Class::Askew],
_ => &[Class::Shape, Class::Poly],
};
classes.contains(&input.class)
@@ -134,9 +139,9 @@ fn svg_trace(svg: &str) -> TraceResult {
_ => panic!("unread transform {t}"),
});
}
Some(TracedPath { path: vectorcraft_geom::PathData::from_bezpath(&path), color: [0; 3], pixels: 0, stroke: None })
Some(TracedPath { path: vectorcraft_geom::PathData::from_bezpath(&path), color: [0; 3], pixels: 0, stroke: None, shape: None })
});
TraceResult { paths: paths.collect(), palette: vec![[0; 3]] }
TraceResult { paths: paths.collect(), palette: vec![[0; 3]], ..Default::default() }
}
fn run_all(presets: &[(&'static str, TraceParams)], drawn: &[Drawing]) -> Vec<Run> {
@@ -172,12 +177,12 @@ fn stat(runs: &[&Metrics], field: impl Fn(&Metrics) -> f64, max: bool, digits: u
if x.is_finite() { format!("{:.digits$}", x + 0.0) } else { "all".into() }
}
const HEADER: &str = "| ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |\n|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|\n";
const HEADER: &str = "| ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke | shapes % | grouped % |\n|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|\n";
fn row(runs: &[&Run]) -> String {
let m: Vec<&Metrics> = runs.iter().filter_map(|r| r.metrics.as_ref()).collect();
if m.is_empty() {
return format!("| refused |{}", " |".repeat(16));
return format!("| refused |{}", " |".repeat(22));
}
let cells = [
stat(&m, |m| m.delta, false, 2),
@@ -189,6 +194,7 @@ fn row(runs: &[&Run]) -> String {
stat(&m, |m| m.anchors, false, 0),
stat(&m, |m| m.anchor_ratio, false, 2),
stat(&m, |m| m.lines, false, 0),
stat(&m, |m| m.upright, false, 0),
stat(&m, |m| m.corners, false, 0),
stat(&m, |m| m.kinks, false, 1),
stat(&m, |m| m.slivers, false, 1),
@@ -197,6 +203,11 @@ fn row(runs: &[&Run]) -> String {
stat(&m, |m| m.overlap, false, 1),
stat(&m, |m| m.specks, false, 1),
stat(&m, |m| m.outside, true, 1),
stat(&m, |m| m.centre_off, false, 3),
stat(&m, |m| m.width_off, false, 2),
stat(&m, |m| m.stroke_anchors, false, 1),
stat(&m, |m| m.shapes, false, 0),
stat(&m, |m| m.grouped, false, 0),
];
format!("| {} |", cells.join(" | "))
}
@@ -220,10 +231,21 @@ fn cost(img: &Raster, params: &TraceParams) -> String {
}
fn report() -> String {
let presets = presets();
// The built-in presets, and some of them with the Shapes or the Auto Grouping option on.
let mut presets = presets();
for (name, base) in [("Default with Shapes", "Default"), ("6 Colors with Shapes", "6 Colors")] {
presets.push((name, TraceParams { shapes: true, ..vectorcraft_trace::preset(base).unwrap() }));
}
for (name, base) in [
("6 Colors with Auto Grouping", "6 Colors"),
("Shades of Gray with Auto Grouping", "Shades of Gray"),
("High Fidelity Photo with Auto Grouping", "High Fidelity Photo"),
] {
presets.push((name, TraceParams { auto_grouping: true, ..vectorcraft_trace::preset(base).unwrap() }));
}
let inputs = inputs();
let drawn = draw_all(&inputs);
let one_colour = |d: &&Drawing| matches!(d.0.class, Class::Shape | Class::Poly | Class::Lines);
let one_colour = |d: &&Drawing| matches!(d.0.class, Class::Shape | Class::Poly | Class::Lines | Class::Askew | Class::Plain);
if let Ok(dir) = std::env::var("TRACE_QUALITY_IMAGES") {
for (input, size, variant, img) in &drawn {
std::fs::write(format!("{dir}/{}.png", stem(input, *size, *variant)), img.encode_png()).unwrap();
@@ -270,7 +292,7 @@ fn report() -> String {
.collect();
let _ = write!(md, "\n## 4. Yardstick\n\n{} traces by another tracer, measured the same way.\n\n| inputs | variant ", theirs.len());
md += HEADER.replacen("\n|", "\n|:--|:--|", 1).as_str();
for class in [Class::Shape, Class::Poly, Class::Lines] {
for class in [Class::Shape, Class::Poly, Class::Lines, Class::Askew, Class::Plain] {
for variant in VARIANTS {
let rows: Vec<&Run> = theirs.iter().filter(|r| r.class == class && r.variant == variant).collect();
let _ = writeln!(md, "| {} | {} {}", class.name(), variant.name(), row(&rows));
+83 -3
View File
@@ -175,6 +175,8 @@ pub struct Metrics {
pub anchor_ratio: f64,
/// Share (%) of the outline length made of straight segments.
pub lines: f64,
/// Share (%) of the outline length that is exactly level or upright.
pub upright: f64,
/// Share (%) of the art's sharp corners that have a traced corner within 1.5 px.
pub corners: f64,
/// Anchors where the path turns by 10° or more away from any true corner.
@@ -185,17 +187,47 @@ pub struct Metrics {
/// Area (px) no path covers, and area covered twice.
pub gap: f64,
pub overlap: f64,
/// Stroked centre lines: their mean distance (px) from the true line's centre line, the mean
/// error (px) of their widths, and anchors per stroked path.
pub centre_off: f64,
pub width_off: f64,
pub stroke_anchors: f64,
/// Share (%) of the art's plain shapes that came out as a shape lying within 0.6 px of it.
pub shapes: f64,
/// Auto Grouping: share (%) of the paths that lie inside another.
pub grouped: f64,
/// Paths under 6 px of area.
pub specks: f64,
/// How far (px) the trace reaches outside the image: a stray curve.
pub outside: f64,
}
/// The order a grouped result is painted in: each path, then those inside it.
fn draw_order(res: &TraceResult) -> Vec<usize> {
let mut inside = vec![vec![]; res.paths.len()];
let mut stack = vec![];
for i in (0..res.paths.len()).rev() {
match res.parents.get(i).copied().flatten() {
Some(p) => inside[p].insert(0, i),
None => stack.push(i),
}
}
let mut order = vec![];
while let Some(i) = stack.pop() {
order.push(i);
stack.extend(inside[i].iter().rev());
}
order
}
/// Measure `res`, the trace of `src` (`input` drawn `size` px square as `variant`) with `params`.
pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params: &TraceParams, res: &TraceResult) -> Metrics {
let n = size as usize;
let shapes: Vec<(BezPath, [u8; 3])> = res.paths.iter().map(|tp| (painted(tp), tp.color)).collect();
let (delta_mean, edge_delta) = fidelity(src, &render(&shapes, [255; 3], n, n));
// Painted as the result is grouped: each path, then those inside it.
let drawn: Vec<(BezPath, [u8; 3])> = draw_order(res).into_iter().map(|i| shapes[i].clone()).collect();
let (delta_mean, edge_delta) = fidelity(src, &render(&drawn, [255; 3], n, n));
let grouped = if res.parents.is_empty() { f64::NAN } else { 100.0 * res.parents.iter().flatten().count() as f64 / res.paths.len().max(1) as f64 };
// The background's frame: a path as large as the image.
let is_frame = |p: &BezPath| {
@@ -224,7 +256,7 @@ pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params:
let kept = true_corners.iter().filter(|c| turns.iter().any(|(p, t)| *t >= 20.0 && p.distance(**c) <= 1.5)).count();
corners = 100.0 * kept as f64 / true_corners.len() as f64;
}
if input.class != Class::Lines {
if !matches!(input.class, Class::Lines | Class::Askew | Class::Plain) {
kinks = turns.iter().filter(|(p, t)| *t >= 10.0 && true_corners.iter().all(|c| p.distance(*c) > 2.0)).count() as f64;
}
if params.mode != Mode::BlackAndWhite {
@@ -234,7 +266,7 @@ pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params:
}
}
let (mut length, mut straight) = (0.0, 0.0);
let (mut length, mut straight, mut upright) = (0.0, 0.0, 0.0);
for sp in art.iter().flat_map(|(tp, _)| &tp.path.subpaths) {
for i in 0..sp.segment_count() {
let len = sp.segment(i).arclen(0.05);
@@ -243,6 +275,9 @@ pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params:
let c = sp.segment(i);
if seg_dist(c.p1, c.p0, c.p3) < 0.02 && seg_dist(c.p2, c.p0, c.p3) < 0.02 {
straight += len;
if (c.p0.x - c.p3.x).abs() < 1e-9 || (c.p0.y - c.p3.y).abs() < 1e-9 {
upright += len;
}
}
}
}
@@ -265,7 +300,51 @@ pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params:
}
}
// Stroked paths against the lines they trace: each against the true line nearest its middle.
let (mut centre_off, mut width_off, mut stroke_anchors) = (nan, nan, nan);
let truth = input.scaled_strokes(size);
let stroked: Vec<&TracedPath> = res.paths.iter().filter(|tp| tp.stroke.is_some()).collect();
if !truth.is_empty() && !stroked.is_empty() {
let (mut off, mut samples, mut width) = (0.0, 0.0, 0.0);
for tp in &stroked {
let line = Outline::new([&tp.path.to_bezpath()], None);
let Some(middle) = line.segs.get(line.segs.len() / 2).map(|s| s.0.midpoint(s.1)) else { continue };
let Some((a, b, w)) = truth.iter().copied().min_by(|p, q| seg_dist(middle, p.0, p.1).total_cmp(&seg_dist(middle, q.0, q.1))) else {
continue;
};
for s in &line.segs {
// The distance to the line through the true ends: a centre line may be shorter or
// longer than the true one by how its caps are drawn.
let d = b - a;
off += (s.0.midpoint(s.1) - a).cross(d).abs() / d.hypot().max(1e-9);
samples += 1.0;
}
width += (tp.stroke.unwrap_or(0.0) - w).abs();
}
if samples > 0.0 {
(centre_off, width_off) = (off / samples, width / stroked.len() as f64);
stroke_anchors = stroked.iter().map(|tp| tp.path.anchor_count()).sum::<usize>() as f64 / stroked.len() as f64;
}
}
// Plain shapes: each is found when some path that is a shape lies on it.
let plain = input.scaled_plain(size);
let mut shapes_found = nan;
if !plain.is_empty() {
let made: Vec<Outline> = res.paths.iter().filter(|tp| tp.shape.is_some()).map(|tp| Outline::new([&tp.path.to_bezpath()], None)).collect();
let found = plain.iter().filter(|p| {
let truth = Outline::new([*p], None);
made.iter().any(|m| m.distance_to(&truth).1 <= 0.6 && truth.distance_to(m).1 <= 0.6)
});
shapes_found = 100.0 * found.count() as f64 / plain.len() as f64;
}
Metrics {
grouped,
shapes: shapes_found,
centre_off,
width_off,
stroke_anchors,
delta: delta_mean,
edge_delta,
geo_mean,
@@ -275,6 +354,7 @@ pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params:
anchors: anchors as f64,
anchor_ratio: if input.anchors > 0 { anchors as f64 / input.anchors as f64 } else { nan },
lines: if length > 0.0 { 100.0 * straight / length } else { nan },
upright: if length > 0.0 { 100.0 * upright / length } else { nan },
corners,
kinks,
slivers,
+4
View File
@@ -234,7 +234,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
// File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview: off, images show
// their pixels as they rasterize, hard-edged.
let smooth_images = pixel.is_none() || app.session.prefs.anti_aliased_bitmaps;
// Paths made into guides are drawn as the ruler guides are (Preferences › Guides & Grid).
let guide_look = LineLook::guides(&app.session.prefs, &t);
let key = CacheKey {
guide: ([guide_look.color.r(), guide_look.color.g(), guide_look.color.b()], guide_look.dots),
doc: st.uid as usize,
revision: st.revision,
interacting: st.interaction.is_some(),
@@ -297,6 +300,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let [r, g, b, _] = c.to_rgba8(1.0);
[r, g, b]
}),
guide: key.guide,
mask_view,
highlight_substitutions: true,
// General › Anti-aliased Artwork: off, edges are hard on screen (raster effects and
+2
View File
@@ -84,6 +84,7 @@ pub mod spot_colors;
pub(crate) mod svg_options;
pub mod swatch_conflict;
pub mod swatch_options;
pub mod symbol_options;
pub mod symbolism_options;
mod text_export;
pub mod text_import;
@@ -290,6 +291,7 @@ registry! {
LiquifyOptions: [liquify::KIND] => liquify::SPEC,
FreehandOptions: [freehand::KIND] => freehand::SPEC,
FlareOptions: [flare_options::KIND] => flare_options::SPEC,
SymbolOptions: [symbol_options::KIND] => symbol_options::SPEC,
SymbolismOptions: [symbolism_options::KIND] => symbolism_options::SPEC,
PerspectiveGridPresets: [perspective_presets::KIND] => perspective_presets::SPEC,
PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC,
@@ -0,0 +1,45 @@
//! Symbol Options: the name of the symbol New Symbol makes from the selection (`symbol.new`), asked
//! before the symbol is made, as the Symbols panel's New Symbol button and menu item do.
//!
//! Fields: `name`.
use serde_json::{Value, json};
use super::swatch_options::{grid, label};
use super::{DialogSpec, form};
use crate::VectorcraftApp;
use crate::state::Dialog;
/// The dialog kind of Symbol Options.
pub const KIND: &str = "symbolOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Symbol Options").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM };
/// Open Symbol Options to name a new symbol made from the selection.
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
let st = app.session.active().ok_or("no document open")?;
if st.selection.is_empty() {
return Err("nothing selected".into());
}
let name = st.doc.unique_symbol_name("New Symbol");
app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name })));
Ok(Value::Null)
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
grid(ui, |ui| {
label(ui, tl!("Name:"));
form::text(ui, d, "name", 190.0);
ui.end_row();
});
false
}
/// OK: make the symbol from the selection with the name typed.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let r = app.run("symbol.new", json!({ "name": d.str("name") }));
if r.is_ok() {
app.ui.dialog = None;
}
r
}
+6
View File
@@ -621,6 +621,7 @@
VectorCraft on GitHub VectorCraft na GitHubu
Search Commands… Hledat příkazy…
VectorCraft Help… Nápověda VectorCraft…
Auto Color Automatická barva
Automatic Automaticky
Isolate Selected Group Izolovat vybranou skupinu
Exit Isolation Mode Ukončit izolovaný režim
@@ -807,6 +808,8 @@ lighting Light Světlo
File Info Dialog Dialogové okno Informace o souboru
Go to Artboard Přejít na pracovní plochu
Graphic Style Options… Možnosti grafického stylu…
Symbol Options Možnosti symbolu
Symbol Options… Možnosti symbolu…
Home Domů
Import Keyboard Shortcuts… Importovat klávesové zkratky…
Justify all lines Do bloku včetně posledního řádku
@@ -1424,6 +1427,8 @@ lighting Light Světlo
Abutting Sousedící
Black and White Černobílé
Corners Rohy
Auto Grouping Automatické seskupení
Ignore Color Ignorovat barvu
Ignore White Ignorovat bílou
Palette: Paleta:
Limited Omezená
@@ -3660,6 +3665,7 @@ effect Regular Pravidelný
Stroke Pressure Tlak tahu
# Plurals: one|few|other
imageTrace Outline Obrys
@plural {n} item|{n} items {n} položka|{n} položky|{n} položek
@plural Color Table ({n} color)|Color Table ({n} colors) Tabulka barev ({n} barva)|Tabulka barev ({n} barvy)|Tabulka barev ({n} barev)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Odstranit {n} grafický styl?|Odstranit {n} grafické styly?|Odstranit {n} grafických stylů?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -229,6 +229,7 @@
Author Title: Titel des Autors:
Author: Autor:
Auto Auto
Auto Color Automatische Farbe
Automatic Automatisch
Auto Size New Area Type Größe von neuem Flächentext automatisch anpassen
Auto Spell Check Automatische Rechtschreibprüfung
@@ -1127,6 +1128,8 @@
Graphic Style Library Grafikstil-Bibliothek
Graphic Style Options Grafikstiloptionen
Graphic Style Options… Grafikstiloptionen…
Symbol Options Symboloptionen
Symbol Options… Symboloptionen…
Graphic Styles Grafikstile
Graphic Styles Options Optionen für Grafikstile
Graphics Grafik
@@ -1226,6 +1229,8 @@
Identify Objects By Objekte identifizieren nach
Ignore Ignorieren
Ignore Artboards (all the art as one page) Zeichenflächen ignorieren (gesamte Grafik als eine Seite)
Auto Grouping Automatisch gruppieren
Ignore Color Farbe ignorieren
Ignore White Weiß ignorieren
Palette: Palette:
Limited Begrenzt
@@ -3135,6 +3140,7 @@ effect Points Punkte
{warning}: click to correct to the closest color ({hex}) {warning}: Klicken korrigiert auf die nächstliegende Farbe ({hex})
“{file}” is protected by a password. „{file}“ ist durch ein Kennwort geschützt.
● Recording ● Aufzeichnung
imageTrace Outline Kontur
@plural {n} item|{n} items {n} Element|{n} Elemente
@plural Color Table ({n} color)|Color Table ({n} colors) Farbtabelle ({n} Farbe)|Farbtabelle ({n} Farben)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Diesen {n} Grafikstil löschen?|Diese {n} Grafikstile löschen?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -247,6 +247,7 @@
Author Title: Cargo del autor:
Author: Autor:
Auto Auto
Auto Color Color automático
Automatic Automático
Auto Size New Area Type Ajustar automáticamente el tamaño del nuevo texto de área
Auto Spell Check Revisión ortográfica automática
@@ -1144,6 +1145,8 @@
Graphic Style Library Biblioteca de estilos gráficos
Graphic Style Options Opciones de estilo gráfico
Graphic Style Options… Opciones de estilo gráfico…
Symbol Options Opciones de símbolo
Symbol Options… Opciones de símbolo…
Graphic Styles Estilos gráficos
Graphic Styles Options Opciones de estilos gráficos
Graphics Gráficos
@@ -1244,6 +1247,8 @@
Identify Objects By Identificar objetos por
Ignore Ignorar
Ignore Artboards (all the art as one page) Ignorar mesas de trabajo (toda la ilustración como una página)
Auto Grouping Agrupación automática
Ignore Color Ignorar color
Ignore White Ignorar blanco
Palette: Paleta:
Limited Limitada
@@ -3236,6 +3241,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}: clic para corregir al color más cercano ({hex})
“{file}” is protected by a password. “{file}” está protegido con contraseña.
● Recording ● Grabando
imageTrace Outline Contorno
@plural {n} item|{n} items {n} elemento|{n} elementos
@plural Color Table ({n} color)|Color Table ({n} colors) Tabla de colores ({n} color)|Tabla de colores ({n} colores)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? ¿Eliminar {n} estilo gráfico?|¿Eliminar estos {n} estilos gráficos?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -248,6 +248,7 @@
Author Title: Fonction de l’auteur :
Author: Auteur :
Auto Auto
Auto Color Couleur automatique
Automatic Automatique
Auto Size New Area Type Taille automatique du nouveau texte captif
Auto Spell Check Vérification orthographique automatique
@@ -1143,6 +1144,8 @@
Graphic Style Library Bibliothèque de styles graphiques
Graphic Style Options Options de style graphique
Graphic Style Options… Options de style graphique…
Symbol Options Options de symbole
Symbol Options… Options de symbole…
Graphic Styles Styles graphiques
Graphic Styles Options Options des styles graphiques
Graphics Graphiques
@@ -1243,6 +1246,8 @@
Identify Objects By Identifier les objets par
Ignore Ignorer
Ignore Artboards (all the art as one page) Ignorer les plans de travail (toute l’illustration sur une page)
Auto Grouping Groupement automatique
Ignore Color Ignorer la couleur
Ignore White Ignorer le blanc
Palette: Palette :
Limited Limitée
@@ -3233,6 +3238,7 @@
{warning}: click to correct to the closest color ({hex}) {warning} : cliquez pour corriger avec la couleur la plus proche ({hex})
“{file}” is protected by a password. « {file} » est protégé par un mot de passe.
● Recording ● Enregistrement
imageTrace Outline Contour
@plural {n} item|{n} items {n} élément|{n} éléments
@plural Color Table ({n} color)|Color Table ({n} colors) Table des couleurs ({n} couleur)|Table des couleurs ({n} couleurs)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Supprimer {n} style graphique ?|Supprimer ces {n} styles graphiques ?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -360,6 +360,7 @@
Auto Spell Check Automatikus helyesírás-ellenőrzés
Auto-Collapse Iconic Panels Ikonpanelek automatikus becsukása
Auto-Rotate (turn the paper to each artboard) Automatikus elforgatás (a papír igazodik minden rajztáblához)
Auto Color Automatikus szín
Automatic Automatikus
Automatic flatness Automatikus simaság
Automatically Automatikusan
@@ -1469,6 +1470,8 @@
Graphic Style Library Grafikaistílus-könyvtár
Graphic Style Options Grafikai stílus beállításai
Graphic Style Options… Grafikai stílus beállításai…
Symbol Options Szimbólum beállításai
Symbol Options… Szimbólum beállításai…
Graphic Styles Grafikai stílusok
Graphic Styles (4) Grafikai stílusok (4)
Graphic Styles Options Grafikai stílusok beállításai
@@ -1608,6 +1611,8 @@
Identify Objects By Objektumok azonosítása
Ignore Kihagy
Ignore Artboards (all the art as one page) Rajztáblák figyelmen kívül hagyása (az összes grafika egy oldalon)
Auto Grouping Automatikus csoportosítás
Ignore Color Szín figyelmen kívül hagyása
Ignore White Fehér figyelmen kívül hagyása
Image Kép
Image Info Kép adatai
@@ -3935,6 +3940,7 @@ lighting Light Fény
theme Appearance Kinézet
# Plurals: one|other
imageTrace Outline Körvonal
@plural Color Table ({n} color)|Color Table ({n} colors) Színtábla ({n} szín)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? {n} grafikai stílus törlése?
@plural {n} area highlighted|{n} areas highlighted Kiemelt terület: {n}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -247,6 +247,7 @@
Author Title: Qualifica dell'autore:
Author: Autore:
Auto Auto
Auto Color Colore automatico
Automatic Automatico
Auto Size New Area Type Dimensionamento automatico del nuovo testo in area
Auto Spell Check Controllo ortografico automatico
@@ -1143,6 +1144,8 @@
Graphic Style Library Libreria stili grafici
Graphic Style Options Opzioni stile grafico
Graphic Style Options… Opzioni stile grafico…
Symbol Options Opzioni simbolo
Symbol Options… Opzioni simbolo…
Graphic Styles Stili grafici
Graphic Styles Options Opzioni stili grafici
Graphics Grafica
@@ -1243,6 +1246,8 @@
Identify Objects By Identifica oggetti per
Ignore Ignora
Ignore Artboards (all the art as one page) Ignora tavole da disegno (tutta la grafica come una sola pagina)
Auto Grouping Raggruppamento automatico
Ignore Color Ignora colore
Ignore White Ignora bianco
Palette: Palette:
Limited Limitata
@@ -3236,6 +3241,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}: fai clic per correggere con il colore più vicino ({hex})
“{file}” is protected by a password. “{file}” è protetto da password.
● Recording ● Registrazione in corso
imageTrace Outline Contorno
@plural {n} item|{n} items {n} elemento|{n} elementi
@plural Color Table ({n} color)|Color Table ({n} colors) Tabella colori ({n} colore)|Tabella colori ({n} colori)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Eliminare {n} stile grafico?|Eliminare questi {n} stili grafici?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -666,6 +666,7 @@
Hide Print Tiling プリント分割を隠す
Unlock Grid グリッドをロック解除
Edit Envelope エンベロープを編集
Auto Color 自動カラー
Automatic 自動
Isolate Selected Group 選択したグループを分離
Exit Isolation Mode 分離モードを終了
@@ -1421,6 +1422,8 @@
Graphic Style Library グラフィックスタイルライブラリ
Graphic Style Options グラフィックスタイルオプション
Graphic Style Options… グラフィックスタイルオプション…
Symbol Options シンボルオプション
Symbol Options… シンボルオプション…
Graphic Styles Options グラフィックスタイルオプション
Graphics Processor グラフィックプロセッサー
Graphics: {adapter} グラフィックス: {adapter}
@@ -1481,6 +1484,8 @@
Identify Objects By オブジェクトの識別方法
Ignore 無視
Ignore Artboards (all the art as one page) アートボードを無視 (すべてのアートを 1 ページとして扱う)
Auto Grouping 自動グループ化
Ignore Color カラーを無視
Ignore White 白を無視
Palette: パレット:
Limited 限定
@@ -3225,6 +3230,7 @@
Yellow イエロー
[Normal Character Style] [標準文字スタイル]
[Normal Paragraph Style] [標準段落スタイル]
imageTrace Outline アウトライン
@plural Color Table ({n} color)|Color Table ({n} colors) カラーテーブル ({n} 色)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? {n} 個のグラフィックスタイルを削除しますか?
@plural {n} Layer|{n} Layers {n} レイヤー
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -664,6 +664,7 @@
Hide Print Tiling 인쇄 타일링 숨기기
Unlock Grid 격자 잠금 해제
Edit Envelope 엔벨로프 편집
Auto Color 자동 색상
Automatic 자동
Isolate Selected Group 선택한 그룹 격리
Exit Isolation Mode 격리 모드 종료
@@ -1416,6 +1417,8 @@
Graphic Style Library 그래픽 스타일 라이브러리
Graphic Style Options 그래픽 스타일 옵션
Graphic Style Options… 그래픽 스타일 옵션…
Symbol Options 심볼 옵션
Symbol Options… 심볼 옵션…
Graphic Styles Options 그래픽 스타일 옵션
Graphics Processor 그래픽 프로세서
Graphics: {adapter} 그래픽: {adapter}
@@ -1476,6 +1479,8 @@
Identify Objects By 개체 식별 기준
Ignore 무시
Ignore Artboards (all the art as one page) 아트보드 무시 (모든 아트웍을 한 페이지로)
Auto Grouping 자동 그룹화
Ignore Color 색상 무시
Ignore White 흰색 무시
Palette: 팔레트:
Limited 제한
@@ -3218,6 +3223,7 @@
Yellow 노랑
[Normal Character Style] [기본 문자 스타일]
[Normal Paragraph Style] [기본 단락 스타일]
imageTrace Outline 윤곽선
@plural Color Table ({n} color)|Color Table ({n} colors) 색상 표 ({n}색)|색상 표 ({n}색)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? 그래픽 스타일 {n}개를 삭제하시겠습니까?|그래픽 스타일 {n}개를 삭제하시겠습니까?
@plural {n} Layer|{n} Layers 레이어 {n}개|레이어 {n}개
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -606,6 +606,7 @@
VectorCraft on GitHub VectorCraft w serwisie GitHub
Search Commands… Szukaj poleceń…
VectorCraft Help… Pomoc VectorCraft…
Auto Color Kolor automatyczny
Automatic Automatycznie
Isolate Selected Group Izoluj zaznaczoną grupę
Exit Isolation Mode Wyjdź z trybu izolacji
@@ -781,6 +782,8 @@ lighting Light Światło
File Info Dialog Okno informacji o pliku
Go to Artboard Przejdź do obszaru kompozycji
Graphic Style Options… Opcje stylu graficznego…
Symbol Options Opcje symbolu
Symbol Options… Opcje symbolu…
Home Strona startowa
Import Keyboard Shortcuts… Importuj skróty klawiaturowe…
Justify all lines Justuj wszystkie wiersze
@@ -1374,6 +1377,8 @@ lighting Light Światło
Abutting Stykające się
Black and White Czarno-biały
Corners Narożniki
Auto Grouping Automatyczne grupowanie
Ignore Color Ignoruj kolor
Ignore White Ignoruj biel
Palette: Paleta:
Limited Ograniczona
@@ -3528,6 +3533,7 @@ effect Regular Normalny
Stroke Direction Kierunek pociągnięć
Stroke Width Szerokość pociągnięć
Stroke Pressure Nacisk pociągnięć
imageTrace Outline Kontur
@plural {n} item|{n} items {n} element|{n} elementy|{n} elementów
@plural Color Table ({n} color)|Color Table ({n} colors) Tablica kolorów ({n} kolor)|Tablica kolorów ({n} kolory)|Tablica kolorów ({n} kolorów)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Usunąć {n} styl graficzny?|Usunąć {n} style graficzne?|Usunąć {n} stylów graficznych?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -1
View File
@@ -983,6 +983,8 @@
Graphic Style Library Biblioteca de Estilos Gráficos
Graphic Style Options Opções de Estilo Gráfico
Graphic Style Options… Opções de Estilo Gráfico…
Symbol Options Opções de Símbolo
Symbol Options… Opções de Símbolo…
Graphic Styles Estilos Gráficos
Graphic Styles Options Opções de Estilos Gráficos
Graphics Processor Processador Gráfico
@@ -1070,7 +1072,7 @@
Identify Objects By Identificar Objetos Por
Ignore Ignorar
Ignore Artboards (all the art as one page) Ignorar Mesas de Trabalho (toda a arte como uma página)
Ignore White Ignorar Branco
Ignore Color Ignorar Cor
Image Info Informações da Imagem
Image Map: Mapa da Imagem:
Image Size Tamanho da Imagem
@@ -2661,6 +2663,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}: clique para corrigir para a cor mais próxima ({hex})
“{file}” is protected by a password. “{file}” é protegido por uma senha.
● Recording ● Gravando
imageTrace Outline Contorno
@plural {n} item|{n} items {n} item|{n} itens
@plural Color Table ({n} color)|Color Table ({n} colors) Tabela de Cores ({n} cor)|Tabela de Cores ({n} cores)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Excluir este {n} estilo gráfico?|Excluir estes {n} estilos gráficos?
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -245,6 +245,7 @@
Author Title: Должность автора:
Author: Автор:
Auto Авто
Auto Color Автоцвет
Automatic Автоматически
Auto Size New Area Type Автоматически подгонять размер нового текста в области
Auto Spell Check Автоматическая проверка орфографии
@@ -1141,6 +1142,8 @@
Graphic Style Library Библиотека графических стилей
Graphic Style Options Параметры графического стиля
Graphic Style Options… Параметры графического стиля…
Symbol Options Параметры символа
Symbol Options… Параметры символа…
Graphic Styles Графические стили
Graphic Styles Options Параметры графических стилей
Graphics Графика
@@ -1241,6 +1244,8 @@
Identify Objects By Определять объекты по
Ignore Игнорировать
Ignore Artboards (all the art as one page) Игнорировать монтажные области (весь рисунок как одна страница)
Auto Grouping Автогруппировка
Ignore Color Игнорировать цвет
Ignore White Игнорировать белый
Palette: Палитра:
Limited Ограниченная
@@ -3232,6 +3237,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}: щёлкните для исправления на ближайший цвет ({hex})
“{file}” is protected by a password. «{file}» защищён паролем.
● Recording ● Запись
imageTrace Outline Контур
@plural {n} item|{n} items {n} элемент|{n} элемента|{n} элементов
@plural Color Table ({n} color)|Color Table ({n} colors) Цветовая таблица ({n} цвет)|Цветовая таблица ({n} цвета)|Цветовая таблица ({n} цветов)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Удалить этот графический стиль ({n})?|Удалить эти графические стили ({n})?|Удалить эти графические стили ({n})?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -241,6 +241,7 @@
Author Title: Посада автора:
Author: Автор:
Auto Авто
Auto Color Автоколір
Automatic Автоматично
Auto Size New Area Type Автоматично підганяти розмір нового тексту в області
Auto Spell Check Автоматична перевірка орфографії
@@ -1137,6 +1138,8 @@
Graphic Style Library Бібліотека графічних стилів
Graphic Style Options Параметри графічного стилю
Graphic Style Options… Параметри графічного стилю…
Symbol Options Параметри символу
Symbol Options… Параметри символу…
Graphic Styles Графічні стилі
Graphic Styles Options Параметри графічних стилів
Graphics Графіка
@@ -1237,6 +1240,8 @@
Identify Objects By Визначати об’єкти по
Ignore Ігнорувати
Ignore Artboards (all the art as one page) Ігнорувати монтажні області (весь малюнок як одна сторінка)
Auto Grouping Автогрупування
Ignore Color Ігнорувати колір
Ignore White Ігнорувати білий
Palette: Палітра:
Limited Обмежена
@@ -3229,6 +3234,7 @@ effect Points Точки
{warning}: click to correct to the closest color ({hex}) {warning}: натисніть , щоб виправити найближчий колір ({hex})
“{file}” is protected by a password. "{file}" захищений паролем.
● Recording ● Запис
imageTrace Outline Контур
@plural {n} item|{n} items {n} елемент|{n} елементи|{n} елементів
@plural Color Table ({n} color)|Color Table ({n} colors) Таблиця кольорів ({n} колір)|Таблиця кольорів ({n} кольори)|Таблиця кольорів ({n} кольорів)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Видалити цей графічний стиль ({n})?|Видалити ці графічні стилі ({n})?|Видалити ці графічні стилі ({n})?
Can't render this file because it contains an unexpected character in line 660 and column 40.
+6
View File
@@ -288,6 +288,7 @@
Auto Spell Check Kiểm tra chính tả tự động
Auto-Collapse Iconic Panels Tự động thu gọn bảng dạng biểu tượng
Auto-Rotate (turn the paper to each artboard) Tự động xoay (xoay trang cho mỗi bảng vẽ)
Auto Color Màu tự động
Automatic Tự động
Automatic flatness Độ phẳng tự động
Automatically Tự động
@@ -1322,6 +1323,8 @@
Graphic Style Library Thư viện kiểu đồ họa
Graphic Style Options Tùy chọn kiểu đồ họa
Graphic Style Options… Tùy chọn kiểu đồ họa…
Symbol Options Tùy chọn ký hiệu
Symbol Options… Tùy chọn ký hiệu…
Graphic Styles Kiểu đồ họa
Graphic Styles Options Tùy chọn kiểu đồ họa
Graphics Đồ họa
@@ -1437,6 +1440,8 @@
Identify Objects By Nhận diện đối tượng theo
Ignore Bỏ qua
Ignore Artboards (all the art as one page) Bỏ qua bảng vẽ (toàn bộ bản vẽ thành một trang)
Auto Grouping Tự động nhóm
Ignore Color Bỏ qua màu
Ignore White Bỏ qua màu trắng
Image Hình ảnh
Image Info Thông tin hình ảnh
@@ -3556,6 +3561,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}: bấm để chỉnh sang màu gần nhất ({hex})
“{file}” is protected by a password. “{file}” được bảo vệ bằng mật khẩu.
● Recording ● Đang ghi
imageTrace Outline Đường viền
@plural Color Table ({n} color)|Color Table ({n} colors) Bảng màu ({n} màu)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? Xóa {n} kiểu đồ họa này?
@plural {n} area highlighted|{n} areas highlighted {n} vùng được tô sáng
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
288
289
290
291
292
293
294
1323
1324
1325
1326
1327
1328
1329
1330
1440
1441
1442
1443
1444
1445
1446
1447
3561
3562
3563
3564 imageTrace
3565 @plural
3566 @plural
3567 @plural
+6
View File
@@ -205,6 +205,7 @@
Author Title: 作者职务:
Author: 作者:
Auto 自动
Auto Color 自动颜色
Automatic 自动
Auto Size New Area Type 自动调整新区域文字大小
Auto Spell Check 自动拼写检查
@@ -1094,6 +1095,8 @@
Graphic Style Library 图形样式库
Graphic Style Options 图形样式选项
Graphic Style Options… 图形样式选项…
Symbol Options 符号选项
Symbol Options… 符号选项…
Graphic Styles 图形样式
Graphic Styles Options 图形样式选项
Graphics 图形
@@ -1189,6 +1192,8 @@
Identify Objects By 对象识别方式
Ignore 忽略
Ignore Artboards (all the art as one page) 忽略画板 (所有图稿作为一页)
Auto Grouping 自动编组
Ignore Color 忽略颜色
Ignore White 忽略白色
Palette: 调板:
Limited 有限
@@ -3129,6 +3134,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}:单击以修正为最接近的颜色 ({hex})
“{file}” is protected by a password. “{file}”受密码保护。
● Recording ● 录制中
imageTrace Outline 轮廓
@plural {n} item|{n} items {n} 个项目
@plural Color Table ({n} color)|Color Table ({n} colors) 颜色表 ({n} 色)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? 要删除这 {n} 个图形样式吗?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+6
View File
@@ -216,6 +216,7 @@
Author Title: 作者職稱:
Author: 作者:
Auto 自動
Auto Color 自動顏色
Automatic 自動
Auto Size New Area Type 自動調整新區域文字大小
Auto Spell Check 自動拼字檢查
@@ -1118,6 +1119,8 @@
Graphic Style Library 繪圖樣式資料庫
Graphic Style Options 繪圖樣式選項
Graphic Style Options… 繪圖樣式選項…
Symbol Options 符號選項
Symbol Options… 符號選項…
Graphic Styles 繪圖樣式
Graphic Styles Options 繪圖樣式選項
Graphics 圖形
@@ -1219,6 +1222,8 @@
Identify Objects By 識別物件依據
Ignore 略過
Ignore Artboards (all the art as one page) 忽略工作區域 (所有圖稿視為單一頁面)
Auto Grouping 自動群組
Ignore Color 忽略顏色
Ignore White 忽略白色
Palette: 調色盤:
Limited 有限
@@ -3222,6 +3227,7 @@
{warning}: click to correct to the closest color ({hex}) {warning}:按一下以修正為最接近的顏色 ({hex})
“{file}” is protected by a password. 「{file}」受密碼保護。
● Recording ● 錄製中
imageTrace Outline 輪廓
@plural {n} item|{n} items {n} 個項目
@plural Color Table ({n} color)|Color Table ({n} colors) 色彩表 ({n} 色)
@plural Delete this {n} graphic style?|Delete these {n} graphic styles? 要刪除這 {n} 個繪圖樣式嗎?
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -321,6 +321,8 @@ pub struct CacheKey {
/// Appearance of Black › On Screen › Display All Blacks as Rich Black
/// ([`vectorcraft_render::RenderOptions::rich_black`]).
pub rich_black: bool,
/// Colour and style of guide paths ([`vectorcraft_render::RenderOptions::guide`]).
pub guide: ([u8; 3], bool),
}
#[derive(Clone, Copy, Debug, Default)]
+7
View File
@@ -403,6 +403,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{name?} open Graphic Style Options (dialog `graphicStyleOptions`, field `name`): for style `name` OK renames it (graphicStyle.rename); without, OK makes a new style of that name from the selection (graphicStyle.new)",
),
(
"ui.symbolOptions",
"Symbol Options…",
"",
"{} open Symbol Options for a new symbol made from the selection, what the Symbols panel's New Symbol opens (dialog `symbolOptions`, field `name`, default the first free \"New Symbol\"/\"New Symbol N\"); OK runs symbol.new {name}",
),
(
"tool.options",
"Tool Options…",
@@ -1290,6 +1296,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
}
"ui.colorPicker" => crate::dialogs::open_color_picker(app, p),
"ui.graphicStyleOptions" => crate::dialogs::graphic_style_options::open(app, s("name").as_deref()),
"ui.symbolOptions" => crate::dialogs::symbol_options::open(app),
"ui.brushOptions" => crate::dialogs::brush_options::open(app, s("name").as_deref()),
"tool.options" => crate::toolbar::open_options(app, &s("tool").unwrap_or_default()),
"ui.colorGuideOptions" => {
+49 -6
View File
@@ -19,6 +19,16 @@ const MODES: [(&str, &str); 4] = [("blackAndWhite", "Black and White"), ("graysc
const PALETTES: [&str; 4] = ["limited", "fullTone", "automatic", "documentLibrary"];
/// Palette `id`, in the UI language.
/// The colour Ignore Color leaves out in `params` (`ignoreColor`, `#rrggbb`): white when there is none.
fn ignore_color(params: &Value) -> [u8; 3] {
let hex = params["ignoreColor"].as_str().unwrap_or("").trim_start_matches('#');
let channel = |i: usize| hex.get(i * 2..i * 2 + 2).and_then(|h| u8::from_str_radix(h, 16).ok());
match (hex.len(), channel(0), channel(1), channel(2)) {
(6, Some(r), Some(g), Some(b)) => [r, g, b],
_ => [255; 3],
}
}
fn palette_label(id: &str) -> &'static str {
match id {
"fullTone" => tl!("Full Tone"),
@@ -128,10 +138,16 @@ fn trace_with(app: &mut VectorcraftApp, preset: &str) {
/// The built-in presets to choose from (`current` highlighted) → the one chosen. Listed only
/// while the menu is open.
/// A preset's name as shown: translated as a preset's name where the catalog has it so (the
/// Outline preset is not the Outline view), as any other string else.
fn preset_label(name: &str) -> &str {
crate::i18n::tr_ctx(crate::i18n::current(), "imageTrace", name)
}
fn preset_list(app: &mut VectorcraftApp, ui: &mut Ui, current: &str) -> Option<String> {
let mut chosen = None;
for (name, _) in presets(app) {
if ui.add(egui::Button::selectable(name == current, tl!(&name))).clicked() {
if ui.add(egui::Button::selectable(name == current, preset_label(&name))).clicked() {
chosen = Some(name);
}
}
@@ -338,14 +354,41 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
retrace |= release;
}
for (key, label) in [("snapCurvesToLines", tl!("Snap Curves To Lines")), ("ignoreWhite", tl!("Ignore White"))] {
let on = st.params[key].as_bool().unwrap_or(false);
if widgets::check(ui, label, on, true) {
st.params[key] = json!(!on);
let snap = st.params["snapCurvesToLines"].as_bool().unwrap_or(false);
if widgets::check(ui, tl!("Snap Curves To Lines"), snap, true) {
st.params["snapCurvesToLines"] = json!(!snap);
st.preset = "Custom".into();
retrace = true;
}
let shapes = st.params["shapes"].as_bool().unwrap_or(false);
if widgets::check(ui, tl!("Shapes"), shapes, true) {
st.params["shapes"] = json!(!shapes);
st.preset = "Custom".into();
retrace = true;
}
let grouping = st.params["autoGrouping"].as_bool().unwrap_or(false);
if widgets::check(ui, tl!("Auto Grouping"), grouping, true) {
st.params["autoGrouping"] = json!(!grouping);
st.preset = "Custom".into();
retrace = true;
}
// Ignore Color: white, unless another colour is picked.
ui.horizontal(|ui| {
let on = st.params["ignoreWhite"].as_bool().unwrap_or(false);
if widgets::check(ui, tl!("Ignore Color"), on, true) {
st.params["ignoreWhite"] = json!(!on);
st.preset = "Custom".into();
retrace = true;
}
}
if on {
let mut rgb = ignore_color(&st.params);
if ui.color_edit_button_srgb(&mut rgb).changed() {
st.params["ignoreColor"] = json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]));
st.preset = "Custom".into();
retrace = true;
}
}
});
}
widgets::divider(ui);
if let Some((p, a, c)) = st.info {
+37 -2
View File
@@ -155,7 +155,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0));
if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Symbol"), false, has_sel, 24.0).clicked() {
app.run("symbol.new", json!({})).ok();
// Alt skips Symbol Options: the symbol is made under the default name.
let cmd = if ui.input(|i| i.modifiers.alt) { "symbol.new" } else { "ui.symbolOptions" };
app.run(cmd, json!({})).ok();
}
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some(), 24.0).clicked()
&& let Some(n) = &sel
@@ -171,7 +173,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let inst = has_instances_selected(app);
let name = json!({"name": sel});
let items: [(&str, bool, &str, Value); 9] = [
(tl!("New Symbol…"), has_sel, "symbol.new", json!({})),
(tl!("New Symbol…"), has_sel, "ui.symbolOptions", json!({})),
(tl!("Redefine Symbol"), has_sel && sel.is_some(), "symbol.update", name.clone()),
(tl!("Duplicate Symbol"), sel.is_some(), "symbol.duplicate", name.clone()),
(tl!("Delete Symbol"), sel.is_some(), "symbol.delete", name.clone()),
@@ -245,4 +247,37 @@ mod tests {
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile + vec2(80.0, 80.0))]);
assert_eq!(egui::DragAndDrop::payload::<PanelDrag>(&ctx).as_deref(), Some(&PanelDrag::Symbol(names[0].clone())));
}
/// One headless frame of the panel menu; returns its painted labels.
fn menu_frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>) -> Vec<(String, Rect)> {
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 400.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| menu(app, ui));
out.textures_delta.clear();
crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>())
}
#[test]
fn new_symbol_asks_for_symbol_options_before_making_the_symbol() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let labels = menu_frame(&ctx, &mut app, vec![]);
let item = labels.iter().find(|(t, _)| t.trim() == "New Symbol…").map(|(_, r)| r.center()).expect("New Symbol… in the menu");
menu_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(item), button(item, true)]);
menu_frame(&ctx, &mut app, vec![button(item, false)]);
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("symbolOptions"));
assert!(symbols(&mut app).is_empty(), "no symbol before OK");
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Symbol");
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
// The next one is offered a name no symbol has, and OK keeps the name typed.
app.run("shape.rectangle", json!({"x": 60, "y": 10, "width": 30, "height": 20})).unwrap();
app.run("ui.symbolOptions", json!({})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Symbol 2");
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Badge"));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(symbols(&mut app), ["New Symbol", "Badge"]);
}
}
+53 -7
View File
@@ -258,10 +258,18 @@ pub fn entries() -> &'static [Entry] {
.collect();
let mut seen = HashSet::new();
for c in vectorcraft_engine::command_specs() {
if (c.menu.is_empty() && c.shortcut.is_none()) || !seen.insert(c.id) {
// Commands outside the menus (panel menu items) are
// listed when a key alone can run them; queries and aliases never are.
let listed = if c.menu.is_empty() {
c.shortcut.is_some() || (c.journal && !vectorcraft_engine::cmd::is_alias(c.id) && runs_without_params(c.params))
} else {
true
};
if !listed || !seen.insert(c.id) {
continue;
}
v.push(Entry { key: c.id.into(), label: c.label.into(), group: c.menu.join(" › "), is_tool: false });
let group = if c.menu.is_empty() { id_group(c.id) } else { c.menu.join(" › ") };
v.push(Entry { key: c.id.into(), label: c.label.into(), group, is_tool: false });
}
v.extend(crate::state::all_panels().map(|(id, label)| Entry {
key: format!("panel:{id}"),
@@ -273,16 +281,26 @@ pub fn entries() -> &'static [Entry] {
if !seen.insert(id) || (sc.is_empty() && !params.starts_with("{}") && !params.starts_with("{path")) {
continue;
}
let group = id.split('.').next().map(|g| {
let mut c = g.chars();
c.next().map(|f| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
});
v.push(Entry { key: id.to_string(), label: label.to_string(), group: group.unwrap_or_default(), is_tool: false });
v.push(Entry { key: id.to_string(), label: label.to_string(), group: id_group(id), is_tool: false });
}
v
})
}
/// The group of a command no menu holds: the first part of its id, capitalised (`edit.x` → "Edit").
fn id_group(id: &str) -> String {
let mut c = id.split('.').next().unwrap_or_default().chars();
c.next().map(|f| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
}
/// Does a command's params doc (`{a?, b?} …`) say it runs with no params, as a shortcut runs it?
fn runs_without_params(params: &str) -> bool {
params
.strip_prefix('{')
.and_then(|r| r.split_once('}'))
.is_some_and(|(fields, _)| fields.split(',').all(|f| f.trim().is_empty() || f.trim_end().ends_with('?')))
}
pub fn entry(key: &str) -> Option<&'static Entry> {
entries().iter().find(|e| e.key == key)
}
@@ -955,4 +973,32 @@ mod tests {
let mut seen = HashSet::new();
assert!(entries().iter().all(|e| seen.insert(e.key.clone())), "duplicate entries");
}
#[test]
fn duplicate_can_be_given_a_shortcut() {
// Duplicate has no default chord (#1539): it must still be listed so one can be set.
assert_eq!(entry("edit.duplicate").map(|e| e.group.as_str()), Some("Edit"));
let mut ov = BTreeMap::new();
assign(&mut ov, "edit.duplicate", Some("Cmd+Alt+Shift+F8"), false).unwrap();
assert_eq!(effective_in(&ov, "edit.duplicate").as_deref(), Some("Cmd+Alt+Shift+F8"));
}
/// #1539: a command outside the menus that runs without params (a panel menu's Break Link to
/// Symbol) can be given a shortcut; one that needs params, a query or an alias can't.
#[test]
fn menuless_commands_that_run_bare_are_listed() {
let e = entry("symbol.breakLink").expect("listed");
assert_eq!((e.label.as_str(), e.group.as_str()), ("Break Link to Symbol", "Symbol"));
for id in ["brush.duplicate", "path.convertAnchors", "document.json", "prefs.list"] {
assert!(entry(id).is_none(), "{id}");
}
for (doc, bare) in
[("{}", true), ("{dx?, dy?} x", true), ("{id?|ids?, clip?}", true), ("{name, newName?}", false), ("{a?: {b}}", false), ("same as", false)]
{
assert_eq!(runs_without_params(doc), bare, "{doc}");
}
let mut ov = BTreeMap::new();
assert!(assign(&mut ov, "symbol.breakLink", Some("Cmd+Alt+Shift+F9"), true).is_ok());
assert_eq!(effective_in(&ov, "symbol.breakLink").as_deref(), Some("Cmd+Alt+Shift+F9"));
}
}
+3
View File
@@ -114,6 +114,9 @@ dialog open); without `name` it names a new style made from the selection (`grap
`ui.mergeGraphicStyles {names}` opens the same dialog to name the style `ui.dialog.confirm` merges from them
(`graphicStyle.merge`). Deleting styles from the panel asks first with a `confirm` dialog (`graphicStyle.delete`).
Symbols: `ui.symbolOptions` opens the `symbolOptions` dialog (field `name`) that the Symbols panel's New Symbol asks
first; `ui.dialog.confirm` makes the symbol from the selection with that name (`symbol.new`).
Gradient panel: double-clicking a stop on the panel's slider opens the same `gradientStop` popover, with a `screen`
field (`[x, y]`, screen points) in place of `x`/`y`. The panel's stop eyedropper selects the Eyedropper tool with the
tool option `stop` (the tool to return to; `tool.setOption {key: "stop", value: "gradient"}`): its next click on art
+452 -265
View File
@@ -1,6 +1,6 @@
# Image Trace quality
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (thin lines in blurred and scanned art: Line Art 78 anchors to 46, 0.093 px off to 0.055)
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Auto Grouping: the same picture with 27 to 69% of its paths inside groups)
What Image Trace (`crates/trace`) makes of images whose right answer is known, measured by a harness and
compared with the numbers before each change. Nothing here is a judgement by eye, and nothing is compared
@@ -24,6 +24,8 @@ anti-aliasing):
| curved shapes | a circle; two ellipses; a rounded badge with a round hole; a letter D with its counter |
| polygons | a triangle, a turned square and a hexagon; a five-pointed star; a letter A with its counter |
| thin lines | five lines 0.5, 1, 2, 3 and 5 units wide (of a 256-unit square), a few degrees off the pixel grid |
| askew shapes | a rectangle, a plus sign and two lines, each 1 to 2 degrees off level and upright |
| plain shapes | a circle, a rectangle, a square, a rounded rectangle, an ellipse and a hexagon, each alone on white (measured from the Shapes change on, not at the baseline) |
| flat colour art | a disc of two halves in different colours that touch; a badge of four colours on white |
| gradient, photo | a smooth two-way gradient with a glow; a photo-like image (soft-edged shaded objects, texture, sensor noise) |
@@ -40,11 +42,15 @@ The letters are drawn by hand from lines and curves: the repository has no font
| paths, anchors | counts, the background's frame aside |
| × art | anchors per anchor point of the art itself (1.00 is as economical as the original) |
| lines % | share of the outline length made of straight segments (100 is right for polygons, 0 for a circle) |
| upright % | share of the outline length that is exactly level or upright |
| corners % | share of the art's sharp corners with a traced corner within 1.5 px |
| kinks | anchors where the outline turns by 10° or more away from any true corner |
| odd colours, odd px | paths (and their area) in a colour the art doesn't have: slivers along anti-aliased edges |
| gap px, overlap px | area no path covers, and area covered twice (abutting traces with a white background only) |
| specks | paths under 6 px of area |
| centre off, width off, anchors a stroke | stroked centre lines (Create: Strokes) against the thin lines they trace: mean distance (px) from the true line's centre line, mean error (px) of the stroke width, anchors per stroked path |
| shapes % | with Shapes on, the share of the plain shapes that came out as that shape (a shape, not a path, whose outline lies within 0.6 px of the true one) |
| grouped % | with Auto Grouping on, the share of the paths that were put inside another (the picture is painted in the grouped order for every other measure of that row, so a ΔE equal to the row without the option says grouping changed nothing that shows) |
| outside px | how far the trace reaches outside the image: a curve that strays |
Black-and-white presets trace black: on the scanned variant their ΔE includes the paper's and ink's own
@@ -56,201 +62,325 @@ Each preset on the inputs it is meant for, all sizes and variants.
### Baseline (before any change, main at `baf9693f`)
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.79 | 17.15 | 0.313 | 1.51 | 2.52 | 1.9 | 14 | 1.45 | 34 | 76 | 1.4 | – | – | 63.5 | 67.0 | 0.0 | 0.0 |
| High Fidelity Photo | gradient, photo-like | 2.30 | 2.63 | – | – | – | 3652.6 | 46866 | – | 84 | – | – | – | – | 0.0 | – | 1169.4 | 0.3 |
| Low Fidelity Photo | gradient, photo-like | 2.61 | 2.94 | – | – | – | 128.3 | 3867 | – | 42 | – | – | – | – | 0.0 | – | 0.0 | 0.7 |
| 3 Colors | two touching halves | 4.93 | 11.38 | 0.482 | 2.35 | 89.09 | 2.1 | 14 | 2.31 | 12 | 29 | 2.1 | 1.6 | 15418 | 118.0 | 87.3 | 0.0 | 0.0 |
| 6 Colors | two touching halves, four-colour badge | 0.78 | 8.82 | 4.383 | 83.10 | 30.11 | 21.2 | 270 | 30.18 | 51 | 98 | 213.8 | 2.4 | 272 | 198.8 | 194.6 | 0.0 | 0.3 |
| 16 Colors | two touching halves, four-colour badge | 0.74 | 9.02 | 5.115 | 86.21 | 4.32 | 69.9 | 923 | 102.80 | 58 | 100 | 765.9 | 5.9 | 670 | 293.7 | 329.9 | 0.0 | 0.5 |
| Shades of Gray | gradient, photo-like | 9.29 | 9.16 | – | – | – | 90.8 | 1921 | – | 42 | – | – | – | – | 0.0 | – | 0.2 | 0.5 |
| Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.76 | 15.96 | 0.270 | 1.76 | 2.53 | 1.4 | 31 | 3.78 | 90 | 81 | 15.3 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.85 | 17.04 | 0.302 | 1.63 | 105.63 | 1.8 | 14 | 1.34 | 28 | 18 | 1.4 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | circle, ellipses, badge with a hole, letter D | 3.04 | 21.41 | 0.573 | 2.88 | 2.89 | 1.2 | 10 | 1.16 | 9 | 86 | 1.4 | – | – | – | – | 0.0 | 0.0 |
| Line Art | lines | 2.77 | 12.51 | 0.158 | 1.13 | 105.76 | 5.4 | 25 | 1.27 | 90 | – | – | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | polygons, star, letter A, lines | 2.66 | 13.45 | 0.201 | 1.37 | 105.76 | 4.8 | 32 | 2.23 | 100 | 85 | 5.9 | – | – | – | – | 0.1 | 0.0 |
| Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 |
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.79 | 17.15 | 0.313 | 1.51 | 2.52 | 1.9 | 14 | 1.45 | 34 | 5 | 76 | 1.4 | – | – | 63.5 | 67.0 | 0.0 | 0.0 | – | – | – |
| High Fidelity Photo | gradient, photo-like | 2.30 | 2.63 | – | – | – | 3652.6 | 46866 | – | 84 | 29 | – | – | – | – | 0.0 | – | 1169.4 | 0.3 | – | – | – |
| Low Fidelity Photo | gradient, photo-like | 2.61 | 2.94 | – | – | – | 128.3 | 3867 | – | 42 | 22 | – | – | – | – | 0.0 | – | 0.0 | 0.7 | – | – | – |
| 3 Colors | two touching halves | 4.93 | 11.38 | 0.482 | 2.35 | 89.09 | 2.1 | 14 | 2.31 | 12 | 8 | 29 | 2.1 | 1.6 | 15418 | 118.0 | 87.3 | 0.0 | 0.0 | – | – | – |
| 6 Colors | two touching halves, four-colour badge | 0.78 | 8.82 | 4.383 | 83.10 | 30.11 | 21.2 | 270 | 30.18 | 51 | 40 | 98 | 213.8 | 2.4 | 272 | 198.8 | 194.6 | 0.0 | 0.3 | – | – | – |
| 16 Colors | two touching halves, four-colour badge | 0.74 | 9.02 | 5.115 | 86.21 | 4.32 | 69.9 | 923 | 102.80 | 58 | 47 | 100 | 765.9 | 5.9 | 670 | 293.7 | 329.9 | 0.0 | 0.5 | – | – | – |
| Shades of Gray | gradient, photo-like | 9.29 | 9.16 | – | – | – | 90.8 | 1921 | – | 42 | 27 | – | – | – | – | 0.0 | – | 0.2 | 0.5 | – | – | – |
| Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.76 | 15.96 | 0.270 | 1.76 | 2.53 | 1.4 | 31 | 3.78 | 90 | 17 | 81 | 15.3 | – | – | – | – | 0.0 | 0.0 | – | – | – |
| Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.85 | 17.04 | 0.302 | 1.63 | 105.63 | 1.8 | 14 | 1.34 | 28 | 8 | 18 | 1.4 | – | – | – | – | 0.0 | 0.0 | – | – | – |
| Silhouettes | circle, ellipses, badge with a hole, letter D | 3.04 | 21.41 | 0.573 | 2.88 | 2.89 | 1.2 | 10 | 1.16 | 9 | 7 | 86 | 1.4 | – | – | – | – | 0.0 | 0.0 | – | – | – |
| Line Art | lines | 2.77 | 12.51 | 0.158 | 1.13 | 105.76 | 5.4 | 25 | 1.27 | 90 | 6 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – |
| Technical Drawing | polygons, star, letter A, lines, askew shapes and lines | 2.73 | 13.67 | 0.210 | 1.43 | 105.76 | 4.7 | 31 | 2.01 | 100 | 9 | 85 | 5.9 | – | – | – | – | 0.1 | 0.0 | – | – | – |
| Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 10 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 | – | – | – |
### Now (sub-pixel edges, the palette, photo smoothing, shared edges, memory, thin lines)
### Now (with Auto Grouping)
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 10.02 | 0.090 | 1.02 | 2.22 | 1.9 | 17 | 1.74 | 29 | 69 | 0.4 | – | – | 0.5 | 0.6 | 0.0 | 0.0 |
| High Fidelity Photo | gradient, photo-like | 1.75 | 1.97 | – | – | – | 120.7 | 10001 | – | 38 | – | – | – | – | 0.0 | – | 10.3 | 0.0 |
| Low Fidelity Photo | gradient, photo-like | 2.45 | 2.69 | – | – | – | 37.1 | 2118 | – | 41 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| 3 Colors | two touching halves | 0.42 | 3.98 | 0.104 | 0.91 | 0.91 | 2.0 | 11 | 1.81 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | two touching halves, four-colour badge | 0.55 | 4.98 | 0.140 | 1.34 | 1.19 | 4.0 | 50 | 3.13 | 38 | 86 | 5.2 | 0.0 | 0 | 0.2 | 0.3 | 0.0 | 0.0 |
| 16 Colors | two touching halves, four-colour badge | 0.55 | 4.90 | 0.141 | 1.31 | 1.19 | 4.2 | 62 | 3.79 | 39 | 86 | 6.4 | 0.0 | 0 | 0.2 | 0.2 | 0.0 | 0.0 |
| Shades of Gray | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.5 | 695 | – | 51 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.58 | 9.33 | 0.059 | 1.02 | 2.22 | 1.4 | 29 | 2.88 | 39 | 69 | 0.3 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.71 | 11.98 | 0.186 | 1.00 | 105.61 | 1.8 | 17 | 1.47 | 16 | 86 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | circle, ellipses, badge with a hole, letter D | 2.98 | 19.28 | 0.524 | 2.19 | 2.18 | 1.2 | 14 | 1.65 | 11 | 79 | 0.3 | – | – | – | – | 0.0 | 0.0 |
| Line Art | lines | 2.62 | 9.44 | 0.055 | 0.96 | 105.82 | 5.4 | 46 | 2.29 | 41 | – | – | – | – | – | – | 0.1 | 0.0 |
| Technical Drawing | polygons, star, letter A, lines | 2.54 | 9.65 | 0.084 | 1.02 | 105.82 | 4.8 | 46 | 3.23 | 71 | 73 | 0.8 | – | – | – | – | 1.2 | 0.0 |
| Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 |
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke | shapes % | grouped % |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 10.05 | 0.091 | 1.02 | 2.18 | 1.9 | 17 | 1.73 | 30 | 6 | 76 | 0.4 | – | – | 0.3 | 0.5 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | two touching halves, four-colour badge | 0.55 | 4.99 | 0.141 | 1.33 | 1.21 | 4.0 | 51 | 3.19 | 39 | 16 | 91 | 6.0 | 0.0 | 0 | 1.5 | 1.2 | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | gradient, photo-like | 2.45 | 2.69 | – | – | – | 37.1 | 2134 | – | 43 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | two touching halves | 0.42 | 3.98 | 0.104 | 0.91 | 0.91 | 2.0 | 11 | 1.81 | 39 | 6 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | two touching halves, four-colour badge | 0.55 | 4.99 | 0.140 | 1.34 | 1.19 | 4.0 | 49 | 3.07 | 39 | 16 | 91 | 6.1 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | two touching halves, four-colour badge | 0.55 | 4.91 | 0.142 | 1.31 | 1.19 | 4.2 | 63 | 3.87 | 40 | 16 | 91 | 7.7 | 0.0 | 0 | 0.5 | 0.6 | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.6 | 700 | – | 51 | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.59 | 9.46 | 0.064 | 1.02 | 2.17 | 1.4 | 27 | 2.68 | 42 | 11 | 76 | 0.3 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.71 | 12.02 | 0.185 | 1.00 | 105.61 | 1.8 | 16 | 1.42 | 19 | 8 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | circle, ellipses, badge with a hole, letter D | 2.98 | 19.30 | 0.524 | 2.19 | 2.19 | 1.2 | 14 | 1.64 | 12 | 9 | 93 | 0.4 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | lines | 2.62 | 9.49 | 0.056 | 0.99 | 105.72 | 5.4 | 43 | 2.14 | 40 | 0 | – | – | – | – | – | – | 0.1 | 0.0 | – | – | – | – | – |
| Technical Drawing | polygons, star, letter A, lines, askew shapes and lines | 2.71 | 12.14 | 0.178 | 4.21 | 105.64 | 4.7 | 38 | 2.55 | 81 | 24 | 78 | 0.8 | – | – | – | – | 0.5 | 0.0 | – | – | – | – | – |
| Outline | lines | 2.60 | 9.16 | 0.055 | 2.94 | 105.75 | 5.0 | 14 | 0.71 | 95 | 4 | – | – | – | – | – | – | 0.0 | 0.0 | 0.053 | 0.02 | 2.4 | – | – |
| Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 10 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 | – | – | – | – | – |
| Default with Shapes | plain shapes | 3.18 | 9.53 | 0.102 | 1.00 | 1.00 | 6.0 | 40 | 1.32 | 41 | 27 | – | – | – | – | 15.8 | 11.5 | 0.0 | 0.0 | – | – | – | 57 | – |
| 6 Colors with Shapes | plain shapes | 0.91 | 6.82 | 0.089 | 0.99 | 0.99 | 6.0 | 43 | 1.43 | 49 | 33 | – | – | 0.0 | 0 | 10.2 | 10.9 | 0.0 | 0.0 | – | – | – | 62 | – |
| 6 Colors with Auto Grouping | two touching halves, four-colour badge | 0.55 | 4.99 | 0.140 | 1.34 | 1.19 | 4.0 | 49 | 3.07 | 39 | 16 | 91 | 6.1 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 | – | – | – | – | 27 |
| Shades of Gray with Auto Grouping | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.6 | 700 | – | 51 | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 47 |
| High Fidelity Photo with Auto Grouping | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | 69 |
The sections below are the numbers now (sub-pixel edges, the palette, photo smoothing, shared edges, memory, thin lines).
The sections below are the numbers now (with Auto Grouping).
## 1. Each preset on the inputs it is meant for
All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs.
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 10.02 | 0.090 | 1.02 | 2.22 | 1.9 | 17 | 1.74 | 29 | 69 | 0.4 | – | – | 0.5 | 0.6 | 0.0 | 0.0 |
| High Fidelity Photo | gradient, photo-like | 1.75 | 1.97 | – | – | – | 120.7 | 10001 | – | 38 | – | – | – | – | 0.0 | – | 10.3 | 0.0 |
| Low Fidelity Photo | gradient, photo-like | 2.45 | 2.69 | – | – | – | 37.1 | 2118 | – | 41 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| 3 Colors | two touching halves | 0.42 | 3.98 | 0.104 | 0.91 | 0.91 | 2.0 | 11 | 1.81 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | two touching halves, four-colour badge | 0.55 | 4.98 | 0.140 | 1.34 | 1.19 | 4.0 | 50 | 3.13 | 38 | 86 | 5.2 | 0.0 | 0 | 0.2 | 0.3 | 0.0 | 0.0 |
| 16 Colors | two touching halves, four-colour badge | 0.55 | 4.90 | 0.141 | 1.31 | 1.19 | 4.2 | 62 | 3.79 | 39 | 86 | 6.4 | 0.0 | 0 | 0.2 | 0.2 | 0.0 | 0.0 |
| Shades of Gray | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.5 | 695 | – | 51 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.58 | 9.33 | 0.059 | 1.02 | 2.22 | 1.4 | 29 | 2.88 | 39 | 69 | 0.3 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.71 | 11.98 | 0.186 | 1.00 | 105.61 | 1.8 | 17 | 1.47 | 16 | 86 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | circle, ellipses, badge with a hole, letter D | 2.98 | 19.28 | 0.524 | 2.19 | 2.18 | 1.2 | 14 | 1.65 | 11 | 79 | 0.3 | – | – | – | – | 0.0 | 0.0 |
| Line Art | lines | 2.62 | 9.44 | 0.055 | 0.96 | 105.82 | 5.4 | 46 | 2.29 | 41 | – | – | – | – | – | – | 0.1 | 0.0 |
| Technical Drawing | polygons, star, letter A, lines | 2.54 | 9.65 | 0.084 | 1.02 | 105.82 | 4.8 | 46 | 3.23 | 71 | 73 | 0.8 | – | – | – | – | 1.2 | 0.0 |
| Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 |
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke | shapes % | grouped % |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 10.05 | 0.091 | 1.02 | 2.18 | 1.9 | 17 | 1.73 | 30 | 6 | 76 | 0.4 | – | – | 0.3 | 0.5 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | two touching halves, four-colour badge | 0.55 | 4.99 | 0.141 | 1.33 | 1.21 | 4.0 | 51 | 3.19 | 39 | 16 | 91 | 6.0 | 0.0 | 0 | 1.5 | 1.2 | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | gradient, photo-like | 2.45 | 2.69 | – | – | – | 37.1 | 2134 | – | 43 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | two touching halves | 0.42 | 3.98 | 0.104 | 0.91 | 0.91 | 2.0 | 11 | 1.81 | 39 | 6 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | two touching halves, four-colour badge | 0.55 | 4.99 | 0.140 | 1.34 | 1.19 | 4.0 | 49 | 3.07 | 39 | 16 | 91 | 6.1 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | two touching halves, four-colour badge | 0.55 | 4.91 | 0.142 | 1.31 | 1.19 | 4.2 | 63 | 3.87 | 40 | 16 | 91 | 7.7 | 0.0 | 0 | 0.5 | 0.6 | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.6 | 700 | – | 51 | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| Black and White Logo | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.59 | 9.46 | 0.064 | 1.02 | 2.17 | 1.4 | 27 | 2.68 | 42 | 11 | 76 | 0.3 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | circle, ellipses, badge with a hole, letter D, lines | 2.71 | 12.02 | 0.185 | 1.00 | 105.61 | 1.8 | 16 | 1.42 | 19 | 8 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | circle, ellipses, badge with a hole, letter D | 2.98 | 19.30 | 0.524 | 2.19 | 2.19 | 1.2 | 14 | 1.64 | 12 | 9 | 93 | 0.4 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | lines | 2.62 | 9.49 | 0.056 | 0.99 | 105.72 | 5.4 | 43 | 2.14 | 40 | 0 | – | – | – | – | – | – | 0.1 | 0.0 | – | – | – | – | – |
| Technical Drawing | polygons, star, letter A, lines, askew shapes and lines | 2.71 | 12.14 | 0.178 | 4.21 | 105.64 | 4.7 | 38 | 2.55 | 81 | 24 | 78 | 0.8 | – | – | – | – | 0.5 | 0.0 | – | – | – | – | – |
| Outline | lines | 2.60 | 9.16 | 0.055 | 2.94 | 105.75 | 5.0 | 14 | 0.71 | 95 | 4 | – | – | – | – | – | – | 0.0 | 0.0 | 0.053 | 0.02 | 2.4 | – | – |
| Flat Logo | two touching halves, four-colour badge | 0.43 | 3.53 | 3.106 | 88.50 | 0.78 | 69.9 | 352 | 38.92 | 38 | 10 | 80 | 237.0 | 0.0 | 0 | 13.8 | – | 59.1 | 0.2 | – | – | – | – | – |
| Default with Shapes | plain shapes | 3.18 | 9.53 | 0.102 | 1.00 | 1.00 | 6.0 | 40 | 1.32 | 41 | 27 | – | – | – | – | 15.8 | 11.5 | 0.0 | 0.0 | – | – | – | 57 | – |
| 6 Colors with Shapes | plain shapes | 0.91 | 6.82 | 0.089 | 0.99 | 0.99 | 6.0 | 43 | 1.43 | 49 | 33 | – | – | 0.0 | 0 | 10.2 | 10.9 | 0.0 | 0.0 | – | – | – | 62 | – |
| 6 Colors with Auto Grouping | two touching halves, four-colour badge | 0.55 | 4.99 | 0.140 | 1.34 | 1.19 | 4.0 | 49 | 3.07 | 39 | 16 | 91 | 6.1 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 | – | – | – | – | 27 |
| Shades of Gray with Auto Grouping | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.6 | 700 | – | 51 | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 47 |
| High Fidelity Photo with Auto Grouping | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | 69 |
## 2. Each preset on each kind of input (clean, three sizes)
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | curved shapes | 0.09 | 3.47 | 0.061 | 0.64 | 0.78 | 1.8 | 15 | 1.62 | 13 | 83 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | polygons | 0.05 | 2.12 | 0.035 | 0.43 | 1.24 | 2.0 | 14 | 1.26 | 88 | 89 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | thin lines | 0.53 | 4.28 | 0.045 | 0.47 | 105.51 | 3.3 | 23 | 1.15 | 48 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | flat colour art | 19.91 | 17.97 | 0.172 | 0.51 | 179.34 | 1.5 | 16 | 1.17 | 14 | 0 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | gradient, photo | 39.03 | 38.65 | – | – | – | 3.2 | 601 | – | 54 | – | – | – | – | 19.1 | 38.4 | 0.0 | 0.0 |
| High Fidelity Photo | curved shapes | 0.04 | 1.66 | 0.026 | 0.18 | 0.20 | 1.2 | 19 | 2.12 | 20 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| High Fidelity Photo | polygons | 0.05 | 1.88 | 0.031 | 0.51 | 1.34 | 1.7 | 16 | 1.43 | 88 | 89 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| High Fidelity Photo | thin lines | 0.34 | 5.96 | 0.075 | 0.85 | 4.31 | 40.3 | 339 | 16.93 | 37 | – | – | 27.3 | 4490 | 0.0 | – | 19.0 | 0.0 |
| High Fidelity Photo | flat colour art | 0.14 | 4.16 | 0.030 | 0.20 | 0.20 | 3.5 | 73 | 4.62 | 35 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| High Fidelity Photo | gradient, photo | 1.80 | 2.04 | – | – | – | 134.7 | 11321 | – | 39 | – | – | – | – | 0.0 | – | 7.3 | 0.0 |
| Low Fidelity Photo | curved shapes | 0.06 | 2.65 | 0.047 | 0.31 | 0.31 | 1.2 | 13 | 1.47 | 17 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | polygons | 0.05 | 2.15 | 0.036 | 0.51 | 1.34 | 1.7 | 14 | 1.22 | 88 | 89 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | thin lines | 0.51 | 5.69 | 0.083 | 0.86 | 45.41 | 7.0 | 56 | 2.80 | 47 | – | – | 4.0 | 3966 | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | flat colour art | 0.15 | 4.29 | 0.045 | 0.28 | 0.28 | 3.5 | 54 | 3.38 | 31 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | gradient, photo | 2.53 | 2.80 | – | – | – | 42.7 | 2282 | – | 41 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| 3 Colors | curved shapes | 0.06 | 2.65 | 0.047 | 0.31 | 0.31 | 1.8 | 16 | 1.70 | 15 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | polygons | 0.05 | 2.15 | 0.036 | 0.51 | 1.34 | 2.0 | 15 | 1.32 | 88 | 89 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | thin lines | 0.45 | 4.38 | 0.113 | 0.90 | 45.41 | 5.0 | 44 | 2.18 | 50 | – | – | 2.0 | 290 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | flat colour art | 1.31 | 3.61 | 0.125 | 0.90 | 60.25 | 2.5 | 25 | 1.86 | 45 | 100 | 0.0 | 1.0 | 9914 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | gradient, photo | 7.49 | 7.60 | – | – | – | 5.7 | 530 | – | 47 | – | – | – | – | 6.3 | 7.4 | 0.0 | 0.0 |
| 6 Colors | curved shapes | 0.06 | 2.45 | 0.043 | 0.27 | 0.28 | 1.8 | 17 | 1.78 | 15 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | polygons | 0.05 | 2.08 | 0.035 | 0.51 | 1.34 | 2.0 | 15 | 1.33 | 88 | 89 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | thin lines | 0.50 | 4.49 | 0.102 | 0.86 | 45.41 | 4.0 | 33 | 1.65 | 53 | – | – | 1.7 | 396 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | flat colour art | 0.12 | 2.70 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | gradient, photo | 5.00 | 5.28 | – | – | – | 13.0 | 832 | – | 36 | – | – | – | – | 6.1 | 8.2 | 0.0 | 0.0 |
| 16 Colors | curved shapes | 0.05 | 2.21 | 0.038 | 0.24 | 0.25 | 1.8 | 17 | 1.85 | 16 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | polygons | 0.05 | 1.97 | 0.032 | 0.51 | 1.34 | 2.0 | 16 | 1.39 | 88 | 89 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | thin lines | 0.41 | 3.77 | 0.110 | 0.86 | 23.65 | 8.7 | 50 | 2.52 | 52 | – | – | 5.7 | 305 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | flat colour art | 0.12 | 2.52 | 0.052 | 0.31 | 0.30 | 3.0 | 28 | 2.11 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | gradient, photo | 3.00 | 3.30 | – | – | – | 47.2 | 2132 | – | 27 | – | – | – | – | 12.3 | 18.0 | 0.0 | 0.0 |
| Shades of Gray | curved shapes | 0.06 | 2.67 | 0.047 | 0.31 | 0.31 | 1.2 | 14 | 1.49 | 17 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| Shades of Gray | polygons | 0.05 | 2.08 | 0.035 | 0.47 | 1.25 | 1.7 | 14 | 1.22 | 88 | 90 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 |
| Shades of Gray | thin lines | 0.55 | 6.55 | 0.083 | 0.86 | 45.40 | 10.3 | 73 | 3.67 | 50 | – | – | 8.0 | 4272 | 0.0 | – | 0.0 | 0.0 |
| Shades of Gray | flat colour art | 7.33 | 11.24 | 0.257 | 1.43 | 179.39 | 4.7 | 200 | 18.45 | 22 | 50 | 0.3 | 3.7 | 173367 | 0.0 | – | 0.0 | 0.0 |
| Shades of Gray | gradient, photo | 9.71 | 9.55 | – | – | – | 18.2 | 748 | – | 51 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| Black and White Logo | curved shapes | 0.03 | 1.49 | 0.024 | 0.51 | 0.59 | 1.2 | 21 | 2.23 | 20 | 83 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | polygons | 0.04 | 1.78 | 0.029 | 0.42 | 1.24 | 1.7 | 16 | 1.42 | 89 | 89 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | thin lines | 0.63 | 6.33 | 0.117 | 0.50 | 77.21 | 10.3 | 53 | 2.63 | 89 | – | – | – | – | – | – | 2.7 | 0.0 |
| Black and White Logo | flat colour art | 19.90 | 17.76 | 0.157 | 0.30 | 179.51 | 1.0 | 18 | 1.12 | 23 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | gradient, photo | 38.98 | 38.54 | – | – | – | 9.0 | 573 | – | 70 | – | – | – | – | – | – | 1.2 | 0.0 |
| Sketched Art | curved shapes | 0.14 | 5.67 | 0.109 | 0.46 | 0.46 | 1.2 | 12 | 1.38 | 16 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | polygons | 0.12 | 4.65 | 0.084 | 0.53 | 1.06 | 1.7 | 13 | 1.19 | 86 | 89 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | thin lines | 0.41 | 4.87 | 0.099 | 0.96 | 52.90 | 4.3 | 35 | 1.77 | 38 | – | – | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | flat colour art | 20.25 | 19.55 | 0.146 | 1.15 | 179.63 | 1.5 | 26 | 1.53 | 16 | 0 | 0.3 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | gradient, photo | 45.43 | 45.21 | – | – | – | 1.0 | 240 | – | 44 | – | – | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | curved shapes | 0.35 | 14.47 | 0.280 | 0.69 | 0.69 | 1.2 | 12 | 1.33 | 15 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | polygons | 0.34 | 13.60 | 0.255 | 0.65 | 0.69 | 1.7 | 14 | 1.22 | 77 | 85 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | thin lines | 1.10 | 15.03 | 0.296 | 1.37 | 17.91 | 5.0 | 37 | 1.83 | 44 | – | – | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | flat colour art | 23.83 | 33.06 | 0.200 | 0.69 | 188.27 | 1.0 | 11 | 0.77 | 25 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | gradient, photo | 61.57 | 61.32 | – | – | – | 0.2 | 2 | – | 47 | – | – | – | – | – | – | 0.0 | 0.0 |
| Line Art | curved shapes | 0.04 | 1.85 | 0.032 | 0.54 | 0.62 | 1.2 | 16 | 1.76 | 19 | 83 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Line Art | polygons | 0.05 | 1.86 | 0.030 | 0.39 | 1.24 | 1.7 | 15 | 1.29 | 88 | 89 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Line Art | thin lines | 0.52 | 3.94 | 0.036 | 0.47 | 105.71 | 3.3 | 28 | 1.38 | 45 | – | – | – | – | – | – | 0.0 | 0.0 |
| Line Art | flat colour art | 19.90 | 17.83 | 0.162 | 0.42 | 179.43 | 1.0 | 18 | 1.30 | 21 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Line Art | gradient, photo | 38.98 | 38.54 | – | – | – | 7.7 | 471 | – | 57 | – | – | – | – | – | – | 0.2 | 0.0 |
| Technical Drawing | curved shapes | 0.03 | 1.49 | 0.024 | 0.51 | 0.59 | 1.2 | 21 | 2.23 | 20 | 83 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | polygons | 0.04 | 1.79 | 0.029 | 0.42 | 1.24 | 1.7 | 16 | 1.41 | 90 | 90 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | thin lines | 0.53 | 5.80 | 0.121 | 0.50 | 55.74 | 15.3 | 70 | 3.50 | 88 | – | – | – | – | – | – | 7.7 | 0.0 |
| Technical Drawing | flat colour art | 19.90 | 17.76 | 0.157 | 0.30 | 179.51 | 1.0 | 18 | 1.12 | 23 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | gradient, photo | 38.94 | 38.51 | – | – | – | 20.0 | 627 | – | 71 | – | – | – | – | – | – | 11.3 | 0.0 |
| Flat Logo | curved shapes | 0.06 | 2.14 | 0.036 | 0.25 | 0.25 | 1.8 | 31 | 3.38 | 19 | 100 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 |
| Flat Logo | polygons | 0.09 | 3.44 | 0.060 | 0.35 | 1.03 | 2.0 | 35 | 3.17 | 67 | 94 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 |
| Flat Logo | thin lines | 1.17 | 11.78 | 0.050 | 0.49 | all | 3.0 | 189 | 9.45 | 28 | – | – | 0.0 | 0 | 5467.1 | – | 0.0 | 0.0 |
| Flat Logo | flat colour art | 0.07 | 1.53 | 0.065 | 0.31 | 0.27 | 3.0 | 36 | 3.51 | 47 | 100 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 |
| Flat Logo | gradient, photo | refused | | | | | | | | | | | | | | | | |
| preset | inputs | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke | shapes % | grouped % |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | curved shapes | 0.09 | 3.40 | 0.060 | 0.38 | 0.38 | 1.8 | 14 | 1.57 | 14 | 14 | 100 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | polygons | 0.06 | 2.37 | 0.041 | 0.47 | 1.26 | 2.0 | 14 | 1.26 | 88 | 5 | 95 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | thin lines | 0.54 | 4.46 | 0.049 | 0.47 | 105.72 | 3.3 | 25 | 1.27 | 40 | 0 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | askew shapes | 0.16 | 3.29 | 0.044 | 0.34 | 36.06 | 3.7 | 23 | 0.94 | 100 | 3 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | plain shapes | 0.13 | 2.63 | 0.055 | 0.35 | 0.35 | 6.0 | 40 | 1.34 | 53 | 48 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | 0 | – |
| Default | flat colour art | 19.91 | 17.97 | 0.172 | 0.51 | 179.34 | 1.5 | 16 | 1.17 | 14 | 14 | 0 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | gradient, photo | 39.03 | 38.64 | – | – | – | 3.2 | 595 | – | 55 | 41 | – | – | – | – | 18.8 | 35.5 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | curved shapes | 0.06 | 2.43 | 0.043 | 0.27 | 0.28 | 1.8 | 16 | 1.77 | 16 | 16 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | polygons | 0.06 | 2.42 | 0.041 | 0.42 | 1.39 | 2.0 | 16 | 1.43 | 87 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | thin lines | 0.70 | 5.57 | 0.066 | 0.50 | 105.72 | 5.0 | 38 | 1.88 | 43 | 3 | – | – | 4.0 | 3225 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | askew shapes | 0.64 | 3.76 | 0.047 | 0.36 | 9.49 | 4.0 | 24 | 1.00 | 100 | 3 | – | – | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | plain shapes | 0.10 | 2.07 | 0.043 | 0.30 | 0.26 | 6.0 | 42 | 1.41 | 59 | 49 | – | – | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | 0 | – |
| Auto Color | flat colour art | 0.13 | 2.72 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | gradient, photo | 2.07 | 2.39 | – | – | – | 159.3 | 4441 | – | 21 | 8 | – | – | – | – | 37.6 | 46.7 | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | curved shapes | 0.04 | 1.63 | 0.026 | 0.15 | 0.15 | 1.2 | 19 | 2.11 | 21 | 21 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | polygons | 0.06 | 2.33 | 0.039 | 0.50 | 1.32 | 1.7 | 17 | 1.47 | 88 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | thin lines | 0.35 | 6.05 | 0.085 | 0.90 | 4.35 | 40.3 | 348 | 17.42 | 35 | 9 | – | – | 27.3 | 4505 | 0.0 | – | 19.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | askew shapes | 0.17 | 3.97 | 0.100 | 0.94 | 1.73 | 8.7 | 157 | 6.54 | 71 | 5 | – | – | 4.3 | 3998 | 0.0 | – | 1.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | plain shapes | 0.08 | 1.66 | 0.035 | 0.25 | 0.28 | 6.0 | 50 | 1.68 | 62 | 51 | – | – | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | 0 | – |
| High Fidelity Photo | flat colour art | 0.14 | 4.16 | 0.029 | 0.20 | 0.20 | 3.5 | 73 | 4.59 | 35 | 34 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | gradient, photo | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | curved shapes | 0.07 | 2.74 | 0.048 | 0.31 | 0.31 | 1.2 | 13 | 1.44 | 18 | 18 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | polygons | 0.06 | 2.49 | 0.042 | 0.42 | 1.39 | 1.7 | 14 | 1.28 | 86 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | thin lines | 0.49 | 5.70 | 0.082 | 0.70 | 45.31 | 7.0 | 56 | 2.80 | 49 | 11 | – | – | 4.0 | 3985 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | askew shapes | 0.23 | 4.65 | 0.059 | 0.70 | 21.55 | 6.0 | 42 | 1.76 | 85 | 3 | – | – | 2.3 | 2630 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | plain shapes | 0.11 | 2.25 | 0.047 | 0.30 | 0.28 | 6.0 | 41 | 1.38 | 58 | 48 | – | – | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Low Fidelity Photo | flat colour art | 0.15 | 4.25 | 0.044 | 0.28 | 0.28 | 3.5 | 54 | 3.38 | 31 | 29 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | gradient, photo | 2.53 | 2.80 | – | – | – | 42.7 | 2307 | – | 43 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | curved shapes | 0.07 | 2.74 | 0.048 | 0.31 | 0.31 | 1.8 | 16 | 1.67 | 15 | 15 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | polygons | 0.06 | 2.49 | 0.042 | 0.42 | 1.39 | 2.0 | 16 | 1.42 | 86 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | thin lines | 0.43 | 4.48 | 0.117 | 1.04 | 45.31 | 5.0 | 43 | 2.13 | 54 | 13 | – | – | 2.0 | 308 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | askew shapes | 0.18 | 3.58 | 0.054 | 0.70 | 14.59 | 4.0 | 25 | 1.04 | 97 | 3 | – | – | 0.7 | 23 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | plain shapes | 0.11 | 2.25 | 0.047 | 0.30 | 0.28 | 6.0 | 41 | 1.38 | 58 | 48 | – | – | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | 0 | – |
| 3 Colors | flat colour art | 1.31 | 3.61 | 0.125 | 0.90 | 60.25 | 2.5 | 25 | 1.86 | 45 | 21 | 100 | 0.0 | 1.0 | 9914 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | gradient, photo | 7.49 | 7.60 | – | – | – | 5.7 | 524 | – | 48 | 40 | – | – | – | – | 6.9 | 8.7 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | curved shapes | 0.06 | 2.43 | 0.043 | 0.27 | 0.28 | 1.8 | 16 | 1.77 | 16 | 16 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | polygons | 0.06 | 2.40 | 0.041 | 0.42 | 1.39 | 2.0 | 16 | 1.45 | 86 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | thin lines | 0.49 | 4.76 | 0.108 | 0.70 | 45.31 | 4.0 | 30 | 1.50 | 63 | 19 | – | – | 1.7 | 413 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | askew shapes | 0.19 | 3.72 | 0.058 | 0.73 | 36.06 | 4.0 | 25 | 1.06 | 90 | 3 | – | – | 0.3 | 11 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | plain shapes | 0.10 | 2.07 | 0.043 | 0.30 | 0.26 | 6.0 | 42 | 1.41 | 59 | 49 | – | – | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | 0 | – |
| 6 Colors | flat colour art | 0.13 | 2.72 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | gradient, photo | 5.00 | 5.28 | – | – | – | 13.0 | 829 | – | 37 | 28 | – | – | – | – | 6.9 | 8.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | curved shapes | 0.05 | 2.21 | 0.038 | 0.24 | 0.25 | 1.8 | 17 | 1.83 | 16 | 16 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | polygons | 0.06 | 2.34 | 0.039 | 0.42 | 1.39 | 2.0 | 16 | 1.48 | 86 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | thin lines | 0.39 | 3.90 | 0.113 | 0.70 | 23.53 | 8.7 | 52 | 2.58 | 56 | 17 | – | – | 5.7 | 322 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | askew shapes | 0.18 | 3.65 | 0.062 | 0.73 | 21.55 | 4.3 | 28 | 1.17 | 87 | 3 | – | – | 0.7 | 17 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | plain shapes | 0.09 | 1.92 | 0.041 | 0.30 | 0.26 | 6.0 | 44 | 1.46 | 60 | 49 | – | – | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | 0 | – |
| 16 Colors | flat colour art | 0.12 | 2.53 | 0.051 | 0.31 | 0.30 | 3.0 | 28 | 2.11 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | gradient, photo | 3.00 | 3.30 | – | – | – | 47.2 | 2126 | – | 30 | 16 | – | – | – | – | 15.2 | 19.6 | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | curved shapes | 0.07 | 2.70 | 0.047 | 0.31 | 0.31 | 1.2 | 13 | 1.45 | 18 | 18 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | polygons | 0.06 | 2.28 | 0.039 | 0.41 | 1.26 | 1.7 | 13 | 1.18 | 89 | 5 | 95 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | thin lines | 0.56 | 7.08 | 0.088 | 0.68 | 45.31 | 10.3 | 73 | 3.65 | 55 | 11 | – | – | 8.0 | 4319 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | askew shapes | 0.25 | 4.95 | 0.049 | 0.73 | 21.52 | 6.0 | 39 | 1.61 | 95 | 3 | – | – | 2.3 | 2638 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | plain shapes | 0.12 | 2.31 | 0.048 | 0.31 | 0.28 | 6.0 | 42 | 1.39 | 54 | 48 | – | – | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Shades of Gray | flat colour art | 7.33 | 11.24 | 0.257 | 1.47 | 179.39 | 4.7 | 199 | 18.39 | 22 | 22 | 50 | 0.3 | 3.7 | 173367 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | gradient, photo | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| Black and White Logo | curved shapes | 0.03 | 1.48 | 0.024 | 0.16 | 0.16 | 1.2 | 20 | 2.12 | 21 | 21 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | polygons | 0.06 | 2.36 | 0.040 | 0.47 | 1.26 | 1.7 | 14 | 1.24 | 93 | 8 | 95 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | thin lines | 0.59 | 6.00 | 0.105 | 0.50 | 77.21 | 10.3 | 44 | 2.18 | 90 | 10 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | askew shapes | 0.89 | 20.99 | 0.613 | 4.21 | 9.63 | 4.0 | 25 | 1.03 | 98 | 98 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | plain shapes | 0.08 | 1.56 | 0.033 | 0.31 | 0.30 | 6.0 | 55 | 1.84 | 58 | 52 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Black and White Logo | flat colour art | 19.90 | 17.76 | 0.157 | 0.30 | 179.51 | 1.0 | 18 | 1.12 | 23 | 23 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | gradient, photo | 38.97 | 38.53 | – | – | – | 9.0 | 634 | – | 73 | 54 | – | – | – | – | – | – | 1.2 | 0.0 | – | – | – | – | – |
| Sketched Art | curved shapes | 0.14 | 5.71 | 0.110 | 0.46 | 0.46 | 1.2 | 12 | 1.37 | 16 | 16 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | polygons | 0.12 | 4.71 | 0.085 | 0.37 | 1.08 | 1.7 | 13 | 1.16 | 86 | 5 | 93 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | thin lines | 0.47 | 5.70 | 0.113 | 0.96 | 52.59 | 4.3 | 29 | 1.45 | 64 | 3 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | askew shapes | 0.32 | 6.03 | 0.091 | 0.45 | 35.94 | 3.3 | 23 | 0.94 | 96 | 4 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | plain shapes | 0.30 | 5.84 | 0.107 | 0.47 | 0.47 | 6.0 | 40 | 1.32 | 53 | 47 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Sketched Art | flat colour art | 20.25 | 19.55 | 0.146 | 1.15 | 179.63 | 1.5 | 26 | 1.53 | 16 | 16 | 0 | 0.3 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | gradient, photo | 45.42 | 45.21 | – | – | – | 1.0 | 244 | – | 46 | 35 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | curved shapes | 0.35 | 14.53 | 0.281 | 0.69 | 0.69 | 1.2 | 12 | 1.33 | 16 | 16 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | polygons | 0.34 | 13.44 | 0.251 | 0.64 | 1.67 | 1.7 | 14 | 1.28 | 74 | 6 | 86 | 0.1 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | thin lines | 0.99 | 13.74 | 0.266 | 0.77 | 18.26 | 5.0 | 30 | 1.50 | 67 | 12 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | askew shapes | 0.58 | 14.93 | 0.297 | 0.55 | 0.55 | 4.0 | 24 | 1.01 | 95 | 3 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | plain shapes | 0.76 | 15.17 | 0.291 | 0.69 | 0.69 | 6.0 | 39 | 1.29 | 54 | 46 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Silhouettes | flat colour art | 23.83 | 33.06 | 0.200 | 0.69 | 188.27 | 1.0 | 11 | 0.77 | 25 | 25 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | gradient, photo | 61.57 | 61.32 | – | – | – | 0.2 | 2 | – | 47 | 21 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | curved shapes | 0.04 | 1.83 | 0.031 | 0.22 | 0.22 | 1.2 | 16 | 1.70 | 20 | 20 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | polygons | 0.05 | 2.11 | 0.036 | 0.47 | 1.26 | 1.7 | 14 | 1.26 | 89 | 5 | 95 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | thin lines | 0.52 | 4.09 | 0.040 | 0.47 | 105.72 | 3.3 | 29 | 1.45 | 40 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | askew shapes | 0.12 | 3.13 | 0.044 | 0.48 | 9.60 | 4.0 | 24 | 1.01 | 98 | 3 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | plain shapes | 0.09 | 1.76 | 0.038 | 0.30 | 0.29 | 6.0 | 47 | 1.56 | 56 | 51 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Line Art | flat colour art | 19.90 | 17.83 | 0.162 | 0.42 | 179.43 | 1.0 | 18 | 1.30 | 21 | 21 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | gradient, photo | 38.98 | 38.54 | – | – | – | 7.7 | 472 | – | 58 | 44 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | curved shapes | 0.03 | 1.48 | 0.024 | 0.16 | 0.16 | 1.2 | 20 | 2.12 | 21 | 21 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | polygons | 0.06 | 2.36 | 0.040 | 0.47 | 1.26 | 1.7 | 14 | 1.23 | 94 | 8 | 95 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | thin lines | 0.50 | 5.46 | 0.109 | 0.50 | 55.93 | 15.3 | 62 | 3.12 | 86 | 14 | – | – | – | – | – | – | 3.3 | 0.0 | – | – | – | – | – |
| Technical Drawing | askew shapes | 0.88 | 20.92 | 0.611 | 4.21 | 4.19 | 4.3 | 26 | 1.08 | 98 | 98 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | plain shapes | 0.08 | 1.63 | 0.035 | 0.33 | 0.33 | 6.0 | 55 | 1.82 | 60 | 52 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Technical Drawing | flat colour art | 19.90 | 17.76 | 0.157 | 0.30 | 179.51 | 1.0 | 18 | 1.12 | 23 | 23 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | gradient, photo | 38.94 | 38.51 | – | – | – | 20.0 | 659 | – | 73 | 51 | – | – | – | – | – | – | 11.7 | 0.0 | – | – | – | – | – |
| Outline | curved shapes | 23.38 | 44.76 | 0.499 | 0.71 | 0.52 | 1.2 | 16 | 1.70 | 20 | 20 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Outline | polygons | 17.94 | 44.26 | 0.494 | 0.94 | 0.76 | 1.7 | 14 | 1.26 | 89 | 5 | 95 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Outline | thin lines | 0.52 | 3.89 | 0.038 | 2.94 | 105.73 | 3.3 | 7 | 0.35 | 100 | 7 | – | – | – | – | – | – | 0.0 | 0.0 | 0.056 | 0.02 | 2.1 | – | – |
| Outline | askew shapes | 23.68 | 36.71 | 0.425 | 1.50 | 6.62 | 4.0 | 20 | 0.83 | 100 | 3 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Outline | plain shapes | 34.32 | 43.23 | 0.495 | 0.73 | 0.51 | 6.0 | 47 | 1.56 | 56 | 51 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Outline | flat colour art | 26.69 | 32.18 | 0.494 | 0.92 | 178.93 | 1.0 | 18 | 1.30 | 21 | 21 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Outline | gradient, photo | 39.38 | 39.18 | – | – | – | 7.5 | 445 | – | 59 | 43 | – | – | – | – | – | – | 3.3 | 0.5 | – | – | – | – | – |
| Flat Logo | curved shapes | 0.06 | 2.14 | 0.036 | 0.25 | 0.25 | 1.8 | 31 | 3.38 | 19 | 6 | 100 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | polygons | 0.09 | 3.44 | 0.060 | 0.35 | 1.03 | 2.0 | 35 | 3.17 | 67 | 0 | 94 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | thin lines | 1.17 | 11.78 | 0.050 | 0.49 | all | 3.0 | 189 | 9.45 | 28 | 9 | – | – | 0.0 | 0 | 5467.1 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | askew shapes | 0.15 | 3.78 | 0.058 | 0.43 | 0.50 | 4.0 | 114 | 4.76 | 39 | 0 | – | – | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | plain shapes | 0.16 | 3.09 | 0.061 | 0.30 | 0.27 | 6.0 | 70 | 2.32 | 57 | 4 | – | – | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 | – | – | – | 0 | – |
| Flat Logo | flat colour art | 0.07 | 1.53 | 0.065 | 0.31 | 0.27 | 3.0 | 36 | 3.51 | 47 | 11 | 100 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | gradient, photo | refused | | | | | | | | | | | | | | | | | | | | | | |
| Default with Shapes | curved shapes | 0.08 | 3.19 | 0.058 | 0.38 | 0.38 | 1.8 | 13 | 1.44 | 14 | 14 | 100 | 0.0 | – | – | 12.2 | 7.4 | 0.0 | 0.0 | – | – | – | – | – |
| Default with Shapes | polygons | 0.06 | 2.34 | 0.040 | 0.47 | 1.26 | 2.0 | 14 | 1.26 | 88 | 5 | 95 | 0.0 | – | – | 0.0 | 2.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default with Shapes | thin lines | 0.54 | 4.53 | 0.049 | 0.52 | 105.72 | 3.3 | 25 | 1.27 | 40 | 0 | – | – | – | – | 0.0 | 18.3 | 0.0 | 0.0 | – | – | – | – | – |
| Default with Shapes | askew shapes | 0.17 | 3.85 | 0.050 | 0.37 | 36.06 | 3.7 | 23 | 0.94 | 100 | 3 | – | – | – | – | 0.0 | 42.1 | 0.0 | 0.0 | – | – | – | – | – |
| Default with Shapes | plain shapes | 0.13 | 2.62 | 0.055 | 0.35 | 0.34 | 6.0 | 38 | 1.28 | 55 | 48 | – | – | – | – | 16.4 | 8.2 | 0.0 | 0.0 | – | – | – | 72 | – |
| Default with Shapes | flat colour art | 19.91 | 17.91 | 0.176 | 0.51 | 179.34 | 1.5 | 16 | 1.01 | 14 | 14 | 0 | 0.0 | – | – | 14.0 | 7.5 | 0.0 | 0.0 | – | – | – | – | – |
| Default with Shapes | gradient, photo | 39.03 | 38.64 | – | – | – | 3.2 | 595 | – | 55 | 41 | – | – | – | – | 18.8 | 35.5 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Shapes | curved shapes | 0.05 | 2.24 | 0.041 | 0.27 | 0.28 | 1.8 | 15 | 1.59 | 16 | 16 | 100 | 0.0 | 0.0 | 0 | 9.7 | 5.1 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Shapes | polygons | 0.06 | 2.35 | 0.040 | 0.42 | 1.39 | 2.0 | 16 | 1.45 | 86 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | 6.1 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Shapes | thin lines | 0.49 | 4.78 | 0.108 | 0.70 | 45.31 | 4.0 | 30 | 1.50 | 63 | 19 | – | – | 1.7 | 413 | 0.0 | 10.8 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Shapes | askew shapes | 0.20 | 4.24 | 0.064 | 0.73 | 36.06 | 4.0 | 25 | 1.03 | 99 | 3 | – | – | 0.3 | 11 | 0.0 | 49.7 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Shapes | plain shapes | 0.10 | 2.00 | 0.042 | 0.30 | 0.26 | 6.0 | 40 | 1.33 | 59 | 49 | – | – | 0.0 | 0 | 8.5 | 6.4 | 0.0 | 0.0 | – | – | – | 78 | – |
| 6 Colors with Shapes | flat colour art | 0.13 | 2.71 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.5 | 9.1 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Shapes | gradient, photo | 5.00 | 5.28 | – | – | – | 13.0 | 829 | – | 37 | 28 | – | – | – | – | 7.1 | 8.2 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors with Auto Grouping | curved shapes | 0.08 | 3.46 | 0.043 | 0.27 | 0.28 | 1.8 | 16 | 1.77 | 16 | 16 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 17 |
| 6 Colors with Auto Grouping | polygons | 0.07 | 2.87 | 0.041 | 0.42 | 1.39 | 2.0 | 16 | 1.45 | 86 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 11 |
| 6 Colors with Auto Grouping | thin lines | 0.49 | 4.76 | 0.108 | 0.70 | 45.31 | 4.0 | 30 | 1.50 | 63 | 19 | – | – | 1.7 | 413 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0 |
| 6 Colors with Auto Grouping | askew shapes | 0.19 | 3.72 | 0.058 | 0.73 | 36.06 | 4.0 | 25 | 1.06 | 90 | 3 | – | – | 0.3 | 11 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0 |
| 6 Colors with Auto Grouping | plain shapes | 0.10 | 2.07 | 0.043 | 0.30 | 0.26 | 6.0 | 42 | 1.41 | 59 | 49 | – | – | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | 0 | 0 |
| 6 Colors with Auto Grouping | flat colour art | 0.13 | 2.72 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 30 |
| 6 Colors with Auto Grouping | gradient, photo | 5.00 | 5.28 | – | – | – | 13.0 | 829 | – | 37 | 28 | – | – | – | – | 6.9 | 8.0 | 0.0 | 0.0 | – | – | – | – | 15 |
| Shades of Gray with Auto Grouping | curved shapes | 0.07 | 2.70 | 0.047 | 0.31 | 0.31 | 1.2 | 13 | 1.45 | 18 | 18 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 0 |
| Shades of Gray with Auto Grouping | polygons | 0.06 | 2.28 | 0.039 | 0.41 | 1.26 | 1.7 | 13 | 1.18 | 89 | 5 | 95 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 0 |
| Shades of Gray with Auto Grouping | thin lines | 0.56 | 7.08 | 0.088 | 0.68 | 45.31 | 10.3 | 73 | 3.65 | 55 | 11 | – | – | 8.0 | 4319 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 0 |
| Shades of Gray with Auto Grouping | askew shapes | 0.25 | 4.95 | 0.049 | 0.73 | 21.52 | 6.0 | 39 | 1.61 | 95 | 3 | – | – | 2.3 | 2638 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 3 |
| Shades of Gray with Auto Grouping | plain shapes | 0.12 | 2.31 | 0.048 | 0.31 | 0.28 | 6.0 | 42 | 1.39 | 54 | 48 | – | – | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | 0 | 0 |
| Shades of Gray with Auto Grouping | flat colour art | 7.33 | 11.24 | 0.257 | 1.47 | 179.39 | 4.7 | 199 | 18.39 | 22 | 22 | 50 | 0.3 | 3.7 | 173367 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 52 |
| Shades of Gray with Auto Grouping | gradient, photo | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 53 |
| High Fidelity Photo with Auto Grouping | curved shapes | 0.04 | 1.63 | 0.026 | 0.15 | 0.15 | 1.2 | 19 | 2.11 | 21 | 21 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 0 |
| High Fidelity Photo with Auto Grouping | polygons | 0.06 | 2.33 | 0.039 | 0.50 | 1.32 | 1.7 | 17 | 1.47 | 88 | 7 | 93 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 0 |
| High Fidelity Photo with Auto Grouping | thin lines | 0.35 | 6.05 | 0.085 | 0.90 | 4.35 | 40.3 | 348 | 17.42 | 35 | 9 | – | – | 27.3 | 4505 | 0.0 | – | 19.0 | 0.0 | – | – | – | – | 23 |
| High Fidelity Photo with Auto Grouping | askew shapes | 0.17 | 3.97 | 0.100 | 0.94 | 1.73 | 8.7 | 157 | 6.54 | 71 | 5 | – | – | 4.3 | 3998 | 0.0 | – | 1.0 | 0.0 | – | – | – | – | 18 |
| High Fidelity Photo with Auto Grouping | plain shapes | 0.08 | 1.66 | 0.035 | 0.25 | 0.28 | 6.0 | 50 | 1.68 | 62 | 51 | – | – | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | 0 | 0 |
| High Fidelity Photo with Auto Grouping | flat colour art | 0.14 | 4.16 | 0.029 | 0.20 | 0.20 | 3.5 | 73 | 4.59 | 35 | 34 | 100 | 0.0 | 0.0 | 0 | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 25 |
| High Fidelity Photo with Auto Grouping | gradient, photo | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | 69 |
## 3. Each preset on its inputs, by variant (256 px)
| preset | variant | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | clean | 0.07 | 2.89 | 0.050 | 0.64 | 1.24 | 1.9 | 15 | 1.46 | 45 | 88 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | aliased | 0.37 | 20.23 | 0.233 | 1.02 | 1.35 | 1.9 | 15 | 1.45 | 21 | 96 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | blurred | 0.81 | 16.57 | 0.078 | 0.98 | 2.22 | 1.9 | 24 | 2.34 | 5 | 19 | 1.3 | – | – | 0.0 | 0.0 | 0.0 | 0.0 |
| Default | noisy | 1.74 | 4.53 | 0.054 | 0.54 | 1.32 | 1.9 | 15 | 1.47 | 41 | 84 | 0.0 | – | – | 1.9 | 0.3 | 0.0 | 0.0 |
| Default | scanned | 15.06 | 20.17 | 0.114 | 0.79 | 2.14 | 1.9 | 26 | 2.53 | 2 | 25 | 1.3 | – | – | 1.6 | 4.2 | 0.0 | 0.0 |
| High Fidelity Photo | clean | 1.80 | 2.04 | – | – | – | 134.7 | 11321 | – | 39 | – | – | – | – | 0.0 | – | 7.3 | 0.0 |
| High Fidelity Photo | aliased | 1.77 | 2.00 | – | – | – | 73.0 | 8004 | – | 40 | – | – | – | – | 0.0 | – | 5.5 | 0.0 |
| High Fidelity Photo | blurred | 1.59 | 1.95 | – | – | – | 72.0 | 7388 | – | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| High Fidelity Photo | noisy | 2.02 | 2.18 | – | – | – | 155.5 | 10856 | – | 33 | – | – | – | – | 0.0 | – | 18.5 | 0.0 |
| High Fidelity Photo | scanned | 1.45 | 1.55 | – | – | – | 140.5 | 9795 | – | 36 | – | – | – | – | 0.0 | – | 25.5 | 0.0 |
| Low Fidelity Photo | clean | 2.53 | 2.80 | – | – | – | 42.7 | 2282 | – | 41 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | aliased | 2.53 | 2.78 | – | – | – | 36.5 | 1498 | – | 44 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | blurred | 2.35 | 2.75 | – | – | – | 29.0 | 2005 | – | 42 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | noisy | 2.70 | 2.85 | – | – | – | 32.0 | 1926 | – | 41 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| Low Fidelity Photo | scanned | 1.95 | 2.04 | – | – | – | 34.5 | 2548 | – | 39 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| 3 Colors | clean | 0.06 | 2.00 | 0.070 | 0.31 | 0.30 | 2.0 | 11 | 1.78 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | aliased | 0.13 | 8.77 | 0.242 | 0.91 | 0.91 | 2.0 | 12 | 2.00 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | blurred | 0.34 | 7.83 | 0.073 | 0.24 | 0.24 | 2.0 | 9 | 1.50 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | noisy | 0.71 | 2.03 | 0.114 | 0.32 | 0.32 | 2.0 | 11 | 1.83 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 Colors | scanned | 1.57 | 3.22 | 0.089 | 0.30 | 0.30 | 2.0 | 12 | 2.00 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | clean | 0.12 | 2.70 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | aliased | 0.29 | 10.43 | 0.298 | 1.00 | 1.00 | 3.0 | 27 | 2.05 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 6 Colors | blurred | 0.62 | 9.33 | 0.204 | 1.34 | 1.19 | 6.0 | 98 | 5.46 | 39 | 50 | 16.0 | 0.0 | 0 | 0.6 | 0.0 | 0.0 | 0.0 |
| 6 Colors | noisy | 0.85 | 2.58 | 0.076 | 0.52 | 0.58 | 3.0 | 34 | 2.34 | 28 | 88 | 0.0 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 |
| 6 Colors | scanned | 1.74 | 4.42 | 0.228 | 1.20 | 0.94 | 7.0 | 108 | 6.10 | 28 | 62 | 20.5 | 0.0 | 0 | 0.8 | 1.6 | 0.0 | 0.0 |
| 16 Colors | clean | 0.12 | 2.52 | 0.052 | 0.31 | 0.30 | 3.0 | 28 | 2.11 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | aliased | 0.29 | 10.38 | 0.290 | 1.00 | 1.00 | 3.0 | 31 | 2.25 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | blurred | 0.63 | 9.38 | 0.215 | 1.31 | 1.19 | 7.0 | 132 | 7.21 | 42 | 50 | 21.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 16 Colors | noisy | 0.85 | 2.56 | 0.075 | 0.52 | 0.58 | 3.0 | 35 | 2.45 | 27 | 88 | 0.0 | 0.0 | 0 | 0.2 | 0.2 | 0.0 | 0.0 |
| 16 Colors | scanned | 1.72 | 4.43 | 0.256 | 1.09 | 0.96 | 7.5 | 151 | 8.31 | 33 | 62 | 24.0 | 0.0 | 0 | 1.2 | 1.2 | 0.0 | 0.0 |
| Shades of Gray | clean | 9.71 | 9.55 | – | – | – | 18.2 | 748 | – | 51 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| Shades of Gray | aliased | 9.71 | 9.52 | – | – | – | 11.0 | 642 | – | 57 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| Shades of Gray | blurred | 9.66 | 9.32 | – | – | – | 11.5 | 577 | – | 55 | – | – | – | – | 0.0 | – | 0.5 | 0.0 |
| Shades of Gray | noisy | 9.74 | 9.45 | – | – | – | 19.5 | 643 | – | 45 | – | – | – | – | 0.0 | – | 0.0 | 0.0 |
| Shades of Gray | scanned | 6.58 | 6.46 | – | – | – | 19.0 | 762 | – | 47 | – | – | – | – | 0.0 | – | 1.0 | 0.0 |
| Black and White Logo | clean | 0.04 | 1.62 | 0.026 | 0.51 | 1.24 | 1.4 | 19 | 1.89 | 50 | 88 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | aliased | 0.39 | 20.35 | 0.172 | 1.02 | 1.35 | 1.4 | 22 | 2.32 | 53 | 96 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | blurred | 0.80 | 16.24 | 0.049 | 0.84 | 2.22 | 1.4 | 40 | 3.92 | 21 | 19 | 1.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | noisy | 1.72 | 3.86 | 0.027 | 0.49 | 1.32 | 1.4 | 21 | 2.12 | 42 | 84 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Black and White Logo | scanned | 15.05 | 20.00 | 0.090 | 0.79 | 2.10 | 1.4 | 63 | 6.11 | 9 | 25 | 1.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | clean | 0.19 | 5.51 | 0.107 | 0.96 | 52.90 | 1.9 | 17 | 1.46 | 20 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | aliased | 0.47 | 19.58 | 0.213 | 1.00 | 105.54 | 1.6 | 11 | 1.10 | 26 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | blurred | 1.05 | 19.32 | 0.242 | 0.76 | 105.55 | 1.6 | 19 | 1.59 | 9 | 50 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | noisy | 1.76 | 6.13 | 0.116 | 0.90 | 1.06 | 2.0 | 18 | 1.48 | 12 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Sketched Art | scanned | 15.10 | 22.32 | 0.407 | 0.78 | 105.61 | 1.6 | 21 | 1.75 | 2 | 50 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | clean | 0.35 | 14.47 | 0.280 | 0.69 | 0.69 | 1.2 | 12 | 1.33 | 15 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | aliased | 0.39 | 21.69 | 0.289 | 1.00 | 1.00 | 1.2 | 9 | 0.99 | 12 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | blurred | 1.24 | 28.75 | 0.818 | 1.44 | 1.92 | 1.2 | 14 | 1.72 | 10 | 50 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | noisy | 2.11 | 11.05 | 0.284 | 0.68 | 0.69 | 1.2 | 11 | 1.28 | 9 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Silhouettes | scanned | 16.03 | 30.07 | 1.438 | 2.19 | 2.18 | 1.2 | 28 | 3.55 | 3 | 0 | 2.0 | – | – | – | – | 0.0 | 0.0 |
| Line Art | clean | 0.52 | 3.94 | 0.036 | 0.47 | 105.71 | 3.3 | 28 | 1.38 | 45 | – | – | – | – | – | – | 0.0 | 0.0 |
| Line Art | aliased | 0.67 | 14.44 | 0.112 | 0.73 | 55.44 | 14.0 | 59 | 2.95 | 87 | – | – | – | – | – | – | 0.0 | 0.0 |
| Line Art | blurred | 1.56 | 16.20 | 0.068 | 0.96 | 105.82 | 3.0 | 53 | 2.65 | 19 | – | – | – | – | – | – | 0.0 | 0.0 |
| Line Art | noisy | 1.02 | 3.98 | 0.030 | 0.50 | 56.08 | 8.0 | 56 | 2.80 | 42 | – | – | – | – | – | – | 1.0 | 0.0 |
| Line Art | scanned | 13.55 | 19.60 | 0.069 | 0.87 | 105.78 | 3.0 | 70 | 3.50 | 4 | – | – | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | clean | 0.17 | 2.79 | 0.052 | 0.50 | 55.74 | 5.1 | 30 | 1.93 | 89 | 90 | 0.0 | – | – | – | – | 1.9 | 0.0 |
| Technical Drawing | aliased | 0.43 | 18.51 | 0.204 | 1.02 | 55.44 | 6.8 | 33 | 2.06 | 95 | 94 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | blurred | 1.03 | 16.48 | 0.076 | 0.96 | 105.82 | 2.0 | 58 | 4.73 | 26 | 25 | 2.7 | – | – | – | – | 0.0 | 0.0 |
| Technical Drawing | noisy | 1.41 | 4.24 | 0.053 | 0.50 | 31.79 | 7.8 | 41 | 2.51 | 85 | 86 | 0.0 | – | – | – | – | 2.8 | 0.0 |
| Technical Drawing | scanned | 14.44 | 19.98 | 0.100 | 0.87 | 105.79 | 2.0 | 98 | 7.51 | 23 | 34 | 2.7 | – | – | – | – | 0.0 | 0.0 |
| Flat Logo | clean | 0.07 | 1.53 | 0.065 | 0.31 | 0.27 | 3.0 | 36 | 3.51 | 47 | 100 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 |
| Flat Logo | aliased | 0.13 | 5.87 | 0.218 | 0.65 | 0.65 | 3.0 | 31 | 1.96 | 44 | 50 | 0.0 | 0.0 | 0 | 5.8 | – | 0.0 | 0.0 |
| Flat Logo | blurred | 0.54 | 8.33 | 0.052 | 0.62 | 0.78 | 3.0 | 28 | 1.78 | 43 | 50 | 0.0 | 0.0 | 0 | 5.8 | – | 0.0 | 0.0 |
| Flat Logo | noisy | 0.81 | 1.77 | 0.076 | 0.39 | 0.32 | 3.5 | 44 | 2.63 | 31 | 100 | 1.0 | 0.0 | 0 | 6.9 | – | 0.5 | 0.0 |
| Flat Logo | scanned | 1.31 | 4.15 | 21.204 | 88.50 | 0.66 | 470.5 | 2251 | 255.53 | 11 | 62 | 1658.0 | 0.0 | 0 | 58.0 | – | 413.5 | 0.2 |
| preset | variant | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke | shapes % | grouped % |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| Default | clean | 0.07 | 2.96 | 0.052 | 0.47 | 1.26 | 1.9 | 14 | 1.43 | 46 | 10 | 96 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | aliased | 0.37 | 20.23 | 0.233 | 1.02 | 1.35 | 1.9 | 15 | 1.45 | 21 | 8 | 96 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | blurred | 0.81 | 16.55 | 0.077 | 0.98 | 2.18 | 1.9 | 24 | 2.39 | 7 | 5 | 23 | 1.3 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Default | noisy | 1.74 | 4.55 | 0.056 | 0.46 | 1.25 | 1.9 | 15 | 1.48 | 40 | 0 | 96 | 0.0 | – | – | 1.2 | 0.3 | 0.0 | 0.0 | – | – | – | – | – |
| Default | scanned | 15.06 | 20.16 | 0.118 | 0.74 | 2.14 | 1.9 | 25 | 2.49 | 3 | 0 | 30 | 1.3 | – | – | 1.0 | 3.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | clean | 0.13 | 2.72 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | aliased | 0.29 | 10.43 | 0.298 | 1.00 | 1.00 | 3.0 | 27 | 2.05 | 43 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | blurred | 0.61 | 9.30 | 0.211 | 1.33 | 1.21 | 6.0 | 118 | 6.51 | 41 | 11 | 50 | 21.0 | 0.0 | 0 | 0.2 | 0.4 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | noisy | 0.85 | 2.62 | 0.075 | 0.32 | 0.32 | 3.0 | 28 | 2.04 | 35 | 0 | 100 | 0.0 | 0.0 | 0 | 0.3 | 0.5 | 0.0 | 0.0 | – | – | – | – | – |
| Auto Color | scanned | 1.73 | 4.43 | 0.230 | 1.20 | 0.94 | 7.0 | 102 | 5.72 | 30 | 0 | 88 | 21.0 | 0.0 | 0 | 9.6 | 7.5 | 0.0 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | clean | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | aliased | 1.78 | 2.01 | – | – | – | 73.0 | 8021 | – | 40 | 38 | – | – | – | – | 0.0 | – | 5.5 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | blurred | 1.59 | 1.95 | – | – | – | 72.0 | 7450 | – | 40 | 39 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | noisy | 2.02 | 2.19 | – | – | – | 155.5 | 10670 | – | 33 | 31 | – | – | – | – | 0.0 | – | 18.5 | 0.0 | – | – | – | – | – |
| High Fidelity Photo | scanned | 1.45 | 1.55 | – | – | – | 140.5 | 9723 | – | 36 | 35 | – | – | – | – | 0.0 | – | 24.5 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | clean | 2.53 | 2.80 | – | – | – | 42.7 | 2307 | – | 43 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | aliased | 2.53 | 2.78 | – | – | – | 36.5 | 1505 | – | 46 | 38 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | blurred | 2.35 | 2.75 | – | – | – | 29.0 | 1990 | – | 44 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | noisy | 2.70 | 2.86 | – | – | – | 32.0 | 1972 | – | 43 | 38 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Low Fidelity Photo | scanned | 1.95 | 2.05 | – | – | – | 34.5 | 2550 | – | 42 | 33 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | clean | 0.06 | 2.00 | 0.070 | 0.31 | 0.30 | 2.0 | 11 | 1.78 | 39 | 0 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | aliased | 0.13 | 8.77 | 0.242 | 0.91 | 0.91 | 2.0 | 12 | 2.00 | 39 | 39 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | blurred | 0.34 | 7.83 | 0.073 | 0.24 | 0.24 | 2.0 | 9 | 1.50 | 39 | 0 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | noisy | 0.71 | 2.03 | 0.114 | 0.32 | 0.32 | 2.0 | 11 | 1.83 | 39 | 0 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 3 Colors | scanned | 1.57 | 3.22 | 0.089 | 0.30 | 0.30 | 2.0 | 12 | 2.00 | 39 | 0 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | clean | 0.13 | 2.72 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | aliased | 0.29 | 10.43 | 0.298 | 1.00 | 1.00 | 3.0 | 27 | 2.05 | 43 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | blurred | 0.62 | 9.31 | 0.208 | 1.34 | 1.19 | 6.0 | 100 | 5.61 | 41 | 11 | 50 | 21.5 | 0.0 | 0 | 0.8 | 0.4 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | noisy | 0.85 | 2.59 | 0.075 | 0.32 | 0.32 | 3.0 | 28 | 2.04 | 35 | 0 | 100 | 0.0 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 | – | – | – | – | – |
| 6 Colors | scanned | 1.74 | 4.42 | 0.229 | 1.20 | 0.94 | 7.0 | 102 | 5.80 | 30 | 0 | 88 | 21.0 | 0.0 | 0 | 0.8 | 1.6 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | clean | 0.12 | 2.53 | 0.051 | 0.31 | 0.30 | 3.0 | 28 | 2.11 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | aliased | 0.29 | 10.38 | 0.290 | 1.00 | 1.00 | 3.0 | 31 | 2.25 | 43 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | blurred | 0.62 | 9.36 | 0.216 | 1.31 | 1.19 | 7.0 | 139 | 7.53 | 44 | 13 | 50 | 25.5 | 0.0 | 0 | 0.2 | 0.5 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | noisy | 0.85 | 2.57 | 0.074 | 0.32 | 0.32 | 3.0 | 29 | 2.15 | 34 | 0 | 100 | 0.0 | 0.0 | 0 | 0.2 | 0.2 | 0.0 | 0.0 | – | – | – | – | – |
| 16 Colors | scanned | 1.72 | 4.44 | 0.257 | 1.09 | 0.96 | 7.5 | 158 | 8.81 | 32 | 0 | 88 | 28.5 | 0.0 | 0 | 3.4 | 3.6 | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | clean | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| Shades of Gray | aliased | 9.71 | 9.52 | – | – | – | 11.0 | 647 | – | 57 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| Shades of Gray | blurred | 9.66 | 9.32 | – | – | – | 11.5 | 585 | – | 55 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – |
| Shades of Gray | noisy | 9.74 | 9.44 | – | – | – | 19.5 | 648 | – | 46 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – |
| Shades of Gray | scanned | 6.58 | 6.46 | – | – | – | 19.0 | 764 | – | 47 | 37 | – | – | – | – | 0.0 | – | 1.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | clean | 0.04 | 1.86 | 0.031 | 0.47 | 1.26 | 1.4 | 17 | 1.74 | 52 | 15 | 96 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | aliased | 0.38 | 20.25 | 0.172 | 1.02 | 1.35 | 1.4 | 22 | 2.32 | 53 | 14 | 96 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | blurred | 0.80 | 16.26 | 0.050 | 0.83 | 2.17 | 1.4 | 39 | 3.84 | 22 | 12 | 23 | 1.1 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | noisy | 1.73 | 4.15 | 0.038 | 0.46 | 1.25 | 1.4 | 18 | 1.80 | 45 | 7 | 96 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Black and White Logo | scanned | 15.05 | 20.00 | 0.099 | 0.76 | 2.07 | 1.4 | 57 | 5.56 | 17 | 1 | 30 | 1.1 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | clean | 0.20 | 5.71 | 0.111 | 0.96 | 52.59 | 1.9 | 16 | 1.39 | 26 | 14 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | aliased | 0.47 | 19.58 | 0.213 | 1.00 | 105.54 | 1.6 | 11 | 1.10 | 26 | 9 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | blurred | 1.04 | 19.04 | 0.229 | 0.81 | 105.58 | 1.6 | 17 | 1.49 | 13 | 8 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | noisy | 1.76 | 6.07 | 0.113 | 0.97 | 1.06 | 2.0 | 17 | 1.44 | 17 | 0 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Sketched Art | scanned | 15.10 | 22.33 | 0.406 | 0.78 | 105.61 | 1.6 | 21 | 1.74 | 2 | 0 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | clean | 0.35 | 14.53 | 0.281 | 0.69 | 0.69 | 1.2 | 12 | 1.33 | 16 | 16 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | aliased | 0.39 | 21.69 | 0.289 | 1.00 | 1.00 | 1.2 | 9 | 0.99 | 12 | 9 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | blurred | 1.23 | 28.62 | 0.814 | 1.41 | 1.41 | 1.2 | 14 | 1.64 | 13 | 10 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | noisy | 2.12 | 11.10 | 0.285 | 0.68 | 0.69 | 1.2 | 12 | 1.30 | 9 | 0 | 100 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Silhouettes | scanned | 16.04 | 30.09 | 1.439 | 2.19 | 2.19 | 1.2 | 28 | 3.56 | 3 | 0 | 50 | 2.8 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | clean | 0.52 | 4.09 | 0.040 | 0.47 | 105.72 | 3.3 | 29 | 1.45 | 40 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | aliased | 0.67 | 14.44 | 0.112 | 0.73 | 55.44 | 14.0 | 59 | 2.95 | 87 | 1 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | blurred | 1.56 | 16.06 | 0.061 | 0.99 | 105.72 | 3.0 | 53 | 2.65 | 22 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Line Art | noisy | 1.03 | 4.10 | 0.035 | 0.44 | 56.07 | 8.0 | 50 | 2.50 | 43 | 0 | – | – | – | – | – | – | 1.0 | 0.0 | – | – | – | – | – |
| Line Art | scanned | 13.54 | 19.55 | 0.062 | 0.92 | 105.68 | 3.0 | 51 | 2.55 | 8 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | clean | 0.31 | 6.70 | 0.168 | 4.21 | 55.93 | 4.9 | 26 | 1.58 | 93 | 27 | 95 | 0.0 | – | – | – | – | 0.7 | 0.0 | – | – | – | – | – |
| Technical Drawing | aliased | 0.56 | 20.39 | 0.275 | 2.42 | 55.44 | 6.2 | 31 | 1.85 | 96 | 28 | 94 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | blurred | 1.12 | 17.47 | 0.168 | 2.41 | 105.64 | 2.4 | 49 | 3.84 | 47 | 19 | 30 | 2.7 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Technical Drawing | noisy | 1.68 | 6.90 | 0.160 | 2.13 | 31.79 | 7.0 | 34 | 1.98 | 93 | 26 | 95 | 0.0 | – | – | – | – | 1.4 | 0.0 | – | – | – | – | – |
| Technical Drawing | scanned | 14.69 | 20.15 | 0.136 | 1.17 | 105.57 | 2.4 | 73 | 5.42 | 49 | 12 | 39 | 3.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| Outline | clean | 0.52 | 3.89 | 0.038 | 2.94 | 105.73 | 3.3 | 7 | 0.35 | 100 | 7 | – | – | – | – | – | – | 0.0 | 0.0 | 0.056 | 0.02 | 2.1 | – | – |
| Outline | aliased | 0.62 | 13.33 | 0.169 | 0.88 | 55.55 | 11.0 | 51 | 2.55 | 64 | 9 | – | – | – | – | – | – | 0.0 | 0.0 | 0.154 | 0.01 | 4.6 | – | – |
| Outline | blurred | 1.57 | 16.26 | 0.032 | 0.86 | 105.75 | 3.0 | 6 | 0.30 | 100 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | 0.013 | 0.03 | 2.0 | – | – |
| Outline | noisy | 0.95 | 3.50 | 0.029 | 0.72 | 55.94 | 8.0 | 16 | 0.80 | 100 | 2 | – | – | – | – | – | – | 0.0 | 0.0 | 0.010 | 0.02 | 2.0 | – | – |
| Outline | scanned | 13.53 | 19.37 | 0.044 | 0.88 | 105.64 | 3.0 | 6 | 0.30 | 100 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | 0.027 | 0.03 | 2.0 | – | – |
| Flat Logo | clean | 0.07 | 1.53 | 0.065 | 0.31 | 0.27 | 3.0 | 36 | 3.51 | 47 | 11 | 100 | 0.0 | 0.0 | 0 | 6.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | aliased | 0.13 | 5.87 | 0.218 | 0.65 | 0.65 | 3.0 | 31 | 1.96 | 44 | 19 | 50 | 0.0 | 0.0 | 0 | 5.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | blurred | 0.54 | 8.33 | 0.052 | 0.62 | 0.78 | 3.0 | 28 | 1.78 | 43 | 16 | 50 | 0.0 | 0.0 | 0 | 5.8 | – | 0.0 | 0.0 | – | – | – | – | – |
| Flat Logo | noisy | 0.81 | 1.77 | 0.076 | 0.39 | 0.32 | 3.5 | 44 | 2.63 | 31 | 0 | 100 | 1.0 | 0.0 | 0 | 6.9 | – | 0.5 | 0.0 | – | – | – | – | – |
| Flat Logo | scanned | 1.31 | 4.15 | 21.204 | 88.50 | 0.66 | 470.5 | 2251 | 255.53 | 11 | 0 | 62 | 1658.0 | 0.0 | 0 | 58.0 | – | 413.5 | 0.2 | – | – | – | – | – |
| Default with Shapes | clean | 0.13 | 2.62 | 0.055 | 0.35 | 0.34 | 6.0 | 38 | 1.28 | 55 | 48 | – | – | – | – | 16.4 | 8.2 | 0.0 | 0.0 | – | – | – | 72 | – |
| Default with Shapes | aliased | 0.63 | 16.01 | 0.264 | 1.00 | 0.90 | 6.0 | 31 | 1.03 | 45 | 31 | – | – | – | – | 19.2 | 17.7 | 0.0 | 0.0 | – | – | – | 33 | – |
| Default with Shapes | blurred | 1.58 | 15.95 | 0.096 | 0.81 | 1.00 | 6.0 | 51 | 1.70 | 17 | 17 | – | – | – | – | 8.0 | 7.6 | 0.0 | 0.0 | – | – | – | 33 | – |
| Default with Shapes | noisy | 2.63 | 4.94 | 0.063 | 0.38 | 0.38 | 6.0 | 34 | 1.13 | 53 | 0 | – | – | – | – | 25.3 | 22.4 | 0.0 | 0.0 | – | – | – | 83 | – |
| Default with Shapes | scanned | 17.04 | 21.98 | 0.125 | 0.52 | 0.60 | 6.0 | 46 | 1.53 | 8 | 0 | – | – | – | – | 8.8 | 8.2 | 0.0 | 0.0 | – | – | – | 33 | – |
| 6 Colors with Shapes | clean | 0.10 | 2.00 | 0.042 | 0.30 | 0.26 | 6.0 | 40 | 1.33 | 59 | 49 | – | – | 0.0 | 0 | 8.5 | 6.4 | 0.0 | 0.0 | – | – | – | 78 | – |
| 6 Colors with Shapes | aliased | 0.62 | 15.75 | 0.234 | 0.99 | 0.99 | 6.0 | 37 | 1.23 | 46 | 32 | – | – | 0.0 | 0 | 7.5 | 10.1 | 0.0 | 0.0 | – | – | – | 17 | – |
| 6 Colors with Shapes | blurred | 1.55 | 15.53 | 0.114 | 0.57 | 0.61 | 6.0 | 46 | 1.53 | 49 | 49 | – | – | 0.0 | 0 | 12.8 | 23.1 | 0.0 | 0.0 | – | – | – | 67 | – |
| 6 Colors with Shapes | noisy | 1.78 | 3.44 | 0.047 | 0.28 | 0.29 | 6.0 | 36 | 1.20 | 54 | 0 | – | – | 0.0 | 0 | 6.7 | 15.9 | 0.0 | 0.0 | – | – | – | 83 | – |
| 6 Colors with Shapes | scanned | 2.13 | 7.02 | 0.101 | 0.53 | 0.60 | 6.0 | 62 | 2.07 | 17 | 0 | – | – | 0.0 | 0 | 19.3 | 8.2 | 0.0 | 0.0 | – | – | – | 33 | – |
| 6 Colors with Auto Grouping | clean | 0.13 | 2.72 | 0.057 | 0.31 | 0.30 | 3.0 | 28 | 2.00 | 43 | 19 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 30 |
| 6 Colors with Auto Grouping | aliased | 0.29 | 10.43 | 0.298 | 1.00 | 1.00 | 3.0 | 27 | 2.05 | 43 | 43 | 100 | 0.0 | 0.0 | 0 | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 30 |
| 6 Colors with Auto Grouping | blurred | 0.62 | 9.31 | 0.208 | 1.34 | 1.19 | 6.0 | 100 | 5.61 | 41 | 11 | 50 | 21.5 | 0.0 | 0 | 0.8 | 0.4 | 0.0 | 0.0 | – | – | – | – | 23 |
| 6 Colors with Auto Grouping | noisy | 0.85 | 2.59 | 0.075 | 0.32 | 0.32 | 3.0 | 28 | 2.04 | 35 | 0 | 100 | 0.0 | 0.0 | 0 | 0.3 | 0.3 | 0.0 | 0.0 | – | – | – | – | 30 |
| 6 Colors with Auto Grouping | scanned | 1.74 | 4.42 | 0.229 | 1.20 | 0.94 | 7.0 | 102 | 5.80 | 30 | 0 | 88 | 21.0 | 0.0 | 0 | 0.8 | 1.6 | 0.0 | 0.0 | – | – | – | – | 15 |
| Shades of Gray with Auto Grouping | clean | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 53 |
| Shades of Gray with Auto Grouping | aliased | 9.71 | 9.52 | – | – | – | 11.0 | 647 | – | 57 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 54 |
| Shades of Gray with Auto Grouping | blurred | 9.66 | 9.32 | – | – | – | 11.5 | 585 | – | 55 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 47 |
| Shades of Gray with Auto Grouping | noisy | 9.74 | 9.44 | – | – | – | 19.5 | 648 | – | 46 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 28 |
| Shades of Gray with Auto Grouping | scanned | 6.58 | 6.46 | – | – | – | 19.0 | 764 | – | 47 | 37 | – | – | – | – | 0.0 | – | 1.0 | 0.0 | – | – | – | – | 37 |
| High Fidelity Photo with Auto Grouping | clean | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | 69 |
| High Fidelity Photo with Auto Grouping | aliased | 1.78 | 2.01 | – | – | – | 73.0 | 8021 | – | 40 | 38 | – | – | – | – | 0.0 | – | 5.5 | 0.0 | – | – | – | – | 65 |
| High Fidelity Photo with Auto Grouping | blurred | 1.59 | 1.95 | – | – | – | 72.0 | 7450 | – | 40 | 39 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 65 |
| High Fidelity Photo with Auto Grouping | noisy | 2.02 | 2.19 | – | – | – | 155.5 | 10670 | – | 33 | 31 | – | – | – | – | 0.0 | – | 18.5 | 0.0 | – | – | – | – | 72 |
| High Fidelity Photo with Auto Grouping | scanned | 1.45 | 1.55 | – | – | – | 140.5 | 9723 | – | 36 | 35 | – | – | – | – | 0.0 | – | 24.5 | 0.0 | – | – | – | – | 75 |
## 4. Yardstick
@@ -258,23 +388,33 @@ Inkscape 1.4.2's Trace Bitmap, run as a black box through its `object-trace` act
removed, its other settings as they come) on the same one-colour drawings, and its SVG output measured by
the same code. Compare with the black-and-white presets on the same kinds of input in section 2 and 3.
| inputs | variant | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| curved shapes | clean | 0.25 | 11.05 | 0.198 | 0.97 | 0.97 | 1.0 | 15 | 1.98 | 11 | 83 | 1.8 | – | – | – | – | 0.0 | 0.0 |
| curved shapes | aliased | 0.38 | 20.79 | 0.192 | 0.88 | 0.88 | 1.0 | 16 | 1.88 | 14 | 50 | 1.8 | – | – | – | – | 0.0 | 0.0 |
| curved shapes | blurred | 0.79 | 16.57 | 0.182 | 0.86 | 1.14 | 1.0 | 15 | 1.84 | 6 | 0 | 1.8 | – | – | – | – | 0.0 | 0.0 |
| curved shapes | noisy | 2.01 | 8.14 | 0.191 | 0.93 | 0.93 | 1.0 | 17 | 2.06 | 10 | 75 | 2.8 | – | – | – | – | 0.0 | 0.0 |
| curved shapes | scanned | 15.31 | 20.22 | 0.191 | 0.97 | 1.14 | 1.0 | 16 | 1.98 | 7 | 0 | 2.0 | – | – | – | – | 0.0 | 0.0 |
| polygons | clean | 0.16 | 6.45 | 0.124 | 1.26 | 2.36 | 1.0 | 21 | 1.93 | 27 | 34 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| polygons | aliased | 0.35 | 18.33 | 0.150 | 1.22 | 2.35 | 1.0 | 20 | 1.81 | 20 | 28 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| polygons | blurred | 0.90 | 17.54 | 0.190 | 1.20 | 2.56 | 1.0 | 19 | 1.75 | 0 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| polygons | noisy | 1.62 | 6.05 | 0.119 | 1.22 | 2.32 | 1.0 | 22 | 2.01 | 29 | 19 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| polygons | scanned | 14.75 | 20.30 | 0.170 | 1.49 | 2.55 | 1.0 | 21 | 1.90 | 4 | 3 | 0.0 | – | – | – | – | 0.0 | 0.0 |
| thin lines | clean | 0.44 | 6.05 | 0.109 | 0.98 | 76.81 | 1.0 | 34 | 1.72 | 2 | – | – | – | – | – | – | 0.0 | 0.0 |
| thin lines | aliased | 0.74 | 16.37 | 0.099 | 1.17 | 55.97 | 1.0 | 22 | 1.10 | 6 | – | – | – | – | – | – | 0.0 | 0.0 |
| thin lines | blurred | 1.72 | 18.80 | 0.254 | 0.81 | 106.00 | 1.0 | 18 | 0.90 | 0 | – | – | – | – | – | – | 0.0 | 0.0 |
| thin lines | noisy | 1.02 | 6.25 | 0.112 | 1.00 | 37.89 | 1.0 | 25 | 1.25 | 0 | – | – | – | – | – | – | 0.0 | 0.0 |
| thin lines | scanned | 28.94 | 46.83 | 12.147 | 43.11 | 20.79 | 1.0 | 2763 | 138.15 | 2 | – | – | – | – | – | – | 0.0 | 0.1 |
| inputs | variant | ΔE | edge ΔE | off mean | off max | missed max | paths | anchors | × art | lines % | upright % | corners % | kinks | odd colours | odd px | gap px | overlap px | specks | outside px | centre off | width off | anchors a stroke | shapes % | grouped % |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| curved shapes | clean | 0.25 | 11.05 | 0.198 | 0.97 | 0.97 | 1.0 | 15 | 1.98 | 11 | 7 | 83 | 1.8 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| curved shapes | aliased | 0.38 | 20.79 | 0.192 | 0.88 | 0.88 | 1.0 | 16 | 1.88 | 14 | 8 | 50 | 1.8 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| curved shapes | blurred | 0.79 | 16.57 | 0.182 | 0.86 | 1.14 | 1.0 | 15 | 1.84 | 6 | 5 | 0 | 1.8 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| curved shapes | noisy | 2.01 | 8.14 | 0.191 | 0.93 | 0.93 | 1.0 | 17 | 2.06 | 10 | 0 | 75 | 2.8 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| curved shapes | scanned | 15.31 | 20.22 | 0.191 | 0.97 | 1.14 | 1.0 | 16 | 1.98 | 7 | 2 | 0 | 2.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| polygons | clean | 0.16 | 6.45 | 0.124 | 1.26 | 2.36 | 1.0 | 21 | 1.93 | 27 | 4 | 34 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| polygons | aliased | 0.35 | 18.33 | 0.150 | 1.22 | 2.35 | 1.0 | 20 | 1.81 | 20 | 3 | 28 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| polygons | blurred | 0.90 | 17.54 | 0.190 | 1.20 | 2.56 | 1.0 | 19 | 1.75 | 0 | 0 | 0 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| polygons | noisy | 1.62 | 6.05 | 0.119 | 1.22 | 2.32 | 1.0 | 22 | 2.01 | 29 | 4 | 19 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| polygons | scanned | 14.75 | 20.30 | 0.170 | 1.49 | 2.55 | 1.0 | 21 | 1.90 | 4 | 0 | 3 | 0.0 | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| thin lines | clean | 0.44 | 6.05 | 0.109 | 0.98 | 76.81 | 1.0 | 34 | 1.72 | 2 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| thin lines | aliased | 0.74 | 16.37 | 0.099 | 1.17 | 55.97 | 1.0 | 22 | 1.10 | 6 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| thin lines | blurred | 1.72 | 18.80 | 0.254 | 0.81 | 106.00 | 1.0 | 18 | 0.90 | 0 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| thin lines | noisy | 1.02 | 6.25 | 0.112 | 1.00 | 37.89 | 1.0 | 25 | 1.25 | 0 | 0 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| thin lines | scanned | 28.94 | 46.83 | 12.147 | 43.11 | 20.79 | 1.0 | 2763 | 138.15 | 2 | 0 | – | – | – | – | – | – | 0.0 | 0.1 | – | – | – | – | – |
| askew shapes | clean | 0.31 | 7.76 | 0.142 | 1.08 | 3.38 | 1.0 | 45 | 1.89 | 32 | 15 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| askew shapes | aliased | 0.45 | 14.06 | 0.149 | 1.14 | 1.35 | 1.0 | 47 | 1.96 | 30 | 4 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| askew shapes | blurred | 1.19 | 17.05 | 0.195 | 1.20 | 3.65 | 1.0 | 44 | 1.83 | 29 | 11 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| askew shapes | noisy | 2.03 | 6.74 | 0.116 | 0.85 | 1.04 | 1.0 | 62 | 2.58 | 37 | 7 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| askew shapes | scanned | 15.69 | 20.78 | 0.217 | 1.47 | 3.65 | 1.0 | 44 | 1.83 | 26 | 8 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – |
| plain shapes | clean | 0.56 | 11.27 | 0.218 | 1.00 | 1.02 | 1.0 | 46 | 1.54 | 46 | 36 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| plain shapes | aliased | 0.57 | 14.69 | 0.239 | 1.00 | 1.00 | 1.0 | 49 | 1.63 | 47 | 25 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| plain shapes | blurred | 1.68 | 17.28 | 0.262 | 1.14 | 1.14 | 1.0 | 45 | 1.50 | 31 | 8 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| plain shapes | noisy | 2.94 | 9.59 | 0.234 | 0.98 | 0.98 | 1.0 | 53 | 1.77 | 50 | 25 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
| plain shapes | scanned | 17.09 | 22.56 | 0.281 | 0.99 | 1.10 | 1.0 | 51 | 1.70 | 31 | 8 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | 0 | – |
## 5. Cost
@@ -282,89 +422,136 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab
| preset | image | megapixels | result |
|:--|:--|--:|:--|
| Default | photo-like | 1 | 0.04 s, 29 paths, 6701 anchors, 12 MB |
| Default | photo-like | 1 | 0.04 s, 29 paths, 6278 anchors, 12 MB |
| Default | four-colour badge | 1 | 0.02 s, 3 paths, 64 anchors, 12 MB |
| High Fidelity Photo | photo-like | 1 | 0.21 s, 968 paths, 114383 anchors, 170 MB |
| High Fidelity Photo | four-colour badge | 1 | 0.05 s, 6 paths, 172 anchors, 23 MB |
| Low Fidelity Photo | photo-like | 1 | 0.11 s, 129 paths, 22929 anchors, 104 MB |
| Low Fidelity Photo | four-colour badge | 1 | 0.05 s, 6 paths, 136 anchors, 23 MB |
| 3 Colors | photo-like | 1 | 0.08 s, 9 paths, 3367 anchors, 18 MB |
| 3 Colors | four-colour badge | 1 | 0.05 s, 4 paths, 88 anchors, 13 MB |
| 6 Colors | photo-like | 1 | 0.08 s, 78 paths, 9008 anchors, 24 MB |
| 6 Colors | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| 16 Colors | photo-like | 1 | 0.10 s, 178 paths, 20466 anchors, 31 MB |
| Auto Color | photo-like | 1 | 0.17 s, 732 paths, 45265 anchors, 24 MB |
| Auto Color | four-colour badge | 1 | 0.06 s, 5 paths, 84 anchors, 12 MB |
| High Fidelity Photo | photo-like | 1 | 0.28 s, 968 paths, 117041 anchors, 147 MB |
| High Fidelity Photo | four-colour badge | 1 | 0.06 s, 6 paths, 172 anchors, 23 MB |
| Low Fidelity Photo | photo-like | 1 | 0.14 s, 129 paths, 23014 anchors, 101 MB |
| Low Fidelity Photo | four-colour badge | 1 | 0.06 s, 6 paths, 136 anchors, 23 MB |
| 3 Colors | photo-like | 1 | 0.10 s, 9 paths, 3341 anchors, 18 MB |
| 3 Colors | four-colour badge | 1 | 0.07 s, 4 paths, 88 anchors, 13 MB |
| 6 Colors | photo-like | 1 | 0.10 s, 78 paths, 9076 anchors, 23 MB |
| 6 Colors | four-colour badge | 1 | 0.06 s, 5 paths, 84 anchors, 12 MB |
| 16 Colors | photo-like | 1 | 0.12 s, 178 paths, 20470 anchors, 31 MB |
| 16 Colors | four-colour badge | 1 | 0.05 s, 5 paths, 88 anchors, 12 MB |
| Shades of Gray | photo-like | 1 | 0.08 s, 81 paths, 6698 anchors, 41 MB |
| Shades of Gray | four-colour badge | 1 | 0.06 s, 10 paths, 733 anchors, 34 MB |
| Black and White Logo | photo-like | 1 | 0.04 s, 71 paths, 5047 anchors, 8 MB |
| Shades of Gray | photo-like | 1 | 0.09 s, 81 paths, 6698 anchors, 43 MB |
| Shades of Gray | four-colour badge | 1 | 0.06 s, 10 paths, 725 anchors, 35 MB |
| Black and White Logo | photo-like | 1 | 0.04 s, 71 paths, 4933 anchors, 8 MB |
| Black and White Logo | four-colour badge | 1 | 0.02 s, 1 paths, 47 anchors, 6 MB |
| Sketched Art | photo-like | 1 | 0.04 s, 5 paths, 2880 anchors, 8 MB |
| Sketched Art | photo-like | 1 | 0.04 s, 5 paths, 2873 anchors, 8 MB |
| Sketched Art | four-colour badge | 1 | 0.02 s, 2 paths, 114 anchors, 6 MB |
| Silhouettes | photo-like | 1 | 0.03 s, 2 paths, 573 anchors, 9 MB |
| Silhouettes | photo-like | 1 | 0.04 s, 2 paths, 556 anchors, 9 MB |
| Silhouettes | four-colour badge | 1 | 0.02 s, 1 paths, 24 anchors, 6 MB |
| Line Art | photo-like | 1 | 0.04 s, 54 paths, 4579 anchors, 8 MB |
| Line Art | photo-like | 1 | 0.04 s, 54 paths, 4495 anchors, 8 MB |
| Line Art | four-colour badge | 1 | 0.02 s, 1 paths, 38 anchors, 6 MB |
| Technical Drawing | photo-like | 1 | 0.04 s, 221 paths, 6443 anchors, 8 MB |
| Technical Drawing | photo-like | 1 | 0.04 s, 221 paths, 6258 anchors, 8 MB |
| Technical Drawing | four-colour badge | 1 | 0.02 s, 1 paths, 47 anchors, 6 MB |
| Flat Logo | photo-like | 1 | refused after 0.09 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 1 | 0.65 s, 5 paths, 78 anchors, 146 MB |
| Default | photo-like | 4 | 0.18 s, 48 paths, 15720 anchors, 49 MB |
| Default | four-colour badge | 4 | 0.09 s, 3 paths, 72 anchors, 48 MB |
| High Fidelity Photo | photo-like | 4 | 0.69 s, 4475 paths, 341272 anchors, 634 MB |
| High Fidelity Photo | four-colour badge | 4 | 0.23 s, 6 paths, 200 anchors, 93 MB |
| Low Fidelity Photo | photo-like | 4 | 0.35 s, 304 paths, 61093 anchors, 401 MB |
| Low Fidelity Photo | four-colour badge | 4 | 0.22 s, 6 paths, 152 anchors, 94 MB |
| 3 Colors | photo-like | 4 | 0.27 s, 20 paths, 8453 anchors, 73 MB |
| 3 Colors | four-colour badge | 4 | 0.18 s, 4 paths, 122 anchors, 53 MB |
| 6 Colors | photo-like | 4 | 0.26 s, 115 paths, 23128 anchors, 83 MB |
| 6 Colors | four-colour badge | 4 | 0.18 s, 5 paths, 92 anchors, 46 MB |
| 16 Colors | photo-like | 4 | 0.29 s, 353 paths, 52749 anchors, 109 MB |
| 16 Colors | four-colour badge | 4 | 0.18 s, 5 paths, 96 anchors, 46 MB |
| Shades of Gray | photo-like | 4 | 0.28 s, 138 paths, 18794 anchors, 158 MB |
| Shades of Gray | four-colour badge | 4 | 0.26 s, 10 paths, 1265 anchors, 137 MB |
| Black and White Logo | photo-like | 4 | 0.20 s, 208 paths, 13717 anchors, 32 MB |
| Outline | photo-like | 1 | 0.06 s, 45 paths, 4110 anchors, 16 MB |
| Outline | four-colour badge | 1 | 0.03 s, 1 paths, 38 anchors, 11 MB |
| Flat Logo | photo-like | 1 | refused after 0.10 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 1 | 0.66 s, 5 paths, 78 anchors, 146 MB |
| Default with Shapes | photo-like | 1 | 0.05 s, 29 paths, 6278 anchors, 12 MB |
| Default with Shapes | four-colour badge | 1 | 0.02 s, 3 paths, 63 anchors, 12 MB |
| 6 Colors with Shapes | photo-like | 1 | 0.10 s, 78 paths, 9076 anchors, 23 MB |
| 6 Colors with Shapes | four-colour badge | 1 | 0.06 s, 5 paths, 84 anchors, 12 MB |
| 6 Colors with Auto Grouping | photo-like | 1 | 0.09 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors with Auto Grouping | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| Shades of Gray with Auto Grouping | photo-like | 1 | 0.09 s, 81 paths, 6698 anchors, 43 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 1 | 0.06 s, 10 paths, 725 anchors, 34 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 1 | 0.24 s, 968 paths, 117041 anchors, 169 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 1 | 0.05 s, 6 paths, 172 anchors, 23 MB |
| Default | photo-like | 4 | 0.19 s, 48 paths, 14438 anchors, 49 MB |
| Default | four-colour badge | 4 | 0.10 s, 3 paths, 72 anchors, 48 MB |
| Auto Color | photo-like | 4 | 0.47 s, 1469 paths, 149389 anchors, 111 MB |
| Auto Color | four-colour badge | 4 | 0.20 s, 5 paths, 92 anchors, 46 MB |
| High Fidelity Photo | photo-like | 4 | 0.71 s, 4479 paths, 337884 anchors, 628 MB |
| High Fidelity Photo | four-colour badge | 4 | 0.23 s, 6 paths, 200 anchors, 94 MB |
| Low Fidelity Photo | photo-like | 4 | 0.36 s, 299 paths, 61601 anchors, 414 MB |
| Low Fidelity Photo | four-colour badge | 4 | 0.23 s, 6 paths, 152 anchors, 97 MB |
| 3 Colors | photo-like | 4 | 0.27 s, 20 paths, 8473 anchors, 73 MB |
| 3 Colors | four-colour badge | 4 | 0.19 s, 4 paths, 122 anchors, 51 MB |
| 6 Colors | photo-like | 4 | 0.29 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| 16 Colors | photo-like | 4 | 0.32 s, 353 paths, 52770 anchors, 128 MB |
| 16 Colors | four-colour badge | 4 | 0.19 s, 5 paths, 96 anchors, 46 MB |
| Shades of Gray | photo-like | 4 | 0.30 s, 137 paths, 18823 anchors, 159 MB |
| Shades of Gray | four-colour badge | 4 | 0.27 s, 10 paths, 1256 anchors, 136 MB |
| Black and White Logo | photo-like | 4 | 0.20 s, 208 paths, 13546 anchors, 32 MB |
| Black and White Logo | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 MB |
| Sketched Art | photo-like | 4 | 0.17 s, 9 paths, 5358 anchors, 32 MB |
| Sketched Art | four-colour badge | 4 | 0.08 s, 2 paths, 196 anchors, 24 MB |
| Silhouettes | photo-like | 4 | 0.19 s, 2 paths, 1457 anchors, 36 MB |
| Sketched Art | photo-like | 4 | 0.20 s, 9 paths, 5387 anchors, 32 MB |
| Sketched Art | four-colour badge | 4 | 0.09 s, 2 paths, 196 anchors, 24 MB |
| Silhouettes | photo-like | 4 | 0.22 s, 2 paths, 1465 anchors, 36 MB |
| Silhouettes | four-colour badge | 4 | 0.08 s, 1 paths, 28 anchors, 24 MB |
| Line Art | photo-like | 4 | 0.18 s, 127 paths, 11461 anchors, 32 MB |
| Line Art | photo-like | 4 | 0.20 s, 127 paths, 11330 anchors, 32 MB |
| Line Art | four-colour badge | 4 | 0.08 s, 1 paths, 42 anchors, 24 MB |
| Technical Drawing | photo-like | 4 | 0.18 s, 931 paths, 21061 anchors, 32 MB |
| Technical Drawing | photo-like | 4 | 0.20 s, 931 paths, 20628 anchors, 32 MB |
| Technical Drawing | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 MB |
| Outline | photo-like | 4 | 0.25 s, 113 paths, 10815 anchors, 64 MB |
| Outline | four-colour badge | 4 | 0.13 s, 1 paths, 42 anchors, 44 MB |
| Flat Logo | photo-like | 4 | refused after 0.10 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 4 | 0.68 s, 5 paths, 69 anchors, 146 MB |
| Default | photo-like | 16 | 1.08 s, 127 paths, 34766 anchors, 199 MB |
| Default | four-colour badge | 16 | 0.47 s, 3 paths, 88 anchors, 184 MB |
| High Fidelity Photo | photo-like | 16 | 3.68 s, 31125 paths, 1175671 anchors, 880 MB |
| High Fidelity Photo | four-colour badge | 16 | 1.05 s, 6 paths, 274 anchors, 257 MB |
| Low Fidelity Photo | photo-like | 16 | 1.78 s, 2036 paths, 181832 anchors, 754 MB |
| Low Fidelity Photo | four-colour badge | 16 | 1.07 s, 6 paths, 187 anchors, 257 MB |
| 3 Colors | photo-like | 16 | 1.17 s, 76 paths, 22878 anchors, 290 MB |
| 3 Colors | four-colour badge | 16 | 0.82 s, 4 paths, 190 anchors, 205 MB |
| 6 Colors | photo-like | 16 | 1.09 s, 470 paths, 60539 anchors, 298 MB |
| 6 Colors | four-colour badge | 16 | 0.81 s, 5 paths, 126 anchors, 184 MB |
| 16 Colors | photo-like | 16 | 1.09 s, 1973 paths, 145173 anchors, 303 MB |
| 16 Colors | four-colour badge | 16 | 0.83 s, 5 paths, 128 anchors, 184 MB |
| Shades of Gray | photo-like | 16 | 1.61 s, 686 paths, 53872 anchors, 407 MB |
| Shades of Gray | four-colour badge | 16 | 1.35 s, 9 paths, 2216 anchors, 270 MB |
| Black and White Logo | photo-like | 16 | 1.01 s, 1128 paths, 45117 anchors, 129 MB |
| Black and White Logo | four-colour badge | 16 | 0.36 s, 1 paths, 69 anchors, 88 MB |
| Sketched Art | photo-like | 16 | 0.99 s, 15 paths, 11568 anchors, 128 MB |
| Sketched Art | four-colour badge | 16 | 0.39 s, 2 paths, 361 anchors, 88 MB |
| Silhouettes | photo-like | 16 | 1.30 s, 3 paths, 3358 anchors, 144 MB |
| Silhouettes | four-colour badge | 16 | 0.41 s, 1 paths, 32 anchors, 95 MB |
| Line Art | photo-like | 16 | 0.95 s, 713 paths, 36212 anchors, 129 MB |
| Line Art | four-colour badge | 16 | 0.34 s, 1 paths, 49 anchors, 88 MB |
| Technical Drawing | photo-like | 16 | 1.09 s, 4578 paths, 79049 anchors, 130 MB |
| Technical Drawing | four-colour badge | 16 | 0.37 s, 1 paths, 69 anchors, 88 MB |
| Flat Logo | photo-like | 16 | refused after 0.11 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 16 | 0.67 s, 5 paths, 67 anchors, 146 MB |
| Flat Logo | four-colour badge | 4 | 0.71 s, 5 paths, 69 anchors, 146 MB |
| Default with Shapes | photo-like | 4 | 0.20 s, 48 paths, 14438 anchors, 49 MB |
| Default with Shapes | four-colour badge | 4 | 0.10 s, 3 paths, 72 anchors, 48 MB |
| 6 Colors with Shapes | photo-like | 4 | 0.28 s, 115 paths, 23295 anchors, 99 MB |
| 6 Colors with Shapes | four-colour badge | 4 | 0.21 s, 5 paths, 92 anchors, 46 MB |
| 6 Colors with Auto Grouping | photo-like | 4 | 0.35 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors with Auto Grouping | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| Shades of Gray with Auto Grouping | photo-like | 4 | 0.32 s, 137 paths, 18823 anchors, 168 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 4 | 0.28 s, 10 paths, 1256 anchors, 137 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 4 | 0.92 s, 4479 paths, 337884 anchors, 638 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 4 | 0.22 s, 6 paths, 200 anchors, 94 MB |
| Default | photo-like | 16 | 1.11 s, 127 paths, 34034 anchors, 199 MB |
| Default | four-colour badge | 16 | 0.52 s, 3 paths, 88 anchors, 184 MB |
| Auto Color | photo-like | 16 | 1.78 s, 5390 paths, 454670 anchors, 277 MB |
| Auto Color | four-colour badge | 16 | 0.84 s, 5 paths, 112 anchors, 184 MB |
| High Fidelity Photo | photo-like | 16 | 4.09 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo | four-colour badge | 16 | 1.12 s, 6 paths, 264 anchors, 257 MB |
| Low Fidelity Photo | photo-like | 16 | 2.03 s, 2036 paths, 182778 anchors, 754 MB |
| Low Fidelity Photo | four-colour badge | 16 | 1.08 s, 6 paths, 180 anchors, 257 MB |
| 3 Colors | photo-like | 16 | 1.27 s, 76 paths, 23024 anchors, 290 MB |
| 3 Colors | four-colour badge | 16 | 0.87 s, 4 paths, 190 anchors, 205 MB |
| 6 Colors | photo-like | 16 | 1.30 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors | four-colour badge | 16 | 0.92 s, 5 paths, 112 anchors, 184 MB |
| 16 Colors | photo-like | 16 | 1.39 s, 1973 paths, 145642 anchors, 292 MB |
| 16 Colors | four-colour badge | 16 | 0.86 s, 5 paths, 114 anchors, 184 MB |
| Shades of Gray | photo-like | 16 | 1.72 s, 686 paths, 53654 anchors, 407 MB |
| Shades of Gray | four-colour badge | 16 | 1.43 s, 9 paths, 2208 anchors, 270 MB |
| Black and White Logo | photo-like | 16 | 1.22 s, 1128 paths, 45273 anchors, 129 MB |
| Black and White Logo | four-colour badge | 16 | 0.44 s, 1 paths, 69 anchors, 88 MB |
| Sketched Art | photo-like | 16 | 1.16 s, 15 paths, 11486 anchors, 128 MB |
| Sketched Art | four-colour badge | 16 | 0.41 s, 2 paths, 361 anchors, 88 MB |
| Silhouettes | photo-like | 16 | 1.40 s, 3 paths, 3350 anchors, 144 MB |
| Silhouettes | four-colour badge | 16 | 0.45 s, 1 paths, 32 anchors, 95 MB |
| Line Art | photo-like | 16 | 1.15 s, 713 paths, 37370 anchors, 129 MB |
| Line Art | four-colour badge | 16 | 0.46 s, 1 paths, 49 anchors, 88 MB |
| Technical Drawing | photo-like | 16 | 1.29 s, 4577 paths, 76559 anchors, 130 MB |
| Technical Drawing | four-colour badge | 16 | 0.42 s, 1 paths, 69 anchors, 88 MB |
| Outline | photo-like | 16 | 1.39 s, 632 paths, 34884 anchors, 256 MB |
| Outline | four-colour badge | 16 | 0.66 s, 1 paths, 49 anchors, 176 MB |
| Flat Logo | photo-like | 16 | refused after 0.13 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 16 | 0.74 s, 5 paths, 67 anchors, 146 MB |
| Default with Shapes | photo-like | 16 | 1.10 s, 127 paths, 34034 anchors, 199 MB |
| Default with Shapes | four-colour badge | 16 | 0.52 s, 3 paths, 88 anchors, 184 MB |
| 6 Colors with Shapes | photo-like | 16 | 1.21 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors with Shapes | four-colour badge | 16 | 0.88 s, 5 paths, 112 anchors, 184 MB |
| 6 Colors with Auto Grouping | photo-like | 16 | 1.24 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors with Auto Grouping | four-colour badge | 16 | 0.87 s, 5 paths, 112 anchors, 184 MB |
| Shades of Gray with Auto Grouping | photo-like | 16 | 1.78 s, 686 paths, 53654 anchors, 407 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 16 | 1.52 s, 9 paths, 2208 anchors, 270 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 16 | 7.33 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 16 | 1.13 s, 6 paths, 264 anchors, 257 MB |
## Revision history
| date | change |
|:--|:--|
| 2026-10-13 | Auto Grouping (off unless asked for): each path that has others inside it is grouped with them; a "grouped %" column, and three presets measured with the option on |
| 2026-10-13 | Shapes (off unless asked for): filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that shape; corners whose pixel is mostly outside the shape are corners; plain-shape inputs and a "shapes %" column |
| 2026-10-13 | Create: Strokes: centre lines placed in the middle of their ink and as wide as it, fitted as outlines are; centre-line distance, width error and anchors per stroke measured against the thin lines' truth |
| 2026-10-13 | Snap Curves To Lines levels lines within 2.5 degrees of level or upright; askew inputs and an "upright %" column, so the baseline was measured again with them (the tracer of main at `baf9693f`) |
| 2026-10-13 | Ignore Color; the Auto Color and Outline presets, measured from here on (Outline is the first measure of Create: Strokes) |
| 2026-10-13 | Thin lines: black and white read against the image's darkest black and the paper near each edge |
| 2026-10-13 | Memory: layers traced within the box of their pixels and a few at a time (16 megapixel photo: 3.9 GB to 0.9 GB) |
| 2026-10-13 | Shared edges: outlines are cut at their junctions and each piece is fitted once, the same from either side |