# Inside a browser-side SVG tracing pipeline

Tracing an image into SVG involves several different jobs: decoding a file, grouping its colors, finding shapes, fitting paths, and presenting a result someone can inspect. WebAssembly handles the tracing computation in this implementation, while ordinary browser APIs handle the surrounding workflow.

The example here comes from JPG to SVG. Its conversion engine uses VisionCortex’s [VTracer](https://github.com/visioncortex/vtracer), packaged as a local WebAssembly asset and a module worker. The interesting implementation details are the boundaries between those pieces: when memory is allocated, which buffer is transferred, and what happens when a job ends.

## Validate dimensions before decoding

Compressed file size tells only part of the memory story. A 2,000 × 2,000 image needs 16 million bytes for a single RGBA pixel buffer, before canvases, decoded images, and tracing data are considered.

The loader reads the file bytes and inspects the image signature and dimensions before calling the browser decoder. It accepts supported JPEG, PNG, and still WebP inputs within three separate limits: 20,000,000 file bytes, 4,000,000 pixels, and 8,192 pixels along either edge.

After decoding, dimensions are checked again. The image is drawn into a canvas, and `getImageData()` supplies the RGBA array. The loader also records whether any pixels are transparent or partially transparent. Those flags later help the interface explain how to inspect soft edges.

This stage still involves main-thread canvas work. Moving tracing into a worker does not make every part of image preparation asynchronous.

## Make buffer ownership explicit

The usual tracing path creates a worker for each call; a separate one-pixel fast path can return a rectangle directly. Here is the worker message handoff from the existing engine function, with surrounding validation and error handlers omitted:

```ts
worker = new Worker('/vendor/vtracer/worker.js', {
  type: 'module',
  name: 'local-vtracer'
});

worker.postMessage({
  type: 'load',
  width: work.width,
  height: work.height,
  pixels: work.pixels.buffer
}, [work.pixels.buffer]);

worker.postMessage({
  type: 'trace',
  job,
  options: mapSettings(effective)
});
```

The second argument to the first `postMessage()` transfers ownership of the working buffer. The sending side must treat that buffer as detached afterward.

The engine prepares a separate working pixel array before this handoff, so it can retain the original image for another attempt. This is not a claim that the entire pipeline avoids copying: the JavaScript-to-WASM binding also copies pixels into WebAssembly memory. Transferable buffers address one specific boundary.

## Let the worker report actual work

The worker loads its sibling WASM binary from the same origin and constructs the tracer from the supplied dimensions and pixels. It then starts color clustering and path generation.

During tracing, a loop calls the engine’s `tick()` method and gathers the returned SVG markup. It aims to batch this work within roughly 25-millisecond intervals, yielding through a `MessageChannel` between batches. An individual tick can take longer, so that interval is a scheduling target, not a timing guarantee.

The worker sends separate messages for clustering progress, emitted shapes, completion, and errors. The interface can therefore draw paths that actually exist. Loading has an indeterminate state; clustering and tracing each report their own progress. Combining them into a single smooth percentage would require a weighting model the implementation does not have.

## Keep previews separate from final output

For parameter previews, the engine normally reduces the longest image edge to 480 pixels. Formal conversions use the full accepted input dimensions. Very narrow images are an exception to preview resizing, because rounding their short side up to one pixel could distort their proportions.

The two controls map to tracing parameters. Color precision affects retained color information; a value of six does not mean “produce six colors.” Smoothing selects pixel paths at zero and spline paths above zero, with a corresponding simplification tolerance.

A reduced preview helps choose settings, but the full result deserves its own inspection, especially around narrow gaps, small lettering, and partially transparent edges.

## Finish with validation and cleanup

For worker-based traces, the final SVG is parsed before becoming the accepted result. This implementation requires at least one path, permits only `svg`, `g`, and `path` elements, and rejects event-handler attributes and resource references. It also sets dimensions and the `viewBox` from the traced image. That is an output check for this known producer, not a general-purpose sanitizer for arbitrary SVG uploads.

For these jobs, completion, cancellation, and the default 60-second timeout all terminate the worker. When the source image is no longer needed, its object URL is revoked and its retained pixels are released. Giving each job a clear lifetime makes cancellation meaningful and avoids leaving obsolete tracing work running.

You can explore the resulting workflow in [JPG to SVG](https://jpgtosvg.io/). Image pixels stay in the browser during conversion; the engine asset itself is downloaded like other site resources. When investigating a slow job or an unexpected result, follow those pixels through decoding, canvas readback, worker transfer, WASM memory, and the final SVG. Each boundary has a different job to do.

*Disclosure: I’m affiliated with JPG to SVG, the project discussed here.*
