Vecline

MCP serverDocs & knowledge

vecline is a local-first toolkit for working with images, vectors, and documents. Once added, your AI can handle these files directly on your device, and nothing is uploaded anywhere. The toolkit also measures its own output, so you can tell how well each job turned out.

Unavailable. This server has no hosted endpoint yet, so Ahel can't serve it.

Add to setup to save this item as a reference. Ahel cannot run it, and signing in will not install it.

What your AI can do with it

  • Work with images, vectors, and documents in one place
  • Keep every file on your device, with nothing uploaded
  • Process documents and images without sending them anywhere
  • See how well each result turned out, since the toolkit measures its own output

Getting started

  1. Save this item in Your setup as a reference.
  2. Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
  3. Check this page for availability before trying to install it through Ahel.

From the project's README

As published by shunyagatha/vecline in README.md.

Measurable raster ⇄ SVG conversion — and a broad image + document toolkit. Ten raster formats, every one convertible to every other, with the accuracy of every conversion actually measured rather than asserted; plus PDF & Office (docx/xlsx/pptx) rendering and conversion, images → PDF, DXF at a real physical cut size alongside EPS and G-code for makers, centerline tracing and content-aware crop — most of it in a zero-dependency core that is CI-proven to bundle for a browser.

On flat artwork — logos, icons, UI, screenshots, pixel art — the output is bit-exact: SSIM 1.0000, PSNR ∞, zero differing pixels, and on a real logo smaller than imagetracerjs or vtracer manage while still only approximating — 24 KB against their 60 and 64. That comes from recognising the image is cheaper to encode exactly than to approximate, not from a better curve fit. Tracing now emits real curves by default — sub-pixel edge placement is on, which is the only thing that lets the curve fitter run at all; before this the default returned a staircase with zero curve commands on real artwork. It costs about 2.35x the gzipped bytes, because moving coordinates off the integer lattice costs compression, and --no-subpixel buys the old smaller output back. Pixel and exact modes are untouched and stay bit-exact. potrace is smaller than everyone and much less accurate, which is a real trade and not one this README will hide. On real photographs it leads all three on SSIM by 0.05–0.18 at their default settings; one documented vtracer flag closes part of that. It is not the fastest — imagetracerjs is quicker on every fixture measured. Every number in this README is reproducible: node scripts/fetch-corpus.mjs && npm run compare. (The corpus is fetched rather than committed — the photographs are not ours to redistribute.)

▶ Vecline Studio — vecline.xyz — the whole toolkit in your browser. Drop an image, a PDF, or a TGA/PNM/ICO no browser can read; tune it; watch live SSIM / PSNR / CIEDE2000; export SVG, DXF at a real cut size, EPS, PDF, G-code, React/Vue/Svelte/Solid components, colour separations, sprite sheets, favicons, BlurHash, or just re-encode between ten raster formats. Free, unlimited, no signup, works offline, and nothing is uploaded — every conversion runs in your tab, not on a server. (How it compares · dev playground)

vecline vectorize logo.png --verify
✓ logo.svg  400×300  pixel mode  4 shapes  2.1 KB → 7.5 KB  228 ms
  · Auto-selected pixel mode: 4 distinct colours, 1.0% run density. Output is bit-exact.

Accuracy  bit-exact (lossless)
  PSNR        ∞
  SSIM        1.000000 (luma 1.000000)
  RMSE        0.0000

The lossless guarantee

vecline vectorize anything.png --lossless

--lossless returns a bit-exact SVG or it fails. It never silently gives you a near-miss.

That is enforced by measurement, not by construction. Every candidate encoding is rendered back to pixels and compared against the source; anything that is not bit-identical is discarded, and if nothing survives, the command errors out. Candidates are tried in order of how useful the result is:

  1. Exact geometry — real, editable, infinitely scalable paths. Rectangles or contours, whichever encodes smaller.
  2. Embed, original bytes preserved — a byte-identical round trip of the source file, not just of its pixels.
  3. Embed, re-encoded as PNG — always renders exactly, but no longer carries the original file.

Measured across every fixture in the test suite — flat artwork, pixel art, soft alpha, photographs, JPEG sources, a single pixel — all of them come back PSNR ∞, maxChannelDiff 0.

