SVG Me
Developer guide

How to convert an image to SVG in Node.js

Image libraries like sharp read and write pixels, but none of them trace pixels into vector shapes. For that you need a tracer. This guide covers the potrace package, which runs locally, and the SVG Me API for full-color results. Every sample was run as written.

Which one to use

  • `potrace` on npm (GPL-2.0): free and local. Traces one color: logos in black, line art, laser designs, stamps. It can also posterize an image into several shades of one color.
  • The SVG Me API: full-color tracing to a few flat colors, cleanup of noisy JPGs, and outputs for cutters, lasers and print. No native dependencies. 1 credit per image, from 10¢ in bulk.

Option 1: the potrace package

The potrace package is a JavaScript port of Potrace that reads PNG, JPEG, BMP and other images through Jimp, so it needs no system libraries:

npm install potrace

Its functions take callbacks; promisify turns them into promises:

import { writeFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import potrace from 'potrace';

const trace = promisify(potrace.trace);

const svg = await trace('mandala.png', {
  threshold: 128,     // pixels darker than this become the shape
  turdSize: 2,        // ignore specks up to 2 px
  optTolerance: 0.2,  // how far curves may be simplified
  color: '#000000',
  background: 'transparent',
});
await writeFile('mandala.svg', svg);
Save as trace.mjs and run node trace.mjs. Writes the traced shapes to mandala.svg.
  • threshold decides which pixels count as the shape. Raise it to pick up lighter strokes, lower it to drop faint ones.
  • turdSize removes specks of up to that many pixels. Raise it for scanned or compressed images.
  • optTolerance lets curves be simplified further. Higher values give fewer points and slightly looser fits.
  • color and background set the SVG’s fill and background, so you get a transparent SVG without editing it afterwards.

Transparent PNGs work as they are: transparent areas are treated as background.

Posterize: several tones, one color

posterize traces the image several times at different thresholds and stacks the results as shades of one color:

import { writeFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import potrace from 'potrace';

const posterize = promisify(potrace.posterize);

// Four shades of one color, stacked: a toned look, not the original colors.
const svg = await posterize('logo.png', { steps: 4, color: '#1d3557', background: 'transparent' });
await writeFile('logo-posterized.svg', svg);

That gives a stylized, screen-printed look. It does not keep the original colors: an orange-and-green logo comes out as four shades of blue. For the image’s own colors, use a color tracer, such as VTracer from Python (see the Python guide) or the API below.

Option 2: the SVG Me API

In Node.js 20 or later, fetch, FormData and openAsBlob are built in, so no packages are needed:

import { openAsBlob } from 'node:fs';
import { writeFile } from 'node:fs/promises';

const form = new FormData();
form.append('image', await openAsBlob('logo.png'), 'logo.png');
form.append('colors', '4');

const res = await fetch('https://svgme.app/api/v1/convert', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.SVGME_API_KEY}` },
  body: form,
});
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).message}`);
await writeFile('logo.svg', await res.text());
console.log('Credits left:', res.headers.get('x-credits-remaining'));
Save as convert.mjs, set SVGME_API_KEY, and run node convert.mjs.

Add options as more form fields: format (layers for cutting machines, outline for lasers, sticker), remove_background, palette for exact brand colors, or file_type for PDF, EPS or DXF. The API guide lists them all.

  • Keep the key on the server. Call the API from your backend or a script, never from code that runs in the browser, where anyone could copy the key.
  • Serverless functions with short timeouts: a sync conversion usually takes a few seconds but can take longer for large images. Queue a job with POST /api/v1/jobs and poll it instead.

Converting a folder

import { mkdir, readdir, writeFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import potrace from 'potrace';

const trace = promisify(potrace.trace);

await mkdir('svg', { recursive: true });
for (const file of await readdir('images')) {
  if (!/\.(png|jpe?g)$/i.test(file)) continue;
  const svg = await trace(`images/${file}`, { threshold: 128, turdSize: 2 });
  await writeFile(`svg/${file.replace(/\.\w+$/, '')}.svg`, svg);
  console.log(`${file} -> svg/${file.replace(/\.\w+$/, '')}.svg`);
}
Traces every PNG and JPG in images/ into svg/ with potrace.

For the API, the API guide’s folder script converts two images at a time (the per-account limit for sync requests) and retries rate limits. See also batch converting images to SVG.

Questions

Can sharp convert PNG to SVG?

No. sharp can read an SVG and render it to PNG, but it can’t trace a PNG into vector shapes. Use a tracer such as potrace, then sharp for anything else you need to do with the pixels.

Why is my potrace SVG empty or solid black?

The threshold is splitting the image in the wrong place. If the design is light on a dark background, lower the threshold or invert the image first; if it is faint, raise it.

Can I use the potrace package in a commercial app?

It is GPL-2.0, which has conditions when you distribute software that includes it. Running it on your own server is treated differently from shipping it. Check with whoever handles licensing for your project.

Does the API work from Deno or Bun?

Yes. It is a plain HTTPS endpoint that takes multipart form data, so any runtime with fetch and FormData can call it the same way.