Image Trace: large images trace with a warning; anchor limit 8,000,000

The public documentation says Image Trace warns about images larger
than 1500 pixels on a side but traces them, only more slowly. VectorCraft
traced them silently, and refused any trace of more than 2,000,000
anchors, which a 4000 x 3000 photo at High Fidelity Photo nearly reaches
(1.9 million on a code-drawn photo) and a noisier one passes.

- imageTrace.make and makeAndExpand return `warnings`: one for an image
  over 1500 px on a side (traced, but more slowly; a lower-fidelity
  preset or a smaller copy is quicker), one for an image over 64 MP
  that was traced from a scaled-down copy. The app shows the first in
  the status bar.
- MAX_TRACE_ANCHORS is 8,000,000 natively (2,000,000 on wasm, where a
  page has at most 4 GB). Measured through imageTrace.make on
  code-drawn photos: about 90 bytes an anchor kept in the document;
  7.9 million anchors kept 685 MB with an 876 MB peak (4000 x 3000,
  Full Tone at Colors 100, Paths 100, Noise 1, 37 s).
- The refusal keeps its translated advice: raise Noise, lower Paths or
  Colors, or pick a lower-fidelity preset.

Tests: a_large_image_traces_with_a_warning (1600 x 40 and 1500 x 8
code-drawn images, make and make-and-expand),
trace_warnings_name_a_large_or_scaled_down_image, a compile-time check
of the limit's range, and a UI test that the status bar shows the note.

