Skip to content

Instantly share code, notes, and snippets.

@JTBrinkmann
Created January 31, 2020 15:13
Show Gist options
  • Save JTBrinkmann/67af1a44b93a43febca7811dfbe1c850 to your computer and use it in GitHub Desktop.
Save JTBrinkmann/67af1a44b93a43febca7811dfbe1c850 to your computer and use it in GitHub Desktop.
small tool to resize SVGs internal coordinates
<!DOCTYPE html>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
}
body {
background: #afa;
padding: 2%;
font-family: sans-serif;
}
#wrapper {
flex: 0 0 auto;
display: flex;
flex-direction: column;
height: 100%;
padding: 10px 40px;
border-radius: 10px;
background: white;
}
#wrapper > * {
flex: 0 0 0;
}
textarea {
display: block;
width: 100%;
min-height: 20em;
flex: 1 1 auto !important;
margin-top: 1em;
background: #f4fff4;
box-shadow: 1px 1px 3px #aaa;
border: none;
padding: 1em;
}
.slider {
display: flex;
}
.slider > input {
flex: 1 0 auto;
}
.slider > span {
width: 6em;
text-align: right;
}
#buttons {
display: flex;
}
#buttons > button {
flex: 1 0 auto;
height: 3em;
font-size: 2em;
}
body:not(.dragging-file) #dragndrop-notice {
display: none;
}
#dragndrop-notice {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
</style>
<div id="wrapper">
<h1>SVG Scaling Tool</h1>
<span id="instructions">
<bold>Drag'n'drop your .svg file onto this page</bold> or paste it's content
into the field below. You can also just enter the SVG path directly (e.g.
copied from the database)
</span>
<textarea id="$input"></textarea>
<p id="options">
<label class="slider">
<span class="label">width: </span>
<input type="range" id="$width" min="1" max="1000000" />
<span id="$widthSpan" class="slider-val"></span>
</label>
<label class="slider">
<span class="label">height: </span>
<input type="range" id="$height" min="1" max="1000000" />
<span id="$heightSpan" class="slider-val"></span>
</label>
</p>
<p id="buttons">
<button id="$copyBtn">copy</button>
&nbsp;
<button id="$downloadBtn">download</button>
</p>
<div id="dragndrop-notice">
Drop the file to read it!
</div>
</div>
<script>
/*
// get commandParams by going to https://svgwg.org/specs/paths/
// and executing the following in the browser devtool's console:
// (the `copy(…)` at the end will copy the result to the clipboard)
commandParams = {}
for (const table of $$(".PathDataTable")) {
const rows = table.querySelectorAll("tr:not(:first-child)")
for (const row of rows) {
const commands = Array.from(row.children[0].querySelectorAll("strong")).map(c => c.textContent)
const rawParams = row.children[2].textContent
const params = rawParams
.replace(/[\(\)\+\n]|none/g, '')
.split(/\s+/g)
.filter(p => p != "")
const repeatedParams_ = /\(.*?\)\+$|[\w\-]+\+/.exec(rawParams)
const repeatedParams = repeatedParams_ ?
repeatedParams_[0]
.replace(/[\(\)\+\n]|none/g, '')
.split(/\s+/g)
.filter(p => p != "")
.length
: 0
for (const command of commands) {
commandParams[command] = {params, repeatedParams}
}
// console.log(commands.join(", "), {params, repeatedParams})
}
}
copy(JSON.stringify(commandParams))
*/
const commandParams = {
M: { params: ["x", "y"], repeatedParams: 2 },
m: { params: ["x", "y"], repeatedParams: 2 },
Z: { params: [""], repeatedParams: 0 },
z: { params: [""], repeatedParams: 0 },
L: { params: ["x", "y"], repeatedParams: 2 },
l: { params: ["x", "y"], repeatedParams: 2 },
H: { params: ["x"], repeatedParams: 1 },
h: { params: ["x"], repeatedParams: 1 },
V: { params: ["y"], repeatedParams: 1 },
v: { params: ["y"], repeatedParams: 1 },
C: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 6 },
c: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 6 },
S: { params: ["x2", "y2", "x", "y"], repeatedParams: 4 },
s: { params: ["x2", "y2", "x", "y"], repeatedParams: 4 },
Q: { params: ["x1", "y1", "x", "y"], repeatedParams: 4 },
q: { params: ["x1", "y1", "x", "y"], repeatedParams: 4 },
T: { params: ["x", "y"], repeatedParams: 2 },
t: { params: ["x", "y"], repeatedParams: 2 },
A: {
params: [
"rx",
"ry",
"x-axis-rotation",
"large-arc-flag",
"sweep-flag",
"x",
"y",
],
repeatedParams: 0,
},
a: {
params: [
"rx",
"ry",
"x-axis-rotation",
"large-arc-flag",
"sweep-flag",
"x",
"y",
],
repeatedParams: 0,
},
R: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 2 },
r: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 2 },
B: { params: ["angle"], repeatedParams: 1 },
b: { params: ["angle"], repeatedParams: 1 },
}
const coordinateParams = ["x", "y", "x1", "y1", "x2", "y2", "rx", "ry"] // non-coordinate params: 'x-axis-rotation', 'large-arc-flag', 'sweep-flag', 'angle'
const scalePath = (scaleFactor, d) => {
const regexp = /(z|[a-yA-Z]+)|(\d*\.\d+|\d+)/g
let paramNum = 0
let lastCommand = "M"
return d.replace(regexp, (_, command, arg) => {
if (command) {
paramNum = 0
lastCommand = command
return command
} else {
if (!(lastCommand in commandParams)) {
console.warn(`${lastCommand} is an unknown path command`)
debugger
paramNum++
return arg
}
const firstParamCount = commandParams[lastCommand].params.length
const repeatedParamCount = commandParams[lastCommand].repeatedParams
const nonrepeatedParamCount = firstParamCount - repeatedParamCount
const effectiveParamNum =
paramNum < firstParamCount
? paramNum
: ((paramNum - nonrepeatedParamCount) % repeatedParamCount) +
nonrepeatedParamCount
const effectiveParam =
commandParams[lastCommand].params[effectiveParamNum]
paramNum++
const isCoordinate = coordinateParams.includes(effectiveParam)
if (isCoordinate) {
return `${parseFloat(arg) * scaleFactor}`
} else {
return arg
}
}
})
}
const updateScaleSlider = () => {
$width.value = width
$widthSpan.textContent = width.toLocaleString()
$height.value = height
$heightSpan.textContent = height.toLocaleString()
}
const isSvg = (str) => /^\s*</.test(str)
const scaleInput = (scaleFactor) => {
const scaleFloat = (coord) => {
return /^\d*\.\d+|\d+$/.test(coord)
? parseFloat(coord) * scaleFactor
: coord
}
if (isSvg(inputValue)) {
// SVG mode
$input.value = inputValue
.replace(/\bd="(.*?)"/g, (_, d) => `d="${scalePath(scaleFactor, d)}"`)
.replace(
/\b(x|y|width|height)="(\d*\.\d+|\d+)"/g,
(_, attr, val) => `${attr}="${scaleFloat(val)}"`
)
.replace(
/\b(points|viewBox)="(.*?)"/g,
(_, attr, points) =>
`${attr}="${points.replace(/\d*\.\d+|\d+/g, scaleFloat)}"`
)
//.replace(/\b(points|viewBox|x|y|width|height)="(.*?)"/g, (_, attr, points) => `${attr}="${points.split(/\s+|,/g).map(scaleFloat).join(' ')}"`)
} else {
$input.value = scalePath(scaleFactor, inputValue)
}
width = Math.round(srcWidth * scaleFactor)
height = Math.round(srcHeight * scaleFactor)
updateScaleSlider()
}
const debounce = (cb, lock, delay) => {
return (...args) => {
if (scaleInputLock.id) {
clearTimeout(scaleInputLock.id)
}
scaleInputLock.id = setTimeout(() => cb(...args), delay)
}
}
/** width / height */
let aspectRatio
let width, height, srcWidth, srcHeight
let inputValue
const onInputChanged = () => {
inputValue = $input.value
if (isSvg(inputValue)) {
const viewBox = /\bviewBox="\s*(\d*\.\d+|\d+)\s+(\d*\.\d+|\d+)\s+(\d*\.\d+|\d+)\s+(\d*\.\d+|\d+)\s*"/.exec(
inputValue
)
if (viewBox != null) {
srcWidth = width = Math.round(parseFloat(viewBox[3]) - parseFloat(viewBox[1])) // x2 - x1
srcHeight = height = Math.round(parseFloat(viewBox[4]) - parseFloat(viewBox[2])) // y2 - y1
aspectRatio = width / height
} else {
alert("SVGs without a viewBox attribute are unsupported!")
}
updateScaleSlider()
}
}
$input.addEventListener("input", onInputChanged)
if ($input.value != "") {
onInputChanged()
}
const scaleInputLock = {}
$width.addEventListener(
"input",
debounce(
() => scaleInput(parseFloat($width.value) / srcWidth),
scaleInputLock,
10
)
)
$height.addEventListener(
"input",
debounce(
() => scaleInput(parseFloat($height.value) / srcHeight),
scaleInputLock,
10
)
)
// enable file drag'n'drop
document.body.addEventListener("dragover", (e) => {
e.stopPropagation()
e.preventDefault()
document.body.classList.add("dragging-file")
e.dataTransfer.dropEffect = "copy"
})
document.body.addEventListener("drop", (e) => {
e.stopPropagation()
e.preventDefault()
document.body.classList.remove("dragging-file")
const file = e.dataTransfer.files[0]
if (file != null) {
if (file.type != "image/svg+xml" && !file.type.startsWith('text/')) {
console.warn("file type:", file.type)
alert("invalid file format, must be a text file!")
} else {
var reader = new FileReader()
reader.onload = (e2) => {
$input.value = e2.target.result
onInputChanged()
}
reader.readAsText(file)
}
}
if (e.dataTransfer.files.length > 1) {
alert("dropping multiple files is not supported")
}
})
document.body.addEventListener("dragleave", (e) => {
e.stopPropagation()
e.preventDefault()
document.body.classList.remove("dragging-file")
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment