cd /news/developer-tools/vectorizer-ai-downloader-tampermonke… · home › topics › developer-tools › article
[ARTICLE · art-154391] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Vectorizer.AI Downloader Tampermonkey

A developer known as k33bs published a Tampermonkey userscript that extracts full-resolution SVG output from vectorizer.ai without paying, by hooking the site's in-page VectorImage model rather than intercepting the canvas. The script defines a setter on Object.prototype.isReady to capture the model instance once all result chunks load, then serializes every curve—including off-screen beziers the site's renderer culls—into a downloadable SVG via a cloned download button.

by read6 min views9 publishedOct 7, 2026

| | // ==UserScript== | | | // @name Vectorizer.AI Down | | | // @version 0.1.0 | | | // @description Reads the vector result straight from vectorizer.ai's in-page model and downloads it as a full SVG for free. - fix of https://gist.github.com/mateoltd/b976a33bd1c3d106d5c51b3d000a346d | | | // @author k33bs | | | // @namespace http://tampermonkey.net/ | | | // @license MIT | | | // @match https://vectorizer.ai/images/* | | | // @grant none |

|  | // @run-at document-start | 
|  | // ==/UserScript== | 
|  | (function() { | 

| | 'use strict'; | | | console.log('Vectorizer.AI Down: Script injected and waiting for the vector result.'); | | | /** | | | * Canvas interception can't work here: the site only draws what's on screen. Loops outside the | | | * viewport are skipped, and big shapes get their off-screen beziers drawn as straight lines. | | | * So we grab the site's VectorImage model itself and serialize every curve from it. | | | * | | | * VectorImage sets isReady = true once all result chunks are loaded and committed, | | | * so a setter on Object.prototype hands us the instance. | | | */ | | | let vectorImage = null; | | | Object.defineProperty(Object.prototype, 'isReady', { | | | get() { | | | return this.__isReady; | | | }, |

|  | set(value) { | 
|  | this.__isReady = value; | 
|  | if (value === true && this.vectorShapes && this.topVectorShapes) { | 
|  | vectorImage = this; | 
|  | console.log(`Vector result captured: ${this.vectorShapes.length} shapes, ${this.imageWidth}x${this.imageHeight}`); | 
|  | createDownloadButton(); | 

| | } | | | }, | | | configurable: true | | | }); | | | // UI | | | let customDownloadButton = null; | | | function createDownloadButton() { | | | if (customDownloadButton) return; // Only create it once | | | // We use a MutationObserver to wait for the page's UI to be ready ;) |

|  | const observer = new MutationObserver((mutations, obs) => { | 
|  | const originalButton = document.querySelector('#App-DownloadLink'); | 
|  | if (originalButton) { | 

| | console.log("Original download button found. Creating custom button."); | | | customDownloadButton = originalButton.cloneNode(true); // Clone to keep the style | | | customDownloadButton.id = 'custom-download-button'; | | | customDownloadButton.href = '#'; // Clear original link | | | customDownloadButton.style.backgroundColor = '#5cb85c'; // Green color for distinction |

|  | const textSpan = customDownloadButton.querySelector('.showPaid'); | 
|  | if(textSpan) textSpan.textContent = 'DOWNLOAD SVG'; | 

| | originalButton.parentNode.insertBefore(customDownloadButton, originalButton); |

|  | customDownloadButton.addEventListener('click', (e) => { | 
|  | e.preventDefault(); | 
|  | e.stopPropagation(); | 
|  | const svgContent = buildSvgFromData(); | 
|  | const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' }); | 
|  | const url = URL.createObjectURL(blob); | 

| | // Set filename from the original, if available | | | let filename = 'vectorized-result.svg'; | | | if (window.ResumeImage && window.ResumeImage.originalFilename) { | | | filename = window.ResumeImage.originalFilename.replace(/.[^/.]+$/, ".svg"); | | | } | | | // Create a temporary link to trigger download |

|  | const tempLink = document.createElement('a'); | 
|  | tempLink.href = url; | 
|  | tempLink.download = filename; | 
|  | document.body.appendChild(tempLink); | 
|  | tempLink.click(); | 
|  | document.body.removeChild(tempLink); | 
|  | URL.revokeObjectURL(url); | 
|  | }, true); | 

| | obs.disconnect(); // Stop observing once the button is created | | | } |

|  | }); | 
|  | observer.observe(document.body, { childList: true, subtree: true }); | 

| | } | | | // SVG Generation | | | /** | | | * Mirrors the site's VectorImage.drawTo() without the viewport culling: | | | * top-level gap fillers first, then per shape its fill followed by the gap fillers of its inner loops. | | | */ |

|  | function buildSvgFromData() { | 
|  | const vi = vectorImage; | 
|  | const out = []; | 

| | // Gap fillers: 2px non-scaling strokes with bevel joins, same as the site draws them |

|  | const strokeInterfaces = list => { | 
|  | for (const i of list) { | 
|  | if (!i.shouldDraw()) continue; | 
|  | const hex = '#' + (i.argbInt & 0xffffff).toString(16).padStart(6, '0'); | 
|  | out.push(`<path d="${curvesToD(i.curves)}" fill="none" ${paint('stroke', hex, (i.argbInt >>> 24) / 255)} vector-effect="non-scaling-stroke"/>`); | 

| | } |

|  | }; | 
|  | for (const top of vi.topVectorShapes) strokeInterfaces(top.vectorLoops[0].vectorInterfacesToDraw); | 
|  | for (const shape of vi.vectorShapes) { | 
|  | const color = vi.userPalette.getMutRgba(shape.paletteIndex); | 
|  | if (color.a !== 0) { | 
|  | const d = shape.vectorLoopsToDraw.map(loop => curvesToD(loop.curveLoop.curves) + ' Z').join(' '); | 
|  | if (d) out.push(`<path d="${d}" ${paint('fill', color.toCssRgbHex(), color.a)}/>`); | 

| | } |

|  | for (const loop of shape.vectorLoops) { | 
|  | if (loop.index !== 0) strokeInterfaces(loop.vectorInterfacesToDraw); | 

| | } | | | } |

|  | const w = vi.imageWidth, h = vi.imageHeight; | 
|  | return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">` + | 
|  | `<g stroke-width="2" stroke-linejoin="bevel">${out.join('')}</g></svg>`; | 

| | } | | | // Helper Functions | | | // Helper to format numbers cleanly for the SVG string | | | const F = (val) => Number(val.toFixed(3)).toString(); | | | // rgba() isn't valid in SVG 1.1 paint attributes, so emit hex + opacity | | | const paint = (attr, hex, alpha) => ${attr}="${hex}" + (alpha < 1 ? ${attr}-opacity="${F(alpha)}" : ''); | | | /** | | | * Converts the site's curve objects into an SVG path string. | | | * Curve classes are minified, so detect them by their (unminified) fields. | | | * ponytail: no full-circle arc split; a single 2π arc would vanish as an SVG A. The site | | | * pre-splits circles (0 seen in testing), add a two-half split if one ever shows up. | | | */ |

|  | function curvesToD(curves) { | 
|  | return `M ${F(curves[0].startX)} ${F(curves[0].startY)} ` + curves.map(c => { | 
|  | const end = `${F(c.endX)} ${F(c.endY)}`; | 
|  | if ('controlStartX' in c) return `C ${F(c.controlStartX)} ${F(c.controlStartY)} ${F(c.controlEndX)} ${F(c.controlEndY)} ${end}`; | 
|  | if ('controlX' in c) return `Q ${F(c.controlX)} ${F(c.controlY)} ${end}`; | 
|  | if ('radiusX' in c) return `A ${F(c.radiusX)} ${F(c.radiusY)} ${F(c.rotationRad * 180 / Math.PI)} ${c.largeArcFlag()} ${c.sweepFlag()} ${end}`; | 
|  | if ('radius' in c) return `A ${F(c.radius)} ${F(c.radius)} 0 ${c.largeArcFlag()} ${c.sweepFlag()} ${end}`; | 
|  | return `L ${end}`; | 
|  | }).join(' '); | 

| | } | | | })(); |

── more in #developer-tools 4 stories · sorted by recency
── more on @vectorizer.ai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/vectorizer-ai-downlo…] indexed:0 read:6min 2026-10-07 · —