Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
250da4226e | ||
|
|
8d2d838884 | ||
|
|
bc5bdf47d1 | ||
|
|
6b99150aac | ||
|
|
26bb452ba1 | ||
|
|
dbb8bdcaaa | ||
|
|
ecf4ae7a3a | ||
|
|
e518974913 | ||
|
|
a20b0308b8 | ||
|
|
3126e82ebd | ||
|
|
d8a298a310 | ||
|
|
9c42a2942a | ||
|
|
36286cc783 | ||
|
|
bc343414d8 | ||
|
|
7b0164e4e7 | ||
|
|
670946ef75 | ||
|
|
bdb9963c73 | ||
|
|
dc64aaa41b | ||
|
|
5366cf689e | ||
|
|
cca3a63e4a | ||
|
|
507c6e0e63 | ||
|
|
dfc1b846cc | ||
|
|
a169693817 | ||
|
|
db85d465c8 | ||
|
|
de8d5c96ab | ||
|
|
d44615753a | ||
|
|
e794c1ac63 | ||
|
|
0573d3cdfb | ||
|
|
9ca74ac011 | ||
|
|
f0f3cb4eb1 | ||
|
|
5fe916a425 | ||
|
|
8e7b9ba436 | ||
|
|
cbcee3bd54 | ||
|
|
0810a15f6e | ||
|
|
db7e568809 | ||
|
|
9d72686d95 | ||
|
|
d79fb145c2 | ||
|
|
6a16627de6 | ||
|
|
acc1c9a85d | ||
|
|
8fbf724cf6 | ||
|
|
02ed637c68 | ||
|
|
9d2b074d12 | ||
|
|
632ea2a9f3 | ||
|
|
523fd6e185 | ||
|
|
612082e26e | ||
|
|
fea316911d | ||
|
|
3af08505f8 | ||
|
|
2c01a42b35 | ||
|
|
45334df881 | ||
|
|
ee97225c19 | ||
|
|
6085197bf4 | ||
|
|
50911d8608 | ||
|
|
6939eff2f9 | ||
|
|
9652d1d6b2 | ||
|
|
79b5d73ca7 | ||
|
|
05fdd15b45 | ||
|
|
0a16b62a22 | ||
|
|
20f5174710 | ||
|
|
00195e8d28 | ||
|
|
05e321f4e0 | ||
|
|
85770d3afb | ||
|
|
421443b8ac | ||
|
|
e2a63a8baf | ||
|
|
44d170f059 | ||
|
|
fce90e3144 | ||
|
|
761cc2371a | ||
|
|
3752b3ec31 |
No files matched your search
Generated
-1
@@ -4916,7 +4916,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-pathops",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (community PRs and fixes of late 2026-10-12: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; French catalog complete again, #1511; headline numbers unchanged; Image Trace colour and photo presets, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-12: Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; many-object edits, #1429 in part; headline numbers unchanged; Image Trace places edges to a fraction of a pixel, measured in `docs/image-trace-quality.md`, headline numbers unchanged; Transform dialogs preview and the transform tools act on direct-selected anchors, #1412 and #1408; `.ai` compound path paint, #1410, overprint, #1388, and text-only PDF parts, #1387; community PRs: strokes wider than their shape, corner drags without the bounding box, pasted pictures, compound paths in `document.inspect`, MCP guide; headline numbers unchanged; community PRs: `.ai` editing-data follow-ups (gradient stops, type of objects with an appearance, short area-type frames), PDF bleed with editing data, dialog keyboard focus, art flush with an artboard, font rescan, symbol instance redraw, `--control` port check; headline numbers unchanged; Rectangular Grid and Polar Grid tool options, #1377 in part; `.ai` files with placed (linked) pictures open from their editing data, #1264; community PRs: `.ai` and PDF bleed and editing-data import, gradient shapes imported as paths, ruler origin, Align To, Live Corners corner selection; headline numbers unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; community PRs: graph designs, Graph Data worksheet and Graph Type options; Align both centres; Pen endpoint fixes; earlier: Effect › Brush Strokes landed: Photoshop-style filters 28 of 57, raster effects 40% → 48%; `.ai` gradients, meshes and artboard names from the editing data; headline numbers unchanged; `.ai` art styles and clipping fills from the editing data; headline numbers unchanged; `editingData: "always"`; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (community PRs and fixes of 2026-10-13: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; `.ai`, PDF, EPS and SVG import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; headline numbers unchanged; community PRs and fixes of late 2026-10-12: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; French catalog complete again, #1511; headline numbers unchanged; Image Trace colour and photo presets, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-12: Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; many-object edits, #1429 in part; headline numbers unchanged; Image Trace places edges to a fraction of a pixel, measured in `docs/image-trace-quality.md`, headline numbers unchanged; Transform dialogs preview and the transform tools act on direct-selected anchors, #1412 and #1408; `.ai` compound path paint, #1410, overprint, #1388, and text-only PDF parts, #1387; community PRs: strokes wider than their shape, corner drags without the bounding box, pasted pictures, compound paths in `document.inspect`, MCP guide; headline numbers unchanged; community PRs: `.ai` editing-data follow-ups (gradient stops, type of objects with an appearance, short area-type frames), PDF bleed with editing data, dialog keyboard focus, art flush with an artboard, font rescan, symbol instance redraw, `--control` port check; headline numbers unchanged; Rectangular Grid and Polar Grid tool options, #1377 in part; `.ai` files with placed (linked) pictures open from their editing data, #1264; community PRs: `.ai` and PDF bleed and editing-data import, gradient shapes imported as paths, ruler origin, Align To, Live Corners corner selection; headline numbers unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; community PRs: graph designs, Graph Data worksheet and Graph Type options; Align both centres; Pen endpoint fixes; earlier: Effect › Brush Strokes landed: Photoshop-style filters 28 of 57, raster effects 40% → 48%; `.ai` gradients, meshes and artboard names from the editing data; headline numbers unchanged; `.ai` art styles and clipping fills from the editing data; headline numbers unchanged; `editingData: "always"`; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
|
||||
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
|
||||
@@ -127,6 +127,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-13 | The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492). Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499). Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Import: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537). Live Corners: a round corner next to a curved side curves smoothly instead of sticking out of the shape, and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533). Guides: Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534). Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). Fonts: the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux). |
|
||||
| 2026-10-13 | Image Trace's colour presets trace flat art with its own colours however many are asked for (noise no longer shatters it: 16 Colors 70 → 4 paths on the harness's flat art; no slivers of in-between colour along anti-aliased edges), and the photo presets trace a photo into smooth tones without specks or hairlines (High Fidelity Photo 3,653 → 121 paths, colour error 2.30 → 1.75; 16 megapixels traced where 4 were refused) ([`docs/image-trace-quality.md`](docs/image-trace-quality.md)). |
|
||||
| 2026-10-13 | Image Trace traces anti-aliased and blurred images to a fraction of a pixel in Black and White, Grayscale and Color, with corners put back, straight edges as lines and round outlines as circles; a harness measures every preset before and after ([`docs/image-trace-quality.md`](docs/image-trace-quality.md)): outlines 0.31 → 0.09 px off on average (Default), 0.27 → 0.06 (Black and White Logo). |
|
||||
| 2026-10-13 | Effect › 3D and Materials: Inflate takes Twist, Taper and Cap (Hollow leaves the back open), as the 3D docs list for every type. |
|
||||
|
||||
@@ -249,7 +249,7 @@ pub(super) fn builtin_rgb_bytes(name: &str) -> Result<Vec<u8>, CmsError> {
|
||||
|
||||
/// Encode `p` as an ICC file. The creation date is fixed so exports are reproducible.
|
||||
pub(super) fn encode(p: &ColorProfile) -> Result<Vec<u8>, CmsError> {
|
||||
let mut bytes = p.encode().map_err(|e| CmsError::BadProfile(format!("{e:?}")))?;
|
||||
let mut bytes = aligned(p.encode().map_err(|e| CmsError::BadProfile(format!("{e:?}")))?);
|
||||
// Header bytes 24..36: year, month, day, hours, minutes, seconds (u16 each).
|
||||
if let Some(date) = bytes.get_mut(24..36) {
|
||||
date.copy_from_slice(&[2026u16, 1, 1, 0, 0, 0].map(u16::to_be_bytes).concat());
|
||||
@@ -257,6 +257,55 @@ pub(super) fn encode(p: &ColorProfile) -> Result<Vec<u8>, CmsError> {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// `icc` with every tagged element starting on a 4-byte boundary, padded with zeros, as ICC.1
|
||||
/// §7.1.2 requires (moxcms packs them, so a 62-byte `desc` left the next tag unaligned, #1584).
|
||||
/// Tags that share data keep sharing it. A profile whose tag table doesn't read is returned as it is.
|
||||
fn aligned(icc: Vec<u8>) -> Vec<u8> {
|
||||
const TABLE: usize = 128;
|
||||
let be = |b: &[u8], at: usize| b.get(at..at + 4).map(|v| u32::from_be_bytes([v[0], v[1], v[2], v[3]]) as usize);
|
||||
let Some(n) = be(&icc, TABLE).filter(|n| *n <= 1000) else { return icc };
|
||||
let data_start = TABLE + 4 + 12 * n;
|
||||
let mut entries = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let e = TABLE + 4 + 12 * i;
|
||||
let (Some(off), Some(size)) = (be(&icc, e + 4), be(&icc, e + 8)) else { return icc };
|
||||
if off < data_start || off.checked_add(size).is_none_or(|end| end > icc.len()) {
|
||||
return icc;
|
||||
}
|
||||
entries.push((off, size));
|
||||
}
|
||||
if entries.iter().all(|(off, _)| off.is_multiple_of(4)) && icc.len().is_multiple_of(4) {
|
||||
return icc;
|
||||
}
|
||||
let mut out = icc.get(..data_start).map(<[u8]>::to_vec).unwrap_or_default();
|
||||
let mut moved: Vec<((usize, usize), usize)> = Vec::new();
|
||||
let mut order: Vec<usize> = (0..n).collect();
|
||||
order.sort_by_key(|&i| entries[i].0);
|
||||
for i in order {
|
||||
let (off, size) = entries[i];
|
||||
let at = match moved.iter().find(|(k, _)| *k == (off, size)) {
|
||||
Some((_, at)) => *at,
|
||||
None => {
|
||||
out.resize(out.len().next_multiple_of(4), 0);
|
||||
let at = out.len();
|
||||
out.extend_from_slice(&icc[off..off + size]);
|
||||
moved.push(((off, size), at));
|
||||
at
|
||||
}
|
||||
};
|
||||
let e = TABLE + 4 + 12 * i + 4;
|
||||
out[e..e + 4].copy_from_slice(&(at as u32).to_be_bytes());
|
||||
}
|
||||
out.resize(out.len().next_multiple_of(4), 0);
|
||||
let len = out.len() as u32;
|
||||
out[0..4].copy_from_slice(&len.to_be_bytes());
|
||||
// The profile ID (an MD5 of the old bytes) no longer matches: zero is "not computed".
|
||||
if let Some(id) = out.get_mut(84..100) {
|
||||
id.fill(0);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn text_tag(s: &str) -> Option<ProfileText> {
|
||||
Some(ProfileText::Localizable(vec![LocalizableString::new("en".into(), "US".into(), s.into())]))
|
||||
}
|
||||
|
||||
@@ -111,3 +111,26 @@ fn black_point_compensation_maps_the_darkest_ink_to_rgb_black() {
|
||||
let abs = |bpc| icc_cmyk(bpc).cmyk_to_rgb(rich, Intent::AbsoluteColorimetric);
|
||||
assert_eq!(abs(true), abs(false));
|
||||
}
|
||||
|
||||
/// #1584: the profiles written here (built-in RGB spaces) have every tag on a 4-byte boundary and
|
||||
/// a size that is a multiple of 4 (ICC.1 §7.1.2), and still read back as the same space.
|
||||
#[test]
|
||||
fn written_profiles_align_their_tags() {
|
||||
for name in [super::SRGB, super::DISPLAY_P3, super::WIDE_GAMUT_RGB, super::PROPHOTO_RGB] {
|
||||
let icc = super::icc_bytes(name).unwrap();
|
||||
let be = |at: usize| u32::from_be_bytes([icc[at], icc[at + 1], icc[at + 2], icc[at + 3]]) as usize;
|
||||
assert_eq!(be(0), icc.len(), "{name}: header size");
|
||||
assert_eq!(icc.len() % 4, 0, "{name}: {} bytes", icc.len());
|
||||
let n = be(128);
|
||||
for i in 0..n {
|
||||
let (off, size) = (be(132 + 12 * i + 4), be(132 + 12 * i + 8));
|
||||
assert_eq!(off % 4, 0, "{name}: tag {i} at {off}");
|
||||
assert!(off + size <= icc.len());
|
||||
}
|
||||
let back = moxcms::ColorProfile::new_from_slice(&icc).unwrap();
|
||||
let orig = moxcms::ColorProfile::new_srgb();
|
||||
if name == super::SRGB {
|
||||
assert_eq!((back.red_colorant, back.green_colorant, back.blue_colorant), (orig.red_colorant, orig.green_colorant, orig.blue_colorant));
|
||||
}
|
||||
}
|
||||
}
|
||||
+230
-16
@@ -1,5 +1,7 @@
|
||||
//! Hit testing over the document tree.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use vectorcraft_geom::hit::{fill_contains, stroke_contains};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
@@ -83,6 +85,77 @@ pub fn hit_test_visible(doc: &Document, p: Point, opt: HitOptions) -> Option<Hit
|
||||
/// first: what clicks there select, one below the other (Cmd/Ctrl-click selects behind).
|
||||
pub fn objects_at(doc: &Document, p: Point, opt: HitOptions, scope: Option<NodeId>) -> Vec<NodeId> {
|
||||
let opt = HitOptions { scope: scope.or(opt.scope), ..opt };
|
||||
// Normally one hit test per *object* suffices: layers and sublayers aren't selectable,
|
||||
// while a group (including a clipping group) is selected as one object. Repeatedly
|
||||
// searching the full tree for each hit made a stack of n overlapping paths O(n^2).
|
||||
// Isolation and Edit Contents change where selection groups begin; keep the original
|
||||
// traversal for those cases rather than change their Select Behind semantics.
|
||||
if opt.scope.is_some() {
|
||||
return objects_at_repeated(doc, p, opt, scope);
|
||||
}
|
||||
let mut tops = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for layer in doc.layers.iter().rev() {
|
||||
// Malformed files may contain non-layer roots; let the general traversal handle them.
|
||||
if !layer.is_layer() {
|
||||
return objects_at_repeated(doc, p, opt, scope);
|
||||
}
|
||||
if !layer.visible || layer.locked || layer.is_template() || doc.pattern_edit.as_ref().is_some_and(|e| e.layer != layer.id) {
|
||||
continue;
|
||||
}
|
||||
if !objects_at_layer(layer, p, opt, &mut tops, &mut seen) {
|
||||
// An envelope in Edit Contents may expose objects *within* a top-level group.
|
||||
// The repeated traversal handles that mixed hierarchy precisely.
|
||||
return objects_at_repeated(doc, p, opt, scope);
|
||||
}
|
||||
}
|
||||
tops
|
||||
}
|
||||
|
||||
/// One traversal through a layer and its sublayers. Each selectable object's subtree is
|
||||
/// inspected once (only until its first hit). `false` asks the caller to use the general
|
||||
/// algorithm for an envelope whose contents are currently being edited.
|
||||
fn objects_at_layer(layer: &Node, p: Point, opt: HitOptions, tops: &mut Vec<NodeId>, seen: &mut HashSet<NodeId>) -> bool {
|
||||
let Some(children) = layer.children() else { return true };
|
||||
if layer.clips() && children.first().is_some_and(|clip| !clip_contains(clip, p)) {
|
||||
return true;
|
||||
}
|
||||
let opt = if matches!(layer.kind, NodeKind::Layer { preview: false, .. }) { HitOptions { outline: true, ..opt } } else { opt };
|
||||
for (i, c) in children.iter().enumerate().rev() {
|
||||
if !may_hit(layer, i, c, p, opt, true) || seen.contains(&c.id) {
|
||||
continue;
|
||||
}
|
||||
if c.is_layer() {
|
||||
if !objects_at_layer(c, p, opt, tops, seen) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Editing an envelope selects its contents rather than the containing object. Even
|
||||
// when nested in a group, the old traversal may select both a member and the group.
|
||||
if c.is_container() {
|
||||
let mut editing = false;
|
||||
c.walk(&mut |n| editing |= edits_contents(n));
|
||||
if editing {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let skip = |id| seen.contains(&id);
|
||||
let mut chain = Vec::new();
|
||||
let hit = match &c.kind {
|
||||
NodeKind::Group { .. } => hit_children(c, p, opt, &skip, &mut chain, true).is_some(),
|
||||
NodeKind::Blend { .. } => hit_children(c, p, opt, &skip, &mut chain, true).is_some() || hit_leaf(c, p, opt).is_some(),
|
||||
_ => hit_leaf(c, p, opt).is_some(),
|
||||
};
|
||||
if hit && seen.insert(c.id) {
|
||||
tops.push(c.id);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The general selection order also handles isolation and nested Edit Contents.
|
||||
fn objects_at_repeated(doc: &Document, p: Point, opt: HitOptions, scope: Option<NodeId>) -> Vec<NodeId> {
|
||||
let mut tops: Vec<NodeId> = vec![];
|
||||
// Each round leaves out the objects found so far (and everything inside them).
|
||||
while let Some(top) = hit_test_skipping(doc, p, opt, &|id| tops.contains(&id), true).map(|h| h.top_object(scope)) {
|
||||
@@ -150,6 +223,22 @@ fn clip_contains(clip: &Node, p: Point) -> bool {
|
||||
clip.clip_shapes(Some(&frame)).iter().any(|(bp, rule)| fill_contains(bp, *rule, p))
|
||||
}
|
||||
|
||||
/// Can a click at `p` hit `c`, the child `i` of `parent`, or something inside it? Not when it is
|
||||
/// hidden, when it can't be edited (`editable_only`), when it is the part of a Shaper group that
|
||||
/// isn't shown, or when `p` is outside its reach.
|
||||
fn may_hit(parent: &Node, i: usize, c: &Node, p: Point, opt: HitOptions, editable_only: bool) -> bool {
|
||||
let source_isolated = |n: &Node| n.children().and_then(|children| children.first()).is_some_and(|source| Some(source.id) == opt.scope);
|
||||
if parent.extras().shaper.is_some() && (i == 0) != source_isolated(parent) {
|
||||
return false;
|
||||
}
|
||||
// Read-only samples can read locked/template art; hidden art never participates.
|
||||
if !c.visible || editable_only && (c.locked || c.is_template()) {
|
||||
return false;
|
||||
}
|
||||
// (An envelope's content sits where it was, not where the envelope draws it.)
|
||||
edits_contents(c) || c.extras().shaper.is_some() && source_isolated(c) || c.reach_bounds().is_none_or(|b| b.inflate(opt.tol, opt.tol).contains(p))
|
||||
}
|
||||
|
||||
fn hit_children(
|
||||
parent: &Node,
|
||||
p: Point,
|
||||
@@ -167,22 +256,7 @@ fn hit_children(
|
||||
return None;
|
||||
}
|
||||
for (i, c) in children.iter().enumerate().rev() {
|
||||
if parent.extras().shaper.is_some() {
|
||||
let construction = children.first().is_some_and(|source| Some(source.id) == opt.scope);
|
||||
if (i == 0) != construction {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Read-only samples can read locked/template art; hidden art never participates.
|
||||
if !c.visible || editable_only && (c.locked || c.is_template()) || skip(c.id) {
|
||||
continue;
|
||||
}
|
||||
// (An envelope's content sits where it was, not where the envelope draws it.)
|
||||
if !(edits_contents(c)
|
||||
|| c.extras().shaper.is_some() && c.children().and_then(|children| children.first()).is_some_and(|source| Some(source.id) == opt.scope))
|
||||
&& let Some(b) = c.reach_bounds()
|
||||
&& !b.inflate(opt.tol, opt.tol).contains(p)
|
||||
{
|
||||
if !may_hit(parent, i, c, p, opt, editable_only) || skip(c.id) {
|
||||
continue;
|
||||
}
|
||||
chain.push(c.id);
|
||||
@@ -619,6 +693,146 @@ world",
|
||||
assert!(at(90.0, None).is_empty());
|
||||
}
|
||||
|
||||
/// Many overlapping paths: Select Behind must list each exactly once, front to back.
|
||||
/// This is quadratic with repeated full-document hit tests, but linear in tree size with
|
||||
/// the per-object walk.
|
||||
#[test]
|
||||
fn objects_at_large_overlapping_stack() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let layer = d.layers[0].id;
|
||||
let mut ids = Vec::new();
|
||||
for _ in 0..256 {
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), Appearance::default_art())).unwrap();
|
||||
ids.push(id);
|
||||
}
|
||||
ids.reverse();
|
||||
assert_eq!(objects_at(&d, Point::new(25.0, 25.0), HitOptions::default(), None), ids);
|
||||
}
|
||||
|
||||
/// Match the original skip-and-retest algorithm for normal groups, sublayers, clipping,
|
||||
/// invisibility, and path/outline-only settings.
|
||||
#[test]
|
||||
fn objects_at_one_pass_matches_repeated_hits() {
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let layer = d.layers[0].id;
|
||||
let add = |d: &mut Document, parent, bounds| {
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(parent), usize::MAX, Node::path(id, shapes::rectangle(bounds), Appearance::default_art())).unwrap();
|
||||
id
|
||||
};
|
||||
add(&mut d, layer, Rect::new(0.0, 0.0, 60.0, 60.0));
|
||||
let sub = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::layer(sub, "Sublayer", crate::LayerColor::Preset(2))).unwrap();
|
||||
add(&mut d, sub, Rect::new(0.0, 0.0, 60.0, 60.0));
|
||||
let group = d.alloc_id();
|
||||
d.insert(Some(layer), usize::MAX, Node::group(group, Vec::new())).unwrap();
|
||||
add(&mut d, group, Rect::new(0.0, 0.0, 60.0, 60.0));
|
||||
add(&mut d, group, Rect::new(5.0, 5.0, 60.0, 60.0));
|
||||
let clip = d.alloc_id();
|
||||
let mask = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(5.0, 5.0, 45.0, 45.0)), Appearance::default_art());
|
||||
let content = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 80.0, 80.0)), Appearance::default_art());
|
||||
let mut clipping = Node::group(clip, vec![Arc::new(mask), Arc::new(content)]);
|
||||
clipping.set_clips(true);
|
||||
d.insert(Some(layer), usize::MAX, clipping).unwrap();
|
||||
for (point, opt) in [
|
||||
(Point::new(25.0, 25.0), HitOptions::default()),
|
||||
(Point::new(3.0, 25.0), HitOptions::default()),
|
||||
(Point::new(25.0, 25.0), HitOptions { outline: true, ..Default::default() }),
|
||||
(Point::new(6.0, 25.0), HitOptions { path_only: true, ..Default::default() }),
|
||||
(Point::new(100.0, 100.0), HitOptions::default()),
|
||||
] {
|
||||
assert_eq!(objects_at(&d, point, opt, None), objects_at_repeated(&d, point, opt, None), "point={point:?}, opt={opt:?}");
|
||||
}
|
||||
if let NodeKind::Layer { preview, .. } = &mut d.node_mut(sub).unwrap().kind {
|
||||
*preview = false;
|
||||
}
|
||||
let p = Point::new(25.0, 25.0);
|
||||
let opt = HitOptions::default();
|
||||
assert_eq!(objects_at(&d, p, opt, None), objects_at_repeated(&d, p, opt, None));
|
||||
d.node_mut(group).unwrap().locked = true;
|
||||
assert_eq!(objects_at(&d, p, opt, None), objects_at_repeated(&d, p, opt, None));
|
||||
}
|
||||
|
||||
/// Documents built at random (nested groups, sublayers, clipping groups, compound paths, blends
|
||||
/// of nothing, hidden and locked objects, outline layers): Select Behind lists the same objects
|
||||
/// in the same order as the hit test repeated over what is left.
|
||||
#[test]
|
||||
fn objects_at_matches_repeated_hits_on_random_documents() {
|
||||
let mut seed = 0x9e37_79b9_u32;
|
||||
let mut next = move |n: u32| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed % n
|
||||
};
|
||||
fn fill(d: &mut Document, parent: NodeId, depth: u32, next: &mut impl FnMut(u32) -> u32) {
|
||||
for _ in 0..2 + next(4) {
|
||||
let id = d.alloc_id();
|
||||
let (x, y) = (f64::from(next(60)), f64::from(next(60)));
|
||||
let rect = Rect::new(x, y, x + 10.0 + f64::from(next(50)), y + 10.0 + f64::from(next(50)));
|
||||
let appearance = if next(4) == 0 { Appearance::default() } else { Appearance::default_art() };
|
||||
let mut n = match next(if depth < 3 { 8 } else { 4 }) {
|
||||
4 | 5 => Node::group(id, Vec::new()),
|
||||
6 => Node::layer(id, "Sublayer", crate::LayerColor::Preset(2)),
|
||||
7 => {
|
||||
let mut g = Node::group(id, Vec::new());
|
||||
g.set_clips(true);
|
||||
g
|
||||
}
|
||||
_ => Node::path(id, shapes::rectangle(rect), appearance),
|
||||
};
|
||||
n.visible = next(8) != 0;
|
||||
n.locked = next(8) == 0;
|
||||
if let NodeKind::Layer { preview, .. } = &mut n.kind {
|
||||
*preview = next(3) != 0;
|
||||
}
|
||||
let container = n.is_container();
|
||||
// A sublayer belongs in a layer; in a group it is a group like another to the hit test.
|
||||
d.insert(Some(parent), usize::MAX, n).unwrap();
|
||||
if container {
|
||||
fill(d, id, depth + 1, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
for round in 0..200 {
|
||||
let mut d = Document::new(120.0, 120.0);
|
||||
let layer = d.layers[0].id;
|
||||
fill(&mut d, layer, 0, &mut next);
|
||||
for _ in 0..12 {
|
||||
let p = Point::new(f64::from(next(120)), f64::from(next(120)));
|
||||
let opt = HitOptions { outline: next(5) == 0, path_only: next(5) == 0, ..Default::default() };
|
||||
assert_eq!(objects_at(&d, p, opt, None), objects_at_repeated(&d, p, opt, None), "round {round}, {p:?}, {opt:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit Contents can expose descendants of a top-level group: preserve the original
|
||||
/// selection order by falling back to the general hit traversal.
|
||||
#[test]
|
||||
fn objects_at_edit_contents_uses_general_order() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let layer = d.layers[0].id;
|
||||
let inner = |id| Arc::new(Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), Appearance::default_art()));
|
||||
let (a, b) = (d.alloc_id(), d.alloc_id());
|
||||
let e = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Envelope {
|
||||
content: vec![inner(a), inner(b)],
|
||||
kind: Box::new(crate::live::EnvelopeKind::Warp { style: "arc".to_string(), bend: 0.0, h: 0.0, v: 0.0, horizontal: true }),
|
||||
fidelity: 100.0,
|
||||
editing: true,
|
||||
options: Default::default(),
|
||||
frame: Box::new(vectorcraft_geom::Affine::IDENTITY),
|
||||
},
|
||||
);
|
||||
d.insert(Some(layer), usize::MAX, e).unwrap();
|
||||
let p = Point::new(25.0, 25.0);
|
||||
let opt = HitOptions::default();
|
||||
assert_eq!(objects_at(&d, p, opt, None), objects_at_repeated(&d, p, opt, None));
|
||||
assert_eq!(objects_at(&d, p, opt, None), vec![b, a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_and_hidden_skip() {
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
+39
-8
@@ -318,8 +318,10 @@ impl WarpStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// The warp map on normalised coordinates: `b` = bend (-1..1), `dh`/`dv` = distortion (-1..1).
|
||||
pub fn warp_point(style: WarpStyle, b: f64, dh: f64, dv: f64, x: f64, y: f64) -> (f64, f64) {
|
||||
/// The warp map on normalised coordinates: `b` = bend (-1..1), `dh`/`dv` = distortion (-1..1);
|
||||
/// `aspect` is the box's half-height over its half-width along the warp (Arc bends in real
|
||||
/// proportions).
|
||||
pub fn warp_point(style: WarpStyle, b: f64, dh: f64, dv: f64, aspect: f64, x: f64, y: f64) -> (f64, f64) {
|
||||
use WarpStyle::*;
|
||||
let t = (y + 1.0) / 2.0; // 0 at top, 1 at bottom
|
||||
let par = 1.0 - x * x; // parabola: 1 at centre, 0 at sides
|
||||
@@ -328,14 +330,18 @@ pub fn warp_point(style: WarpStyle, b: f64, dh: f64, dv: f64, x: f64, y: f64) ->
|
||||
if b.abs() < 1e-6 {
|
||||
(x, y)
|
||||
} else {
|
||||
// Bend the centre line into a circular arc of equal length (sweep = |b|·180°).
|
||||
// Bend the centre line into a circular arc of equal length (sweep = |b|·180°), in
|
||||
// the box's real proportions (lengths in half-widths, so y counts `aspect` each):
|
||||
// the edges are concentric arcs, and one that would pass the centre stops there
|
||||
// instead of folding over (#1578).
|
||||
let k = if aspect.is_finite() && aspect > 0.0 { aspect } else { 1.0 };
|
||||
let sweep = b.abs() * PI;
|
||||
let r0 = 2.0 / sweep;
|
||||
let yy = if b > 0.0 { y } else { -y };
|
||||
let yy = if b > 0.0 { y } else { -y } * k;
|
||||
let a = x / r0;
|
||||
let r = r0 - yy;
|
||||
let r = (r0 - yy).max(0.0);
|
||||
let (s, c) = a.sin_cos();
|
||||
let (nx, ny) = (r * s, r0 - r * c);
|
||||
let (nx, ny) = (r * s, (r0 - r * c) / k);
|
||||
(nx, if b > 0.0 { ny } else { -ny })
|
||||
}
|
||||
}
|
||||
@@ -1122,9 +1128,9 @@ fn surface_of<'a>(kind: &'a EnvelopeKind, src: Rect, frame: Affine) -> Box<dyn F
|
||||
Box::new(move |u: f64, v: f64| {
|
||||
let (x, y) = (2.0 * u - 1.0, 2.0 * v - 1.0);
|
||||
let (x2, y2) = if horizontal {
|
||||
warp_point(st, b, dh, dv, x, y)
|
||||
warp_point(st, b, dh, dv, hh / hw.max(1e-9), x, y)
|
||||
} else {
|
||||
let (a, b2) = warp_point(st, b, dh, dv, y, x);
|
||||
let (a, b2) = warp_point(st, b, dh, dv, hw / hh.max(1e-9), y, x);
|
||||
(b2, a)
|
||||
};
|
||||
frame * Point::new(c.x + x2 * hw, c.y + y2 * hh)
|
||||
@@ -1711,6 +1717,31 @@ pub(crate) fn max_outset(nodes: &[Arc<Node>]) -> f64 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #1578: Arc bends in the box's real proportions. In half-width units (y scaled by the
|
||||
/// aspect), the top and bottom edges are concentric circles about the bend's centre, at their
|
||||
/// real distance from the centre line; an inner edge that would pass the centre stops there.
|
||||
#[test]
|
||||
fn arc_warp_is_round_in_real_proportions() {
|
||||
for (aspect, b) in [(0.25, 0.1), (0.25, 0.5), (0.25, 1.0), (1.0, 0.5), (2.0, 0.3)] {
|
||||
let r0 = 2.0 / (b * PI);
|
||||
for (y, r) in [(-1.0, r0 + aspect), (1.0, (r0 - aspect).max(0.0))] {
|
||||
for i in 0..=10 {
|
||||
let x = -1.0 + 0.2 * f64::from(i);
|
||||
let (nx, ny) = warp_point(WarpStyle::Arc, b, 0.0, 0.0, aspect, x, y);
|
||||
let d = nx.hypot(ny * aspect - r0);
|
||||
assert!((d - r).abs() < 1e-9, "aspect {aspect}, bend {b}, y {y}, x {x}: {d} vs {r}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// At 100 % on a square the inner edge meets at the centre instead of folding over.
|
||||
let (nx, _) = warp_point(WarpStyle::Arc, 1.0, 0.0, 0.0, 1.0, 0.8, 1.0);
|
||||
assert!(nx.abs() < 1e-12);
|
||||
// A square bent less than that is what it always was.
|
||||
let (nx, ny) = warp_point(WarpStyle::Arc, 0.5, 0.0, 0.0, 1.0, 0.5, 0.25);
|
||||
let (r0, a) = (4.0 / PI, 0.5 / (4.0 / PI));
|
||||
assert!((nx - (r0 - 0.25) * a.sin()).abs() < 1e-12 && (ny - (r0 - (r0 - 0.25) * a.cos())).abs() < 1e-12);
|
||||
}
|
||||
use vectorcraft_geom::shapes;
|
||||
|
||||
fn rect_node(x: f64, y: f64, w: f64, h: f64, c: Color) -> Arc<Node> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -22,10 +22,10 @@ pub fn warp(path: &PathData, b: Rect, style: WarpStyle, p: &Value) -> PathData {
|
||||
let f = |q: Point| {
|
||||
let (x, y) = ((q.x - c.x) / hw, (q.y - c.y) / hh);
|
||||
let (x2, y2) = if vertical {
|
||||
let (a, b2) = warp_point(style, bend, dh, dv, y, x);
|
||||
let (a, b2) = warp_point(style, bend, dh, dv, hw / hh, y, x);
|
||||
(b2, a)
|
||||
} else {
|
||||
warp_point(style, bend, dh, dv, x, y)
|
||||
warp_point(style, bend, dh, dv, hh / hw, x, y)
|
||||
};
|
||||
Point::new(c.x + x2 * hw, c.y + y2 * hh)
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), 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,76 @@ 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
|
||||
}
|
||||
|
||||
/// The gradient fill for a linear gradient the tracer found (in the image's pixels), placed in the
|
||||
/// document by `xf`.
|
||||
fn traced_gradient(g: &tr::TracedGradient, xf: vectorcraft_geom::Affine) -> Paint {
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
|
||||
let stops = g.stops.iter().map(|&(at, c)| GradientStop::new(at as f32, Color::rgb8(c[0], c[1], c[2]))).collect();
|
||||
let mut paint = GradientPaint::new(Gradient { kind: GradientKind::Linear, stops });
|
||||
paint.geom = Some(GradientGeom { start: xf * g.start, end: xf * g.end, aspect: 1.0, focal: None });
|
||||
Paint::Gradient(Box::new(paint))
|
||||
}
|
||||
|
||||
/// 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,12 +833,20 @@ 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);
|
||||
// Gradients: a path the tracer found to be a linear gradient is filled with it.
|
||||
let paint = tp.gradient.as_ref().map_or_else(|| Paint::solid(color), |g| traced_gradient(g, xf));
|
||||
n.appearance = match tp.stroke {
|
||||
// Traced lines: round ends and corners, as drawn lines have.
|
||||
Some(width) => Appearance {
|
||||
@@ -782,8 +860,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()) };
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,12 @@ fn accepted(doc: &'static str, depth: usize) -> Option<Vec<&'static str>> {
|
||||
Some(keys)
|
||||
}
|
||||
|
||||
/// Does the description `doc` name `key` as a parameter (its own keys, and those of the commands
|
||||
/// its `…` entries name)?
|
||||
pub(crate) fn takes_key(doc: &'static str, key: &str) -> bool {
|
||||
doc_keys(doc).is_ok_and(|parsed| parsed.keys.contains(&key)) || accepted(doc, 0).is_some_and(|keys| keys.contains(&key))
|
||||
}
|
||||
|
||||
/// The edits (insert, delete, replace, swap of neighbours) that turn `a` into `b`, ignoring case.
|
||||
fn distance(a: &str, b: &str) -> usize {
|
||||
let (a, b): (Vec<char>, Vec<char>) = (a.chars().map(|c| c.to_ascii_lowercase()).collect(), b.chars().map(|c| c.to_ascii_lowercase()).collect());
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -383,10 +383,15 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result<Value> {
|
||||
let mut styles: Vec<Style> = vec![];
|
||||
let mut shapes: Vec<po::Shape> = vec![];
|
||||
let mut names: Vec<Option<String>> = vec![];
|
||||
let mut push = |mut path: PathData, rule: FillRule, st: Style, name: Option<String>| {
|
||||
let mut push = |mut path: PathData, rule: FillRule, mut st: Style, name: Option<String>| {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Trim, Merge and Crop remove any strokes (and Merge then merges by fill alone), as the
|
||||
// public Pathfinder documentation describes.
|
||||
if matches!(op, PathfinderOp::Trim | PathfinderOp::Merge | PathfinderOp::Crop) {
|
||||
st.appearance.items.retain(|i| !matches!(i, AppearanceItem::Stroke(_)));
|
||||
}
|
||||
if op == PathfinderOp::Divide && st.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if !f.paint.is_none())) {
|
||||
// Filled open contours keep their implicit closing edge. A zero-area line
|
||||
// stays a cutter; normalised area also catches zero-signed-area self crossings.
|
||||
@@ -498,6 +503,7 @@ fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
let ids = s.edit("Offset Path", |d, sel| {
|
||||
let (mut out, mut above) = (vec![], vec![]);
|
||||
let mut paths = false;
|
||||
for id in &roots {
|
||||
let Some(n) = d.node(*id).cloned() else { continue };
|
||||
let mut lv = vec![];
|
||||
@@ -505,6 +511,7 @@ fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut made = vec![];
|
||||
for l in lv.iter().filter(|l| !matches!(l.kind, NodeKind::Text(_))) {
|
||||
let Some((path, rule)) = node_path(l) else { continue };
|
||||
paths = true;
|
||||
let base = if matches!(l.kind, NodeKind::Compound { .. }) && path.is_closed() { po::normalize(&path, rule) } else { path };
|
||||
let off = po::offset_path(&base, delta, join, miter);
|
||||
if off.is_empty() {
|
||||
@@ -527,6 +534,9 @@ fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
out.push(new.id);
|
||||
above.push((*id, vec![new]));
|
||||
}
|
||||
if out.is_empty() && paths {
|
||||
return Err(EngineError::Other("Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes)".into()));
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(EngineError::Other("Offset Path: select paths".into()));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -1295,6 +1300,17 @@ impl Session {
|
||||
true
|
||||
}
|
||||
|
||||
/// `spec`'s `enabled` with `ids` selected instead of the selection, which is then put back.
|
||||
fn enabled_with_selection(&mut self, spec: &CommandSpec, ids: Vec<NodeId>) -> std::result::Result<(), String> {
|
||||
let Some(d) = self.active_mut() else { return (spec.enabled)(self) };
|
||||
let old = std::mem::replace(&mut d.selection, Selection { objects: ids, ..Selection::default() });
|
||||
let r = (spec.enabled)(self);
|
||||
if let Some(d) = self.active_mut() {
|
||||
d.selection = old;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Execute a command by id. This is THE entry point for every frontend.
|
||||
pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> {
|
||||
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?;
|
||||
@@ -1304,7 +1320,15 @@ impl Session {
|
||||
cmd::params::check_params(id, params)?;
|
||||
// Clear's menu needs a selection; an agent supplying explicit targets only needs a
|
||||
// document. The command validates those targets before making any edit.
|
||||
let enabled = if id == "edit.clear" && params.get("ids").is_some() { cmd::has_doc(self) } else { (spec.enabled)(self) };
|
||||
let enabled = if id == "edit.clear" && params.get("ids").is_some() {
|
||||
cmd::has_doc(self)
|
||||
} else if let Some(ids) = given_ids(spec.params, params) {
|
||||
// Objects given as `ids` stand in for the selection: the command is enabled as it is
|
||||
// with them selected (#1575).
|
||||
self.enabled_with_selection(spec, ids)
|
||||
} else {
|
||||
(spec.enabled)(self)
|
||||
};
|
||||
if let Err(why) = enabled {
|
||||
return Err(EngineError::Disabled(id.to_string(), why));
|
||||
}
|
||||
@@ -1482,10 +1506,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
|
||||
@@ -1757,6 +1789,8 @@ mod tests_halftone;
|
||||
#[cfg(test)]
|
||||
mod tests_history_mem;
|
||||
#[cfg(test)]
|
||||
mod tests_ids_without_selection;
|
||||
#[cfg(test)]
|
||||
mod tests_inline;
|
||||
#[cfg(test)]
|
||||
mod tests_journal;
|
||||
@@ -1942,3 +1976,10 @@ mod tests_widthprofiles;
|
||||
mod tests_widthtool;
|
||||
#[cfg(test)]
|
||||
mod tests_xform;
|
||||
|
||||
/// The non-empty `ids` given to a command whose params description names `ids` as a key (the
|
||||
/// key callers outside the app may pass it, [`cmd::params::takes_key`]).
|
||||
fn given_ids(description: &'static str, params: &Value) -> Option<Vec<NodeId>> {
|
||||
let ids = cmd::ids_param(params, "ids").filter(|ids| !ids.is_empty())?;
|
||||
cmd::params::takes_key(description, "ids").then_some(ids)
|
||||
}
|
||||
@@ -671,6 +671,108 @@ 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);
|
||||
}
|
||||
|
||||
/// Gradients: an image that is one linear gradient is one path filled with it, the gradient's line
|
||||
/// where the image lies; without the option it is bands of flat colour, as saved documents have it.
|
||||
#[test]
|
||||
fn image_trace_gradients_fills_with_a_gradient() {
|
||||
let art = vectorcraft_trace::Raster::from_fn(100, 60, |x, _| {
|
||||
let t = (f64::from(x) + 0.5) / 100.0;
|
||||
[(20.0 + 200.0 * t) as u8, 40, (220.0 - 180.0 * t) as u8, 255]
|
||||
});
|
||||
let make = |gradients: bool| {
|
||||
let mut s = session();
|
||||
add_image(&mut s, &art, Affine::translate((50.0, 50.0)) * Affine::scale(2.0));
|
||||
let r = s.execute("imageTrace.make", &json!({"preset": "6 Colors", "params": {"gradients": gradients}})).unwrap();
|
||||
traced_paths(&node(&s, NodeId(r["id"].as_u64().unwrap())))
|
||||
};
|
||||
let paths = make(true);
|
||||
assert_eq!(paths.len(), 1);
|
||||
let Paint::Gradient(g) = paths[0].appearance.fill_paint() else { panic!("{:?}", paths[0].appearance.fill_paint()) };
|
||||
assert_eq!(g.gradient.kind, vectorcraft_color::GradientKind::Linear);
|
||||
assert_eq!(g.gradient.stops.len(), 2);
|
||||
// Level, from the image's left edge (x = 50) to its right one (x = 250), at any height.
|
||||
let geom = g.geom.as_ref().unwrap();
|
||||
let (a, b) = if geom.start.x < geom.end.x { (geom.start, geom.end) } else { (geom.end, geom.start) };
|
||||
assert!((a.x - 50.0).abs() < 3.0 && (b.x - 250.0).abs() < 3.0 && (a.y - b.y).abs() < 2.0, "{geom:?}");
|
||||
let flat = make(false);
|
||||
assert!(flat.len() > 1 && flat.iter().all(|n| matches!(n.appearance.fill_paint(), Paint::Solid { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_trace_release_and_expand() {
|
||||
let mut s = session();
|
||||
@@ -795,7 +897,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");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! #1575: commands that take `ids` work on the objects given, with nothing selected; the
|
||||
//! selection is left as it was.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn doc_with_rect() -> (Session, u64) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
(s, id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids_stand_in_for_the_selection() {
|
||||
for (cmd, params) in [
|
||||
("object.repeat.radial", json!({})),
|
||||
("edit.colors.invert", json!({})),
|
||||
("object.pattern.make", json!({"name": "P"})),
|
||||
("symbol.new", json!({"name": "S"})),
|
||||
("object.mesh.create", json!({})),
|
||||
("object.makePixelPerfect", json!({})),
|
||||
] {
|
||||
let (mut s, id) = doc_with_rect();
|
||||
let mut p = params;
|
||||
p["ids"] = json!([id]);
|
||||
let r = s.execute(cmd, &p);
|
||||
assert!(r.is_ok(), "{cmd}: {r:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_selection_is_put_back_and_the_check_still_applies() {
|
||||
let (mut s, id) = doc_with_rect();
|
||||
// Disabled even with the object given: Make with Top Object needs two.
|
||||
let r = s.execute("object.envelope.makeWithTopObject", &json!({"ids": [id]}));
|
||||
assert!(matches!(r, Err(EngineError::Disabled(..))), "{r:?}");
|
||||
assert!(s.doc().unwrap().selection.objects.is_empty(), "the stand-in selection is put back");
|
||||
// Without ids, nothing selected is still nothing selected.
|
||||
let r = s.execute("object.repeat.radial", &json!({}));
|
||||
assert!(matches!(r, Err(EngineError::Disabled(..))), "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_ids_key_counts() {
|
||||
let p = json!({"ids": [3]});
|
||||
assert_eq!(given_ids("{ids?: [id]}", &p), Some(vec![NodeId(3)]));
|
||||
assert_eq!(given_ids("{id?|ids?, name}", &p), Some(vec![NodeId(3)]));
|
||||
assert_eq!(given_ids("{grids: n, guides: bool}", &p), None);
|
||||
// A word in what the description says about another key isn't the key.
|
||||
assert_eq!(given_ids("{name: of the swatch the ids of the selection take}", &p), None);
|
||||
assert_eq!(given_ids("{ids?: [id]}", &json!({"ids": []})), None);
|
||||
}
|
||||
|
||||
/// Every command that names `ids` and is refused with nothing selected: given the object as `ids`
|
||||
/// it does what it does with the object selected (the same result or the same error, and the same
|
||||
/// document), so no command is let through to run on an empty selection.
|
||||
#[test]
|
||||
fn ids_do_what_the_selection_does_in_every_command_that_takes_them() {
|
||||
let mut checked = vec![];
|
||||
for spec in command_specs() {
|
||||
let (mut given, id) = doc_with_rect();
|
||||
if !cmd::params::takes_key(spec.params, "ids") || (spec.enabled)(&given).is_ok() {
|
||||
continue;
|
||||
}
|
||||
let (mut selected, _) = doc_with_rect();
|
||||
selected.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
let (a, b) = (given.execute(spec.id, &json!({"ids": [id]})), selected.execute(spec.id, &json!({})));
|
||||
match (&a, &b) {
|
||||
(Ok(_), Ok(_)) => {}
|
||||
(Err(x), Err(y)) => assert_eq!(x.to_string(), y.to_string(), "{}", spec.id),
|
||||
_ => panic!("{}: with ids {a:?}, with the selection {b:?}", spec.id),
|
||||
}
|
||||
match (given.doc(), selected.doc()) {
|
||||
(Ok(g), Ok(s)) => assert!(g.doc == s.doc, "{}: the documents differ", spec.id),
|
||||
(g, s) => assert_eq!(g.is_ok(), s.is_ok(), "{}", spec.id),
|
||||
}
|
||||
checked.push((spec.id, a.is_ok()));
|
||||
}
|
||||
assert!(checked.iter().filter(|c| c.1).count() >= 20, "{checked:?}");
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -893,3 +893,40 @@ fn divide_keeps_implicitly_closed_filled_paths_including_zero_signed_area() {
|
||||
assert!(pieces.iter().all(|n| n.appearance == source.appearance));
|
||||
}
|
||||
}
|
||||
|
||||
/// #1570: Trim, Merge and Crop remove the objects' strokes; Merge then merges same-coloured
|
||||
/// fills whatever their strokes were.
|
||||
#[test]
|
||||
fn trim_merge_and_crop_remove_strokes() {
|
||||
for op in ["trim", "merge", "crop"] {
|
||||
let mut s = session();
|
||||
let (a, b) = two_rects(&mut s);
|
||||
for (id, c) in [(a, Color::rgb(0.0, 0.0, 0.0)), (b, Color::rgb(1.0, 0.0, 0.0))] {
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(id).unwrap().appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::solid(c), 3.0).into();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let r = ids(&s.execute(&format!("object.pathfinder.{op}"), &json!({})).unwrap());
|
||||
let g = node(&s, r[0]);
|
||||
let pieces = g.children().unwrap();
|
||||
assert!(!pieces.is_empty(), "{op}");
|
||||
assert!(pieces.iter().all(|c| c.appearance.stroke_width() == 0.0 && c.appearance.fill().is_some()), "{op} keeps a stroke");
|
||||
if op == "merge" {
|
||||
assert_eq!(pieces.len(), 1, "same fill, different strokes: merged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #1590: an inset larger than the shape leaves nothing, and the error says so rather than asking
|
||||
/// for paths that are selected.
|
||||
#[test]
|
||||
fn offset_path_that_leaves_nothing_says_so() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
let e = s.execute("object.path.offsetPath", &json!({"offset": -20})).unwrap_err().to_string();
|
||||
assert!(e.contains("leaves nothing") && !e.contains("select paths"), "{e}");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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\
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -341,9 +341,11 @@ fn median_cut(colors: impl Iterator<Item = [u8; 3]>, k: usize, r: Reduction) ->
|
||||
let mut palette: Vec<[f32; 3]> = boxes.iter().map(|&(a, b, ..)| mean(&cells[a..b])).collect();
|
||||
// k-means: move each entry to the mean of the cells nearest to it.
|
||||
for _ in 0..3 {
|
||||
// Centres move between passes, not within one: build the spatial index once per pass.
|
||||
let index = PaletteIndex::new(&palette, w);
|
||||
let mut sums = vec![([0f64; 3], 0f64); palette.len()];
|
||||
for c in &cells {
|
||||
let i = nearest(&palette, c.mean, w);
|
||||
let i = index.nearest(c.mean);
|
||||
for ch in 0..3 {
|
||||
sums[i].0[ch] += c.mean[ch] as f64 * c.count as f64;
|
||||
}
|
||||
@@ -371,8 +373,137 @@ fn median_cut(colors: impl Iterator<Item = [u8; 3]>, k: usize, r: Reduction) ->
|
||||
out
|
||||
}
|
||||
|
||||
fn nearest(palette: &[[f32; 3]], c: [f32; 3], w: [f32; 3]) -> usize {
|
||||
palette.iter().enumerate().min_by(|a, b| dist(*a.1, c, w).total_cmp(&dist(*b.1, c, w))).map_or(0, |(i, _)| i)
|
||||
/// An exact 3-D nearest-neighbour index for the weighted RGB distance used by palette export.
|
||||
///
|
||||
/// Balanced at the weighted widest axis. Every subtree carries tight channel bounds, so a
|
||||
/// branch-and-bound search skips it when even its nearest *possible* colour is farther than the
|
||||
/// best one found. Equal distances still choose the earliest palette index (as a linear scan did).
|
||||
/// For very small palettes a scan is cheaper than traversing the tree.
|
||||
struct PaletteNode {
|
||||
index: usize,
|
||||
lo: [f32; 3],
|
||||
hi: [f32; 3],
|
||||
left: Option<usize>,
|
||||
right: Option<usize>,
|
||||
}
|
||||
|
||||
struct PaletteIndex<'a> {
|
||||
colors: &'a [[f32; 3]],
|
||||
weights: [f32; 3],
|
||||
nodes: Vec<PaletteNode>,
|
||||
root: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> PaletteIndex<'a> {
|
||||
const SCAN_CUTOFF: usize = 12;
|
||||
|
||||
fn new(colors: &'a [[f32; 3]], weights: [f32; 3]) -> Self {
|
||||
let mut nodes = if colors.len() <= Self::SCAN_CUTOFF { Vec::new() } else { Vec::with_capacity(colors.len()) };
|
||||
let root = if colors.len() <= Self::SCAN_CUTOFF {
|
||||
None
|
||||
} else {
|
||||
let mut indices: Vec<usize> = (0..colors.len()).collect();
|
||||
Self::build(colors, weights, &mut indices, &mut nodes)
|
||||
};
|
||||
Self { colors, weights, nodes, root }
|
||||
}
|
||||
|
||||
// Median partitioning takes O(k log k) time and space for k palette entries.
|
||||
fn build(colors: &[[f32; 3]], w: [f32; 3], ids: &mut [usize], nodes: &mut Vec<PaletteNode>) -> Option<usize> {
|
||||
if ids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (mut lo, mut hi) = ([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]);
|
||||
for &id in ids.iter() {
|
||||
let Some(c) = colors.get(id) else { continue };
|
||||
for axis in 0..3 {
|
||||
lo[axis] = lo[axis].min(c[axis]);
|
||||
hi[axis] = hi[axis].max(c[axis]);
|
||||
}
|
||||
}
|
||||
let axis = (0..3).max_by(|&a, &b| (w[a] * (hi[a] - lo[a])).total_cmp(&(w[b] * (hi[b] - lo[b])))).unwrap_or(0);
|
||||
let mid = ids.len() / 2;
|
||||
ids.select_nth_unstable_by(mid, |&a, &b| {
|
||||
let ac = colors.get(a).map_or(0.0, |c| c[axis]);
|
||||
let bc = colors.get(b).map_or(0.0, |c| c[axis]);
|
||||
ac.total_cmp(&bc).then(a.cmp(&b))
|
||||
});
|
||||
let (before, rest) = ids.split_at_mut(mid);
|
||||
let (pivot, after) = rest.split_first_mut()?;
|
||||
let at = nodes.len();
|
||||
nodes.push(PaletteNode { index: *pivot, lo, hi, left: None, right: None });
|
||||
let left = Self::build(colors, w, before, nodes);
|
||||
let right = Self::build(colors, w, after, nodes);
|
||||
if let Some(n) = nodes.get_mut(at) {
|
||||
n.left = left;
|
||||
n.right = right;
|
||||
}
|
||||
Some(at)
|
||||
}
|
||||
|
||||
// Lower bound on *the same f32 weighted squared distance* as `dist`: avoid transforming
|
||||
// coordinates, which would change rounding and potentially break identical-palette ties.
|
||||
fn lower_bound(&self, id: usize, c: [f32; 3]) -> f32 {
|
||||
let Some(n) = self.nodes.get(id) else { return f32::INFINITY };
|
||||
(0..3)
|
||||
.map(|i| {
|
||||
let gap = if c[i] < n.lo[i] {
|
||||
n.lo[i] - c[i]
|
||||
} else if c[i] > n.hi[i] {
|
||||
c[i] - n.hi[i]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(self.weights[i] * gap).powi(2)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn search(&self, at: usize, c: [f32; 3], best: &mut (f32, usize)) {
|
||||
let Some(n) = self.nodes.get(at) else { return };
|
||||
let Some(&p) = self.colors.get(n.index) else { return };
|
||||
let d = dist(p, c, self.weights);
|
||||
if d < best.0 || (d == best.0 && n.index < best.1) {
|
||||
*best = (d, n.index);
|
||||
}
|
||||
// Search the more promising box first, tightening the bound for the second box.
|
||||
let left = n.left.map(|id| (id, self.lower_bound(id, c)));
|
||||
let right = n.right.map(|id| (id, self.lower_bound(id, c)));
|
||||
match (left, right) {
|
||||
(Some(a), Some(b)) => {
|
||||
let (first, second) = if a.1 <= b.1 { (a, b) } else { (b, a) };
|
||||
if first.1 <= best.0 {
|
||||
self.search(first.0, c, best);
|
||||
}
|
||||
// `<=` is essential: an equal-distance colour may have an earlier index.
|
||||
if second.1 <= best.0 {
|
||||
self.search(second.0, c, best);
|
||||
}
|
||||
}
|
||||
(Some(a), None) | (None, Some(a)) => {
|
||||
if a.1 <= best.0 {
|
||||
self.search(a.0, c, best);
|
||||
}
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest(&self, c: [f32; 3]) -> usize {
|
||||
if self.colors.len() <= Self::SCAN_CUTOFF {
|
||||
return self
|
||||
.colors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|a, b| dist(*a.1, c, self.weights).total_cmp(&dist(*b.1, c, self.weights)))
|
||||
.map_or(0, |(i, _)| i);
|
||||
}
|
||||
let mut best = (f32::INFINITY, 0);
|
||||
if let Some(root) = self.root {
|
||||
self.search(root, c, &mut best);
|
||||
}
|
||||
best.1
|
||||
}
|
||||
}
|
||||
|
||||
/// The 8×8 ordered-dither threshold at (x, y), in 0..1.
|
||||
@@ -398,19 +529,20 @@ fn noise(x: usize, y: usize) -> f32 {
|
||||
/// names (transparent) get index 0 and take no diffused error.
|
||||
fn map(colors: &[[u8; 3]], width: usize, palette: &[[u8; 3]], w: [f32; 3], dither: Dither, amount: f32, skip: impl Fn(usize) -> bool) -> Vec<u8> {
|
||||
let pal: Vec<[f32; 3]> = palette.iter().map(|c| c.map(f32::from)).collect();
|
||||
let index = PaletteIndex::new(&pal, w);
|
||||
// Nearest entries by colour: exact colours without dithering, a 6-bit grid with it.
|
||||
let mut exact: HashMap<[u8; 3], u8> = HashMap::new();
|
||||
let mut grid = vec![u16::MAX; if dither == Dither::None { 0 } else { 1 << 18 }];
|
||||
let mut lookup = |c: [f32; 3]| -> u8 {
|
||||
let q = c.map(|v| v.round().clamp(0.0, 255.0) as u8);
|
||||
if grid.is_empty() {
|
||||
return *exact.entry(q).or_insert_with(|| nearest(&pal, c, w) as u8);
|
||||
return *exact.entry(q).or_insert_with(|| index.nearest(c) as u8);
|
||||
}
|
||||
let key = (q[0] as usize >> 2) << 12 | (q[1] as usize >> 2) << 6 | q[2] as usize >> 2;
|
||||
match grid.get(key) {
|
||||
Some(&v) if v != u16::MAX => v as u8,
|
||||
_ => {
|
||||
let i = nearest(&pal, c, w) as u8;
|
||||
let i = index.nearest(c) as u8;
|
||||
if let Some(g) = grid.get_mut(key) {
|
||||
*g = i as u16;
|
||||
}
|
||||
@@ -466,3 +598,83 @@ fn map(colors: &[[u8; 3]], width: usize, palette: &[[u8; 3]], w: [f32; 3], dithe
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod nearest_tests {
|
||||
use super::*;
|
||||
|
||||
// The original definition, retained only as a parity oracle for the spatial index.
|
||||
fn linear(colors: &[[f32; 3]], c: [f32; 3], w: [f32; 3]) -> usize {
|
||||
colors.iter().enumerate().min_by(|a, b| dist(*a.1, c, w).total_cmp(&dist(*b.1, c, w))).map_or(0, |(i, _)| i)
|
||||
}
|
||||
|
||||
fn next(seed: &mut u32) -> u32 {
|
||||
*seed ^= *seed << 13;
|
||||
*seed ^= *seed >> 17;
|
||||
*seed ^= *seed << 5;
|
||||
*seed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_nearest_matches_linear_for_palette_sizes_and_weights() {
|
||||
let mut seed = 0x2ab4_1017;
|
||||
for w in [[1.0; 3], [0.55, 0.77, 0.34], [0.1, 1.0, 0.01]] {
|
||||
for size in [0, 1, 2, 8, 12, 13, 16, 17, 32, 64, 128, 256] {
|
||||
// Include duplicated colours, close k-means-style floating-point centres,
|
||||
// and a wide colour gamut. Query points are fractional, as with dithering.
|
||||
let colors: Vec<[f32; 3]> =
|
||||
(0..size)
|
||||
.map(|i| {
|
||||
if i % 11 == 0 {
|
||||
[127.0, 127.0, 127.0]
|
||||
} else {
|
||||
[0, 1, 2].map(|_| (next(&mut seed) % 65_536) as f32 * (255.0 / 65_535.0))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let index = PaletteIndex::new(&colors, w);
|
||||
for _ in 0..500 {
|
||||
let c = [0, 1, 2].map(|_| (next(&mut seed) % 65_536) as f32 * (255.0 / 65_535.0));
|
||||
assert_eq!(index.nearest(c), linear(&colors, c, w), "size {size}, weights {w:?}, query {c:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_distance_always_chooses_first_palette_entry() {
|
||||
// Enough entries to exercise the tree, even when they share one bounding box.
|
||||
let mut colors = vec![[128.0, 128.0, 128.0]; 256];
|
||||
colors[0] = [0.0, 128.0, 128.0];
|
||||
colors[255] = [2.0, 128.0, 128.0];
|
||||
let index = PaletteIndex::new(&colors, [1.0; 3]);
|
||||
assert_eq!(index.nearest([1.0, 128.0, 128.0]), 0, "equal distance to indices 0 and 255");
|
||||
assert_eq!(index.nearest([128.0; 3]), 1, "equal among 254 identical colours");
|
||||
let colors = vec![[10.0, 10.0, 10.0]; 256];
|
||||
assert_eq!(PaletteIndex::new(&colors, [0.55, 0.77, 0.34]).nearest([10.0; 3]), 0);
|
||||
}
|
||||
|
||||
/// Opt-in microbenchmark: compares the same nearest-neighbour queries, without image IO.
|
||||
#[test]
|
||||
#[ignore = "run with cargo test -p vectorcraft-render nearest_palette_benchmark --release -- --ignored --nocapture"]
|
||||
fn nearest_palette_benchmark() {
|
||||
use std::time::Instant;
|
||||
|
||||
let mut seed = 0x87bb_7641;
|
||||
let colors: Vec<[f32; 3]> = (0..256).map(|_| [0, 1, 2].map(|_| (next(&mut seed) % 256) as f32)).collect();
|
||||
let queries: Vec<[f32; 3]> = (0..100_000).map(|_| [0, 1, 2].map(|_| (next(&mut seed) % 65_536) as f32 * (255.0 / 65_535.0))).collect();
|
||||
let w = [0.55, 0.77, 0.34];
|
||||
let start = Instant::now();
|
||||
let baseline: Vec<_> = queries.iter().map(|&c| linear(&colors, c, w)).collect();
|
||||
let old = start.elapsed();
|
||||
let start = Instant::now();
|
||||
let index = PaletteIndex::new(&colors, w);
|
||||
let improved: Vec<_> = queries.iter().map(|&c| index.nearest(c)).collect();
|
||||
let new = start.elapsed();
|
||||
assert_eq!(baseline, improved);
|
||||
eprintln!(
|
||||
"100k nearest-colour queries, 256 entries: linear {old:?}, spatial index {new:?}, speedup {:.2}x",
|
||||
old.as_secs_f64() / new.as_secs_f64()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -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 >", ""] {
|
||||
|
||||
@@ -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)")]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,8 @@ pub(super) fn prepare<'s>(svg: &'s str, xml: &roxmltree::Document, dpi: f64, war
|
||||
let cx = Ctx::new(xml, dpi);
|
||||
let mut out = String::new();
|
||||
let mut copied = 0;
|
||||
for t in xml.descendants().filter(|n| n.is_element() && n.tag_name().name() == "text" && n.tag_name().namespace() == Some(SVG_NS)) {
|
||||
let is_text = |n: &XNode| n.is_element() && matches!(n.tag_name().name(), "text" | "flowRoot") && n.tag_name().namespace() == Some(SVG_NS);
|
||||
for t in xml.descendants().filter(is_text) {
|
||||
let label = t.attribute("id").map_or_else(|| "a text".to_string(), |id| format!("text '{id}'"));
|
||||
if t.ancestors().skip(1).any(|a| a.is_element() && a.tag_name().name() == "clipPath") {
|
||||
warnings.push(format!("{label} in a clip path ignored"));
|
||||
@@ -191,11 +192,13 @@ pub(super) fn prepare<'s>(svg: &'s str, xml: &roxmltree::Document, dpi: f64, war
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(q) = qname.strip_suffix("text").filter(|q| q.is_empty() || q.ends_with(':')) else { continue };
|
||||
let flowed = t.tag_name().name() == "flowRoot";
|
||||
let Some(q) = qname.strip_suffix(t.tag_name().name()).filter(|q| q.is_empty() || q.ends_with(':')) else { continue };
|
||||
if cx.css.own(t, "display").as_deref() == Some("none") || r.start < copied {
|
||||
continue;
|
||||
}
|
||||
let Some((text, paints)) = cx.build(xml, t, &label, warnings) else { continue };
|
||||
let built = if flowed { cx.build_flow(t, &label, warnings) } else { cx.build(xml, t, &label, warnings) };
|
||||
let Some((text, paints)) = built else { continue };
|
||||
out.push_str(&svg[copied..r.start]);
|
||||
placeholder(&mut out, q, &cx.css, t, &slots.prefix, slots.texts.len(), text.marker, &paints);
|
||||
copied = r.end;
|
||||
@@ -560,6 +563,47 @@ impl Ctx {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read one Inkscape `<flowRoot>` (SVG 1.2 flowed text, which browsers don't draw but
|
||||
/// Inkscape writes) as area type: its frame the first `<rect>` of its `<flowRegion>`, a
|
||||
/// paragraph per `<flowPara>`, styled as `<text>` is. Other regions are left out with a warning.
|
||||
fn build_flow(&self, t: XNode, label: &str, warnings: &mut Vec<String>) -> Option<(PendingText, Vec<String>)> {
|
||||
let (vw, vh) = self.viewport;
|
||||
let region = t.children().find(|c| c.is_element() && c.tag_name().name() == "flowRegion");
|
||||
let rect = region.and_then(|r| r.children().find(|c| c.is_element() && c.tag_name().name() == "rect"));
|
||||
let size = |n: XNode, a: &str, pct: f64| n.attribute(a).and_then(|v| self.length(v, DEFAULT_FONT_SIZE, pct));
|
||||
let frame = rect.and_then(|r| {
|
||||
let (w, h) = (size(r, "width", vw)?, size(r, "height", vh)?);
|
||||
(w > 0.0 && h > 0.0).then(|| (r, Rect::new(0.0, 0.0, w, h)))
|
||||
});
|
||||
let Some((r, frame)) = frame else {
|
||||
warnings.push(format!("{label}: flowed text (flowRoot) without a rectangle region was left out"));
|
||||
return None;
|
||||
};
|
||||
let origin = Affine::translate((size(r, "x", vw).unwrap_or(0.0), size(r, "y", vh).unwrap_or(0.0)));
|
||||
let paras: Vec<XNode> = t.descendants().filter(|n| n.is_element() && n.tag_name().name() == "flowPara").collect();
|
||||
let collapse = |n: XNode| {
|
||||
n.descendants().filter(XNode::is_text).filter_map(|c| c.text()).collect::<String>().split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
};
|
||||
let text = paras.iter().map(|p| collapse(*p)).collect::<Vec<_>>().join("\n");
|
||||
if text.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let style = self.elem(paras.first().copied().unwrap_or(t)).style.clone();
|
||||
let mut obj = TextObject::point(Point::ZERO, &text, style);
|
||||
obj.kind = TextKind::Area { frame: vectorcraft_geom::shapes::rectangle(frame) };
|
||||
obj.xf = parse_transform(r.attribute("transform")) * origin;
|
||||
let servers = vec![(None, None); obj.runs.len()];
|
||||
let b = obj.xf.transform_rect_bbox(frame);
|
||||
// A size, place or transform past what numbers hold (`width="1e999"`) has no frame to set type in.
|
||||
if ![b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite()) {
|
||||
warnings.push(format!("{label}: flowed text (flowRoot) with a region too large to place was left out"));
|
||||
return None;
|
||||
}
|
||||
let marker = Rect::new(b.x0, b.y0, b.x1.max(b.x0 + 1.0), b.y1.max(b.y0 + 1.0));
|
||||
let name = t.attribute("id").unwrap_or("").to_string();
|
||||
Some((PendingText { name, obj, servers, paints: 0, non_scaling: false, under: None, marker }, Vec::new()))
|
||||
}
|
||||
|
||||
/// Read one `<text>`: the pending text and its `url(#…)` paints.
|
||||
fn build(&self, xml: &roxmltree::Document, t: XNode, label: &str, warnings: &mut Vec<String>) -> Option<(PendingText, Vec<String>)> {
|
||||
let preserve = t.ancestors().filter(|a| a.is_element()).find_map(|a| a.attribute((roxmltree::NS_XML_URI, "space"))) == Some("preserve");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -583,3 +583,49 @@ 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}");
|
||||
}
|
||||
|
||||
/// Inkscape's flowed text (`<flowRoot>`) becomes area type in its region's rectangle, a paragraph
|
||||
/// per `<flowPara>`; another kind of region is left out with a warning, not silently.
|
||||
#[test]
|
||||
fn flowed_text_becomes_area_type() {
|
||||
let (d, w) = import_with_report(&svg(
|
||||
r#"<flowRoot style="font-size:16px;font-family:sans-serif"><flowRegion><rect x="20" y="30" width="100" height="160"/></flowRegion><flowPara>This is flowed text</flowPara><flowPara>Second <flowSpan>one</flowSpan></flowPara></flowRoot>
|
||||
<flowRoot><flowRegion><path d="M0 0 L10 0 L5 10 Z"/></flowRegion><flowPara>Lost</flowPara></flowRoot>"#,
|
||||
))
|
||||
.unwrap();
|
||||
let t = only_text(&d);
|
||||
assert_eq!(t.plain_text(), "This is flowed text\nSecond one");
|
||||
let TextKind::Area { frame } = &t.kind else { panic!("{:?}", t.kind) };
|
||||
let b = t.xf.transform_rect_bbox(frame.bounds().unwrap());
|
||||
assert!(close(b.x0, 20.0, 1e-6) && close(b.y0, 30.0, 1e-6) && close(b.x1, 120.0, 1e-6) && close(b.y1, 190.0, 1e-6), "{b:?}");
|
||||
assert!(close(t.runs[0].style.size * t.xf.determinant().abs().sqrt(), 16.0, 1e-6));
|
||||
assert!(w.iter().any(|w| w.contains("flowRoot")), "{w:?}");
|
||||
}
|
||||
|
||||
/// Flowed text that can't be read is left out with a warning each, and never stops the import: no
|
||||
/// region, a region too large for numbers, a flowRoot inside a flowRoot (read once, as the outer one).
|
||||
#[test]
|
||||
fn flowed_text_that_cannot_be_read_is_reported() {
|
||||
let (d, w) = import_with_report(&svg(
|
||||
r#"<flowRoot id="none"><flowPara>No region</flowPara></flowRoot>
|
||||
<flowRoot id="huge"><flowRegion><rect width="1e999" height="10"/></flowRegion><flowPara>Huge</flowPara></flowRoot>
|
||||
<flowRoot id="far"><flowRegion><rect x="1e308" width="1e308" height="10" transform="scale(1e308)"/></flowRegion><flowPara>Far</flowPara></flowRoot>
|
||||
<flowRoot id="outer"><flowRegion><rect width="50" height="50"/></flowRegion><flowPara>Outer</flowPara><flowRoot><flowRegion><rect width="5" height="5"/></flowRegion><flowPara>inner</flowPara></flowRoot></flowRoot>"#,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(only_text(&d).plain_text(), "Outer\ninner");
|
||||
for id in ["none", "huge", "far"] {
|
||||
assert!(w.iter().any(|w| w.contains(id) && w.contains("flowRoot")), "{id}: {w:?}");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -10,7 +10,6 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
vectorcraft-geom = { workspace = true }
|
||||
vectorcraft-pathops = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
image = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
//! Gradients: areas whose colour changes evenly along one direction are found before the image is
|
||||
//! reduced to its palette, and each is traced as one path filled with a linear gradient instead of
|
||||
//! as bands of flat colour.
|
||||
//!
|
||||
//! An area is a connected run of pixels that each differ little from their neighbours. It is a
|
||||
//! gradient when a plane fits each of its colour channels with all three sloping the same way, the
|
||||
//! colours along that way are a few stops with even ramps between them, and painting those stops
|
||||
//! back gives the area's own pixels. Anything else (a flat area, a photo's sky lit two ways, two
|
||||
//! flats with a soft edge between them) is left to be traced as before.
|
||||
|
||||
use crate::Raster;
|
||||
use crate::quantize::TRANSPARENT;
|
||||
use kurbo::{Point, Vec2};
|
||||
|
||||
/// The largest step (levels of a channel) between neighbouring pixels of one smooth area.
|
||||
const STEP: i32 = 8;
|
||||
/// The fewest pixels a gradient has, and the least length (px) along its direction.
|
||||
const MIN_PIXELS: usize = 400;
|
||||
const MIN_LENGTH: f64 = 16.0;
|
||||
/// The least change (levels of a channel) from one stop to another: less is a flat colour.
|
||||
const MIN_CHANGE: f64 = 24.0;
|
||||
/// The most stops, and the least distance between two (as a share of the length): closer stops are
|
||||
/// an edge, not a ramp.
|
||||
const MAX_STOPS: usize = 6;
|
||||
const MIN_SPACING: f64 = 0.08;
|
||||
/// How far (levels) the stops may be from the colours along the gradient.
|
||||
const STOP_TOLERANCE: f64 = 2.0;
|
||||
|
||||
/// A linear gradient a traced path is filled with.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TracedGradient {
|
||||
/// The gradient's line, in pixel coordinates: the first stop's colour is at `start` and before
|
||||
/// it, the last one's at `end` and beyond.
|
||||
pub start: Point,
|
||||
pub end: Point,
|
||||
/// The stops: how far along the line (0 to 1, ascending, the first 0 and the last 1) and the colour.
|
||||
pub stops: Vec<(f64, [u8; 3])>,
|
||||
}
|
||||
|
||||
impl TracedGradient {
|
||||
/// The gradient's colour at `p`.
|
||||
pub fn color_at(&self, p: Point) -> [f64; 3] {
|
||||
let d = self.end - self.start;
|
||||
let t = if d.hypot2() > 0.0 { ((p - self.start).dot(d) / d.hypot2()).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let color = |i: usize| self.stops.get(i).map_or([0.0; 3], |s| s.1.map(f64::from));
|
||||
let after = self.stops.iter().position(|s| s.0 >= t).unwrap_or(self.stops.len().saturating_sub(1));
|
||||
let Some(before) = after.checked_sub(1) else { return color(after) };
|
||||
let (ta, tb) = (self.stops.get(before).map_or(0.0, |s| s.0), self.stops.get(after).map_or(1.0, |s| s.0));
|
||||
let k = if tb > ta { (t - ta) / (tb - ta) } else { 1.0 };
|
||||
let (a, b) = (color(before), color(after));
|
||||
[0, 1, 2].map(|c| a[c] + (b[c] - a[c]) * k)
|
||||
}
|
||||
}
|
||||
|
||||
/// The gradients of `img`: for each pixel the gradient it is part of (an index into the list, or
|
||||
/// `u32::MAX`), and the list.
|
||||
pub(crate) fn detect(img: &Raster) -> (Vec<u32>, Vec<TracedGradient>) {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
let px = |i: usize| img.rgba.get(i * 4..i * 4 + 4).filter(|p| p[3] >= 128).map(|p| [i32::from(p[0]), i32::from(p[1]), i32::from(p[2])]);
|
||||
let near = |a: [i32; 3], b: Option<[i32; 3]>| b.is_some_and(|b| (0..3).all(|k| (a[k] - b[k]).abs() <= STEP));
|
||||
// Smooth: an opaque pixel that differs little from each neighbour it has.
|
||||
let smooth: Vec<bool> = (0..w * h)
|
||||
.map(|i| {
|
||||
let (x, y) = (i % w, i / w);
|
||||
px(i).is_some_and(|c| {
|
||||
(x == 0 || near(c, px(i - 1)))
|
||||
&& (x + 1 == w || near(c, px(i + 1)))
|
||||
&& (y == 0 || near(c, px(i - w)))
|
||||
&& (y + 1 == h || near(c, px(i + w)))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// Sloping: smooth, and some pixel a little way off (a 32nd of the image: a gradient across a
|
||||
// large image changes by less than a level from one pixel to the next) is another colour.
|
||||
// Flat colour is no part of a gradient, and left out it does not join a gradient to the flat
|
||||
// area beside it where the two are nearly one colour.
|
||||
let far = (w.min(h) / 32).clamp(4, 64) as i64;
|
||||
let around = [
|
||||
(far, 0),
|
||||
(-far, 0),
|
||||
(0, far),
|
||||
(0, -far),
|
||||
(far * 3 / 4, far * 3 / 4),
|
||||
(far * 3 / 4, -far * 3 / 4),
|
||||
(-far * 3 / 4, far * 3 / 4),
|
||||
(-far * 3 / 4, -far * 3 / 4),
|
||||
];
|
||||
let sloping: Vec<bool> = (0..w * h)
|
||||
.map(|i| {
|
||||
let (x, y) = ((i % w) as i64, (i / w) as i64);
|
||||
let differs = |(dx, dy): (i64, i64)| {
|
||||
let (nx, ny) = (x + dx, y + dy);
|
||||
let other = (nx >= 0 && ny >= 0 && nx < w as i64 && ny < h as i64).then(|| px(ny as usize * w + nx as usize)).flatten();
|
||||
px(i).zip(other).is_some_and(|(a, b)| (0..3).any(|k| (a[k] - b[k]).abs() >= 2))
|
||||
};
|
||||
smooth.get(i).copied().unwrap_or(false) && around.into_iter().any(differs)
|
||||
})
|
||||
.collect();
|
||||
drop(smooth);
|
||||
let smooth = sloping;
|
||||
let mut of = vec![u32::MAX; w * h];
|
||||
let mut seen = vec![false; w * h];
|
||||
let mut depth = vec![0u16; w * h];
|
||||
let mut found = vec![];
|
||||
let (mut members, mut stack) = (vec![], vec![]);
|
||||
for seed in 0..w * h {
|
||||
if seen.get(seed).copied().unwrap_or(true) || !smooth.get(seed).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
// The connected smooth pixels from here.
|
||||
members.clear();
|
||||
stack.push(seed);
|
||||
if let Some(s) = seen.get_mut(seed) {
|
||||
*s = true;
|
||||
}
|
||||
while let Some(i) = stack.pop() {
|
||||
members.push(i);
|
||||
let (x, y) = (i % w, i / w);
|
||||
for j in [(x > 0).then(|| i - 1), (x + 1 < w).then(|| i + 1), (y > 0).then(|| i - w), (y + 1 < h).then(|| i + w)].into_iter().flatten() {
|
||||
if smooth.get(j).copied().unwrap_or(false) && !seen.get(j).copied().unwrap_or(true) {
|
||||
if let Some(s) = seen.get_mut(j) {
|
||||
*s = true;
|
||||
}
|
||||
stack.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if members.len() < MIN_PIXELS || found.len() >= 1000 {
|
||||
continue;
|
||||
}
|
||||
// The gradient is read from the area's inside, where there is one: the pixels at its rim
|
||||
// may be a soft edge to the next area, joined to this one where the two are alike.
|
||||
// (`depth`: how many pixels in from the rim each of the area's pixels is, counted inwards
|
||||
// from the pixels that have a neighbour outside it, as far as matters.)
|
||||
let rim = far as usize + 2;
|
||||
let unknown = u16::MAX;
|
||||
members.iter().for_each(|&i| depth[i] = unknown);
|
||||
let beside = |i: usize| {
|
||||
let (x, y) = (i % w, i / w);
|
||||
[(x > 0).then(|| i - 1), (x + 1 < w).then(|| i + 1), (y > 0).then(|| i - w), (y + 1 < h).then(|| i + w)].into_iter().flatten()
|
||||
};
|
||||
let mut front: Vec<usize> = members.iter().copied().filter(|&i| beside(i).any(|j| depth.get(j).copied() == Some(0))).collect();
|
||||
front.iter().for_each(|&i| depth[i] = 1);
|
||||
for d in 2..=rim as u16 {
|
||||
let mut next = vec![];
|
||||
for &i in &front {
|
||||
for j in beside(i) {
|
||||
if let Some(slot) = depth.get_mut(j).filter(|s| **s == unknown) {
|
||||
*slot = d;
|
||||
next.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
front = next;
|
||||
}
|
||||
let inside: Vec<usize> = members.iter().copied().filter(|&i| depth.get(i).copied() == Some(unknown)).collect();
|
||||
let fitted = fit(img, w, if inside.len() >= MIN_PIXELS { &inside } else { &members }, &members, rim);
|
||||
members.iter().for_each(|&i| depth[i] = 0);
|
||||
if let Some((g, part)) = fitted {
|
||||
for &i in &part {
|
||||
if let Some(o) = of.get_mut(i) {
|
||||
*o = found.len() as u32;
|
||||
}
|
||||
}
|
||||
found.push(g);
|
||||
}
|
||||
}
|
||||
(of, found)
|
||||
}
|
||||
|
||||
/// The linear gradient the pixels `members` of `img` (`w` wide) are, if they are one, and those of
|
||||
/// `all` (the whole area `members` are the inside of, `rim` pixels in) that have its colours.
|
||||
fn fit(img: &Raster, w: usize, members: &[usize], all: &[usize], rim: usize) -> Option<(TracedGradient, Vec<usize>)> {
|
||||
let n = members.len() as f64;
|
||||
let at = |i: usize| Point::new((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
|
||||
let rgb = |i: usize| img.rgba.get(i * 4..i * 4 + 3).map_or([0.0; 3], |p| [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]);
|
||||
let mut middle = Vec2::ZERO;
|
||||
let mut mean = [0.0; 3];
|
||||
for &i in members {
|
||||
middle += at(i).to_vec2();
|
||||
let c = rgb(i);
|
||||
(0..3).for_each(|k| mean[k] += c[k]);
|
||||
}
|
||||
let middle = (middle / n).to_point();
|
||||
let mean = mean.map(|v| v / n);
|
||||
// A plane through each channel, by least squares about the middle.
|
||||
let (mut sxx, mut sxy, mut syy) = (0.0, 0.0, 0.0);
|
||||
let (mut scx, mut scy) = ([0.0; 3], [0.0; 3]);
|
||||
for &i in members {
|
||||
let (d, c) = (at(i) - middle, rgb(i));
|
||||
sxx += d.x * d.x;
|
||||
sxy += d.x * d.y;
|
||||
syy += d.y * d.y;
|
||||
for k in 0..3 {
|
||||
scx[k] += d.x * (c[k] - mean[k]);
|
||||
scy[k] += d.y * (c[k] - mean[k]);
|
||||
}
|
||||
}
|
||||
let det = sxx * syy - sxy * sxy;
|
||||
if det <= 1e-6 * sxx * syy || !det.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Each channel's slope, and the one direction they share: the longer axis of the three
|
||||
// together (the shorter is what they do not share, and must be next to nothing).
|
||||
let slope = [0, 1, 2].map(|k| Vec2::new((scx[k] * syy - scy[k] * sxy) / det, (scy[k] * sxx - scx[k] * sxy) / det));
|
||||
let (mxx, mxy, myy) = slope.iter().fold((0.0, 0.0, 0.0), |m, s| (m.0 + s.x * s.x, m.1 + s.x * s.y, m.2 + s.y * s.y));
|
||||
let spread = ((mxx - myy) / 2.0).hypot(mxy);
|
||||
let (major, minor) = ((mxx + myy) / 2.0 + spread, (mxx + myy) / 2.0 - spread);
|
||||
if major <= 0.0 || minor > 0.03 * major {
|
||||
return None;
|
||||
}
|
||||
let angle = 0.5 * (2.0 * mxy).atan2(mxx - myy);
|
||||
let dir = Vec2::new(angle.cos(), angle.sin());
|
||||
// The colours along it, a pixel's width at a time.
|
||||
let along = |i: usize| (at(i) - middle).dot(dir);
|
||||
let (t0, t1) = members.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |m, &i| (m.0.min(along(i) - 0.5), m.1.max(along(i) + 0.5)));
|
||||
let length = t1 - t0;
|
||||
if !(MIN_LENGTH..1e6).contains(&length) {
|
||||
return None;
|
||||
}
|
||||
let bins = length.ceil() as usize;
|
||||
// (Each step's mean colour, at the mean place of its pixels: the few pixels at the tip of a
|
||||
// round area are not at the middle of their step.)
|
||||
let mut sums = vec![([0.0; 3], 0.0f64, 0.0f64); bins];
|
||||
for &i in members {
|
||||
let t = along(i) - t0;
|
||||
if let Some(b) = sums.get_mut((t as usize).min(bins - 1)) {
|
||||
let c = rgb(i);
|
||||
(0..3).for_each(|k| b.0[k] += c[k]);
|
||||
b.1 += 1.0;
|
||||
b.2 += t;
|
||||
}
|
||||
}
|
||||
let curve: Vec<(f64, [f64; 3])> = sums.iter().filter(|b| b.1 > 0.0).map(|b| (b.2 / b.1 / length, b.0.map(|v| v / b.1))).collect();
|
||||
// The fewest stops whose ramps pass within tolerance of every colour along the way: the two
|
||||
// ends, then again and again the colour farthest from the ramp it lies on.
|
||||
let mut keep = vec![0, curve.len().checked_sub(1)?];
|
||||
loop {
|
||||
let mut worst = (STOP_TOLERANCE, 0);
|
||||
for pair in keep.windows(2) {
|
||||
let (Some(&(ta, a)), Some(&(tb, b))) = (curve.get(pair[0]), curve.get(pair[1])) else { return None };
|
||||
for (j, &(t, c)) in curve.iter().enumerate().take(pair[1]).skip(pair[0] + 1) {
|
||||
let k = (t - ta) / (tb - ta);
|
||||
let off = (0..3).map(|ch| (c[ch] - (a[ch] + (b[ch] - a[ch]) * k)).abs()).fold(0.0, f64::max);
|
||||
if off > worst.0 {
|
||||
worst = (off, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if worst.1 == 0 {
|
||||
break;
|
||||
}
|
||||
// (More than are let stand: those close together become one below.)
|
||||
if keep.len() >= 4 * MAX_STOPS {
|
||||
return None;
|
||||
}
|
||||
keep.push(worst.1);
|
||||
keep.sort_unstable();
|
||||
}
|
||||
// Two stops close together are one stop where the ramp turns (a turn is not sharp to the
|
||||
// pixel, least of all in a blurred image), unless their colours are far apart: that is an
|
||||
// edge, not a ramp.
|
||||
let mut stops: Vec<(f64, [f64; 3])> = vec![];
|
||||
for &(t, c) in keep.iter().filter_map(|&j| curve.get(j)) {
|
||||
match stops.last_mut() {
|
||||
Some(last) if t - last.0 < MIN_SPACING => {
|
||||
if (0..3).any(|k| (c[k] - last.1[k]).abs() >= MIN_CHANGE) {
|
||||
return None;
|
||||
}
|
||||
*last = ((last.0 + t) / 2.0, [0, 1, 2].map(|k| (last.1[k] + c[k]) / 2.0));
|
||||
}
|
||||
_ => stops.push((t, c)),
|
||||
}
|
||||
}
|
||||
let (first, last) = (stops.first()?.0, stops.last()?.0);
|
||||
// A change of colour, not one colour.
|
||||
let change =
|
||||
stops.iter().flat_map(|a| stops.iter().map(move |b| (0..3).map(|k| (a.1[k] - b.1[k]).abs()).fold(0.0, f64::max))).fold(0.0, f64::max);
|
||||
// And ramps most of the way: two flat colours with a soft edge between them are no gradient.
|
||||
let level: f64 = stops.windows(2).filter(|s| (0..3).all(|k| (s[1].1[k] - s[0].1[k]).abs() < 4.0)).map(|s| s[1].0 - s[0].0).sum();
|
||||
if !(2..=MAX_STOPS).contains(&stops.len()) || last - first < MIN_SPACING || change < MIN_CHANGE || level > 0.3 * (last - first) {
|
||||
return None;
|
||||
}
|
||||
let gradient = TracedGradient {
|
||||
start: middle + dir * (t0 + first * length),
|
||||
end: middle + dir * (t0 + last * length),
|
||||
stops: stops.iter().map(|&(t, c)| ((t - first) / (last - first), c.map(|v| v.round().clamp(0.0, 255.0) as u8))).collect(),
|
||||
};
|
||||
// Carried on to the ends of the whole area, each end ramp going on as it goes.
|
||||
let d = gradient.end - gradient.start;
|
||||
let place = |i: usize| (at(i) - gradient.start).dot(d) / d.hypot2();
|
||||
let reach = rim as f64 * 1.5 / d.hypot();
|
||||
let (u0, u1) = all.iter().fold((0.0f64, 1.0f64), |m, &i| (m.0.min(place(i)), m.1.max(place(i))));
|
||||
let (u0, u1) = (u0.max(-reach), u1.min(1.0 + reach));
|
||||
let ramp = |a: (f64, [u8; 3]), b: (f64, [u8; 3]), u: f64| {
|
||||
[0, 1, 2].map(|k| (f64::from(a.1[k]) + (f64::from(b.1[k]) - f64::from(a.1[k])) * (u - a.0) / (b.0 - a.0)).round().clamp(0.0, 255.0) as u8)
|
||||
};
|
||||
let mut stops = gradient.stops.clone();
|
||||
if let (&[a, b, ..], &[.., y, z]) = (stops.as_slice(), stops.as_slice()) {
|
||||
if let Some(first) = stops.first_mut() {
|
||||
*first = (u0, ramp(a, b, u0));
|
||||
}
|
||||
if let Some(last) = stops.last_mut() {
|
||||
*last = (u1, ramp(y, z, u1));
|
||||
}
|
||||
}
|
||||
let gradient = TracedGradient {
|
||||
start: gradient.start + d * u0,
|
||||
end: gradient.start + d * u1,
|
||||
stops: stops.into_iter().map(|(u, c)| ((u - u0) / (u1 - u0), c)).collect(),
|
||||
};
|
||||
// Painted back, it is the area: most pixels of its inside within a few levels, and all that
|
||||
// are (the rest: noise, a soft rim, the flat colour just beyond it) on average nearer still.
|
||||
let off = |i: usize| {
|
||||
let (c, g) = (rgb(i), gradient.color_at(at(i)));
|
||||
(0..3).map(|k| (c[k] - g[k]).abs()).fold(0.0, f64::max)
|
||||
};
|
||||
let part: Vec<usize> = all.iter().copied().filter(|&i| off(i) <= 6.0).collect();
|
||||
let mean = part.iter().map(|&i| off(i)).sum::<f64>() / part.len().max(1) as f64;
|
||||
let inside = members.iter().filter(|&&i| off(i) <= 6.0).count();
|
||||
(part.len() >= MIN_PIXELS && inside * 4 >= members.len() * 3 && mean <= 3.5).then_some((gradient, part))
|
||||
}
|
||||
|
||||
/// Settles the pixels along the edge of each gradient (they are part gradient and part what lies
|
||||
/// beyond, and no part of the smooth area the gradient was found in): each joins the gradient
|
||||
/// (`of`) when more than half of it is the gradient's colour there, and else takes the label of
|
||||
/// what lies beyond. `labels` are the image's palette labels, the gradients' pixels not among them.
|
||||
pub(crate) fn settle_edges(img: &Raster, of: &mut [u32], gradients: &[TracedGradient], labels: &mut [u16]) {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
let rgb = |i: usize| img.rgba.get(i * 4..i * 4 + 4).filter(|p| p[3] >= 128).map(|p| [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]);
|
||||
// Twice: a soft edge is two pixels wide.
|
||||
for _ in 0..2 {
|
||||
let mut joins: Vec<(usize, u32)> = vec![];
|
||||
let mut takes: Vec<(usize, u16)> = vec![];
|
||||
for i in 0..w * h {
|
||||
if of.get(i).copied() != Some(u32::MAX) {
|
||||
continue;
|
||||
}
|
||||
let Some(c) = rgb(i) else { continue };
|
||||
let (x, y) = ((i % w) as i64, (i / w) as i64);
|
||||
let index = |x: i64, y: i64| (x >= 0 && y >= 0 && x < w as i64 && y < h as i64).then(|| y as usize * w + x as usize);
|
||||
// The gradient on one side, and what lies two pixels out on the other.
|
||||
let Some((g, dx, dy)) = [(1, 0), (-1, 0), (0, 1), (0, -1)].into_iter().find_map(|(dx, dy)| {
|
||||
let g = index(x + dx, y + dy).and_then(|j| of.get(j).copied()).filter(|&g| g != u32::MAX)?;
|
||||
Some((g, dx, dy))
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let Some(gradient) = gradients.get(g as usize) else { continue };
|
||||
let own = gradient.color_at(Point::new(x as f64 + 0.5, y as f64 + 0.5));
|
||||
let off = (0..3).map(|k| (c[k] - own[k]).abs()).fold(0.0, f64::max);
|
||||
let beyond = index(x - 2 * dx, y - 2 * dy).filter(|&j| of.get(j).copied() == Some(u32::MAX));
|
||||
let other = beyond.and_then(|j| Some((rgb(j)?, labels.get(j).copied()?)));
|
||||
match other {
|
||||
Some((o, label)) if (0..3).map(|k| (o[k] - own[k]).abs()).fold(0.0, f64::max) > 16.0 => {
|
||||
// How much of the pixel is the other colour.
|
||||
let d = [0, 1, 2].map(|k| o[k] - own[k]);
|
||||
let share = (0..3).map(|k| (c[k] - own[k]) * d[k]).sum::<f64>() / d.iter().map(|v| v * v).sum::<f64>();
|
||||
if share < 0.5 {
|
||||
joins.push((i, g));
|
||||
} else if label != TRANSPARENT {
|
||||
takes.push((i, label));
|
||||
}
|
||||
}
|
||||
// Gradient beyond too, or nothing to tell it from: part of it if it has its colour.
|
||||
_ if off <= 16.0 => joins.push((i, g)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for (i, g) in joins {
|
||||
if let Some(o) = of.get_mut(i) {
|
||||
*o = g;
|
||||
}
|
||||
}
|
||||
for (i, l) in takes {
|
||||
if let Some(slot) = labels.get_mut(i) {
|
||||
*slot = l;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ramp(w: u32, h: u32, f: impl Fn(f64, f64) -> [f64; 3]) -> Raster {
|
||||
Raster::from_fn(w, h, |x, y| {
|
||||
let c = f(f64::from(x) + 0.5, f64::from(y) + 0.5);
|
||||
[c[0].round() as u8, c[1].round() as u8, c[2].round() as u8, 255]
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linear_ramp_is_one_gradient_with_its_direction_and_ends() {
|
||||
// Dark blue to yellow, 30° down from level.
|
||||
let dir = Vec2::new(30f64.to_radians().cos(), 30f64.to_radians().sin());
|
||||
let reach = Vec2::new(120.0, 80.0).dot(dir);
|
||||
let img = ramp(120, 80, |x, y| {
|
||||
let t = Vec2::new(x, y).dot(dir) / reach;
|
||||
[20.0 + 220.0 * t, 40.0 + 180.0 * t, 160.0 - 140.0 * t]
|
||||
});
|
||||
let (of, found) = detect(&img);
|
||||
assert_eq!(found.len(), 1);
|
||||
assert!(of.iter().all(|&g| g == 0));
|
||||
let g = &found[0];
|
||||
let d = g.end - g.start;
|
||||
assert!((d.atan2().to_degrees() - 30.0).abs() < 1.0, "{d:?}");
|
||||
assert_eq!(g.stops.len(), 2);
|
||||
for (stop, truth) in g.stops.iter().zip([[20.0, 40.0, 160.0], [240.0, 220.0, 20.0]]) {
|
||||
assert!((0..3).all(|k| (f64::from(stop.1[k]) - truth[k]).abs() < 6.0), "{:?}", g.stops);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_colour_soft_edges_and_two_way_shading_are_no_gradient() {
|
||||
// One colour.
|
||||
assert!(detect(&ramp(64, 64, |_, _| [200.0, 30.0, 30.0])).1.is_empty());
|
||||
// Two flat colours with a soft edge between them.
|
||||
let soft = ramp(96, 64, |x, _| [100.0 + 30.0 / (1.0 + (-(x - 48.0)).exp()); 3]);
|
||||
assert!(detect(&soft).1.is_empty());
|
||||
// Red changing across and green changing down.
|
||||
assert!(detect(&ramp(96, 96, |x, y| [40.0 + 2.0 * x, 40.0 + 2.0 * y, 90.0])).1.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_stops_are_three_stops() {
|
||||
let img = ramp(200, 40, |x, _| {
|
||||
let t = x / 200.0;
|
||||
if t < 0.5 { [255.0 - 400.0 * t, 40.0, 40.0 + 300.0 * t] } else { [55.0 + 300.0 * (t - 0.5), 40.0 + 400.0 * (t - 0.5), 190.0] }
|
||||
});
|
||||
let (_, found) = detect(&img);
|
||||
assert_eq!(found.len(), 1);
|
||||
assert_eq!(found[0].stops.len(), 3, "{:?}", found[0].stops);
|
||||
assert!((found[0].stops[1].0 - 0.5).abs() < 0.03);
|
||||
}
|
||||
}
|
||||
@@ -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, gradient: 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)]);
|
||||
}
|
||||
}
|
||||
+127
-40
@@ -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,12 @@
|
||||
mod centerline;
|
||||
mod contour;
|
||||
mod fit;
|
||||
mod gradient;
|
||||
mod group;
|
||||
mod logo;
|
||||
mod mosaic;
|
||||
mod quantize;
|
||||
mod shapes;
|
||||
mod smooth;
|
||||
mod subpixel;
|
||||
|
||||
@@ -56,8 +60,10 @@ use serde::{Deserialize, Serialize};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
pub use contour::{Component, Loop, trace_mask};
|
||||
pub use gradient::TracedGradient;
|
||||
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 +275,23 @@ 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,
|
||||
/// Gradients (Grayscale and Color): an area whose colour changes evenly along one direction
|
||||
/// is one path filled with a linear gradient ([`TracedPath::gradient`]), not bands of colour.
|
||||
pub gradients: bool,
|
||||
/// Create: trace areas as filled shapes.
|
||||
pub fills: bool,
|
||||
/// Create: trace lines no wider than `stroke_width` as stroked centre lines.
|
||||
@@ -302,7 +320,11 @@ 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,
|
||||
gradients: false,
|
||||
fills: true,
|
||||
strokes: false,
|
||||
stroke_width: 10.0,
|
||||
@@ -312,20 +334,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 +352,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 +373,7 @@ pub const PRESET_NAMES: &[&str] = &[
|
||||
"Silhouettes",
|
||||
"Line Art",
|
||||
"Technical Drawing",
|
||||
"Outline",
|
||||
"Flat Logo",
|
||||
];
|
||||
|
||||
@@ -392,6 +402,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 +417,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 +442,10 @@ 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>,
|
||||
/// Gradients: the linear gradient the path is filled with (`color` is its middle colour).
|
||||
pub gradient: Option<TracedGradient>,
|
||||
}
|
||||
|
||||
/// The traced result, bottom-most path first.
|
||||
@@ -434,6 +454,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 +469,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,13 +492,54 @@ 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 ¶ms.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);
|
||||
let mut q = quantize(img, params);
|
||||
// Gradients are found first and kept out of the palette: no colour is spent on bands of them.
|
||||
let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(img)).filter(|f| !f.1.is_empty());
|
||||
let without = found.as_ref().map(|(of, _)| {
|
||||
let mut rest = img.clone();
|
||||
for (p, &g) in rest.rgba.as_chunks_mut::<4>().0.iter_mut().zip(of) {
|
||||
if g != u32::MAX {
|
||||
p[3] = 0;
|
||||
}
|
||||
}
|
||||
rest
|
||||
});
|
||||
let mut q = quantize(without.as_ref().unwrap_or(img), params);
|
||||
drop(without);
|
||||
// Each gradient is a palette entry of its own (its colour: the gradient's middle).
|
||||
let mut gradients: Vec<Option<TracedGradient>> = vec![None; q.palette.len()];
|
||||
if let Some((mut of, found)) = found {
|
||||
gradient::settle_edges(img, &mut of, &found, &mut q.labels);
|
||||
let first = q.palette.len();
|
||||
for (l, &g) in q.labels.iter_mut().zip(&of) {
|
||||
if g != u32::MAX {
|
||||
*l = u16::try_from(first + g as usize).unwrap_or(TRANSPARENT);
|
||||
}
|
||||
}
|
||||
q.palette.extend(found.iter().map(|g| g.color_at(g.start.midpoint(g.end)).map(|v| v.round().clamp(0.0, 255.0) as u8)));
|
||||
gradients.extend(found.into_iter().map(Some));
|
||||
}
|
||||
denoise(&mut q.labels, w, h, params.noise as usize);
|
||||
// Pixel count per palette entry, and the box its pixels lie in.
|
||||
let mut counts = vec![0usize; q.palette.len()];
|
||||
@@ -500,21 +572,17 @@ 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,
|
||||
gradients: &gradients,
|
||||
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 +639,16 @@ 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)>>,
|
||||
/// Gradients: the gradient each palette entry is, if it is one.
|
||||
gradients: &'a [Option<TracedGradient>],
|
||||
/// 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 +660,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.
|
||||
@@ -672,8 +742,15 @@ impl Layers<'_> {
|
||||
let tone = |l: u16| [if l == 1 { black } else { white }; 3];
|
||||
(tone(la), tone(lb))
|
||||
} else {
|
||||
let (Some(ca), Some(cb)) = (self.q.palette.get(la as usize), self.q.palette.get(lb as usize)) else { return 0.0 };
|
||||
(ca.map(f64::from), cb.map(f64::from))
|
||||
// A side's own colour: its palette entry's, or its gradient's colour here.
|
||||
let own = |l: u16, k: i32| {
|
||||
let flat = self.q.palette.get(l as usize)?.map(f64::from);
|
||||
let Some(g) = self.gradients.get(l as usize).and_then(Option::as_ref) else { return Some(flat) };
|
||||
let i = c.at(k)?;
|
||||
Some(g.color_at(vectorcraft_geom::Point::new((i % self.w) as f64 + 0.5, (i / self.w) as f64 + 0.5)))
|
||||
};
|
||||
let (Some(ca), Some(cb)) = (own(la, 0), own(lb, 1)) else { return 0.0 };
|
||||
(ca, cb)
|
||||
};
|
||||
let d = [0, 1, 2].map(|k| cb[k] - ca[k]);
|
||||
let len2: f64 = d.iter().map(|v| v * v).sum();
|
||||
@@ -736,13 +813,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 +846,14 @@ 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 });
|
||||
// (A gradient fills; a stroked line of it is its middle colour.)
|
||||
let gradient = self.gradients.get(ci).cloned().flatten().filter(|_| stroke.is_none());
|
||||
paths.push(TracedPath { path, color, pixels, stroke, shape, gradient });
|
||||
true
|
||||
};
|
||||
let mut comps = trace_mask(&mask, bw, bh);
|
||||
@@ -794,12 +875,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,14 @@ 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,
|
||||
gradient: None,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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, ¶ms).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(¶ms).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}");
|
||||
}
|
||||
@@ -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,20 @@ 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,
|
||||
/// Linear gradients whose line and stops are known: one over the whole image, one in a disc.
|
||||
Ramp,
|
||||
}
|
||||
|
||||
pub const CLASSES: [Class; 5] = [Class::Shape, Class::Poly, Class::Lines, Class::Flat, Class::Tone];
|
||||
pub const CLASSES: [Class; 8] = [Class::Shape, Class::Poly, Class::Lines, Class::Askew, Class::Plain, Class::Flat, Class::Tone, Class::Ramp];
|
||||
|
||||
impl Class {
|
||||
pub fn name(self) -> &'static str {
|
||||
@@ -29,8 +36,11 @@ 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",
|
||||
Class::Ramp => "linear gradients",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,9 +88,69 @@ 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)>,
|
||||
/// The linear gradient it is, where it is one.
|
||||
pub ramp: Option<Ramp>,
|
||||
tone: Option<Tone>,
|
||||
}
|
||||
|
||||
/// A linear gradient: its line (256-unit coordinates) and its stops along it (0 to 1).
|
||||
#[derive(Clone)]
|
||||
pub struct Ramp {
|
||||
pub start: Point,
|
||||
pub end: Point,
|
||||
pub stops: Vec<(f64, [f64; 3])>,
|
||||
}
|
||||
|
||||
impl Ramp {
|
||||
pub fn color_at(&self, p: Point) -> [f64; 3] {
|
||||
let d = self.end - self.start;
|
||||
let t = ((p - self.start).dot(d) / d.hypot2()).clamp(0.0, 1.0);
|
||||
let after = self.stops.iter().position(|s| s.0 >= t).unwrap().max(1);
|
||||
let (a, b) = (self.stops[after - 1], self.stops[after]);
|
||||
let k = (t - a.0) / (b.0 - a.0);
|
||||
[0, 1, 2].map(|c| a.1[c] + (b.1[c] - a.1[c]) * k)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dark blue to yellow across the whole image, 25° down from level.
|
||||
fn two_stop_ramp() -> Ramp {
|
||||
let dir = kurbo::Vec2::from_angle(25f64.to_radians());
|
||||
let reach = kurbo::Vec2::new(128.0, 128.0).dot(dir);
|
||||
let centre = Point::new(128.0, 128.0);
|
||||
Ramp { start: centre - dir * reach, end: centre + dir * reach, stops: vec![(0.0, [24.0, 40.0, 150.0]), (1.0, [250.0, 215.0, 60.0])] }
|
||||
}
|
||||
|
||||
/// Red through white to green across a disc, 110° from level.
|
||||
fn three_stop_ramp() -> Ramp {
|
||||
let dir = kurbo::Vec2::from_angle(110f64.to_radians());
|
||||
let centre = Point::new(128.0, 128.0);
|
||||
Ramp {
|
||||
start: centre - dir * DISC,
|
||||
end: centre + dir * DISC,
|
||||
stops: vec![(0.0, [200.0, 30.0, 40.0]), (0.4, [245.0, 240.0, 230.0]), (1.0, [20.0, 120.0, 70.0])],
|
||||
}
|
||||
}
|
||||
|
||||
/// The radius of the disc the three-stop gradient fills.
|
||||
const DISC: f64 = 90.0;
|
||||
|
||||
fn two_stop(u: f64, v: f64, _: u32, _: u32) -> [f64; 3] {
|
||||
two_stop_ramp().color_at(Point::new(u * 256.0, v * 256.0))
|
||||
}
|
||||
|
||||
fn three_stop(u: f64, v: f64, x: u32, _: u32) -> [f64; 3] {
|
||||
let p = Point::new(u * 256.0, v * 256.0);
|
||||
let inside = three_stop_ramp().color_at(p);
|
||||
// The disc's edge, anti-aliased: how much of the pixel (256 / size units wide) is in it.
|
||||
let pixel = 256.0 * u / (f64::from(x) + 0.5);
|
||||
let cover = (0.5 + (DISC - p.distance(Point::new(128.0, 128.0))) / pixel).clamp(0.0, 1.0);
|
||||
[0, 1, 2].map(|c| 255.0 + (inside[c] - 255.0) * cover)
|
||||
}
|
||||
|
||||
const WHITE: [u8; 3] = [255, 255, 255];
|
||||
const INK: [u8; 3] = [0, 0, 0];
|
||||
|
||||
@@ -166,9 +236,23 @@ pub fn inputs() -> Vec<Input> {
|
||||
bg: WHITE,
|
||||
anchors,
|
||||
corners,
|
||||
strokes: vec![],
|
||||
plain: vec![],
|
||||
ramp: None,
|
||||
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![],
|
||||
ramp: None,
|
||||
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 +292,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 +347,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 +356,22 @@ 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),
|
||||
]
|
||||
Input { class: Class::Ramp, ramp: Some(two_stop_ramp()), ..tone("two-stop gradient", two_stop) },
|
||||
Input { class: Class::Ramp, ramp: Some(three_stop_ramp()), ..tone("three-stop gradient in a disc", three_stop) },
|
||||
];
|
||||
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 +401,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()
|
||||
}
|
||||
|
||||
@@ -65,14 +65,20 @@ 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],
|
||||
name if name.ends_with("Gradients") => &[Class::Ramp],
|
||||
"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 +140,16 @@ 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,
|
||||
gradient: 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 +185,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 % | gradient paths | found % | angle off | ramp off |\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(26));
|
||||
}
|
||||
let cells = [
|
||||
stat(&m, |m| m.delta, false, 2),
|
||||
@@ -189,6 +202,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 +211,15 @@ 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),
|
||||
stat(&m, |m| m.gradients, false, 1),
|
||||
stat(&m, |m| m.ramp_found, false, 0),
|
||||
stat(&m, |m| m.ramp_angle, true, 1),
|
||||
stat(&m, |m| m.ramp_off, true, 0),
|
||||
];
|
||||
format!("| {} |", cells.join(" | "))
|
||||
}
|
||||
@@ -220,10 +243,30 @@ 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() }));
|
||||
}
|
||||
// Gradients: on the inputs that are linear gradients, with the option and without it.
|
||||
for (name, base, gradients) in [
|
||||
("6 Colors, no Gradients", "6 Colors", false),
|
||||
("6 Colors with Gradients", "6 Colors", true),
|
||||
("High Fidelity Photo, no Gradients", "High Fidelity Photo", false),
|
||||
("High Fidelity Photo with Gradients", "High Fidelity Photo", true),
|
||||
] {
|
||||
presets.push((name, TraceParams { gradients, ..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 +313,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));
|
||||
@@ -304,6 +347,15 @@ fn report() -> String {
|
||||
fn quality_report() {
|
||||
let md = report();
|
||||
println!("{md}");
|
||||
// Gradients finds none in art that is flat colour, however it is degraded.
|
||||
let inputs = inputs();
|
||||
for (name, _) in presets().iter().filter(|(_, p)| p.mode != vectorcraft_trace::Mode::BlackAndWhite && p.mode != vectorcraft_trace::Mode::Logo) {
|
||||
let params = TraceParams { gradients: true, ..vectorcraft_trace::preset(name).unwrap() };
|
||||
for (input, size, variant, img) in draw_all(&inputs).iter().filter(|d| !matches!(d.0.class, Class::Tone | Class::Ramp)) {
|
||||
let found = trace_within(img, ¶ms, MAX_ANCHORS).map_or(0, |res| res.paths.iter().filter(|p| p.gradient.is_some()).count());
|
||||
assert_eq!(found, 0, "{name} with Gradients on {} at {size} px, {variant:?}", input.name);
|
||||
}
|
||||
}
|
||||
if let Ok(path) = std::env::var("TRACE_QUALITY_REPORT") {
|
||||
std::fs::write(path, md).unwrap();
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ use std::collections::HashMap;
|
||||
|
||||
use kurbo::{BezPath, ParamCurveArclen, Point, Shape, Stroke, StrokeOpts};
|
||||
use vectorcraft_geom::SubPath;
|
||||
use vectorcraft_trace::{Method, Mode, Raster, TraceParams, TraceResult, TracedPath};
|
||||
use vectorcraft_trace::{Method, Mode, Raster, TraceParams, TraceResult, TracedGradient, TracedPath};
|
||||
|
||||
use crate::inputs::{Class, Input, Variant};
|
||||
use crate::raster::{coverage_into, polylines, render};
|
||||
use crate::raster::{coverage_into, polylines, render_painted};
|
||||
|
||||
/// sRGB to Oklab (Björn Ottosson, "A perceptual color space for image processing", 2020).
|
||||
pub fn oklab(c: [u8; 3]) -> [f64; 3] {
|
||||
@@ -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,79 @@ 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,
|
||||
/// Gradients: paths filled with one; and where the input is a known gradient, whether one was
|
||||
/// found (%), the angle (°) between its line and the largest traced gradient's, and the largest
|
||||
/// difference (levels of a channel) between their colours along the true line.
|
||||
pub gradients: f64,
|
||||
pub ramp_found: f64,
|
||||
pub ramp_angle: f64,
|
||||
pub ramp_off: 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], Option<&TracedGradient>)> =
|
||||
draw_order(res).into_iter().map(|i| (shapes[i].0.clone(), shapes[i].1, res.paths[i].gradient.as_ref())).collect();
|
||||
let (delta_mean, edge_delta) = fidelity(src, &render_painted(&drawn, [255; 3], n, n));
|
||||
// Gradients: how many paths have one; and the largest of them against the gradient the input
|
||||
// is: the angle between their lines, and the largest difference of a channel between their
|
||||
// colours at 17 points along the true line.
|
||||
let gradients = res.paths.iter().filter(|tp| tp.gradient.is_some()).count() as f64;
|
||||
let (mut ramp_found, mut ramp_angle, mut ramp_off) = (f64::NAN, f64::NAN, f64::NAN);
|
||||
if let Some(ramp) = &input.ramp {
|
||||
let k = f64::from(size) / 256.0;
|
||||
let found = res.paths.iter().filter(|tp| tp.gradient.is_some()).max_by_key(|tp| tp.pixels).and_then(|tp| tp.gradient.as_ref());
|
||||
ramp_found = if found.is_some() { 100.0 } else { 0.0 };
|
||||
if let Some(g) = found {
|
||||
let (truth, own) = (ramp.end - ramp.start, g.end - g.start);
|
||||
let between = (truth.atan2() - own.atan2()).to_degrees().rem_euclid(180.0);
|
||||
ramp_angle = between.min(180.0 - between);
|
||||
ramp_off = (0..=16)
|
||||
.map(|j| {
|
||||
let p = ramp.start + truth * (f64::from(j) / 16.0);
|
||||
// (The true colour as this variant shows it: a scan's are its own.)
|
||||
let a = variant.color(ramp.color_at(p).map(|v| v.round() as u8)).map(f64::from);
|
||||
let b = g.color_at((p.to_vec2() * k).to_point());
|
||||
(0..3).map(|c| (a[c] - b[c]).abs()).fold(0.0, f64::max)
|
||||
})
|
||||
.fold(0.0, f64::max);
|
||||
}
|
||||
}
|
||||
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| {
|
||||
@@ -207,7 +271,7 @@ pub fn measure(input: &Input, variant: Variant, size: u32, src: &Raster, params:
|
||||
|
||||
let nan = f64::NAN;
|
||||
let (mut geo_mean, mut geo_max, mut miss_max, mut corners, mut kinks, mut slivers, mut sliver_area) = (nan, nan, nan, nan, nan, nan, nan);
|
||||
if input.class != Class::Tone {
|
||||
if !matches!(input.class, Class::Tone | Class::Ramp) {
|
||||
let truth_shapes = input.scaled(size);
|
||||
let truth = Outline::new(truth_shapes.iter().map(|(p, _)| p), None);
|
||||
let traced = Outline::new(shapes.iter().map(|(p, _)| p), Some(f64::from(size)));
|
||||
@@ -224,7 +288,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 +298,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 +307,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 +332,55 @@ 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,
|
||||
gradients,
|
||||
ramp_found,
|
||||
ramp_angle,
|
||||
ramp_off,
|
||||
shapes: shapes_found,
|
||||
centre_off,
|
||||
width_off,
|
||||
stroke_anchors,
|
||||
delta: delta_mean,
|
||||
edge_delta,
|
||||
geo_mean,
|
||||
@@ -275,6 +390,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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! from ground-truth vector shapes here, never loaded from files.
|
||||
|
||||
use kurbo::{BezPath, PathEl, Point};
|
||||
use vectorcraft_trace::Raster;
|
||||
use vectorcraft_trace::{Raster, TracedGradient};
|
||||
|
||||
/// The closed polylines of `path` (one per subpath), flattened within `tol`.
|
||||
pub fn polylines(path: &BezPath, tol: f64) -> Vec<Vec<Point>> {
|
||||
@@ -78,15 +78,25 @@ pub fn coverage(path: &BezPath, w: usize, h: usize) -> Vec<f32> {
|
||||
|
||||
/// Paints `scene` (bottom first) over `bg`, anti-aliased, the way a renderer shows a trace.
|
||||
pub fn render(scene: &[(BezPath, [u8; 3])], bg: [u8; 3], w: usize, h: usize) -> Raster {
|
||||
let scene: Vec<(BezPath, [u8; 3], Option<&TracedGradient>)> = scene.iter().map(|(p, c)| (p.clone(), *c, None)).collect();
|
||||
render_painted(&scene, bg, w, h)
|
||||
}
|
||||
|
||||
/// [`render`], with the paths that have a gradient filled with it.
|
||||
pub fn render_painted(scene: &[(BezPath, [u8; 3], Option<&TracedGradient>)], bg: [u8; 3], w: usize, h: usize) -> Raster {
|
||||
let mut buf = vec![bg.map(f32::from); w * h];
|
||||
let mut cov = vec![0.0f32; w * h];
|
||||
for (path, color) in scene {
|
||||
for (path, color, gradient) in scene {
|
||||
let (r0, r1) = coverage_into(path, w, h, &mut cov);
|
||||
for i in r0 * w..r1 * w {
|
||||
let a = cov[i].min(1.0);
|
||||
if a > 0.0 {
|
||||
let color = match gradient {
|
||||
Some(g) => g.color_at(Point::new((i % w) as f64 + 0.5, (i / w) as f64 + 0.5)).map(|v| v as f32),
|
||||
None => color.map(f32::from),
|
||||
};
|
||||
for ch in 0..3 {
|
||||
buf[i][ch] += (f32::from(color[ch]) - buf[i][ch]) * a;
|
||||
buf[i][ch] += (color[ch] - buf[i][ch]) * a;
|
||||
}
|
||||
cov[i] = 0.0;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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ů?
|
||||
@@ -3734,6 +3740,7 @@ effect Regular Pravidelný
|
||||
@msg No saved selections (Select → Save Selection…) Žádné uložené výběry (Výběr → Uložit výběr…)
|
||||
@msg No saved views (View → New View…) Žádná uložená zobrazení (Zobrazení → Nové zobrazení…)
|
||||
@msg Offset Path: select paths Odsadit cestu: vyberte cesty
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Odsadit cestu: po odsazení z vybraných cest nic nezbude (odsazení dovnitř větší než tvary)
|
||||
@msg Opened with {_1} note(s): {_2} Otevřeno, poznámky ({_1}): {_2}
|
||||
@msg Opened {url} Otevřeno {url}
|
||||
@msg Outline Stroke: no stroked paths selected Převést tah na obrys: nejsou vybrány žádné cesty s tahem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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?
|
||||
@@ -3335,6 +3341,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg No saved selections (Select → Save Selection…) Keine gespeicherten Auswahlen (Auswahl → Auswahl speichern…)
|
||||
@msg No saved views (View → New View…) Keine gespeicherten Ansichten (Ansicht → Neue Ansicht…)
|
||||
@msg Offset Path: select paths Pfad verschieben: Wählen Sie Pfade aus
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Pfad verschieben: Der Versatz lässt von den ausgewählten Pfaden nichts übrig (ein Versatz nach innen, der größer ist als die Formen)
|
||||
@msg Opened with {_1} note(s): {_2} Mit {_1} Hinweis(en) geöffnet: {_2}
|
||||
@msg Opened {url} {url} geöffnet
|
||||
@msg Outline Stroke: no stroked paths selected Konturlinie: keine Pfade mit Kontur ausgewählt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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?
|
||||
@@ -3435,6 +3441,7 @@ axis Both Ambos
|
||||
@msg No saved selections (Select → Save Selection…) No hay selecciones guardadas (Selección → Guardar selección…)
|
||||
@msg No saved views (View → New View…) No hay vistas guardadas (Ver → Nueva vista…)
|
||||
@msg Offset Path: select paths Desplazar trazado: selecciona trazados
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Desplazar trazado: el desplazamiento no deja nada de los trazados seleccionados (un desplazamiento hacia dentro mayor que las formas)
|
||||
@msg Opened with {_1} note(s): {_2} Se abrió con {_1} nota(s): {_2}
|
||||
@msg Opened {url} Se abrió {url}
|
||||
@msg Outline Stroke: no stroked paths selected Contornear trazo: no hay trazados con trazo seleccionados
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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 ?
|
||||
@@ -3432,6 +3438,7 @@ axis Both Les deux
|
||||
@msg No saved selections (Select → Save Selection…) Aucune sélection enregistrée (Sélection → Enregistrer la sélection…)
|
||||
@msg No saved views (View → New View…) Aucune vue enregistrée (Affichage → Nouvelle vue…)
|
||||
@msg Offset Path: select paths Décalage : sélectionnez des tracés
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Décalage : le décalage ne laisse rien des tracés sélectionnés (un décalage vers l’intérieur plus grand que les formes)
|
||||
@msg Opened with {_1} note(s): {_2} Ouvert avec {_1} remarque(s) : {_2}
|
||||
@msg Opened {url} {url} ouvert
|
||||
@msg Outline Stroke: no stroked paths selected Vectoriser le contour : aucun tracé avec contour sélectionné
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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}
|
||||
@@ -4333,6 +4339,7 @@ theme Appearance Kinézet
|
||||
@msg object {id} is not a selected top-level object az objektum nem kijelölt legfelső szintű objektum: {id}
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet a több kitöltésű vagy körvonalú, effektusos vagy ecsetes objektumok a megrajzolt kinézetükkel kerültek be ({_1}): a megjelenésük még nem szerkeszthető
|
||||
@msg Offset Path: select paths Útvonal eltolása: jelölj ki útvonalakat
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Útvonal eltolása: az eltolás után semmi sem marad a kijelölt útvonalakból (a befelé eltolás nagyobb az alakzatoknál)
|
||||
@msg onto must name a floating panel az onto egy lebegő panelt kell megnevezzen
|
||||
@msg onto must name a panel floating in another group az onto egy másik csoportban lebegő panelt kell megnevezzen
|
||||
@msg Opened with {_1} note(s): {_2} Megnyitva; megjegyzések: {_1}: {_2}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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?
|
||||
@@ -3435,6 +3441,7 @@ axis Both Entrambi
|
||||
@msg No saved selections (Select → Save Selection…) Nessuna selezione salvata (Selezione → Salva selezione…)
|
||||
@msg No saved views (View → New View…) Nessuna vista salvata (Visualizza → Nuova vista…)
|
||||
@msg Offset Path: select paths Sposta tracciato: seleziona tracciati
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Sposta tracciato: lo spostamento non lascia nulla dei tracciati selezionati (uno spostamento verso l’interno maggiore delle forme)
|
||||
@msg Opened with {_1} note(s): {_2} Aperto con {_1} nota/e: {_2}
|
||||
@msg Opened {url} Aperto {url}
|
||||
@msg Outline Stroke: no stroked paths selected Contorna traccia: nessun tracciato con traccia selezionato
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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} レイヤー
|
||||
@@ -3592,6 +3598,7 @@ theme Appearance 外観
|
||||
@msg Divide Objects Below: select a path 背面のオブジェクトで型抜き: パスを選択してください
|
||||
@msg Divide Objects Below: select exactly one path 背面のオブジェクトで型抜き: パスを 1 つだけ選択してください
|
||||
@msg Offset Path: select paths パスのオフセット: パスを選択してください
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) パスのオフセット: オフセットすると選択したパスが何も残りません (内側へのオフセットがシェイプより大きい)
|
||||
@msg Outline Stroke: no stroked paths selected パスのアウトライン: 線のあるパスが選択されていません
|
||||
@msg Split Into Grid: select paths large enough for the grid グリッドに分割: グリッドが収まる大きさのパスを選択してください
|
||||
@msg empty cutter 型抜きに使うパスが空です
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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}개
|
||||
@@ -3570,6 +3576,7 @@ theme Appearance 외관
|
||||
@msg Divide Objects Below: select a path 아래 개체 분할: 패스를 선택하세요
|
||||
@msg Divide Objects Below: select exactly one path 아래 개체 분할: 패스를 하나만 선택하세요
|
||||
@msg Offset Path: select paths 패스 오프셋: 패스를 선택하세요
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) 패스 오프셋: 오프셋을 적용하면 선택한 패스가 남지 않습니다(안쪽 오프셋이 모양보다 큼)
|
||||
@msg Outline Stroke: no stroked paths selected 선 윤곽선화: 선이 있는 패스가 선택되지 않았습니다
|
||||
@msg Split Into Grid: select paths large enough for the grid 격자로 분할: 격자에 충분한 크기의 패스를 선택하세요
|
||||
@msg empty cutter 빈 커터입니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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?
|
||||
@@ -3600,6 +3606,7 @@ effect Regular Normalny
|
||||
@msg No saved selections (Select → Save Selection…) Brak zapisanych zaznaczeń (Zaznacz → Zapisz zaznaczenie…)
|
||||
@msg No saved views (View → New View…) Brak zapisanych widoków (Widok → Nowy widok…)
|
||||
@msg Offset Path: select paths Odsuń ścieżkę: zaznacz ścieżki
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Odsuń ścieżkę: po odsunięciu nic nie zostaje z zaznaczonych ścieżek (odsunięcie do wewnątrz większe niż kształty)
|
||||
@msg Opened with {_1} note(s): {_2} Otwarto, uwagi ({_1}): {_2}
|
||||
@msg Opened {url} Otwarto {url}
|
||||
@msg Outline Stroke: no stroked paths selected Obrys konturu: nie zaznaczono ścieżek z obrysem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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?
|
||||
@@ -3091,6 +3094,7 @@ axis Both Ambos
|
||||
@msg No saved selections (Select → Save Selection…) Nenhuma seleção salva (Selecionar → Salvar seleção…)
|
||||
@msg No saved views (View → New View…) Nenhuma vista salva (Exibir → Nova vista…)
|
||||
@msg Offset Path: select paths Deslocar traçado: selecione traçados
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Deslocar traçado: o deslocamento não deixa nada dos traçados selecionados (um deslocamento para dentro maior que as formas)
|
||||
@msg Opened with {_1} note(s): {_2} Aberto com {_1} observação(ões): {_2}
|
||||
@msg Opened {url} {url} aberto
|
||||
@msg Outline Stroke: no stroked paths selected Contornar traço: nenhum traçado com traço selecionado
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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})?
|
||||
@@ -3431,6 +3437,7 @@ axis Both Оба
|
||||
@msg No saved selections (Select → Save Selection…) Нет сохранённых выделений (Выделение → Сохранить выделение…)
|
||||
@msg No saved views (View → New View…) Нет сохранённых видов (Просмотр → Новый вид…)
|
||||
@msg Offset Path: select paths Смещение контура: выделите контуры
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Смещение контура: после смещения от выделенных контуров ничего не остаётся (смещение внутрь больше самих фигур)
|
||||
@msg Opened with {_1} note(s): {_2} Открыто с примечаниями ({_1}) : {_2}
|
||||
@msg Opened {url} Открыто {url}
|
||||
@msg Outline Stroke: no stroked paths selected Обводка контура: не выделены контуры с обводкой
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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})?
|
||||
@@ -3419,6 +3425,7 @@ axis Both Обидві
|
||||
@msg No saved selections (Select → Save Selection…) Немає збережених виділень (Виділення → Зберегти виділення…)
|
||||
@msg No saved views (View → New View…) Немає збережених видів (Перегляд → Новий вид…)
|
||||
@msg Offset Path: select paths Зміщення контуру: виділіть контури
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Зміщення контуру: після зміщення від виділених контурів нічого не лишається (зміщення всередину більше за самі фігури)
|
||||
@msg Opened with {_1} note(s): {_2} Відкрито із примітками ({_1}) : {_2}
|
||||
@msg Opened {url} Відкрито {url}
|
||||
@msg Outline Stroke: no stroked paths selected Обведення контуру: не виділено контури з обведенням
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 660 and column 40.
|
@@ -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
|
||||
@@ -3962,6 +3968,7 @@ theme Appearance Giao diện
|
||||
@msg object {id} is not a selected top-level object đối tượng {id} không phải là đối tượng cấp cao nhất đã chọn
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet đối tượng có nhiều tô hoặc nét, hiệu ứng hoặc cọ đã vào dưới dạng bề ngoài ({_1}): diện mạo chưa thể chỉnh sửa
|
||||
@msg Offset Path: select paths Đường dẫn độ lệch: hãy chọn đường dẫn
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Đường dẫn độ lệch: độ lệch không để lại gì từ các đường dẫn đã chọn (độ lệch vào trong lớn hơn hình)
|
||||
@msg onto must name a floating panel onto phải đặt tên bảng đang thả nổi
|
||||
@msg onto must name a panel floating in another group onto phải đặt tên bảng đang thả nổi trong nhóm khác
|
||||
@msg Opened with {_1} note(s): {_2} Đã mở với {_1} ghi chú: {_2}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3590 and column 10.
|
@@ -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.
|
@@ -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.
|
@@ -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)]
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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,48 @@ 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;
|
||||
}
|
||||
// (Black and White has one colour and Flat Logo flat ones: no gradients to find.)
|
||||
let gradients = st.params["gradients"].as_bool().unwrap_or(false);
|
||||
if widgets::check(ui, tl!("Gradients"), gradients, mode == "grayscale" || mode == "color") {
|
||||
st.params["gradients"] = json!(!gradients);
|
||||
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 {
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Effects parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every entry of Illustrator's Effect menu, by its public feature name, against VectorCraft's effects catalogue
|
||||
(`effect_catalog()` in `crates/effects/src/lib.rs`) and the Effect menu built from it
|
||||
@@ -99,6 +99,7 @@ Revolve caps, 3D Classic, Turntable, 3D export) is 25–45 h.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536); scores unchanged (estimated) |
|
||||
| 2026-10-13 | minor | 3D and Materials: outlined words and other shapes of many small curves take Rotate, Extrude & Bevel and Inflate (curves are sampled by a tolerance; Rotate takes up to 65,536 outline points; #1481); Extrude & Bevel's Twist runs 0° to 360° as the public panel reference gives it, a negative value drawing untwisted (#1524); a twisted extrusion keeps its walls, which were dropped from about 45° (#1525); details in [three-d.md](three-d.md); 3D stays ~25%, 25–45 h (estimated) |
|
||||
| 2026-10-13 | minor | Inflate takes Twist, Taper and Cap ([three-d.md](three-d.md)); 3D stays ~25%, 25–45 h: materials, lights and shadows are the bulk of what remains |
|
||||
| 2026-10-12 | minor | Extrude & Bevel, Inflate and Rotate (initial, [three-d.md](three-d.md)); 3D ~8% → ~25%, 45–75 → 25–45 h |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/mod.rs`). Illustrator's list comes from its public
|
||||
@@ -56,13 +56,13 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
||||
|
||||
| Format | Illustrator | Ours: open/place | Ours: save/export | Fidelity and tests |
|
||||
|---|---|---|---|---|
|
||||
| VectorCraft `.vectorcraft` / `.vctemplate` | — | yes | yes | lossless JSON, versioned, save down to v1; property-tested round trips (`format/tests/prop_format.rs`) |
|
||||
| VectorCraft `.vectorcraft` / `.vctemplate` | — | yes | yes | lossless JSON, versioned, save down to v1; property-tested round trips (`format/tests/prop_format.rs`); documents with a lot of path data are written as compact JSON, about a fifth of the size (#1492) |
|
||||
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure) | PDF-compatible only | see above; no real-file corpus |
|
||||
| Illustrator template `.ait` | open, save | yes | no (save as `.vctemplate`) | |
|
||||
| PDF | open, place, save | yes (layers, masks, editable text, security, overprint; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+) | fuzzed import; PDF text placement issues in some files (#722) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type and CCITTFax images not read; #505 |
|
||||
| PDF | open, place, save | yes (layers, masks, editable text, security, overprint; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+) | fuzzed import; PDF text placement issues in some files (#722); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type and CCITTFax images not read; #505; Type 42 (TrueType) fonts set type at its size (#1553); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499) |
|
||||
| PostScript `.ps` | open | no | via Print to PostScript | |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests` |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527) |
|
||||
| DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). CIRCLE/ARC export fitting, CNC preset and VCarve QA remain open |
|
||||
| DWG | open, export | no | no | no open spec; DXF instead |
|
||||
| EMF / WMF | open, export | yes | yes | |
|
||||
@@ -86,6 +86,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542) (the `.ai` row for #1528 was written by that PR); scores unchanged (estimated) |
|
||||
| 2026-10-13 | minor | `.ai` read as its PDF part: a page whose art reaches past its artboard to the left (notes beside a design) moves right in the row until that art clears the page before it, so it is no longer taken for that page's art; a multi-artboard `.ai` with such notes opened as its PDF part ("the art on them differs from the page's"), without the art outside its artboards, and now opens from its editing data |
|
||||
| 2026-10-13 | minor | TIFF: grayscale files with an alpha channel or other extra channels place, relink and open, read as CMYK files with alpha already were (8 and 16 bits, either polarity; #1489, part of #1466); the rows for #1434 (strokes kept apart from a faded or masked fill, a `.ai`'s RGB numbers) and #1435 (SVG knockout groups without masks, `--decimals`) were written by those PRs; scores unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Affinity import: a text frame too short for its first line (a 64 pt title in a 54 pt frame) is made taller from its top, with a warning in the open result, instead of showing nothing (#1423), as `.ai` area type is (#1392); the spacing Affinity itself fits the line with isn't read; scores unchanged |
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadataLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editingLine truncated
|
||||
|
||||
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
|
||||
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
|
||||
@@ -54,6 +54,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
OS cursor display and Caps Lock remain unverified.
|
||||
- **Done (#1251):** Eyedropper source reads are independent of locks and isolation, including Shift/Shift+Alt and gradient stops. Source and selection preservation, unchanged Alt reverse/selection restrictions, hidden art and Undo/Redo have regression coverage. This addresses the reported behavior, with no new full-parity measurement.
|
||||
- **Done (#1253):** adding paint to an unpainted zero-width stroke restores the existing 1 pt default, including new-art defaults and Swap Fill/Stroke. The real Eyedropper inheritance sequence, targeted appearance items, preserved widths and Undo/Redo have regression coverage.
|
||||
- **Missing:** Average and Join in one step, with a corner or smooth choice for the joined anchor. `path.average` and `path.join` are separate commands and Join has no such choice (coinciding anchors merge into one since #1533). [ui-parity.md](ui-parity.md) listed "Join with corner/smooth choice" as done until 2026-10-13. About 1–2 h (estimated), inside the estimate below.
|
||||
- **Missing:** a tool-by-tool, panel-by-panel pass over modifiers, cursors, the Properties panel per context,
|
||||
isolation mode and small behaviours, against the public tool and shortcut documentation. Itemized in
|
||||
[ui-parity.md](ui-parity.md). The Shaper's construction-mode widget and gesture refinements remain.
|
||||
@@ -236,6 +237,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | G1: Average and Join in one step with a corner or smooth choice is listed as missing (the ui-parity row said done; the command does not exist); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). G11: Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). G9: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); Polish catalog (#1498); estimates unchanged, no gap entry closed |
|
||||
| 2026-10-13 | minor | Interaction: Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1508), sliders in dialogs show their rail (#1483), a template layer stays in Preview in Outline view (#1486), a tab stop in the Tabs panel drags from where the press landed (#1458); type: commands and MCP `add_text` report area type that does not fit its frame (#1471); 3D and Materials on outlined type and other shapes of many small curves (#1481), Twist 0° to 360° (#1524), twisted walls keep their faces (#1525); print-size images draw, export and crop (#1517); `vectorcraft-cli perf` reports load and memory on Windows (#1502; the idle-machine run is still missing); French catalog complete again (#1511); `graph.setData`'s CSV keeps each value exactly (#1485); estimates unchanged, no gap entry closed |
|
||||
| 2026-10-13 | minor | G1: Split Into Grid has its documented options (#1462), Transform Objects in the Transform dialogs and Transform Each and Reflect across an angle (#1448, #1460), the Pathfinder panel works on a group selected alone (#1450; one plain path alone is not settled), a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part); G9: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434); G4: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); G6: commands refuse a parameter key they don't take (#1442); G10: a window surface past 8192 px no longer closes the app (#1459 in part); G15: edits and Alt-drags of thousands of objects (#1429 in part); G16: 128 more Czech strings (#1461); estimates unchanged |
|
||||
| 2026-10-12 | minor | G3: Extrude & Bevel, Inflate and Rotate with Position presets ([three-d.md](three-d.md), #1407); materials, lights, shadows, Classic, Turntable and export remain, 45–75 → 25–45 h |
|
||||
|
||||
+566
-271
@@ -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 (Gradients: on linear gradients 6 Colors goes from ΔE 2.12 in 6.4 paths to 0.62 in 3.4)
|
||||
|
||||
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,9 @@ 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) |
|
||||
| linear gradients | a two-stop gradient over the whole image at 25 degrees, and a three-stop gradient at 110 degrees in a disc on white: their lines and stops are known (measured from the Gradients 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 +43,17 @@ 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) |
|
||||
| gradient paths | paths filled with a linear gradient (on inputs that are flat colour, every one would be a gradient that is not there) |
|
||||
| found %, angle off, ramp off | on the linear-gradient inputs: the share of them in which a gradient was found; and for those found, the largest angle (degrees) between the true gradient line and the traced one, and the largest difference (levels of a channel, 0 to 255) between the true colours and the traced gradient's at 17 points along the true line |
|
||||
| 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 +65,405 @@ 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 Gradients)
|
||||
|
||||
| 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 % | gradient paths | found % | angle off | ramp off |
|
||||
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | gradient, photo-like | 2.45 | 2.69 | – | – | – | 37.1 | 2134 | – | 43 | 37 | – | – | – | – | 0.0 | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.6 | 700 | – | 51 | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | two-stop gradient, three-stop gradient in a disc | 2.12 | 3.04 | – | – | – | 6.4 | 469 | – | 21 | 14 | – | – | – | – | 3.9 | 12.7 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors with Gradients | two-stop gradient, three-stop gradient in a disc | 0.62 | 1.36 | – | – | – | 3.4 | 126 | – | 8 | 4 | – | – | – | – | 1.8 | 2.7 | 0.0 | 0.0 | – | – | – | – | – | 1.1 | 86 | 1.5 | 21 |
|
||||
| High Fidelity Photo, no Gradients | two-stop gradient, three-stop gradient in a disc | 0.79 | 2.98 | – | – | – | 38.1 | 3827 | – | 31 | 30 | – | – | – | – | 0.0 | – | 1.1 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo with Gradients | two-stop gradient, three-stop gradient in a disc | 0.46 | 1.31 | – | – | – | 20.6 | 759 | – | 9 | 5 | – | – | – | – | 0.0 | – | 2.9 | 0.0 | – | – | – | – | – | 1.0 | 86 | 1.5 | 21 |
|
||||
|
||||
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 Gradients).
|
||||
|
||||
## 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 % | gradient paths | found % | angle off | ramp off |
|
||||
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | gradient, photo-like | 2.45 | 2.69 | – | – | – | 37.1 | 2134 | – | 43 | 37 | – | – | – | – | 0.0 | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | gradient, photo-like | 9.26 | 9.06 | – | – | – | 16.6 | 700 | – | 51 | 40 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | two-stop gradient, three-stop gradient in a disc | 2.12 | 3.04 | – | – | – | 6.4 | 469 | – | 21 | 14 | – | – | – | – | 3.9 | 12.7 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors with Gradients | two-stop gradient, three-stop gradient in a disc | 0.62 | 1.36 | – | – | – | 3.4 | 126 | – | 8 | 4 | – | – | – | – | 1.8 | 2.7 | 0.0 | 0.0 | – | – | – | – | – | 1.1 | 86 | 1.5 | 21 |
|
||||
| High Fidelity Photo, no Gradients | two-stop gradient, three-stop gradient in a disc | 0.79 | 2.98 | – | – | – | 38.1 | 3827 | – | 31 | 30 | – | – | – | – | 0.0 | – | 1.1 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo with Gradients | two-stop gradient, three-stop gradient in a disc | 0.46 | 1.31 | – | – | – | 20.6 | 759 | – | 9 | 5 | – | – | – | – | 0.0 | – | 2.9 | 0.0 | – | – | – | – | – | 1.0 | 86 | 1.5 | 21 |
|
||||
|
||||
## 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 % | gradient paths | found % | angle off | ramp off |
|
||||
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Default | gradient, photo | 39.03 | 38.64 | – | – | – | 3.2 | 595 | – | 55 | 41 | – | – | – | – | 18.8 | 35.5 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Default | linear gradients | 25.64 | 29.86 | – | – | – | 2.0 | 43 | – | 33 | 32 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Auto Color | gradient, photo | 2.07 | 2.39 | – | – | – | 159.3 | 4441 | – | 21 | 8 | – | – | – | – | 37.6 | 46.7 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Auto Color | linear gradients | 1.02 | 1.74 | – | – | – | 15.7 | 1272 | – | 12 | 8 | – | – | – | – | 9.1 | 7.4 | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | gradient, photo | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | linear gradients | 0.64 | 3.29 | – | – | – | 34.5 | 4318 | – | 32 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | gradient, photo | 2.53 | 2.80 | – | – | – | 42.7 | 2307 | – | 43 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | linear gradients | 0.69 | 3.09 | – | – | – | 24.3 | 1061 | – | 31 | 31 | – | – | – | – | 0.0 | – | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 3 Colors | gradient, photo | 7.49 | 7.60 | – | – | – | 5.7 | 524 | – | 48 | 40 | – | – | – | – | 6.9 | 8.7 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 3 Colors | linear gradients | 4.45 | 5.79 | – | – | – | 3.0 | 78 | – | 28 | 24 | – | – | – | – | 10.3 | 1.9 | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors | gradient, photo | 5.00 | 5.28 | – | – | – | 13.0 | 829 | – | 37 | 28 | – | – | – | – | 6.9 | 8.0 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors | linear gradients | 2.07 | 3.07 | – | – | – | 6.2 | 266 | – | 20 | 14 | – | – | – | – | 2.4 | 9.3 | 0.0 | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 16 Colors | gradient, photo | 3.00 | 3.30 | – | – | – | 47.2 | 2126 | – | 30 | 16 | – | – | – | – | 15.2 | 19.6 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 16 Colors | linear gradients | 0.98 | 1.67 | – | – | – | 15.8 | 1340 | – | 8 | 6 | – | – | – | – | 13.9 | 15.5 | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | gradient, photo | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | linear gradients | 5.25 | 8.40 | – | – | – | 9.0 | 477 | – | 31 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Black and White Logo | gradient, photo | 38.97 | 38.53 | – | – | – | 9.0 | 634 | – | 73 | 54 | – | – | – | – | – | – | 1.2 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Black and White Logo | linear gradients | 25.62 | 29.78 | – | – | – | 1.5 | 127 | – | 46 | 33 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Sketched Art | gradient, photo | 45.42 | 45.21 | – | – | – | 1.0 | 244 | – | 46 | 35 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Sketched Art | linear gradients | 29.20 | 33.47 | – | – | – | 1.5 | 54 | – | 35 | 34 | – | – | – | – | – | – | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Silhouettes | gradient, photo | 61.57 | 61.32 | – | – | – | 0.2 | 2 | – | 47 | 21 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Silhouettes | linear gradients | 39.85 | 46.48 | – | – | – | 1.0 | 37 | – | 1 | 0 | – | – | – | – | – | – | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Line Art | gradient, photo | 38.98 | 38.54 | – | – | – | 7.7 | 472 | – | 58 | 44 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Line Art | linear gradients | 25.63 | 29.80 | – | – | – | 1.5 | 95 | – | 33 | 32 | – | – | – | – | – | – | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Technical Drawing | gradient, photo | 38.94 | 38.51 | – | – | – | 20.0 | 659 | – | 73 | 51 | – | – | – | – | – | – | 11.7 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Technical Drawing | linear gradients | 25.62 | 29.80 | – | – | – | 1.5 | 118 | – | 49 | 33 | – | – | – | – | – | – | 0.0 | 0.0 | – | – | – | – | – | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | 0.0 | – | – | – |
|
||||
| Outline | askew shapes | 23.68 | 36.71 | 0.425 | 1.50 | 6.62 | 4.0 | 20 | 0.83 | 100 | 3 | – | – | – | – | – | – | 0.0 | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Outline | gradient, photo | 39.38 | 39.18 | – | – | – | 7.5 | 445 | – | 59 | 43 | – | – | – | – | – | – | 3.3 | 0.5 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Outline | linear gradients | 25.08 | 30.63 | – | – | – | 1.5 | 93 | – | 33 | 32 | – | – | – | – | – | – | 0.0 | 0.5 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Flat Logo | gradient, photo | refused | | | | | | | | | | | | | | | | | | | | | | | | | | |
|
||||
| Flat Logo | linear gradients | 6.77 | 6.85 | – | – | – | 8918.3 | 41974 | – | 7 | 0 | – | – | – | – | 2733.2 | – | 7382.7 | 0.2 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Default with Shapes | gradient, photo | 39.03 | 38.64 | – | – | – | 3.2 | 595 | – | 55 | 41 | – | – | – | – | 18.8 | 35.5 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Default with Shapes | linear gradients | 25.64 | 29.86 | – | – | – | 2.0 | 43 | – | 33 | 32 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors with Shapes | linear gradients | 2.07 | 3.07 | – | – | – | 6.2 | 266 | – | 20 | 14 | – | – | – | – | 2.4 | 9.3 | 0.0 | 0.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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 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 | 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 | 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 6 Colors with Auto Grouping | linear gradients | 2.07 | 3.07 | – | – | – | 6.2 | 266 | – | 20 | 14 | – | – | – | – | 2.4 | 9.3 | 0.0 | 0.0 | – | – | – | – | 0 | 0.0 | 0 | – | – |
|
||||
| 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 | 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 | 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 | 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 | 0.0 | – | – | – |
|
||||
| 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 | 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 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | gradient, photo | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 53 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | linear gradients | 5.25 | 8.40 | – | – | – | 9.0 | 477 | – | 31 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 68 | 0.0 | 0 | – | – |
|
||||
| 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 | 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 | 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 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 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | gradient, photo | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | 69 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | linear gradients | 0.64 | 3.29 | – | – | – | 34.5 | 4318 | – | 32 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 72 | 0.0 | 0 | – | – |
|
||||
| 6 Colors, no Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | 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.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | 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.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | 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.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | gradient, photo | 5.00 | 5.28 | – | – | – | 13.0 | 829 | – | 37 | 28 | – | – | – | – | 6.9 | 8.0 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | linear gradients | 2.07 | 3.07 | – | – | – | 6.2 | 266 | – | 20 | 14 | – | – | – | – | 2.4 | 9.3 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors with Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors with Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors with Gradients | 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.0 | – | – | – |
|
||||
| 6 Colors with Gradients | 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.0 | – | – | – |
|
||||
| 6 Colors with Gradients | 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.0 | – | – | – |
|
||||
| 6 Colors with Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 6 Colors with Gradients | gradient, photo | 4.52 | 4.86 | – | – | – | 16.3 | 919 | – | 36 | 26 | – | – | – | – | 8.8 | 12.1 | 0.0 | 0.0 | – | – | – | – | – | 0.7 | – | – | – |
|
||||
| 6 Colors with Gradients | linear gradients | 0.40 | 1.09 | – | – | – | 1.3 | 39 | – | 0 | 0 | – | – | – | – | 0.5 | 1.1 | 0.0 | 0.0 | – | – | – | – | – | 0.8 | 83 | 0.2 | 5 |
|
||||
| High Fidelity Photo, no Gradients | 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.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | 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.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | 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.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | gradient, photo | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo, no Gradients | linear gradients | 0.64 | 3.29 | – | – | – | 34.5 | 4318 | – | 32 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo with Gradients | 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.0 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | 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.0 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | 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.0 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | gradient, photo | 1.80 | 2.04 | – | – | – | 140.7 | 10844 | – | 41 | 39 | – | – | – | – | 0.0 | – | 10.0 | 0.0 | – | – | – | – | – | 0.8 | – | – | – |
|
||||
| High Fidelity Photo with Gradients | linear gradients | 0.23 | 1.19 | – | – | – | 6.0 | 252 | – | 0 | 0 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.8 | 83 | 0.2 | 5 |
|
||||
|
||||
## 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 % | gradient paths | found % | angle off | ramp off |
|
||||
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | clean | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | aliased | 1.78 | 2.01 | – | – | – | 73.0 | 8021 | – | 40 | 38 | – | – | – | – | 0.0 | – | 5.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | blurred | 1.59 | 1.95 | – | – | – | 72.0 | 7450 | – | 40 | 39 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | noisy | 2.02 | 2.19 | – | – | – | 155.5 | 10670 | – | 33 | 31 | – | – | – | – | 0.0 | – | 18.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| High Fidelity Photo | scanned | 1.45 | 1.55 | – | – | – | 140.5 | 9723 | – | 36 | 35 | – | – | – | – | 0.0 | – | 24.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | clean | 2.53 | 2.80 | – | – | – | 42.7 | 2307 | – | 43 | 37 | – | – | – | – | 0.0 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | blurred | 2.35 | 2.75 | – | – | – | 29.0 | 1990 | – | 44 | 37 | – | – | – | – | 0.0 | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Low Fidelity Photo | scanned | 1.95 | 2.05 | – | – | – | 34.5 | 2550 | – | 42 | 33 | – | – | – | – | 0.0 | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | clean | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | aliased | 9.71 | 9.52 | – | – | – | 11.0 | 647 | – | 57 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | blurred | 9.66 | 9.32 | – | – | – | 11.5 | 585 | – | 55 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| Shades of Gray | noisy | 9.74 | 9.44 | – | – | – | 19.5 | 648 | – | 46 | 37 | – | – | – | – | 0.0 | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | 0.0 | – | – | – |
|
||||
| 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 | – | – | 0.0 | – | – | – |
|
||||
| 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 | – | – | 0.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 | – | – | 0.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 | – | – | 0.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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | – | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| 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 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | clean | 9.71 | 9.55 | – | – | – | 18.3 | 753 | – | 51 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 53 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | aliased | 9.71 | 9.52 | – | – | – | 11.0 | 647 | – | 57 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 54 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | blurred | 9.66 | 9.32 | – | – | – | 11.5 | 585 | – | 55 | 41 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 47 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | noisy | 9.74 | 9.44 | – | – | – | 19.5 | 648 | – | 46 | 37 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | 28 | 0.0 | – | – | – |
|
||||
| Shades of Gray with Auto Grouping | scanned | 6.58 | 6.46 | – | – | – | 19.0 | 764 | – | 47 | 37 | – | – | – | – | 0.0 | – | 1.0 | 0.0 | – | – | – | – | 37 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | clean | 1.80 | 2.05 | – | – | – | 134.7 | 11212 | – | 39 | 37 | – | – | – | – | 0.0 | – | 7.3 | 0.0 | – | – | – | – | 69 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | aliased | 1.78 | 2.01 | – | – | – | 73.0 | 8021 | – | 40 | 38 | – | – | – | – | 0.0 | – | 5.5 | 0.0 | – | – | – | – | 65 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | blurred | 1.59 | 1.95 | – | – | – | 72.0 | 7450 | – | 40 | 39 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | 65 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | noisy | 2.02 | 2.19 | – | – | – | 155.5 | 10670 | – | 33 | 31 | – | – | – | – | 0.0 | – | 18.5 | 0.0 | – | – | – | – | 72 | 0.0 | – | – | – |
|
||||
| High Fidelity Photo with Auto Grouping | scanned | 1.45 | 1.55 | – | – | – | 140.5 | 9723 | – | 36 | 35 | – | – | – | – | 0.0 | – | 24.5 | 0.0 | – | – | – | – | 75 | 0.0 | – | – | – |
|
||||
| 6 Colors, no Gradients | clean | 2.07 | 3.07 | – | – | – | 6.2 | 266 | – | 20 | 14 | – | – | – | – | 2.4 | 9.3 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors, no Gradients | aliased | 2.04 | 3.07 | – | – | – | 6.5 | 500 | – | 22 | 14 | – | – | – | – | 0.0 | 0.6 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors, no Gradients | blurred | 2.08 | 3.69 | – | – | – | 6.0 | 344 | – | 23 | 14 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors, no Gradients | noisy | 2.36 | 2.87 | – | – | – | 8.0 | 864 | – | 19 | 12 | – | – | – | – | 10.5 | 30.5 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors, no Gradients | scanned | 2.16 | 2.41 | – | – | – | 6.0 | 776 | – | 20 | 12 | – | – | – | – | 9.8 | 30.4 | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| 6 Colors with Gradients | clean | 0.40 | 1.09 | – | – | – | 1.3 | 39 | – | 0 | 0 | – | – | – | – | 0.5 | 1.1 | 0.0 | 0.0 | – | – | – | – | – | 0.8 | 83 | 0.2 | 5 |
|
||||
| 6 Colors with Gradients | aliased | 0.22 | 0.77 | – | – | – | 0.5 | 6 | – | 0 | 0 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – | 1.0 | 100 | 0.2 | 5 |
|
||||
| 6 Colors with Gradients | blurred | 0.33 | 2.29 | – | – | – | 3.0 | 161 | – | 8 | 0 | – | – | – | – | 2.3 | 5.1 | 0.0 | 0.0 | – | – | – | – | – | 1.0 | 100 | 0.3 | 6 |
|
||||
| 6 Colors with Gradients | noisy | 1.01 | 1.36 | – | – | – | 11.0 | 321 | – | 9 | 1 | – | – | – | – | 5.6 | 8.0 | 0.0 | 0.0 | – | – | – | – | – | 2.5 | 100 | 0.2 | 8 |
|
||||
| 6 Colors with Gradients | scanned | 1.57 | 1.82 | – | – | – | 5.5 | 279 | – | 23 | 14 | – | – | – | – | 3.2 | 2.4 | 0.0 | 0.0 | – | – | – | – | – | 0.5 | 50 | 1.5 | 21 |
|
||||
| High Fidelity Photo, no Gradients | clean | 0.64 | 3.29 | – | – | – | 34.5 | 4318 | – | 32 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo, no Gradients | aliased | 0.67 | 3.46 | – | – | – | 37.5 | 3184 | – | 32 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo, no Gradients | blurred | 0.72 | 3.25 | – | – | – | 33.5 | 3324 | – | 32 | 31 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo, no Gradients | noisy | 1.15 | 2.67 | – | – | – | 37.5 | 3468 | – | 29 | 28 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo, no Gradients | scanned | 1.05 | 1.62 | – | – | – | 54.5 | 3858 | – | 29 | 28 | – | – | – | – | 0.0 | – | 7.5 | 0.0 | – | – | – | – | – | 0.0 | 0 | – | – |
|
||||
| High Fidelity Photo with Gradients | clean | 0.23 | 1.19 | – | – | – | 6.0 | 252 | – | 0 | 0 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 0.8 | 83 | 0.2 | 5 |
|
||||
| High Fidelity Photo with Gradients | aliased | 0.21 | 0.52 | – | – | – | 0.5 | 24 | – | 0 | 0 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | 1.0 | 100 | 0.2 | 5 |
|
||||
| High Fidelity Photo with Gradients | blurred | 0.33 | 2.29 | – | – | – | 55.5 | 940 | – | 4 | 0 | – | – | – | – | 0.0 | – | 13.5 | 0.0 | – | – | – | – | – | 1.0 | 100 | 0.3 | 6 |
|
||||
| High Fidelity Photo with Gradients | noisy | 1.00 | 1.24 | – | – | – | 23.5 | 652 | – | 21 | 7 | – | – | – | – | 0.0 | – | 0.5 | 0.0 | – | – | – | – | – | 2.0 | 100 | 0.2 | 8 |
|
||||
| High Fidelity Photo with Gradients | scanned | 0.97 | 1.55 | – | – | – | 46.5 | 2942 | – | 18 | 15 | – | – | – | – | 0.0 | – | 6.0 | 0.0 | – | – | – | – | – | 0.5 | 50 | 1.5 | 21 |
|
||||
|
||||
## 4. Yardstick
|
||||
|
||||
@@ -258,23 +471,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 % | gradient paths | found % | angle off | ramp off |
|
||||
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 0.0 | – | – | – |
|
||||
| askew shapes | clean | 0.31 | 7.76 | 0.142 | 1.08 | 3.38 | 1.0 | 45 | 1.89 | 32 | 15 | – | – | – | – | – | – | 0.0 | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | – | – | – | – | 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 | – | 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 | – | 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 | – | 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 | – | 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 | – | 0.0 | – | – | – |
|
||||
|
||||
## 5. Cost
|
||||
|
||||
@@ -282,89 +505,161 @@ 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 | 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 |
|
||||
| Default | photo-like | 1 | 0.05 s, 29 paths, 6278 anchors, 12 MB |
|
||||
| Default | four-colour badge | 1 | 0.03 s, 3 paths, 64 anchors, 12 MB |
|
||||
| Auto Color | photo-like | 1 | 0.18 s, 732 paths, 45265 anchors, 25 MB |
|
||||
| Auto Color | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
|
||||
| High Fidelity Photo | photo-like | 1 | 0.23 s, 968 paths, 117041 anchors, 170 MB |
|
||||
| High Fidelity Photo | four-colour badge | 1 | 0.07 s, 6 paths, 172 anchors, 23 MB |
|
||||
| Low Fidelity Photo | photo-like | 1 | 0.21 s, 129 paths, 23014 anchors, 103 MB |
|
||||
| Low Fidelity Photo | four-colour badge | 1 | 0.06 s, 6 paths, 136 anchors, 23 MB |
|
||||
| 3 Colors | photo-like | 1 | 0.09 s, 9 paths, 3341 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 |
|
||||
| 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 |
|
||||
| 6 Colors | photo-like | 1 | 0.10 s, 78 paths, 9076 anchors, 24 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.06 s, 5 paths, 88 anchors, 12 MB |
|
||||
| Shades of Gray | photo-like | 1 | 0.10 s, 81 paths, 6698 anchors, 40 MB |
|
||||
| Shades of Gray | four-colour badge | 1 | 0.08 s, 10 paths, 725 anchors, 35 MB |
|
||||
| Black and White Logo | photo-like | 1 | 0.05 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.05 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 |
|
||||
| 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 |
|
||||
| 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 | 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 | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 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 |
|
||||
| Outline | photo-like | 1 | 0.05 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.69 s, 5 paths, 78 anchors, 146 MB |
|
||||
| Default with Shapes | photo-like | 1 | 0.04 s, 29 paths, 6278 anchors, 12 MB |
|
||||
| Default with Shapes | four-colour badge | 1 | 0.03 s, 3 paths, 63 anchors, 12 MB |
|
||||
| 6 Colors with Shapes | photo-like | 1 | 0.24 s, 78 paths, 9076 anchors, 24 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.10 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.10 s, 81 paths, 6698 anchors, 44 MB |
|
||||
| Shades of Gray with Auto Grouping | four-colour badge | 1 | 0.07 s, 10 paths, 725 anchors, 35 MB |
|
||||
| High Fidelity Photo with Auto Grouping | photo-like | 1 | 0.28 s, 968 paths, 117041 anchors, 167 MB |
|
||||
| High Fidelity Photo with Auto Grouping | four-colour badge | 1 | 0.09 s, 6 paths, 172 anchors, 23 MB |
|
||||
| 6 Colors, no Gradients | photo-like | 1 | 0.10 s, 78 paths, 9076 anchors, 24 MB |
|
||||
| 6 Colors, no Gradients | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
|
||||
| 6 Colors with Gradients | photo-like | 1 | 0.14 s, 78 paths, 9076 anchors, 32 MB |
|
||||
| 6 Colors with Gradients | four-colour badge | 1 | 0.07 s, 5 paths, 84 anchors, 12 MB |
|
||||
| High Fidelity Photo, no Gradients | photo-like | 1 | 0.24 s, 968 paths, 117041 anchors, 169 MB |
|
||||
| High Fidelity Photo, no Gradients | four-colour badge | 1 | 0.07 s, 6 paths, 172 anchors, 23 MB |
|
||||
| High Fidelity Photo with Gradients | photo-like | 1 | 0.43 s, 968 paths, 117041 anchors, 166 MB |
|
||||
| High Fidelity Photo with Gradients | four-colour badge | 1 | 0.09 s, 6 paths, 172 anchors, 23 MB |
|
||||
| Default | photo-like | 4 | 0.25 s, 48 paths, 14438 anchors, 49 MB |
|
||||
| Default | four-colour badge | 4 | 0.16 s, 3 paths, 72 anchors, 48 MB |
|
||||
| Auto Color | photo-like | 4 | 0.72 s, 1469 paths, 149389 anchors, 113 MB |
|
||||
| Auto Color | four-colour badge | 4 | 0.30 s, 5 paths, 92 anchors, 46 MB |
|
||||
| High Fidelity Photo | photo-like | 4 | 1.05 s, 4479 paths, 337884 anchors, 638 MB |
|
||||
| High Fidelity Photo | four-colour badge | 4 | 0.35 s, 6 paths, 200 anchors, 96 MB |
|
||||
| Low Fidelity Photo | photo-like | 4 | 0.54 s, 299 paths, 61601 anchors, 392 MB |
|
||||
| Low Fidelity Photo | four-colour badge | 4 | 0.33 s, 6 paths, 152 anchors, 97 MB |
|
||||
| 3 Colors | photo-like | 4 | 0.38 s, 20 paths, 8473 anchors, 72 MB |
|
||||
| 3 Colors | four-colour badge | 4 | 0.28 s, 4 paths, 122 anchors, 53 MB |
|
||||
| 6 Colors | photo-like | 4 | 0.42 s, 115 paths, 23295 anchors, 100 MB |
|
||||
| 6 Colors | four-colour badge | 4 | 0.27 s, 5 paths, 92 anchors, 46 MB |
|
||||
| 16 Colors | photo-like | 4 | 0.36 s, 353 paths, 52770 anchors, 129 MB |
|
||||
| 16 Colors | four-colour badge | 4 | 0.24 s, 5 paths, 96 anchors, 47 MB |
|
||||
| Shades of Gray | photo-like | 4 | 0.34 s, 137 paths, 18823 anchors, 158 MB |
|
||||
| Shades of Gray | four-colour badge | 4 | 0.27 s, 10 paths, 1256 anchors, 135 MB |
|
||||
| Black and White Logo | photo-like | 4 | 0.21 s, 208 paths, 13546 anchors, 32 MB |
|
||||
| Black and White Logo | four-colour badge | 4 | 0.09 s, 1 paths, 57 anchors, 24 MB |
|
||||
| Sketched Art | photo-like | 4 | 0.26 s, 9 paths, 5387 anchors, 32 MB |
|
||||
| Sketched Art | four-colour badge | 4 | 0.13 s, 2 paths, 196 anchors, 24 MB |
|
||||
| Silhouettes | photo-like | 4 | 0.32 s, 2 paths, 1465 anchors, 36 MB |
|
||||
| Silhouettes | four-colour badge | 4 | 0.13 s, 1 paths, 28 anchors, 24 MB |
|
||||
| Line Art | photo-like | 4 | 0.30 s, 127 paths, 11330 anchors, 32 MB |
|
||||
| Line Art | four-colour badge | 4 | 0.12 s, 1 paths, 42 anchors, 24 MB |
|
||||
| Technical Drawing | photo-like | 4 | 0.31 s, 931 paths, 20628 anchors, 32 MB |
|
||||
| Technical Drawing | four-colour badge | 4 | 0.13 s, 1 paths, 57 anchors, 24 MB |
|
||||
| Outline | photo-like | 4 | 0.39 s, 113 paths, 10815 anchors, 64 MB |
|
||||
| Outline | four-colour badge | 4 | 0.20 s, 1 paths, 42 anchors, 44 MB |
|
||||
| Flat Logo | photo-like | 4 | refused after 0.19 s (not flat art), 40 MB |
|
||||
| Flat Logo | four-colour badge | 4 | 1.31 s, 5 paths, 69 anchors, 146 MB |
|
||||
| Default with Shapes | photo-like | 4 | 0.30 s, 48 paths, 14438 anchors, 49 MB |
|
||||
| Default with Shapes | four-colour badge | 4 | 0.15 s, 3 paths, 72 anchors, 48 MB |
|
||||
| 6 Colors with Shapes | photo-like | 4 | 0.45 s, 115 paths, 23295 anchors, 100 MB |
|
||||
| 6 Colors with Shapes | four-colour badge | 4 | 0.31 s, 5 paths, 92 anchors, 46 MB |
|
||||
| 6 Colors with Auto Grouping | photo-like | 4 | 0.44 s, 115 paths, 23295 anchors, 100 MB |
|
||||
| 6 Colors with Auto Grouping | four-colour badge | 4 | 0.30 s, 5 paths, 92 anchors, 47 MB |
|
||||
| Shades of Gray with Auto Grouping | photo-like | 4 | 0.47 s, 137 paths, 18823 anchors, 162 MB |
|
||||
| Shades of Gray with Auto Grouping | four-colour badge | 4 | 0.39 s, 10 paths, 1256 anchors, 137 MB |
|
||||
| High Fidelity Photo with Auto Grouping | photo-like | 4 | 1.34 s, 4479 paths, 337884 anchors, 615 MB |
|
||||
| High Fidelity Photo with Auto Grouping | four-colour badge | 4 | 0.28 s, 6 paths, 200 anchors, 97 MB |
|
||||
| 6 Colors, no Gradients | photo-like | 4 | 0.35 s, 115 paths, 23295 anchors, 100 MB |
|
||||
| 6 Colors, no Gradients | four-colour badge | 4 | 0.26 s, 5 paths, 92 anchors, 46 MB |
|
||||
| 6 Colors with Gradients | photo-like | 4 | 0.66 s, 115 paths, 23295 anchors, 128 MB |
|
||||
| 6 Colors with Gradients | four-colour badge | 4 | 0.39 s, 5 paths, 92 anchors, 46 MB |
|
||||
| High Fidelity Photo, no Gradients | photo-like | 4 | 0.79 s, 4479 paths, 337884 anchors, 617 MB |
|
||||
| High Fidelity Photo, no Gradients | four-colour badge | 4 | 0.27 s, 6 paths, 200 anchors, 97 MB |
|
||||
| High Fidelity Photo with Gradients | photo-like | 4 | 1.14 s, 4479 paths, 337884 anchors, 636 MB |
|
||||
| High Fidelity Photo with Gradients | four-colour badge | 4 | 0.40 s, 6 paths, 200 anchors, 97 MB |
|
||||
| Default | photo-like | 16 | 1.15 s, 127 paths, 34034 anchors, 199 MB |
|
||||
| Default | four-colour badge | 16 | 0.64 s, 3 paths, 88 anchors, 184 MB |
|
||||
| Auto Color | photo-like | 16 | 2.06 s, 5390 paths, 454670 anchors, 280 MB |
|
||||
| Auto Color | four-colour badge | 16 | 1.31 s, 5 paths, 112 anchors, 185 MB |
|
||||
| High Fidelity Photo | photo-like | 16 | 5.28 s, 31139 paths, 1192715 anchors, 880 MB |
|
||||
| High Fidelity Photo | four-colour badge | 16 | 1.36 s, 6 paths, 264 anchors, 257 MB |
|
||||
| Low Fidelity Photo | photo-like | 16 | 3.01 s, 2036 paths, 182778 anchors, 754 MB |
|
||||
| Low Fidelity Photo | four-colour badge | 16 | 1.55 s, 6 paths, 180 anchors, 263 MB |
|
||||
| 3 Colors | photo-like | 16 | 1.82 s, 76 paths, 23024 anchors, 290 MB |
|
||||
| 3 Colors | four-colour badge | 16 | 1.30 s, 4 paths, 190 anchors, 205 MB |
|
||||
| 6 Colors | photo-like | 16 | 1.76 s, 470 paths, 60831 anchors, 298 MB |
|
||||
| 6 Colors | four-colour badge | 16 | 1.27 s, 5 paths, 112 anchors, 185 MB |
|
||||
| 16 Colors | photo-like | 16 | 1.93 s, 1973 paths, 145642 anchors, 303 MB |
|
||||
| 16 Colors | four-colour badge | 16 | 1.26 s, 5 paths, 114 anchors, 185 MB |
|
||||
| Shades of Gray | photo-like | 16 | 2.01 s, 686 paths, 53654 anchors, 407 MB |
|
||||
| Shades of Gray | four-colour badge | 16 | 1.76 s, 9 paths, 2208 anchors, 270 MB |
|
||||
| Black and White Logo | photo-like | 16 | 1.45 s, 1128 paths, 45273 anchors, 129 MB |
|
||||
| Black and White Logo | four-colour badge | 16 | 0.88 s, 1 paths, 69 anchors, 88 MB |
|
||||
| Sketched Art | photo-like | 16 | 1.60 s, 15 paths, 11486 anchors, 128 MB |
|
||||
| Sketched Art | four-colour badge | 16 | 0.75 s, 2 paths, 361 anchors, 88 MB |
|
||||
| Silhouettes | photo-like | 16 | 2.21 s, 3 paths, 3350 anchors, 144 MB |
|
||||
| Silhouettes | four-colour badge | 16 | 0.51 s, 1 paths, 32 anchors, 95 MB |
|
||||
| Line Art | photo-like | 16 | 1.21 s, 713 paths, 37370 anchors, 129 MB |
|
||||
| Line Art | four-colour badge | 16 | 0.44 s, 1 paths, 49 anchors, 88 MB |
|
||||
| Technical Drawing | photo-like | 16 | 1.52 s, 4577 paths, 76559 anchors, 130 MB |
|
||||
| Technical Drawing | four-colour badge | 16 | 0.54 s, 1 paths, 69 anchors, 88 MB |
|
||||
| Outline | photo-like | 16 | 1.83 s, 632 paths, 34884 anchors, 256 MB |
|
||||
| Outline | four-colour badge | 16 | 0.91 s, 1 paths, 49 anchors, 176 MB |
|
||||
| Flat Logo | photo-like | 16 | refused after 0.19 s (not flat art), 40 MB |
|
||||
| Flat Logo | four-colour badge | 16 | 1.04 s, 5 paths, 67 anchors, 146 MB |
|
||||
| Default with Shapes | photo-like | 16 | 1.38 s, 127 paths, 34034 anchors, 199 MB |
|
||||
| Default with Shapes | four-colour badge | 16 | 0.64 s, 3 paths, 88 anchors, 184 MB |
|
||||
| 6 Colors with Shapes | photo-like | 16 | 1.66 s, 470 paths, 60831 anchors, 298 MB |
|
||||
| 6 Colors with Shapes | four-colour badge | 16 | 1.19 s, 5 paths, 112 anchors, 185 MB |
|
||||
| 6 Colors with Auto Grouping | photo-like | 16 | 2.10 s, 470 paths, 60831 anchors, 298 MB |
|
||||
| 6 Colors with Auto Grouping | four-colour badge | 16 | 1.08 s, 5 paths, 112 anchors, 184 MB |
|
||||
| Shades of Gray with Auto Grouping | photo-like | 16 | 3.20 s, 686 paths, 53654 anchors, 407 MB |
|
||||
| Shades of Gray with Auto Grouping | four-colour badge | 16 | 2.32 s, 9 paths, 2208 anchors, 270 MB |
|
||||
| High Fidelity Photo with Auto Grouping | photo-like | 16 | 10.42 s, 31139 paths, 1192715 anchors, 880 MB |
|
||||
| High Fidelity Photo with Auto Grouping | four-colour badge | 16 | 1.11 s, 6 paths, 264 anchors, 257 MB |
|
||||
| 6 Colors, no Gradients | photo-like | 16 | 1.21 s, 470 paths, 60831 anchors, 298 MB |
|
||||
| 6 Colors, no Gradients | four-colour badge | 16 | 0.86 s, 5 paths, 112 anchors, 184 MB |
|
||||
| 6 Colors with Gradients | photo-like | 16 | 3.03 s, 470 paths, 60831 anchors, 512 MB |
|
||||
| 6 Colors with Gradients | four-colour badge | 16 | 1.28 s, 5 paths, 112 anchors, 185 MB |
|
||||
| High Fidelity Photo, no Gradients | photo-like | 16 | 4.74 s, 31139 paths, 1192715 anchors, 880 MB |
|
||||
| High Fidelity Photo, no Gradients | four-colour badge | 16 | 1.12 s, 6 paths, 264 anchors, 257 MB |
|
||||
| High Fidelity Photo with Gradients | photo-like | 16 | 7.70 s, 31139 paths, 1192715 anchors, 880 MB |
|
||||
| High Fidelity Photo with Gradients | four-colour badge | 16 | 2.65 s, 6 paths, 264 anchors, 257 MB |
|
||||
|
||||
## Revision history
|
||||
|
||||
| date | change |
|
||||
|:--|:--|
|
||||
| 2026-10-13 | Gradients (off unless asked for): areas that are a linear gradient are found before the palette is made and traced as one path with a gradient fill; linear-gradient inputs with known lines and stops, and four columns for them |
|
||||
| 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 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (French: 423 strings added since the catalog was measured and 188 entries reworded, #1511; Hungarian: one message in the informal address, #1474; scores and estimates unchanged; Vietnamese catalog added, complete under the tests; 12-language score 63% → 71%; Czech: 128 strings added since the catalog was completed are translated, #1461; scores and estimates unchanged; Korean catalog added, complete under the tests; 12-language score 54% → 63%; Hungarian catalog added; Czech render-site localization follow-up; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Polish: 4,510 rows counted in the catalog (the row said 4,492), #1498; French: 423 strings added since the catalog was measured and 188 entries reworded, #1511; Hungarian: one message in the informal address, #1474; scores and estimates unchanged; Vietnamese catalog added, complete under the tests; 12-language score 63% → 71%; Czech: 128 strings added since the catalog was completed are translated, #1461; scores and estimates unchanged; Korean catalog added, complete under the tests; 12-language score 54% → 63%; Hungarian catalog added; Czech render-site localization follow-up; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Per-language status of VectorCraft's interface. How the catalogs work and how to add a language:
|
||||
[`development.md` › Localisation](development.md#localisation). Part of
|
||||
@@ -50,7 +50,7 @@ plus review. Native-speaker review is human work and isn't in the hours.
|
||||
| Russian | `ru` | 4,009 / 4,020 | full |
|
||||
| Ukrainian | `uk` | 4,009 / 4,020 (one/few/many plurals) | full |
|
||||
| Hungarian | `hu` | 4,524 rows: menus, `tl!` literals and messages complete (tests); one plural form | full |
|
||||
| Polish | `pl` | 4,492 rows: menus, `tl!` literals and messages complete (tests); one/few/many plurals | full; native review pending |
|
||||
| Polish | `pl` | 4,510 rows (counted 2026-10-13): menus, `tl!` literals and messages complete (tests); one/few/many plurals | full; native review pending |
|
||||
| Czech | `cs` | 4,411 catalog entries, including 636 `@msg` rows (measured locally 2026-10-11); menu, literal and message coverage enforced by tests | catalog complete; render-site gaps reduced; native review pending |
|
||||
|
||||
Fifteen non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
|
||||
@@ -80,6 +80,7 @@ font for the web build.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Polish (`pl`, #1498): the row count is corrected to the 4,510 rows in `pl.tsv` (it said 4,492), 654 of them `@msg` rows; AI-drafted, native review pending; scores and estimates unchanged |
|
||||
| 2026-10-13 | minor | French: the 423 strings added since the catalog was last complete are translated, and 188 entries are reworded to the feature names French users know (#1511); Hungarian: one message uses the informal address like the rest of the interface (#1474); the entry counts in the tables are the earlier measurements; native review still pending; scores and estimates unchanged |
|
||||
| 2026-10-12 | minor | Vietnamese (`vi`) catalog added, complete under the tests (menus, literals, messages; one plural form); 12-language score 63% → 71%, hours 60–100 → 55–90 |
|
||||
| 2026-10-13 | minor | Czech: 128 more strings translated (tool names, PDF and Image Trace presets, pie graph options and others added since the catalog was completed; #1461); the entry count in the table is the 2026-10-11 measurement; native review still pending; scores and estimates unchanged |
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Forward-looking plan. Where VectorCraft stands is in [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[target-app-parity.md](target-app-parity.md); the ranked work list is [gaps.md](gaps.md). Hours are Opus 5.5
|
||||
@@ -80,6 +80,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498); Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542); Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539); Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536); the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); estimates unchanged |
|
||||
| 2026-10-13 | minor | Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1508); commands and MCP `add_text` report area type that does not fit its frame (#1471), a tab stop in the Tabs panel drags from where the press landed (#1458); M9: 3D and Materials on outlined type and other shapes of many small curves (#1481), Twist 0° to 360° (#1524), twisted walls keep their faces (#1525); print-size images draw, export and crop (#1517), `vectorcraft-cli perf` reports load and memory on Windows (#1502); sliders in dialogs show their rail (#1483), a template layer stays in Preview in Outline view (#1486), French catalog complete again (#1511), a Hungarian message in the informal address (#1474); `graph.setData`'s CSV keeps each value exactly (#1485); estimates unchanged |
|
||||
| 2026-10-13 | minor | M6: Split Into Grid has its documented options (#1462), the Pathfinder panel works on a group selected alone (#1450); M2: Transform Objects in the Transform dialogs and Transform Each, Reflect across an angle (#1448, #1460); M4: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434), SVG knockout groups without masks (#1435); M7: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); M13: commands refuse a parameter key they don't take (#1442); M5: edits and Alt-drags of thousands of objects (#1429 in part); M14: a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part), a window surface past 8192 px no longer closes the app (#1459 in part); estimates unchanged |
|
||||
| 2026-10-12 | minor | M9: Extrude & Bevel, Inflate and Rotate (initial 3D types, [three-d.md](three-d.md)) |
|
||||
|
||||
+13
-10
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, Line truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user scoreLine truncated
|
||||
|
||||
The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is
|
||||
the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md),
|
||||
@@ -425,26 +425,26 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Align and Distribute Spacing keep a compound path or selected group that contains the key object in place, as they keep the key object itself.
|
||||
Anchor Align moves the selected points to their bounds or the artboard (#1180). Repeating it, or moving by at most 1e-9 pt on both axes as whole-object Align does, leaves the document, Undo, Redo and saved state unchanged; actual moves still take one undo step. `crates/engine/src/tests.rs` covers repeated alignment, rounding-only alignment, unchanged handles, saved state, command recording and Undo/Redo.
|
||||
Clear deletes individual selected compound-path members without deleting their siblings, and explicit object ids work without a selection; Cut still removes the same objects it copies (#786).
|
||||
- **Compound paths:** Make keeps the backmost object's appearance and each contour; selecting a member in Direct Selection reads and changes the compound's fill rule in Attributes. Direction remains a per-member edit. Rendered holes, unchanged contour geometry, no-op history and Undo/Redo are covered by `engine/tests/compound_attributes.rs` (#1197). Behaviour reference: [public compound-path documentation](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-paths.html).
|
||||
- **Compound paths:** Make keeps the backmost object's appearance and each contour; selecting a member in Direct Selection reads and changes the compound's fill rule in Attributes. Direction remains a per-member edit. Rendered holes, unchanged contour geometry, no-op history and Undo/Redo are covered by `engine/tests/compound_attributes.rs` (#1197). Behaviour reference: [public compound-path documentation](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-paths.html). Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537).
|
||||
- **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Not yet: Update Selection and the Layers panel's saved-selection menu.
|
||||
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; Alt-click or double-click toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners.
|
||||
Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open.
|
||||
Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512).
|
||||
- **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812; the on-canvas pie widget is still to come). A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812).
|
||||
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
|
||||
- **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres; Alt-click opens the tool's dialog, and a drag with Alt down at the release, from the press or pressed mid-drag, transforms a copy), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag).
|
||||
Align, Distribute, Distribute Spacing, Reflect and Shear return an error for any `horizontal`, `vertical`, `to`, `axis`, `bounds` or `spacing` value they don't take, listing the choices, and Align returns one when a call gives neither `horizontal` nor `vertical`; nothing moves and no undo step is recorded.
|
||||
Transform Again uses the Copy, Transform Patterns, Scale Strokes & Effects and Scale Corners choices of the transform it repeats; after a bounding-box handle drag on area type, it resizes the type area again and the type keeps its size (`object.transformAgain`). With anchors selected with Direct Selection, Move, Rotate, Scale, Reflect, Shear and Transform Again (the commands, their dialogs and the four tools' drags) transform only those anchors, their handles with them, about the centre of the box around them unless a point was clicked; a copy still copies the whole object (#1408; behaviour reference: [adjust path segments](https://helpx.adobe.com/illustrator/using/adjust-path-segments.html); the default reference point and the copy are our choices). The Move, Rotate, Scale, Reflect and Shear dialogs have a Preview checkbox, off when they open: while it is on the canvas follows the values, Cancel rolls it back and OK is still one undo step; Up/Down step the Rotate and Shear angles and the Scale percentages by 1, Shift by 10 (#1412). The five dialogs and Transform Each have Transform Objects beside Transform Patterns: off, the art stays and only its pattern tiles transform (`objects` on the transform commands), and Transform Again repeats the choice (#1448; behaviour reference: [scale objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-objects.html); the checkboxes are always enabled and the Scale page's pair is applied to all of them, our choices). Reflect's axis is Horizontal, Vertical or an angle (#1460; [reflect objects along an axis](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/reflect-objects-along-an-axis.html)). The link that keeps Transform Each's two scales equal is not built.
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442). Live Corners work on any path's corners too, as on a rectangle's (#511): a star's, a live polygon's (which also shows them to the Selection tool, has a Corner Radius field in the Properties and Transform panels and keeps its radius as its sides change) and a pen path's, wherever two sides meet at an angle at an anchor that isn't smooth: curved sides too, trimmed to what the cut leaves of them (#1144). Direct Selection shows the widgets of every corner, or of the corners holding selected anchors; each corner is a circle tangent to both sides, drawn no larger than takes its cut halfway along its shorter side; a path keeps its uncut outline so its corners stay editable, and the same command, dialog, angle preference and View › Hide Corner Widget apply. With Direct Selection a click on a widget (no drag) selects its corner, as a click on the corner's anchor does, so the next drag on it rounds that corner alone; Shift-click toggles it (#1239). While a corner widget is dragged with the Selection tool the bounding box hides, so the rounding shows against the path alone (#1229).
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442). Live Corners work on any path's corners too, as on a rectangle's (#511): a star's, a live polygon's (which also shows them to the Selection tool, has a Corner Radius field in the Properties and Transform panels and keeps its radius as its sides change) and a pen path's, wherever two sides meet at an angle at an anchor that isn't smooth: curved sides too, trimmed to what the cut leaves of them (#1144). Direct Selection shows the widgets of every corner, or of the corners holding selected anchors; each corner is a circle tangent to both sides, drawn no larger than takes its cut halfway along its shorter side; a path keeps its uncut outline so its corners stay editable, and the same command, dialog, angle preference and View › Hide Corner Widget apply. With Direct Selection a click on a widget (no drag) selects its corner, as a click on the corner's anchor does, so the next drag on it rounds that corner alone; Shift-click toggles it (#1239). While a corner widget is dragged with the Selection tool the bounding box hides, so the rounding shows against the path alone (#1229). A round corner next to a curved side curves smoothly instead of sticking out of the shape (#1547, fixes #1512).
|
||||
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve (symmetric handles, Alt breaks them) and the release closes the path wherever it ends (#501). Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494).
|
||||
- **Direct Selection (#1278):** clicking a segment picks its connection, with hollow endpoint anchors and only that curve's direction handles. Delete leaves the neighboring curves and appearance intact, opens/splits contours in one undo step, and another Delete removes the remaining selected path. A whole-path drag and deliberate anchor selection keep their existing behavior; egui frames and native control/art checked, OS mouse/visual QA pending. Where anchors of overlapping objects coincide, a click or drag takes the anchor of the selected object, also when another object lies over it (#1509, fixes #1508).
|
||||
- **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path).
|
||||
- **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Average and Join in one step, with a corner or smooth choice for the joined anchor, is missing ([gap 1](gaps.md)).
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character or Appearance controls, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. Column designs remain open. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485).
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446; Object › Artboards › Rearrange All Artboards lays them out in a grid by row or by column, in one row or one column, left to right or right to left, from the menu, the Artboards panel or Properties' Rearrange All, #681; Scale Artwork with Artboard, in the Control bar and Properties beside Move Artwork with Artboard, resizes the artboard proportionally with the art fully inside it and its guides, strokes and corners per the Scale preferences, #602), Measure, Type, Hand, Zoom, Rotate View.
|
||||
- **Editing type:** double-clicking type with a selection tool switches to the Type tool with the caret where it was clicked.
|
||||
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
|
||||
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, onto the Swatches panel a pattern swatch and onto the Brushes panel an Art brush; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
|
||||
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, the tolerances, the brush size), and (#812) the Flare tool its Flare Tool Options, the Artboard tool the active artboard's Artboard Options, the graph tools Graph Type for the selected graph, the Symbolism tools the Symbolism Tools Options (Diameter, Intensity, Symbol Set Density) and the Magic Wand tool its panel.
|
||||
- **Ruler guides as layer objects:** a guide goes on the current layer, shows, hides, locks and is deleted with it, is listed in the Layers panel (its row selects it) and moves to another layer (`guide.setLayer`, or its row dragged onto that layer's rows); guides on a hidden or locked layer can't be selected, moved or deleted, Duplicate copies a layer's guides and Merge Selected and Flatten Artwork keep them on the merged layer; guides from files saved before guides had layers go on the top visible, unlocked layer when the document opens. Not done: guides stacked among the art of their layer (they are listed above it), a guide row's own eye and lock.
|
||||
- **Ruler guides as layer objects:** a guide goes on the current layer, shows, hides, locks and is deleted with it, is listed in the Layers panel (its row selects it) and moves to another layer (`guide.setLayer`, or its row dragged onto that layer's rows); guides on a hidden or locked layer can't be selected, moved or deleted, Duplicate copies a layer's guides and Merge Selected and Flatten Artwork keep them on the merged layer; guides from files saved before guides had layers go on the top visible, unlocked layer when the document opens. Not done: guides stacked among the art of their layer (they are listed above it), a guide row's own eye and lock. Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534).
|
||||
- **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies) and a dimmed copy of the row under the pointer (dropped on the trash rows are deleted, on New Layer duplicated, as in Illustrator), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets, each with a chip of its colour, or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting. A document opens with only its top-level layers open; the rows left open are saved in the native file (nothing is written for the default) and reopen that way. Only the rows scrolled into view are laid out, from a row list made once per change: a layer of 5,000 groups of 10 paths, all open, draws the panel in 0.4 ms a frame instead of about 29 s (#638).
|
||||
`object.setProps` with `visible: false` or `locked: true` deselects every selected object that is then hidden or locked, the key object included, as the eye and lock columns do.
|
||||
Object › Arrange › Send to Current Layer returns the error "the target layer is locked or hidden" and leaves the selected objects where they are when the current layer is hidden or locked, or is inside a hidden or locked layer.
|
||||
@@ -465,7 +465,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Nearly unchanged appearance is expected when reducing points. Percentage/auto-simplify controls, the corner-angle UI and Show Original are not implemented; the checklist is partial, without raising scores.
|
||||
Simplify and Add Anchor Points record no undo step when they change no path, and a path they leave as it is keeps its live shape.
|
||||
- **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `<use>` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boLine truncated
|
||||
- **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); German, Spanish, French, Italian, Japanese, Russian and Ukrainian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||
- **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); German, Spanish, French, Italian, Japanese, Russian and Ukrainian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498).
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks. The constant alpha soft mask that PDF and `.ai` export draw a CMYK page with transparency through is white, so applications that read every soft mask by its luminosity show the page too (#1359, part of #1286).
|
||||
Disable, Enable, Unlink and Link Opacity Mask and Opacity Mask Options (`transparency.setOpacityMask`) record no undo step when every mask they act on already has the flags they set.
|
||||
- **Image Trace, measured:** `crates/trace/tests/quality` traces drawings made from known vector art with every preset and [`image-trace-quality.md`](image-trace-quality.md) records the numbers against the baseline. Black and White, Grayscale and Color place each edge by the blend of the pixels across it (to a fraction of a pixel on anti-aliased and blurred images; hard-edged images are read as staircases) and fit outlines as Flat Logo does. *Measured* on each preset's own inputs, all sizes and variants: Default's outlines went from 0.31 px off on average to 0.09 (largest 1.51 to 1.02), Black and White Logo's from 0.27 to 0.06 with 0.3 kinks per trace where it had 15, and the gaps between abutting paths from 64 px to 22. On clean curved shapes Black and White Logo is 0.02 px off (the open tracer measured beside it: 0.20). The palette is made of the picture's own colours (blends along edges take none of it, colours that noise made of one colour merge, a blend pixel goes to one of the two colours it lies between; #1504): 16 Colors on flat art, all variants, went from 70 paths to 4.2 and from 5.1 px off to 0.13, with no path left in a colour the art doesn't have (5.9 before). Photos are smoothed within their tones before they are quantised and stacked layers go by lightness (#1516): High Fidelity Photo went from 3,653 paths to 121 with its colour error down from 2.30 to 1.75, and a 16 megapixel photo-like image is traced in 3.5 s where 4 megapixels were refused. Still weak, by the same numbers: abutting paths leave 22 to 50 px of gaps (each region's edge is still fitted on its own), thin lines in blurred and scanned images take several times the art's anchors, hairlines under half a pixel are lost to the threshold, a scan's uneven paper splits into tones when more colours are asked for than the art has, and a 16 megapixel photo takes about 4 GB while it is traced.
|
||||
@@ -489,9 +489,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included.
|
||||
All Artwork Above and Other Layers (Object › Lock and Object › Hide), Transform Each, Rearrange All Artboards, the width point and width profile commands, Liquify, Puppet Warp, Define Perspective Grid, Attach to Active Plane, Release with Perspective, moves and transforms in perspective, Move Plane and Move Plane to Match Object record no undo step when they change nothing.
|
||||
Change Case, Smart Punctuation, Find and Replace, Convert To Area Type, Convert To Point Type, Type on a Path Options, the Type on a Path effects and Fill with Placeholder Text record no undo step when they change nothing.
|
||||
- **Fonts:** a font is found by any of its names (family and style in every language of its name table, legacy family/style pairs, PostScript name), so documents from other apps open with the font they name; Find Font and `text.fonts` say for each font whether it was found exactly, by another name, substituted or missing, and how many glyphs are missing. PDF and `.ai` import brings Japanese text back as live type: CID fonts without a ToUnicode map read through the standard CMaps, vertical columns (WMode 1) as vertical type, letter spacing as tracking; a glyph is live type only when the installed font (in the closest style of its family) draws the same glyph, and text in a font that isn't installed comes in as live type shown in the fallback font until the font is installed (exports that draw it — PDF, EPS, EMF/WMF, raster, SVG outlines or embedded fonts — warn that they wrote the fallback font; `textAs: outlines` keeps the PDF's embedded glyphs). Two installed versions of one family and style (a font library keeping an old file beside a new one) both load, the first staying the one the family and style resolve to; PDF and `.ai` import tries each version when the glyphs differ, and type set in a version other than that one names it (`CharStyle::font_version`), which layout, the substituted-glyph highlight, Find Font, Package and Document Info follow. Fonts outside the font folders are listed too: those font services such as Adobe Fonts load on Windows, through DirectWrite, and those apps and font managers register with CoreText on macOS (#579). When a document opens with fonts that aren't available, fonts used only in symbols included, a Missing Fonts dialog lists them after the linked-file questions, with Find Fonts (Type › Find Font on them) and Close. Its Find in Folder… (in Find Font too) searches a folder on several threads, reads only what the font scan reads of font files and skips app packages, the Program Files and ProgramData folders, the system's folders and the app data folders in a home folder; Add Fonts copies the font files the user chooses, suitcase fonts with their resource fork, into VectorCraft's own Fonts folder, which the desktop app and `vectorcraft-cli` read (`text.missingFonts`, `text.findFontFiles`, `text.addFontFiles`).
|
||||
- **Fonts:** a font is found by any of its names (family and style in every language of its name table, legacy family/style pairs, PostScript name), so documents from other apps open with the font they name; Find Font and `text.fonts` say for each font whether it was found exactly, by another name, substituted or missing, and how many glyphs are missing. PDF and `.ai` import brings Japanese text back as live type: CID fonts without a ToUnicode map read through the standard CMaps, vertical columns (WMode 1) as vertical type, letter spacing as tracking; a glyph is live type only when the installed font (in the closest style of its family) draws the same glyph, and text in a font that isn't installed comes in as live type shown in the fallback font until the font is installed (exports that draw it — PDF, EPS, EMF/WMF, raster, SVG outlines or embedded fonts — warn that they wrote the fallback font; `textAs: outlines` keeps the PDF's embedded glyphs). Two installed versions of one family and style (a font library keeping an old file beside a new one) both load, the first staying the one the family and style resolve to; PDF and `.ai` import tries each version when the glyphs differ, and type set in a version other than that one names it (`CharStyle::font_version`), which layout, the substituted-glyph highlight, Find Font, Package and Document Info follow. Fonts outside the font folders are listed too: those font services such as Adobe Fonts load on Windows, through DirectWrite, and those apps and font managers register with CoreText on macOS (#579). When a document opens with fonts that aren't available, fonts used only in symbols included, a Missing Fonts dialog lists them after the linked-file questions, with Find Fonts (Type › Find Font on them) and Close. Its Find in Folder… (in Find Font too) searches a folder on several threads, reads only what the font scan reads of font files and skips app packages, the Program Files and ProgramData folders, the system's folders and the app data folders in a home folder; Add Fonts copies the font files the user chooses, suitcase fonts with their resource fork, into VectorCraft's own Fonts folder, which the desktop app and `vectorcraft-cli` read (`text.missingFonts`, `text.findFontFiles`, `text.addFontFiles`). The font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux).
|
||||
- **Files (M4):**
|
||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465).
|
||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492).
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; art reaching past a plain PDF's page clipped to its crop box, as PDF viewers show it, each such object in a clip group of its own (a PDF-compatible `.ai` opens whole: #675); a `.ai` on its trim boxes with the bleed boxes round them as the document's bleed, as Crop To › Trim opens a PDF (#1266); masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, an `.ai`'s white page under its layers left out rather than coming in as art, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). Overprint is kept from PDF import (opened, the PDF part of an `.ai`, placed) to PDF export: the graphics state's `/OP` and `/op` become the overprint of fills, strokes and type, so a spot colour over a process colour still overprints; a page whose overprint can't be read for certain knocks out with a note, as does `/OPM 0` being read as nonzero overprint (#1388; hand-written fixtures, `pdf/src/tests_import_overprint.rs`). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.
|
||||
A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304).
|
||||
Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests_links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).
|
||||
@@ -503,6 +503,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Place and the Links panel, Package (placed `.vectorcraft` documents packaged with their own linked files and fonts, relinked to the copies: #777), File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
|
||||
- Missing linked files can be looked for in a folder and its subfolders: Find in Folder… in the Linked File Not Found dialog and in the Links panel menu lists the files with each one's name, those with the size and contents saved with the link first, and relinks the files chosen, copying an image's file into the document's folder when asked, without replacing a file there (`links.findFiles`, `links.relink {copy}`). In the Links panel it looks for the selected linked files when any are selected.
|
||||
- Linked files another app changes while their document is open follow Preferences › File Handling › Update Links, as they do when it opens: Automatically reads them again (an Update Links undo step), Ask When Modified offers to, Manually leaves them modified in the Links panel. The desktop app compares the active document’s linked files by size and modification time every two seconds and when its window comes back to the front, on a worker thread; a file still being written is tried again (`links.updateChanged`).
|
||||
- Import and paste, 2026-10-13: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499).
|
||||
- **Tool cursors:** the Eyedropper has an original pipette glyph with its tip at the hotspot;
|
||||
Use Precise Cursors restores the crosshair. The same glyph feeds OS bitmaps and painted
|
||||
cursors. DPI/hotspot and preference checks in `cursors.rs` and `tests_prefs.rs` cover #1269;
|
||||
@@ -520,13 +521,15 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **Moving objects with raster effects (#1257):** during a drag or a dialog's preview, an object whose content only moved draws its cached drop shadows, outer glows and filtered content (Gaussian Blur, Feather and the Photoshop-style filters) from the rasters of an earlier frame at the nearest whole pixel, without running the effects again. The renderer first draws the moved content where the old content lies in its raster and compares the pixels. De-Interlace runs again. The frame after the drag runs the effects again and draws what a fresh renderer draws; on the desktop, the canvas renders that frame on the render worker. `texture_drag_perf` times a drag's frames and the frame after it.
|
||||
|
||||
- **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace), Page Knockout Group, Document Setup's substitution highlights, a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes.
|
||||
- **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace), Page Knockout Group, Document Setup's substitution highlights, a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes. Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536).
|
||||
|
||||
- **Full font names (#1258):** OpenType name ID 4 identifies its face alongside family/legacy/PostScript names, including localized names, for loaded fonts, cataloged files and Missing Fonts searches. Synthetic name-table regressions cover full names distinct from their PostScript name, real-family precedence and variable fonts' default faces. Windows font-service activation remains unverified; scores and hours are unchanged.
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Files: Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Rendering: Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). Fonts: the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux). Localization: The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498) (4,510 rows); scores and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | Tools and UI: Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). Correction: the combined Average and Join with its corner or smooth choice was listed as done in [ui-parity.md](ui-parity.md) and does not exist in the app (`path.join` and `path.average` are separate commands, and Join has no such choice); it is now listed as missing there and in [gap 1](gaps.md); scores and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | UI and tools: sliders in Save for Web, the raster export options, Preferences and the opacity popup show their rail, filled up to the handle (#1506, fixes #1483); in Outline view a template layer stays in Preview, dimmed (#1505, fixes #1486); Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1509, fixes #1508); a tab stop in the Tabs panel drags from where the press landed (#1487, fixes #1458); type: commands and MCP `add_text` report area type that does not fit its frame (#1500, fixes #1471); graphs: `graph.setData`'s CSV keeps each value exactly (#1485); scores and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | 3D and Materials: outlined words and other shapes of many small curves take Rotate, Extrude & Bevel and Inflate (#1481), Twist runs 0° to 360° (#1524) and twisted walls keep their faces (#1525), all described in [three-d.md](three-d.md); rendering: print-size images draw, export and crop (#1517); `vectorcraft-cli perf` on Windows reports load and memory (#1502; no budget re-run); localization: French catalog complete again with 423 strings added and 188 reworded (#1511), a Hungarian message in the informal address (#1474); scores and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | Image Trace, measured in `image-trace-quality.md`: the palette is made of the picture's own colours (#1504): 16 Colors on flat art 70 → 4.2 paths and 5.1 → 0.13 px off, paths in a colour the art lacks 5.9 → 0; photos are smoothed within their tones first and stacked layers go by lightness (#1516): High Fidelity Photo 3,653 → 121 paths with its colour error 2.30 → 1.75, 16 megapixels traced where 4 were refused. Score and hours unchanged (estimated) |
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Type parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
|
||||
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
|
||||
@@ -26,7 +26,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
|
||||
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
|
||||
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference unused (#394) |
|
||||
| Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #339; script filter: Japanese, Latin, Thai |
|
||||
| Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux) |
|
||||
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
|
||||
| Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending |
|
||||
| Variable fonts | partial | named instances as styles; axis sliders missing |
|
||||
@@ -56,6 +56,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | The font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); scores and hours unchanged |
|
||||
| 2026-10-13 | minor | The commands that make or restyle type, and MCP `add_text`, report area type that does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471); a tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458); scores and hours unchanged |
|
||||
| 2026-10-13 | minor | Fit Headline no longer pushes the tracking to its ceiling and loses glyphs when the frame holds one line (#1445, fixes #1444); text frames, wrap shapes and type paths with coordinates beyond a billion points hold no text, and layout limits its search for room, instead of hanging (#1451); scores and hours unchanged |
|
||||
| 2026-10-12 | minor | A character no installed font had is looked for again after the fonts are rescanned, so installing a font fixes it without a restart (#1376, from #992); scores and hours unchanged |
|
||||
|
||||
+10
-4
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
How VectorCraft's tools, handles, snapping and panels behave next to Illustrator's. A vector illustrator's
|
||||
trust is in the details: where a handle lands, what Shift and Alt do mid-drag, what a smart guide snaps to,
|
||||
@@ -54,7 +54,9 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Curvature tool: click/drag points, double-click or Alt-click for a corner, temporary Direct Selection, point conversion | partial | circular point markers without direction handles or duplicate markers, Anchor Size and Hide Edges regression-tested (#1236, in part); existing-path interpolation request and modifiers requested in #975 remain |
|
||||
| Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | partial | Keep selected and Alt to Smooth are missing |
|
||||
| Join tool scrubbing, Average, Join with corner/smooth choice | done | |
|
||||
| Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533) |
|
||||
| Average and Join in one step, with a corner or smooth choice for the joined anchor | missing | `path.join` and `path.average` are separate commands, and Join has no corner/smooth choice; [gap 1](gaps.md) |
|
||||
| Smooth tool smooths only the stretch of path under the drag | done | #1512 |
|
||||
| Simplify (slider, auto, corner angle threshold) | partial | Tolerance, default-on Preview and original/new point counts (#1272); slider, auto-simplify, corner-angle UI and Show Original remain missing |
|
||||
|
||||
## Direct Selection, anchors and handles
|
||||
@@ -72,7 +74,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Show handles when multiple anchors are selected; Highlight anchors on mouse over; anchor and handle size | done | Preferences › Selection & Anchor Display |
|
||||
| Convert selected anchors to corner/smooth from the Control bar and Properties | done | |
|
||||
| Cut path at selected anchors, Remove Anchor Points, Delete removes segments | done | #1278: segment Delete preserves neighboring edges and endpoint anchors; a second Delete removes the remainder; explicit anchor Delete retains its behavior |
|
||||
| Live Corners widgets (radius readout, Alt-click cycles kind, double-click the Corners dialog) | done | a Direct Selection click on a widget selects its corner (#1239); a press-and-drag on a widget of a path selected whole still rounds every corner (#908), and adding widgets to the selection by Shift-click (#1232) is missing; the bounding box hides while a widget is dragged with the Selection tool (#1229) |
|
||||
| Live Corners widgets (radius readout, Alt-click cycles kind, double-click the Corners dialog) | done | a Direct Selection click on a widget selects its corner (#1239); a press-and-drag on a widget of a path selected whole still rounds every corner (#908), and adding widgets to the selection by Shift-click (#1232) is missing; the bounding box hides while a widget is dragged with the Selection tool (#1229); a round corner next to a curved side curves smoothly instead of sticking out of the shape (#1512) |
|
||||
| Corners between curved sides (#1144), Relative rounding | partial | Relative rounding missing |
|
||||
| Isolation mode by double-click (groups and single objects), breadcrumbs, Esc to exit | done | Single paths, compound paths, images, meshes and placed files isolate too ([helpx: Isolate objects](https://helpx.adobe.com/africa/illustrator/desktop/manage-objects/select-objects/isolate-objects.html)) |
|
||||
| "Magnetic" moves of selected anchors with a modifier (#955 request) | missing | not an Illustrator feature as documented; under discussion |
|
||||
@@ -97,6 +99,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Ruler guides: drag out, select, move, delete, lock, Make/Release Guides | done | #414, #451 |
|
||||
| Ruler origin: corner drag with preview, double-click reset to the active artboard, relative X/Y fields, native save/reopen | done | #1212; `document.setRulerOrigin`, `document.resetRulerOrigin`; geometry and command coordinates stay absolute |
|
||||
| Ruler guides as Layers rows: shown, hidden, locked and deleted with their layer; rows dragged to another layer | done | `guide.setLayer`; guides from older files go on a layer |
|
||||
| Objects made into guides: drawn in the guide colour and style of Preferences; Lock Guides locks them too (no select, move or delete); a dragged selection snaps to ruler guides with Smart Guides on | done | #1534 (#1543, #1583) |
|
||||
| Alt swaps a ruler guide's orientation while dragging | missing | |
|
||||
| Global/artboard ruler mode switch and video rulers | missing | menu stubs; custom document origin is done (#1212) |
|
||||
|
||||
@@ -157,7 +160,8 @@ tools, never checked side by side) isn't captured row by row.
|
||||
(`crates/tools/src/catalog.rs`), a shortcut editor with chords, and saved sets. Not yet diffed item by item
|
||||
against Illustrator's published default shortcut list; estimated ~70% of it. Remaining: the panel shortcuts,
|
||||
the type shortcuts (tracking/kerning/leading/baseline steps with Alt/Shift), and selection shortcuts in the
|
||||
Layers panel.
|
||||
Layers panel. The shortcut editor also lists the commands that are in no menu but run without parameters (Edit › Duplicate,
|
||||
panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1539).
|
||||
|
||||
## Panels, Control bar, Properties, context menus
|
||||
|
||||
@@ -173,6 +177,7 @@ Layers panel.
|
||||
| Control bar with Transform and Stroke popovers | done | |
|
||||
| Pathfinder shape results keep their source names and SVG IDs | done | #1293; Minus Front keeps the back name, Minus Back the front name; Unite/Intersect/Exclude use the backmost named input; compound output carries it once; geometry and paint unchanged |
|
||||
| Attributes fill-rule controls with a compound member selected | done | Edits the compound's rule; direction still edits only the member; rendered-hole and Undo/Redo regression tests (#1197) |
|
||||
| Make Compound Path on a group (such as outlined type) keeps its backmost path's paint; compound paths joined into one keep their holes | done | #1537 |
|
||||
| Pathfinder panel on one group selected alone; a greyed-out button says what it needs | partial | #1450; the group's members are combined; one plain path selected alone stays disabled (the public documentation doesn't say what the operations do to a single path) |
|
||||
| Pathfinder Divide: an open cutting line splits a filled path | done | #1307; straight, crossed, bent and curved cutters, holes and frontmost appearance regression-tested; native automation: independent piece movement, one Undo/Redo, saved-document round trip and window capture; existing bounded-map fallback remains |
|
||||
| Canvas context menu | done | Layers rows, panels and type have none (gap) |
|
||||
@@ -196,6 +201,7 @@ re-measure of the panels added since.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); correction: "Join with corner/smooth choice" was listed as done, but `path.join` and `path.average` are separate commands and Join has no such choice, so Average and Join in one step with that choice is its own row, missing; the Smooth tool smooths only the stretch of path under the drag and a round corner next to a curved side curves smoothly instead of sticking out of the shape (#1547, fixes #1512); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539); Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); scores unchanged (estimated) |
|
||||
| 2026-10-13 | minor | Sliders in Save for Web, the raster export options, Preferences and the opacity popup show their rail (#1506, fixes #1483); a template layer stays in Preview, dimmed, in Outline view (#1505, fixes #1486); Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1509, fixes #1508); a tab stop in the Tabs panel drags from where the press landed (#1487, fixes #1458); scores unchanged (estimated) |
|
||||
| 2026-10-13 | minor | Split Into Grid has its row and column sizes, gutters, totals, Add Guides and Preview (#1488, fixes #1462); Transform Objects in the Transform dialogs and Transform Each, and Reflect across an angle (#1494, fixes #1448 and #1460); the Pathfinder panel works on a group selected alone and its greyed-out buttons say what they need (#1493, for #1450); selected text in value fields is white or black on the highlight (#1490, part of #1465); a focused field follows its value when a slider or preview changes it (#1497, fixes #1457); an Alt-drag copies once per drag (#1464); scores unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Transform dialogs (Move, Rotate, Scale, Reflect, Shear) preview on the canvas and step their angles and percentages with the arrow keys (#1428, fixes #1412); the transform tools, dialogs and Move act on anchors selected with Direct Selection (#1427, fixes #1408); the bounding box hides during a corner drag with the Selection tool (#1402, fixes #1229); a picture pastes when blank text sits beside it on the clipboard (#1404); scores unchanged (estimated) |
|
||||
|
||||
Reference in new issue
Block a user