Canvas(ブラウザ)からBase64変換した画像をNode.jsのサーバーに送って画像保存する #linebootawards


#linebootawardsの公式ハッカソンでLIFFとClovaを使った絵描き歌アプリを作っている学生チームがありました。

そこの子がハマっていたので作ってみた感じのコードになります。

今回書いてるコードは直接的にLINEのAPIとは関係ないですがLIFFのお絵描きサンプルを使ってごにょごにょしたい人が見るかなぁという想定です。

環境はmacOS Sierra、Google Chrome v69、Node.js v10.11.0です。

作ったもの

ブラウザでお絵描き出来るCanvasがあり、そこで書いたものがBase64の画像になってサーバーに送られてサーバー側で保存される。といった内容です。

ブラウザ側は3秒に一回サーバーに画像を送って、サーバー側では画像が更新されていきます。

作り方

コピペで動くと思います。

プロジェクト作成だけしておきます。myappフォルダに作っていきます。

mkdir myapp
cd myapp
npm init -y

ブラウザ(クライアント)側

myappフォルダにindex.htmlとserver.jsを作成しておきましょう。

まずはhtml側から。

以下の二つのライブラリを利用しています。

  • signaturepad: canvasでお絵描きして画像書き出しまでを簡略化してくれる
  • axios: JS向けのメジャーなHTTPクライアント。サーバーにPOSTするときに利用します
index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Signature Pad demo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
</head>
<body onselectstart="return false">

  <div id="signature-pad" class="signature-pad">
    <div class="signature-pad--body">
      <canvas></canvas>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/signature_pad.min.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script>
      const canvas = document.querySelector("canvas");
      const signaturePad = new SignaturePad(canvas, {
          backgroundColor: 'rgb(238,238,238)',
      });

setInterval(() => {
    const base64 = signaturePad.toDataURL("image/jpeg"); 
    console.log(base64)

    axios.post('/post', {
      pad: base64
    })
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
},3000)
  </script>
</body>
</html>

コード解説

とりあえず動かす場合はここの解説無視でOK、↑のindex.htmlをコピペしておきましょ。

  • お絵描き領域を確保
<canvas></canvas>
  • canvasをお絵描き出来る状態に
 const canvas = document.querySelector("canvas");
 const signaturePad = new SignaturePad(canvas, {
  backgroundColor: 'rgb(238,238,238)',
 });
  • base64変換してaxiosでサーバーに送信
    const base64 = signaturePad.toDataURL("image/jpeg"); //jpegの画像としてbase64変換
    console.log(base64); //base64の文字列を確認
    //サーバーの`/post`に対して`{pad: <base64文字列>}`といったJSON形式で送信
    axios.post('/post', {
      pad: base64
    })
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
  • setIntervalで3秒(3000ミリ秒)ごと実行
setInterval(() => {
//省略
},3000)

サーバー側

server.js側です。二つのモジュールを利用します。

  • express: Node.jsのWebフレームワーク
  • body-parser: Expressのミドルウェア。POSTリクエストを処理する場合にほぼ必須。

npm経由でインストールします。

npm i express body-parser
server.js
'use strict';

const fs = require('fs');
const express = require('express') ;
const app = express();
const PORT = process.env.PORT || 3000;
const bodyParser = require('body-parser');
app.use(bodyParser());

app.get('/',  (req, res)  => res.sendFile(__dirname+'/index.html'));

app.post('/post',  (req, res)  => {
    console.log(req.body);
    console.log("postリクエストがきたよ!");

    const base64 = req.body.pad.split(',')[1];
    const decode = new Buffer.from(base64,'base64');
    fs.writeFile('xxx.png', decode, (err) => {
        if(err){
            console.log(err)
        }else{
            console.log('saved');
        }
    });
});

app.listen(PORT);
console.log(`listening on *: ${PORT}`);

コード解説

これもとりあえず動かすならスルーでserver.jsをコピペだけして読み飛ばしましょ

  • body-parserの読み込み
const bodyParser = require('body-parser');
app.use(bodyParser());

これを宣言しておくとPOSTリクエストで送られてきたBODYを処理できます。具体的にはルーティングのコールバック内でreq.bodyが利用可能になります。

  • index.htmlにアクセスできるようにする
app.get('/',  (req, res)  => res.sendFile(__dirname+'/index.html'));

/にアクセスするとさっき書いたindex.htmlの内容が呼び出されます。

  • サーバーで/postを受け付けることができるようにする
app.post('/post',  (req, res)  => {
//省略
});
  • base64文字列から本体だけを抽出
const base64 = req.body.pad.split(',')[1];

data:image/jpeg;base64,.........(すごーく長い文字列)という形式で送られてくるので,で区切った後半の内容だけを利用します。

ex: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/....(まだまだ続く)

  • 抽出された画像本体のエンコード文字列をデコードして復元する
const decode = new Buffer.from(base64,'base64');
  • 復元された画像データをマシンに保存する
fs.writeFile('xxx.png', decode, function(err) {
    if(err){
        console.log(err)
    }else{
        console.log('saved');
    }
});

実行

node server.js

これでhttp://localhost:3000にブラウザからアクセスするとcanvas画面が表示されてお絵描きするとserver.jsがあるフォルダにxxx.pngが作成されて3秒毎に更新されるのが確認できると思います。

まとめ

ポイントになりそうなのはbase64文字列を,で区切るあたりとnew Buffer.from()でデコードするあたりかなぁと思います。
それ以外はsignaturepad,axios,express,body-parserなどのライブラリを利用して楽しましょ。

よもやま

ハッカソン参加者の学生さんはどこでハマってたのかが気になってるのでなんとなく思ったこと。

PC画面をみてログをみた感じ、base64文字列がパーセントエンコードされてサーバーに送られていた様子で、それが原因だったのかなぁと思ったりしてます。クライアント側からサーバーに送る際はaxiosではなく$.ajaxを利用していてjQuery利用だった模様です。jQuery使わなくって久しいですが$.ajaxだとbase64がパーセントエンコードされちゃう現象とかあるのかなぁ(?)

あとbody-parser使わずにreq.on('data')のchunkを取ってたのでその辺も関係しているかも(?)

気が向いたらもう少し調べてみます。