Heroku の PhantomJS buildpack

つくりました。

GitHub - stomita/heroku-buildpack-phantomjs

何をする?

PhantomJS (www.phantomjs.org) はheadless(GUI環境を必要としない)のWebKitで、JavaScriptおよびCoffeeScriptスクリプトを書くことができます。

このPhantomJSをheroku cedar stackで実行可能にしたものが、heroku-buildpack-phantomjs です。

※ heroku devcenter ドキュメントの 3rd party buildpack リストに載っけていただきました
https://devcenter.heroku.com/articles/third-party-buildpacks

利用方法

READMEに書いてあるように、以下のコマンドでheroku cedar上にアプリを作成できます。

$ heroku create --stack cedar --buildpack http://github.com/stomita/heroku-buildpack-phantomjs.git

とりあえず試しにスクリーンショットを取得するサーバを書いてみます。

screenshot-server.js:

var fs = require('fs');

var formHtml = [
  '<html><body>',
  '<form method="get">',
  'URL: <input type="text" name="url"><br>',
  'Width: <input type="text" name="width" value="320"><br>',
  'Height: <input type="text" name="height" value="480"><br>',
  'Filetype: <select name="ftype">',
  '<option>jpg</option><option>png</option><option>pdf</option>',
  '</select><br>',
  'Wait: <input type="text" name="sleep" value="400"><br>',
  '<input type="submit" value="Get Screenshot">',
  '</form>',
  '</body></html>'
].join('');

function toNumber(str, defaultValue) {
  var n = Number(str);
  if (isNaN(n)) { return defaultValue; }
  return n;
}

function parseQueryString(url) {
  var params = {};
  var qstr = url.split('?', 2)[1];
  if (qstr) {
    var pairs = qstr.split('&');
    for (var i=0; i<pairs.length; i++) {
      var pair = pairs[i].split('=');
      params[pair[0]] = decodeURIComponent(pair[1]);
    }
  }
  return params;
}


var port = phantom.args[0];

var server = require('webserver').create();
console.log('server listening port: '+port);
var service = server.listen(port, function (request, response) {
  var params = parseQueryString(request.url);
  if (!params.url) {
    response.statusCode = 200;
    response.write(formHtml);
    response.close();
    return;
  }
  var url = params.url;
  var ftype = params.ftype || 'jpg';
  var width = toNumber(params.width, 320);
  var height = toNumber(params.height, 480);
  var sleep = toNumber(params.sleep, 400);
  var page = new WebPage();
  page.viewportSize = { width: width, height: height };
  page.open(url, function (status) {
    if (status !== 'success') {
      response.statusCode = 500;
      response.write('Unable to load the address!');
      response.close();
    } else {
      setTimeout(function() {
        var file = "/tmp/image_"+ Math.random().toString(36).substring(2, 10) + "." + ftype;
        page.render(file);
        var contentType =
          ftype === 'jpg' ? 'image/jpeg' :
          ftype === 'png' ? 'image/png' :
          ftype === 'pdf' ? 'application/pdf' : 'application/octet-stream';
        response.statusCode = 200;
        response.setHeader("Content-Type", contentType);
        var stream = fs.open(file, "rb");
        response.write(stream.read());
        stream.close();
        fs.remove(file);
        response.close();
      }, sleep);
    }
  });
});


Procfile:

web:  phantomjs screenshot-server.js $PORT

heroku上に配布したものがこちらになります。

http://growing-lightning-3614.herokuapp.com/

これまで

実はすでにPhantomJS buildpackは PhantomJS 1.4.1ベースで作成していましたが、Xvfb(X11のheadless)を必要としていました。そのため、buildpack内にXvfb環境を埋め込んで提供していたので、slug sizeが90MB近くになっていました(これでも結構削った結果)。

今回、1.5.0ベースにしたことで、pure headless (X11を必要としない)となり、slug sizeは17MBほどまで減りました。

制限

まだ日本語フォントが使えません。headlessなPhantomJSでフォント追加する方法を調査中です。
なお @font-face でWeb Font なら使えるかな、と思いましたが、対応してないようです。

(追記)
fontconfigを使う環境では追加のTTFフォントを ~/.fonts に入れておくだけで大丈夫な模様。(参考:書体関係 Wiki - unixuser200403-2
なので、日本語フォント対応するには、IPAフォントなりをダウンロードし、.fontsディレクトリを作成してTTFファイルを突っ込んだ上でherokuにpushしておけばOK。
(容量の問題もあるのでbuildpackには含めていません)

ビルド方法メモ

以下、個人的なメモとしてのbuildpack作成の手順。

https://gist.github.com/2725125
https://gist.github.com/2725122

vulcanに以上の2つのスクリプトを載せてビルドプロセスを走らせれば良いわけだけど、どうもvulcanはビルドの途中で落ちてしまうことが多い。PhantomJSのソースコードコンパイルはQtを含んでおりheroku上で優に1時間くらいはかかるので、落とされると大変困る。heroku run bash で対話的にやるほうがよいかとおもう。ビルド結果は自分でアーカイブしてscpで適当なサーバに転送した。