フロントエンドだけでフォームの内容によって動的に生成した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を置いておく