title: SVG to Image export performance issues (using canvg / XMLSerializer / getComputedStyle)
body: <p>I am using canvg to convert a chart svg to an image. We have the issue that by default not all CSS attributes are applied to the image so we ended up using getComputedStyle in a loop.</p>
  
  <p>This is a total mess under performance aspects if we have 10 or even 20 charts to be exported at once.</p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>var labels = ['2018-10-01', '2018-10-02', '2018-10-03', '2018-10-04', '2018-10-05', '2018-10-06', '2018-10-07', '2018-10-08', '2018-10-09', '2018-10-10', '2018-10-11', '2018-10-12', '2018-10-13', '2018-10-14', '2018-10-15', '2018-10-16', '2018-10-17', '2018-10-18', '2018-10-19', '2018-10-20', '2018-10-21', '2018-10-22', '2018-10-23', '2018-10-24', '2018-10-25', '2018-10-26', '2018-10-27', '2018-10-28', '2018-10-29', '2018-10-30', '2018-10-31', '2018-11-01', '2018-11-02', '2018-11-03', '2018-11-04', '2018-11-05', '2018-11-06', '2018-11-07', '2018-11-08', '2018-11-09', '2018-11-10', '2018-11-11', '2018-11-12', '2018-11-13', '2018-11-14', '2018-11-15', '2018-11-16', '2018-11-17', '2018-11-18', '2018-11-19', '2018-11-20', '2018-11-21', '2018-11-22', '2018-11-23', '2018-11-24', '2018-11-25', '2018-11-26', '2018-11-27', '2018-11-28', '2018-11-29', '2018-11-30', '2018-12-01', '2018-12-02', '2018-12-03', '2018-12-04', '2018-12-05', '2018-12-06', '2018-12-07', '2018-12-08', '2018-12-09', '2018-12-10', '2018-12-11', '2018-12-12', '2018-12-13', '2018-12-14', '2018-12-15', '2018-12-16', '2018-12-17', '2018-12-18', '2018-12-19', '2018-12-20', '2018-12-21', '2018-12-22', '2018-12-23', '2018-12-24', '2018-12-25', '2018-12-26', '2018-12-27', '2018-12-28', '2018-12-29', '2018-12-30', '2018-12-31', '2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04', '2019-01-05', '2019-01-06', '2019-01-07', '2019-01-08', '2019-01-09', '2019-01-10', '2019-01-11', '2019-01-12', '2019-01-13', '2019-01-14', '2019-01-15', '2019-01-16', '2019-01-17', '2019-01-18', '2019-01-19', '2019-01-20', '2019-01-21', '2019-01-22', '2019-01-23', '2019-01-24', '2019-01-25', '2019-01-26', '2019-01-27', '2019-01-28', '2019-01-29', '2019-01-30', '2019-01-31', '2019-02-01', '2019-02-02', '2019-02-03', '2019-02-04', '2019-02-05', '2019-02-06', '2019-02-07', '2019-02-08', '2019-02-09', '2019-02-10', '2019-02-11', '2019-02-12', '2019-02-13', '2019-02-14', '2019-02-15', '2019-02-16', '2019-02-17', '2019-02-18', '2019-02-19', '2019-02-20', '2019-02-21', '2019-02-22', '2019-02-23', '2019-02-24', '2019-02-25', '2019-02-26', '2019-02-27', '2019-02-28'];
  var columns = ['data101', 'data2', 'data347'];
  var data = [
  
    [0, 0, 2, 2, 1, 2, 7, 3, 1, 7, 5, 5, 5, 5, 6, 6, 11, 7, 2, 7, 16, 7, 3, 5, 10, 9, 11, 7, 3, 7, 7, 10, 10, 9, 18, 10, 20, 13, 9, 19, 16, 13, 20, 18, 14, 15, 18, 20, 19, 11, 13, 13, 12, 16, 11, 12, 21, 20, 23, 19, 19, 23, 23, 24, 23, 25, 21, 23, 20, 22, 21, 23, 24, 25, 27, 29, 28, 25, 24, 17, 20, 24, 22, 27, 21, 27, 19, 26, 31, 27, 28, 27, 21, 20, 27, 22, 22, 19, 17, 21, 23, 19, 22, 20, 21, 25, 15, 19, 20, 19, 21, 28, 17, 20, 14, 18, 17, 20, 27, 21, 18, 18, 20, 16, 27, 16, 16, 9, 18, 8, 19, 13, 8, 16, 15, 16, 9, 15, 10, 13, 10, 11, 10, 13, 12, 7, 14, 16, 13, 14, 8],
    [0, 0, 338, 1201, 1268, 1371, 1286, 1148, 446, 288, 228, 253, 193, 201, 283, 393, 436, 379, 421, 444, 444, 417, 513, 353, 364, 399, 238, 191, 305, 337, 365, 349, 365, 244, 101, 39, 55, 72, 151, 98, 31, 127, 114, 92, 104, 196, 307, 245, 84, 168, 41, 38, 292, 488, 536, 569, 495, 448, 408, 358, 344, 380, 328, 334, 332, 330, 345, 312, 369, 377, 356, 301, 226, 273, 237, 116, 178, 133, 114, 138, 95, 143, 74, 74, 83, 47, 75, 101, 96, 59, 46, 128, 70, 57, 93, 80, 94, 93, 63, 86, 81, 63, 70, 102, 91, 67, 69, 68, 88, 76, 79, 70, 119, 88, 74, 94, 76, 54, 82, 90, 75, 130, 67, 78, 106, 91, 81, 27, 77, 21, 104, 83, 55, 60, 62, 304, 393, 191, 292, 77, 76, 55, 125, 89, 99, 127, 60, 75, 99, 120, 56],
    [0, 0, 0, 1419, 7454, 12638, 10944, 7652, 4272, 11219, 9071, 7207, 7929, 8373, 9566, 6310, 7406, 9286, 8415, 7659, 6457, 3380, 10902, 10952, 10508, 7219, 4625, 4484, 4396, 5178, 5991, 7927, 14132, 14307, 5094, 10011, 6257, 9184, 18574, 12597, 11415, 7118, 9991, 10225, 14337, 4417, 12701, 17833, 23553, 10037, 4833, 5894, 19421, 14735, 12597, 8730, 5888, 11836, 13143, 17219, 10492, 10528, 8649, 11868, 10502, 6758, 7672, 8479, 11142, 22330, 26595, 4423, 17434, 8709, 9657, 7823, 9135, 19765, 18016, 16010, 8419, 7300, 8877, 9611, 9050, 8680, 8211, 6635, 3069, 10739, 6288, 6761, 7807, 16243, 20415, 23051, 19727, 8721, 6445, 8585, 13688, 14728, 17113, 16255, 3898, 4622, 3869, 3774, 4190, 3461, 4824, 4608, 4613, 3677, 3648, 3575, 3556, 4036, 3732, 2517, 4676, 4129, 3250, 4142, 3987, 4396, 3362, 2964, 1849, 2609, 2851, 3003, 3583, 3473, 3190, 2658, 4363, 3959, 4588, 3771, 4315, 3178, 3354, 3159, 2695, 4114, 4292, 3322, 1218, 3526, 3717]
  ];
  var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
  var padding = 5;
  
  //prepare chart data
  var columnData = [];
  var chartDataColumns = [];
  var chartData = [];
  
  chartData.push([columns[0]].concat(data[0]));
  
  chartDataColumns = [
    ['x'].concat(labels)
  ].concat(chartData);
  
  var chart1 = c3.generate({
    bindto: d3.select('#chart1'),
    data: {
      x: 'x',
      columns: [['x'].concat(labels)].concat(chartData),
      type: 'line',
      onmouseover: function(d) {
        chart1.focus(d.id);
        chart2.focus(d.id);
      },
      onmouseout: function() {
        chart1.revert();
        chart2.revert();
      }
    },
    legend: {
      position: 'right',
      show: true,
      item: {
        onclick: function(id) {
          if (chart1) chart1.toggle(id);
          if (chart2) chart2.toogle(id);
        },
        onmouseover: function(id) {
          if (chart1) chart1.focus(id);
          if (chart2) chart2.focus(id);
        },
        onmouseout: function(id) {
          if (chart1) chart1.revert();
          if (chart2) chart2.revert();
        }
      }
    },
    tooltip: {
      show: true,
      format: {
        value: function(value) {
          return d3.format(",.0f")(value);
        }
      }
    },
    zoom: {
      enabled: true
    },
    axis: {
      x: {
        type: 'timeseries',
        tick: {
          rotate: 90,
          format: '%Y-%m-%d'
        }
      },
      y: {
        label: 'sample-data',
        tick: {
          format: d3.format(",")
        }
      }
    },
    color: {
      pattern: colors
    }
  });
  
  
  var chart2 = c3.generate({
    bindto: d3.select('#chart2'),
    data: {
      columns: [[columns[0]].concat(data[0])],
      type: 'pie',
      onmouseover: function(id) {
        if (chart1) chart1.focus(id);
        if (chart2) chart2.focus(id);
      },
      onmouseout: function(id) {
        if (chart1) chart1.revert();
        if (chart2) chart2.revert();
      }
    },
    legend: {
      position: 'right',
      show: true,
      item: {
        onclick: function(id) {
          if (chart1) chart1.toggle(id);
          if (chart2) chart2.toogle(id);
        },
        onmouseover: function(id) {
          if (chart1) chart1.focus(id);
          if (chart2) chart2.focus(id);
        },
        onmouseout: function(id) {
          if (chart1) chart1.revert();
          if (chart2) chart2.revert();
        }
      }
  
    },
    color: {
      pattern: colors
    },
  });
  
  
  
  for (var i = 1; i &lt; columns.length; i++) {
    setTimeout(function(column) {
      chart1.load({
        columns: [
          [columns[column]].concat(data[column])
        ]
      });
      chart2.load({
        columns: [[columns[column]].concat(data[column])]
      });
  
    }, (i * 5000 / columns.length), i);
  }
  
  document.getElementById("exportButton").onclick = function() {
    exportChartToImage();
  };
  
  function exportChartToImage() {
    var createImagePromise = new Promise(function(resolve, reject) {
      var images = [];
      d3.selectAll('svg').each(function() {
        if (this.parentNode) {
          images.push(getSvgImage(this.parentNode, true));
        }
      });
      if (images.length &gt; 0)
        resolve(images);
      else
        reject(images);
    });
    createImagePromise.then(function(images) {
        images.forEach(function(img, n) {
          img.toBlob(function(blob) {
            saveAs(blob, "image_" + (n + 1) + ".png");
          });
        });
      })
      .catch(function(error) {
        throw error;
      });
  };
  
  /**
   * Converts a SVG-Chart to a canvas and returns it.
   */
  function getSvgImage(svgContainer, png) {
    var svgEl = d3.select(svgContainer).select('svg').node();
    var svgCopyEl = svgEl.cloneNode(true);
  
  
    if (!svgCopyEl)
      return;
    //remove elements not for printing
    lensObject = d3.selectAll(".hidden-print").remove().exit();
  
    //add temp document objects
    var emptySvgEl = d3.select(document.createElementNS("http://www.w3.org/2000/svg", "svg")).attr("id", "emptysvg")
      .attr("version", 1.1)
      .attr("height", 2)
      .node();
    var canvasComputed = d3.select(document.createElement("canvas")).attr("id", "canvasComputed").node();
  
    var container = d3.select(document.createElement("div")).attr("style", "display: none;")
      .attr("class", "c3").node();
    svgContainer.append(container);
    container.append(svgCopyEl);
    container.append(emptySvgEl);
    container.append(canvasComputed);
  
    //apply all CSS styles to SVG
    exportStyles(svgCopyEl, emptySvgEl);
  
    // transform SVG to canvas using external canvg
    canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
  
    //remove temp document objects
    canvasComputed.remove();
    emptySvgEl.remove();
    svgCopyEl.remove();
  
    container.remove();
  
    return canvasComputed;
  }
  
  function exportStyles(svg, emptySvg) {
    var tree = [];
    var emptySvgDeclarationComputed = getComputedStyle(emptySvg);
    //d3.select(svg).selectAll().each(function() {
    $(svg).find("*").each(function() {
      explicitlySetStyle(this, emptySvgDeclarationComputed);
    });
  }
  
  
  function traverse(obj, tree) {
    tree.push(obj);
    if (obj.hasChildNodes()) {
      var child = obj.firstChild;
      while (child) {
        if (child.nodeType === 1 &amp;&amp; child.nodeName != 'SCRIPT') {
          traverse(child, tree);
        }
        child = child.nextSibling;
      }
    }
    return tree;
  }
  
  function explicitlySetStyle(element, emptySvgDeclarationComputed) {
    var cSSStyleDeclarationComputed = getComputedStyle(element);
    var i, len, key, value;
    var computedStyleStr = "";
    for (i = 0, len = cSSStyleDeclarationComputed.length; i &lt; len; i++) {
      key = cSSStyleDeclarationComputed[i];
      value = cSSStyleDeclarationComputed.getPropertyValue(key);
      if (value !== emptySvgDeclarationComputed.getPropertyValue(key)) {
        if (key == 'visibility' &amp;&amp; value == 'hidden') {
          computedStyleStr += 'display: none;';
        } else {
          computedStyleStr += key + ":" + value + ";";
        }
      }
    }
    element.setAttribute('style', computedStyleStr);
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" /&gt;
  &lt;script src="https://d3js.org/d3.v5.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"&gt;&lt;/script&gt;
  &lt;!-- Required to convert named colors to RGB --&gt;
  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"&gt;&lt;/script&gt;
  &lt;!-- Optional if you want blur --&gt;
  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"&gt;&lt;/script&gt;
  &lt;!-- Main canvg code --&gt;
  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.js"&gt;&lt;/script&gt;
  &lt;script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"&gt;&lt;/script&gt;
  
  &lt;div id="chart1" class "c3"&gt;
  
  &lt;/div&gt;
  
  &lt;div id="chart2" class="c3"&gt;
  
  &lt;/div&gt;
  
  &lt;button type="button" id="exportButton"&gt;
  export to SVG
  &lt;/button&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p>=> <a href="http://jsfiddle.net/gothmogg/jLt3yq75/" rel="nofollow noreferrer">http://jsfiddle.net/gothmogg/jLt3yq75/</a></p>
  
  <p>This sample shows exemplary how we apply the CSS styles to the SVG. If we use normal canvg without getComputedStyle the x and y axes of the c3 chart look like a total mess...</p>
  
  <p>Do you know faster way to get valid images?
  Is there a way to filter the CSS-styles? Maybe use "c3" styles only?</p>