[HTML5] Canvasを画像に変換しサーバへ送信する

Canvasに描画した内容を画像としてサーバへ送信し、そのままサーバに保存してみます。

今回は入力したテキストがそのままCanvasに描画される簡単なサンプルを用意しました。文字色と背景色もおまけで変更できます。もう少し頑張るとバナー画像ジェネレーターとか作れそうですね。

サーバ側のプログラムはPHPを採用していますが、他の言語でも似たような処理になります。

Canvasの内容をサーバへ送信する

実行例

以下から実際のサンプルをお試しいただけます。 miku3.net

左側にあるテキストボックスへ入力するとその場でCanvasに反映されます。同様に文字色や背景色も指定したタイミングでCanvasが書き換わります。ブラウザによって挙動が変わると思いますがカラーピッカーをグリグリいじる度にCanvasが変わっていく様子を見ていると中々楽しいですねw

入力が終わったら「サーバへ保存」ボタンを押すとCanvasの内容がサーバ上へ送信されます。正常に保存されると右側に表示されます。

  • アイコンはFontAwesomeからお借りしています。
  • 保存された画像は定期的に削除されます
  • 保存された画像は誰でも閲覧可能です。また裁判所などを通して開示請求があった場合はIPアドレスの開示などを行いますので悪いことには利用しないでくださいね。

ソース

JavaScriptがちょっと長くなってしまったので、HTMLと分離しています。またCSSも別ファイルにしました。

HTML

HTML5よりでカラーピッカーの機能が利用できますが、IEとiOSのWebViewは非対応です。テキストボックスが表示されるハズですので文字色やRGBなどを直接入力してください。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>CANVASの情報をサーバへ送信</title>
  <link rel="stylesheet" href="style.css" type="text/css" media="all">
</head>
<body>

<h1>Canvasを画像に変換しサーバへ送信</h1>

<div id="container">
  <section id="previw">
    <h2>プレビュー</h2>
    <canvas id="board" width="350" height="150"></canvas>

    <form id="frm">
      文字色:<input id="color-text" type="color" value="#0000FF"> 背景色:<input id="color-bg" type="color" value="#FFFFFF"><br>
      <input type="text" id="txt-message" placeholder="文字を入力してください"><br>
      <button type="button" id="btn-send">サーバへ保存</button>
    </form>
  </section>

  <section id="arrow">
    <div><img src="icon/arrow-circle-right-solid.svg" width="50" height="50" alt="→"></div>
  </section>

  <section id="savelist">
    <h2>保存済み画像</h2>
    <ul id="result"></ul>
  </section>
</div>

<script src="app.js"></script>

</body>
</html>

JavaScript

HTMLから呼び出されるJavaScriptです。

//---------------------------------------------
// 定数定義
//---------------------------------------------
// 保存を行うプログラムがあるURL
const SAVE_URL = 'http://example.com/receive.php';

// 画像が保存されているURL
const IMAGE_URL = 'http://example.com/image';

//---------------------------------------------
// オブジェクト
//---------------------------------------------
const Banner = {
  bgcolor: "#FFFFFF",  // 背景色
  font: "48px serif",  // フォント
  fontcolor: "Blue",   // 文字色
  text: "Hello World", // テキスト

  // Canvas情報
  canvas: {
    width: null,   // 横幅
    height: null,  // 高さ
    ctx: null      // context
  }
}

