Compare commits

...
Author SHA1 Message Date
bflatastic c10da25ce4 Importers: nesting is capped, so a deeply nested file can't overflow the stack 2026-10-14 07:37:52 +07:00
BFlat 8ab574b7de Merge pull request #1848 from storytold/u/trace-gradients2
Image Trace: gradients grown over noise and soft rims
2026-10-14 07:18:39 +07:00
bflatastic e0ba6e77d1 Image Trace: gradients grown over noise and soft rims
Gradients (off unless asked for) found a blurred or noisy linear gradient in
part or in pieces, with bands around it.

The areas that may be a gradient are tried largest first, and one that is a
gradient is grown over every pixel next to it whose colour is within 10 levels
of the gradient's there: the pixels noise put a little off, a soft rim, the
pieces noise cut off it.

Linear-gradient inputs with Gradients on, paths beside the gradient, before ->
after: blurred 3.0 -> 0.5 (6 Colors) and 55.5 -> 0.5 (High Fidelity Photo);
noisy 11.0 -> 0.5 and 23.5 -> 1.5; scanned 5.5 -> 3.0 and 46.5 -> 37.0; clean
and aliased as before; dE within 0.02. No gradient on any input that is flat
colour, with any preset (the harness fails if there is one).
2026-10-14 07:14:07 +07:00
BFlat 9c289db91d Merge pull request #1847 from storytold/u/trace-enhanced
Image Trace: enhanced versions of the presets
2026-10-14 07:08:27 +07:00
BFlat b1a5b79d67 Merge pull request #1846 from storytold/u/trace-caps
Image Trace: stroked lines get square caps where their ink is cut square
2026-10-14 07:07:45 +07:00
BFlat 156fbe9912 Merge pull request #1845 from storytold/u/trace-shapes2
Image Trace: Shapes finds rounded rectangles and shapes in aliased, blurred and scanned art
2026-10-14 07:07:15 +07:00
38 changed files with 1713 additions and 235 deletions

No files matched your search

