{"slug": "vectorizer-ai-downloader-tampermonkey", "title": "Vectorizer.AI Downloader Tampermonkey", "summary": "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.", "body_md": "|  | // ==UserScript== | \n|  | // @name Vectorizer.AI Downloader | \n|  | // @version 0.1.0 | \n|  | // @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 | \n|  | // @author k33bs | \n|  | // @namespace http://tampermonkey.net/ | \n|  | // @license MIT | \n|  | // @match https://vectorizer.ai/images/* | \n|  | // @grant none | \n|  | // @run-at document-start | \n|  | // ==/UserScript== | \n|  | (function() { | \n|  | 'use strict'; | \n|  | console.log('Vectorizer.AI Downloader: Script injected and waiting for the vector result.'); | \n|  | /** | \n|  | * Canvas interception can't work here: the site only draws what's on screen. Loops outside the | \n|  | * viewport are skipped, and big shapes get their off-screen beziers drawn as straight lines. | \n|  | * So we grab the site's VectorImage model itself and serialize every curve from it. | \n|  | * | \n|  | * VectorImage sets `isReady = true` once all result chunks are loaded and committed, | \n|  | * so a setter on Object.prototype hands us the instance. | \n|  | */ | \n|  | let vectorImage = null; | \n|  | Object.defineProperty(Object.prototype, 'isReady', { | \n|  | get() { | \n|  | return this.__isReady; | \n|  | }, | \n|  | set(value) { | \n|  | this.__isReady = value; | \n|  | if (value === true && this.vectorShapes && this.topVectorShapes) { | \n|  | vectorImage = this; | \n|  | console.log(`Vector result captured: ${this.vectorShapes.length} shapes, ${this.imageWidth}x${this.imageHeight}`); | \n|  | createDownloadButton(); | \n|  | } | \n|  | }, | \n|  | configurable: true | \n|  | }); | \n|  | // UI | \n|  | let customDownloadButton = null; | \n|  | function createDownloadButton() { | \n|  | if (customDownloadButton) return; // Only create it once | \n|  | // We use a MutationObserver to wait for the page's UI to be ready ;) | \n|  | const observer = new MutationObserver((mutations, obs) => { | \n|  | const originalButton = document.querySelector('#App-DownloadLink'); | \n|  | if (originalButton) { | \n|  | console.log(\"Original download button found. Creating custom button.\"); | \n|  | customDownloadButton = originalButton.cloneNode(true); // Clone to keep the style | \n|  | customDownloadButton.id = 'custom-download-button'; | \n|  | customDownloadButton.href = '#'; // Clear original link | \n|  | customDownloadButton.style.backgroundColor = '#5cb85c'; // Green color for distinction | \n|  | const textSpan = customDownloadButton.querySelector('.showPaid'); | \n|  | if(textSpan) textSpan.textContent = 'DOWNLOAD SVG'; | \n|  | originalButton.parentNode.insertBefore(customDownloadButton, originalButton); | \n|  | customDownloadButton.addEventListener('click', (e) => { | \n|  | e.preventDefault(); | \n|  | e.stopPropagation(); | \n|  | const svgContent = buildSvgFromData(); | \n|  | const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' }); | \n|  | const url = URL.createObjectURL(blob); | \n|  | // Set filename from the original, if available | \n|  | let filename = 'vectorized-result.svg'; | \n|  | if (window.ResumeImage && window.ResumeImage.originalFilename) { | \n|  | filename = window.ResumeImage.originalFilename.replace(/\\.[^/.]+$/, \".svg\"); | \n|  | } | \n|  | // Create a temporary link to trigger download | \n|  | const tempLink = document.createElement('a'); | \n|  | tempLink.href = url; | \n|  | tempLink.download = filename; | \n|  | document.body.appendChild(tempLink); | \n|  | tempLink.click(); | \n|  | document.body.removeChild(tempLink); | \n|  | URL.revokeObjectURL(url); | \n|  | }, true); | \n|  | obs.disconnect(); // Stop observing once the button is created | \n|  | } | \n|  | }); | \n|  | observer.observe(document.body, { childList: true, subtree: true }); | \n|  | } | \n|  | // SVG Generation | \n|  | /** | \n|  | * Mirrors the site's VectorImage.drawTo() without the viewport culling: | \n|  | * top-level gap fillers first, then per shape its fill followed by the gap fillers of its inner loops. | \n|  | */ | \n|  | function buildSvgFromData() { | \n|  | const vi = vectorImage; | \n|  | const out = []; | \n|  | // Gap fillers: 2px non-scaling strokes with bevel joins, same as the site draws them | \n|  | const strokeInterfaces = list => { | \n|  | for (const i of list) { | \n|  | if (!i.shouldDraw()) continue; | \n|  | const hex = '#' + (i.argbInt & 0xffffff).toString(16).padStart(6, '0'); | \n|  | out.push(`<path d=\"${curvesToD(i.curves)}\" fill=\"none\" ${paint('stroke', hex, (i.argbInt >>> 24) / 255)} vector-effect=\"non-scaling-stroke\"/>`); | \n|  | } | \n|  | }; | \n|  | for (const top of vi.topVectorShapes) strokeInterfaces(top.vectorLoops[0].vectorInterfacesToDraw); | \n|  | for (const shape of vi.vectorShapes) { | \n|  | const color = vi.userPalette.getMutRgba(shape.paletteIndex); | \n|  | if (color.a !== 0) { | \n|  | const d = shape.vectorLoopsToDraw.map(loop => curvesToD(loop.curveLoop.curves) + ' Z').join(' '); | \n|  | if (d) out.push(`<path d=\"${d}\" ${paint('fill', color.toCssRgbHex(), color.a)}/>`); | \n|  | } | \n|  | for (const loop of shape.vectorLoops) { | \n|  | if (loop.index !== 0) strokeInterfaces(loop.vectorInterfacesToDraw); | \n|  | } | \n|  | } | \n|  | const w = vi.imageWidth, h = vi.imageHeight; | \n|  | return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${w} ${h}\" width=\"${w}\" height=\"${h}\">` + | \n|  | `<g stroke-width=\"2\" stroke-linejoin=\"bevel\">${out.join('')}</g></svg>`; | \n|  | } | \n|  | // Helper Functions | \n|  | // Helper to format numbers cleanly for the SVG string | \n|  | const F = (val) => Number(val.toFixed(3)).toString(); | \n|  | // rgba() isn't valid in SVG 1.1 paint attributes, so emit hex + opacity | \n|  | const paint = (attr, hex, alpha) => `${attr}=\"${hex}\"` + (alpha < 1 ? ` ${attr}-opacity=\"${F(alpha)}\"` : ''); | \n|  | /** | \n|  | * Converts the site's curve objects into an SVG path string. | \n|  | * Curve classes are minified, so detect them by their (unminified) fields. | \n|  | * ponytail: no full-circle arc split; a single 2π arc would vanish as an SVG A. The site | \n|  | * pre-splits circles (0 seen in testing), add a two-half split if one ever shows up. | \n|  | */ | \n|  | function curvesToD(curves) { | \n|  | return `M ${F(curves[0].startX)} ${F(curves[0].startY)} ` + curves.map(c => { | \n|  | const end = `${F(c.endX)} ${F(c.endY)}`; | \n|  | if ('controlStartX' in c) return `C ${F(c.controlStartX)} ${F(c.controlStartY)} ${F(c.controlEndX)} ${F(c.controlEndY)} ${end}`; | \n|  | if ('controlX' in c) return `Q ${F(c.controlX)} ${F(c.controlY)} ${end}`; | \n|  | if ('radiusX' in c) return `A ${F(c.radiusX)} ${F(c.radiusY)} ${F(c.rotationRad * 180 / Math.PI)} ${c.largeArcFlag()} ${c.sweepFlag()} ${end}`; | \n|  | if ('radius' in c) return `A ${F(c.radius)} ${F(c.radius)} 0 ${c.largeArcFlag()} ${c.sweepFlag()} ${end}`; | \n|  | return `L ${end}`; | \n|  | }).join(' '); | \n|  | } | \n|  | })(); |", "url": "https://wpnews.pro/news/vectorizer-ai-downloader-tampermonkey", "canonical_source": "https://gist.github.com/k33bs/805422a8e5e4e3b0b7945b36078c6b37", "published_at": "2026-10-07 20:00:34+00:00", "updated_at": "2026-10-21 09:11:10.878406+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Vectorizer.AI", "Tampermonkey", "k33bs"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/vectorizer-ai-downloader-tampermonkey", "markdown": "https://wpnews.pro/news/vectorizer-ai-downloader-tampermonkey.md", "text": "https://wpnews.pro/news/vectorizer-ai-downloader-tampermonkey.txt", "jsonld": "https://wpnews.pro/news/vectorizer-ai-downloader-tampermonkey.jsonld"}}