10 Comments

Saving Browser-based SVGs as Images

svg-to-png

I’ve spent a lot of time with D3 over the past several months, and while I’ve enjoyed it immensely, one thing had eluded me: saving visualizations as images. When I needed to turn a browser-drawn SVG (scalable vector graphic) into an image, I used FileSaver.js to save the content of the webpage’s SVG node as a file, then opened the file in Inkscape and rasterized to an image format. Unfortunately, this process often created discrepancies between how the visualization looked in the browser and how it looked in the final image.

While making a calendar as a gift over the holidays, I finally figured out how to save SVG content as an image directly from the browser. Not only is the workflow a lot smoother, but the results are much more consistent than processing the SVG through Inkscape. The core code for the process is laid out below, and the complete script is on GitHub.

1. Get the SVG Code

Saving a browser-drawn SVG as an image requires several steps, but they’re all reasonably straight-forward. After generating the visualization, we can base-64 encode the SVG and build a data URI to use as the source of an image element. The image, in turn, can be drawn to a canvas, from which the data URI for a PNG image can be generated and a PNG file downloaded.

Having drawn an SVG in the browser, whether by hand, with D3, or using some other library, we need the XML code from the SVG element. While browser support for the .innerHTML attribute is common, what we really need is the much less commonly supported .outerHTML attribute, which includes the HTML code of the SVG element itself. We can use .innerHTML to get the outer HTML of a given element by cloning the node and adding it to another element:

1
2
3
4
5
function outerHTML(el) {
  var outer = document.createElement('div');
  outer.appendChild(el.cloneNode(true));
  return outer.innerHTML;
}

If we save the SVG code at this point, we won’t have any of the styling defined in stylesheets. We need to include the CSS styles from the webpage. It is possible to inline the styles on the individual elements, but SVGs also allow a <style> tag where CSS rules can be defined. I prefer to loop through all the CSS rules on the page and include only the ones with matching elements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function styles(dom) {
  var used = "";
  var sheets = document.styleSheets;
  for (var i = 0; i < sheets.length; i++) {
    var rules = sheets[i].cssRules;
    for (var j = 0; j < rules.length; j++) {
      var rule = rules[j];
      if (typeof(rule.style) != "undefined") {
        var elems = dom.querySelectorAll(rule.selectorText);
        if (elems.length > 0) {
          used += rule.selectorText + " { " + rule.style.cssText + " }\n";
        }
      }
    }
  }
 
  var s = document.createElement('style');
  s.setAttribute('type', 'text/css');
  s.innerHTML = "<![CDATA[\n" + used + "\n]]>";
 
  var defs = document.createElement('defs');
  defs.appendChild(s);
  dom.insertBefore(defs, dom.firstChild);
}

The styles are wrapped in the CDATA construct so any angle brackets in the CSS code do not confuse the XML parser.

2. Add Some Boilerplate

With your page’s styles included, you can save the SVG to a file and open it in Inkscape with decent results. However, to load the SVG as a browser image we need to add the XML version and doctype. First, prepend the following:

1
2
<?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">;

Then add the version, xmlns, and xmlns:xlink attributes to the SVG node.

1
2
3
4
5
function setAttributes(el) {
  el.setAttribute("version", "1.1");
  el.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  el.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
}

3. Convert to a Data URI

Mike Bostock’s instructions for creating an SVG fallback image shows how to use a saved SVG file to download a PNG image. Rather than saving an SVG file only to load it back into the browser, we can create a data URI of the SVG and use it as the source for the image instead.

1
2
3
4
function svgImage(xml) {
  var image = new Image();
  image.src = 'data:image/svg+xml;base64,' + window.btoa(xml);
}

To support Unicode characters, we need to amend the above snippet to do a bit more work:

1
window.btoa(unescape(encodeURIComponent(xml)))

4. Convert to an Image

To save an image file from our image tag, we need to create a canvas of the right size, draw the image to it, get a data URI of the canvas’s contents, then set the data URI as the destination of a link and click the link.

1
2
3
4
5
6
7
8
9
10
11
12
13
image.onload = function() {
  var canvas = document.createElement('canvas');
  canvas.width = image.width;
  canvas.height = image.height;
  var context = canvas.getContext('2d');
  context.drawImage(image, 0, 0);
 
  var a = document.createElement('a');
  a.download = "image.png";
  a.href = canvas.toDataURL('image/png');
  document.body.appendChild(a);
  a.click();
}

Celebrate!

There you have it! Put all the steps together, and we can download an SVG drawn in the browser as a PNG image. To rasterize the SVG to a higher resolution, we can scale the SVG node’s width and height attributes and wrap the contents of the SVG node in a <g> element with a transform attribute to scale the drawing by some multiplier.

One stumbling block I discovered is the use of images within the SVG. To include them in the rasterized image, they need to be inlined as data URIs. Even if you only want to save the SVG code as an SVG file, it can be helpful to inline images using data URIs as it makes the SVG self-contained rather than relying on external files.

The full, functioning script is on GitHub. You can drop it into a webpage, call the saveSvgAsPng function, and it will download an image of the SVG you see in your browser. The script is the product of my own tinkering, and far from universally tested, so if you run into problems, be sure to let me know.

If you have any other tricks for SVG-to-PNG conversion or otherwise getting SVGs out of the browser, I’d love to hear them!