Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a91be7983 | ||
|
|
5bfe774420 | ||
|
|
e918615347 | ||
|
|
250da4226e | ||
|
|
17b4396a97 | ||
|
|
5adeada26d | ||
|
|
175ad4a1e3 | ||
|
|
95304ad7f6 | ||
|
|
02da9bbdc9 | ||
|
|
331fc48b24 | ||
|
|
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]]
|
||||
|
||||
+3
-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 (Image Trace edges, strokes, memory and three new options, measured in `docs/image-trace-quality.md`, headline numbers unchanged; 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,8 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-13 | Image Trace: neighbouring colours share one fitted edge (gaps between them 22 to 50 px of area down to under 2), a 16 megapixel photo traces in 0.9 GB (was 3.9), stroked centre lines lie in the middle of their ink at its width (0.30 to 0.05 px off, 18 to 3 anchors a stroke), nearly level lines are levelled, more sharp corners are kept; new: Ignore Color, the Auto Color and Outline presets, and the Shapes, Auto Grouping and Gradients options (off by default) ([`docs/image-trace-quality.md`](docs/image-trace-quality.md)). |
|
||||
| 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 {
|
||||
|
||||
@@ -249,7 +249,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"distort.freeDistort",
|
||||
"Free Distort…",
|
||||
DT,
|
||||
"{corners: [[x,y]×4] new positions of the bounding-box corners TL, TR, BR, BL in unit box coordinates (identity [[0,0],[1,0],[1,1],[0,1]])}",
|
||||
"{corners: [[x,y]×4] (new positions of the bounding-box corners TL, TR, BR, BL in unit box coordinates; identity [[0,0],[1,0],[1,1],[0,1]])}",
|
||||
json!({"corners": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]}),
|
||||
),
|
||||
g("distort.puckerBloat", "Pucker & Bloat…", DT, "{amount: % (-200 pucker … 200 bloat, default 0)}", json!({"amount": 0.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
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,20 @@ fn move_effect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "ids": ids_json(&ids), "index": index, "item": item.flatten() }))
|
||||
}
|
||||
|
||||
/// The ids of the effects `effect.setParams` with `p` edits, for the check of their parameters
|
||||
/// (none when `p` doesn't say which: running it reports why).
|
||||
pub(crate) fn set_params_effects(s: &Session, p: &Value) -> Vec<String> {
|
||||
const C: &str = "effect.setParams";
|
||||
let (Ok(index), Ok(item), Ok(ids), Ok(st)) = (index_param(p, "index", C), item_target(s, p, C), appearance_targets(s, p), s.doc()) else {
|
||||
return vec![];
|
||||
};
|
||||
let effect = |id: &NodeId| {
|
||||
let ap = &st.doc.node(*id)?.appearance;
|
||||
Some(ap.effects_at(item.effects_item(ap, C).ok()?)?.get(index)?.id.clone())
|
||||
};
|
||||
ids.iter().filter_map(effect).collect()
|
||||
}
|
||||
|
||||
fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "effect.setParams";
|
||||
let index = index_param(p, "index", C)?;
|
||||
@@ -566,7 +580,7 @@ mod tests {
|
||||
for other in ["threeD.revolve", "threeD.extrude", "threeD.rotate"] {
|
||||
assert!(s.execute("effect.apply", &json!({"effect": other, "ids": [id.0], "item": null})).is_err(), "{effect} + {other}");
|
||||
}
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"rotationY": 50, "depth": 120}, "ids": [id.0], "item": null})).unwrap();
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"rotationY": 50, "rotationX": 10}, "ids": [id.0], "item": null})).unwrap();
|
||||
assert_ne!(effects::solid_art(&node(&s, id)).unwrap(), art, "{effect}: options change the art");
|
||||
assert!(s.execute("effect.setParams", &json!({"index": 0, "params": {"bevel": "nope"}, "ids": [id.0], "item": null})).is_err());
|
||||
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
//!
|
||||
//! Callers outside the app (the control channel, MCP, the CLI) are answered with a parameter error
|
||||
//! for a key the description doesn't name ([`check_params`]): a misspelt `widht` no longer runs the
|
||||
//! command with its default. [`Session::execute`] itself takes what it is given, so the journal of
|
||||
//! an older version still replays.
|
||||
//! command with its default. So are the `params` of an effect (#1592): `effect.apply` and
|
||||
//! `effect.setParams` answer a key the effect doesn't take, or a value of another type than its
|
||||
//! default, with a parameter error ([`check_effect_params`]). [`Session::execute`] itself takes
|
||||
//! what it is given, so the journal of an older version still replays.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -150,6 +152,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());
|
||||
@@ -183,15 +191,56 @@ pub fn check_keys(id: &str, doc: &'static str, params: &Value) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(keys) = accepted(doc, 0) else { return Ok(()) };
|
||||
let Some(key) = given.keys().find(|k| !keys.contains(&k.as_str())) else { return Ok(()) };
|
||||
Err(bad(
|
||||
match given.keys().find(|k| !keys.contains(&k.as_str())) {
|
||||
Some(key) => Err(unknown(id, key, &keys)),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameter error of command `id` for `key`, which isn't one of `keys`: with the nearest of
|
||||
/// them, or all of them.
|
||||
fn unknown(id: &str, key: &str, keys: &[&str]) -> crate::EngineError {
|
||||
bad(
|
||||
id,
|
||||
match nearest(key, &keys) {
|
||||
match nearest(key, keys) {
|
||||
Some(near) => format!("unknown parameter `{key}`; did you mean `{near}`?"),
|
||||
None if keys.is_empty() => format!("unknown parameter `{key}`; the command takes no parameters"),
|
||||
None => format!("unknown parameter `{key}`; the command takes: {}", keys.join(", ")),
|
||||
},
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
/// `params` of built-in effect `effect`, as command `id` was given them (#1592): only keys the
|
||||
/// effect takes (those of its dialog defaults and of its params description), each of its
|
||||
/// default's type. Numbers out of range pass: the effect clamps them when it draws. An effect
|
||||
/// plug-in resolves its own against its manifest, and an unknown effect is the command's to report.
|
||||
pub fn check_effect_params(id: &str, effect: &str, params: &Value) -> Result<()> {
|
||||
let Value::Object(given) = params else { return Ok(()) };
|
||||
if effect.starts_with(vectorcraft_plugins::effect::PREFIX) {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(info) = vectorcraft_render::effects::effect_info(effect) else { return Ok(()) };
|
||||
let defaults = info.defaults.as_object();
|
||||
let mut keys: Vec<&str> = defaults.into_iter().flat_map(|d| d.keys().map(String::as_str)).collect();
|
||||
// A description that doesn't parse names no more keys (the tests rule that out).
|
||||
for key in doc_keys(info.params).map(|d| d.keys).unwrap_or_default() {
|
||||
if !keys.contains(&key) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
for (key, v) in given {
|
||||
if !keys.contains(&key.as_str()) {
|
||||
return Err(unknown(id, key, &keys));
|
||||
}
|
||||
match defaults.and_then(|d| d.get(key)) {
|
||||
Some(Value::Number(_)) if !v.is_number() => return Err(bad(id, format!("`{key}` must be a number"))),
|
||||
Some(Value::Bool(_)) if !v.is_boolean() => return Err(bad(id, format!("`{key}` must be true or false"))),
|
||||
// (A colour is also taken as `[r, g, b]` or as a colour object.)
|
||||
Some(Value::String(_)) if v.is_number() || v.is_boolean() => return Err(bad(id, format!("`{key}` must be a string"))),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commands whose params aren't checked yet (#1442): they read keys in ways the audit of the
|
||||
@@ -327,10 +376,16 @@ pub const UNCHECKED: &[&str] = &[
|
||||
"webExport.settings",
|
||||
];
|
||||
|
||||
/// [`check_keys`] for an engine command, the steps of a `command.batch` included. A command the
|
||||
/// engine doesn't have passes: running it reports that.
|
||||
/// [`check_keys`] for an engine command, the steps of a `command.batch` included, and
|
||||
/// [`check_effect_params`] for the effect `effect.apply` adds. A command the engine doesn't have
|
||||
/// passes: running it reports that.
|
||||
pub fn check_params(id: &str, params: &Value) -> Result<()> {
|
||||
let Some(spec) = find_command(id) else { return Ok(()) };
|
||||
if let ("effect.apply" | "appearance.addEffect", Some(effect), Some(given)) =
|
||||
(id, params.get("effect").or_else(|| params.get("id")).and_then(Value::as_str), params.get("params"))
|
||||
{
|
||||
check_effect_params(id, effect, given)?;
|
||||
}
|
||||
if UNCHECKED.contains(&id) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -344,3 +399,18 @@ pub fn check_params(id: &str, params: &Value) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl crate::Session {
|
||||
/// [`check_params`], and [`check_effect_params`] for the effects `effect.setParams` edits in
|
||||
/// this session's document (not as a step of a `command.batch`: what it edits is only there
|
||||
/// once the steps before it ran).
|
||||
pub fn check_params(&self, id: &str, params: &Value) -> Result<()> {
|
||||
check_params(id, params)?;
|
||||
if let ("effect.setParams", Some(given)) = (id, params.get("params")) {
|
||||
for effect in super::effectcmd::set_params_effects(self, params) {
|
||||
check_effect_params(id, &effect, given)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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,16 +1300,35 @@ 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()))?;
|
||||
// The crate's own tests are the audit of the params descriptions (#1442): a key a test
|
||||
// passes that its command's description doesn't name fails the test.
|
||||
#[cfg(test)]
|
||||
cmd::params::check_params(id, params)?;
|
||||
self.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()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::cmd::params::{UNCHECKED, check_keys, check_params, doc_keys};
|
||||
use crate::cmd::params::{UNCHECKED, check_effect_params, check_keys, check_params, doc_keys};
|
||||
use crate::{EngineError, Session, command_specs, find_command};
|
||||
|
||||
fn keys(doc: &str) -> Vec<&str> {
|
||||
@@ -95,6 +95,50 @@ fn a_batch_checks_its_steps() {
|
||||
assert_eq!(refusal("command.batch", json!({"steps": []})), "unknown parameter `steps`; the command takes: label, commands, artboard");
|
||||
}
|
||||
|
||||
/// #1592: the `params` of an effect are held to the effect's own keys and types, where the effect
|
||||
/// is named (`effect.apply`) and where the document says which it is (`effect.setParams`).
|
||||
#[test]
|
||||
fn effect_params_are_the_effects_own() {
|
||||
use vectorcraft_render::effects::effect_catalog;
|
||||
for e in effect_catalog() {
|
||||
doc_keys(e.params).unwrap_or_else(|why| panic!("{}: {why}", e.id));
|
||||
check_effect_params("effect.apply", e.id, &e.defaults).unwrap_or_else(|why| panic!("{}'s defaults: {why}", e.id));
|
||||
}
|
||||
let apply = |effect: &str, params| json!({"effect": effect, "params": params});
|
||||
assert_eq!(refusal("effect.apply", apply("blur.gaussian", json!({"blurr": 3}))), "unknown parameter `blurr`; the command takes: radius");
|
||||
assert_eq!(
|
||||
refusal("appearance.addEffect", apply("stylize.dropShadow", json!({"opacty": 50}))),
|
||||
"unknown parameter `opacty`; did you mean `opacity`?"
|
||||
);
|
||||
assert_eq!(refusal("effect.apply", apply("blur.gaussian", json!({"radius": "big"}))), "`radius` must be a number");
|
||||
assert_eq!(refusal("effect.apply", apply("threeD.revolve", json!({"shade": 1}))), "`shade` must be true or false");
|
||||
assert_eq!(refusal("effect.apply", apply("threeD.revolve", json!({"edge": 1}))), "`edge` must be a string");
|
||||
// Out of range is the effect's to clamp; a key only its description names is one it takes;
|
||||
// a plug-in's are its manifest's; an unknown effect is the command's to report.
|
||||
for (effect, params) in [
|
||||
("stylize.dropShadow", json!({"opacity": 500, "x": -3, "color": "#ff0000"})),
|
||||
("stylize.dropShadow", json!({"color": [1, 0, 0]})),
|
||||
("threeD.revolve", json!({"segments": 100000})),
|
||||
("path.outlineStroke", json!({"width": 2})),
|
||||
("plugin.anything", json!({"whatever": 1})),
|
||||
("no.such", json!({"whatever": 1})),
|
||||
] {
|
||||
assert!(check_params("effect.apply", &apply(effect, params.clone())).is_ok(), "{effect} {params}");
|
||||
}
|
||||
// `effect.setParams` names an index: the session knows the effect there.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
s.execute("effect.apply", &apply("blur.gaussian", json!({"radius": 2}))).unwrap();
|
||||
for (params, why) in [(json!({"blurr": 3}), "unknown parameter `blurr`"), (json!({"radius": true}), "`radius` must be a number")] {
|
||||
let e = s.execute("effect.setParams", &json!({"index": 0, "params": params})).unwrap_err().to_string();
|
||||
assert!(e.contains(why), "{e}");
|
||||
}
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"radius": 1e9}, "visible": false})).unwrap();
|
||||
// No effect at the index: the command says so, not the check.
|
||||
assert!(s.check_params("effect.setParams", &json!({"index": 7, "params": {"blurr": 3}})).is_ok());
|
||||
}
|
||||
|
||||
/// What a command notes for its journal entry is a key its description names, so the entry
|
||||
/// passes the check a caller's params get.
|
||||
#[test]
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -106,10 +106,19 @@ fn listed_applied_edited_and_undone() {
|
||||
assert_eq!((&e["raster"], &e["menu"]), (&json!(true), &json!(["Effect", menu])), "{id}");
|
||||
}
|
||||
}
|
||||
// Junk parameters are stored as given and read clamped; the canvas still draws.
|
||||
// Numbers far out of range are stored as given and read clamped; the canvas still draws.
|
||||
for (id, _) in EFFECTS {
|
||||
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": {"amount": 1e308, "radius": -1e308, "threshold": "x", "quality": 7}}))
|
||||
.unwrap();
|
||||
let defaults = vectorcraft_render::effects::default_params(id).unwrap();
|
||||
let junk: serde_json::Map<String, Value> = defaults
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, v)| v.is_number())
|
||||
.enumerate()
|
||||
.map(|(i, (k, _))| (k.clone(), json!(if i % 2 == 0 { 1e308 } else { -1e308 })))
|
||||
.collect();
|
||||
assert!(!junk.is_empty(), "{id}");
|
||||
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": junk})).unwrap();
|
||||
}
|
||||
let _ = render(&s);
|
||||
let n = s.doc().unwrap().doc.node(NodeId(g)).unwrap().clone();
|
||||
|
||||
@@ -22,6 +22,28 @@ fn a_journal_with_a_key_no_longer_read_still_replays() {
|
||||
assert_eq!((st.doc.artboards[0].rect.width(), st.doc.layers[0].children().unwrap().len()), (300.0, 1));
|
||||
}
|
||||
|
||||
/// So do an effect's own parameters (#1592): a journal or a file with a key the effect doesn't
|
||||
/// take, or a value of another type, replays and opens; the effect reads what it knows, clamped.
|
||||
#[test]
|
||||
fn effect_params_no_longer_taken_still_replay_and_open() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
let old = [
|
||||
("effect.apply", json!({"effect": "blur.gaussian", "params": {"blurr": 3, "radius": "big"}})),
|
||||
("effect.setParams", json!({"index": 0, "params": {"amount": 1e308}})),
|
||||
];
|
||||
for (id, params) in &old {
|
||||
assert!(s.check_params(id, params).is_err(), "{id}");
|
||||
s.execute(id, params).unwrap();
|
||||
}
|
||||
let doc = &s.active().unwrap().doc;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap();
|
||||
let stored = |d: &vectorcraft_doc::Document| d.layers[0].children().unwrap()[0].appearance.effects[0].params.clone();
|
||||
assert_eq!(stored(&back), stored(doc));
|
||||
assert_eq!(stored(&back)["blurr"], json!(3));
|
||||
}
|
||||
|
||||
/// What a command adds to its own journal entry passes the check, so a recorded journal can be
|
||||
/// sent back through the control channel or MCP.
|
||||
#[test]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Headless {
|
||||
// A key the command's description doesn't name is an error, not a default (#1442).
|
||||
match HOST_COMMANDS.iter().find(|c| c.0 == id) {
|
||||
Some((_, _, doc)) => vectorcraft_engine::check_keys(id, doc, params),
|
||||
None => vectorcraft_engine::check_params(id, params),
|
||||
None => self.session.check_params(id, params),
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
match id {
|
||||
|
||||
+81
-19
@@ -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(),
|
||||
}
|
||||
@@ -1234,11 +1241,50 @@ impl<'p> Builder<'p> {
|
||||
}
|
||||
|
||||
fn flush_text(&mut self) {
|
||||
let Some((t, opacity, facts)) = self.text.take().and_then(TextLine::finish) else { return };
|
||||
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
|
||||
n.opacity = opacity;
|
||||
self.lines.insert(n.id, facts);
|
||||
self.push_node(n);
|
||||
let Some(mut line) = self.text.take() else { return };
|
||||
let held = std::mem::take(&mut line.held);
|
||||
if let Some((t, opacity, facts)) = line.finish() {
|
||||
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
|
||||
n.opacity = opacity;
|
||||
self.lines.insert(n.id, facts);
|
||||
self.push_node(n);
|
||||
}
|
||||
// The strokes of its glyphs' outlines, right after it ([`import_lines`] makes them its stroke).
|
||||
for n in held {
|
||||
self.push_node(n);
|
||||
}
|
||||
}
|
||||
|
||||
/// A stroke of the outline of the glyph just drawn, in the glyph's own paint, while every glyph
|
||||
/// before it on the open line was stroked so too, is held with the line instead of ending it
|
||||
/// (an editor writes type on a path glyph by glyph, each followed by the stroke added to the
|
||||
/// type). `false`: it isn't one.
|
||||
fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &PathDrawMode) -> bool {
|
||||
let PathDrawMode::Stroke(props) = draw_mode else { return false };
|
||||
if self.mask.is_some() || self.blend != BlendMode::Normal || matches!(paint, hayro_interpret::Paint::Pattern(_)) {
|
||||
return false;
|
||||
}
|
||||
let (p, opacity) = self.paint(paint, true);
|
||||
let Some(line) = &self.text else { return false };
|
||||
let Some(ink) = line.ink.last() else { return false };
|
||||
if line.held.len() + 1 != line.ink.len() || opacity < 0.999 || !line.last_fill().is_some_and(|f| *f == p) {
|
||||
return false;
|
||||
}
|
||||
let mut bp = path.clone();
|
||||
bp.apply_affine(transform);
|
||||
let b = bp.bounding_box();
|
||||
let tol = 0.01 * (ink.width() + ink.height()) + 0.01;
|
||||
if ![(b.x0, ink.x0), (b.y0, ink.y0), (b.x1, ink.x1), (b.y1, ink.y1)].iter().all(|(a, c)| (a - c).abs() <= tol) {
|
||||
return false;
|
||||
}
|
||||
let mut st = stroke_layer(p, opacity, props, mean_scale(transform));
|
||||
// Every painted path takes its turn in the scan's overprint list, a held one too.
|
||||
st.overprint = self.path_overprint();
|
||||
let n = Node::path(self.id(), PathData::from_bezpath(&bp), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
|
||||
if let Some(line) = &mut self.text {
|
||||
line.held.push(n);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Convert a hayro paint to a VectorCraft paint and opacity (`stroke`: for a stroke).
|
||||
@@ -1299,7 +1345,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 +1379,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;
|
||||
@@ -1652,6 +1706,10 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||
|
||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &PathDrawMode) {
|
||||
self.flush_glyphs();
|
||||
if self.hold_glyph_stroke(path, transform, paint, draw_mode) {
|
||||
self.pending = false;
|
||||
return;
|
||||
}
|
||||
self.flush_text();
|
||||
self.pending = false;
|
||||
let overprint = self.path_overprint();
|
||||
@@ -1754,14 +1812,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 +1890,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 +1916,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 +1934,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 +1950,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],
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{CharStyle, ParaDirection, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_doc::{CharStyle, Node, ParaDirection, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_text::TextLayout;
|
||||
|
||||
/// One glyph's placement: its baseline origin, advance direction, size and horizontal scale.
|
||||
@@ -144,6 +144,9 @@ pub(crate) struct TextLine {
|
||||
gaps: Vec<(usize, f64)>,
|
||||
/// Each glyph's advance in the file's font (points), with [`Self::baseline`].
|
||||
advances: Vec<f64>,
|
||||
/// The strokes of its glyphs' outlines drawn between them, one per glyph with ink so far
|
||||
/// (written right after the line).
|
||||
pub held: Vec<Node>,
|
||||
}
|
||||
|
||||
/// What a finished line of type tells beyond its text object (see [`crate::import_lines`]).
|
||||
@@ -177,9 +180,15 @@ impl TextLine {
|
||||
ink: vec![],
|
||||
gaps: vec![],
|
||||
advances: vec![],
|
||||
held: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// The paint the last glyph is filled with.
|
||||
pub fn last_fill(&self) -> Option<&Paint> {
|
||||
self.runs.last()?.0.fill.as_ref()
|
||||
}
|
||||
|
||||
/// Add glyph `text` set vertically (`upright`) if it continues this column (or makes this
|
||||
/// line's single glyph a column): below the last one, in the same column. `false`: it starts
|
||||
/// another line.
|
||||
@@ -350,6 +359,20 @@ impl TextLine {
|
||||
t.runs = tracked_each(std::mem::take(&mut t.runs), own, self.at.size);
|
||||
}
|
||||
if let Some(path) = path {
|
||||
// Evenly spaced type keeps its length along the path: the path runs through the
|
||||
// glyphs' origins, one curve each, to the end of the last glyph, and tracking makes up
|
||||
// the difference from the advances.
|
||||
if own.is_none() {
|
||||
let length: f64 = path.segments().take(self.baseline.len()).map(|seg| kurbo::ParamCurveArclen::arclen(&seg, 1e-3)).sum();
|
||||
let laid = vectorcraft_text::layout(db, &t);
|
||||
let natural: f64 = laid.glyphs.iter().map(|g| g.advance).sum();
|
||||
let chars: usize = t.runs.iter().map(|r| r.text.chars().count()).sum();
|
||||
let size = self.at.size;
|
||||
if chars > 1 && length.is_finite() && (length - natural).abs() > size * 0.01 {
|
||||
let (tracking, spaces) = spacing(&laid, &[], length - natural, chars, size);
|
||||
t.runs = tracked(std::mem::take(&mut t.runs), tracking, &spaces);
|
||||
}
|
||||
}
|
||||
t.kind = TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&path), start: 0.0, end: None };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.cached_bounds = Some(vectorcraft_text::layout(db, &t).bounds);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -153,6 +153,25 @@ fn a_stroke_on_the_type_of_a_moved_ai_page_is_still_the_types_stroke() {
|
||||
assert!(matches!(art[1].appearance.items.as_slice(), [AppearanceItem::Stroke(_)]), "{:?}", art[1].appearance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyphs_each_followed_by_their_stroked_outline_stay_one_object() {
|
||||
// Type on a path is written glyph by glyph: each glyph, then its outline stroked in the same
|
||||
// colour (a shadow under the type).
|
||||
let glyph = |c: char, x: f64, stroke: &str| format!("BT 0 0 1 rg /F1 24 Tf {x} 50 Td ({c}) Tj ET {stroke} 0.5 w {}", outline(c, x));
|
||||
let i_at = 10.0 + 0.722 * 24.0;
|
||||
let d = import(&page(&(glyph('H', 10.0, "0 0 1 RG") + &glyph('i', i_at, "0 0 1 RG")))).unwrap();
|
||||
let art = d.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 1, "one object: {art:?}");
|
||||
let NodeKind::Text(t) = &art[0].kind else { panic!("{:?}", art[0].kind) };
|
||||
assert_eq!(t.plain_text(), "Hi");
|
||||
let [AppearanceItem::Stroke(st)] = art[0].appearance.items.as_slice() else { panic!("{:?}", art[0].appearance) };
|
||||
assert_eq!(st.paint.color(), Some(Color::rgb(0.0, 0.0, 1.0)));
|
||||
// A stroke of another colour drawn over each glyph in turn would also cover the next one's
|
||||
// neighbour as one object's stroke: the glyphs stay as the file draws them.
|
||||
let d = import(&page(&(glyph('H', 10.0, "1 1 1 RG") + &glyph('i', i_at, "1 1 1 RG")))).unwrap();
|
||||
assert_eq!(texts(&d).len(), 2);
|
||||
}
|
||||
|
||||
/// A 300 × 200 pt page with `lines` of (x, y from the top, size, text) in Source Sans 3.
|
||||
fn source_sans(lines: &[(f32, f32, f32, &str)]) -> Vec<u8> {
|
||||
crate::tests_live_text::text_pdf(lines, false)
|
||||
|
||||
@@ -424,3 +424,22 @@ fn glyphs_spaced_each_their_own_way_keep_their_place() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Evenly tracked type on a path comes back with its tracking: each glyph where it was along the
|
||||
/// curve, not closed up to the font's advances.
|
||||
#[test]
|
||||
fn evenly_tracked_type_on_a_path_keeps_its_length() {
|
||||
let mut arc = BezPath::new();
|
||||
arc.move_to((40.0, 200.0));
|
||||
arc.curve_to((100.0, 80.0), (260.0, 80.0), (320.0, 200.0));
|
||||
let mut t = TextObject::point(Point::ORIGIN, "ARCHING TYPE", CharStyle { tracking: 80.0, ..style(24.0) });
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&arc), start: 0.0, end: None };
|
||||
let d = doc(vec![t]);
|
||||
let want = origins(&d);
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::default());
|
||||
let got = origins(&back);
|
||||
assert_eq!(got.len(), want.len(), "{:?}", texts(&back));
|
||||
for (g, w) in got.iter().zip(&want) {
|
||||
assert!(g.distance(*w) < 0.3, "{got:?}\nvs {want:?}");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
let params = p.get("params").cloned().unwrap_or(json!({}));
|
||||
let params = if params.is_null() { json!({}) } else { params };
|
||||
// A key the command's description doesn't name is an error, not a default (#1442).
|
||||
if let Err(e) = crate::menus::check_params(id, ¶ms) {
|
||||
if let Err(e) = crate::menus::check_params(&app.session, id, ¶ms) {
|
||||
return err(e);
|
||||
}
|
||||
wrap(app.run(id, params))
|
||||
|
||||
@@ -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)]
|
||||
@@ -621,7 +623,7 @@ impl VectorcraftApp {
|
||||
// In debug builds the app's own calls are held to the params descriptions too (#1442): a
|
||||
// panel passing a key its command doesn't read shows up in the tests.
|
||||
if cfg!(debug_assertions) {
|
||||
menus::check_params(id, ¶ms)?;
|
||||
menus::check_params(&self.session, id, ¶ms)?;
|
||||
}
|
||||
if let Some(r) = menus::run_ui_command(self, id, ¶ms) {
|
||||
return r;
|
||||
|
||||
@@ -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…",
|
||||
@@ -899,11 +905,11 @@ pub(crate) fn opt_bool(p: &Value, key: &str) -> Result<Option<bool>, String> {
|
||||
/// `params` has only keys the params description of engine command `id` names, or the parameter
|
||||
/// error naming the one it doesn't (#1442). The UI's own commands aren't checked yet: their
|
||||
/// descriptions haven't been audited against what they read.
|
||||
pub fn check_params(id: &str, params: &Value) -> Result<(), String> {
|
||||
pub fn check_params(session: &vectorcraft_engine::Session, id: &str, params: &Value) -> Result<(), String> {
|
||||
if UI_COMMANDS.iter().any(|c| c.0 == id) {
|
||||
return Ok(());
|
||||
}
|
||||
vectorcraft_engine::check_params(id, params).map_err(|e| e.to_string())
|
||||
session.check_params(id, params).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
|
||||
@@ -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 |
|
||||
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user