We're hiring!

We're actively seeking designers and developers for all three of our locations.

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:

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.

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:

<?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.

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.

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:

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.

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 getting SVGs out of the browser, I’d love to hear them!
 

This entry was posted in Web Apps and tagged , . Bookmark the permalink. Both comments and trackbacks are currently closed.

10 Comments

  1. Sip
    Posted May 5, 2014 at 8:52 am

    Only left corner part of the chart is coming when i am trying for svg export
    here is my code:

    saveSvgAsPng(document.getElementById(“chart-svg”), “diagram.png”, 3);

  2. Kimberly Nicholls
    Posted June 19, 2014 at 3:41 pm

    Thanks, this was very helpful. I found all sorts of much more complicated ways to do it, so I really like the simplicity.

  3. Amirali
    Posted June 24, 2014 at 9:27 pm

    Looks great, but could not get it to work in my case. I have some linked images in my SVG code (png and svg files, less than 50KB) and your code seems to stop recreating them in canvas. I have also used markers on my paths. is it able to reproduce those as well?

    Thanks again.

    • Eric Shull
      Posted June 25, 2014 at 9:43 am

      It’s true: the code above doesn’t go into being able to save images in the SVG, but if you look at the GitHub repo (specifically here: https://github.com/exupero/saveSvgAsPng/blob/gh-pages/saveSvgAsPng.js#L6-L30) there’s some code for inlining images that you can use before putting the SVG on a canvas an exporting as a PNG. I can’t guarantee that it works in all cases, but it’s worked for me so far.

      The only path markers I’ve tried exporting are end markers, but those worked fine. If you’re seeing something that isn’t exported correctly, feel free to file an issue on GitHub (https://github.com/exupero/saveSvgAsPng/issues?state=open) with some sample SVG code.

      • Amirali
        Posted June 27, 2014 at 10:20 am

        Thanks for the reply.
        I was actually talking about your latest script on github, updated 3 days ago. I now tried it with multiple linked png and svg images. what I can see in the png outcome is only the enlarged linked png that is used inside the svg code and not the rest. here is my svg:

        text

        text

        The 2 linked images are here:
        png : http://i.imgur.com/bV5qrz3.png
        svg: http://upload.wikimedia.org/wikipedia/commons/3/30/Vector-based_example.svg

        • Eric Shull
          Posted June 27, 2014 at 10:27 am

          It’s possible I’ve broken that functionality. I’m in the process of adding tests to make sure the script doesn’t break. If you could, create an issue on the GitHub repository with your example code (especially since example code doesn’t appear in our blog’s comments).

      • Amirali
        Posted June 27, 2014 at 10:27 am

        Apparently the code vanished from the comment. here it is:
        http://jsfiddle.net/vYZ3y/2/

        I realized that saving svg with online image sources linked, will result in a security error by your script. therefore please try it with offline copies.

  4. Peter
    Posted July 10, 2014 at 12:38 pm

    I downloaded your script and tried it, but I’ve got a problem… Chrome says:

    Uncaught SyntaxError: Unexpected token ILLEGAL saveSvgAsPng.js:1

    If I click on saveSvgAsPng.js:1, Chrome does show me lots of Chinese characters.

    • Eric Shull
      Posted July 10, 2014 at 12:51 pm

      If you would, post an issue on GitHub (https://github.com/exupero/saveSvgAsPng/issues) with a screenshot of what you’re seeing.

    • Peter
      Posted July 10, 2014 at 1:28 pm

      I opened your script file with Notepad and then saved it in Unicode format. Now its working fine.
      Strange is, that your demo page (index.html) did work before I did this…