+1
View File
@@ -214,6 +214,7 @@ pub fn import(bytes: &[u8], o: &ImportOptions) -> Result<Imported, String> {
let (mut document, mut warnings) = build::Build::new(&d, o).run(layout, natural, ratio)?;
document.units = if o.fit { unit } else { o.unit.unwrap_or(unit) };
warnings.extend(damaged);
document.limit_depth_noting(&mut warnings);
Ok(Imported { document, warnings })
}
+4 -2
View File
@@ -24,8 +24,10 @@ pub struct Clip {
}
/// Deepest nesting of clips, and of groups, an importer records ([`nest`] goes one level deeper
/// for each): deeper ones are left out.
pub const MAX_NEST: usize = 128;
/// for each): deeper ones are left out. As deep as a document's objects may nest
/// ([`crate::MAX_DEPTH`]) under a layer, around the art; clips in groups that deep together are
/// merged by [`Document::limit_depth`].
pub const MAX_NEST: usize = crate::MAX_DEPTH - 2;
/// The warning for clips left out for nesting deeper than [`MAX_NEST`].
pub fn deep_clips() -> String {
+588
View File
@@ -0,0 +1,588 @@
//! How deep objects may nest.
//!
//! The tree is walked by recursion everywhere (drawing, bounds, hit tests, saving, exporting), so
//! a tree of unbounded depth overflows the stack: an abort no guard catches. No document deeper
//! than [`MAX_DEPTH`] is ever kept, nor one whose symbols and patterns draw deeper than
//! [`MAX_DRAWN_DEPTH`] (a symbol holding an instance of itself, or a pattern painting its own
//! tile, draws without end): importers and the native reader merge deeper objects into the group
//! around them and leave such instances and paints out
//! ([`Document::limit_depth`], with [`merged_note`] and [`symbols_note`] as their warnings), and
//! the engine refuses an edit that would nest deeper ([`Document::too_deep`]).
use std::collections::HashMap;
use std::sync::Arc;
use vectorcraft_color::Paint;
use crate::{Document, Node, NodeKind};
/// The deepest an object may be: a top-level layer is 1 deep, what it holds 2, and so on (the art
/// of an opacity mask is one deeper than the object it masks). A symbol's or pattern's art counts
/// from 1 again.
///
/// A native file writes three levels of JSON for each level of objects, and its reader stops at
/// 128 levels: 32 leaves the deepest object 29 of them for its own data, so every document that
/// can be made can be saved and opened again.
pub const MAX_DEPTH: usize = 32;
/// The deepest an object may be drawn: as [`MAX_DEPTH`], the art of a symbol counted under its
/// instance and a pattern's tile under the object it paints (and what those use, under them).
/// Symbols a few objects deep nest in one another dozens deep within it.
pub const MAX_DRAWN_DEPTH: usize = 4 * MAX_DEPTH;
/// The warning of a file whose objects were nested deeper than [`MAX_DEPTH`].
pub fn merged_note() -> String {
format!("objects nested more than {MAX_DEPTH} deep were merged into the group around them")
}
/// The warning of a file with symbols or patterns that use themselves or are drawn deeper than
/// [`MAX_DRAWN_DEPTH`].
pub fn symbols_note() -> String {
format!(
"symbol instances and pattern paints inside their own symbol or pattern, or drawn more than {MAX_DRAWN_DEPTH} objects deep, were left out"
)
}
/// Run `f` where recursion has room: on a thread of its own with a 16 MB stack (the main thread
/// has 1 MB on Windows), or here where there are no threads (the web) or none can be started. For
/// readers whose recursion is capped but heavy; a panic in `f` goes on to the caller.
pub fn with_deep_stack<T: Send>(f: impl Fn() -> T + Sync) -> T {
#[cfg(not(target_arch = "wasm32"))]
{
let threaded = std::thread::scope(|s| {
let thread = std::thread::Builder::new().stack_size(16 << 20).spawn_scoped(s, &f).ok()?;
Some(thread.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic)))
});
if let Some(done) = threaded {
return done;
}
}
f()
}
/// Is anything in `now` (each `depth` deep) deeper than [`MAX_DEPTH`], or `over` (given its
/// depth)? Without recursion: the trees may be of any depth. With `before` (the same list before
/// an edit, within the caps), only what the edit changed is looked at: a subtree still shared with
/// it, in the same place, is as it was.
fn deeper(now: &[Arc<Node>], before: Option<&[Arc<Node>]>, depth: usize, over: &dyn Fn(&Node, usize) -> bool) -> bool {
type Pending<'a> = Vec<(&'a Node, Option<&'a Node>, usize)>;
fn push<'a>(stack: &mut Pending<'a>, now: &'a [Arc<Node>], before: Option<&'a [Arc<Node>]>, depth: usize) {
for (i, n) in now.iter().enumerate() {
let was = before.and_then(|b| b.get(i));
if !was.is_some_and(|w| Arc::ptr_eq(w, n)) {
stack.push((n, was.map(|w| &**w), depth));
}
}
}
let mut stack = vec![];
push(&mut stack, now, before, depth);
while let Some((n, was, d)) = stack.pop() {
if d > MAX_DEPTH || over(n, d) {
return true;
}
if let Some(ch) = n.children() {
push(&mut stack, ch, was.and_then(Node::children).map(Vec::as_slice), d + 1);
}
if let Some(m) = &n.extras().mask {
let was = was.and_then(|w| w.extras().mask.as_ref()).map(|m| std::slice::from_ref(&m.art));
push(&mut stack, std::slice::from_ref(&m.art), was, d + 1);
}
}
false
}
/// [`deeper`] for the nesting alone.
fn nested_deeper(now: &[Arc<Node>], depth: usize) -> bool {
deeper(now, None, depth, &|_, _| false)
}
/// Merge what is deeper than [`MAX_DEPTH`] in `nodes` (each `depth` deep) into the group around
/// it: a container at the cap is replaced by what it holds (a clipping group's clipping path and
/// an opacity mask too deep are left out); what it held stays hidden or locked as it was. Returns
/// whether anything changed. Recurses no deeper than the cap.
fn limit(nodes: &mut Vec<Arc<Node>>, depth: usize) -> bool {
let mut changed = false;
let mut i = 0;
while let Some(n) = nodes.get_mut(i) {
if !nested_deeper(std::slice::from_ref(n), depth) {
i += 1;
continue;
}
changed = true;
let n = Arc::make_mut(n);
if n.extras().mask.as_ref().is_some_and(|m| nested_deeper(std::slice::from_ref(&m.art), depth + 1)) {
n.extras_mut().mask = None;
}
if depth < MAX_DEPTH {
if let Some(ch) = n.children_mut() {
limit(ch, depth + 1);
}
i += 1;
continue;
}
let clip = matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. });
let (visible, locked) = (n.visible, n.locked);
let mut held = n.children_mut().map(std::mem::take).unwrap_or_default();
if clip && !held.is_empty() {
held.remove(0);
}
for c in &mut held {
if (!visible && c.visible) || (locked && !c.locked) {
let c = Arc::make_mut(c);
c.visible &= visible;
c.locked |= locked;
}
}
// What it held takes its place, and is looked at in its turn.
nodes.splice(i..=i, held);
}
changed
}
/// A symbol (`false`) or a pattern (`true`) of the document, by name: what art draws by name, an
/// instance its symbol's art and a pattern paint its tile's.
type Def<'a> = (bool, &'a str);
/// How deep each symbol's and pattern's art is drawn, the art of the symbols and patterns it uses
/// included (drawn under the object that uses them): `None` for one that uses itself, in its art
/// or through what that uses, or is drawn deeper than [`MAX_DRAWN_DEPTH`].
type Heights<'a> = HashMap<Def<'a>, Option<usize>>;
/// What `n` itself draws by name: the symbol it is an instance of, the patterns that paint it.
fn uses(n: &Node) -> impl Iterator<Item = Def<'_>> {
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => Some((false, symbol.as_str())),
_ => None,
};
let patterns = crate::swatches::node_paints(n).filter_map(|p| match p {
Paint::Pattern { pattern, .. } => Some((true, pattern.as_str())),
_ => None,
});
symbol.into_iter().chain(patterns)
}
/// [`Heights`] of the document's symbols and patterns. Without recursion: they may use one
/// another in a chain of any length. Their own art is within [`MAX_DEPTH`].
fn heights(doc: &Document) -> Heights<'_> {
// Each one's own depth, and what its art uses with the deepest use of each.
type Own<'a> = (usize, Vec<(Def<'a>, usize)>);
let arts = (doc.symbols.iter().map(|s| ((false, s.name.as_str()), std::slice::from_ref(&s.art))))
.chain(doc.patterns.iter().map(|p| ((true, p.name.as_str()), &p.art[..])));
let mut own: HashMap<Def, Own> = HashMap::new();
for (def, art) in arts {
let (mut deepest, mut used) = (0, HashMap::<Def, usize>::new());
let mut stack: Vec<(&Node, usize)> = art.iter().map(|n| (&**n, 1)).collect();
while let Some((n, d)) = stack.pop() {
deepest = deepest.max(d);
for u in uses(n) {
let at = used.entry(u).or_default();
*at = (*at).max(d);
}
stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| (&**c, d + 1)));
}
// (The first of two of one name is the one drawn.)
own.entry(def).or_insert((deepest, used.into_iter().collect()));
}
// Depth first over what uses what. One met again while it is being worked out (`None` in its
// entry) uses itself.
let mut found: HashMap<Def, Option<Option<usize>>> = HashMap::new();
let starts: Vec<Def> = own.keys().copied().collect();
for start in starts {
if found.contains_key(&start) {
continue;
}
// (which, the next of its uses to look at, the deepest found so far: `None` once it can't be drawn)
let mut path: Vec<(Def, usize, Option<usize>)> = vec![(start, 0, own.get(&start).map(|o| o.0))];
found.insert(start, None);
while let Some(&(def, next, deepest)) = path.last() {
let used = own.get(&def).map_or(&[][..], |o| &o.1);
let Some(&(other, at)) = used.get(next) else {
// Everything it uses is worked out.
let deepest = deepest.filter(|d| *d <= MAX_DRAWN_DEPTH);
found.insert(def, Some(deepest));
path.pop();
if let Some((user, next, held)) = path.last_mut() {
let at = own.get(user).and_then(|o| o.1.get(next.wrapping_sub(1))).map_or(1, |u| u.1);
*held = held.zip(deepest).map(|(h, d)| h.max(at + d));
}
continue;
};
let known = found.get(&other).copied();
if known.is_none() && own.contains_key(&other) {
found.insert(other, None);
}
let Some((_, next, deepest)) = path.last_mut() else { break };
*next += 1;
match known {
// Not in the document: it draws nothing.
None if !own.contains_key(&other) => {}
None => path.push((other, 0, own.get(&other).map(|o| o.0))),
Some(Some(Some(d))) => *deepest = deepest.map(|h| h.max(at + d)),
// It uses itself (it is on the way down here), or can't be drawn.
Some(None | Some(None)) => *deepest = None,
}
}
}
found.into_iter().map(|(def, h)| (def, h.flatten())).collect()
}
/// Can't `used`, by an object `depth` deep, be drawn there?
fn undrawable(heights: &Heights, used: Def, depth: usize) -> bool {
heights.get(&used).is_some_and(|h| h.is_none_or(|h| depth + h > MAX_DRAWN_DEPTH))
}
/// Take out of `nodes` (each `depth` deep) the uses `bad` picks (given the depth of the object):
/// an instance goes, a pattern paint becomes no paint. Returns whether anything changed. For trees
/// within [`MAX_DEPTH`].
fn strip(nodes: &mut Vec<Arc<Node>>, depth: usize, bad: &dyn Fn(Def, usize) -> bool) -> bool {
let any = |n: &Node, d: usize| uses(n).any(|u| bad(u, d));
let before = nodes.len();
nodes.retain(|n| !matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if bad((false, symbol.as_str()), depth)));
let mut changed = nodes.len() != before;
for n in nodes.iter_mut() {
if !deeper(std::slice::from_ref(n), None, depth, &any) {
continue;
}
changed = true;
let n = Arc::make_mut(n);
if uses(n).any(|u| bad(u, depth)) {
for p in crate::swatches::node_paints_mut(n) {
if matches!(p, Paint::Pattern { pattern, .. } if bad((true, pattern.as_str()), depth)) {
*p = Paint::None;
}
}
}
// An opacity mask whose art is such an instance goes with it.
let mut mask = n.extras().mask.as_ref().map(|m| vec![m.art.clone()]);
if let Some(art) = &mut mask
&& strip(art, depth + 1, bad)
{
match art.pop() {
Some(art) => {
if let Some(m) = n.extras_mut().mask.as_mut() {
m.art = art;
}
}
None => n.extras_mut().mask = None,
}
}
if let Some(ch) = n.children_mut() {
strip(ch, depth + 1, bad);
}
}
changed
}
impl Document {
/// Is anything nested deeper than [`MAX_DEPTH`], or drawn deeper than [`MAX_DRAWN_DEPTH`]
/// (a symbol or pattern that uses itself is)? With `before` (this document before an edit,
/// itself within the caps), only what the edit changed is looked at.
pub fn too_deep(&self, before: Option<&Document>) -> bool {
fn symbol(d: &Document, i: usize) -> Option<&[Arc<Node>]> {
d.symbols.get(i).map(|s| std::slice::from_ref(&s.art))
}
fn pattern(d: &Document, i: usize) -> Option<&[Arc<Node>]> {
d.patterns.get(i).map(|p| &p.art[..])
}
let nested = |_: &Node, _: usize| false;
if (0..self.symbols.len()).any(|i| symbol(self, i).is_some_and(|art| deeper(art, before.and_then(|b| symbol(b, i)), 1, &nested)))
|| (0..self.patterns.len()).any(|i| pattern(self, i).is_some_and(|art| deeper(art, before.and_then(|b| pattern(b, i)), 1, &nested)))
{
return true;
}
if self.symbols.is_empty() && self.patterns.is_empty() {
return deeper(&self.layers, before.map(|b| &b.layers[..]), 1, &nested);
}
let heights = heights(self);
if heights.values().any(Option::is_none) {
return true;
}
// Symbols and patterns draw as deep as before when none changed; else every use of them
// is looked at again.
let same = |x: &[Arc<Node>], y: &[Arc<Node>]| x.len() == y.len() && x.iter().zip(y).all(|(x, y)| Arc::ptr_eq(x, y));
let before = before.filter(|b| {
b.symbols.len() == self.symbols.len()
&& b.patterns.len() == self.patterns.len()
&& b.symbols.iter().zip(&self.symbols).all(|(x, y)| x.name == y.name && Arc::ptr_eq(&x.art, &y.art))
&& b.patterns.iter().zip(&self.patterns).all(|(x, y)| x.name == y.name && same(&x.art, &y.art))
});
let over = |n: &Node, d: usize| uses(n).any(|u| undrawable(&heights, u, d));
deeper(&self.layers, before.map(|b| &b.layers[..]), 1, &over)
}
/// [`Self::limit_depth`] → whether objects were merged, and whether uses of symbols and
/// patterns were left out.
fn limit_depths(&mut self) -> (bool, bool) {
let mut merged = limit(&mut self.layers, 1);
for s in &mut self.symbols {
// The art itself is 1 deep, so it stays the one object it is.
let mut art = vec![s.art.clone()];
if limit(&mut art, 1)
&& let Some(one) = art.pop()
{
s.art = one;
merged = true;
}
}
for p in &mut self.patterns {
merged |= limit(&mut p.art, 1);
}
if self.symbols.is_empty() && self.patterns.is_empty() {
return (merged, false);
}
// The symbols and patterns that can't be drawn lose what their art uses: each is then as
// deep as its own art, and what uses it can be drawn.
let (symbols, patterns): (Vec<usize>, Vec<usize>) = {
let heights = heights(self);
let bad = |def: Def| heights.get(&def).is_some_and(Option::is_none);
(
(0..self.symbols.len()).filter(|i| self.symbols.get(*i).is_some_and(|s| bad((false, s.name.as_str())))).collect(),
(0..self.patterns.len()).filter(|i| self.patterns.get(*i).is_some_and(|p| bad((true, p.name.as_str())))).collect(),
)
};
let mut left_out = !(symbols.is_empty() && patterns.is_empty());
let all = |_: Def, _: usize| true;
for i in symbols {
let Some(s) = self.symbols.get_mut(i) else { continue };
let mut art = vec![s.art.clone()];
strip(&mut art, 1, &all);
// (An instance alone was the whole art: an empty group stands in.)
s.art = art.pop().unwrap_or_else(|| Arc::new(Node::group(s.art.id, vec![])));
}
for i in patterns {
if let Some(p) = self.patterns.get_mut(i) {
strip(&mut p.art, 1, &all);
}
}
// Then the uses drawn too deep where they are.
let mut layers = self.layers.clone();
left_out |= {
let heights = heights(self);
strip(&mut layers, 1, &|used, depth| undrawable(&heights, used, depth))
};
self.layers = layers;
(merged, left_out)
}
/// Bring the document within the caps: merge the objects nested deeper than [`MAX_DEPTH`]
/// into the group around them, and leave out the uses of symbols and patterns that can't be
/// drawn (inside their own symbol or pattern, or deeper than [`MAX_DRAWN_DEPTH`]). What an
/// importer does to what it read. Returns whether anything changed.
pub fn limit_depth(&mut self) -> bool {
let (merged, left_out) = self.limit_depths();
merged || left_out
}
/// [`Self::limit_depth`], adding [`merged_note`] and [`symbols_note`] to an importer's
/// `warnings` for what it did.
pub fn limit_depth_noting(&mut self, warnings: &mut Vec<String>) {
let (merged, left_out) = self.limit_depths();
warnings.extend(merged.then(merged_note));
warnings.extend(left_out.then(symbols_note));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{NodeId, OpacityMask};
/// One path in `levels` groups, each holding a second path beside the next group.
fn nested(levels: usize) -> Arc<Node> {
let mut id = 0;
let mut next = || {
id += 1;
NodeId(id)
};
let path = |id| Arc::new(Node::path(id, Default::default(), Default::default()));
let mut inner = path(next());
for _ in 0..levels {
inner = Arc::new(Node::group(next(), vec![inner, path(next())]));
}
inner
}
fn doc(art: Arc<Node>) -> Document {
let mut d = Document::new(100.0, 100.0);
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
ch.push(art);
}
d
}
#[test]
fn a_document_at_the_cap_is_left_alone() {
// The layer, the groups and the path in them: MAX_DEPTH deep.
let mut d = doc(nested(MAX_DEPTH - 2));
let before = d.clone();
assert!(!d.too_deep(None));
assert!(!d.limit_depth());
assert_eq!(d, before);
}
#[test]
fn deeper_objects_are_merged_into_the_group_around_them() {
for levels in [MAX_DEPTH - 1, MAX_DEPTH + 5, 100_000] {
let mut d = doc(nested(levels));
assert!(d.too_deep(None), "{levels}");
assert!(d.limit_depth(), "{levels}");
assert!(!d.too_deep(None), "{levels}");
// Every path is kept, under the layer and the groups down to the cap.
assert_eq!(d.node_count(), 1 + (MAX_DEPTH - 2) + (levels + 1), "{levels}");
assert!(!d.limit_depth(), "nothing more to merge");
}
}
#[test]
fn a_clipping_path_and_a_mask_too_deep_are_left_out_and_hidden_art_stays_hidden() {
let mut inner = Node::new(NodeId(9000), NodeKind::Group { children: vec![nested(0), nested(0)], clip: true });
inner.visible = false;
let mut art = Arc::new(inner);
for i in 0..MAX_DEPTH {
art = Arc::new(Node::group(NodeId(9001 + i as u64), vec![art]));
}
let mut masked = Node::group(NodeId(8000), vec![]);
masked.extras_mut().mask = Some(Box::new(OpacityMask::new((*nested(MAX_DEPTH)).clone(), true)));
let mut d = doc(art);
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
ch.push(Arc::new(masked));
}
assert!(d.limit_depth());
assert!(!d.too_deep(None));
let mut leaves = vec![];
d.walk(|n| {
if n.children().is_none() {
leaves.push(n.visible);
}
});
assert_eq!(leaves, [false], "the clipping path is gone, what it clipped is still hidden");
assert!(d.node(NodeId(8000)).is_some_and(|n| n.extras().mask.is_none()));
}
#[test]
fn an_edit_is_checked_by_what_it_changed() {
let before = doc(nested(MAX_DEPTH - 2));
let mut wrapped = before.clone();
if let Some(ch) = wrapped.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
let all = std::mem::take(ch);
ch.push(Arc::new(Node::group(NodeId(7000), all)));
}
assert!(wrapped.too_deep(Some(&before)), "the same art, one deeper");
let mut beside = before.clone();
if let Some(ch) = beside.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
ch.push(nested(3));
}
assert!(!beside.too_deep(Some(&before)));
assert!(!before.too_deep(Some(&before)));
}
#[test]
fn symbols_and_patterns_are_limited_too() {
let mut d = Document::new(10.0, 10.0);
d.symbols.push(crate::Symbol { name: "s".into(), art: nested(MAX_DEPTH + 3) });
assert!(d.too_deep(None));
assert!(d.limit_depth());
assert!(!d.too_deep(None));
assert!(d.symbols.first().is_some_and(|s| s.art.children().is_some()), "still one object");
}
fn instance(id: u64, of: &str) -> Arc<Node> {
Arc::new(Node::new(NodeId(id), NodeKind::SymbolInstance { symbol: of.into(), xf: Default::default() }))
}
/// `count` symbols, each `levels` groups around an instance of the next; the last one's of `last`.
fn symbols(count: usize, levels: usize, last: &str) -> Document {
let mut d = doc(instance(1, "s0"));
for i in 0..count {
let mut art = instance(2, &if i + 1 < count { format!("s{}", i + 1) } else { last.to_string() });
for _ in 0..levels {
art = Arc::new(Node::group(NodeId(3), vec![art]));
}
d.symbols.push(crate::Symbol { name: format!("s{i}"), art });
}
d
}
fn instances(d: &Document) -> usize {
let mut n = 0;
let mut count = |x: &Node| n += usize::from(matches!(x.kind, NodeKind::SymbolInstance { .. }));
d.walk(&mut count);
d.symbols.iter().for_each(|s| s.art.walk(&mut count));
n
}
#[test]
fn symbols_in_symbols_are_drawn_only_so_deep() {
// Three objects deep each (two groups and the instance), under the instance on the layer.
let fits = MAX_DRAWN_DEPTH / 3 - 1;
let mut d = symbols(fits, 2, "none");
assert!(!d.too_deep(None) && !d.limit_depth());
let mut d = symbols(fits + 2, 2, "none");
assert!(d.too_deep(None));
let before = instances(&d);
assert!(d.limit_depth() && !d.too_deep(None));
assert!(instances(&d) < before && instances(&d) > 0, "only the instances drawn too deep go");
// A chain of any length is measured, not walked by recursion.
let mut d = symbols(100_000, 0, "none");
assert!(d.too_deep(None) && d.limit_depth() && !d.too_deep(None));
}
#[test]
fn a_symbol_holding_itself_loses_the_instance() {
for (count, last) in [(1, "s0"), (3, "s0"), (3, "s1")] {
let mut d = symbols(count, 1, last);
assert!(d.too_deep(None), "{count} {last}");
let mut warnings = vec![];
d.limit_depth_noting(&mut warnings);
assert_eq!(warnings, [symbols_note()]);
assert!(!d.too_deep(None), "{count} {last}");
assert!(d.symbols.iter().all(|s| s.art.children().is_some()), "the art is still a group");
}
// An edit that makes one hold itself is seen, whatever else stayed as it was.
let before = symbols(2, 1, "none");
let mut after = before.clone();
after.symbols[1].art = Arc::new(Node::group(NodeId(9), vec![instance(8, "s0")]));
assert!(!before.too_deep(None) && after.too_deep(Some(&before)));
}
/// A path painted with the pattern named `with`.
fn painted(id: u64, with: &str) -> Arc<Node> {
let fill = Paint::Pattern { pattern: with.into(), xf: Default::default() };
Arc::new(Node::path(NodeId(id), Default::default(), crate::Appearance::basic(fill, Paint::None, 0.0)))
}
fn pattern_fills(d: &Document) -> usize {
let mut n = 0;
d.visit_paints(&mut |p| n += usize::from(matches!(p, Paint::Pattern { .. })));
n
}
#[test]
fn a_pattern_painting_its_own_tile_loses_the_paint() {
// One that paints its own tile; two that paint each other's; one in a symbol it holds.
let itself = |d: &mut Document| d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "p")]));
let each_other = |d: &mut Document| {
d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "q")]));
d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "p")]));
};
let through_a_symbol = |d: &mut Document| {
d.patterns.push(crate::PatternDef::new("p", vec![instance(2, "s")]));
d.symbols.push(crate::Symbol { name: "s".into(), art: painted(3, "p") });
};
for make in [&itself as &dyn Fn(&mut Document), &each_other, &through_a_symbol] {
let mut d = doc(painted(1, "p"));
make(&mut d);
assert!(d.too_deep(None));
let mut warnings = vec![];
d.limit_depth_noting(&mut warnings);
assert_eq!(warnings, [symbols_note()]);
assert!(!d.too_deep(None));
assert_eq!(pattern_fills(&d), 1, "the object on the layer keeps its pattern");
}
// A pattern whose tile is painted with another is fine.
let mut d = doc(painted(1, "p"));
d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "q")]));
d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "none")]));
assert!(!d.too_deep(None) && !d.limit_depth());
}
}
+2
View File
@@ -11,6 +11,7 @@ pub mod blend;
pub mod clipnest;
pub mod cmyk;
pub mod corners;
pub mod depth;
pub mod graph;
pub mod hit;
mod id_index;
@@ -79,6 +80,7 @@ pub use appearance::{
};
pub use assets::ExportAsset;
pub use corners::LiveCorners;
pub use depth::MAX_DEPTH;
pub use graph::{
GraphDesign, GraphKind, GraphSpec, LabelStyles, MAX_GRAPH_CATEGORIES, MAX_GRAPH_SERIES, PiePosition, PieSort, SeriesPaint, TickLength,
ValueAxisSide,
+2 -2
View File
@@ -224,7 +224,7 @@ fn item_paint_mut(it: &mut AppearanceItem) -> &mut Paint {
}
/// The paints a node holds itself (not its children): appearance items, then text runs.
fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
pub(crate) fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
let runs = match &n.kind {
NodeKind::Text(t) => t.runs.as_slice(),
_ => &[],
@@ -233,7 +233,7 @@ fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
}
/// Mutable [`node_paints`], in the same order.
fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
pub(crate) fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
let runs = match &mut n.kind {
NodeKind::Text(t) => t.runs.as_mut_slice(),
_ => &mut [],
+1
View File
@@ -32,6 +32,7 @@ pub(super) fn import(name: &str, bytes: &[u8]) -> Result<Imported> {
Ok(d) => {
let mut b = Builder { doc: Document::new(0.0, 0.0), warnings: Vec::new(), k: 72.0 / d.dpi, layer: 0 };
b.document(&d)?;
b.doc.limit_depth_noting(&mut b.warnings);
let mut warnings = d.warnings.iter().map(|w| format!("Affinity: not imported or approximated: {w}.")).collect::<Vec<_>>();
warnings.extend(b.warnings.iter().map(|w| format!("Affinity: {w}.")));
Ok(Imported { doc: b.doc, warnings, preview_only: false })
+2 -1
View File
@@ -139,7 +139,8 @@ fn restore_or_import(
/// (with a warning for each that can't be used).
pub(crate) fn native_file(bytes: &[u8]) -> Result<(Document, vectorcraft_format::FileInfo, Vec<String>)> {
let f = vectorcraft_format::load_file(bytes).map_err(err)?;
let warnings = super::native::install_profiles(f.profiles);
let mut warnings = super::native::install_profiles(f.profiles);
warnings.extend(f.depth_notes);
Ok((f.doc, f.info, warnings))
}
+12 -1
View File
@@ -583,6 +583,10 @@ 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, .. }))
}
/// The error of an edit that would nest objects deeper than [`vectorcraft_doc::MAX_DEPTH`].
pub const TOO_DEEP: &str = "objects can't be nested more than 32 deep";
const _: () = assert!(vectorcraft_doc::MAX_DEPTH == 32, "TOO_DEEP (and its catalog rows) names the cap");
/// 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 {
@@ -1245,6 +1249,9 @@ impl Session {
}
/// Add a document and make it active.
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
// Every edit is checked against the document before it, so the first one is within the cap
// (importers and the native reader have seen to theirs already, with a warning).
doc.limit_depth();
Self::refresh_text_bounds(&mut doc);
// Guides from files saved before guides had layers go on a layer.
doc.adopt_guides();
@@ -1262,6 +1269,7 @@ impl Session {
if index >= self.docs.len() {
return false;
}
doc.limit_depth();
Self::refresh_text_bounds(&mut doc);
doc.adopt_guides();
if self.active == Some(index) {
@@ -1476,7 +1484,10 @@ impl Session {
if !st.doc.variables.bindings.is_empty() {
Arc::make_mut(&mut st.doc).prune_variable_bindings();
}
if doc_sane(&st.doc, &st.selection) {
if st.doc.too_deep(Some(&before)) {
// Deeper trees overflow the stack of what walks them.
Err(EngineError::Other(TOO_DEEP.into()))
} else if doc_sane(&st.doc, &st.selection) {
Ok(v)
} else {
Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()))
+342
View File
@@ -2440,3 +2440,345 @@ proptest! {
checked("mutated script", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "sample.jsx"}))?;
}
}
// ---------- deep nesting ----------
//
// The tree is walked by recursion, so a file nested deeply enough overflowed the stack: an abort,
// which nothing catches. Every reader caps what it reads, and no document is deeper than
// `MAX_DEPTH`. These run on a thread with the stack of a Windows main thread (1 MB), where an
// overflow ends the test run.
use vectorcraft_doc::MAX_DEPTH;
/// `f` on a thread with a 1 MB stack.
fn on_a_small_stack(f: impl FnOnce() + Send + 'static) {
std::thread::Builder::new().stack_size(1 << 20).spawn(f).unwrap().join().unwrap();
}
/// Open `bytes` as the file `name`: an error, or a document within the cap that renders and
/// exports. Returns the document and the import's warnings.
fn open_deep(name: &str, bytes: &[u8]) -> Option<(Document, Vec<String>)> {
let loaded = vectorcraft_engine::cmd::fileio::load(name, bytes).ok()?;
assert!(!loaded.doc.too_deep(None), "{name}: deeper than the cap");
let doc = loaded.doc.clone();
survive(name, move || Some(doc)).unwrap();
Some((loaded.doc, loaded.warnings))
}
fn merged(warnings: &[String]) -> bool {
warnings.iter().any(|w| w.contains("nested more than"))
}
fn paths_in(doc: &Document) -> usize {
let mut paths = 0;
doc.walk(|n| paths += usize::from(n.path_data().is_some()));
paths
}
const SVG_NS: &str = "xmlns=\"http://www.w3.org/2000/svg\"";
/// `n` groups in one another, each named (so it is kept) and with a rectangle beside the next.
fn nested_svg(n: usize) -> String {
let open: String = (0..n).map(|i| format!("<g id=\"g{i}\"><rect width=\"5\" height=\"5\"/>")).collect();
format!("<svg {SVG_NS} width=\"100\" height=\"100\">{open}<rect width=\"10\" height=\"10\"/>{}</svg>", "</g>".repeat(n))
}
#[test]
fn deeply_nested_svg_is_refused_or_merged() {
on_a_small_stack(|| {
// Within what the reader takes: opened, the groups past the cap merged into the last.
let (doc, warnings) = open_deep("deep.svg", nested_svg(40).as_bytes()).expect("40 groups open");
assert!(merged(&warnings), "{warnings:?}");
assert_eq!(paths_in(&doc), 41, "no art is lost");
let (_, warnings) = open_deep("shallow.svg", nested_svg(20).as_bytes()).expect("20 groups open");
assert!(!merged(&warnings), "{warnings:?}");
// As deep as the reader goes (on a stack of its own): opened and merged the same.
let most = vectorcraft_svg::MAX_ELEMENT_DEPTH;
let (doc, warnings) = open_deep("deepest.svg", nested_svg(most - 2).as_bytes()).expect("opens at the reader's cap");
assert!(merged(&warnings), "{warnings:?}");
assert_eq!(paths_in(&doc), most - 1, "no art is lost");
// Past it: an error that says why, whatever the depth.
for n in [most, 2 * most, 200_000] {
let e = vectorcraft_engine::cmd::fileio::load("deep.svg", nested_svg(n).as_bytes()).err().expect("refused").to_string();
assert!(e.contains(&format!("nested more than {most} deep")), "{n}: {e}");
}
// Unnamed groups of one object aren't kept, but usvg walks them all the same.
let plain = format!(
"<svg {SVG_NS} width=\"9\" height=\"9\">{}<rect width=\"1\" height=\"1\"/>{}</svg>",
"<g>".repeat(2 * most),
"</g>".repeat(2 * most)
);
assert!(open_deep("plain.svg", plain.as_bytes()).is_none());
// A chain of `<use>`: shallow XML, a deep tree.
let links: String = (0..3000).map(|i| format!("<g id=\"u{i}\"><use href=\"#u{}\"/></g>", i + 1)).collect();
let chain = format!(
"<svg {SVG_NS} width=\"9\" height=\"9\"><defs>{links}<rect id=\"u3000\" width=\"1\" height=\"1\"/></defs><use href=\"#u0\"/></svg>"
);
assert!(open_deep("chain.svg", chain.as_bytes()).is_none());
// Placed and pasted, as opened.
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
for n in [40, 2 * most] {
let p = json!({"name": "deep.svg", "dataBase64": vectorcraft_format::base64_encode(nested_svg(n).as_bytes())});
let _ = no_panic(s.execute("file.place", &p));
let _ = no_panic(s.execute("clipboard.importSvg", &json!({"svg": nested_svg(n)})));
let _ = no_panic(s.execute("edit.paste", &json!({})));
assert!(!s.doc().unwrap().doc.too_deep(None));
}
});
}
/// A page whose content is `content`, with `resources`.
fn deep_pdf(content: &str, resources: &str) -> Vec<u8> {
handmade_pdf(content, resources, "[0 0 100 100]")
}
/// An EPS whose program is `body`.
fn deep_eps(body: &str) -> Vec<u8> {
format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}\nshowpage\n%%EOF\n").into_bytes()
}
/// A test for each file: it opens as an error or as a document within the cap, on a small stack.
macro_rules! deep_files {
($($test:ident: $name:literal, $bytes:expr;)*) => {$(
#[test]
fn $test() {
on_a_small_stack(|| {
open_deep($name, &$bytes);
});
}
)*};
}
deep_files! {
// PDF: clips inside clips (saved states never restored); arrays in arrays as an operand and as
// an object; dictionaries in dictionaries as a resource and inline; marked content never ended.
pdf_clips_in_clips: "clips.pdf", deep_pdf(&format!("{} 0 0 5 5 re f", "q 0 0 50 50 re W n ".repeat(20_000)), "<< >>");
pdf_arrays_in_an_operand: "arrays.pdf", deep_pdf(&format!("BT {}{} TJ ET 0 0 5 5 re f", "[".repeat(100_000), "]".repeat(100_000)), "<< >>");
pdf_arrays_in_an_object: "boxes.pdf", handmade_pdf("0 0 5 5 re f", "<< >>", &format!("{}0 0 100 100{}", "[".repeat(100_000), "]".repeat(100_000)));
pdf_dictionaries_in_resources: "dicts.pdf", deep_pdf("0 0 5 5 re f", &format!("{}{}", "<< /A ".repeat(50_000), ">> ".repeat(50_000)));
pdf_dictionaries_in_content: "inline.pdf", deep_pdf(&format!("/P {}{} DP 0 0 5 5 re f", "<< /A ".repeat(50_000), ">> ".repeat(50_000)), "<< >>");
pdf_marked_content_never_ended: "marked.pdf", deep_pdf(&format!("{} 0 0 5 5 re f", "/Span << /MCID 0 >> BDC ".repeat(50_000)), "<< >>");
// PostScript: clips inside clips; procedures in procedures; arrays in arrays; a procedure that
// calls itself; dictionaries never ended.
eps_clips_in_clips: "clips.eps", deep_eps(&format!("{} 0 0 5 5 rectfill", "gsave 0 0 50 50 rectclip ".repeat(20_000)));
eps_procedures_in_procedures: "procs.eps", deep_eps(&format!("{}{} 0 0 5 5 rectfill", "{".repeat(100_000), "}".repeat(100_000)));
eps_arrays_in_arrays: "arrays.eps", deep_eps(&format!("{}{} pop 0 0 5 5 rectfill", "[".repeat(100_000), "]".repeat(100_000)));
eps_a_procedure_calling_itself: "runaway.eps", deep_eps("/f { 0 0 5 5 rectfill f } def f");
eps_dictionaries_never_ended: "dicts.eps", deep_eps(&format!("{} 0 0 5 5 rectfill", "1 dict begin ".repeat(100_000)));
}
#[test]
fn deeply_nested_dxf_blocks_are_read_within_the_cap() {
on_a_small_stack(|| {
// Block `B0` inserts `B1`, which inserts `B2`, and so on; the last one holds a line.
let pair = |c: i32, v: &str| (c, v.to_string());
let mut pairs = vec![pair(0, "SECTION"), pair(2, "BLOCKS")];
for i in 0..2_000 {
pairs.extend([pair(0, "BLOCK"), pair(2, &format!("B{i}")), pair(10, "0"), pair(20, "0")]);
if i + 1 < 2_000 {
pairs.extend([pair(0, "INSERT"), pair(2, &format!("B{}", i + 1)), pair(10, "1"), pair(20, "1")]);
} else {
pairs.extend([pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "5"), pair(21, "5")]);
}
pairs.push(pair(0, "ENDBLK"));
}
pairs.extend([pair(0, "ENDSEC"), pair(0, "SECTION"), pair(2, "ENTITIES"), pair(0, "INSERT"), pair(2, "B0"), pair(10, "0"), pair(20, "0")]);
pairs.extend([pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "9"), pair(21, "9"), pair(0, "ENDSEC"), pair(0, "EOF")]);
open_deep("blocks.dxf", dxf_text(&pairs).as_bytes()).expect("opens");
});
}
/// A layer holding `levels` groups in one another around a rectangle, each with a second
/// rectangle beside the next: `levels + 2` deep.
fn nested_document(levels: usize) -> Document {
use vectorcraft_doc::Node;
use vectorcraft_testkit::fixtures::DocBuilder;
let mut b = DocBuilder::new(100.0, 100.0);
let rect =
|b: &mut DocBuilder, x: f64| b.rect_node(vectorcraft_geom::Rect::new(x, 0.0, x + 10.0, 10.0), vectorcraft_color::Color::rgb8(200, 0, 0));
let mut inner = rect(&mut b, 0.0);
for _ in 0..levels {
let beside = rect(&mut b, 20.0);
inner = Node::group(b.alloc(), vec![std::sync::Arc::new(inner), std::sync::Arc::new(beside)]);
inner.opacity = 0.9;
}
b.add(inner);
b.build()
}
#[test]
fn a_document_at_the_cap_saves_opens_and_exports() {
on_a_small_stack(|| {
let doc = nested_document(MAX_DEPTH - 2);
assert!(!doc.too_deep(None));
let back = vectorcraft_format::load_file(&vectorcraft_format::save_file(&doc)).expect("what can be made opens again");
assert!(back.depth_notes.is_empty());
assert_eq!(back.doc.layers, doc.layers);
survive("at the cap", || Some(doc.clone())).unwrap();
// Every format that keeps the nesting reads its own file back within the cap.
let mut s = vectorcraft_engine::Session::new();
s.add_document(doc, None);
for format in ["svg", "pdf", "eps", "ai"] {
let r = s.execute("document.export", &json!({"format": format})).unwrap();
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
open_deep(&format!("cap.{format}"), &bytes).unwrap_or_else(|| panic!("{format} opens"));
}
for cmd in ["select.all", "document.inspect", "document.info", "edit.copy", "edit.paste", "object.ungroup", "edit.undo"] {
no_panic(s.execute(cmd, &json!({}))).unwrap_or_else(|e| panic!("{cmd}: {e}"));
}
});
}
#[test]
fn a_native_file_nested_too_deep_is_merged_or_refused() {
on_a_small_stack(|| {
// Saved before the cap by a version that could still open it: opened, the deepest merged.
let old = vectorcraft_format::save_file(&nested_document(MAX_DEPTH + 2));
let (doc, warnings) = open_deep("old.vectorcraft", &old).expect("opens");
assert!(merged(&warnings), "{warnings:?}");
assert_eq!(paths_in(&doc), MAX_DEPTH + 3, "no art is lost");
// Deeper than any version could open, and a hostile file: an error that says why. Written
// by hand: a tree this deep can't be built and saved.
let text = String::from_utf8(vectorcraft_format::save(&nested_document(0), false)).unwrap();
let (open, close) = ("{\"id\":900000,\"kind\":{\"type\":\"group\",\"children\":[", "]}}");
let at = text.find("\"children\":[").expect("the layer's children") + "\"children\":[".len();
for levels in [100, 50_000] {
let deep = format!("{}{}{},{}", &text[..at], open.repeat(levels), close.repeat(levels), &text[at..]);
let e = vectorcraft_engine::cmd::fileio::load("deep.vectorcraft", deep.as_bytes()).err().expect("refused").to_string();
assert!(e.contains("nested too deep"), "{levels}: {e}");
}
});
}
#[test]
fn commands_cannot_nest_deeper_than_the_cap() {
on_a_small_stack(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
s.execute("select.all", &json!({})).unwrap();
let mut grouped = 0;
for _ in 0..10_000 {
match no_panic(s.execute("object.group", &json!({}))) {
Ok(_) => grouped += 1,
Err(e) => assert!(e.to_string().contains(vectorcraft_engine::TOO_DEEP), "after {grouped} groups: {e}"),
}
}
// The layer, the groups and the rectangle.
assert_eq!(grouped, MAX_DEPTH - 2);
assert!(!s.doc().unwrap().doc.too_deep(None));
// Other ways down: a clipping mask, a copy pasted, a symbol made of it.
for (cmd, p) in [
("object.clippingMask.make", json!({})),
("edit.copy", json!({})),
("edit.pasteInFront", json!({})),
("symbol.new", json!({"name": "deep"})),
] {
let _ = no_panic(s.execute(cmd, &p));
assert!(!s.doc().unwrap().doc.too_deep(None), "{cmd}");
}
survive("grouped to the cap", || Some((*s.doc().unwrap().doc).clone())).unwrap();
});
}
/// A document whose one object is an instance of the first of `count` symbols: each symbol's art
/// is `levels` groups around an instance of the next symbol, the last one of `last`.
fn symbols_document(count: usize, levels: usize, last: &str) -> Document {
use vectorcraft_doc::{Node, NodeKind, Symbol};
let instance =
|doc: &mut Document, of: String| Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: of, xf: vectorcraft_geom::Affine::IDENTITY });
let mut doc = nested_document(1);
for i in 0..count {
let mut art = instance(&mut doc, if i + 1 < count { format!("s{}", i + 1) } else { last.to_string() });
for _ in 0..levels {
art = Node::group(doc.alloc_id(), vec![std::sync::Arc::new(art)]);
}
doc.symbols.push(Symbol { name: format!("s{i}"), art: std::sync::Arc::new(art) });
}
let first = instance(&mut doc, "s0".into());
let layer = doc.layers[0].id;
doc.insert(Some(layer), usize::MAX, first).unwrap();
doc
}
/// `doc` saved (as an older version, or a hostile file, could have) and opened again: within the
/// caps (`kept`: as it was), drawn, exported and edited.
fn symbols_survive(what: &str, doc: Document, kept: bool) {
let (back, warnings) = open_deep("symbols.vectorcraft", &vectorcraft_format::save_file(&doc)).expect("opens");
assert_eq!(!warnings.iter().any(|w| w.contains("symbol instances")), kept, "{what}: {warnings:?}");
let mut s = vectorcraft_engine::Session::new();
s.add_document(back, None);
for cmd in [
"select.all",
"document.inspect",
"document.info",
"object.expand",
"symbol.breakLink",
"object.flattenTransparency",
"edit.copy",
"edit.paste",
] {
let _ = no_panic(s.execute(cmd, &json!({})));
assert!(!s.doc().unwrap().doc.too_deep(None), "{what}: {cmd}");
}
for format in ["svg", "pdf", "eps", "png"] {
let _ = no_panic(s.execute("document.export", &json!({"format": format})));
}
}
#[test]
fn a_symbol_holding_itself_opens_without_the_instance() {
on_a_small_stack(|| {
symbols_survive("a symbol holding itself", symbols_document(1, 1, "s0"), false);
symbols_survive("symbols holding one another", symbols_document(3, 2, "s0"), false);
});
}
#[test]
fn symbols_in_symbols_are_drawn_only_so_deep() {
on_a_small_stack(|| {
// As deep as symbols draw: each one's art as deep as art goes.
symbols_survive("symbols at the cap", symbols_document(3, MAX_DEPTH - 2, "none"), true);
symbols_survive("symbols in symbols", symbols_document(40, MAX_DEPTH - 1, "none"), false);
// A symbol can't be made to hold itself, or one that holds it, by editing either.
let mut s = vectorcraft_engine::Session::new();
s.add_document(symbols_document(2, 1, "none"), None);
s.execute("select.all", &json!({})).unwrap();
for (cmd, p) in [
("symbol.new", json!({"name": "s1"})),
("symbol.update", json!({"name": "s1"})),
("symbol.update", json!({"name": "s0"})),
("symbol.replace", json!({"name": "s1"})),
] {
let _ = no_panic(s.execute(cmd, &p));
assert!(!s.doc().unwrap().doc.too_deep(None), "{cmd} {p}");
}
});
}
#[test]
fn a_pattern_painting_its_own_tile_opens_without_the_paint() {
on_a_small_stack(|| {
use vectorcraft_color::Paint;
use vectorcraft_doc::{Appearance, PatternDef};
let painted = |doc: &mut Document, with: &str| {
let fill = Paint::Pattern { pattern: with.into(), xf: vectorcraft_geom::Affine::IDENTITY };
let square = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 20.0, 20.0));
vectorcraft_doc::Node::path(doc.alloc_id(), square, Appearance::basic(fill, Paint::None, 0.0))
};
// One pattern painting its own tile, and two painting each other's.
for names in [["p", "p"], ["p", "q"]] {
let mut doc = nested_document(1);
let (first, second) = (painted(&mut doc, names[1]), painted(&mut doc, names[0]));
doc.patterns.push(PatternDef::new(names[0], vec![std::sync::Arc::new(first)]));
if names[0] != names[1] {
doc.patterns.push(PatternDef::new(names[1], vec![std::sync::Arc::new(second)]));
}
let on_layer = painted(&mut doc, "p");
let layer = doc.layers[0].id;
doc.insert(Some(layer), usize::MAX, on_layer).unwrap();
symbols_survive("a pattern painting its own tile", doc, false);
}
});
}
+1 -2
View File
@@ -4,7 +4,6 @@
//! Every operation counts against a budget and procedures nest only so deep, so a looping or
//! recursing program ends with [`PsError::Limit`] instead of hanging.
use std::cell::RefCell;
use std::rc::Rc;
use vectorcraft_geom::Affine;
@@ -86,7 +85,7 @@ struct SaveMark {
}
fn new_dict() -> DictRef {
Rc::new(RefCell::new(Dict::new()))
DictRef::new(Dict::new())
}
/// An encoding vector: 256 `/.notdef` names (type is read as Latin-1 whatever the encoding).
+3 -1
View File
@@ -268,5 +268,7 @@ fn finish_with(out: Out, layers: Vec<Arc<Node>>) -> Imported {
}
}
doc.layers = layers;
Imported { document: doc, warnings: out.warnings, preview: false }
let mut warnings = out.warnings;
doc.limit_depth_noting(&mut warnings);
Imported { document: doc, warnings, preview: false }
}
+1
View File
@@ -993,6 +993,7 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr
}
None => unmark(&mut doc.layers, true, &mut vec![]),
}
doc.limit_depth_noting(&mut notes);
Ok((doc, notes))
}
+81 -5
View File
@@ -12,7 +12,73 @@ use super::graphics::GState;
pub(crate) type Dict = HashMap<Key, Obj>;
/// A shared, mutable dictionary.
pub(crate) type DictRef = Rc<RefCell<Dict>>;
#[derive(Clone, Debug)]
pub(crate) struct DictRef(Rc<RefCell<Dict>>);
impl DictRef {
pub fn new(d: Dict) -> Self {
Self(Rc::new(RefCell::new(d)))
}
}
impl std::ops::Deref for DictRef {
type Target = Rc<RefCell<Dict>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for DictRef {
fn drop(&mut self) {
if Rc::strong_count(&self.0) == 1
&& let Ok(mut d) = self.0.try_borrow_mut()
{
drop_held(d.drain().map(|(_, v)| v));
}
}
}
thread_local! {
/// What the composite objects being dropped held, waiting for their turn (while any is).
static HELD: RefCell<Option<Vec<Obj>>> = const { RefCell::new(None) };
}
/// Drop what a composite object held, now that nothing else refers to it, without recursion: a
/// program nests arrays and dictionaries as deep as it likes (`[ exch ]` in a loop), and dropping
/// them one inside the other overflows the stack. What `held` holds in its turn waits in [`HELD`]
/// for the outermost drop, which works through it.
fn drop_held(held: impl Iterator<Item = Obj>) {
let holds = |o: &Obj| matches!(o, Obj::Array { .. } | Obj::Dict(_) | Obj::File { .. } | Obj::GState(_));
// (Without the list, when the thread is ending: dropped as they come.)
let outermost = HELD.try_with(|h| {
let Ok(mut h) = h.try_borrow_mut() else { return false };
let outermost = h.is_none();
h.get_or_insert_with(Vec::new).extend(held.filter(holds));
outermost
});
if !matches!(outermost, Ok(true)) {
return;
}
// Each drop here puts what it held on the list instead of dropping it.
while let Ok(Some(next)) = HELD.try_with(|h| h.try_borrow_mut().ok().and_then(|mut h| h.as_mut().and_then(Vec::pop))) {
drop(next);
}
let _ = HELD.try_with(|h| h.try_borrow_mut().map(|mut h| *h = None)); // Nothing to do when the thread is ending.
}
/// What [`Shared`] values may be: bytes, or objects (which may hold more of themselves).
pub(crate) trait Held: Sized {
/// Drop the values of storage nothing refers to any more.
fn release(_values: Vec<Self>) {}
}
impl Held for u8 {}
impl Held for Obj {
fn release(values: Vec<Self>) {
drop_held(values.into_iter());
}
}
/// A dictionary key.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -72,7 +138,7 @@ impl Obj {
}
pub fn dict(d: Dict) -> Self {
Self::Dict(Rc::new(RefCell::new(d)))
Self::Dict(DictRef::new(d))
}
/// The number this is (integers and reals).
@@ -135,13 +201,23 @@ impl Obj {
/// The values of a string or an array: a run of storage that other strings or arrays may share
/// (`getinterval` makes a new object over part of the same values, as PostScript does).
#[derive(Clone, Debug)]
pub(crate) struct Shared<T> {
pub(crate) struct Shared<T: Held> {
buf: Rc<RefCell<Vec<T>>>,
at: usize,
len: usize,
}
impl<T: Clone> Shared<T> {
impl<T: Held> Drop for Shared<T> {
fn drop(&mut self) {
if Rc::strong_count(&self.buf) == 1
&& let Ok(mut values) = self.buf.try_borrow_mut()
{
T::release(std::mem::take(&mut *values));
}
}
}
impl<T: Clone + Held> Shared<T> {
pub fn new(v: Vec<T>) -> Self {
Self { len: v.len(), buf: Rc::new(RefCell::new(v)), at: 0 }
}
@@ -198,7 +274,7 @@ impl<T: Clone> Shared<T> {
}
/// Hashes what [`Shared::same`] compares: the storage and the run of it.
impl<T> Hash for Shared<T> {
impl<T: Held> Hash for Shared<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Rc::as_ptr(&self.buf).hash(state);
(self.at, self.len).hash(state);
+2 -2
View File
@@ -389,7 +389,7 @@ impl Interp<'_> {
d.insert(Key::name("FontMatrix"), matrix_obj(Affine::scale(0.001)));
d.insert(Key::name("FontBBox"), Obj::array(vec![Obj::Int(0), Obj::Int(-250), Obj::Int(1000), Obj::Int(1000)]));
d.insert(Key::name("Encoding"), Obj::array(vec![Obj::name(".notdef"); 256]));
let font = Rc::new(std::cell::RefCell::new(d));
let font = DictRef::new(d);
self.fonts.borrow_mut().insert(key, Obj::Dict(font.clone()));
Ok(font)
}
@@ -489,7 +489,7 @@ fn transformed(font: &DictRef, m: Affine) -> DictRef {
let mut d = font.borrow().clone();
let fm = d.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
d.insert(Key::name("FontMatrix"), matrix_obj(m * fm));
Rc::new(std::cell::RefCell::new(d))
DictRef::new(d)
}
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
+11 -2
View File
@@ -482,6 +482,9 @@ pub fn load(bytes: &[u8]) -> Result<Document, FormatError> {
#[derive(Debug)]
pub struct NativeFile {
pub doc: Document,
/// What was done to bring the document within the nesting caps
/// ([`Document::limit_depth_noting`]: a file saved before them, or a damaged one).
pub depth_notes: Vec<String>,
pub info: FileInfo,
/// ICC files by profile name (saved with Embed ICC Profiles; undecodable ones are left out).
pub profiles: BTreeMap<String, Vec<u8>>,
@@ -511,7 +514,11 @@ pub fn load_info(bytes: &[u8]) -> Result<(Document, FileInfo), FormatError> {
/// [`load`], with everything else the file carries.
pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
let file = unpack::Unpacked::new(bytes)?;
let f: File = serde_json::from_slice(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
// The JSON reader stops at 128 levels (a document's objects use three each).
let f: File = serde_json::from_slice(file.json()).map_err(|e| {
let why = if e.to_string().contains("recursion limit") { format!("its objects are nested too deep ({e})") } else { e.to_string() };
FormatError::NotVectorcraft(why)
})?;
if f.format != EXTENSION && f.format != LEGACY_EXTENSION {
return Err(FormatError::NotVectorcraft(format!("format is `{}`", f.format)));
}
@@ -545,8 +552,10 @@ pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
// Untrusted: capped, named and pointing at objects the file has.
doc.tidy_saved_selections();
doc.tidy_graph_designs();
let mut depth_notes = vec![];
doc.limit_depth_noting(&mut depth_notes);
let profiles = f.profiles.into_iter().filter_map(|(name, icc)| Some((name, bytes_of(&icc)?))).collect();
Ok(NativeFile { doc, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
Ok(NativeFile { doc, depth_notes, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
}
/// The members of a native file besides its document (which is skipped, not decoded).
+3 -1
View File
@@ -102,7 +102,9 @@ pub struct Imported {
/// Read an EMF or WMF file (with or without a placeable header) into a document whose one
/// artboard is the picture's frame.
pub fn import(bytes: &[u8]) -> Result<Imported, String> {
import::import(bytes)
let mut i = import::import(bytes)?;
i.document.limit_depth_noting(&mut i.warnings);
Ok(i)
}
#[cfg(test)]
+3
View File
@@ -149,6 +149,7 @@ pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it,
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
let original = crate::pages::open(bytes, opts.password.as_deref())?;
let picked = crate::pages::picked(opts, original.pages().len())?;
picked.iter().filter_map(|&n| original.pages().get(n)).try_for_each(crate::pages::content_too_deep)?;
let mut ocgs = Ocgs::read(&original);
let route = opts.layers && !ocgs.list.is_empty();
// Content that is off imports as hidden layers: an update of the file turns every group on
@@ -166,6 +167,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
let all_on = turned_on.is_some();
let pdf = turned_on.unwrap_or(original);
let pages = pdf.pages();
picked.iter().filter_map(|&n| pages.get(n)).try_for_each(crate::pages::content_too_deep)?;
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
let sink2 = sink.clone();
@@ -417,6 +419,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
}
}
}
doc.limit_depth_noting(&mut warnings);
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf), only_text })
}
+70
View File
@@ -33,12 +33,76 @@ pub fn is_postscript(bytes: &[u8]) -> bool {
bytes.starts_with(b"%!PS") || bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
}
/// The deepest a file's arrays and dictionaries may nest. The PDF reader (hayro) reads them by
/// recursion, without a cap of its own: a file nested deeply enough overflows the stack, which is
/// an abort, not an error. Far beyond what a PDF writer makes.
const MAX_OBJECT_NESTING: usize = 256;
fn too_deep() -> PdfError {
PdfError::Parse(format!("its arrays and dictionaries are nested more than {MAX_OBJECT_NESTING} deep"))
}
/// Do the arrays and dictionaries in the text of `pdf` nest deeper than [`MAX_OBJECT_NESTING`]?
/// Read as tokens (strings and comments hold none), an object at a time. The data of a stream
/// with a filter is skipped: it isn't PDF text until decoded, which this doesn't do, so nesting
/// inside compressed streams is not seen here.
fn nests_too_deep(pdf: &[u8]) -> bool {
let after =
|from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
let word = |at: usize, w: &[u8]| pdf.get(at..).is_some_and(|rest| rest.starts_with(w));
let (mut depth, mut filtered) = (0usize, false);
let mut i = 0;
while let Some(&c) = pdf.get(i) {
i += 1;
match c {
b'%' => i += pdf.get(i..).and_then(|rest| rest.iter().position(|c| matches!(c, b'\n' | b'\r'))).unwrap_or(pdf.len()),
b'(' => {
// A string: parentheses nest in it, a backslash takes the next byte.
let mut open = 1usize;
while open > 0 {
match pdf.get(i) {
None => return false,
Some(b'\\') => i += 1,
Some(b'(') => open += 1,
Some(b')') => open -= 1,
Some(_) => {}
}
i += 1;
}
}
b'[' => depth += 1,
b']' => depth = depth.saturating_sub(1),
b'<' if pdf.get(i) == Some(&b'<') => (depth, i) = (depth + 1, i + 1),
b'>' if pdf.get(i) == Some(&b'>') => (depth, i) = (depth.saturating_sub(1), i + 1),
// A string in hexadecimal.
b'<' => i = after(i, b">").unwrap_or(pdf.len()),
b'/' if word(i - 1, b"/Filter") => filtered = true,
b'e' if word(i - 1, b"endobj") => (depth, filtered) = (0, false),
b'e' if word(i - 1, b"endstream") => i += 8,
b's' if word(i - 1, b"stream") => {
depth = 0;
if std::mem::take(&mut filtered) {
i = after(i, b"endstream").unwrap_or(pdf.len());
}
}
_ => {}
}
if depth > MAX_OBJECT_NESTING {
return true;
}
}
false
}
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions
/// password opens a file too.
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
if is_postscript(bytes) {
return Err(PdfError::PostScript);
}
if nests_too_deep(bytes) {
return Err(too_deep());
}
let password = password.unwrap_or_default();
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
read(password)
@@ -57,6 +121,12 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
})
}
/// The error of a page whose content, decoded, nests arrays and dictionaries too deep (see
/// [`MAX_OBJECT_NESTING`]): its operands are read the same way as the file's objects.
pub(crate) fn content_too_deep(page: &Page<'_>) -> Result<(), PdfError> {
if page.page_stream().is_some_and(nests_too_deep) { Err(too_deep()) } else { Ok(()) }
}
/// The pages and their boxes. An encrypted PDF needs its `password`
/// ([`PdfError::NeedsPassword`] / [`PdfError::WrongPassword`]).
pub fn info(bytes: &[u8], password: Option<&str>) -> Result<PdfInfo, PdfError> {
+34 -14
View File
@@ -18,10 +18,13 @@ use crate::{ImportOptions, SvgError, fnv1a};
use css::XNode;
use text::TextSlots;
pub use depth::MAX_ELEMENT_DEPTH;
pub(crate) use depth::xml_too_deep;
pub(crate) use fx::BLEND;
mod css;
mod css_transform;
mod depth;
mod files;
mod fx;
mod spread;
@@ -29,11 +32,30 @@ mod symbol_units;
mod text;
mod vector_effect;
/// The XML of `svg`.
fn parse(svg: &str) -> Result<roxmltree::Document<'_>, SvgError> {
roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() })
.map_err(|e| SvgError::Parse(e.to_string()))
}
pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<String>), SvgError> {
let (svg, found) = prepass(svg, opts);
// Before anything reads the tree: the XML reader, usvg, and what is made of its tree, all by
// recursion.
let too_deep = || SvgError::TooDeep(depth::MAX_ELEMENT_DEPTH);
if depth::xml_too_deep(svg) {
return Err(too_deep());
}
let xml = parse(svg)?;
if depth::too_deep(&xml) {
return Err(too_deep());
}
let (svg, found) = prepass(svg, &xml, opts);
// The prepass's edits are in the text: the XML is read again only when it made any.
let xml = match &svg {
Cow::Borrowed(_) => xml,
Cow::Owned(edited) => parse(edited)?,
};
let svg = svg.as_ref();
let xml = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() })
.map_err(|e| SvgError::Parse(e.to_string()))?;
let units = RootUnits::of(xml.root_element());
let root_name = root_layer_name(xml.root_element());
let mut warnings = found.warnings;
@@ -165,6 +187,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
promote_sublayers(Arc::make_mut(l), &im.sublayer_groups);
}
}
im.doc.limit_depth_noting(&mut im.warnings);
Ok((im.doc, im.warnings))
}
@@ -554,7 +577,7 @@ impl Edits {
/// * linked files are read ([`files`]);
/// * percentage lengths in a `<symbol>` with a `viewBox` become numbers ([`symbol_units`]);
/// * what style sheet rules with a `:not()` set goes in `style` attributes ([`not_rules`]).
fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
fn prepass<'s>(svg: &'s str, xml: &roxmltree::Document, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
let mut found = Found::default();
// (`:` for prefixed elements such as `<svg:image>`.)
let css_transform = has_css_transform(svg);
@@ -565,9 +588,6 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
{
return (svg.into(), found);
}
let Ok(xml) = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }) else {
return (svg.into(), found);
};
let mut edits = Edits::default();
let elements = || xml.descendants().filter(|n| n.is_element());
for a in elements().filter(|n| n.tag_name().name() == "a") {
@@ -592,24 +612,24 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
}
let (display, non_scaling) = (svg.contains("display"), svg.contains(vector_effect::NON_SCALING));
if display || non_scaling || not {
let css = css::Styles::new(&xml);
let css = css::Styles::new(xml);
// First: an undisplayed object shown for usvg is shown after these.
if not && css.has_not_rules() {
not_rules(svg, &xml, &css, &mut edits);
not_rules(svg, xml, &css, &mut edits);
}
if display {
hidden_objects(svg, &xml, &css, &mut edits, &mut found.hidden);
hidden_objects(svg, xml, &css, &mut edits, &mut found.hidden);
}
if non_scaling {
vector_effect::find(svg, &xml, &css, &mut edits, &mut found.non_scaling, &mut found.warnings);
vector_effect::find(svg, xml, &css, &mut edits, &mut found.non_scaling, &mut found.warnings);
}
}
found.files = files::Files::read(svg, &xml, opts, &mut edits, &mut found.warnings);
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));
edits.list.extend(symbol_units::rewrites(xml));
}
if css_transform {
edits.list.extend(css_transform::rewrites(svg, &xml));
edits.list.extend(css_transform::rewrites(svg, xml));
}
(edits.apply(svg), found)
}
+7 -2
View File
@@ -17,7 +17,7 @@ pub(super) type XNode<'a, 'i> = roxmltree::Node<'a, 'i>;
/// `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)> {
pub(super) fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
s.split(';')
.filter_map(|d| {
let (k, v) = d.split_once(':')?;
@@ -371,8 +371,13 @@ impl Styles {
Self { rules, index, cache: RefCell::default() }
}
/// Do the document's style sheets have any rules?
pub(super) fn has_rules(&self) -> bool {
!self.rules.is_empty()
}
/// Properties set on `n` by rules and its `style` attribute, after the cascade.
fn declared(&self, n: XNode) -> Rc<HashMap<String, String>> {
pub(super) fn declared(&self, n: XNode) -> Rc<HashMap<String, String>> {
if let Some(m) = self.cache.borrow().get(&n.id()) {
return m.clone();
}
+264
View File
@@ -0,0 +1,264 @@
//! How deep an SVG's elements nest, found in the XML before usvg runs: usvg converts the tree by
//! recursion, a few stack frames an element, so a file nested deeply enough overflows the stack
//! (an abort, not an error). Such a file is refused: its nesting can't be merged before usvg has
//! read it, and usvg is what can't read it.
//!
//! What counts is the tree usvg walks, not the XML alone: a `<use>` holds a copy of what it
//! refers to, and a clip path, mask, pattern, marker or filter is converted where it is used
//! (`clip-path: url(#a)` on an element of a clip path makes a chain as deep as there are clip
//! paths). So each element's depth is the deepest of what it holds and what it refers to.
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use usvg::roxmltree;
use super::css::{Styles, XNode, parse_decls};
/// The deepest an SVG's elements may nest, references followed. Far beyond what drawing apps
/// write, and deeper than a document's objects may nest ([`vectorcraft_doc::MAX_DEPTH`]: wrappers
/// that hold one object aren't kept, and what is still deeper is merged into the group around
/// it). usvg uses about 15 kB of stack for each level: the importer runs on a thread with 16 MB
/// ([`vectorcraft_doc::depth::with_deep_stack`]), except on the web, where there is the one
/// 1 MB stack.
pub const MAX_ELEMENT_DEPTH: usize = if cfg!(target_arch = "wasm32") { 48 } else { 256 };
/// Properties whose value an element's descendants use too: the paint server they name is
/// converted at each descendant, under everything between.
const INHERITED: [&str; 6] = ["fill", "stroke", "marker", "marker-start", "marker-mid", "marker-end"];
/// The ids of the `url(#id)` references in a property value.
fn urls(value: &str) -> impl Iterator<Item = &str> {
value.split("url(").skip(1).filter_map(|rest| {
let id = rest.trim_start().trim_start_matches(['"', '\'']).strip_prefix('#')?;
id.split([')', '"', '\'']).next().map(str::trim).filter(|id| !id.is_empty())
})
}
/// The elements `n` refers to, each with whether `n`'s descendants use it too.
fn references<'a, 'i>(n: XNode<'a, 'i>, ids: &HashMap<&str, XNode<'a, 'i>>, css: &Styles, out: &mut Vec<(XNode<'a, 'i>, bool)>) {
let add = |out: &mut Vec<(XNode<'a, 'i>, bool)>, name: &str, value: &str| {
let inherited = INHERITED.contains(&name);
out.extend(urls(value).filter_map(|id| ids.get(id)).map(|t| (*t, inherited)));
};
for a in n.attributes() {
match a.name() {
"href" => out.extend(a.value().trim().strip_prefix('#').and_then(|id| ids.get(id)).map(|t| (*t, false))),
// Read with the style sheets' rules below.
"style" if css.has_rules() => {}
"style" => parse_decls(a.value()).iter().for_each(|(k, v, _)| add(out, k, v)),
name => add(out, name, a.value()),
}
}
if css.has_rules() {
css.declared(n).iter().for_each(|(k, v)| add(out, k, v));
}
}
/// Do the elements of the XML text `svg` nest deeper than [`MAX_ELEMENT_DEPTH`]? Read from the
/// text, before any XML reader: roxmltree (ours and usvg's) reads elements by recursion too. Tags
/// are counted as the text has them; markup in the values of a document type's entities counts
/// ten times (how deep entities may refer to one another), on top of the deepest element.
pub(crate) fn xml_too_deep(svg: &str) -> bool {
let b = svg.as_bytes();
let find =
|from: usize, what: &[u8]| b.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
// The end of the tag starting at `from` (past its `>`), and whether it closes itself.
let tag_end = |from: usize| {
let mut quote = None;
for (i, c) in b.iter().enumerate().skip(from) {
match (quote, c) {
(None, b'"' | b'\'') => quote = Some(*c),
(Some(q), c) if q == *c => quote = None,
(None, b'>') => return (i + 1, b.get(i.wrapping_sub(1)) == Some(&b'/')),
_ => {}
}
}
(b.len(), false)
};
// (now, deepest) of the elements, and of the markup inside the document type.
let (mut elements, mut entities) = ((0usize, 0usize), (0usize, 0usize));
let mut doctype_end = 0;
let mut i = 0;
while let Some(at) = b.get(i..).and_then(|rest| rest.iter().position(|c| *c == b'<')).map(|at| i + at) {
let rest = b.get(at..).unwrap_or_default();
let depth = if at < doctype_end { &mut entities } else { &mut elements };
i = if rest.starts_with(b"<!--") {
find(at, b"-->").unwrap_or(b.len())
} else if rest.starts_with(b"<![CDATA[") {
find(at, b"]]>").unwrap_or(b.len())
} else if rest.starts_with(b"<?") {
find(at, b"?>").unwrap_or(b.len())
} else if rest.starts_with(b"<!DOCTYPE") {
// Up to the `>` that ends it: after its internal subset (`[` … `]`) when it has one.
let plain = tag_end(at).0;
let subset = b.get(at..plain).is_some_and(|head| head.contains(&b'['));
doctype_end = if subset { find(at, b"]>").unwrap_or(b.len()) } else { plain };
at + 2
} else if rest.starts_with(b"<!") {
// A declaration of the document type: the markup in its value is counted as it comes.
at + 2
} else if rest.starts_with(b"</") {
depth.0 = depth.0.saturating_sub(1);
tag_end(at).0
} else if at < doctype_end {
// In an entity's value, itself in quotes: up to the tag's `>`.
let end = find(at, b">").unwrap_or(b.len());
depth.1 = depth.1.max(depth.0 + 1);
depth.0 += usize::from(b.get(end.wrapping_sub(2)) != Some(&b'/'));
end
} else {
let (end, empty) = tag_end(at);
depth.1 = depth.1.max(depth.0 + 1);
depth.0 += usize::from(!empty);
end
};
if elements.1 + 10 * entities.1 > MAX_ELEMENT_DEPTH {
return true;
}
}
false
}
/// Does anything in `xml` nest deeper than [`MAX_ELEMENT_DEPTH`], references followed? Without
/// recursion.
pub(super) fn too_deep(xml: &roxmltree::Document) -> bool {
let elements = || xml.descendants().filter(|n| n.is_element());
let ids: HashMap<&str, XNode> = elements().filter_map(|n| Some((n.attribute("id")?, n))).collect();
let css = Styles::new(xml);
// How deep each element's own tree goes, itself included; 0 while it is being worked out: a
// reference back to it then is a loop, which usvg leaves out.
let mut heights: HashMap<roxmltree::NodeId, usize> = HashMap::new();
enum Step<'a, 'i> {
Enter(XNode<'a, 'i>),
Leave(XNode<'a, 'i>, Vec<(XNode<'a, 'i>, bool)>),
}
let mut stack = vec![Step::Enter(xml.root_element())];
while let Some(step) = stack.pop() {
match step {
Step::Enter(n) => {
match heights.entry(n.id()) {
Entry::Occupied(_) => continue,
Entry::Vacant(v) => v.insert(0),
};
let mut refs = vec![];
references(n, &ids, &css, &mut refs);
let targets: Vec<XNode> = refs.iter().map(|(t, _)| *t).collect();
stack.push(Step::Leave(n, refs));
stack.extend(n.children().filter(|c| c.is_element()).chain(targets).map(Step::Enter));
}
Step::Leave(n, refs) => {
let of = |n: XNode| heights.get(&n.id()).copied().unwrap_or(0);
let held = n.children().filter(|c| c.is_element()).map(of).max().unwrap_or(0);
let used = |inherited: bool| refs.iter().filter(|(_, i)| *i == inherited).map(|(t, _)| of(*t)).max().unwrap_or(0);
let height = 1 + held.max(used(false)) + used(true);
if height > MAX_ELEMENT_DEPTH {
return true;
}
heights.insert(n.id(), height);
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn deep(svg: &str) -> bool {
too_deep(&roxmltree::Document::parse(svg).unwrap())
}
fn svg(body: &str) -> String {
format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10" height="10">{body}</svg>"#)
}
/// `n` groups in one another around a rectangle.
fn groups(n: usize) -> String {
format!("{}<rect width='1' height='1'/>{}", "<g>".repeat(n), "</g>".repeat(n))
}
#[test]
fn elements_nest_up_to_the_cap() {
// The root, the groups and the rectangle.
let (at, past) = (svg(&groups(MAX_ELEMENT_DEPTH - 2)), svg(&groups(MAX_ELEMENT_DEPTH - 1)));
assert!(!deep(&at) && !xml_too_deep(&at));
assert!(deep(&past) && xml_too_deep(&past));
assert!(xml_too_deep(&svg(&groups(200_000))), "any depth is measured");
}
#[test]
fn the_text_is_measured_as_xml_reads_it() {
let many = r#"<g a='>' b="<g>"><rect/></g>"#.repeat(500);
assert!(!xml_too_deep(&svg(&many)), "side by side, with `>` and tags in attribute values");
let hidden = format!("<!-- {0} --><![CDATA[{0}]]><?pi {0}?>", "<g>".repeat(500));
assert!(!xml_too_deep(&svg(&hidden)), "comments, character data and instructions hold no elements");
assert!(xml_too_deep(&format!("<svg>{}", "<g>".repeat(MAX_ELEMENT_DEPTH))), "never closed");
assert!(!xml_too_deep(&format!("{}<svg/>", "</g>".repeat(2 * MAX_ELEMENT_DEPTH))), "ends without beginnings");
// Entities: namespaces as drawing apps declare them, and markup that would be read ten deep.
let names = r#"<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ <!ENTITY ns_svg "http://www.w3.org/2000/svg"> ]>"#;
assert!(!xml_too_deep(&format!("{names}{}", svg(&groups(30)))));
let tenth = MAX_ELEMENT_DEPTH / 10 + 1;
let bomb = format!("<!DOCTYPE svg [ <!ENTITY a \"{}&a;{}\"> ]>", "<g>".repeat(tenth), "</g>".repeat(tenth));
assert!(xml_too_deep(&format!("{bomb}<svg>&a;</svg>")));
assert!(!xml_too_deep(""));
}
#[test]
fn a_chain_of_uses_is_as_deep_as_what_it_copies() {
let chain = |n: usize| {
let defs: String = (0..n).map(|i| format!("<g id='g{i}'><use xlink:href='#g{}'/></g>", i + 1)).collect();
svg(&format!("<defs>{defs}<rect id='g{n}' width='1' height='1'/></defs><use href='#g0'/>"))
};
assert!(!deep(&chain(10)));
assert!(deep(&chain(MAX_ELEMENT_DEPTH / 2)), "two elements a link");
assert!(deep(&chain(5000)));
}
#[test]
fn chains_of_clips_masks_and_patterns_count_by_attribute_style_and_style_sheet() {
let by_attribute = |n: usize| -> String {
(0..n).map(|i| format!("<clipPath id='c{i}' clip-path='url(#c{})'><rect width='1' height='1'/></clipPath>", i + 1)).collect()
};
let by_style = |n: usize| -> String {
(0..n).map(|i| format!("<mask id='c{i}' style='mask: url( \"#c{}\" )'><rect width='1' height='1'/></mask>", i + 1)).collect()
};
let by_sheet = |n: usize| -> String {
let rules: String = (0..n).map(|i| format!(".p{i} {{ fill: url(#c{}) }}", i + 1)).collect();
let patterns: String = (0..n)
.map(|i| format!("<pattern id='c{i}' width='1' height='1'><g class='p{i}'><rect width='1' height='1'/></g></pattern>"))
.collect();
format!("<style>{rules}</style>{patterns}")
};
for chain in [&by_attribute as &dyn Fn(usize) -> String, &by_style, &by_sheet] {
let file =
|n| svg(&format!("<defs>{}</defs><rect width='5' height='5' fill='url(#c0)' clip-path='url(#c0)' mask='url(#c0)'/>", chain(n)));
assert!(!deep(&file(5)));
assert!(deep(&file(MAX_ELEMENT_DEPTH)));
}
}
#[test]
fn many_paint_servers_used_side_by_side_are_not_a_chain() {
// As drawing apps write gradients: a class each, used by objects that aren't inside one another.
let rules: String = (0..300).map(|i| format!(".c{i}{{fill:url(#g{i})}}")).collect();
let defs: String = (0..300).map(|i| format!("<linearGradient id='g{i}'><stop class='c{i}' offset='0'/></linearGradient>")).collect();
let art: String = (0..300).map(|i| format!("<g class='c{i}'><rect class='c{i}' width='1' height='1'/></g>")).collect();
assert!(!deep(&svg(&format!("<style>{rules}</style><defs>{defs}</defs>{art}"))));
}
#[test]
fn a_loop_of_references_is_not_deep() {
assert!(!deep(&svg("<g id='a'><use href='#b'/></g><g id='b'><use href='#a'/></g><clipPath id='c' clip-path='url(#c)'/>")));
}
#[test]
fn an_inherited_paint_is_converted_under_everything_between() {
// Neither the pattern nor the art is too deep; the pattern under the art's last object is.
let half = MAX_ELEMENT_DEPTH / 2;
let pattern = format!("<pattern id='p' width='1' height='1'>{}</pattern>", groups(half));
assert!(!deep(&svg(&format!("<defs>{pattern}</defs><g fill='url(#p)'>{}</g>", groups(half / 2)))));
assert!(deep(&svg(&format!("<defs>{pattern}</defs><g fill='url(#p)'>{}</g>", groups(half)))));
}
}
+10
View File
@@ -203,12 +203,16 @@ impl Files {
ImageHrefResolver {
resolve_data: Box::new(move |mime, bytes, opts| {
if mime == "image/svg+xml" {
if nested_too_deep(&bytes) {
return None;
}
self.note_text(&bytes);
}
data(mime, bytes, opts)
}),
resolve_string: Box::new(move |h, opts| match self.sources.get(h).or_else(|| self.sources.get(h.trim()))? {
Source::Raster(kind, bytes) => Some(kind(bytes.clone())),
Source::Svg(bytes) if nested_too_deep(bytes) => None,
Source::Svg(bytes) => {
self.note_text(bytes);
usvg::Tree::from_data_nested(bytes, opts).ok().map(ImageKind::SVG)
@@ -218,6 +222,12 @@ impl Files {
}
}
/// Is the SVG image `bytes` nested too deep for usvg to read (as the SVG holding it would be)?
/// It is then left out, as an image usvg can't read.
fn nested_too_deep(bytes: &[u8]) -> bool {
crate::text_of(bytes).is_ok_and(|text| super::xml_too_deep(&text))
}
#[cfg(test)]
mod tests {
use super::*;
+11 -3
View File
@@ -312,6 +312,9 @@ pub struct LinkedImage {
pub enum SvgError {
#[error("SVG parse error: {0}")]
Parse(String),
/// Its elements nest (references followed) deeper than the reader goes.
#[error("the SVG's elements are nested more than {0} deep")]
TooDeep(usize),
}
/// Export a document as an SVG string (linked images are not returned: see [`export_full`]).
@@ -347,7 +350,8 @@ pub struct Editing {
/// The native document an SVG carries, if any, and whether the SVG was edited since.
pub fn editing(svg: &str) -> Option<Editing> {
if !svg.contains(EDITING_NS) {
// (An SVG nested too deep for the XML reader carries none: it isn't one of ours.)
if !svg.contains(EDITING_NS) || import::xml_too_deep(svg) {
return None;
}
let opts = usvg::roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
@@ -500,14 +504,18 @@ pub fn import_with_report(svg: &str) -> Result<(Document, Vec<String>), SvgError
import_with(svg, &ImportOptions::default())
}
pub use import::MAX_ELEMENT_DEPTH;
/// Import an SVG document reading the files it links to as `opts` says, also returning warnings.
pub fn import_with(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<String>), SvgError> {
import::import(svg, opts)
// usvg converts the tree by recursion, in large stack frames: the nesting it is given is
// capped (`import::depth`), and it gets room for that cap whatever thread asks.
vectorcraft_doc::depth::with_deep_stack(|| import::import(svg, opts))
}
/// Reads a linked file: its bytes and the link to it (absolute path, size, time and hash), or
/// `None` when there is no readable file at the (absolute) path.
pub type ReadFile<'a> = &'a dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)>;
pub type ReadFile<'a> = &'a (dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)> + Sync);
/// How [`import_with`] reads the files an SVG's `<image>` elements link to.
///
+49 -15
View File
@@ -25,6 +25,8 @@ 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;
/// How far (levels) a pixel beside a gradient may be from the gradient's colour there and join it.
const GROW: f64 = 10.0;
/// A linear gradient a traced path is filled with.
#[derive(Clone, Debug, PartialEq)]
@@ -52,6 +54,12 @@ impl TracedGradient {
}
}
/// The pixels beside pixel `i` of a `w` × `h` image.
fn beside(i: usize, w: usize, h: usize) -> impl Iterator<Item = 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()
}
/// 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>) {
@@ -103,11 +111,8 @@ pub(crate) fn detect(img: &Raster) -> (Vec<u32>, Vec<TracedGradient>) {
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.
// The connected smooth pixels from `seed` that `seen` has not: into `members`.
let mut flood = |seed: usize, seen: &mut [bool], members: &mut Vec<usize>| {
members.clear();
stack.push(seed);
if let Some(s) = seen.get_mut(seed) {
@@ -115,8 +120,7 @@ pub(crate) fn detect(img: &Raster) -> (Vec<u32>, Vec<TracedGradient>) {
}
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() {
for j in beside(i, w, h) {
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;
@@ -125,7 +129,26 @@ pub(crate) fn detect(img: &Raster) -> (Vec<u32>, Vec<TracedGradient>) {
}
}
}
if members.len() < MIN_PIXELS || found.len() >= 1000 {
};
// The areas, largest first: a gradient that noise or a soft rim broke into several is found in
// its largest piece and grown over the rest.
let mut areas: Vec<(usize, usize)> = vec![];
for seed in 0..w * h {
if !seen.get(seed).copied().unwrap_or(true) && smooth.get(seed).copied().unwrap_or(false) {
flood(seed, &mut seen, &mut members);
if members.len() >= MIN_PIXELS {
areas.push((members.len(), seed));
}
}
}
areas.sort_unstable_by(|a, b| b.cmp(a));
seen.fill(false);
for (_, seed) in areas {
if seen.get(seed).copied().unwrap_or(true) || found.len() >= 1000 {
continue;
}
flood(seed, &mut seen, &mut members);
if members.len() < MIN_PIXELS {
continue;
}
// The gradient is read from the area's inside, where there is one: the pixels at its rim
@@ -135,16 +158,12 @@ pub(crate) fn detect(img: &Raster) -> (Vec<u32>, Vec<TracedGradient>) {
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();
let mut front: Vec<usize> = members.iter().copied().filter(|&i| beside(i, w, h).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) {
for j in beside(i, w, h) {
if let Some(slot) = depth.get_mut(j).filter(|s| **s == unknown) {
*slot = d;
next.push(j);
@@ -156,12 +175,27 @@ pub(crate) fn detect(img: &Raster) -> (Vec<u32>, Vec<TracedGradient>) {
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 {
if let Some((g, mut part)) = fitted {
for &i in &part {
if let Some(o) = of.get_mut(i) {
*o = found.len() as u32;
}
}
// Grown over every pixel next to it that has its colour there: the pixels noise put a
// little off, a soft rim, the pieces noise cut off it.
while let Some(i) = part.pop() {
for j in beside(i, w, h) {
let near = px(j).is_some_and(|c| {
let own = g.color_at(Point::new((j % w) as f64 + 0.5, (j / w) as f64 + 0.5));
(0..3).all(|k| (f64::from(c[k]) - own[k]).abs() <= GROW)
});
if near && of.get(j).copied() == Some(u32::MAX) {
of[j] = found.len() as u32;
seen[j] = true;
part.push(j);
}
}
}
found.push(g);
}
}
+2
View File
@@ -4099,6 +4099,8 @@ imageTrace Outline Obrys
@msg recovery copy `{file}`: {e} kopie pro obnovení `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi rozlišení musí být mezi 1 a 2400 ppi
@msg result would exceed the canvas (coordinates out of range) výsledek by přesáhl plátno (souřadnice mimo rozsah)
@msg objects can't be nested more than 32 deep objekty nelze vnořit hlouběji než do 32 úrovní
@msg the SVG's elements are nested more than {0} deep prvky SVG jsou vnořeny hlouběji než do {0} úrovní
@msg scale must be a number from {_1} to {_2}, not {_3} měřítko musí být číslo od {_1} do {_2}, ne {_3}
@msg scale must be a number from {_1} to {_2}, not {s} měřítko musí být číslo od {_1} do {_2}, ne {s}
@msg select (or target) objects whose appearance isn't basic vyberte (nebo zacilte) objekty, jejichž vzhled není základní
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3705,6 +3705,8 @@ theme Appearance Erscheinungsbild
@msg recovery copy `{file}`: {e} Wiederherstellungskopie `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi Auflösungen müssen zwischen 1 und 2400 ppi liegen
@msg result would exceed the canvas (coordinates out of range) das Ergebnis würde die Arbeitsfläche überschreiten (Koordinaten außerhalb des Bereichs)
@msg objects can't be nested more than 32 deep Objekte können nicht tiefer als 32 Ebenen verschachtelt werden
@msg the SVG's elements are nested more than {0} deep die Elemente der SVG-Datei sind tiefer als {0} Ebenen verschachtelt
@msg scale must be a number from {_1} to {_2}, not {_3} scale muss eine Zahl von {_1} bis {_2} sein, nicht {_3}
@msg scale must be a number from {_1} to {_2}, not {s} scale muss eine Zahl von {_1} bis {_2} sein, nicht {s}
@msg select (or target) objects whose appearance isn't basic wählen Sie Objekte aus (oder als Ziel), deren Aussehen nicht einfach ist
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3804,6 +3804,8 @@ axis Both Ambos
@msg recovery copy `{file}`: {e} copia de recuperación `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi las resoluciones deben estar entre 1 y 2400 ppi
@msg result would exceed the canvas (coordinates out of range) el resultado excedería el lienzo (coordenadas fuera de rango)
@msg objects can't be nested more than 32 deep los objetos no pueden anidarse a más de 32 niveles
@msg the SVG's elements are nested more than {0} deep los elementos del SVG están anidados a más de {0} niveles
@msg scale must be a number from {_1} to {_2}, not {_3} la escala debe ser un número de {_1} a {_2}, no {_3}
@msg scale must be a number from {_1} to {_2}, not {s} la escala debe ser un número de {_1} a {_2}, no {s}
@msg select (or target) objects whose appearance isn't basic selecciona (o apunta a) objetos cuya apariencia no sea básica
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3801,6 +3801,8 @@ axis Both Les deux
@msg recovery copy `{file}`: {e} copie de récupération `{file}` : {e}
@msg resolutions must be between 1 and 2400 ppi les résolutions doivent être comprises entre 1 et 2400 ppp
@msg result would exceed the canvas (coordinates out of range) le résultat dépasserait la zone de travail (coordonnées hors limites)
@msg objects can't be nested more than 32 deep les objets ne peuvent pas être imbriqués sur plus de 32 niveaux
@msg the SVG's elements are nested more than {0} deep les éléments du SVG sont imbriqués sur plus de {0} niveaux
@msg scale must be a number from {_1} to {_2}, not {_3} l’échelle doit être un nombre compris entre {_1} et {_2}, pas {_3}
@msg scale must be a number from {_1} to {_2}, not {s} l’échelle doit être un nombre compris entre {_1} et {_2}, pas {s}
@msg select (or target) objects whose appearance isn't basic sélectionnez (ou ciblez) des objets dont l’aspect n’est pas basique
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4441,6 +4441,8 @@ imageTrace Outline Körvonal
@msg resolutions must be between 1 and 2400 ppi a felbontás 1 és 2400 ppi között lehet
@msg Restored {_1} recovered document(s): save them to keep them Helyreállított dokumentumok: {_1}; mentsd őket, ha meg akarod tartani
@msg result would exceed the canvas (coordinates out of range) az eredmény túllépné a vásznat (a koordináták tartományon kívül esnek)
@msg objects can't be nested more than 32 deep az objektumok legfeljebb 32 szint mélyen ágyazhatók egymásba
@msg the SVG's elements are nested more than {0} deep az SVG elemei több mint {0} szint mélyen vannak egymásba ágyazva
@msg Save for Web: the image is empty Mentés webre: a kép üres
@msg Save writes {_1}, not {_2}: use document.export for other formats a Mentés ezt írja: {_1}, ezt nem: {_2}; más formátumhoz használd a document.export parancsot
@msg saved panel layout is invalid; reset the workspace first a mentett panelelrendezés érvénytelen; előbb állítsd vissza a munkaterületet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3804,6 +3804,8 @@ axis Both Entrambi
@msg recovery copy `{file}`: {e} copia di recupero `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi le risoluzioni devono essere comprese tra 1 e 2400 ppi
@msg result would exceed the canvas (coordinates out of range) il risultato supererebbe l'area di disegno (coordinate fuori intervallo)
@msg objects can't be nested more than 32 deep gli oggetti non possono essere nidificati oltre 32 livelli
@msg the SVG's elements are nested more than {0} deep gli elementi dell'SVG sono nidificati oltre {0} livelli
@msg scale must be a number from {_1} to {_2}, not {_3} la scala deve essere un numero da {_1} a {_2}, non {_3}
@msg scale must be a number from {_1} to {_2}, not {s} la scala deve essere un numero da {_1} a {_2}, non {s}
@msg select (or target) objects whose appearance isn't basic seleziona (o imposta come destinazione) oggetti con un aspetto non di base
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3730,6 +3730,8 @@ theme Appearance 外観
@msg no interaction in progress 進行中の操作がありません
@msg no such object {0} オブジェクト {0} が見つかりません
@msg result would exceed the canvas (coordinates out of range) 結果がカンバスの範囲を超えます (座標が範囲外です)
@msg objects can't be nested more than 32 deep オブジェクトは 32 階層より深く入れ子にできません
@msg the SVG's elements are nested more than {0} deep SVG の要素が {0} 階層より深く入れ子になっています
@msg unknown command `{0}` `{0}` は不明なコマンドです
@msg `{what}` closes nothing `{what}` に対応する開始部分がありません
@msg `{what}` closes something it didn't open `{what}` が対応しない開始部分を閉じています
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3708,6 +3708,8 @@ theme Appearance 외관
@msg no interaction in progress 진행 중인 상호작용이 없습니다
@msg no such object {0} 개체 {0}이(가) 없습니다
@msg result would exceed the canvas (coordinates out of range) 결과가 캔버스를 벗어납니다(좌표 범위 초과)
@msg objects can't be nested more than 32 deep 개체는 32단계보다 깊게 중첩할 수 없습니다
@msg the SVG's elements are nested more than {0} deep SVG 요소가 {0}단계보다 깊게 중첩되어 있습니다
@msg unknown command `{0}` 알 수 없는 명령 `{0}`
@msg `{what}` closes nothing `{what}`이(가) 닫는 항목이 없습니다
@msg `{what}` closes something it didn't open `{what}`이(가) 열지 않은 항목을 닫습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3965,6 +3965,8 @@ imageTrace Outline Kontur
@msg recovery copy `{file}`: {e} kopia odzyskiwania `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi rozdzielczości muszą mieścić się w zakresie od 1 do 2400 ppi
@msg result would exceed the canvas (coordinates out of range) wynik wykroczyłby poza płótno (współrzędne poza zakresem)
@msg objects can't be nested more than 32 deep obiektów nie można zagnieżdżać głębiej niż na 32 poziomy
@msg the SVG's elements are nested more than {0} deep elementy SVG są zagnieżdżone głębiej niż na {0} poziomów
@msg scale must be a number from {_1} to {_2}, not {_3} skala musi być liczbą od {_1} do {_2}, nie {_3}
@msg scale must be a number from {_1} to {_2}, not {s} skala musi być liczbą od {_1} do {_2}, nie {s}
@msg select (or target) objects whose appearance isn't basic zaznacz (lub wskaż) obiekty, których wygląd nie jest podstawowy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3449,6 +3449,8 @@ axis Both Ambos
@msg recovery copy `{file}`: {e} cópia de recuperação `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi as resoluções devem estar entre 1 e 2400 ppi
@msg result would exceed the canvas (coordinates out of range) o resultado excederia a tela (coordenadas fora do intervalo)
@msg objects can't be nested more than 32 deep os objetos não podem ser aninhados em mais de 32 níveis
@msg the SVG's elements are nested more than {0} deep os elementos do SVG estão aninhados em mais de {0} níveis
@msg scale must be a number from {_1} to {_2}, not {_3} a escala deve ser um número de {_1} a {_2}, não {_3}
@msg scale must be a number from {_1} to {_2}, not {s} a escala deve ser um número de {_1} a {_2}, não {s}
@msg select (or target) objects whose appearance isn't basic selecione (ou direcione) objetos cuja aparência não seja básica
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -3800,6 +3800,8 @@ axis Both Оба
@msg recovery copy `{file}`: {e} копия восстановления `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi разрешения должны быть от 1 до 2400 ppi
@msg result would exceed the canvas (coordinates out of range) результат превысил бы холст (координаты вне диапазона)
@msg objects can't be nested more than 32 deep объекты нельзя вкладывать глубже 32 уровней
@msg the SVG's elements are nested more than {0} deep элементы SVG вложены глубже {0} уровней
@msg scale must be a number from {_1} to {_2}, not {_3} масштаб должен быть числом от {_1} до {_2}, а не {_3}
@msg scale must be a number from {_1} to {_2}, not {s} масштаб должен быть числом от {_1} до {_2}, а не {s}
@msg select (or target) objects whose appearance isn't basic выделите (или укажите как объект) объекты, оформление которых не является основным
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3788,6 +3788,8 @@ axis Both Обидві
@msg recovery copy `{file}`: {e} копія відновлення `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi дозволи мають бути від 1 до 2400 ppi
@msg result would exceed the canvas (coordinates out of range) результат перевищив би полотно (координати поза діапазоном)
@msg objects can't be nested more than 32 deep об’єкти не можна вкладати глибше ніж на 32 рівні
@msg the SVG's elements are nested more than {0} deep елементи SVG вкладено глибше ніж на {0} рівнів
@msg scale must be a number from {_1} to {_2}, not {_3} масштаб має бути числом від {_1} до {_2}, а не {_3}
@msg scale must be a number from {_1} to {_2}, not {s} масштаб має бути числом від {_1} до {_2}, а не {s}
@msg select (or target) objects whose appearance isn't basic виділіть (або вкажіть як об’єкт) об’єкти, оформлення яких не є основним
Can't render this file because it contains an unexpected character in line 713 and column 40.
+2
View File
@@ -4071,6 +4071,8 @@ theme Appearance Giao diện
@msg resolutions must be between 1 and 2400 ppi độ phân giải phải nằm trong khoảng 1 đến 2400 ppi
@msg Restored {_1} recovered document(s): save them to keep them Đã phục hồi {_1} tài liệu: hãy lưu để giữ lại
@msg result would exceed the canvas (coordinates out of range) kết quả sẽ vượt quá vùng vẽ (tọa độ ngoài phạm vi)
@msg objects can't be nested more than 32 deep không thể lồng các đối tượng sâu quá 32 cấp
@msg the SVG's elements are nested more than {0} deep các phần tử SVG lồng nhau sâu quá {0} cấp
@msg Save for Web: the image is empty Lưu cho Web: ảnh trống
@msg Save writes {_1}, not {_2}: use document.export for other formats Lưu ghi {_1}, không phải {_2}: dùng document.export cho các định dạng khác
@msg saved panel layout is invalid; reset the workspace first bố cục bảng đã lưu không hợp lệ; hãy đặt lại không gian làm việc trước
Can't render this file because it contains an unexpected character in line 3642 and column 10.
+183 -182
View File
@@ -1,6 +1,6 @@
# Image Trace quality
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (enhanced versions of the presets, each measured beside its preset)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Gradients: grown over noise and soft rims)
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
@@ -88,7 +88,7 @@ Each preset on the inputs it is meant for, all sizes and variants.
| 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 (with the enhanced presets)
### Now (with gradients grown over noise and soft rims)
| 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 % | false shapes | grouped % | gradient paths | found % | angle off | ramp off |
|:--|:--|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
@@ -113,9 +113,9 @@ Each preset on the inputs it is meant for, all sizes and variants.
| 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 |
| 6 Colors with Gradients | two-stop gradient, three-stop gradient in a disc | 0.62 | 1.34 | – | – | – | 1.2 | 60 | – | 0 | 0 | – | – | – | – | 1.6 | 1.2 | 0.0 | 0.0 | – | – | – | – | – | – | 0.9 | 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 |
| High Fidelity Photo with Gradients | two-stop gradient, three-stop gradient in a disc | 0.46 | 1.29 | – | – | – | 8.2 | 545 | – | 1 | 0 | – | – | – | – | 0.0 | – | 0.8 | 0.0 | – | – | – | – | – | – | 0.9 | 86 | 1.5 | 21 |
| Default, enhanced | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 9.88 | 0.083 | 1.00 | 2.18 | 1.9 | 16 | 1.59 | 36 | 6 | 83 | 0.4 | – | – | 0.3 | 0.5 | 0.0 | 0.0 | – | – | – | – | 0.02 | 14 | 0.0 | – | – | – |
| Auto Color, enhanced | two touching halves, four-colour badge | 0.55 | 5.00 | 0.139 | 1.33 | 1.00 | 4.0 | 48 | 3.03 | 41 | 16 | 100 | 6.0 | 0.0 | 0 | 1.3 | 1.1 | 0.0 | 0.0 | – | – | – | – | 0.00 | 27 | 0.0 | – | – | – |
| High Fidelity Photo, enhanced | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | – | 69 | 0.0 | – | – | – |
@@ -131,7 +131,7 @@ Each preset on the inputs it is meant for, all sizes and variants.
| Technical Drawing, enhanced | polygons, star, letter A, lines, askew shapes and lines | 2.65 | 10.93 | 0.128 | 1.46 | 105.64 | 4.7 | 34 | 2.26 | 86 | 19 | 86 | 0.8 | – | – | – | – | 0.5 | 0.0 | – | – | – | – | 0.09 | 0 | 0.0 | – | – | – |
| Outline, enhanced | lines | 2.61 | 9.39 | 0.050 | 0.88 | 105.74 | 5.0 | 10 | 0.52 | 99 | 4 | – | – | – | – | – | – | 0.0 | 0.0 | 0.039 | 0.02 | 2.0 | – | 0.00 | 0 | 0.0 | – | – | – |
The sections below are the numbers now (with the enhanced presets).
The sections below are the numbers now (with gradients grown over noise and soft rims).
## 1. Each preset on the inputs it is meant for
@@ -160,9 +160,9 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs
| 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 |
| 6 Colors with Gradients | two-stop gradient, three-stop gradient in a disc | 0.62 | 1.34 | – | – | – | 1.2 | 60 | – | 0 | 0 | – | – | – | – | 1.6 | 1.2 | 0.0 | 0.0 | – | – | – | – | – | – | 0.9 | 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 |
| High Fidelity Photo with Gradients | two-stop gradient, three-stop gradient in a disc | 0.46 | 1.29 | – | – | – | 8.2 | 545 | – | 1 | 0 | – | – | – | – | 0.0 | – | 0.8 | 0.0 | – | – | – | – | – | – | 0.9 | 86 | 1.5 | 21 |
| Default, enhanced | circle, ellipses, badge with a hole, letter D, polygons, star, letter A | 2.60 | 9.88 | 0.083 | 1.00 | 2.18 | 1.9 | 16 | 1.59 | 36 | 6 | 83 | 0.4 | – | – | 0.3 | 0.5 | 0.0 | 0.0 | – | – | – | – | 0.02 | 14 | 0.0 | – | – | – |
| Auto Color, enhanced | two touching halves, four-colour badge | 0.55 | 5.00 | 0.139 | 1.33 | 1.00 | 4.0 | 48 | 3.03 | 41 | 16 | 100 | 6.0 | 0.0 | 0 | 1.3 | 1.1 | 0.0 | 0.0 | – | – | – | – | 0.00 | 27 | 0.0 | – | – | – |
| High Fidelity Photo, enhanced | gradient, photo-like | 1.75 | 1.98 | – | – | – | 120.7 | 9929 | – | 38 | 36 | – | – | – | – | 0.0 | – | 10.1 | 0.0 | – | – | – | – | – | 69 | 0.0 | – | – | – |
@@ -356,7 +356,7 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs
| 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.00 | – | 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.00 | – | 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.00 | – | 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 | gradient, photo | 4.49 | 4.84 | – | – | – | 15.0 | 909 | – | 36 | 27 | – | – | – | – | 9.7 | 12.0 | 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.00 | – | 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.00 | – | 0.0 | – | – | – |
@@ -372,7 +372,7 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs
| 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.00 | – | 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.00 | – | 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.00 | – | 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 | gradient, photo | 1.79 | 2.04 | – | – | – | 138.0 | 11156 | – | 40 | 39 | – | – | – | – | 0.0 | – | 9.2 | 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 |
| Default, enhanced | curved shapes | 0.08 | 3.21 | 0.056 | 0.38 | 0.38 | 1.8 | 13 | 1.44 | 14 | 14 | 100 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0.00 | 17 | 0.0 | – | – | – |
| Default, enhanced | polygons | 0.07 | 2.58 | 0.045 | 0.50 | 1.26 | 2.0 | 13 | 1.16 | 97 | 5 | 100 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0.00 | 11 | 0.0 | – | – | – |
@@ -598,9 +598,9 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs
| 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 |
| 6 Colors with Gradients | blurred | 0.33 | 2.23 | – | – | – | 0.5 | 24 | – | 0 | 0 | – | – | – | – | 7.1 | 3.7 | 0.0 | 0.0 | – | – | – | – | – | – | 1.0 | 100 | 0.3 | 6 |
| 6 Colors with Gradients | noisy | 1.02 | 1.27 | – | – | – | 0.5 | 14 | – | 0 | 0 | – | – | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | – | – | 1.0 | 100 | 0.2 | 8 |
| 6 Colors with Gradients | scanned | 1.57 | 1.82 | – | – | – | 3.0 | 264 | – | 3 | 0 | – | – | – | – | 2.8 | 1.6 | 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 | – | – |
@@ -608,9 +608,9 @@ All sizes and variants (clean, aliased, blurred, noisy, scanned) of those inputs
| 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 |
| High Fidelity Photo with Gradients | blurred | 0.33 | 2.20 | – | – | – | 0.5 | 48 | – | 1 | 0 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | – | 1.0 | 100 | 0.3 | 6 |
| High Fidelity Photo with Gradients | noisy | 1.02 | 1.21 | – | – | – | 1.5 | 116 | – | 0 | 0 | – | – | – | – | 0.0 | – | 0.0 | 0.0 | – | – | – | – | – | – | 1.0 | 100 | 0.2 | 8 |
| High Fidelity Photo with Gradients | scanned | 0.97 | 1.55 | – | – | – | 37.0 | 2868 | – | 5 | 3 | – | – | – | – | 0.0 | – | 5.5 | 0.0 | – | – | – | – | – | – | 0.5 | 50 | 1.5 | 21 |
| Default, enhanced | clean | 0.08 | 2.94 | 0.051 | 0.50 | 1.26 | 1.9 | 13 | 1.32 | 50 | 10 | 100 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0.00 | 14 | 0.0 | – | – | – |
| Default, enhanced | aliased | 0.36 | 19.23 | 0.194 | 1.00 | 1.35 | 1.9 | 14 | 1.42 | 28 | 8 | 98 | 0.0 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0.14 | 14 | 0.0 | – | – | – |
| Default, enhanced | blurred | 0.81 | 16.50 | 0.073 | 0.98 | 2.18 | 1.9 | 22 | 2.15 | 17 | 5 | 38 | 1.3 | – | – | 0.0 | 0.0 | 0.0 | 0.0 | – | – | – | – | 0.00 | 14 | 0.0 | – | – | – |
@@ -722,27 +722,27 @@ 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.07 s, 29 paths, 6278 anchors, 12 MB |
| Default | four-colour badge | 1 | 0.04 s, 3 paths, 64 anchors, 12 MB |
| Auto Color | photo-like | 1 | 0.19 s, 732 paths, 45265 anchors, 23 MB |
| Auto Color | four-colour badge | 1 | 0.08 s, 5 paths, 84 anchors, 12 MB |
| High Fidelity Photo | photo-like | 1 | 0.24 s, 968 paths, 117041 anchors, 151 MB |
| High Fidelity Photo | four-colour badge | 1 | 0.08 s, 6 paths, 172 anchors, 23 MB |
| Low Fidelity Photo | photo-like | 1 | 0.21 s, 129 paths, 23014 anchors, 106 MB |
| Low Fidelity Photo | four-colour badge | 1 | 0.08 s, 6 paths, 136 anchors, 23 MB |
| Default | photo-like | 1 | 0.04 s, 29 paths, 6278 anchors, 12 MB |
| Default | four-colour badge | 1 | 0.02 s, 3 paths, 64 anchors, 12 MB |
| Auto Color | photo-like | 1 | 0.17 s, 732 paths, 45265 anchors, 24 MB |
| Auto Color | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| High Fidelity Photo | photo-like | 1 | 0.22 s, 968 paths, 117041 anchors, 171 MB |
| High Fidelity Photo | four-colour badge | 1 | 0.06 s, 6 paths, 172 anchors, 23 MB |
| Low Fidelity Photo | photo-like | 1 | 0.12 s, 129 paths, 23014 anchors, 105 MB |
| Low Fidelity Photo | four-colour badge | 1 | 0.05 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.07 s, 4 paths, 88 anchors, 13 MB |
| 6 Colors | photo-like | 1 | 0.12 s, 78 paths, 9076 anchors, 23 MB |
| 6 Colors | four-colour badge | 1 | 0.06 s, 5 paths, 84 anchors, 12 MB |
| 16 Colors | photo-like | 1 | 0.11 s, 178 paths, 20470 anchors, 31 MB |
| 3 Colors | four-colour badge | 1 | 0.05 s, 4 paths, 88 anchors, 13 MB |
| 6 Colors | photo-like | 1 | 0.10 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| 16 Colors | photo-like | 1 | 0.11 s, 178 paths, 20470 anchors, 26 MB |
| 16 Colors | four-colour badge | 1 | 0.05 s, 5 paths, 88 anchors, 12 MB |
| Shades of Gray | photo-like | 1 | 0.09 s, 81 paths, 6698 anchors, 40 MB |
| Shades of Gray | four-colour badge | 1 | 0.07 s, 10 paths, 725 anchors, 34 MB |
| Black and White Logo | photo-like | 1 | 0.06 s, 71 paths, 4933 anchors, 8 MB |
| Shades of Gray | photo-like | 1 | 0.10 s, 81 paths, 6698 anchors, 41 MB |
| Shades of Gray | four-colour badge | 1 | 0.06 s, 10 paths, 725 anchors, 34 MB |
| Black and White Logo | photo-like | 1 | 0.04 s, 71 paths, 4933 anchors, 8 MB |
| Black and White Logo | four-colour badge | 1 | 0.02 s, 1 paths, 47 anchors, 6 MB |
| Sketched Art | photo-like | 1 | 0.05 s, 5 paths, 2873 anchors, 8 MB |
| Sketched Art | four-colour badge | 1 | 0.03 s, 2 paths, 114 anchors, 6 MB |
| Silhouettes | photo-like | 1 | 0.04 s, 2 paths, 556 anchors, 9 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, 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, 4495 anchors, 8 MB |
| Line Art | four-colour badge | 1 | 0.02 s, 1 paths, 38 anchors, 6 MB |
@@ -750,42 +750,42 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab
| Technical Drawing | four-colour badge | 1 | 0.02 s, 1 paths, 47 anchors, 6 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.12 s (not flat art), 40 MB |
| Flat Logo | photo-like | 1 | refused after 0.14 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 1 | 0.66 s, 5 paths, 78 anchors, 146 MB |
| Default with Shapes | photo-like | 1 | 0.05 s, 29 paths, 6278 anchors, 12 MB |
| Default with Shapes | four-colour badge | 1 | 0.02 s, 3 paths, 62 anchors, 12 MB |
| 6 Colors with Shapes | photo-like | 1 | 0.12 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors with Shapes | photo-like | 1 | 0.11 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors with Shapes | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| 6 Colors with Auto Grouping | photo-like | 1 | 0.11 s, 78 paths, 9076 anchors, 24 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, 43 MB |
| Shades of Gray with Auto Grouping | photo-like | 1 | 0.10 s, 81 paths, 6698 anchors, 41 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 1 | 0.06 s, 10 paths, 725 anchors, 34 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 1 | 0.27 s, 968 paths, 117041 anchors, 170 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 1 | 0.24 s, 968 paths, 117041 anchors, 169 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 1 | 0.05 s, 6 paths, 172 anchors, 23 MB |
| 6 Colors, no Gradients | photo-like | 1 | 0.08 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.16 s, 78 paths, 9076 anchors, 32 MB |
| 6 Colors with Gradients | four-colour badge | 1 | 0.12 s, 5 paths, 84 anchors, 12 MB |
| High Fidelity Photo, no Gradients | photo-like | 1 | 0.25 s, 968 paths, 117041 anchors, 153 MB |
| High Fidelity Photo, no Gradients | four-colour badge | 1 | 0.08 s, 6 paths, 172 anchors, 23 MB |
| High Fidelity Photo with Gradients | photo-like | 1 | 0.30 s, 968 paths, 117041 anchors, 157 MB |
| High Fidelity Photo with Gradients | four-colour badge | 1 | 0.11 s, 6 paths, 172 anchors, 23 MB |
| Default, enhanced | photo-like | 1 | 0.10 s, 29 paths, 6278 anchors, 12 MB |
| Default, enhanced | four-colour badge | 1 | 0.03 s, 3 paths, 62 anchors, 12 MB |
| Auto Color, enhanced | photo-like | 1 | 0.18 s, 732 paths, 45260 anchors, 24 MB |
| Auto Color, enhanced | four-colour badge | 1 | 0.06 s, 5 paths, 84 anchors, 12 MB |
| 6 Colors, no Gradients | photo-like | 1 | 0.09 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors, no Gradients | four-colour badge | 1 | 0.07 s, 5 paths, 84 anchors, 12 MB |
| 6 Colors with Gradients | photo-like | 1 | 0.15 s, 78 paths, 9076 anchors, 32 MB |
| 6 Colors with Gradients | four-colour badge | 1 | 0.08 s, 5 paths, 84 anchors, 12 MB |
| High Fidelity Photo, no Gradients | photo-like | 1 | 0.23 s, 968 paths, 117041 anchors, 170 MB |
| High Fidelity Photo, no Gradients | four-colour badge | 1 | 0.06 s, 6 paths, 172 anchors, 23 MB |
| High Fidelity Photo with Gradients | photo-like | 1 | 0.26 s, 968 paths, 117041 anchors, 169 MB |
| High Fidelity Photo with Gradients | four-colour badge | 1 | 0.09 s, 6 paths, 172 anchors, 23 MB |
| Default, enhanced | photo-like | 1 | 0.05 s, 29 paths, 6278 anchors, 12 MB |
| Default, enhanced | four-colour badge | 1 | 0.02 s, 3 paths, 62 anchors, 12 MB |
| Auto Color, enhanced | photo-like | 1 | 0.17 s, 732 paths, 45260 anchors, 27 MB |
| Auto Color, enhanced | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| High Fidelity Photo, enhanced | photo-like | 1 | 0.26 s, 968 paths, 117041 anchors, 169 MB |
| High Fidelity Photo, enhanced | four-colour badge | 1 | 0.05 s, 6 paths, 172 anchors, 23 MB |
| Low Fidelity Photo, enhanced | photo-like | 1 | 0.12 s, 129 paths, 23014 anchors, 105 MB |
| Low Fidelity Photo, enhanced | photo-like | 1 | 0.14 s, 129 paths, 23014 anchors, 105 MB |
| Low Fidelity Photo, enhanced | four-colour badge | 1 | 0.05 s, 6 paths, 116 anchors, 23 MB |
| 3 Colors, enhanced | photo-like | 1 | 0.08 s, 9 paths, 3341 anchors, 18 MB |
| 3 Colors, enhanced | photo-like | 1 | 0.09 s, 9 paths, 3341 anchors, 18 MB |
| 3 Colors, enhanced | four-colour badge | 1 | 0.05 s, 4 paths, 76 anchors, 13 MB |
| 6 Colors, enhanced | photo-like | 1 | 0.18 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors, enhanced | photo-like | 1 | 0.10 s, 78 paths, 9076 anchors, 24 MB |
| 6 Colors, enhanced | four-colour badge | 1 | 0.05 s, 5 paths, 84 anchors, 12 MB |
| 16 Colors, enhanced | photo-like | 1 | 0.11 s, 178 paths, 20467 anchors, 31 MB |
| 16 Colors, enhanced | photo-like | 1 | 0.13 s, 178 paths, 20467 anchors, 31 MB |
| 16 Colors, enhanced | four-colour badge | 1 | 0.05 s, 5 paths, 88 anchors, 12 MB |
| Shades of Gray, enhanced | photo-like | 1 | 0.09 s, 81 paths, 6698 anchors, 42 MB |
| Shades of Gray, enhanced | four-colour badge | 1 | 0.06 s, 10 paths, 623 anchors, 34 MB |
| Shades of Gray, enhanced | photo-like | 1 | 0.10 s, 81 paths, 6698 anchors, 40 MB |
| Shades of Gray, enhanced | four-colour badge | 1 | 0.06 s, 10 paths, 623 anchors, 35 MB |
| Black and White Logo, enhanced | photo-like | 1 | 0.05 s, 71 paths, 4933 anchors, 8 MB |
| Black and White Logo, enhanced | four-colour badge | 1 | 0.02 s, 1 paths, 46 anchors, 6 MB |
| Sketched Art, enhanced | photo-like | 1 | 0.04 s, 5 paths, 2873 anchors, 8 MB |
@@ -798,163 +798,164 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab
| Technical Drawing, enhanced | four-colour badge | 1 | 0.02 s, 1 paths, 46 anchors, 6 MB |
| Outline, enhanced | photo-like | 1 | 0.05 s, 45 paths, 4110 anchors, 16 MB |
| Outline, enhanced | four-colour badge | 1 | 0.03 s, 1 paths, 38 anchors, 11 MB |
| Default | photo-like | 4 | 0.18 s, 48 paths, 14438 anchors, 49 MB |
| Default | photo-like | 4 | 0.20 s, 48 paths, 14438 anchors, 49 MB |
| Default | four-colour badge | 4 | 0.10 s, 3 paths, 72 anchors, 48 MB |
| Auto Color | photo-like | 4 | 0.44 s, 1469 paths, 149389 anchors, 113 MB |
| Auto Color | four-colour badge | 4 | 0.18 s, 5 paths, 92 anchors, 46 MB |
| High Fidelity Photo | photo-like | 4 | 0.71 s, 4479 paths, 337884 anchors, 636 MB |
| Auto Color | photo-like | 4 | 0.44 s, 1469 paths, 149389 anchors, 105 MB |
| Auto Color | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 47 MB |
| High Fidelity Photo | photo-like | 4 | 0.80 s, 4479 paths, 337884 anchors, 636 MB |
| High Fidelity Photo | four-colour badge | 4 | 0.25 s, 6 paths, 200 anchors, 94 MB |
| Low Fidelity Photo | photo-like | 4 | 0.37 s, 299 paths, 61601 anchors, 400 MB |
| Low Fidelity Photo | four-colour badge | 4 | 0.22 s, 6 paths, 152 anchors, 94 MB |
| 3 Colors | photo-like | 4 | 0.29 s, 20 paths, 8473 anchors, 73 MB |
| 3 Colors | four-colour badge | 4 | 0.19 s, 4 paths, 122 anchors, 53 MB |
| 6 Colors | photo-like | 4 | 0.26 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| 16 Colors | photo-like | 4 | 0.29 s, 353 paths, 52770 anchors, 119 MB |
| 16 Colors | four-colour badge | 4 | 0.19 s, 5 paths, 96 anchors, 46 MB |
| Shades of Gray | photo-like | 4 | 0.28 s, 137 paths, 18823 anchors, 161 MB |
| Shades of Gray | four-colour badge | 4 | 0.27 s, 10 paths, 1256 anchors, 134 MB |
| Low Fidelity Photo | photo-like | 4 | 0.40 s, 299 paths, 61601 anchors, 395 MB |
| Low Fidelity Photo | four-colour badge | 4 | 0.41 s, 6 paths, 152 anchors, 97 MB |
| 3 Colors | photo-like | 4 | 0.79 s, 20 paths, 8473 anchors, 73 MB |
| 3 Colors | four-colour badge | 4 | 0.30 s, 4 paths, 122 anchors, 53 MB |
| 6 Colors | photo-like | 4 | 0.34 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors | four-colour badge | 4 | 0.22 s, 5 paths, 92 anchors, 46 MB |
| 16 Colors | photo-like | 4 | 0.34 s, 353 paths, 52770 anchors, 129 MB |
| 16 Colors | four-colour badge | 4 | 0.22 s, 5 paths, 96 anchors, 47 MB |
| Shades of Gray | photo-like | 4 | 0.32 s, 137 paths, 18823 anchors, 156 MB |
| Shades of Gray | four-colour badge | 4 | 0.28 s, 10 paths, 1256 anchors, 136 MB |
| Black and White Logo | photo-like | 4 | 0.20 s, 208 paths, 13546 anchors, 32 MB |
| Black and White Logo | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 MB |
| Sketched Art | photo-like | 4 | 0.19 s, 9 paths, 5387 anchors, 32 MB |
| Sketched Art | four-colour badge | 4 | 0.08 s, 2 paths, 196 anchors, 24 MB |
| Sketched Art | photo-like | 4 | 0.20 s, 9 paths, 5387 anchors, 32 MB |
| Sketched Art | four-colour badge | 4 | 0.09 s, 2 paths, 196 anchors, 24 MB |
| Silhouettes | photo-like | 4 | 0.21 s, 2 paths, 1465 anchors, 36 MB |
| Silhouettes | four-colour badge | 4 | 0.08 s, 1 paths, 28 anchors, 24 MB |
| Line Art | photo-like | 4 | 0.19 s, 127 paths, 11330 anchors, 32 MB |
| Line Art | four-colour badge | 4 | 0.08 s, 1 paths, 42 anchors, 24 MB |
| Technical Drawing | photo-like | 4 | 0.19 s, 931 paths, 20628 anchors, 32 MB |
| Line Art | photo-like | 4 | 0.20 s, 127 paths, 11330 anchors, 32 MB |
| Line Art | four-colour badge | 4 | 0.09 s, 1 paths, 42 anchors, 24 MB |
| Technical Drawing | photo-like | 4 | 0.20 s, 931 paths, 20628 anchors, 32 MB |
| Technical Drawing | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 MB |
| Outline | photo-like | 4 | 0.24 s, 113 paths, 10815 anchors, 64 MB |
| Outline | photo-like | 4 | 0.25 s, 113 paths, 10815 anchors, 64 MB |
| Outline | four-colour badge | 4 | 0.13 s, 1 paths, 42 anchors, 44 MB |
| Flat Logo | photo-like | 4 | refused after 0.14 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 4 | 0.67 s, 5 paths, 69 anchors, 146 MB |
| Default with Shapes | photo-like | 4 | 0.59 s, 48 paths, 14438 anchors, 49 MB |
| Default with Shapes | four-colour badge | 4 | 0.09 s, 3 paths, 72 anchors, 48 MB |
| 6 Colors with Shapes | photo-like | 4 | 0.30 s, 115 paths, 23292 anchors, 100 MB |
| Default with Shapes | photo-like | 4 | 0.22 s, 48 paths, 14438 anchors, 49 MB |
| Default with Shapes | four-colour badge | 4 | 0.10 s, 3 paths, 72 anchors, 48 MB |
| 6 Colors with Shapes | photo-like | 4 | 0.31 s, 115 paths, 23292 anchors, 100 MB |
| 6 Colors with Shapes | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| 6 Colors with Auto Grouping | photo-like | 4 | 0.27 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors with Auto Grouping | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| Shades of Gray with Auto Grouping | photo-like | 4 | 0.29 s, 137 paths, 18823 anchors, 170 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 4 | 0.28 s, 10 paths, 1256 anchors, 136 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 4 | 1.00 s, 4479 paths, 337884 anchors, 629 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 4 | 0.23 s, 6 paths, 200 anchors, 93 MB |
| 6 Colors with Auto Grouping | photo-like | 4 | 0.34 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors with Auto Grouping | four-colour badge | 4 | 0.21 s, 5 paths, 92 anchors, 47 MB |
| Shades of Gray with Auto Grouping | photo-like | 4 | 0.32 s, 137 paths, 18823 anchors, 155 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 4 | 0.30 s, 10 paths, 1256 anchors, 136 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 4 | 0.85 s, 4479 paths, 337884 anchors, 639 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 4 | 0.23 s, 6 paths, 200 anchors, 94 MB |
| 6 Colors, no Gradients | photo-like | 4 | 0.27 s, 115 paths, 23295 anchors, 100 MB |
| 6 Colors, no Gradients | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| 6 Colors with Gradients | photo-like | 4 | 0.63 s, 115 paths, 23295 anchors, 128 MB |
| 6 Colors with Gradients | four-colour badge | 4 | 0.29 s, 5 paths, 92 anchors, 46 MB |
| High Fidelity Photo, no Gradients | photo-like | 4 | 0.70 s, 4479 paths, 337884 anchors, 640 MB |
| 6 Colors with Gradients | photo-like | 4 | 0.78 s, 115 paths, 23295 anchors, 128 MB |
| 6 Colors with Gradients | four-colour badge | 4 | 0.49 s, 5 paths, 92 anchors, 48 MB |
| High Fidelity Photo, no Gradients | photo-like | 4 | 0.72 s, 4479 paths, 337884 anchors, 633 MB |
| High Fidelity Photo, no Gradients | four-colour badge | 4 | 0.23 s, 6 paths, 200 anchors, 97 MB |
| High Fidelity Photo with Gradients | photo-like | 4 | 1.50 s, 4479 paths, 337884 anchors, 359 MB |
| High Fidelity Photo with Gradients | four-colour badge | 4 | 0.59 s, 6 paths, 200 anchors, 97 MB |
| Default, enhanced | photo-like | 4 | 0.39 s, 48 paths, 14438 anchors, 49 MB |
| Default, enhanced | four-colour badge | 4 | 0.17 s, 3 paths, 72 anchors, 48 MB |
| Auto Color, enhanced | photo-like | 4 | 0.79 s, 1469 paths, 149374 anchors, 115 MB |
| Auto Color, enhanced | four-colour badge | 4 | 0.20 s, 5 paths, 92 anchors, 47 MB |
| High Fidelity Photo, enhanced | photo-like | 4 | 0.84 s, 4479 paths, 337884 anchors, 625 MB |
| High Fidelity Photo with Gradients | photo-like | 4 | 1.06 s, 4479 paths, 337884 anchors, 642 MB |
| High Fidelity Photo with Gradients | four-colour badge | 4 | 0.34 s, 6 paths, 200 anchors, 96 MB |
| Default, enhanced | photo-like | 4 | 0.22 s, 48 paths, 14438 anchors, 49 MB |
| Default, enhanced | four-colour badge | 4 | 0.10 s, 3 paths, 72 anchors, 48 MB |
| Auto Color, enhanced | photo-like | 4 | 0.49 s, 1469 paths, 149374 anchors, 113 MB |
| Auto Color, enhanced | four-colour badge | 4 | 0.18 s, 5 paths, 92 anchors, 46 MB |
| High Fidelity Photo, enhanced | photo-like | 4 | 0.83 s, 4479 paths, 337884 anchors, 638 MB |
| High Fidelity Photo, enhanced | four-colour badge | 4 | 0.23 s, 6 paths, 200 anchors, 93 MB |
| Low Fidelity Photo, enhanced | photo-like | 4 | 0.39 s, 299 paths, 61598 anchors, 399 MB |
| Low Fidelity Photo, enhanced | photo-like | 4 | 0.39 s, 299 paths, 61598 anchors, 408 MB |
| Low Fidelity Photo, enhanced | four-colour badge | 4 | 0.23 s, 6 paths, 128 anchors, 93 MB |
| 3 Colors, enhanced | photo-like | 4 | 0.29 s, 20 paths, 8471 anchors, 73 MB |
| 3 Colors, enhanced | photo-like | 4 | 0.28 s, 20 paths, 8471 anchors, 73 MB |
| 3 Colors, enhanced | four-colour badge | 4 | 0.19 s, 4 paths, 84 anchors, 51 MB |
| 6 Colors, enhanced | photo-like | 4 | 0.29 s, 115 paths, 23292 anchors, 100 MB |
| 6 Colors, enhanced | photo-like | 4 | 0.30 s, 115 paths, 23292 anchors, 100 MB |
| 6 Colors, enhanced | four-colour badge | 4 | 0.19 s, 5 paths, 92 anchors, 46 MB |
| 16 Colors, enhanced | photo-like | 4 | 0.32 s, 353 paths, 52772 anchors, 125 MB |
| 16 Colors, enhanced | photo-like | 4 | 0.33 s, 353 paths, 52772 anchors, 126 MB |
| 16 Colors, enhanced | four-colour badge | 4 | 0.19 s, 5 paths, 96 anchors, 46 MB |
| Shades of Gray, enhanced | photo-like | 4 | 0.32 s, 137 paths, 18823 anchors, 161 MB |
| Shades of Gray, enhanced | four-colour badge | 4 | 0.27 s, 10 paths, 1056 anchors, 137 MB |
| Shades of Gray, enhanced | photo-like | 4 | 0.30 s, 137 paths, 18823 anchors, 163 MB |
| Shades of Gray, enhanced | four-colour badge | 4 | 0.26 s, 10 paths, 1056 anchors, 137 MB |
| Black and White Logo, enhanced | photo-like | 4 | 0.21 s, 208 paths, 13546 anchors, 32 MB |
| Black and White Logo, enhanced | four-colour badge | 4 | 0.09 s, 1 paths, 57 anchors, 24 MB |
| Sketched Art, enhanced | photo-like | 4 | 0.20 s, 9 paths, 5387 anchors, 32 MB |
| Sketched Art, enhanced | four-colour badge | 4 | 0.09 s, 2 paths, 38 anchors, 24 MB |
| Black and White Logo, enhanced | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 MB |
| Sketched Art, enhanced | photo-like | 4 | 0.19 s, 9 paths, 5387 anchors, 32 MB |
| Sketched Art, enhanced | four-colour badge | 4 | 0.08 s, 2 paths, 38 anchors, 24 MB |
| Silhouettes, enhanced | photo-like | 4 | 0.22 s, 2 paths, 1465 anchors, 36 MB |
| Silhouettes, enhanced | four-colour badge | 4 | 0.08 s, 1 paths, 8 anchors, 24 MB |
| Line Art, enhanced | photo-like | 4 | 0.20 s, 127 paths, 11330 anchors, 32 MB |
| Line Art, enhanced | four-colour badge | 4 | 0.08 s, 1 paths, 42 anchors, 24 MB |
| Line Art, enhanced | photo-like | 4 | 0.21 s, 127 paths, 11330 anchors, 32 MB |
| Line Art, enhanced | four-colour badge | 4 | 0.09 s, 1 paths, 42 anchors, 24 MB |
| Technical Drawing, enhanced | photo-like | 4 | 0.21 s, 931 paths, 20628 anchors, 32 MB |
| Technical Drawing, enhanced | four-colour badge | 4 | 0.08 s, 1 paths, 57 anchors, 24 MB |
| Outline, enhanced | photo-like | 4 | 0.25 s, 113 paths, 10815 anchors, 64 MB |
| Outline, enhanced | four-colour badge | 4 | 0.13 s, 1 paths, 42 anchors, 44 MB |
| Default | photo-like | 16 | 1.06 s, 127 paths, 34034 anchors, 199 MB |
| Default | four-colour badge | 16 | 0.49 s, 3 paths, 88 anchors, 184 MB |
| Auto Color | photo-like | 16 | 1.66 s, 5390 paths, 454670 anchors, 283 MB |
| Auto Color | four-colour badge | 16 | 0.81 s, 5 paths, 112 anchors, 184 MB |
| High Fidelity Photo | photo-like | 16 | 3.73 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo | four-colour badge | 16 | 1.06 s, 6 paths, 264 anchors, 257 MB |
| Low Fidelity Photo | photo-like | 16 | 1.90 s, 2036 paths, 182778 anchors, 754 MB |
| Low Fidelity Photo | four-colour badge | 16 | 1.11 s, 6 paths, 180 anchors, 257 MB |
| 3 Colors | photo-like | 16 | 1.66 s, 76 paths, 23024 anchors, 290 MB |
| 3 Colors | four-colour badge | 16 | 1.50 s, 4 paths, 190 anchors, 205 MB |
| 6 Colors | photo-like | 16 | 2.07 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors | four-colour badge | 16 | 0.88 s, 5 paths, 112 anchors, 184 MB |
| 16 Colors | photo-like | 16 | 1.16 s, 1973 paths, 145642 anchors, 304 MB |
| 16 Colors | four-colour badge | 16 | 1.05 s, 5 paths, 114 anchors, 184 MB |
| Shades of Gray | photo-like | 16 | 2.07 s, 686 paths, 53654 anchors, 407 MB |
| Shades of Gray | four-colour badge | 16 | 1.69 s, 9 paths, 2208 anchors, 270 MB |
| Black and White Logo | photo-like | 16 | 1.27 s, 1128 paths, 45273 anchors, 129 MB |
| Black and White Logo | four-colour badge | 16 | 0.46 s, 1 paths, 69 anchors, 88 MB |
| Sketched Art | photo-like | 16 | 1.22 s, 15 paths, 11486 anchors, 128 MB |
| Outline, enhanced | photo-like | 4 | 0.24 s, 113 paths, 10815 anchors, 64 MB |
| Outline, enhanced | four-colour badge | 4 | 0.12 s, 1 paths, 42 anchors, 44 MB |
| Default | photo-like | 16 | 1.08 s, 127 paths, 34034 anchors, 199 MB |
| Default | four-colour badge | 16 | 0.51 s, 3 paths, 88 anchors, 184 MB |
| Auto Color | photo-like | 16 | 1.66 s, 5390 paths, 454670 anchors, 277 MB |
| Auto Color | four-colour badge | 16 | 0.84 s, 5 paths, 112 anchors, 184 MB |
| High Fidelity Photo | photo-like | 16 | 4.31 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo | four-colour badge | 16 | 1.10 s, 6 paths, 264 anchors, 257 MB |
| Low Fidelity Photo | photo-like | 16 | 1.98 s, 2036 paths, 182778 anchors, 754 MB |
| Low Fidelity Photo | four-colour badge | 16 | 1.09 s, 6 paths, 180 anchors, 257 MB |
| 3 Colors | photo-like | 16 | 1.50 s, 76 paths, 23024 anchors, 290 MB |
| 3 Colors | four-colour badge | 16 | 1.05 s, 4 paths, 190 anchors, 213 MB |
| 6 Colors | photo-like | 16 | 1.57 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors | four-colour badge | 16 | 1.07 s, 5 paths, 112 anchors, 184 MB |
| 16 Colors | photo-like | 16 | 1.68 s, 1973 paths, 145642 anchors, 303 MB |
| 16 Colors | four-colour badge | 16 | 1.09 s, 5 paths, 114 anchors, 188 MB |
| Shades of Gray | photo-like | 16 | 2.48 s, 686 paths, 53654 anchors, 407 MB |
| Shades of Gray | four-colour badge | 16 | 1.96 s, 9 paths, 2208 anchors, 270 MB |
| Black and White Logo | photo-like | 16 | 1.37 s, 1128 paths, 45273 anchors, 129 MB |
| Black and White Logo | four-colour badge | 16 | 0.47 s, 1 paths, 69 anchors, 88 MB |
| Sketched Art | photo-like | 16 | 1.19 s, 15 paths, 11486 anchors, 128 MB |
| Sketched Art | four-colour badge | 16 | 0.52 s, 2 paths, 361 anchors, 88 MB |
| Silhouettes | photo-like | 16 | 1.54 s, 3 paths, 3350 anchors, 144 MB |
| Silhouettes | four-colour badge | 16 | 0.53 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.53 s, 1 paths, 49 anchors, 88 MB |
| Silhouettes | photo-like | 16 | 1.45 s, 3 paths, 3350 anchors, 144 MB |
| Silhouettes | four-colour badge | 16 | 0.48 s, 1 paths, 32 anchors, 95 MB |
| Line Art | photo-like | 16 | 1.23 s, 713 paths, 37370 anchors, 129 MB |
| Line Art | four-colour badge | 16 | 0.50 s, 1 paths, 49 anchors, 88 MB |
| Technical Drawing | photo-like | 16 | 1.58 s, 4577 paths, 76559 anchors, 130 MB |
| Technical Drawing | four-colour badge | 16 | 0.56 s, 1 paths, 69 anchors, 88 MB |
| Outline | photo-like | 16 | 1.79 s, 632 paths, 34884 anchors, 256 MB |
| Outline | four-colour badge | 16 | 0.81 s, 1 paths, 49 anchors, 176 MB |
| Flat Logo | photo-like | 16 | refused after 0.18 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 16 | 0.96 s, 5 paths, 67 anchors, 146 MB |
| Default with Shapes | photo-like | 16 | 1.23 s, 127 paths, 34034 anchors, 199 MB |
| Default with Shapes | four-colour badge | 16 | 0.55 s, 3 paths, 88 anchors, 184 MB |
| 6 Colors with Shapes | photo-like | 16 | 1.59 s, 470 paths, 60823 anchors, 298 MB |
| 6 Colors with Shapes | four-colour badge | 16 | 1.21 s, 5 paths, 112 anchors, 185 MB |
| 6 Colors with Auto Grouping | photo-like | 16 | 1.82 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors with Auto Grouping | four-colour badge | 16 | 1.30 s, 5 paths, 112 anchors, 185 MB |
| Shades of Gray with Auto Grouping | photo-like | 16 | 2.24 s, 686 paths, 53654 anchors, 407 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 16 | 3.67 s, 9 paths, 2208 anchors, 263 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 16 | 9.11 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 16 | 1.15 s, 6 paths, 264 anchors, 257 MB |
| 6 Colors, no Gradients | photo-like | 16 | 1.64 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors, no Gradients | four-colour badge | 16 | 1.12 s, 5 paths, 112 anchors, 184 MB |
| 6 Colors with Gradients | photo-like | 16 | 3.23 s, 470 paths, 60831 anchors, 512 MB |
| 6 Colors with Gradients | four-colour badge | 16 | 1.66 s, 5 paths, 112 anchors, 185 MB |
| High Fidelity Photo, no Gradients | photo-like | 16 | 5.24 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo, no Gradients | four-colour badge | 16 | 2.11 s, 6 paths, 264 anchors, 257 MB |
| High Fidelity Photo with Gradients | photo-like | 16 | 6.50 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo with Gradients | four-colour badge | 16 | 2.09 s, 6 paths, 264 anchors, 263 MB |
| Default, enhanced | photo-like | 16 | 1.29 s, 127 paths, 34034 anchors, 199 MB |
| Default, enhanced | four-colour badge | 16 | 0.64 s, 3 paths, 88 anchors, 184 MB |
| Auto Color, enhanced | photo-like | 16 | 2.81 s, 5390 paths, 454626 anchors, 274 MB |
| Auto Color, enhanced | four-colour badge | 16 | 1.20 s, 5 paths, 112 anchors, 188 MB |
| High Fidelity Photo, enhanced | photo-like | 16 | 7.90 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo, enhanced | four-colour badge | 16 | 1.21 s, 6 paths, 264 anchors, 257 MB |
| Low Fidelity Photo, enhanced | photo-like | 16 | 1.94 s, 2036 paths, 182770 anchors, 754 MB |
| Low Fidelity Photo, enhanced | four-colour badge | 16 | 1.07 s, 6 paths, 149 anchors, 257 MB |
| 3 Colors, enhanced | photo-like | 16 | 1.30 s, 76 paths, 23019 anchors, 290 MB |
| 3 Colors, enhanced | four-colour badge | 16 | 0.91 s, 4 paths, 98 anchors, 205 MB |
| 6 Colors, enhanced | photo-like | 16 | 1.21 s, 470 paths, 60823 anchors, 298 MB |
| 6 Colors, enhanced | four-colour badge | 16 | 0.84 s, 5 paths, 112 anchors, 184 MB |
| 16 Colors, enhanced | photo-like | 16 | 1.22 s, 1973 paths, 145623 anchors, 293 MB |
| 16 Colors, enhanced | four-colour badge | 16 | 0.84 s, 5 paths, 114 anchors, 184 MB |
| Shades of Gray, enhanced | photo-like | 16 | 1.67 s, 686 paths, 53648 anchors, 407 MB |
| Shades of Gray, enhanced | four-colour badge | 16 | 1.35 s, 9 paths, 1989 anchors, 270 MB |
| Black and White Logo, enhanced | photo-like | 16 | 1.15 s, 1128 paths, 45272 anchors, 129 MB |
| Black and White Logo, enhanced | four-colour badge | 16 | 0.40 s, 1 paths, 69 anchors, 88 MB |
| Sketched Art, enhanced | photo-like | 16 | 1.11 s, 15 paths, 11486 anchors, 128 MB |
| Sketched Art, enhanced | four-colour badge | 16 | 0.41 s, 2 paths, 44 anchors, 88 MB |
| Silhouettes, enhanced | photo-like | 16 | 1.40 s, 3 paths, 3350 anchors, 144 MB |
| Silhouettes, enhanced | four-colour badge | 16 | 0.43 s, 1 paths, 8 anchors, 95 MB |
| Line Art, enhanced | photo-like | 16 | 1.17 s, 713 paths, 37370 anchors, 129 MB |
| Line Art, enhanced | four-colour badge | 16 | 0.42 s, 1 paths, 49 anchors, 88 MB |
| Technical Drawing, enhanced | photo-like | 16 | 1.34 s, 4577 paths, 76558 anchors, 130 MB |
| Technical Drawing, enhanced | four-colour badge | 16 | 0.39 s, 1 paths, 69 anchors, 88 MB |
| Outline, enhanced | photo-like | 16 | 1.38 s, 632 paths, 34884 anchors, 256 MB |
| Outline, enhanced | four-colour badge | 16 | 0.77 s, 1 paths, 49 anchors, 176 MB |
| Technical Drawing | four-colour badge | 16 | 0.44 s, 1 paths, 69 anchors, 88 MB |
| Outline | photo-like | 16 | 1.47 s, 632 paths, 34884 anchors, 256 MB |
| Outline | four-colour badge | 16 | 0.75 s, 1 paths, 49 anchors, 176 MB |
| Flat Logo | photo-like | 16 | refused after 0.20 s (not flat art), 40 MB |
| Flat Logo | four-colour badge | 16 | 0.80 s, 5 paths, 67 anchors, 146 MB |
| Default with Shapes | photo-like | 16 | 1.19 s, 127 paths, 34034 anchors, 199 MB |
| Default with Shapes | four-colour badge | 16 | 0.54 s, 3 paths, 88 anchors, 184 MB |
| 6 Colors with Shapes | photo-like | 16 | 1.40 s, 470 paths, 60823 anchors, 298 MB |
| 6 Colors with Shapes | four-colour badge | 16 | 1.00 s, 5 paths, 112 anchors, 185 MB |
| 6 Colors with Auto Grouping | photo-like | 16 | 1.64 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors with Auto Grouping | four-colour badge | 16 | 1.04 s, 5 paths, 112 anchors, 185 MB |
| Shades of Gray with Auto Grouping | photo-like | 16 | 2.19 s, 686 paths, 53654 anchors, 407 MB |
| Shades of Gray with Auto Grouping | four-colour badge | 16 | 1.63 s, 9 paths, 2208 anchors, 270 MB |
| High Fidelity Photo with Auto Grouping | photo-like | 16 | 9.72 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo with Auto Grouping | four-colour badge | 16 | 1.64 s, 6 paths, 264 anchors, 257 MB |
| 6 Colors, no Gradients | photo-like | 16 | 1.66 s, 470 paths, 60831 anchors, 298 MB |
| 6 Colors, no Gradients | four-colour badge | 16 | 1.13 s, 5 paths, 112 anchors, 184 MB |
| 6 Colors with Gradients | photo-like | 16 | 3.71 s, 470 paths, 60831 anchors, 512 MB |
| 6 Colors with Gradients | four-colour badge | 16 | 1.71 s, 5 paths, 112 anchors, 188 MB |
| High Fidelity Photo, no Gradients | photo-like | 16 | 4.87 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo, no Gradients | four-colour badge | 16 | 1.50 s, 6 paths, 264 anchors, 257 MB |
| High Fidelity Photo with Gradients | photo-like | 16 | 6.74 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo with Gradients | four-colour badge | 16 | 1.68 s, 6 paths, 264 anchors, 257 MB |
| Default, enhanced | photo-like | 16 | 1.18 s, 127 paths, 34034 anchors, 199 MB |
| Default, enhanced | four-colour badge | 16 | 0.57 s, 3 paths, 88 anchors, 184 MB |
| Auto Color, enhanced | photo-like | 16 | 2.31 s, 5390 paths, 454626 anchors, 283 MB |
| Auto Color, enhanced | four-colour badge | 16 | 0.98 s, 5 paths, 112 anchors, 188 MB |
| High Fidelity Photo, enhanced | photo-like | 16 | 9.09 s, 31139 paths, 1192715 anchors, 880 MB |
| High Fidelity Photo, enhanced | four-colour badge | 16 | 1.47 s, 6 paths, 264 anchors, 263 MB |
| Low Fidelity Photo, enhanced | photo-like | 16 | 2.70 s, 2036 paths, 182770 anchors, 754 MB |
| Low Fidelity Photo, enhanced | four-colour badge | 16 | 1.60 s, 6 paths, 149 anchors, 257 MB |
| 3 Colors, enhanced | photo-like | 16 | 1.82 s, 76 paths, 23019 anchors, 290 MB |
| 3 Colors, enhanced | four-colour badge | 16 | 1.24 s, 4 paths, 98 anchors, 205 MB |
| 6 Colors, enhanced | photo-like | 16 | 2.20 s, 470 paths, 60823 anchors, 298 MB |
| 6 Colors, enhanced | four-colour badge | 16 | 2.19 s, 5 paths, 112 anchors, 185 MB |
| 16 Colors, enhanced | photo-like | 16 | 2.03 s, 1973 paths, 145623 anchors, 303 MB |
| 16 Colors, enhanced | four-colour badge | 16 | 1.13 s, 5 paths, 114 anchors, 188 MB |
| Shades of Gray, enhanced | photo-like | 16 | 2.76 s, 686 paths, 53648 anchors, 407 MB |
| Shades of Gray, enhanced | four-colour badge | 16 | 3.36 s, 9 paths, 1989 anchors, 270 MB |
| Black and White Logo, enhanced | photo-like | 16 | 1.58 s, 1128 paths, 45272 anchors, 129 MB |
| Black and White Logo, enhanced | four-colour badge | 16 | 0.59 s, 1 paths, 69 anchors, 88 MB |
| Sketched Art, enhanced | photo-like | 16 | 1.52 s, 15 paths, 11486 anchors, 128 MB |
| Sketched Art, enhanced | four-colour badge | 16 | 0.54 s, 2 paths, 44 anchors, 88 MB |
| Silhouettes, enhanced | photo-like | 16 | 1.82 s, 3 paths, 3350 anchors, 144 MB |
| Silhouettes, enhanced | four-colour badge | 16 | 0.56 s, 1 paths, 8 anchors, 95 MB |
| Line Art, enhanced | photo-like | 16 | 1.43 s, 713 paths, 37370 anchors, 129 MB |
| Line Art, enhanced | four-colour badge | 16 | 0.58 s, 1 paths, 49 anchors, 88 MB |
| Technical Drawing, enhanced | photo-like | 16 | 1.76 s, 4577 paths, 76558 anchors, 130 MB |
| Technical Drawing, enhanced | four-colour badge | 16 | 0.52 s, 1 paths, 69 anchors, 88 MB |
| Outline, enhanced | photo-like | 16 | 1.64 s, 632 paths, 34884 anchors, 256 MB |
| Outline, enhanced | four-colour badge | 16 | 0.73 s, 1 paths, 49 anchors, 176 MB |
## Revision history
| date | change |
|:--|:--|
| 2026-10-14 | Gradients: a gradient that noise or a soft rim broke into pieces is found in its largest piece and grown over the rest (blurred and noisy linear gradients: 3 to 11 paths beside the gradient down to 0.5 with 6 Colors, 24 to 56 down to 0.5 to 1.5 with High Fidelity Photo) |
| 2026-10-14 | Enhanced versions of the presets (Auto Grouping, and Shapes in all but High Fidelity Photo; Flat Logo has none), each measured beside its preset; a hole that one colour fills is the same shape as what fills it, rectangles are sized by where their sides lie, and a group keeps its paths in the order they were traced in (grouped, a letter with a counter was painted with a light seam: Default with Auto Grouping, edge ΔE 10.24 to 9.88) |
| 2026-10-14 | Create: Strokes: a line whose ink is cut square at its ends gets square caps, and a wide line whose skeleton hooks towards a corner of its end ends on the line it was following (Outline: largest distance 2.94 px to 0.88; aliased lines 0.88 to 0.81) |
| 2026-10-13 | Shapes: corners that anti-aliasing or blur took a little off are corners, a rounded rectangle gets the radius that fits, a polygon its sides from the outline itself, hard-edged outlines a little more room (plain shapes found: 57 to 62% up to 98 to 100%); a "false shapes" column |