JavaScriptでスタイルがcssに定義されているsvgにスタイルを適用しながらいい感じにpngに変換した

# 経緯
最近javascriptを勉強しています。
svgのグラフをpngに変換したいと思ったら以外と単純ではなかったので書きました。

# 概要
svgのスタイルが外部に適用されているので子要素を全探索し逐一割り当てられているスタイルを計算し直して適用させてpngに変換した。

# 環境
グラフは[c3.js](https://c3js.org/)で書きます。
c3.jsは簡単にチャートが作れるライブラリです。

<details><summary>今回使ったコードまとめました</summary>
<div>

```html

<!-- Load c3.css -->
<link href="./c3-0.7.2/c3.css" rel="stylesheet">

<div id="chart"></div>
<div id="hoge">
  <p>↓画像</p>
</div>

<!-- Load d3.js and c3.js -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="./c3-0.7.2/c3.min.js"></script>
<script>
var chart = c3.generate({
    bindto: '#chart',
    data: {
      columns: [
        ['data1', 30, 200, 100, 400, 150, 250],
        ['data2', 50, 20, 10, 40, 15, 25]
      ]
    }
});

function svg2png1(svgElement) {
  const svgData = new XMLSerializer().serializeToString(svgElement);
  const canvas = document.createElement("canvas");
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;

  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.onload = function(){
      ctx.drawImage( image, 0, 0 );
      document.getElementById('hoge').appendChild(canvas)
  }
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
}

function svg2png2(svgElement) {
  const st = performance.now()
  svgElement.version = 1.1
  svgElement.xmlns = 'http://www.w3.org/2000/svg'

  const queue = []
  queue.push(svgElement)
  while (queue.length != 0) {
    const element = queue.pop()

    const computedStyle = window.getComputedStyle(element, '')
    for (let property of computedStyle) {
      element.style[property] = computedStyle.getPropertyValue(property)
    }

    const children = element.children

    for (let child of children) {
      queue.push(child)
    }
  }
  const svgData = new XMLSerializer().serializeToString(svgElement);

  const canvas = document.createElement("canvas");
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;

  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.onload = () => {
      ctx.drawImage( image, 0, 0 );
      document.getElementById('hoge').appendChild(canvas)
      console.log(performance.now() - st)
  }
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
}

function svg2png3(svgElement) {
  const st = performance.now()
  const virtualSvg = svgElement.cloneNode(false) // 子要素は複製しない(自分で追加するため)
  virtualSvg.version = 1.1
  virtualSvg.xmlns = 'http://www.w3.org/2000/svg'

  const queue = []
  queue.push([svgElement, virtualSvg])
  while (queue.length != 0) {
    const pair = queue.pop()
    const rEle = pair[0]
    const vEle = pair[1]

    const computedStyle = window.getComputedStyle(rEle, '')
    for (let property of computedStyle) {
      vEle.style[property] = computedStyle.getPropertyValue(property)
    }

    const rChildren = rEle.children
    if (rChildren.length !== 0) {
      for (let rChild of rChildren) {
        const vChild = rChild.cloneNode(false)
        vEle.appendChild(vChild)
        queue.push([rChild, vChild])
      }
    } else {
      vEle.innerHTML = rEle.innerHTML
    }
  }

  const svgData = new XMLSerializer().serializeToString(virtualSvg)
  const canvas = document.createElement("canvas");
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;

  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.onload = function(){
    ctx.drawImage( image, 0, 0 );
    document.getElementById('hoge').appendChild(canvas)
    console.log(performance.now() - st)
  }
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
}
window.setTimeout(() => {svg2png(document.getElementsByTagName('svg')[0])}, 2000)
</script>

```

</div>
</details>

今回は、c3で描画した↓のsvgのグラフをpngに変換します。
<img width="829" alt="Screen Shot 2019-07-13 at 19.56.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285922/dcd4b30f-7260-b7b1-c36f-9e005c0c5f27.png">

(↓画像　の下にpngが描画される予定です。)

# アプローチ1

[XMLSerializer.serializeToString()](https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer/serializeToString)を使います。
こんな感じです。

```javascript

function svg2png1(svgElement) {
  const svgData = new XMLSerializer().serializeToString(svgElement);
  const canvas = document.createElement("canvas");
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;

  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.onload = function(){
      ctx.drawImage( image, 0, 0 );
      document.getElementById('hoge').appendChild(canvas)
  }
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
}

window.setTimeout(() => {svg2png1(document.getElementsByTagName('svg')[0])}, 2000) // c3の描画が終わるまで雑に待つ

```

するとこうなります。
<img width="829" alt="Screen Shot 2019-07-13 at 19.59.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285922/0a59a068-ec16-9ca0-f676-6639b39e014b.png">

単純にsvgの要素を見ただけではcssで定義されたスタイルを読み込めていないので、これを適用させる必要があると判断しました。

#アプローチ2

[window.getComputedStyle
](https://developer.mozilla.org/ja/docs/Web/API/Window/getComputedStyle)という、
要素に適用されたスタイルを計算し直して値にしてくれる関数があるようなので、これを子要素全てに適用すればいいのではないかと思いました。

なのでDOMツリーをたどって全子要素にスタイルを適用し直します。
(幅優先です。)

```javascript

function svg2png2(svgElement) {
  const st = performance.now()
  svgElement.version = 1.1
  svgElement.xmlns = 'http://www.w3.org/2000/svg'

  const queue = []
  queue.push(svgElement)
  while (queue.length != 0) {
    const element = queue.pop()

    const computedStyle = window.getComputedStyle(element, '')
    for (let property of computedStyle) {
      element.style[property] = computedStyle.getPropertyValue(property)
    }

    const children = element.children

    for (let child of children) {
      queue.push(child)
    }
  }
  const svgData = new XMLSerializer().serializeToString(svgElement);

  const canvas = document.createElement("canvas");
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;

  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.onload = () => {
      ctx.drawImage( image, 0, 0 );
      document.getElementById('hoge').appendChild(canvas)
      console.log(performance.now() - st)
  }
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
}

window.setTimeout(() => {svg2png2(document.getElementsByTagName('svg')[0])}, 2000) // c3の描画が終わるまで雑に待つ

```

こうなります。
<img width="824" alt="Screen Shot 2019-07-13 at 20.06.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285922/be386415-57f0-a19d-c843-4432ca616240.png">

大丈夫そうですね。
しかし実はこの処理に***2576ms***程かかってます。超重いです。
この単純なグラフでこれだけかかるというのは使い物になりません。
そもそも、スタイルを全部適用して描画しているから遅いと判断しました。

# アプローチ3

なのでcloneNodeでコピーを複製し、スタイルを適用するのはそっちにします。

```javascript

function svg2png3(svgElement) {
  const st = performance.now()
  const virtualSvg = svgElement.cloneNode(false) // 子要素は複製しない(自分で追加するため)
  virtualSvg.version = 1.1
  virtualSvg.xmlns = 'http://www.w3.org/2000/svg'

  const queue = []
  queue.push([svgElement, virtualSvg])
  while (queue.length != 0) {
    const pair = queue.pop()
    const rEle = pair[0]
    const vEle = pair[1]

    const computedStyle = window.getComputedStyle(rEle, '')
    for (let property of computedStyle) {
      vEle.style[property] = computedStyle.getPropertyValue(property)
    }

    const rChildren = rEle.children
    if (rChildren.length !== 0) {
      for (let rChild of rChildren) {
        const vChild = rChild.cloneNode(false)
        vEle.appendChild(vChild)
        queue.push([rChild, vChild])
      }
    } else {
      vEle.innerHTML = rEle.innerHTML
    }
  }

  const svgData = new XMLSerializer().serializeToString(virtualSvg)
  const canvas = document.createElement("canvas");
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;

  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.onload = function(){
    ctx.drawImage( image, 0, 0 );
    document.getElementById('hoge').appendChild(canvas)
    console.log(performance.now() - st)
  }
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
}
window.setTimeout(() => {svg2png3(document.getElementsByTagName('svg')[0])}, 2000)

```

結果
<img width="826" alt="Screen Shot 2019-07-13 at 20.10.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285922/fa0ece21-84f7-6621-e4cc-4f2303cdc954.png">

***299ms***でした。
まだ早いとは言えませんが、元の要素に直接スタイルを適用させなくなったのでDOMの更新が走らず処理が早くなったと思います。

これも結局子要素が増えたら時間がかかるのですが、現状はこれで満足しています。
これを高速化するとなると適用させる要素を取捨選択しなくてはいけないのでしょうか？
そもそも他にいいやり方があったら教えてください。