Source:
https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
This commit is contained in:
andeecollard
2026-10-17 19:38:18 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent a2e2eba2e2
commit 17a602d1dc
9 changed files with 157 additions and 14 deletions
+58 -5
View File
@@ -129,7 +129,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Make",
["Object", "Image Trace"],
None,
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), enhanced?: false (the preset's enhanced version: with Auto Grouping, and with Shapes in all but High Fidelity Photo; Flat Logo has none and is traced as it is), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors}",
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), enhanced?: false (the preset's enhanced version: with Auto Grouping, and with Shapes in all but High Fidelity Photo; Flat Logo has none and is traced as it is), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors, warnings: [text…] (the image is over 1500 px on a side, so it traces more slowly; or it was traced scaled down)}",
has_selection_or_ids,
|s, p| trace_make(s, p, false)
),
@@ -859,9 +859,16 @@ fn trace_library(s: &Session, params: &mut tr::TraceParams) -> Result<Vec<([u8;
Ok(library)
}
/// The most anchor points an Image Trace may make: a photo traced at high fidelity can make
/// millions, more than the canvas draws smoothly or a laptop has memory for, so the trace is
/// refused with advice instead (#525).
/// The most anchor points an Image Trace may make, a guard for memory: a trace past it is refused
/// with advice instead (#525). A trace keeps about 90 bytes an anchor in the document (measured on
/// code-drawn photos of 2000 × 2000 to 6000 × 4000 pixels), so 8,000,000 anchors are about
/// 0.7 GB, with a peak of about 1 GB while tracing: a 4000 × 3000 photo at High Fidelity Photo
/// makes about 1.9 million, and even at Paths 100, Noise 1 and every tone about 7.9 million. On the
/// web, where a page has at most 4 GB of memory, the limit stays at 2,000,000.
#[cfg(not(target_arch = "wasm32"))]
pub const MAX_TRACE_ANCHORS: usize = 8_000_000;
/// The most anchor points an Image Trace may make on the web (see the native limit).
#[cfg(target_arch = "wasm32")]
pub const MAX_TRACE_ANCHORS: usize = 2_000_000;
/// The live shape for a shape the tracer found (in the image's pixels), placed in the document by `xf`.
@@ -975,6 +982,7 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
let raster = tr::Raster::decode_to_fit(&blob.bytes).map_err(|e| EngineError::Other(e.to_string()))?;
let library = trace_library(s, &mut params)?;
let res = tr::trace_within(&raster, &params, MAX_TRACE_ANCHORS).map_err(|e| EngineError::Other(e.to_string()))?;
let warnings = trace_warnings((img.width, img.height), (raster.width, raster.height));
// Pixel space of the decoded raster → the image object's pixel space → document.
let sx = img.width.max(1) as f64 / raster.width.max(1) as f64;
let sy = img.height.max(1) as f64 / raster.height.max(1) as f64;
@@ -1044,7 +1052,34 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
sel.set([gid]);
Ok(gid)
})?;
Ok(json!({ "id": id.0, "paths": npaths, "anchors": anchors, "colors": colors }))
Ok(json!({ "id": id.0, "paths": npaths, "anchors": anchors, "colors": colors, "warnings": warnings }))
}
/// Images larger than this on a side are traced, but more slowly: the trace says so.
pub const LARGE_TRACE_SIDE: u32 = 1500;
/// What a trace of an image of `size` pixels, traced at `traced` pixels, tells the user: that a
/// large image takes longer (the public documentation's warning for images over 1500 px), and that
/// one over the tracer's [`tr::MAX_PIXELS`] was traced scaled down.
pub(crate) fn trace_warnings(size: (u32, u32), traced: (u32, u32)) -> Vec<String> {
let mut out = vec![];
if size.0.max(size.1) > LARGE_TRACE_SIDE {
out.push(format!(
"The image is {} × {} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster.",
size.0, size.1
));
}
if u64::from(traced.0) * u64::from(traced.1) < u64::from(size.0) * u64::from(size.1) {
out.push(format!(
"The image was traced at {} × {} pixels, scaled down from {} × {} ({} megapixels at most).",
traced.0,
traced.1,
size.0,
size.1,
tr::MAX_PIXELS >> 20
));
}
out
}
fn trace_targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
@@ -1113,3 +1148,21 @@ fn trace_expand(s: &mut Session, p: &Value) -> Result<Value> {
})?;
Ok(json!({ "ids": ids_json(&targets) }))
}
#[cfg(test)]
mod trace_limit_tests {
use super::{MAX_TRACE_ANCHORS, trace_warnings};
#[test]
fn trace_warnings_name_a_large_or_scaled_down_image() {
assert!(trace_warnings((1500, 1500), (1500, 1500)).is_empty());
assert_eq!(trace_warnings((1501, 10), (1501, 10)).len(), 1);
let w = trace_warnings((16000, 12000), (9459, 7094));
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].contains("16000 × 12000") && w[1].contains("9459 × 7094") && w[1].contains("64 megapixels"), "{w:?}");
}
// A 4000 × 3000 photo at High Fidelity Photo makes about 1.9 million anchors, which must fit;
// the limit still guards memory (about 90 bytes an anchor). Checked when the tests build.
const _: () = assert!(MAX_TRACE_ANCHORS >= 2_000_000 && MAX_TRACE_ANCHORS <= 16_000_000);
}
+28
View File
@@ -908,6 +908,34 @@ fn traced_paths(n: &Node) -> Vec<Arc<Node>> {
n.children().unwrap().iter().filter(|c| !matches!(c.kind, NodeKind::Image(_))).cloned().collect()
}
#[test]
fn a_large_image_traces_with_a_warning() {
// The public documentation: images over 1500 px on a side are traced, only more slowly.
let mut s = session();
let art = vectorcraft_trace::Raster::from_fn(
1600,
40,
|x, y| if (100..1500).contains(&x) && (10..30).contains(&y) { [0, 0, 0, 255] } else { [255; 4] },
);
add_image(&mut s, &art, Affine::IDENTITY);
let r = s.execute("imageTrace.make", &json!({"params": {"ignoreWhite": true}})).unwrap();
assert_eq!(r["paths"], 1);
let w = r["warnings"].as_array().unwrap();
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].as_str().unwrap().contains("1600 × 40") && w[0].as_str().unwrap().contains("1500"), "{w:?}");
// Make and Expand says so too.
let r = s.execute("imageTrace.makeAndExpand", &json!({"id": r["id"], "params": {"ignoreWhite": true}})).unwrap();
assert_eq!(r["warnings"].as_array().map(Vec::len), Some(1));
// At 1500 px and under, no warning.
let mut s = session();
image_doc(&mut s);
let r = s.execute("imageTrace.make", &json!({})).unwrap();
assert_eq!(r["warnings"], json!([]));
let edge = vectorcraft_trace::Raster::from_fn(1500, 8, |x, _| if x < 700 { [0, 0, 0, 255] } else { [255; 4] });
add_image(&mut s, &edge, Affine::IDENTITY);
assert_eq!(s.execute("imageTrace.make", &json!({})).unwrap()["warnings"], json!([]));
}
#[test]
fn image_trace_make_places_paths_over_the_image() {
let mut s = session();
+3 -3
View File
@@ -33,7 +33,7 @@ use measure::{Metrics, measure};
use vectorcraft_trace::{Raster, TraceParams, TraceResult, TracedPath, presets, trace_within};
/// The engine's limit (`MAX_TRACE_ANCHORS`): a trace past it is refused.
const MAX_ANCHORS: usize = 2_000_000;
const MAX_ANCHORS: usize = 8_000_000;
struct Counting;
static LIVE: AtomicUsize = AtomicUsize::new(0);
@@ -243,7 +243,7 @@ fn cost(img: &Raster, params: &TraceParams) -> String {
Ok(res) => format!("{secs:.2} s, {} paths, {} anchors, {mb:.0} MB", res.paths.len(), res.anchor_count()),
Err(e) => {
let why = e.to_string();
let why = if why.contains("anchor points") { "over 2,000,000 anchors" } else { "not flat art" };
let why = if why.contains("anchor points") { "over 8,000,000 anchors" } else { "not flat art" };
format!("refused after {secs:.2} s ({why}), {mb:.0} MB")
}
}
@@ -341,7 +341,7 @@ fn report() -> String {
.map(|mp| (mp.sqrt() * 1024.0).round() as u32)
.collect();
let (photo, badge) = (inputs.iter().find(|i| i.name == "photo-like").unwrap(), inputs.iter().find(|i| i.name == "four-colour badge").unwrap());
md += "\n## 5. Cost\n\nOne trace at a time on this machine (all cores); memory is the peak allocated above the image itself. A trace of more than 2,000,000 anchors is refused, as in the app.\n\n| preset | image | megapixels | result |\n|:--|:--|--:|:--|\n";
md += "\n## 5. Cost\n\nOne trace at a time on this machine (all cores); memory is the peak allocated above the image itself. A trace of more than 8,000,000 anchors is refused, as in the app.\n\n| preset | image | megapixels | result |\n|:--|:--|--:|:--|\n";
for size in sizes {
let n = size as usize;
let images = [("photo-like", photo.draw(size, Variant::Clean)), ("four-colour badge", raster::render(&badge.scaled(size), badge.bg, n, n))];
+6
View File
@@ -751,6 +751,12 @@ impl VectorcraftApp {
match &r {
Err(e) => self.ui.status = e.clone(),
Ok(v) => {
// Image Trace's notes (a large image traces more slowly, or was scaled down).
if id.starts_with("imageTrace.make")
&& let Some(w) = v["warnings"].as_array().and_then(|w| w.first()).and_then(Value::as_str)
{
self.ui.status = w.to_string();
}
if id == "file.new" {
self.ui.status.clear();
// New Document's Pixel preview mode (Overprint Preview is the engine's).
+14
View File
@@ -519,6 +519,20 @@ mod tests {
app
}
#[test]
fn a_large_image_traces_and_the_status_bar_says_it_is_slower() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let img = image::RgbaImage::from_fn(1601, 12, |x, _| image::Rgba(if x < 800 { [0, 0, 0, 255] } else { [255, 255, 255, 255] }));
let mut png = vec![];
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let data = vectorcraft_format::base64_encode(&png);
app.run("file.place", json!({"name": "wide.png", "dataBase64": data, "link": false})).unwrap();
trace_with(&mut app, "Default");
assert!(target(&app).2.is_some(), "the image is traced");
assert!(app.ui.status.contains("1601 × 12") && app.ui.status.contains("more slowly"), "{}", app.ui.status);
}
#[test]
fn sketch_quick_trace_reveals_all_adjustments_and_keeps_the_source() {
let mut app = traced_app();
+9
View File
@@ -26,6 +26,15 @@ so Undo also restores the panel values before its next frame. View can show the
Use **Expand** when finished to turn the result into ordinary editable vector objects;
after expanding, the tracing settings are no longer available. **Release** restores the source.
**Large images.** An image of more than 1500 pixels on a side is traced as any other, only more
slowly, and the trace says so: `imageTrace.make` returns the note in `warnings` and the status bar
shows it (the [results optimization](https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html)
page describes the same warning for images over 1500 px). An image of more than 64 megapixels (up to
192) is traced from a copy scaled down to 64 megapixels, and the trace says that too. A trace that would
make more than 8,000,000 anchor points (2,000,000 on the web) is refused to protect memory, with advice:
raise Noise, lower Paths or Colors, or pick a lower-fidelity preset. A trace keeps
about 90 bytes an anchor; a 4000 × 3000 photo at High Fidelity Photo makes about 1.9 million.
These controls use VectorCraft's existing tracing algorithm. Parameter effects follow its
implementation; they do not claim pixel-identical results with another application.
The terminology and general control behavior are described in the
+32 -3
View File
@@ -1,5 +1,6 @@
# Image Trace quality
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-17 · **Change:** minor (Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; earlier: Gradients: grown over noise and soft rims)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-17 · **Change:** minor (Image Trace: Raster::new returns an error for a buffer of the wrong length; earlier: Gradients: grown over noise and soft rims)
> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (Create Strokes: maximal-disc pruning and junction merging; centre-lines rerun)
> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (Create Strokes: exact crop distances and Guo–Hall thinning; centre-lines rerun)
@@ -14,8 +15,37 @@ compared with the numbers before each change. Nothing here is a judgement by eye
with any other product except the open tracer named under [Yardstick](#4-yardstick).
## Collapsed loops keep few segments
## Large images
Images over 1500 px on a side are traced with a warning that they trace more slowly (see
[image-trace-adjustments.md](image-trace-adjustments.md)), and the anchor limit that guards memory is
8,000,000 (2,000,000 on the web) instead of 2,000,000. Measured through `imageTrace.make` (release
build, 4 cores, one trace at a time; memory is the peak allocated above the placed image, and what the
document keeps after the trace) on code-drawn photo-like pictures (40 soft-edged shaded discs on a
gradient, texture, sensor noise):
| size | preset | time | anchors | paths | peak | kept |
|:--|:--|--:|--:|--:|--:|--:|
| 2000 × 2000 | High Fidelity Photo | 4.1 s | 737,478 | 16,064 | 152 MB | 52 MB |
| 2000 × 2000 | Low Fidelity Photo | 1.6 s | 114,008 | 1,486 | 142 MB | 7 MB |
| 2000 × 2000 | Default | 0.9 s | 103,084 | 1,001 | 117 MB | 6 MB |
| 3000 × 2000 | High Fidelity Photo | 6.4 s | 1,013,114 | 25,256 | 236 MB | 75 MB |
| 3000 × 2000 | Default | 1.2 s | 145,756 | 1,574 | 162 MB | 9 MB |
| 4000 × 3000 | High Fidelity Photo | 12.8 s | 1,914,018 | 58,638 | 471 MB | 150 MB |
| 4000 × 3000 | Low Fidelity Photo | 5.1 s | 258,826 | 4,833 | 428 MB | 18 MB |
| 4000 × 3000 | Default | 2.4 s | 273,392 | 3,568 | 317 MB | 17 MB |
| 4000 × 3000 | 16 Colors | 3.0 s | 103,222 | 2,434 | 364 MB | 7 MB |
| 4000 × 3000 | Full Tone, Colors 85, Paths 90, Noise 4 | 29.6 s | 5,230,847 | 164,687 | 684 MB | 412 MB |
| 4000 × 3000 | Full Tone, Colors 100, Paths 100, Noise 1 | 37.2 s | 7,885,709 | 338,561 | 876 MB | 685 MB |
| 6000 × 4000 | Full Tone, Colors 85, Paths 90, Noise 4 | 106.1 s | 10,445,409 | 398,772 | 1169 MB | 872 MB |
| 6000 × 4000 | Full Tone, Colors 100, Paths 100, Noise 1 | 116.3 s | 13,508,871 | 658,133 | 1456 MB | 1230 MB |
(The last two rows were measured without a limit; they are refused now.) The 4000 × 3000 High Fidelity
Photo trace was 86,000 anchors under the old limit; a noisier scan of the same size would have been
refused. At 8,000,000 anchors a trace keeps about 0.7 GB, with a peak of about 0.9 GB while tracing.
## Collapsed loops keep few segments
A small closed loop (or a piece that returns to its own junction) whose fit collapsed to a single
cubic cannot stay as one, so it kept every positioned point as a straight segment. A looser Paths
collapses more loops, so it could give more segments than a tighter one. Such a loop now tries the
@@ -24,7 +54,6 @@ stays within 1.5 × max(tolerance, 0.5 px) of the points both ways: the fit at u
tighter tolerances (what a slightly higher Paths gives); for a closed loop then two cubics split at
its start and its farthest point, then the outline simplified at the tolerance and finer; the
positioned points last, as before.
Test on code-drawn images: `a_higher_paths_never_gives_fewer_segments` (32 seeded 48 px images of
soft blobs in four colours plus pixel noise, Color and Black and White, Noise 1 and 3, segments at
Paths 0, 25, 50, 75 and 100 must not fall). Before, six runs fell, e.g. 125 → 79 segments from
@@ -32,7 +61,6 @@ Paths 0 to 25 (seed 21, Color, Noise 1) and 71 → 63 → 52 from Paths 0 to 50
White, Noise 3); now they are 75 → 79 and 41 → 48 → 52. One image (seed 16 in Black and White) still
falls from 140 to 136 segments between Paths 0 and 25, before and after this change; that fall has
another cause, still open, and the image is left out of the test. The crate's other tests pass unchanged.
## Notches read only their own piece
Where the pixel at a sharp corner falls to the other colour, the outline cuts the corner in two
one-pixel steps, and between two straight edges of three cracks or more those two steps become one
@@ -1212,6 +1240,7 @@ The remaining spur/extra-junction target is not met by this graph-only change.
| date | change |
|:--|:--|
| 2026-10-17 | Images over 1500 px on a side trace with a warning; the anchor limit is 8,000,000 (2,000,000 on the web); time and memory of 2000 × 2000 to 6000 × 4000 code-drawn photos |
| 2026-10-17 | `Raster::new` returns an error for RGBA bytes of the wrong length (it failed an assertion) and `Raster::pixel` reads nothing past the raster; traces of valid images unchanged |
| 2026-10-17 | M12: maximal-disc graph pruning, split-crossing merges and degree-two joins; correct junction degrees 30→44/45 aa and hard, none missed; remaining spurs unchanged. Centre-lines rerun, scores and earlier tables unchanged. |
| 2026-10-17 | M12: exact per-blob Euclidean distances and Guo–Hall thinning; centre-lines rerun on 46 inputs. Ends found: aa 638→682/688, hard 631→681/688. Whole-blob stroke/fill choice retained; preset/cost tables unchanged. |
+3 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long