Skip to content

Instantly share code, notes, and snippets.

@deanmalmgren
Last active March 28, 2022 19:45
Show Gist options
  • Save deanmalmgren/22d76b9c1f487ad1dde6 to your computer and use it in GitHub Desktop.
Save deanmalmgren/22d76b9c1f487ad1dde6 to your computer and use it in GitHub Desktop.
example of how to export a png directly from an svg
<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>
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;
}
}
@newclique
Copy link

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment