Converting SVG to PNG with JavaScript

I've been using canvg in the past to convert SVG to PNG, but as you'll see from the link, it doesn't support all of SVG capabilities. If you use D3 or any library that likes to work in SVG, then you may come across something that doesn't work - in my case it was textPath. 

SVG provides the capability for drawing text along an arbitrary path, and I needed to do this in my Chord Snip add-on, but I didn't have any way to take that generated SVG and create an image that Google Sheets could use as an embedded image. 

Ideally then, I'd like to use the browser to render the SVG instead of canvg, then somehow get at the rasterized result to make a png image out of it, then pass it to Google Sheets. Here's how.

The Dom has an API method to create an object - createObjectURL(svgtext). The API differs from browser to browser, but here's how to find it.
        var domUrl = window.URL || window.webkitURL || window;
        if (!domUrl) {
          throw new Error("(browser doesnt support this)")
        }

The idea is to create that object from the svg code, load it to an image element, then write that image to canvas. We can then use toDataURL() to make a base64 encoded PNG image and we're done. 

First step is to create the url, but that will fail unless the svg code has its namespace defined, so we'll poke the svg code
        // it needs a namespace
        if (!svgText.match(/xmlns=\"/mi)){
          svgText = svgText.replace ('<svg ','<svg xmlns="http://www.w3.org/2000/svg" ') ;  
        }

make a blob
        // make a blob from the svg
        var svg = new Blob([svgText], {
          type: "image/svg+xml;charset=utf-8"
        });

and then create the URL from the blob
   var url = domUrl.createObjectURL(svg);

next we'll need a canvas, but we need to deduce the canvas size required by digging around in the svg code.
        // figure out the height and width from svg text
        var match = svgText.match(/height=\"(\d+)/m);
        var height = match && match[1] ? parseInt(match[1],10) : 200;
        var match = svgText.match(/width=\"(\d+)/m);
        var width = match && match[1] ? parseInt(match[1],10) : 200;

now we can create a canvas element to temporarily use. I'm also going allow some margin space to be passed over as an argument, so the canvas will need to big enough for the image plus any padding round it.
        // create a canvas element to pass through
        var canvas = document.createElement("canvas");
        canvas.width = height+margin*2;
        canvas.height = width+margin*2;
        var ctx = canvas.getContext("2d");

I'll need a new image, and when it's loaded, I can write it to my canvas. As an additional piece of sugar, I'll allow the image to have an optional background fill - useful if you have a margin or transparent input. If it does have one, then I need a second canvas that can be used to styling to which the first one is written. The whole thing is wrapped in a promise, so it can be resolved passing over the toDataUrl() of the final canvas representation of the svg image.
// create a new image to hold it the converted type
        var img = new Image;
        
        // when the image is loaded we can get it as base64 url
        img.onload = function() {
          // draw it to the canvas
          ctx.drawImage(this, margin, margin);
          
          // if it needs some styling, we need a new canvas
          if (fill) {
            var styled = document.createElement("canvas");
            styled.width = canvas.width;
            styled.height = canvas.height;
            var styledCtx = styled.getContext("2d");
            styledCtx.save();
            styledCtx.fillStyle = fill;   
            styledCtx.fillRect(0,0,canvas.width,canvas.height);
            styledCtx.strokeRect(0,0,canvas.width,canvas.height);
            styledCtx.restore();
            styledCtx.drawImage (canvas, 0,0);
            canvas = styled;
          }
          // we don't need the original any more
          domUrl.revokeObjectURL(url);
          // now we can resolve the promise, passing the base64 url
          resolve(canvas.toDataURL());
        };
        
        // load the image
        img.src = url;

The whole thing can be used like this. 
svgToPng (svgCode , marginSize, canvasFillColor) 
.then (function(data) {
    // do something with the encode b64 image
})
.catch (function (err) {
  // do something with the error
});

The code

I'll report on browser compatibility as I try them out, or let me know if you try them out.
 /**
  * converts an svg string to base64 png using the domUrl
  * @param {string} svgText the svgtext
  * @param {number} [margin=0] the width of the border - the image size will be height+margin by width+margin
  * @param {string} [fill] optionally backgrund canvas fill
  * @return {Promise} a promise to the bas64 png image
  */
  var svgToPng = function (svgText, margin,fill) {
    // convert an svg text to png using the browser
    return new Promise(function(resolve, reject) {
      try {
        // can use the domUrl function from the browser
        var domUrl = window.URL || window.webkitURL || window;
        if (!domUrl) {
          throw new Error("(browser doesnt support this)")
        }
        
        // figure out the height and width from svg text
        var match = svgText.match(/height=\"(\d+)/m);
        var height = match && match[1] ? parseInt(match[1],10) : 200;
        var match = svgText.match(/width=\"(\d+)/m);
        var width = match && match[1] ? parseInt(match[1],10) : 200;
        margin = margin || 0;
        
        // it needs a namespace
        if (!svgText.match(/xmlns=\"/mi)){
          svgText = svgText.replace ('<svg ','<svg xmlns="http://www.w3.org/2000/svg" ') ;  
        }
        
        // create a canvas element to pass through
        var canvas = document.createElement("canvas");
        canvas.width = height+margin*2;
        canvas.height = width+margin*2;
        var ctx = canvas.getContext("2d");
        
        
        // make a blob from the svg
        var svg = new Blob([svgText], {
          type: "image/svg+xml;charset=utf-8"
        });
        
        // create a dom object for that image
        var url = domUrl.createObjectURL(svg);
        
        // create a new image to hold it the converted type
        var img = new Image;
        
        // when the image is loaded we can get it as base64 url
        img.onload = function() {
          // draw it to the canvas
          ctx.drawImage(this, margin, margin);
          
          // if it needs some styling, we need a new canvas
          if (fill) {
            var styled = document.createElement("canvas");
            styled.width = canvas.width;
            styled.height = canvas.height;
            var styledCtx = styled.getContext("2d");
            styledCtx.save();
            styledCtx.fillStyle = fill;   
            styledCtx.fillRect(0,0,canvas.width,canvas.height);
            styledCtx.strokeRect(0,0,canvas.width,canvas.height);
            styledCtx.restore();
            styledCtx.drawImage (canvas, 0,0);
            canvas = styled;
          }
          // we don't need the original any more
          domUrl.revokeObjectURL(url);
          // now we can resolve the promise, passing the base64 url
          resolve(canvas.toDataURL());
        };
        
        // load the image
        img.src = url;
        
      } catch (err) {
        reject('failed to convert svg to png ' + err);
      }
    });
  };

The gadget spec URL could not be found

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.






For more like this, see Google Apps Scripts snippets. Why not join our forumfollow the blog or follow me on twitter to ensure you get updates when they are available. 




Comments