JavaScript
HTML5
canvas

canvasを画像としてダウンロードさせたかった話

Qiita初投稿につき不作法等あるかと思うので、ご指摘いただけると幸いです。

プログラミングぢからも底辺なため、そちらも合わせてご指導願います。


やりたかったこと


  1. ユーザがWebページ上のボタンをクリックする


  2. <canvas>の描画内容がpng画像に変換される

  3. png画像が自動でダウンロードされる

たった3行の要件に結構な時間かかった。つらい。


どのように困ったのか


①「.click()」がFirefoxで動作しない


【問題】

最初に書いたJavaScriptはこんな感じ。仮想の<a>要素を作ってからURLを仕込んでクリックさせればええんちゃう?といった感じ。


let canvas = document.getElementById('targetCanvas')
let link = document.createElement('a')
link.href = canvas.toDataURL()
link.download = 'cancas.png'
link.click()

とりあえず自分のメインブラウザたるChromeで検証したら、あっさり意図したとおりに動いた。いいね!

しかし次にFirefoxで検証したところ、ピクリとも動かず。よくないね!


【原因】

Firefoxでは実在しない要素に対して.click()できない模様。まあ気持ちは分かる。


【対策】

A案:appendChild()で要素を追加してから.click()する。

B案:予めHTML内に<a id="hoge">を書いておき、JS側でhref属性を書き換える。

今回は実装が簡単そうなB案を採用。<a id="hoge">がユーザに見えないようCSSでdisplay: none;したら、見た目も動作も当初案と同じになって無事解決した。


②それでも「.click()」がIE/Edgeで動作しない


【問題】

なんとなく「IE/Edgeはすんなり動いてくれないだろうなあ」と思ってたけど、案の定ノーリアクション。もしやと思ってdisplay: none;外して手動クリックしてみたら、ページ遷移のエラーが発生した。


【原因】

今回のlink.href = canvas.toDataURL()の部分は、いわゆる「https://~」の形式ではなくて、「data:image/png;base64,とても長い文字列~」となる。「データURIスキーム」って言うんだって、知らんかった。

なおこれについてWikipediaで調べてみると……


2018年現在、データURIは主要なほとんどのブラウザで完全にサポートされている。ただし、Internet ExplorerとMicrosoft Edgeでは、一部の機能が実装されていない。


あっはいそうなんすね。マイクロソフトさんさすがっす。


【対策】

当初要件の「png画像を自動でダウンロードさせる」は、IE/Edgeでは実装困難と判断して諦めることに。画像をサーバ側に一回保存して、改めて<a href="https://hoge.com/image.png">とかやれば不可能ではないんだろうけど、そこまでの技術力は無い流石にめんどい。

代用として、ダウンロードさせたかった画像をページ上に小さく表示させ、手動で「右クリック→名前を付けて保存」してもらうことにした。我ながらダサい……もっとスマートなアイディアあったら誰か教えてください。

ちなみにIE/Edgeのときだけ当該部分が表示されるようにCSS弄ってみたりもしたけど、有象無象のスマホブラウザ共が自動ダウンロードできるのかとか、IE/Edge専用CSSがちゃんと反映されるのかとか、色々考えたら(正常動作するChromeとFirefoxも含めて)常に表示しとく方が良いなと思い直した。


というわけで最終形


hoge.html

<body>

<canvas id="targetCanvas"></canvas>
<button onclick="downloadCanvas()">図としてダウンロード</button>

<a id="hiddenLink" download="canvas.png">link</a>
<!-- CSSで「display: none;」して非表示 -->

<p>自動でダウンロードされない場合、下図を右クリックして保存してください。</p>
<img id="canvasImage" src="dummy.png">
<!-- CSSで小さめサイズに調整 -->
</body>



hoge.js

function downloadCanvas() {

let canvas = document.getElementById('targetCanvas')
let link = document.getElementById('hiddenLink')
link.href = canvas.toDataURL()

document.getElementById('canvasImage').src = canvas.toDataURL()

link.click()
}


これでChromeとFirefoxでは要件通りに動作、IE/Edgeでも手動スクショ強要はせずに済んだ。めでたしめでたし。

いやホントはSafariとかスマホ版Chromeとかでも検証しないといけないんだけどね。おいおいやります。


番外編 ~.toDataURL()がエラー吐く話~

ローカルでテストしてたとき、画像ファイルを<canvas>要素に取り込んでから.toDataURL()すると、Chromeが下記のエラーを吐いた。


DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.


汚染されたキャンバスはエクスポートできません?なるほどわからん。

ググったらこんな記事が見つかりました。要約すると「外部コンテンツを取り込んだcanvasはセキュリティの都合でExportできないよ」ってことかな。ええ、いや、自サイト内の画像を取り込んでるだけなんですけど。

結局このエラーが出るのはローカル上で動かしてるときだけで、Web上にデプロイしてからテストすれば何の問題もなく動いた。結構時間食って悲しかったので一応書き添えておきます。根本的な解決方法は知らぬ……。