# Vectorizer.AI Downloader Tampermonkey

> Source: <https://gist.github.com/k33bs/805422a8e5e4e3b0b7945b36078c6b37>
> Published: 2026-10-07 20:00:34+00:00

|  | // ==UserScript== | 
|  | // @name Vectorizer.AI Downloader | 
|  | // @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 Downloader: 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(' '); | 
|  | } | 
|  | })(); |