//---------------------------------------------
// [event] ページ読み込み完了
//---------------------------------------------
window.onload = ()=>{
  const message   = document.querySelector("#txt-message");  // テキストボックス
  const colorText = document.querySelector("#color-text");   // 文字色
  const colorBg   = document.querySelector("#color-bg");     // 背景色

  // Canvasの情報を代入
  const board = document.querySelector("#board");
  Banner.canvas.ctx    = board.getContext("2d");
  Banner.canvas.width  = board.width;   // 横幅
  Banner.canvas.height = board.height;  // 高さ

  // Canvasに最初の文字を描画
  drawCanvas();

  //---------------------------------------------
  // ユーザーの入力があればCanvasを更新する
  //---------------------------------------------
  // 文字入力
  message.addEventListener("keyup", ()=>{
    Banner.text = message.value;
    drawCanvas();
  });

  // 文字色の変更
  colorText.addEventListener("change", ()=>{
    Banner.fontcolor = colorText.value;
    drawCanvas();
  });

  // 背景色の変更
  colorBg.addEventListener("change", ()=>{
    Banner.bgcolor = colorBg.value;
    drawCanvas();
  })

  // submitイベントが発生したらキャンセル
  document.querySelector("#frm").addEventListener("submit", (e)=>{
    e.preventDefault();
  });

  //---------------------------------------------
  // 保存ボタンが押されたらサーバへ送信する
  //---------------------------------------------
  document.querySelector("#btn-send").addEventListener("click", ()=>{
    // Canvasのデータを取得
    const canvas = board.toDataURL("image/png");  // DataURI Schemaが返却される

    // 送信情報の設定
    const param  = {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8"
      },
      body: JSON.stringify({data: canvas})
    };

    // サーバへ送信
    sendServer(SAVE_URL, param);
  });
};


/**
 * Canvasを描画
 *
 * @return {void}
 */
function drawCanvas(){
  ctx    = Banner.canvas.ctx;
  width  = Banner.canvas.width;
  height = Banner.canvas.height;

  // Canvasをお掃除
  ctx.clearRect(0, 0, width, height);

  // 背景を指定色で塗りつぶす
  ctx.fillStyle = Banner.bgcolor;
  ctx.fillRect(0, 0, width, height);

  // 文字を描画
  ctx.font = Banner.font;
  ctx.fillStyle = Banner.fontcolor;
  ctx.fillText(Banner.text, 10, 90, width);
}

/**
 * サーバへJSON送信
 *
 * @param url   {string} 送信先URL
 * @param param {object} fetchオプション
 */
function sendServer(url, param){
  fetch(url, param)
    .then((response)=>{
      return response.json();
    })
    .then((json)=>{
      if(json.status){
        alert("送信に『成功』しました");
        setImage(json.result);    //json.resultにはファイル名が入っている
      }
      else{
        alert("送信に『失敗』しました");
        console.log(`[error1] ${json.result}`);
      }
    })
    .catch((error)=>{
      alert("送信に『失敗』しました");
      console.log(`[error2] ${error}`);
    });
}

/**
 * サーバ上の画像を表示する
 *
 * @param path {string} 画像のURL
 * @return void
 */
function setImage(path){
  const url = `${IMAGE_URL}/${path}`;
  const result = document.querySelector("#result");
  const li = document.createElement("li");
  li.innerHTML = `<a href="${url}" target="_blank" rel="noopener noreferrer"><img src="${url}" class="saveimage"></a>`;
  result.insertBefore(li, result.firstChild);
}

PHP

今回はPHPで受け取っています。画像を保存するディレクトリの用意とパーミッションの設定をお忘れなく。

<?php
/**
 * DataURI Schemaを受け取り保存する
 *
 */

//-------------------------------------------------
// 定数
//-------------------------------------------------
// 画像を保存するディレクトリ
define('SAVE_DIR', 'image/');

//-------------------------------------------------
// POSTで渡されたJSONを取得
//-------------------------------------------------
$json = getParamJSON();

//-------------------------------------------------
// Validation
//-------------------------------------------------
// dataが渡されているか
if( ! isset($json['data']) ){
  sendResult(false, 'Empty query parameter: data');
  exit(1);
}
// データ長が30kbyte以下か
if( strlen($json['data']) > (1024 * 30) ){
  sendResult(false, 'Too long string: data');
  exit(1);
}
// 中身がDataURISchemaか
if( ! preg_match('/^data:image\/png;base64,/', $json['data']) ){
  sendResult(false, 'Not Allow data type: data');
  exit(1);
}

//-------------------------------------------------
// サーバへ保存
//-------------------------------------------------
// Base64をバイナリに戻す
$data = $json['data'];
$data = str_replace('data:image/png;base64,', '', $data);  // 冒頭の部分を削除
$data = str_replace(' ', '+', $data);  // 空白を'+'に変換
$image = base64_decode($data);

