notebook

都内でWEB系エンジニアやってます。

SVGをPNGに変換してDownloadする

フロントエンドだけでフォームの内容によって動的に生成したSVGをPNGに変換してDownloadさせる、というのをやってみたのでそのときのコード辺とメモ

サンプル

Next.jsでサンプル書いたので少し固有のものが混じっているがやっていることは分かるはず

import type { NextPage } from 'next';
import { useRef } from 'react';

const Hoge: NextPage = () => {
  const svgRef = useRef<SVGSVGElement>(null);

  const handlePngDownload = () => {
    if (svgRef.current) {
      const svgData = new XMLSerializer().serializeToString(svgRef.current);
      const canvas = document.createElement('canvas');
      canvas.width = svgRef.current.clientWidth;
      canvas.height = svgRef.current.clientHeight;

      const ctx = canvas.getContext('2d');
      const image = new Image();

      if (ctx && image) {
        const a = document.createElement('a');
        const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });

        image.onload = () => {
          ctx.drawImage(image, 0, 0);
          a.href = canvas.toDataURL('image/png');

          a.download = 'sample.png';
          a.click();
          a.remove();
        };
        image.src = URL.createObjectURL(blob);
      }
    }
  };

  return (
    <>
      <svg style="border" ref={svgRef} x="0" y="0" width="100" height="60">
        <polygon points="50 10, 70 30, 50 50, 30 30" fill="#14F" />
      </svg>
      <button
       className="mx-1 items-center rounded-sm border border-gray-400 bg-white px-4 py-2 text-gray-800 hover:bg-gray-100"
       onClick={handlePngDownload}>PNG DL</button>
    </>
  );
}
export default Hoge;

ボタンクリックでpng画像をDownloadできる

仕組み

ざっくりの流れは下記

  • SVGの情報をバイナリに変換、さらにObjectURLに変換してImageから読めるようにする
    • Imageオブジェクトだとわざわざイメタグを表示させる必要がない
    • 逆にイメタグだと表示させないとonloadが発火しない
  • イメージが読み込まれたらcanvasにイメージを読み込む
  • canvasのtoDataURLでDownload可能なデータにする
    • イメージのURLを生成して直接読ませる方法も試してみたがうまくいかなかった
  • リンクタグを生成しダウンロードさせる

canvasを挟むことでイメージとして保存できるようになる

つまったところ

以下つまったポイントを残しておく

image.srcは非同期読み込み

最初直接onloadを使わずcanvasに読ませようとしたらうまくいかなかった

Imageオブジェクトのsrcにデータをセットしても、画像のロードが非同期で行われるためだった

すぐにcanvasの描画処理を行ってしまうとまだ何もロードされていない状態でcanvasの描画が試みられることになるよう

なのでonloadで画像が読まれたことを保証してからcanvasへ読ます必要がある

イメタグでも良かったが今回はnew Imageで実施した、イメタグをcreateElementする方法だとイメタグに表示させる処理も必要だったため

バイナリからイメージ表示のためのURLへの変換

最初参考にした方法はunescapeを使っていた

VSCodeで開発してたらunescapeは非推奨ですといわれていたのでdecodeURIComponentに変えて試してみたがそれもうまくいかず

試行錯誤の結果、結局次のような感じでバイナリからURLを生成するようにした

const svgData = `<svg xmlns="http://www.w3.org/2000/svg" ...></svg>`;
const blob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
image.src = URL.createObjectURL(blob);

シンプル

対応PR

実際にtoolsへ反映させたのでそのときのPRを置いておく

download png by swfz · Pull Request #908 · swfz/tools