This little gist is used to demonstrate how svg-crowbar can be refactored for
use beyond a bookmarklet.
Click on the download png
button to download a PNG image that is styled from
stylesheets rather than attributes on the SVG elements themselves.
Last active
March 28, 2022 19:45
-
-
Save deanmalmgren/22d76b9c1f487ad1dde6 to your computer and use it in GitHub Desktop.
example of how to export a png directly from an svg
This file contains 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
<html> | |
<head> | |
<!-- this styling is added to the png when it is downloaded --> | |
<style> | |
circle {fill: red; stroke: blue; stroke-width: 3px;} | |
#crowbar-workspace {display: none;} | |
</style> | |
</head> | |
<body> | |
<!-- | |
this is the simple svg that is downloaded. note that it has no styling | |
--> | |
<svg id="export-me" width="100" height="100"> | |
<circle r="10" cx="50" cy="50"></circle> | |
</svg> | |
<button>download png</button> | |
<!-- | |
this is used to download content dynamically from the client side. Note | |
that this div is, by default, not visible with the styling above. | |
--> | |
<div id="crowbar-workspace"> | |
</div> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js"></script> | |
<script type="text/javascript" src="svg-crowbar-export.js"></script> | |
<script> | |
d3.select("button").on("click", download_png) | |
function download_png () { | |
// this is a shitty hack that should probably be embedded in the | |
// svg_crowbar function | |
var svg_el = d3.select("svg") | |
.attr("version", 1.1) | |
.attr("xmlns", "http://www.w3.org/2000/svg") | |
.node(); | |
// this is the main thing that does the work | |
svg_crowbar(d3.select("#export-me").node(), { | |
filename: "export-me.png", | |
width: 100, | |
height: 100, | |
crowbar_el: d3.select("#crowbar-workspace").node(), | |
}) | |
} | |
</script> | |
</body> | |
</html> |
This file contains 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
var svg_crowbar = function (svg_el, options){ | |
// TODO: should probably do some checking to make sure that svg_el is | |
// actually a <svg> and throw a friendly error otherwise | |
// get options passed to svg_crowbar | |
var filename = options.filename || "download.png"; | |
var width = options.width; // TODO: add fallback value based on svg attributes | |
var height = options.height; // TODO: add fallback value based on svg attributes | |
var crowbar_el = options.crowbar_el; // TODO: element for preparing the canvas element | |
// apply the stylesheet to the svg to be sure to capture all of the stylings | |
applyStylesheets(svg_el) | |
// grab the html from the svg and encode the svg in a data url | |
var html = svg_el.outerHTML; | |
var imgsrc = 'data:image/svg+xml;base64,' + btoa(html); | |
// create a canvas element that has the right dimensions | |
crowbar_el.innerHTML = ( | |
'<canvas width="' + width + '" height="' + height + '"></canvas>' | |
) | |
var canvas = crowbar_el.querySelector("canvas"); | |
var context = canvas.getContext("2d"); | |
var image = new Image; | |
image.src = imgsrc; | |
image.onload = function() { | |
// draw the image in the context of the canvas and then get the | |
// image data from the canvas | |
// | |
// TODO: the resulting canvas image is a little on the grainy side. | |
// up until this point the image is lossless, so it definitely has | |
// something to do with the imgsrc getting lost when embedding in | |
// the canvas. this appears to be a problem with just about | |
// anything i've seen | |
context.drawImage(image, 0, 0); | |
var canvasdata = canvas.toDataURL("image/png"); | |
// download the data | |
var a = document.createElement("a"); | |
a.download = filename; | |
a.href = canvasdata; | |
a.click(); | |
}; | |
// this is adapted (barely) from svg-crowbar | |
// https://github.com/NYTimes/svg-crowbar/blob/gh-pages/svg-crowbar-2.js#L211-L250 | |
function applyStylesheets(svgEl) { | |
// use an empty svg to compute the browser applied stylesheets | |
var emptySvg = window.document.createElementNS("http://www.w3.org/2000/svg", 'svg'); | |
window.document.body.appendChild(emptySvg); | |
var emptySvgDeclarationComputed = getComputedStyle(emptySvg); | |
emptySvg.parentNode.removeChild(emptySvg); | |
// traverse the element tree and explicitly set all stylesheet values | |
// on an element. this is ripped from svg-crowbar | |
var allElements = traverse(svgEl); | |
var i = allElements.length; | |
while (i--){ | |
explicitlySetStyle(allElements[i], emptySvgDeclarationComputed); | |
} | |
} | |
function explicitlySetStyle (element, emptySvgDeclarationComputed) { | |
var cSSStyleDeclarationComputed = getComputedStyle(element); | |
var i, len, key, value; | |
var computedStyleStr = ""; | |
for (i=0, len=cSSStyleDeclarationComputed.length; i<len; i++) { | |
key=cSSStyleDeclarationComputed[i]; | |
value=cSSStyleDeclarationComputed.getPropertyValue(key); | |
if (value!==emptySvgDeclarationComputed.getPropertyValue(key)) { | |
computedStyleStr+=key+":"+value+";"; | |
} | |
} | |
element.setAttribute('style', computedStyleStr); | |
} | |
// traverse an svg and append all of the elements to the tree array. This | |
// ignores some elements that can appear in <svg> elements but whose | |
// children's styles should not be tweaked | |
function traverse(obj){ | |
var tree = []; | |
var ignoreElements = { | |
'script': undefined, | |
'defs': undefined, | |
}; | |
tree.push(obj); | |
visit(obj); | |
function visit(node) { | |
if (node && node.hasChildNodes() && !(node.nodeName.toLowerCase() in ignoreElements)) { | |
var child = node.firstChild; | |
while (child) { | |
if (child.nodeType === 1) { | |
tree.push(child); | |
visit(child); | |
} | |
child = child.nextSibling; | |
} | |
} | |
} | |
return tree; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is very cool and works great except for one thing: applyStylesheets breaks my existing SVG's pan and zoom that's provided by the D3 library. I have to manually destroy and rebuild the SVG to get everything back again. Of course, this eradicates the user's current pan / zoom location. (Using Angular v13)