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


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

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

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

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

## 作ったもの

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

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


![スクリーンショット 2018-09-23 21.40.09.png](https://qiita-image-store.s3.amazonaws.com/0/35387/e063fa3b-4c8c-c6a8-6531-dbc7e0e37603.png "スクリーンショット 2018-09-23 21.40.09.png")

## 作り方

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

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

```bash
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/signature_pad@2.3.2/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を取ってたのでその辺も関係しているかも（?）

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