4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-13

経緯

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

概要

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

環境

グラフはc3.jsで書きます。
c3.jsは簡単にチャートが作れるライブラリです。

今回使ったコードまとめました

<!-- 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>

今回は、c3で描画した↓のsvgのグラフをpngに変換します。
Screen Shot 2019-07-13 at 19.56.36.png

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

アプローチ1

XMLSerializer.serializeToString()を使います。
こんな感じです。


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の描画が終わるまで雑に待つ

するとこうなります。
Screen Shot 2019-07-13 at 19.59.25.png

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

#アプローチ2

window.getComputedStyle
という、
要素に適用されたスタイルを計算し直して値にしてくれる関数があるようなので、これを子要素全てに適用すればいいのではないかと思いました。

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


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の描画が終わるまで雑に待つ

こうなります。
Screen Shot 2019-07-13 at 20.06.38.png

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

アプローチ3

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


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)

結果
Screen Shot 2019-07-13 at 20.10.05.png

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

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?