javascripthtmlcsssvgsave

javascript: save <svg> element to file on disk


On my HTML i have an SVG element. It is rendered with d3js and has stylings applied in CSS.

When i right click in my browser i can select "Save image". This actions saves the image as rendered with all the css stylings applied.

I have been searching for a good way to save the file

However when i get the file to disk the extra stylings from my css is not applied to the saved image.

Question: How can i save my SVG as rendered in the browser with css applied.


Solution

  • The CSS parsing is not an easy task, CSS rules are complicated...

    I tried to write something for my SVG2Bitmap little script, but it is still far from being perfect...

    Basically, it parses all the stylesheets in the document, and check if any of the svg's nodes match the rule (thanks to querySelector and Element.matches() methods).

    The problem is that once appended into the svg doc, the rules may not match anymore (e.g body>svg>rect will fail). I still didn't find an elegant way to deal with it, also if anyone has one, please let me know.

    An other issue I faced is that invalid rules will make previously mentioned methods to throw an error. This shouldn't be too much of a concern, but some browsers (Chrome to not tell its name) accept some hacky rules like [xlink\\:href] but save it in the cssRules as [xlink\:href] which will fail and thus throw an error...


    The "save as file" part however is way easier thanks to the XMLSerializer object, which will let the browser create a standalone version of what it parsed, with all that is needed.

    To make a 100% valid svg file, you'll also need to set a Doctype at top of your document.

    So let's jump in the code :

    var exportSVG = function(svg) {
      // first create a clone of our svg node so we don't mess the original one
      var clone = svg.cloneNode(true);
      // parse the styles
      parseStyles(clone);
    
      // create a doctype
      var svgDocType = document.implementation.createDocumentType('svg', "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd");
      // a fresh svg document
      var svgDoc = document.implementation.createDocument('http://www.w3.org/2000/svg', 'svg', svgDocType);
      // replace the documentElement with our clone 
      svgDoc.replaceChild(clone, svgDoc.documentElement);
      // get the data
      var svgData = (new XMLSerializer()).serializeToString(svgDoc);
    
      // now you've got your svg data, the following will depend on how you want to download it
      // e.g yo could make a Blob of it for FileSaver.js
      /*
      var blob = new Blob([svgData.replace(/></g, '>\n\r<')]);
      saveAs(blob, 'myAwesomeSVG.svg');
      */
      // here I'll just make a simple a with download attribute
    
      var a = document.createElement('a');
      a.href = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgData.replace(/></g, '>\n\r<'));
      a.download = 'myAwesomeSVG.svg';
      a.innerHTML = 'download the svg file';
      document.body.appendChild(a);
    
    };
    
    var parseStyles = function(svg) {
      var styleSheets = [];
      var i;
      // get the stylesheets of the document (ownerDocument in case svg is in <iframe> or <object>)
      var docStyles = svg.ownerDocument.styleSheets;
    
      // transform the live StyleSheetList to an array to avoid endless loop
      for (i = 0; i < docStyles.length; i++) {
        styleSheets.push(docStyles[i]);
      }
    
      if (!styleSheets.length) {
        return;
      }
    
      var defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
      if (!defs.parentNode) {
        svg.insertBefore(defs, svg.firstElementChild);
      }
      svg.matches = svg.matches || svg.webkitMatchesSelector || svg.mozMatchesSelector || svg.msMatchesSelector || svg.oMatchesSelector;
    
    
      // iterate through all document's stylesheets
      for (i = 0; i < styleSheets.length; i++) {
        var currentStyle = styleSheets[i]
    
        var rules;
        try {
          rules = currentStyle.cssRules;
        } catch (e) {
          continue;
        }
        // create a new style element
        var style = document.createElement('style');
        // some stylesheets can't be accessed and will throw a security error
        var l = rules && rules.length;
        // iterate through each cssRules of this stylesheet
        for (var j = 0; j < l; j++) {
          // get the selector of this cssRules
          var selector = rules[j].selectorText;
          // probably an external stylesheet we can't access
          if (!selector) {
            continue;
          }
    
          // is it our svg node or one of its children ?
          if ((svg.matches && svg.matches(selector)) || svg.querySelector(selector)) {
    
            var cssText = rules[j].cssText;
            // append it to our <style> node
            style.innerHTML += cssText + '\n';
          }
        }
        // if we got some rules
        if (style.innerHTML) {
          // append the style node to the clone's defs
          defs.appendChild(style);
        }
      }
    
    };
    
    exportSVG(document.getElementById('mySVG'));
    svg >rect {
      fill: yellow
    }
    /* this will fail, it could work with a check for document.querySelector instead of svg.querySelector, but that would just be a kill for performances with a lot of cssRules..., and would need to set the elements' style attribute instead of using a <style> tag */
    
    body > svg >rect {
      stroke: red
    }
    <svg width="120" height="120" viewBox="0 0 120 120" id="mySVG">
      <rect x="10" y="10" width="100" height="100" />
    </svg>

    Ps : the download part of this snippet won't work in FF, you can try it in this fiddle though.