LoginSignup
63
64

More than 5 years have passed since last update.

無料のサービスを使ってHubotで動的に画像を生成して返す

Last updated at Posted at 2013-12-28

Hubotのことを知って、個人的に衝撃を受けたのが、「GitHub社内のDevOpsを支えるツール「Boxen」と「Hubot」(後編)~DevOps Day Tokyo 2013」という記事のこの画像でした。

hubot graph me image

botが画像を動的に作って返す…。これはぜひhubot触ってみないといけない、と思ったのでした。

目標

自宅の環境以外で、24時間サーバが動いていて、いつでもhubotを呼び出せて、画像を生成する、ということが無料で出来る環境を目指します。

前提

いくつかキーワードが出てきますが、以下は知ってる前提で。

また、冒頭の画像は、campfireというチャットサービス上での表示ですが、それもお金かかるので、hipchatという5人まで無料のチャットサービスを利用します。

探索

hubotはnode上で動いているので、nodeが使えるherokuがまず候補にあがります。

冒頭で紹介したレポートでは、

(graph meのグラフ化にはgraphiteが使われているとのこと。セッション後のQ&Aより)

と書いてあったので、graphiteを調べてみました。
heroku add-onのhosted-graphiteがあったのでこちら使えるか見てみましたが、無料だと条件が結構厳しめでした。もう少し自由に使いたいなと。

javascriptで動くグラフ作成ライブラリとしては、d3.jsというのがあります。d3.jsがnode上で動いて、画像ファイル化できるなら使えそうです。

課題

herokuで動かすためには、次の課題を解決する必要があります。

  • node上でd3.jsが動くか
  • d3.jsで生成した画像データをファイル化できるか
  • ファイルをheroku上で保存できるか
  • heroku上で保存したファイルを、URL指定で取り出せるか

node上でd3.jsが動くか?

ここに、npmでd3がインストールできるパッケージが置いてあるので、この課題はクリアです。

$ npm install --save d3

でインストールできます。--saveでpackage.jsonのdependenciesに追記されます。

d3.jsで生成した画像データをファイル化できるか?

d3.jsで生成されるのは、svg形式の画像ファイルフォーマットです。このまま保存しても画像ファイルとして表示してくれないので、png形式に変換する方法を調べます。

canvasとcanvgというモジュールを使うと、node上でsvgをpngに変換かけられるとのこと。こちらを試してみます。

参考URL : https://github.com/yetzt/node-canvg/issues/1

$ npm install --save canvas
$ npm install --save canvg

一点、問題があって、cairoライブラリがインストールされていないと動きません。
heroku上でcairoライブラリを入れるには、この記事のUsing a custom build packの項目を参考にします。

heroku config:add BUILDPACK_URL=https://github.com/mojodna/heroku-buildpack-multi.git#build-env

multi-buildpackという方法で、直接heroku上でcairoライブラリをインストールします。

ファイルをheroku上で保存できるか?

herokuに直接答えが載ってました。
この記事によると、

There are two directories that are writeable: ./tmp and ./log (under your application root).

アプリケーションの直下の、./tmpと./logディレクトリだけは書き込めるよーということなので、こちらに書けばよさそうです。

※heroku run bash等でherokuサーバに入って./tmpファイル内に画像が入っているか確認しようとしても、起動中のインスタンスとbashのインスタンスが違うからか、./tmpディレクトリは見えない作りのようです。

./tmpディレクトリに作るファイルということで、mktemp的なものがないかなと思ったら、まんまのmktempモジュールがありましたのでこちらも使います。

$ npm install --save mktemp

heroku上で保存したファイルを、URL指定で取り出せるか?

hubotは、チャットインターフェースからではなくて、この記事にあるように、urlを直接叩いて呼び出すことができます。

hipchatのチャット画面でURLを貼付けた場合、そのURLが画像っぽい(拡張子で判断してる?)と、チャット画面上に画像を直接表示してくれます。

./tmpに格納されている画像ファイルをレスポンスとして返すhubot-scriptを作ればよさそうです。

作るよー

  • チャットでhubotに円を書いてねという指示を出す
  • hubotが内部のd3を使って円を作る(パラメータはチャット上で入力したものを使う)
  • canvasに書いて、canvsのイベントでフックする
  • canvasのstreamをテンポラリファイルに書き込む
  • 書き込んだらURLをhubotが返す
  • URLはhubotの画像取得リクエストになっていて、hipchatから自動で呼び出される
  • 画像取得リクエスト経由で、画像を返す

スクリプトは、hubot/scriptsディレクトリに格納します。

画像作る側はどうしてもcoffeescriptで作れなかった(インデントエラーが出る)ので、途中までcoffeescriptで作って、最後jsで調整しました。
./tmpディレクトリですが、hubot-scriptは一つ下のscriptsディレクトリがカレントディレクトリになるので、一段ずらしています。

svg.jsが画像を作る方で、tempget.coffeeが画像ファイルを画像として返す方です。