Byte-identical, and provable

When the original file is preserved, the SVG records its SHA-256. extract hands the file back and checks the digest:

vecline vectorize logo.png -o keep.svg --mode embed --embed-strategy preserve
vecline extract keep.svg -o recovered.png --against logo.png
✓ recovered.png  image/png  2.1 KB
  sha256   1c4f95f66b28a40a75672614796de762f6ef8a6afec3191b460dbbd556f10366
  digest   matches the value recorded when the SVG was written
  payload  the original file, preserved byte for byte
  vs source byte-identical to logo.png

extract exits non-zero on any mismatch, so it works as a CI gate. Note the honest caveat: an embedded JPEG survives byte for byte but does not render bit-identically, because resvg's decoder rounds its inverse DCT differently. Strict --lossless detects that, discards the candidate, and says so.

Which one you get

InputResultSizeExact?
Flat artwork 400×300real geometry (contours)3.9 KB✅
Pixel art 128×128real geometry (rectangles)0.6 KB✅
favicon.ico 64×64real geometry (rectangles)0.5 KB✅
BMP 400×300real geometry (contours)3.9 KB✅
Photo 320×240embedded PNG230 KB✅

A photograph can be emitted as exact geometry, and --prefer geometry will do it — but it costs one rectangle per pixel, so that same photo becomes a 1.99 MB file of 42,933 shapes. It is vector in name only, so auto uses the bitmap and tells you the ratio it measured. Nothing is hidden.

The honest version of "100% accuracy"

Most vectorizers claim perfect accuracy. Here is what is actually true, because it determines which tool you should use:

DirectionCan it be exact?Why
SVG → rasterYes, at any resolution you nameRendering is a well-defined computation. Ask for 4000px wide and you get exactly that, correct to the renderer's rasterisation rules.
Raster → SVG, losslessYesTwo different ways, both bit-exact. See pixel and embed below.
Raster → SVG, traced into curvesNo, and it never can beA photograph holds more independent information than any compact set of Bézier curves can encode. Tracing is approximation by definition.

Anyone promising exact photo-to-curves vectorization is either embedding a bitmap and calling it vector, or is wrong. Vecline does both exact conversions properly, does the approximate one well, and always tells you which one you got and how close it landed.

Three strategies, and when each is right

ModeOutputExact?Use it for
losslessReal geometry, or an embedded bitmapBit-exact, verifiedAnything, when exactness is non-negotiable
pixelReal vector geometry (rectangles or contours)Bit-exactLogos, icons, pixel art, screenshots, diagrams, flat colour
traceReal Bézier curvesApproximate, measuredPhotos, complex art, anything you want to scale or edit
embedBitmap inside an SVG wrapperBit-exactYou need this exact image, in an SVG container

auto (the default) inspects the image and picks between pixel and trace. --lossless overrides everything with the guarantee described above.

pixel mode is the one people don't expect. It produces genuine, editable, infinitely-scalable vector paths that rasterise back to your input with zero differing pixels — not "visually identical", literally identical. For flat artwork it is usually what you actually wanted.

Install

npm install -g vecline        # the CLI
npm install vecline           # the library

Node.js 18.17+. The native dependencies (sharp, resvg) ship prebuilt binaries for Linux, macOS and Windows.

Any project architecture

You wantImportNative deps
The full Node toolkitimport { vectorize } from 'vecline'yes
… from CommonJSconst { vectorize } = require('vecline')yes
Just the vectoriserimport { trace } from 'vecline/vectorize'none
Just the metricsimport { compareImages } from 'vecline/metrics'none
Just the pure-TS codecsimport { encodeBmp } from 'vecline/formats'none
Everything portableimport { vectorizeExact } from 'vecline/core'none
Image editing (resize, rotate…)import { editImage } from 'vecline/ops'sharp only
Register a custom codecimport { registerDecoder } from 'vecline/codecs'none

Install only what you use. Every none subpath imports in isolation with the native codecs omitted (npm install vecline --omit=optional), and the package is "sideEffects": false, so a bundler drops everything you never import. vecline/core is the vectorisation and measurement engine with zero dependencies and no Node built-ins. It takes a plain { width, height, data } — byte-for-byte the layout of the browser's ImageData — so canvas pixels go straight in:

