LoginSignup
21
19

More than 5 years have passed since last update.

nodejsで画像生成Serverを作った

Posted at

要約

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

ことのはじまり

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

画像生成Server

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

Canvas Install

CanvasはNative moduleなのでbuildするためにライブラリやVSのインストールが必要です。
本家解説にリンク先が貼ってあるので参照してください。
今回は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

hapijs

httpサーバーといえばexpressがとても作りやすいですが今回はhapijsを選択。
なぜ選んだかというと

Hapi.jsでHappy Coding - Part1: ルーティングとSwaggerプラグイン(masatoさん)

hapi-swaggerの存在が気になったからと言うものですが実際、

  1. routingの管理がしやすい
  2. 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

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

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

に対する対応についてだけ話します。パッケージはnode redisを使います。

バイナリが壊れる

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

node_redisでバイナリデータが上手く読み取れない

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はタダで用意する方法がすぐに見つからなかったので未適用。

21
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
19