// ファイルへ保存
$file = sprintf('%s.png', uniqid());    //ファイル名を作成
$result = file_put_contents(SAVE_DIR.$file, $image, LOCK_EX);

//-------------------------------------------------
// 結果を返却
//-------------------------------------------------
if( $result !== false ){
  sendResult(true, $file);  // ブラウザにファイル名を返却する
}
else{
  // 書き込みエラー
  sendResult(false, 'Can not write image data');
}


/**
 * POSTで渡されたJSONを取得する
 *
 * @return object
 */
function getParamJSON(){
  $buff = file_get_contents('php://input');
  $json = json_decode($buff, true);

  return($json);
}

/**
 * 結果をJSON形式で返却
 *
 * @param  boolean $status 成功:true, 失敗:false
 * @param  mixed   $data   ブラウザに返却するデータ
 * @return void
 */
function sendResult($status, $data){
  // CORS (必要に応じて指定)
  header('Access-Control-Allow-Origin: *');
  header('Access-Control-Allow-Headers: *');

  echo json_encode([
    "status" => $status,
    "result" => $data
  ]);
}

解説

サンプルがちょっと長くなってしまったので、それぞれのトピックス毎にコードを簡略化して解説を入れていきます。

Canvasを画像として取り出す

CanvasのオブジェクトにtoDataURL()メソッドを介して取得することができます。以下でPNG画像を一発です。

const image = document.querySelector("#board").toDataURL("image/png");

toDataURL(type, option)

type
画像形式をimage/png, image/jpegなどと指定します。未指定の場合はPNGになります。GoogleChromeではWebPがサポートされているためimage/webpも利用可能です。
option
画像の品質(圧縮率)を0〜1の数値で指定します。1に近づくほど高クオリティ(未圧縮)になり、未指定時は0.92がデフォルトのようです。この指定はimage/jpegimage/webpで利用可能。

戻り値はバイナリではなくDataURIスキーマとなる点に注意が必要です。バイナリをBase64形式にエンコードし先頭にdata:image/png;base64,といった文字列が付加されます。

ピンと来ない場合は先ほどのコードをブラウザのConsoleで実行するか、戻ってきた値をconsole.log()などで出力して確認すると実際にどのような値が返ってくるかわかります。

今回はこの文字列をそのままサーバへ送りつけ、サーバ上でバイナリに戻しています。

サーバへ送信する

軽いデータならGETで送っても良いのですが、ここではPOSTを利用しています。中身を見ればわかりますがFetchAPIで普通に送っているだけですね。

// Canvasを画像として取り出す
const image = document.querySelector("#board").toDataURL("image/png");

// Fetch APIのパラメーターをセット
const param = {
  method: "POST",
  headers: {
    "Content-Type": "application/json; charset=utf-8"
  },
  body: JSON.stringify({data: image})  // 画像をセット
};

// サーバへ送信
fetch("http://example.com/receive.php", param)
  .then((response)=>{
    return response.json();  // サーバからはJSONが返される想定
  })
  .then((json)=>{
    // 成功時の処理
  })
  .catch((error)=>{
    // エラー時の処理
  });
}

FetchAPIの基本的な使い方は以下を参照ください。 blog.katsubemakito.net

サーバでバイナリとして保存する

ロジックはシンプルで先頭にある情報を削除し実データのみの状態にした上で、Base64をデコードする関数に突っ込んでいます。あとはファイルに保存するなりDBなどに突っ込むなり、ImageMagickなどで画像加工するなど思いのままです。

// JSから送られたデータ
$data = 'data:image/png;base64,(Base64でエンコードされたデータ)';

// Base64をバイナリにデコードする
$data = str_replace('data:image/png;base64,', '', $data);  // 冒頭の部分を削除
$data = str_replace(' ', '+', $data);  // 空白を'+'に変換
$image = base64_decode($data);

// ファイルへ保存
$file = sprintf('%s.png', uniqid());    //ファイル名を作成
file_put_contents($file, $image);

ここではPHPで書いていますが、他言語でも似たような処理になるかと思います。Base64のエンコード/デコードするライブラリなどは大抵の言語で用意されています。

参考ページ