How I built a free JPEG-to-SVG converter that runs 100% in the browser

Every online image converter I tried did the same thing: upload your file to somebody's server, wait in a queue, download the result, hope your image is not kept for "analytics". I wanted a converter with no upload step at all. So I built one: jpegtosvg.com. It turns JPEG, PNG and WebP images into SVG paths entirely on your device. No account, no file uploads, no backend that ever sees your image.

Here is how it works under the hood.

The core decision: no backend

Most converters need a server because raster-to-vector tracing is CPU heavy. My decision was to skip the server completely. The whole site is a static build deployed to Cloudflare Pages with wrangler pages deploy. There is no database, no API, no conversion queue, no API secret anywhere. The build step (tsx scripts/build.ts) generates plain HTML, and that is what gets served.

This has three consequences I really like:

  1. Privacy is structural, not a promise. Your image never leaves your device, so there is nothing to leak, sell, or subpoena.
  2. Cost is near zero. Static hosting on Cloudflare Pages means I pay nothing per conversion. A server-side tracer would bill me CPU time for every image.
  3. It cannot go down under load. There is no conversion server to overload. Ten thousand people converting at once costs me the same as one.

The tradeoff is obvious: the user's device does the work. So the engineering effort went into making heavy computation feel light.

The tracing engine: ImageTracerJS in a Web Worker

The actual raster-to-vector work is done by ImageTracerJS, a JavaScript port of the classic bitmap tracing approach: quantize the colors, trace the boundaries of each color region into paths, and emit SVG. It preserves colors, not just black-and-white silhouettes.

Running that on the main thread would freeze the page for seconds on a large image. So all tracing runs inside a dedicated Web Worker (tracer-worker.js). The UI thread only handles the file picker, the preview, and progress messages. If the worker crashes or hangs, a timeout kills it and the UI reports the failure cleanly instead of locking up the tab. A shared worker instance is reused when tracing several images in a row, then terminated by the caller.

Taming large images before tracing

Tracing cost grows fast with pixel count, so the pipeline caps the work before the tracer ever sees it:

  • Input limits: 12 MB file size, 32 megapixels.
  • Before tracing, the image is drawn to a canvas scaled so the longest side is at most 1200 pixels.

That 1200-pixel cap is the single biggest performance decision. A 4000-pixel photo becomes a 1200-pixel working copy, which traces in a fraction of the time, and the resulting SVG scales back up losslessly because it is vector output. The user gets a small file that renders sharp at any size.

Format support without a server

Browsers cannot decode everything natively, so the converter bundles small decoders, all client-side:

  • HEIC/HEIF (iPhone photos) via heic2any
  • TIFF via utif2
  • PDF pages rendered with pdfjs-dist, then traced like any other raster input
  • Text extraction with tesseract.js, run on-device with the same privacy story

Each of these is lazy-loaded. The PDF renderer, for example, is only imported when the user actually drops a PDF. The initial page load stays light.

SVG cleanup: not every path deserves to survive

Raw tracer output is verbose and can contain junk, so there is a cleanup pass (vector.js, about 180 lines). It sanitizes the SVG through an allowlist of elements (svg, g, path, rect, circle, ellipse, line, polyline, polygon, text, tspan and a few more) and drops everything else. The cleanup is deliberately conservative: it is meant for static artwork and does not try to simplify geometry, because aggressive simplification destroys the details people actually wanted vectorized.

There is also a banner export tool that preserves proportions and accepts output dimensions from 32 to 4096 pixels, for the common "I need this logo as a crisp banner" case.

What I learned

Web Workers are non-negotiable for client-side compute. The difference between "the tab froze for 8 seconds" and "a progress bar moved for 8 seconds" is the difference between a tool people trust and one they close.

Downscale before you compute. Capping the working resolution was worth more than any algorithmic optimization. Vector output means the input resolution barely matters for the final quality.

Static sites can do real work. We default to reaching for a backend, but a surprising amount of "server work" is just computation that a modern phone or laptop handles fine. The backend I did not build is the backend I never have to monitor, scale, or pay for.

Privacy as architecture beats privacy as policy. "We delete your files after 24 hours" is a promise. "Your files never leave your device" is a fact. Users understand the difference.

Try it

The converter is live at jpegtosvg.com. Drop in a JPEG, PNG or WebP and watch your browser do the whole job. If you build something with heavy client-side computation, I would love to hear how you kept the UI responsive.