import { vectorizeExact } from 'vecline/core';

const { data, width, height } = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { svg } = vectorizeExact({ width, height, data });   // bit-exact, no codec needed

The zero-dependency claim is CI-asserted, not stated. Every commit bundles vecline/core for a browser with esbuild — no Node platform, no externals — and fails if a single node: specifier or Node global survives into the output. It comes to 96 KB minified, with a size tripwire either side so nothing large can arrive unnoticed. Reading the import graph would only prove that the imports we can see are clean; bundling proves the thing users actually do works.

Install without the optional native packages (npm install vecline --omit=optional) and core still works, at 2 MB instead of ~100 MB. Reading image files, rendering SVG back to pixels, and the verified lossless guarantee all need real codecs, so those live in the main entry point.

The BMP, ICO, PNM and TGA codecs are written from scratch in this package, so they work with the native packages omitted — no libvips required to read or write any of them. One caveat worth stating rather than discovering: an ICO entry may hold a complete PNG instead of raw pixels (most 256×256 icons do), and decoding that still needs a real PNG codec. The ICO container is parsed here either way; it is the payload inside that has to go somewhere. Entries storing a raw DIB — which is what smaller icons normally use — read with nothing installed.

Formats

ReadWrite
PNG✅✅
JPEG✅✅
WebP✅✅ lossy + lossless
AVIF✅✅ lossy + lossless
TIFF✅✅
GIF✅ first frame✅
BMP✅ all depths, RLE, BITFIELDS✅ 24/32-bit
ICO / CUR✅ largest entry✅ PNG or DIB payload
PNM (PBM/PGM/PPM)✅ P1–P6✅ P4/P5/P6
TGA✅ RLE, colour-mapped✅ RLE optional
SVG✅✅

The matrix is square: every format it reads, it writes. That is 11 × 11 = 121 conversions, and the test suite runs all of them on every commit — PNG→TGA, ICO→WebP, TGA→SVG, SVG→BMP, whatever you need. Every cell is reachable from one command:

vecline convert photo.png thumb.webp   # raster → raster
vecline convert logo.png logo.svg      # raster → vector
vecline convert logo.svg logo@2x.png   # vector → raster

convert decides from the input bytes and the output extension, not the extension alone — a file named .txt that contains <svg> is treated as an SVG, and a .svg that contains PNG bytes is not.

BMP, ICO, PNM and TGA are decoded in pure TypeScript — libvips is not built with them, and each is simple and stable enough to implement completely rather than adding another native dependency. All four are lossless formats, so the test suite asserts bit-exact decoding rather than approximate agreement.

Not built in: HEIC, JPEG XL, JPEG 2000, PSD, PDF and camera RAW. The prebuilt libvips that ships with sharp cannot read or write them — HEIC because HEVC is patent-encumbered, JXL and JP2 because they simply are not compiled in (verified: heifsave: Unsupported compression, jxlsave_buffer not found, JP2 output requires OpenJPEG). Bundling a WASM codec for each would add tens of megabytes to every install, for formats most users never touch.

But you can plug one in. Rather than ship the codecs, Vecline ships the socket. Install a WASM decoder yourself and register it, and that format then reads and writes everywhere any built-in format does:

import decodeJxl from '@jsquash/jxl/decode.js';
import { registerDecoder } from 'vecline/codecs';

registerDecoder({
  format: 'jxl',
  canDecode: (b) => b.length > 2 && b[0] === 0xff && b[1] === 0x0a, // JXL signature
  decode: async (bytes) => {
    const { data, width, height } = await decodeJxl(bytes);
    return { width, height, data: new Uint8ClampedArray(data.buffer) };
  },
});
// vecline can now vectorize, convert, and edit .jxl files.

Everyone who does not need it pays nothing. For a one-off, converting via ImageMagick or libheif first is still the simplest path.

Quick start

# Convert, letting vecline choose the strategy
vecline vectorize logo.png

# Prove the result is what it claims
vecline vectorize logo.png --verify

# Guarantee a bit-exact result whatever the input, or fail
vecline vectorize photo.jpg --lossless

# Trace to curves, escalating settings until it hits a quality target
vecline vectorize portrait.jpg --target-ssim 0.95

# SVG back to raster at any size
vecline rasterize logo.svg -o logo@4x.png --scale 4
vecline rasterize logo.svg -o hero.webp --width 2400 --lossless

# Ask what a file is and what to do with it
vecline info photo.jpg

# Measure any two images against each other — raster or SVG, in any combination
vecline verify original.png result.svg

# Whole directories
vecline batch 'assets/**/*.png' -o dist/ --to svg

Transparent output

--transparent removes a solid background and leaves real transparency behind:

vecline vectorize logo.png --transparent             # detect the background colour
vecline vectorize logo.png --transparent '#ffffff'   # or name it
vecline rasterize icon.svg -o icon.png --transparent

Two details make this safe rather than destructive:

  • It flood-fills from the edges. Removing "every white pixel" also punches holes through the whites inside a logo — the glint in an eye, the counter of an o. Only colour reachable from the border is background. --bg-everywhere opts into the blunt global version.
  • Tolerance is perceptual. A JPEG's white is never exactly #ffffff, and an RGB threshold loose enough to catch it also eats pale yellows. --bg-tolerance is an Oklab distance, so "slightly off-white" and "a genuinely different colour" stay on opposite sides of the line. --feather softens the cut.

Available in core too, so it works in a browser: removeBackground(image, { tolerance: 0.02 }).

Accuracy, measured

Every number below is produced by --verify: the generated SVG is rendered back to pixels and compared against the source. Reproduce them with npm run bench.

InputModeInOutPixels exactPSNRSSIMMean ΔE₀₀
Flat artwork, 400×300pixel2.1 KB7.5 KB100.00%∞1.00000.000
Pixel art sprite, 128×128pixel0.5 KB0.6 KB100.00%∞1.00000.000
Flat artwork, 400×300embed2.1 KB3.2 KB100.00%∞1.00000.000
Photo, 320×240embed14.5 KB19.7 KB87.94%58.04 dB0.99820.060
Photo, 320×240trace (auto and --preset photo alike)14.5 KB49.3 KB0.01%31.31 dB0.84762.931
Flat artwork, 400×300lossless → pixel2.1 KB3.9 KB100.00%∞1.00000.000
Pixel art sprite, 128×128lossless → pixel0.5 KB0.6 KB100.00%∞1.00000.000
Photo, 320×240lossless → embed14.5 KB230.5 KB100.00%∞1.00000.000

The traced photo row moved again — 49.3 KB to 93.8 KB, 31.31 to 30.82 dB, 0.8476 to 0.8116 SSIM — and it is worth being exact about what that means. Sub-pixel edge placement is now on by default, which is what lets the curve fitter run at all; without it the traced output carries zero curve commands and is a bitmap in an SVG wrapper.

This column scores the SVG against the source at the source's own size, and at 1x the pixel lattice cannot lose: it is the source, resampled. So this row gets worse whenever the output becomes more of a vector and less of a copy. That is not a hidden cost being explained away — it is the wrong question for a tracer, and the right one for the three modes above it, which are bit-exact and stay so.

Judged on the instrument that is not circular — npm run bench:scale, which scores against real vector originals the tracer never sees, at a magnification the input never contained — refinement is worth +3.01 dB across 300 real logos. If you want the input reproduced rather than recovered as a shape, pixel, embed and lossless do that exactly, and --no-subpixel returns the older lattice trace at roughly a third of the gzipped bytes.

The traced photo row moved from 52.9 KB to 49.3 KB, and every accuracy figure on it is unchanged to the digit. Closed polygons were re-stating their closing edge before the z that already draws it — z returns to the subpath's initial point by definition, so the explicit return leg only gave it a zero-length gap to cover. Removing it dropped 16.9% of all path commands across the corpus with 0 differing subpixel channels out of ~40 million, at two magnifications, with and without strokes.

Two rows deserve comment, because glossing over them is how tools mislead you:

  • Photo + embed is not 100% exact, even though the original JPEG bytes are preserved verbatim inside the SVG. The residual (max 3/255 per channel) is the SVG renderer's JPEG decoder rounding its inverse DCT differently from the reference decoder. No data was lost; two decoders simply disagree in the last bit. Vecline reports the measurement, not the claim.
  • Photo + trace is genuinely approximate. 0.01% pixels exact is not a bug — it is what tracing a photograph means. If that number matters to you, you want embed.

