Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save JTBrinkmann/b79b4625fe3291b9801ecb27e440ac24 to your computer and use it in GitHub Desktop.
Save JTBrinkmann/b79b4625fe3291b9801ecb27e440ac24 to your computer and use it in GitHub Desktop.
userscript to enable free downloads on vectorizer.ai
// ==UserScript==
// @name vectorizer.ai download patcher
// @match https://vectorizer.ai/*
// @version 1.0
// @description enables free downloads on vectorizer.ai, based on https://github.com/sanz-s/Vectorizer.ai-Unofficial-Downloader-2025/
// ==/UserScript==
const main = () => {
const SVG_NS = 'http://www.w3.org/2000/svg'
const SVG_XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>\n'
const svgEl = (window.svgEl = document.createElementNS(SVG_NS, 'svg'))
svgEl.setAttribute('xmlns', SVG_NS)
class CustomPath2D extends Path2D {
constructor() {
super()
this.pathData = ''
this.currentX = null
this.currentY = null
this.lastCmd = ''
}
_updatePoint(x, y, cmd) {
this.currentX = x
this.currentY = y
this.lastCmd = cmd
}
moveTo(x, y) {
super.moveTo(x, y)
this.pathData += `M${x} ${y} `
this._updatePoint(x, y, 'M')
}
lineTo(x, y) {
super.lineTo(x, y)
this.pathData += `L${x} ${y} `
this._updatePoint(x, y, 'L')
}
closePath() {
super.closePath()
this.pathData += 'Z '
this.currentX = this.currentY = null
this.lastCmd = 'Z'
}
arc(x, y, r, a0, a1, ccw) {
super.arc(x, y, r, a0, a1, ccw)
const startX = x + r * Math.cos(a0)
const startY = y + r * Math.sin(a0)
const endX = x + r * Math.cos(a1)
const endY = y + r * Math.sin(a1)
let da = a1 - a0
if (!ccw && da < 0) da += 2 * Math.PI
if (ccw && da > 0) da -= 2 * Math.PI
const largeArcFlag = Math.abs(da) > Math.PI ? 1 : 0
const sweepFlag = ccw ? 0 : 1
if (this.currentX === null || this.lastCmd === 'Z') {
this.pathData += `M${startX} ${startY} `
} else if (
Math.hypot(this.currentX - startX, this.currentY - startY) > 1e-6
) {
this.pathData += `L${startX} ${startY} `
}
this.pathData += `A${r} ${r} 0 ${largeArcFlag} ${sweepFlag} ${endX} ${endY} `
this._updatePoint(endX, endY, 'A')
}
arcTo(x1, y1, x2, y2, r) {
super.arcTo(x1, y1, x2, y2, r)
if (this.currentX === null || this.currentY === null) {
this.moveTo(x1, y1)
this.lineTo(x2, y2)
return
}
const x0 = this.currentX
const y0 = this.currentY
const dx1 = x0 - x1
const dy1 = y0 - y1
const dx2 = x2 - x1
const dy2 = y2 - y1
const len1 = Math.hypot(dx1, dy1)
const len2 = Math.hypot(dx2, dy2)
if (len1 === 0 || len2 === 0 || r === 0) {
this.lineTo(x1, y1)
return
}
const v1x = dx1 / len1
const v1y = dy1 / len1
const v2x = dx2 / len2
const v2y = dy2 / len2
const dot = v1x * v2x + v1y * v2y
const angle = Math.acos(Math.min(1, Math.max(-1, dot)))
if (angle === 0) {
this.lineTo(x1, y1)
return
}
const tanDist = r / Math.tan(angle / 2)
if (tanDist > len1 || tanDist > len2) {
this.lineTo(x1, y1)
return
}
const t1x = x1 + v1x * tanDist
const t1y = y1 + v1y * tanDist
const t2x = x1 + v2x * tanDist
const t2y = y1 + v2y * tanDist
const bisX = v1x + v2x
const bisY = v1y + v2y
const bisLen = Math.hypot(bisX, bisY)
const bisNx = bisX / bisLen
const bisNy = bisY / bisLen
const sinHalfAngle = Math.sin(angle / 2)
const length = r / sinHalfAngle
const cross = v1x * v2y - v1y * v2x
const clockwise = cross < 0
const centerX = x1 + bisNx * length * (clockwise ? -1 : 1)
const centerY = y1 + bisNy * length * (clockwise ? -1 : 1)
const sweepFlag = clockwise ? 1 : 0
this.lineTo(t1x, t1y)
this.pathData += `A${r} ${r} 0 0 ${sweepFlag} ${t2x} ${t2y} `
this._updatePoint(t2x, t2y, 'A')
}
bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
super.bezierCurveTo(c1x, c1y, c2x, c2y, x, y)
this.pathData += `C${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y} `
this._updatePoint(x, y, 'C')
}
quadraticCurveTo(cx, cy, x, y) {
super.quadraticCurveTo(cx, cy, x, y)
this.pathData += `Q${cx} ${cy} ${x} ${y} `
this._updatePoint(x, y, 'Q')
}
rect(x, y, w, h) {
super.rect(x, y, w, h)
this.pathData += `M${x} ${y} L${x + w} ${y} L${x + w} ${y + h} L${x} ${
y + h
} Z `
this.currentX = this.currentY = null
this.lastCmd = 'Z'
}
roundRect(x, y, w, h, r) {
super.roundRect(x, y, w, h, r)
if (typeof r === 'number') {
r = {
tl: r,
tr: r,
br: r,
bl: r,
}
} else {
r = Object.assign(
{
tl: 0,
tr: 0,
br: 0,
bl: 0,
},
r
)
}
const maxRadius = Math.min(w, h) / 2
r.tl = Math.min(r.tl, maxRadius)
r.tr = Math.min(r.tr, maxRadius)
r.br = Math.min(r.br, maxRadius)
r.bl = Math.min(r.bl, maxRadius)
this.moveTo(x + r.tl, y)
this.lineTo(x + w - r.tr, y)
if (r.tr > 0)
this.pathData += `A${r.tr} ${r.tr} 0 0 1 ${x + w} ${y + r.tr} `
this.lineTo(x + w, y + h - r.br)
if (r.br > 0)
this.pathData += `A${r.br} ${r.br} 0 0 1 ${x + w - r.br} ${y + h} `
this.lineTo(x + r.bl, y + h)
if (r.bl > 0)
this.pathData += `A${r.bl} ${r.bl} 0 0 1 ${x} ${y + h - r.bl} `
this.lineTo(x, y + r.tl)
if (r.tl > 0) this.pathData += `A${r.tl} ${r.tl} 0 0 1 ${x + r.tl} ${y} `
this.closePath()
}
ellipse(cx, cy, rx, ry, rot, a0, a1, ccw) {
super.ellipse(cx, cy, rx, ry, rot, a0, a1, ccw)
const rotDeg = (rot * 180) / Math.PI
const cosRot = Math.cos(rot)
const sinRot = Math.sin(rot)
const startX =
cx + rx * Math.cos(a0) * cosRot - ry * Math.sin(a0) * sinRot
const startY =
cy + rx * Math.cos(a0) * sinRot + ry * Math.sin(a0) * cosRot
const endX = cx + rx * Math.cos(a1) * cosRot - ry * Math.sin(a1) * sinRot
const endY = cy + rx * Math.cos(a1) * sinRot + ry * Math.sin(a1) * cosRot
let da = a1 - a0
if (!ccw && da < 0) da += 2 * Math.PI
if (ccw && da > 0) da -= 2 * Math.PI
const largeArcFlag = Math.abs(da) > Math.PI ? 1 : 0
const sweepFlag = ccw ? 0 : 1
if (this.currentX === null || this.lastCmd === 'Z') {
this.pathData += `M${startX} ${startY} `
} else if (
Math.hypot(this.currentX - startX, this.currentY - startY) > 1e-6
) {
this.pathData += `L${startX} ${startY} `
}
this.pathData += `A${rx} ${ry} ${rotDeg} ${largeArcFlag} ${sweepFlag} ${endX} ${endY} `
this._updatePoint(endX, endY, 'A')
}
addPath(path, transform) {
super.addPath(path, transform)
this.pathData += `/* addPath(...) */ `
}
getPathData() {
return this.pathData.trim()
}
}
window.Path2D = CustomPath2D
const origFill = CanvasRenderingContext2D.prototype.fill
const origStroke = CanvasRenderingContext2D.prototype.stroke
function serializePath(path, ctx) {
const d = path.getPathData().trim()
const t = ctx.getTransform()
const m = [t.a, t.b, t.c, t.d, t.e, t.f].join(' ')
return {
d,
m,
}
}
CanvasRenderingContext2D.prototype.fill = function (path) {
if (arguments.length) origFill.call(this, path)
else origFill.call(this)
if (path instanceof Path2D) {
const { d, m } = serializePath(path, this)
// copy to SVG
const el = document.createElementNS(SVG_NS, 'path')
el.setAttribute('d', d)
el.setAttribute('transform', `matrix(${m})`)
el.setAttribute('fill', this.fillStyle)
el.setAttribute('fill-opacity', this.globalAlpha)
svgEl.append(el)
}
}
CanvasRenderingContext2D.prototype.stroke = function (path) {
if (arguments.length) origStroke.call(this, path)
else origStroke.call(this)
if (path instanceof Path2D) {
const { d, m } = serializePath(path, this)
// copy to SVG
const el = document.createElementNS(SVG_NS, 'path')
el.setAttribute('d', d)
el.setAttribute('transform', `matrix(${m})`)
el.setAttribute('fill', 'none')
el.setAttribute('stroke', this.strokeStyle)
svgEl.append(el)
}
}
// wait for URL of finished vectorization
function checkVectorizationFinishedUrl() {
console.log('waiting for navigation to /image/.../edit')
if (location.pathname.match(/^\/images\/.+\/edit$/) != null) {
waitVectorizationProgressDone()
return true
}
}
if (!checkVectorizationFinishedUrl()) {
const replaceState = history.replaceState
history.replaceState = (...args) => {
replaceState.apply(history, args)
checkVectorizationFinishedUrl()
}
}
// wait for progressbar to fill up
var dlBtn // for some reason, `let` breaks this??
function waitVectorizationProgressDone() {
const progressBar = document.querySelector('#App-Progress-Download-Bar')
dlBtn = document.querySelector('#App-DownloadLink')
console.log('waiting for vectorization', progressBar?.style.width)
if (progressBar?.style.width != '100%' || dlBtn == null) {
requestAnimationFrame(waitVectorizationProgressDone)
} else if (dlBtn.classList.contains('isFree')) {
console.log(
'download is already free, skipping download patch\n(if truly needed, call `download()`)'
)
} else {
requestAnimationFrame(() => {
updateDownloadBtn() // done loading!
})
}
}
// once everything is done, update download button
var blobUrl
function updateDownloadBtn() {
if (window.blobUrl) {
URL.revokeObjectURL(window.blobUrl)
}
blobUrl = window.blobUrl = URL.createObjectURL(
new Blob([SVG_XML_DECLARATION, svgEl.outerHTML], {
type: 'image/svg+xml',
})
)
dlBtn.classList.remove('isPaid')
dlBtn.classList.add('isFree')
dlBtn.download = dlBtn.title = `${document.title.replace(
/(?:\.svg)?\.\w+ - .*/,
''
)}.svg`
dlBtn.href = blobUrl
dlBtn.style.background = 'green'
console.info('updated download button!')
}
// helper function for manual download (not used by the userscript itself)
window.download = () => {
dlBtn = dlBtn || document.createElement('a')
updateDownloadBtn()
document.querySelector('#App-DownloadLink').click()
}
}
window.eval(`(${main})()`)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment