Created
September 1, 2020 19:59
-
-
Save CommandLeo/94ab67a7d6d01843d94fe0e3283b4ffc to your computer and use it in GitHub Desktop.
// source https://jsbin.com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width"> | |
<title>Storage Tech Logo</title> | |
<link href="https://fonts.googleapis.com/css2?family=Bungee&display=swap" rel="stylesheet"> | |
<style> | |
body { | |
min-height: 100%; | |
display: flex; | |
margin: 0; | |
padding: 16px; | |
} | |
svg { | |
height: 90vh; | |
} | |
.st0 { | |
fill: var(--chest-bg, #333); | |
} | |
.st1 { | |
fill: var(--border-in, #333); | |
stroke: var(--border-out, #9b59b6); | |
stroke-width: 16; | |
stroke-miterlimit: 10; | |
} | |
.text { | |
fill: var(--text-fill, #9b59b6); | |
font-family: 'Bungee'; | |
font-size: 209.4232px; | |
letter-spacing: -5.1; | |
} | |
</style> | |
</head> | |
<body> | |
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024;" xml:space="preserve"> | |
<polygon class="st0" points="105.9,250.2 105.9,688.9 512.6,881.7 919.3,688.9 919.3,250.2 512.6,63.8 " /> | |
<g> | |
<path class="st1" d="M105.9,250.2v438.8l406.7,192.7l406.7-192.7V250.2L512.6,63.8L105.9,250.2z M524.9,604.9l102.7-45.5v49.2 | |
l164.3-72.6v-49.4l102.7-45.5v232L524.9,848.5V604.9z M652.2,570.8v-67.5l115-50.8V520L652.2,570.8z M524.9,440.7L894.6,277v137.3 | |
l-102.7,45.5v-45l-164.3,72.6v45.3L524.9,578V440.7z M512.6,419.2L148.3,257.9l364.3-167l364.3,167L512.6,419.2z M130.6,441.3 | |
l369.7,163.7v243.6L130.6,673.4V441.3z M130.6,277l369.7,163.7V578L130.6,414.3V277z" /> | |
</g> | |
<text transform="matrix(0.7114 -0.3317 0 1 622.0651 783.1853)" class="text">ST</text> | |
</svg> | |
<main> | |
<label> | |
Border in color | |
<input type="color" id="border-in"> | |
</label><br> | |
<label> | |
Border out color | |
<input type="color" id="border-out"> | |
</label><br> | |
<label> | |
Chest background | |
<input type="color" id="chest-bg"> | |
</label><br> | |
<label> | |
Text fill | |
<input type="color" id="text-fill"> | |
</label><br> | |
<button id="export">Export</button> | |
</main> | |
<script> | |
(function() { | |
const out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this || window; | |
if (typeof define !== 'undefined') define('save-svg-as-png', [], () => out$); | |
out$.default = out$; | |
const xmlNs = 'http://www.w3.org/2000/xmlns/'; | |
const xhtmlNs = 'http://www.w3.org/1999/xhtml'; | |
const svgNs = 'http://www.w3.org/2000/svg'; | |
const doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [<!ENTITY nbsp " ">]>'; | |
const urlRegex = /url\(["']?(.+?)["']?\)/; | |
const fontFormats = { | |
woff2: 'font/woff2', | |
woff: 'font/woff', | |
otf: 'application/x-font-opentype', | |
ttf: 'application/x-font-ttf', | |
eot: 'application/vnd.ms-fontobject', | |
sfnt: 'application/font-sfnt', | |
svg: 'image/svg+xml' | |
}; | |
const isElement = obj => obj instanceof HTMLElement || obj instanceof SVGElement; | |
const requireDomNode = el => { | |
if (!isElement(el)) throw new Error(`an HTMLElement or SVGElement is required; got ${el}`); | |
}; | |
const requireDomNodePromise = el => | |
new Promise((resolve, reject) => { | |
if (isElement(el)) resolve(el) | |
else reject(new Error(`an HTMLElement or SVGElement is required; got ${el}`)); | |
}) | |
const isExternal = url => url && url.lastIndexOf('http', 0) === 0 && url.lastIndexOf(window.location.host) === -1; | |
const getFontMimeTypeFromUrl = fontUrl => { | |
const formats = Object.keys(fontFormats) | |
.filter(extension => fontUrl.indexOf(`.${extension}`) > 0) | |
.map(extension => fontFormats[extension]); | |
if (formats) return formats[0]; | |
console.error(`Unknown font format for ${fontUrl}. Fonts may not be working correctly.`); | |
return 'application/octet-stream'; | |
}; | |
const arrayBufferToBase64 = buffer => { | |
let binary = ''; | |
const bytes = new Uint8Array(buffer); | |
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); | |
return window.btoa(binary); | |
} | |
const getDimension = (el, clone, dim) => { | |
const v = | |
(el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) || | |
(clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) || | |
el.getBoundingClientRect()[dim] || | |
parseInt(clone.style[dim]) || | |
parseInt(window.getComputedStyle(el).getPropertyValue(dim)); | |
return typeof v === 'undefined' || v === null || isNaN(parseFloat(v)) ? 0 : v; | |
}; | |
const getDimensions = (el, clone, width, height) => { | |
if (el.tagName === 'svg') return { | |
width: width || getDimension(el, clone, 'width'), | |
height: height || getDimension(el, clone, 'height') | |
}; | |
else if (el.getBBox) { | |
const { | |
x, | |
y, | |
width, | |
height | |
} = el.getBBox(); | |
return { | |
width: x + width, | |
height: y + height | |
}; | |
} | |
}; | |
const reEncode = data => | |
decodeURIComponent( | |
encodeURIComponent(data) | |
.replace(/%([0-9A-F]{2})/g, (match, p1) => { | |
const c = String.fromCharCode(`0x${p1}`); | |
return c === '%' ? '%25' : c; | |
}) | |
); | |
const uriToBlob = uri => { | |
const byteString = window.atob(uri.split(',')[1]); | |
const mimeString = uri.split(',')[0].split(':')[1].split(';')[0] | |
const buffer = new ArrayBuffer(byteString.length); | |
const intArray = new Uint8Array(buffer); | |
for (let i = 0; i < byteString.length; i++) { | |
intArray[i] = byteString.charCodeAt(i); | |
} | |
return new Blob([buffer], { | |
type: mimeString | |
}); | |
}; | |
const query = (el, selector) => { | |
if (!selector) return; | |
try { | |
return el.querySelector(selector) || el.parentNode && el.parentNode.querySelector(selector); | |
} catch (err) { | |
console.warn(`Invalid CSS selector "${selector}"`, err); | |
} | |
}; | |
const detectCssFont = (rule, href) => { | |
// Match CSS font-face rules to external links. | |
// @font-face { | |
// src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2); | |
// } | |
const match = rule.cssText.match(urlRegex); | |
const url = (match && match[1]) || ''; | |
if (!url || url.match(/^data:/) || url === 'about:blank') return; | |
const fullUrl = | |
url.startsWith('../') ? `${href}/../${url}` : | |
url.startsWith('./') ? `${href}/.${url}` : | |
url; | |
return { | |
text: rule.cssText, | |
format: getFontMimeTypeFromUrl(fullUrl), | |
url: fullUrl | |
}; | |
}; | |
const inlineImages = el => Promise.all( | |
Array.from(el.querySelectorAll('image')).map(image => { | |
let href = image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || image.getAttribute('href'); | |
if (!href) return Promise.resolve(null); | |
if (isExternal(href)) { | |
href += (href.indexOf('?') === -1 ? '?' : '&') + 't=' + new Date().valueOf(); | |
} | |
return new Promise((resolve, reject) => { | |
const canvas = document.createElement('canvas'); | |
const img = new Image(); | |
img.crossOrigin = 'anonymous'; | |
img.src = href; | |
img.onerror = () => reject(new Error(`Could not load ${href}`)); | |
img.onload = () => { | |
canvas.width = img.width; | |
canvas.height = img.height; | |
canvas.getContext('2d').drawImage(img, 0, 0); | |
image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', canvas.toDataURL('image/png')); | |
resolve(true); | |
}; | |
}); | |
}) | |
); | |
const cachedFonts = {}; | |
const inlineFonts = fonts => Promise.all( | |
fonts.map(font => | |
new Promise((resolve, reject) => { | |
if (cachedFonts[font.url]) return resolve(cachedFonts[font.url]); | |
const req = new XMLHttpRequest(); | |
req.addEventListener('load', () => { | |
// TODO: it may also be worth it to wait until fonts are fully loaded before | |
// attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet) | |
const fontInBase64 = arrayBufferToBase64(req.response); | |
const fontUri = font.text.replace(urlRegex, `url("data:${font.format};base64,${fontInBase64}")`) + '\n'; | |
cachedFonts[font.url] = fontUri; | |
resolve(fontUri); | |
}); | |
req.addEventListener('error', e => { | |
console.warn(`Failed to load font from: ${font.url}`, e); | |
cachedFonts[font.url] = null; | |
resolve(null); | |
}); | |
req.addEventListener('abort', e => { | |
console.warn(`Aborted loading font from: ${font.url}`, e); | |
resolve(null); | |
}); | |
req.open('GET', font.url); | |
req.responseType = 'arraybuffer'; | |
req.send(); | |
}) | |
) | |
).then(fontCss => fontCss.filter(x => x).join('')); | |
let cachedRules = null; | |
const styleSheetRules = () => { | |
if (cachedRules) return cachedRules; | |
return cachedRules = Array.from(document.styleSheets).map(sheet => { | |
try { | |
return { | |
rules: sheet.cssRules, | |
href: sheet.href | |
}; | |
} catch (e) { | |
console.warn(`Stylesheet could not be loaded: ${sheet.href}`, e); | |
return {}; | |
} | |
}); | |
}; | |
const inlineCss = (el, options) => { | |
const { | |
selectorRemap, | |
modifyStyle, | |
modifyCss, | |
fonts, | |
excludeUnusedCss | |
} = options || {}; | |
const generateCss = modifyCss || ((selector, properties) => { | |
const sel = selectorRemap ? selectorRemap(selector) : selector; | |
const props = modifyStyle ? modifyStyle(properties) : properties; | |
return `${sel}{${props}}\n`; | |
}); | |
const css = []; | |
const detectFonts = typeof fonts === 'undefined'; | |
const fontList = fonts || []; | |
styleSheetRules().forEach(({ | |
rules, | |
href | |
}) => { | |
if (!rules) return; | |
Array.from(rules).forEach(rule => { | |
if (typeof rule.style != 'undefined') { | |
if (query(el, rule.selectorText)) css.push(generateCss(rule.selectorText, rule.style.cssText)); | |
else if (detectFonts && rule.cssText.match(/^@font-face/)) { | |
const font = detectCssFont(rule, href); | |
if (font) fontList.push(font); | |
} else if (!excludeUnusedCss) { | |
css.push(rule.cssText); | |
} | |
} | |
}); | |
}); | |
return inlineFonts(fontList).then(fontCss => css.join('\n') + fontCss); | |
}; | |
const downloadOptions = () => { | |
if (!navigator.msSaveOrOpenBlob && !('download' in document.createElement('a'))) { | |
return { | |
popup: window.open() | |
}; | |
} | |
}; | |
out$.prepareSvg = (el, options, done) => { | |
requireDomNode(el); | |
const { | |
left = 0, | |
top = 0, | |
width: w, | |
height: h, | |
scale = 1, | |
responsive = false, | |
excludeCss = false, | |
} = options || {}; | |
return inlineImages(el).then(() => { | |
let clone = el.cloneNode(true); | |
clone.style.backgroundColor = (options || {}).backgroundColor || el.style.backgroundColor; | |
const { | |
width, | |
height | |
} = getDimensions(el, clone, w, h); | |
if (el.tagName !== 'svg') { | |
if (el.getBBox) { | |
if (clone.getAttribute('transform') != null) { | |
clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '')); | |
} | |
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
svg.appendChild(clone); | |
clone = svg; | |
} else { | |
console.error('Attempted to render non-SVG element', el); | |
return; | |
} | |
} | |
clone.setAttribute('version', '1.1'); | |
clone.setAttribute('viewBox', [left, top, width, height].join(' ')); | |
if (!clone.getAttribute('xmlns')) clone.setAttributeNS(xmlNs, 'xmlns', svgNs); | |
if (!clone.getAttribute('xmlns:xlink')) clone.setAttributeNS(xmlNs, 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); | |
if (responsive) { | |
clone.removeAttribute('width'); | |
clone.removeAttribute('height'); | |
clone.setAttribute('preserveAspectRatio', 'xMinYMin meet'); | |
} else { | |
clone.setAttribute('width', width * scale); | |
clone.setAttribute('height', height * scale); | |
} | |
Array.from(clone.querySelectorAll('foreignObject > *')).forEach(foreignObject => { | |
foreignObject.setAttributeNS(xmlNs, 'xmlns', foreignObject.tagName === 'svg' ? svgNs : xhtmlNs); | |
}); | |
if (excludeCss) { | |
const outer = document.createElement('div'); | |
outer.appendChild(clone); | |
const src = outer.innerHTML; | |
if (typeof done === 'function') done(src, width, height); | |
else return { | |
src, | |
width, | |
height | |
}; | |
} else { | |
return inlineCss(el, options).then(css => { | |
const style = document.createElement('style'); | |
style.setAttribute('type', 'text/css'); | |
style.innerHTML = `<![CDATA[\n${css}\n]]>`; | |
const defs = document.createElement('defs'); | |
defs.appendChild(style); | |
clone.insertBefore(defs, clone.firstChild); | |
const outer = document.createElement('div'); | |
outer.appendChild(clone); | |
const src = outer.innerHTML.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href'); | |
if (typeof done === 'function') done(src, width, height); | |
else return { | |
src, | |
width, | |
height | |
}; | |
}); | |
} | |
}); | |
}; | |
out$.svgAsDataUri = (el, options, done) => { | |
requireDomNode(el); | |
return out$.prepareSvg(el, options) | |
.then(({ | |
src, | |
width, | |
height | |
}) => { | |
const svgXml = `data:image/svg+xml;base64,${window.btoa(reEncode(doctype+src))}`; | |
if (typeof done === 'function') { | |
done(svgXml, width, height); | |
} | |
return svgXml; | |
}); | |
}; | |
out$.svgAsPngUri = (el, options, done) => { | |
requireDomNode(el); | |
const { | |
encoderType = 'image/png', | |
encoderOptions = 0.8, | |
canvg | |
} = options || {}; | |
const convertToPng = ({ | |
src, | |
width, | |
height | |
}) => { | |
const canvas = document.createElement('canvas'); | |
const context = canvas.getContext('2d'); | |
const pixelRatio = window.devicePixelRatio || 1; | |
canvas.width = width * pixelRatio; | |
canvas.height = height * pixelRatio; | |
canvas.style.width = `${canvas.width}px`; | |
canvas.style.height = `${canvas.height}px`; | |
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); | |
if (canvg) canvg(canvas, src); | |
else context.drawImage(src, 0, 0); | |
let png; | |
try { | |
png = canvas.toDataURL(encoderType, encoderOptions); | |
} catch (e) { | |
if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name === 'SecurityError') { | |
console.error('Rendered SVG images cannot be downloaded in this browser.'); | |
return; | |
} else throw e; | |
} | |
if (typeof done === 'function') done(png, canvas.width, canvas.height); | |
return Promise.resolve(png); | |
} | |
if (canvg) return out$.prepareSvg(el, options).then(convertToPng); | |
else return out$.svgAsDataUri(el, options).then(uri => { | |
return new Promise((resolve, reject) => { | |
const image = new Image(); | |
image.onload = () => resolve(convertToPng({ | |
src: image, | |
width: image.width, | |
height: image.height | |
})); | |
image.onerror = () => { | |
reject(`There was an error loading the data URI as an image on the following SVG\n${window.atob(uri.slice(26))}Open the following link to see browser's diagnosis\n${uri}`); | |
} | |
image.src = uri; | |
}) | |
}); | |
}; | |
out$.download = (name, uri, options) => { | |
if (navigator.msSaveOrOpenBlob) navigator.msSaveOrOpenBlob(uriToBlob(uri), name); | |
else { | |
const saveLink = document.createElement('a'); | |
if ('download' in saveLink) { | |
saveLink.download = name; | |
saveLink.style.display = 'none'; | |
document.body.appendChild(saveLink); | |
try { | |
const blob = uriToBlob(uri); | |
const url = URL.createObjectURL(blob); | |
saveLink.href = url; | |
saveLink.onclick = () => requestAnimationFrame(() => URL.revokeObjectURL(url)); | |
} catch (e) { | |
console.error(e); | |
console.warn('Error while getting object URL. Falling back to string URL.'); | |
saveLink.href = uri; | |
} | |
saveLink.click(); | |
document.body.removeChild(saveLink); | |
} else if (options && options.popup) { | |
options.popup.document.title = name; | |
options.popup.location.replace(uri); | |
} | |
} | |
}; | |
out$.saveSvg = (el, name, options) => { | |
const downloadOpts = downloadOptions(); // don't inline, can't be async | |
return requireDomNodePromise(el) | |
.then(el => out$.svgAsDataUri(el, options || {})) | |
.then(uri => out$.download(name, uri, downloadOpts)); | |
}; | |
out$.saveSvgAsPng = (el, name, options) => { | |
const downloadOpts = downloadOptions(); // don't inline, can't be async | |
return requireDomNodePromise(el) | |
.then(el => out$.svgAsPngUri(el, options || {})) | |
.then(uri => out$.download(name, uri, downloadOpts)); | |
}; | |
})(); | |
document.querySelector("#export").addEventListener("click", () => saveSvgAsPng(document.querySelector("svg"), 'storage-tech.png')); | |
document.querySelectorAll("input").forEach(el => el.addEventListener("change", e => document.querySelector("svg").style.setProperty("--" + e.target.id, e.target.value))); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment