Releases: shunyagatha/Vecline
Release list
v1.40.1 — the size-budget solver no longer overshoots
Found while size-budgeting a real logo, then reproduced on the corpus. Ask for a 120 KB SVG of a photograph and it returned 72 KB — and every budget from 80 KB down returned that same 72 KB, unable to get smaller. A budget that pays 0.11 SSIM to land at 72 KB when 120 KB was asked is spending accuracy the target never demanded.
Two causes, both fixed.
The relaxation function had a cliff. The structural levers (drop specks, trim palette) are discontinuous — on a photograph, minArea reaching ~20 took the byte count from 319 KB straight to 62 KB with nothing in between, so any target landing in that gap was unreachable and the bisection settled on the far side and called it a fit. Coordinate precision is now a lever, running after fit tolerance and before any structural cut: rounding path coordinates shrinks bytes smoothly (~15–25% per dropped decimal) while deleting no structure, which fills the gap.
The search wasted its steps. It expanded through a fixed ladder and only then bisected. Now that the curve is monotone and cliff-free it is a plain bisection on [0, 1], and the step budget went 4 → 8 (each step only measures bytes; only the baseline and the settled result render, so it stays cheap).
Measured on parrots:
| target | before | after |
|---|---|---|
| 200 KB | 127 KB (−40%) | 173 KB (−14%) |
| 120 KB | 72 KB (−40%) | 114 KB (−5%) |
| 80 KB | 72 KB | 80 KB (0%) |
| 40 KB | 72 KB | honestly reports the ~46 KB lever floor as unreached |
Before, three different targets returned one wrong number. After, each lands close or says plainly it cannot be reached.
523 tests · smoke clean · corpus problems: 0
v1.40.0 — sector primitives, and a despeckle that adapts to the image
Two fixes, both surfaced by a real logo and a real chart rather than by a synthetic test.
Sectors — pie slices and donut segments — as real arcs
The one primitive that cannot be recognised from an outline alone, and the shape every chart is made of. --primitives now turns a four-slice pie chart from 2323 bytes of Bézier into 488 bytes of four true SVG arcs — editable as arcs in Illustrator, and readable as arcs by a CAD tool.
It came out of a five-way bakeoff: five genuinely different algorithms, each in its own git worktree, each held to the same fixed bar — fire on a real traced pie and ring, stay silent on photographs, produce a smaller file, keep the suite green — then independently reproduced in a fresh worktree before any was believed.
Three of the five, and three earlier hand attempts, failed for one reason worth stating: pixel-lattice contour error is bounded and adversarial, not Gaussian. A traced boundary is off by at most half a pixel, and the sign is correlated along each staircase tread — so every estimator whose optimality rests on zero-mean independent noise is solving a problem the data does not pose. The winner minimises the worst boundary distance, not the sum of squares, and judges each region independently rather than per colour — which is what makes it fire on a real chart whose slice colours recur in a legend, the exact case that lets a fitter pass every synthetic test and never work in production.
Measured: centres within 0.1px and radii within 0.15px of ground truth, rendered back to a correct pie. Zero false arcs on photographs at five seeds, or on a disc, annulus, square, triangle, crescent, blob or elliptical wedge. Below ~0.6 rad sweep or ~15px radius it declines to a Bézier path — fails safe. Opt-in and off by default.
Honesty fix in the same change: the curved primitives are documented as a measured trade (~0.02 SSIM against the source), not "render-preserving". An ideal arc cannot follow a pixel staircase exactly. Only the <rect> is genuinely render-identical; the shipped <circle> always made this trade, and the docs now admit it.
Despeckle that adapts to the image
A real logo exposed this: antialiased artwork fragments along every edge into hundreds of one-to-forty-pixel slivers — 99% of components under 40px on a 405×384 logo — and the old fixed threshold (min(16, pixels/50000) = 3 for that image) removed almost none of them, so the trace was either enormous or, once budgeted, visibly wavy.
The threshold is now chosen by area, not count: components are removed only while everything removed still accounts for a negligible share of the picture. That separates the cases by itself — the logo's threshold climbs to 125 and clears 1431 slivers; a photograph's drops to 5 (from 8), more conservative, which is what protects fine detail. Logo SSIM 0.937 → 0.989, corpus problems: 0, benchmark unmoved.
523 tests · smoke clean · corpus problems: 0
v1.39.1 — docs catch up with what shipped
Docs only; no code change.
The README's headline and Studio description were written several releases ago and had quietly fallen behind:
- The headline described DXF/EPS/G-code without mentioning that DXF now declares a real physical cut size — the part a maker cares about — and made no accuracy claim at all. It now leads with the bit-exact result on flat art and the SSIM lead over potrace, imagetracerjs and vtracer, alongside the one command that reproduces both:
npm run compare. - The Studio was described as "drop an image, export SVG/PNG/DXF/EPS/PDF/G-code". It also takes PDFs and the three formats no browser decodes, and exports framework components, colour separations, sprite sheets, favicons, placeholders and ten raster formats. All of that shipped; none of it was mentioned.
- The bundle proof was documented nowhere.
vecline/coreis CI-asserted to bundle for a browser at 85 KB with zeronode:specifiers. Reading the import graph only proves the imports you can see are clean; bundling proves the thing users actually do works.
The npm description now leads with bit-exactness rather than a feature list, because that is the claim that distinguishes this package and the one a reader can check.
v1.39.0 — extend-under: one boundary instead of two
Regions are painted largest-first, so by the time a small shape is drawn the large one beneath it is already down. A region's fill therefore does not have to stop at a shared edge — it can run on underneath, and the render is unchanged, because the later region repaints those pixels.
vecline vectorize chart.png --extend-underA boundary shared by two regions is normally traced twice, once as each one's edge. Here it is traced once. And the hairline seam antialiasing leaves between two abutting shapes cannot appear, because there is no longer a join for it to open at — which is exactly what --stroke-width exists to paint over.
Off by default, and the reason is measured
| Fixture | Size change | Render |
|---|---|---|
| Flat artwork | −28.3% | identical |
| Small logo | +100.6% | identical |
| Parrots (photo) | +125.8% | identical |
| Lighthouse (photo) | +128.9% | identical |
Bit-identical every time — vecline verify a.svg b.svg reports PSNR ∞, SSIM 1.000000 — so this is purely a size and seam trade. It wins clearly on flat artwork with few large colour fields, and loses badly when the later colours are scattered, because the union it traces is then more complex than the region it replaced. The README carries the table rather than a recommendation, because the right answer depends on the image.
Render-identity is the whole justification, so that is what the tests assert: both outputs rendered and compared, on flat art (bit-exact) and on a photograph, and separately scored against the source so two identically-wrong renders cannot pass. Transparency is covered too — a union that swallowed void pixels would fill transparent areas with the colour beneath, and no repaint undoes that.
513 tests · smoke clean · corpus problems: 0
v1.38.1 — package metadata catches up
Metadata only; no code change.
The READMEs were kept current with every release. The description strings were not — they were last written before v1.37.0 and never mentioned vecline serve at all, so the local Office bridge was invisible to anyone reading the npm listing rather than the repository.
package.json now names cut-ready DXF and the local bridge, and gains keywords for the audiences those serve. The registry and MCPB descriptions are capped near 100 characters and remain true at that level of abstraction, so they stand unchanged.
v1.38.0 — cut-ready DXF units, and a core that proves it bundles
Two plan items, both about turning a claim into something checkable.
A part, not a drawing
Every DXF this exporter has ever written said nothing about units — silently. When a file omits $INSUNITS, LightBurn, LibreCAD and Fusion each apply a different default, so the same DXF cuts at three different sizes depending on what opened it. The maker finds out after the cut, in material.
vecline convert logo.png part.dxf --units mm --physical-width 80That writes a real HEADER declaring $INSUNITS (and $MEASUREMENT, which some importers read instead) and emits every coordinate at physical scale. The flag asks for the finished width, because that is how a maker thinks — not in pixels-per-unit. Omitting it prints a notice rather than staying quiet, since silence is exactly how the wrong-size part happens.
The gate parses the coordinates back out of the file and asserts the geometry spans the promised distance. That is the assertion that matters: a header claiming millimetres over pixel coordinates passes every other check and cuts ten times too big. It caught a real defect immediately — the first version scaled the CIRCLE and ELLIPSE coordinates and missed the LWPOLYLINE vertices, which are the bulk of any real file.
The zero-dependency core now proves itself by bundling
The existing portability test reads source and walks imports. That is a good proxy and still a proxy — it trusts that the walk sees every edge, and that no dependency pulls something in behind it. The claim users actually rely on is narrower and directly testable: this bundles for a browser. So it is now bundled for a browser in CI, with no externals and no Node platform, and the output asserted free of node: specifiers and Node globals.
85.4 KB minified, with a size tripwire either side.
Also
Fixed in the test rather than the code: the DXF coordinate parser scanned line by line, but DXF is strictly code/value alternating — a coordinate whose value happened to be 10 was read as an X group code, and two runs disagreed on how many coordinates existed.
508 tests · smoke clean · corpus problems: 0
v1.37.0 — vecline serve: Office conversion without uploading
A browser tab has no office engine. The two usual ways to give it one each cost something this project is built on: a ~300 MB LibreOffice-in-WASM destroys instant load, and uploading the document destroys the privacy claim outright.
So neither. vecline serve runs a tiny loopback endpoint, and Vecline Studio hands the document to your machine:
npm install -g vecline && vecline serveEvery link stays local:
.docx → your LibreOffice → PDF → mupdf in the tab → pixels → traced SVG
Two endpoints, both taking bytes and returning bytes. No request field anywhere names a file, so there is nothing to traverse or overwrite.
Built to be attacked, then actually attacked
Four agents worked distinct threat classes — hostile web page, input handling, auth, and the LibreOffice boundary — and every claimed finding was independently re-tested against a live server before being believed. No breached boundary. Four hardening gaps, all fixed before release:
- A failed conversion returned LibreOffice's raw error, carrying both temp directory layouts, the resolved
sofficepath and hence the OS account name — script-readable by an allowlisted origin. Only messages written for a person are sent now; the detail stays on your console. - Any loopback origin on any port was allowed, so every other page served from localhost had full bridge access. Now only vecline.xyz and origins named with
--allow-origin. - Concurrency was unbounded — six concurrent 64 MB bodies took memory from 100 MB to 722 MB. Capped at two, which costs nothing: LibreOffice serialises on one user profile anyway.
--token=silently started with authentication off. It now refuses, and--token-file/VECLINE_TOKENtake precedence over the flag, because an argument is readable from the process table by exactly the account a token exists to fence out.
The threat model in the source header was corrected in four places where the review showed it overclaimed — most importantly "nothing to exfiltrate", which the error text falsified.
Two bugs the tests caught that reading would not have
- The oversize-body path answered mid-upload, racing the still-arriving body. curl coped;
fetchreportedECONNRESETand never saw the 413. Past the cap the body is now drained rather than kept. - The client-disconnect check tested
req.destroyed, which is also true on the happy path once a body is consumed — it rejected every legitimate request until the empty-document test went red.
500 tests · smoke clean · corpus problems: 0
vecline.mcpb is attached — open it with Claude Desktop to install the MCP server in one click.
v1.36.0 — convert gains raster→raster; component handles real-world SVGs
Two defects the docs promised and the code did not deliver. Both were found by executing the documented surface rather than reading it — both exited 0, and neither was visible without looking at the artefact afterwards.
vecline convert photo.png thumb.webp did not work
Every output extension that was not .svg/.dxf/.eps/.pdf fell through to the rasterizer, which accepts only SVG input — so an ordinary raster conversion failed with "does not look like an SVG". The README advertises a square 11×11 format matrix and the library API delivers one, but 100 of those 121 cells were unreachable from the command named convert.
Routing now reads the input bytes as well as the output extension:
vecline convert photo.png thumb.webp # raster → raster
vecline convert logo.png logo.svg # raster → vector
vecline convert logo.svg logo@2x.png # vector → rasterThe rule lives in src/io/route.ts so it can be tested without importing cli.ts (which parses argv on import), and is exported from vecline/core — any converter UI answers this same question.
vecline component drawing.svg emitted uncompilable JSX
injectProps anchors on /^<svg/, so a file opening with <?xml version="1.0"?> — which is most SVGs not written by vecline — missed the anchor, silently dropped the {...props} spread, and left the processing instruction and HTML comments inside a JSX return. Tracing a raster hid it completely, because vecline's own output starts at <svg. The body is now reduced to the root element, and comments become JSX comments rather than vanishing.
Docs corrected to what reproduces
- APNG was advertised as an
animateinput. The prebuiltsharpbinary does not expose APNG frames (vips_image_get: field "n-pages" not found), so an APNG produced a one-frame SVG rather than an error. Animated GIF and animated WebP are the verified inputs. - The Graver→Vecline rename had turned "en·graver" into "en·vecline" — the README and the G-code exporter both advertised a "laser envecline".
- README now leads with Vecline Studio at https://vecline.xyz, and documents the one-click
.mcpbinstall, the registry listings, and the MCP tool annotations. package.json,server.jsonand the MCPB manifest describe the real breadth instead of "image-to-SVG", and gain themcp/model-context-protocolkeywords that were missing entirely.
476 tests green · smoke clean · corpus problems: 0.
vecline.mcpb is attached — open it with Claude Desktop to install the MCP server in one click.
v1.35.0 — tool annotations, privacy policy, and a one-click desktop bundle
Desktop extension
vecline.mcpb is attached to this release — download it and open it with Claude Desktop to install the MCP server in one click. The bundle is a launcher, not a vendored copy: it prefers a vecline already on your PATH and otherwise runs npx -y vecline@1.35.0 mcp, so npm selects the right native binaries for your machine rather than shipping a per-OS bundle that fails silently on the wrong one.
Tool annotations
All 12 MCP tools now carry standard annotations, so an agent knows what a call will do before making it. Eight are marked destructive because they can overwrite a file at the given path; measure, palette, placeholder and image_info are marked read-only. Every tool is openWorldHint: false — nothing here touches a network.
Privacy policy
Published at https://vecline.xyz/privacy.html. It collects nothing. Rather than claim a clean sweep, it names the two cases where a network genuinely is touched: GitHub Pages serves the site and therefore sees request logs, and installing from npm (or converting Office files via LibreOffice) involves third-party software.
Fix
The tool list is now introspected rather than declared in the bundle manifest — a hard-coded list is a second source of truth that drifts from the server the moment a tool changes.
v1.34.1 — registry metadata for MCP discovery
Metadata only; no functional change.
Adds server.json (official MCP Registry manifest, schema 2025-12-11) and the mcpName field in package.json that proves npm-package ownership to the registry.