Compared with other vectorizers

On speed, and why this table has no Time column. scripts/compare.mjs records a per-tool ms and prints it; these tables drop it, and that omission flattered us. Measured min-of-5 cold processes, PNG in to SVG on disk, vecline is slower than imagetracerjs on every fixture — 1.10× to 1.55× — and 4.08× slower than vtracer on a small logo (280 ms against 69 ms), where ~83 ms of ours is Node loading the module graph before any work starts. On the two heavier photographs vecline is faster in wall clock than vtracer (portrait 1001 ms vs 1386, motorcycles 1160 vs 1970) while also scoring higher.

The ms column is deliberately left out of the tables rather than published misleadingly: the harness times produce(), but the work inside differs per entrant — vecline pays a full PNG encode and decode inside the timed window, imagetracerjs is handed raw pixels and pays no codec at all, and vtracer pays a process spawn. Fixing that methodology is tracked; until then the numbers above are the honest summary and npm run compare will print the raw column for anyone who wants it.

Run it yourself with npm run compare. Every tool's SVG is rendered with the same renderer and scored with the same metrics, on the same white ground, so nothing here depends on Vecline's own view of quality. The field is potrace, imagetracerjs, and — the strongest modern open-source rival — vtracer (VisionCortex, Rust). Point VECLINE_VTRACER at vtracer's released binary to score the tool its authors actually ship; failing that the harness falls back to the @neplex/vectorizer binding, and if neither is present the row is simply absent rather than guessed at.

Synthetic fixtures — reproducible without licensing anyone's photographs:

FixtureToolSizePSNRSSIMMean ΔE₀₀
Bilevelpotrace1.5 KB24.67 dB0.95110.702
imagetracerjs1.7 KB19.75 dB0.86071.667
vecline (auto)1.4 KB∞1.00000.000
Colour artpotrace posterize1.9 KB14.61 dB0.801021.302
imagetracerjs1.6 KB26.56 dB0.93630.662
vtracer1.2 KB27.88 dB0.94900.582
vecline (auto)1.8 KB∞1.00000.000
Photo (gradient + noise)potrace posterize11.5 KB13.19 dB0.602423.911
imagetracerjs11.8 KB26.85 dB0.71435.623
vtracer42.7 KB32.53 dB0.79793.049
vecline (auto / photo)19 KB31.12 dB0.77502.940
vecline lossless43.7 KB∞1.00000.000

Real photographs — the Kodak set at 480px, the test that actually matters. vecline (auto) is the zero-config default: vecline convert photo.png out.svg, which scales the palette to the content on its own.

PhotoToolSizePSNRSSIMMean ΔE₀₀
Portrait (skin, soft)imagetracerjs1621 KB25.47 dB0.70935.957
vtracer987 KB25.88 dB0.76524.743
vecline (auto)1338 KB34.80 dB0.91402.663
Lighthouse (sky)imagetracerjs2010 KB24.58 dB0.74654.900
vtracer1032 KB24.25 dB0.76245.665
vecline (auto)1743 KB36.26 dB0.94532.575
Parrots (fine detail)imagetracerjs301 KB25.81 dB0.76156.317
vtracer344 KB23.17 dB0.79497.728
vecline (auto)309 KB31.18 dB0.84603.686

Bilevel and colour art are bit-exact for vecline (SSIM 1.0000), against potrace's and imagetracerjs's approximations and, on colour art, vtracer's 0.9490 — in a smaller or comparable file.

Both of those fixtures are synthetic, and for a long time this section rested on them alone. Real flat artwork is added below because the story is genuinely different there, and worse for us in one direction.

Real flat artwork — this project's own corpus images, now part of npm run compare so these rows are produced by the same harness, renderer and metrics as every other table here.

Shortened here. Read the whole README on GitHub.

Signals

GitHub stars
17
Forks
2
Last commit
Aug 2026
Weekly downloads
148
Weekly_downloads
148 weekly_downloads
Advanced
Delivery
vecline MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
Item type
mcp-server
Key
io-github-shunyagatha-vecline
Source
github.com/shunyagatha/vecline