nodejsで画像生成Serverを作った


## 要約

1. nodejsで画像生成するなら、サーバーサイドのCanvasと言う手段があるよ
2. hapijsだとpathの作成が楽だよ
3. redisにバイナリ保存するときはoption設定がいるよ


## ことのはじまり

みなさんどうやって大量のデータを人の目で確認するでしょうか。
テーブル生成して目視?　なるほど、いいですね。ですがすべてのユーザが眼grepを得意とする人ではありません。
データがある程度増えてくると、チャートや画像の方がずっと分かりやすいことがよくあります。
今回はnodejsで画像生成ついでにhapiフレームワークとredisでのキャッシュを試してみました。


## 画像生成Server

最初はクライアントサイドでcanvasを使うと言う手も考えました。
ブラウザ上で表示が制御できればいいんじゃないかと言うのもあったのですが、操作が増えるのが嫌煙されていたので却下。
しかし対象の画像をすべて表示しようと思うと1pageあたり100-300枚ほどあり、全部をブラウザでと言うのは現実的でないためサーバで画像生成を行い後述するように一部はキャッシュをすると言う方式を取りました。
幸いなことに[canvas module](https://www.npmjs.com/package/canvas)が存在し、クライアントサイドでやるのとほとんど変わらずに書くことができます。

### Canvas Install

CanvasはNative moduleなのでbuildするためにライブラリやVSのインストールが必要です。
[本家解説](https://github.com/Automattic/node-canvas/wiki/Installation---Windows)にリンク先が貼ってあるので参照してください。
今回はWindowsを想定して進めます。

1. Python2.7系をインストールする
2. Visual Studio 2013をinstallする
3. GTK 2をインストールする
  * `C:\GTK` など適当なフォルダにzipを解凍
  * pathを通す
4. libjpeg-turboをインストールする
5. `npm install canvas` が成功することを確認する

OSとnodejs(たぶん正確にはV8)のversionがあっていればビルド済みのバイナリモジュールを使い回すことができるので、
複数立てるときには一台でビルドしてそれをコピーするのが楽かもしれません。

### Canvasをつかう

使い方はブラウザと同じでContextを生成して描画を行い、pngかjpegをbuffer/DataURLで書き出して使います。

|Method|description|
|:---|:---|
|pngStream()|png形式でStream書き出し|
|jpegStream(opt)|jpeg形式でStream書き出し|
|toBuffer()|png形式で同期書き出し|
|toBuffer(callback(err, buf))|png形式で非同期書き出し|
|toDataURL(['image/png'])|DataURLで同期書き出し|
|toDataURL('image/png', [opt], callback(err, img))|DataURLで非同期書き出し|

描画例

```
const Canvas = require('canvas');

// Context生成
let image = Canvas.image;
let canvas = new Canvas(200,200);
let ctx = canvas.getContext('2d');

// 文字描画
ctx.font = "30px Lato"
ctx.rotate(.1);
ctx.fillText('Canvas', 50, 100);

// ライン描画
let te = ctx.measureText('Canvas');
ctx.stroke.style = 'rgba(0,0,0,0.7)'
ctx.beginPath();
ctx.lineTo(50,102);
ctx.lineTo(50 + te.width, 102);
ctx.stroke();

// 書き出し
canvas.toBuffer((err, buf)=>{
  fs.writeFile("image.png", buf);
})

```
image.png
![test.png](https://qiita-image-store.s3.amazonaws.com/0/92809/f22714c6-8db2-999f-8ad9-8c91cb316795.png)


## hapijs

httpサーバーといえば[express](https://www.npmjs.com/package/express)がとても作りやすいですが今回はhapijsを選択。
なぜ選んだかというと
>[Hapi.jsでHappy Coding - Part1: ルーティングとSwaggerプラグイン(masatoさん)](http://qiita.com/masato/items/87c2bbcf506a729b706b)

で[hapi-swagger](https://www.npmjs.com/package/hapi-swagger)の存在が気になったからと言うものですが実際、

1. routingの管理がしやすい
2. [joi](https://www.npmjs.com/package/joi)をつかったparam/queryのバリデーションが楽

と言うあたりが嬉しい感がある。
しかし移行するにはpluginも使い方も大きく変わるので設定方法など学ぶことが多く
まだ把握しきれていないので語れません。

とりあえずIDとnumberで1URL=1画像生成になるように設定。

```
const lib = require('../lib/image');

server.route({
	method: 'GET',
	path: '/image/{id}/{num}',

	// config にtagsとvalidateを設定。swaggerにも反映される
	config: {
		tags: ["api", "image"],
		validate: {
			params: {
				id: Joi.string().regex(/^[a-zA-Z0-9]{3,20}$/),
				num: Joi.number().integer().min(10).max(1000),
			}
		}
	}
	handler: (req, rep)=>{
		lib(req.params,(err, buf)~>{
			rep(buf).type('image/png');
		})
	}
});

```
## redisでキャッシュ

画像生成サーバはできましたが本番環境の実測では生成に1枚あたり100-200ms程度かかっており、1page(200枚前後)で10-20sec...常時これでは使い物になりません。生成サーバーを並列にして単位時間あたりの生成数を増やし、生成後はredisに一定時間キャッシュするようにします。

![system.dot.png](https://qiita-image-store.s3.amazonaws.com/0/92809/086c9d8f-b500-0e3c-510d-f87cad52ade6.png)

使い方云々はほかを見てもらうとして躓いた点

1. 書き込んだバイナリが壊れている
2. redisが死んだらAppも死ぬ

に対する対応についてだけ話します。パッケージは[node redis](https://www.npmjs.com/package/redis)を使います。

#### バイナリが壊れる

redis-cliで見たら存在するのにnodejsで読み出してみたら壊れているというのにハマりました。
実際data.lengthを見ると小さくなっています。これは先人が記録を残してくれていました。

> [node_redisでバイナリデータが上手く読み取れない](http://takeswim.hateblo.jp/entry/2013/05/10/111306)

node redisでバイナリを読み書きする場合はoptionの設定に `detect_buffers` を追加し、keyのbufferで入力するとBufferのデータ形式がとれました。ちなみに`return_buffers`をtrueにすればすべてバイナリで返ってくるようです。

```
const redis = require('redis');
const client = redis.createClient({detect_buffers:true});  // detect_buffesをtrueにする

// client.setex("key", exipre, data) // 文字列や数字などは文字列でよい
client.setex(new Buffer("key"), exipre, data) // keyをBufferに変換
```

#### redisが死んだらAppも落ちる

単純にcllientを作っただけだとredisが死んだときに一緒に落ちます。
道連れにされるよりは遅くても生成できる方がマシなので対応。
一つは`Client.on('error',(err))`を設定しておくこと。リスナーがいないと例外が飛んで落ちます。
もう一つはgetの前に生存確認を挟んで、死んでる場合はキャッシュ周りをスキップします。

```
client.on("error", function (err) {
	console.error("Error " + err);
});


if(!client.connected) return fn(cb); // 未接続ならcacheの確認をせずに生成して返すなど
client.get(key,(err, buf)=>{
	// hoeghoge
})
```


### さいごに

hapijsは気が効いていいよという話でした。
まだ使いこなせてないので(特にログ周りのケアが...)しばらく実験が必要。

今回のリポジトリとherokuを用意しているので下記を参照してください。
今回初めてやってみましたがherokuのデプロイってすごく楽ですね!
redisはタダで用意する方法がすぐに見つからなかったので未適用。

* [heroku](https://frozen-atoll-99368.herokuapp.com/documentation)
* [Repository](https://github.com/uzuna/hapi-training/tree/redis)