svg.js
// Generated by CoffeeScript 1.6.3
(function() {
  var Canvas, canvg, fs, mktemp, d3;

  d3 = require('d3');
  Canvas = require('canvas');
  mktemp = require('mktemp');
  canvg = require('canvg');
  fs = require('fs');
  path = require('path');

  module.exports = function(robot) {
    return robot.hear(/svg ([0-9]+) ([0-9]+) ([0-9]+) (\w+)/, function(bot) {
      var canvas, svg, temppath;
      var width = 750;
      var height = 750;
      d3.select("body").node().innerHTML = '';
      var svgimage = d3.select("body").append("svg").attr({"width": width, "height": height });
      svgimage.append('circle')
      .attr({
        cx:bot.match[1],
        cy:bot.match[2],
        r:bot.match[3],
        fill:bot.match[4]
      });
      robot.send(bot.envelope, "writing..");
      canvas = new Canvas(width, height);
      temppath = path.join(__dirname, '..', 'tmp');
      url = process.env.HEROKU_URL
      if (url === undefined) {
        url = 'http://localhost:8080'
      }
      var svg = d3.select('body').node().innerHTML;
      canvg(canvas, svg, {
        renderCallback: function() {
          try {
            fs.statSync(temppath);
          } catch (e) {
            console.log(e);
            fs.mkdirSync(temppath, 0700);
          }
          mktemp.createFile(path.join(temppath,'XXXXXXXX.png'), function(err, filename) {
            var outStream, stream;
            stream = canvas.createPNGStream();
            outStream = fs.createWriteStream(filename);
            outStream.on('close', function() {
              robot.send(bot.envelope, url + "/hubot/tempget.png?id=" + path.basename(filename));
            });
            stream.on('data', function(chunk) {
              outStream.write(chunk);
            });
            stream.on('end', function(chunk) {
              outStream.end();
            });
          });
        }
      });
    });
  };
}).call(this);
tempget.coffee
querystring = require('querystring')
fs = require('fs')
path = require('path')

module.exports = (robot) ->
  robot.router.get "/hubot/tempget.png", (req, res) ->
    query = querystring.parse(req._parsedUrl.query)
    tmp = path.join(__dirname, '..', 'tmp',query.id)
    path.exists(tmp, (exists) ->
      if(exists)
        console.log "exist! #{tmp}"
        fs.readFile tmp,(err,data) ->
          res.writeHead(200, {'Content-Type': 'image/png'})
          res.end(data);
      else
        console.log "not! #{tmp}"
        res.status(404).send('Not found')
    )

こう使います

チャット画面で、「svg 100 100 100 yellow」のように入れます。
表記は、「svg x座標 y座標 半径 色指定」 です。

スクリーンショット:
screenshot.png

これはシンプルな円ですが、d3で表現できることならなんでもできるはずなので、綺麗で複雑な画像をぜひhubotに作らせてみてくださいね。

追記

D3関係の記事見てたら、canvg.jsの代わりに、fabric.jsを使った方がよさそうということがわかりました。

fabric.jsの利点

  • canvasに追加した要素をオブジェクトとして管理出来る
  • オブジェクトに対して操作ができる
  • 拡張しやすそう

cairoライブラリを使うのは変わらないようなので、canvg.jsと比べて同等以上と言えそうです。

svg.jsを置き換えてみた

canvgモジュールを使わない代わりに、fabricモジュールを追加します

$ npm uninstall --save canvg
$ npm install --save fabric

canvgで処理していたところを、fabricに置き換えます。

fabric.js
(function() {
  var fabric, fs, mktemp, d3;

  d3 = require('d3');
  mktemp = require('mktemp');
  fabric = require('fabric').fabric;
  fs = require('fs');
  path = require('path');

  module.exports = function(robot) {
    return robot.hear(/rect ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) (\w+)/, function(bot) {
      var svg, temppath;
      var width = 750;
      var height = 750;
      d3.select("body").node().innerHTML = '';
      var svgimage = d3.select("body").append("svg").attr({"width": width, "height": height });
      svgimage.append('rect')
      .attr({
        x:bot.match[1],
        y:bot.match[2],
        width:bot.match[3],
        height:bot.match[4],
        fill:bot.match[5]
      });
      robot.send(bot.envelope, "writing..");
      var canvas = fabric.createCanvasForNode(width, height);
      temppath = path.join(__dirname, '..', 'tmp');
      url = process.env.HEROKU_URL
      if (url === undefined) {
        url = 'http://localhost:8080'
      }
      var svg = d3.select('body').node().innerHTML;
      console.log(svg);
      fabric.loadSVGFromString(svg , function(objects, options) {
        options.top = 0;
        options.left = 0;
        for (var i=0; i<objects.length; i++) {
          objects[i].set ({ angle: 10 });
        }
        var svgGroups = fabric.util.groupSVGElements(objects, options);
        canvas.add(svgGroups).renderAll();
        try {
          fs.statSync(temppath);
        } catch (e) {
          console.log(e);
          fs.mkdirSync(temppath, 0700);
        }
        mktemp.createFile(path.join(temppath,'XXXXXXXX.png'), function(err, filename) {
          var outStream, stream;
          stream = canvas.createPNGStream();
          outStream = fs.createWriteStream(filename);
          outStream.on('close', function() {
            robot.send(bot.envelope, url + "/hubot/tempget.png?id=" + path.basename(filename));
          });
          stream.on('data', function(chunk) {
            outStream.write(chunk);
          });
          stream.on('end', function(chunk) {
            outStream.end();
          });
        });
      });
    });
  };
}).call(this);

動かしてみる

チャット画面で、「rect 100 100 200 200 yellow」のように入れます。
表記は、「rect x座標 y座標 幅 高さ 色指定」 です。

今回は、fabric側の操作を入れてみたかったので、fabricにsvgを渡した後で、10度回転させてみました。

test2.png

fabricでオブジェクトの整列とかもできるようなので、D3.js以外とも組み合わせてパワフルな画像操作ができそうですね。

63
64
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
63
64