[HTML5] Canvasを画像としてダウンロード

canvasタグに現在描画されている内容を画像としてクライアントにダウンロードしてみます。 Webブラウザなどのクライアントだけで完結するためサーバの負荷などを気にする必要がありません。

今回はひたすらカウントアップするcanvasを、ボタンを押したタイミングでダウンロードできるサンプルです。

Canvasダウンロード

サンプル

以下のページから実際にサンプルを実行できます。 miku3.net

適当なタイミングで「ダウンロード」ボタンをクリックしてください。クリックした瞬間のcanvasの状態が画像としてダウンロードされます。

ソースコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>CANVAS ダウンロード</title>
  <style>
    #countup{ border: 1px solid gray; }
    #btn-dl{ width: 300px; height: 50px; }
  </style>
</head>
<body>

<h1>CANVAS ダウンロード</h1>

<section>
  <!-- このCanvasをダウンロードします -->
  <canvas id="countup" width="300" height="200"></canvas>
  <p><button type="button" id="btn-dl"><img src="icon/download-solid.svg" width="32" height="32"></button></p>
</section>

<script>
window.onload = () => {
  const canvas = document.querySelector("#countup");
  const ctx = canvas.getContext("2d");

  // カウントアップを開始
  ctx.font = "38px serif";
  ctx.fillStyle = "Red";
  setInterval( () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillText(getFormatDate(), 10, 110);
  }, 100);

  // DLボタンをクリック
  document.querySelector("#btn-dl").addEventListener("click", ()=>{
    canvasDownload("#countup");
  });
};

/**
 * 現在時間をhh:mm:ss.msecで返却
 *
 * return {string}
 */
function getFormatDate(){
  const time = new Date();
  const str  =  ("0" + time.getHours()).slice(-2)   + ':'
              + ("0" + time.getMinutes()).slice(-2) + ':'
              + ("0" + time.getSeconds()).slice(-2)
              + '.'
              + ("00" + time.getMilliseconds()).slice(-3);

  return(str);
}

/**
 * Canvasを画像としてダウンロード
 *
 * @param {string} id          対象とするcanvasのid
 * @param {string} [type]      画像フォーマット
 * @param {string} [filename]  DL時のデフォルトファイル名
 * @return {void}
 */
function canvasDownload(id, type="image/png", filename="canvas"){
  const blob    = getBlobFromCanvas(id, type);       // canvasをBlobデータとして取得
  const dataURI = window.URL.createObjectURL(blob);  // Blobデータを「URI」に変換

  // JS内部でクリックイベントを発動→ダウンロード
  const event = document.createEvent("MouseEvents");
  event.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  const a = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
  a.href = dataURI;         // URI化した画像
  a.download = filename;    // デフォルトのファイル名
  a.dispatchEvent(event);   // イベント発動
}

/**
  * 現状のCanvasを画像データとして返却
  *
  * @param {string}  id     対象とするcanvasのid
  * @param {string}  [type] MimeType
  * @return {blob}
  */
function getBlobFromCanvas(id, type="image/png"){
  const canvas = document.querySelector(id);
  const base64 = canvas.toDataURL(type);              // "data:image/png;base64,iVBORw0k~"
  const tmp  = base64.split(",");                     // ["data:image/png;base64,", "iVBORw0k~"]
  const data = atob(tmp[1]);                          // 右側のデータ部分(iVBORw0k~)をデコード
  const mime = tmp[0].split(":")[1].split(";")[0];    // 画像形式(image/png)を取り出す

  // Blobのコンストラクタに食わせる値を作成
  let buff = new Uint8Array(data.length);
  for (let i = 0; i < data.length; i++) {
    buff[i] = data.charCodeAt(i);
  }

  return(
    new Blob([buff], { type: mime })
  );
}
</script>
</body>
</html>

ダウンロード部分は以下のページを参考にさせていただきました。 https://oar.st40.xyz/article/133oar.st40.xyz

解説

大雑把なロジック

正直まどろっこしいのですが、canvasのデータを一発で変換できないので以下のような流れをたどります。

  1. CanvasをBase64に変換
  2. Base64をBlobに変換
  3. BlobをdataURIに変換

最終的にこのdataURIをJSで生成したaタグにセット、JSでクリックイベントを発生させダウンロードを行っています。

ここらへんもう少しかんたんに実装できる機能がHTML5標準でありそうな気もするんですけどね。

素材

参考ページ