Search code examples
javascriptsvghighchartshtml2canvas

Javascript - convert HTML div with SVG to image


I use Highcharts to draw a graph.

I want to add "export to PNG" option that includes both Highcharts graph and outer div. When using Highcharts export, I'm unable to add the outer div containing the chart.

I saw several examples here using html2canvas. When I tried using it, the Highcharts' SVG element wansn't included in the output image.

Does anyone know a solution, better than trying to merge SVG image inside outer HTML div image?

Update -

Using the latest version of html2canvas fixed that issue, however the output is slightly different:

Original: enter image description here

html2canvas output:

enter image description here

As you can see some elements are rendered twice in different locations.

Anyone knows how to solve it?


Solution

  • I think you are dealing with a few edge cases of svg to bitmap conversion.

    I am not sure which method html2canvas uses, but it certainly lacks in something here.

    I wrote a function that handles some of theses edge cases that you could use before calling html2canvas, since it is able to draw bitmaps without any problem.

    // without converting the svg to png
    html2canvas(contentDiv, {
        onrendered: function(can) {
          dirty.appendChild(can);
        }
      });
      
    // first convert your svg to png
    exportInlineSVG(svg, function(data, canvas) {
      svg.parentNode.replaceChild(canvas, svg);
      // then call html2canvas
      html2canvas(contentDiv, {
        onrendered: function(can) {
          can.id = 'canvas';
          clean.appendChild(can);
        }
      });
    })
    
    
    function exportInlineSVG(svg, receiver, params, quality) {
      if (!svg || !svg.nodeName || svg.nodeName !== 'svg') {
        console.error('Wrong arguments : should be \n exportSVG(SVGElement, function([dataURL],[canvasElement]) || IMGElement || CanvasElement [, String_toDataURL_Params, Float_Params_quality])')
        return;
      }
    
      var xlinkNS = "http://www.w3.org/1999/xlink";
      var clone;
      // This will convert an external image to a dataURL
      var toDataURL = function(image) {
    
        var img = new Image();
        // CORS workaround, this won't work in IE<11
        // If you are sure you don't need it, remove the next line and the double onerror handler
        // First try with crossorigin set, it should fire an error if not needed
        img.crossOrigin = 'Anonymous';
    
        img.onload = function() {
          // we should now be able to draw it without tainting the canvas
          var canvas = document.createElement('canvas');
          var bbox = image.getBBox();
          canvas.width = bbox.width;
          canvas.height = bbox.height;
          // draw the loaded image
          canvas.getContext('2d').drawImage(this, 0, 0, bbox.width, bbox.height);
          // set our original <image>'s href attribute to the dataURL of our canvas
          image.setAttributeNS(xlinkNS, 'href', canvas.toDataURL());
          // that was the last one
          if (++encoded === total) exportDoc()
        }
    
        // No CORS set in the response		
        img.onerror = function() {
            // save the src
            var oldSrc = this.src;
            // there is an other problem
            this.onerror = function() {
                console.warn('failed to load an image at : ', this.src);
                if (--total === encoded && encoded > 0) exportDoc();
              }
              // remove the crossorigin attribute
            this.removeAttribute('crossorigin');
            // retry
            this.src = '';
            this.src = oldSrc;
          }
          // load our external image into our img
        img.src = image.getAttributeNS(xlinkNS, 'href');
      }
    
      // The final function that will export our svgNode to our receiver
      var exportDoc = function() {
          // check if our svgNode has width and height properties set to absolute values
          // otherwise, canvas won't be able to draw it
          var bbox = svg.getBBox();
          // avoid modifying the original one
          clone = svg.cloneNode(true);
          if (svg.width.baseVal.unitType !== 1) clone.setAttribute('width', bbox.width);
          if (svg.height.baseVal.unitType !== 1) clone.setAttribute('height', bbox.height);
    
          parseStyles();
    
          // serialize our node
          var svgData = (new XMLSerializer()).serializeToString(clone);
          // remember to encode special chars
          var svgURL = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgData);
    
          var svgImg = new Image();
    
          svgImg.onload = function() {
            // if we set a canvas as receiver, then use it
            // otherwise create a new one
            var canvas = (receiver && receiver.nodeName === 'CANVAS') ? receiver : document.createElement('canvas');
            // IE11 doesn't set a width on svg images...
            canvas.width = this.width || bbox.width;
            canvas.height = this.height || bbox.height;
            canvas.getContext('2d').drawImage(this, 0, 0, canvas.width, canvas.height);
    
            // try to catch IE
            try {
              // if we set an <img> as receiver
              if (receiver.nodeName === 'IMG') {
                // make the img looks like the svg
                receiver.setAttribute('style', getSVGStyles(receiver));
                receiver.src = canvas.toDataURL(params, quality);
              } else {
                // make the canvas looks like the canvas
                canvas.setAttribute('style', getSVGStyles(canvas));
                // a container element
                if (receiver.appendChild && receiver !== canvas)
                  receiver.appendChild(canvas);
                // if we set a function
                else if (typeof receiver === 'function')
                  receiver(canvas.toDataURL(params, quality), canvas);
              }
            } catch (ie) {
              console.warn("Your ~browser~ has tainted the canvas.\n The canvas is returned");
              if (receiver.nodeName === 'IMG') receiver.parentNode.replaceChild(canvas, receiver);
              else receiver(null, canvas);
            }
          }
          svgImg.onerror = function(e) {
            if (svg._cleanedNS) {
              console.error("Couldn't export svg, please check that the svgElement passed is a valid svg document.");
              return;
            }
            // Some non-standard NameSpaces can cause this issues
            // This will remove them all
            function cleanNS(el) {
              var attr = el.attributes;
              for (var i = 0; i < attr.length; i++) {
                if (attr[i].name.indexOf(':') > -1) el.removeAttribute(attr[i].name)
              }
            }
            cleanNS(svg);
            for (var i = 0; i < svg.children.length; i++)
              cleanNS(svg.children[i]);
            svg._cleanedNS = true;
            // retry the export
            exportDoc();
          }
          svgImg.src = svgURL;
        }
        // ToDo : find a way to get only usefull rules
      var parseStyles = function() {
        var styleS = [],i;
        // transform the live StyleSheetList to an array to avoid endless loop
        for (i = 0; i < document.styleSheets.length; i++)
          styleS.push(document.styleSheets[i]);
        // Do we have a `<defs>` element already ?
        var defs = clone.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
        if (!defs.parentNode)
          clone.insertBefore(defs, clone.firstElementChild);
    
        // iterate through all document's stylesheets
        for (i = 0; i < styleS.length; i++) {
          var style = document.createElement('style');
          var rules = styleS[i].cssRules,
            l = rules.length;
          for (var j = 0; j < l; j++)
            style.innerHTML += rules[j].cssText + '\n';
    
          defs.appendChild(style);
        }
        // small hack to avoid border and margins being applied inside the <img>
        var s = clone.style;
        s.border = s.padding = s.margin = 0;
        s.transform = 'initial';
      }
      var getSVGStyles = function(node) {
        var dest = node.cloneNode(true);
        svg.parentNode.insertBefore(dest, svg);
        var dest_comp = getComputedStyle(dest);
        var svg_comp = getComputedStyle(svg);
        var mods = "";
        for (var i = 0; i < svg_comp.length; i++) {
          if (svg_comp[svg_comp[i]] !== dest_comp[svg_comp[i]])
            mods += svg_comp[i] + ':' + svg_comp[svg_comp[i]] + ';';
        }
        svg.parentNode.removeChild(dest);
        return mods;
      }
    
      var images = svg.querySelectorAll('image'),
        total = images.length,
        encoded = 0;
      // Loop through all our <images> elements
      for (var i = 0; i < images.length; i++) {
        // check if the image is external
        if (images[i].getAttributeNS(xlinkNS, 'href').indexOf('data:image') < 0)
          toDataURL(images[i]);
        // else increment our counter
        else if (++encoded === total) exportDoc()
      }
      // if there were no <image> element
      if (total === 0) exportDoc();
    }
    rect {
      fill: blue;
      transform: translate(35px) rotate(45deg);
    }
    div {
      width: 250px;
      display: inline-block
    }
    #svg {
      border: 1px solid green;
    }
    #canvas { border: 1px solid red;}
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script>
    <div id="contentDiv">
      <p>Some html content</p>
    
      <svg xmlns="http://www.w3.org/2000/svg" id="svg" width="200">
        <defs>
          <filter id="Alien" color-interpolation-filters="sRGB">
            <feComponentTransfer>
              <fefuncR type="table" tablevalues="1 0 1" />
            </feComponentTransfer>
          </filter>
        </defs>
        <image filter="url(#Alien)" xlink:href="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png" width="100%" height="100%" />
        <rect x="0" y="0" width="50" height="50" />
      </svg>
    </div>
    <div id="clean">clean:<br></div>
    <div id="dirty">dirty :<br></div>

    Note :
    From this question, I started to write a full exportInlineSVG function that you can find here.