Block Rockin’ Codes

back with another one of those block rockin' codes

PhantomJS で HTML のスライドを PDF にし SlideShare にあげる方法

本文

最近は HTML 形式のプレゼンテーションツールも多くなってきました。
しかし、そうしたツールで作ったスライドはそのままでは SlideShare にあげることが出来ません。

もちろん HTML なのでサーバに上げて URL を共有することもできますが、
やはり、「終わったら SlideShare で共有」ってのは割と恒例になってるし、一元管理できるし、色々便利です。

方法としては、 HTML を PDF とかに直せれば良いのですが、
「見た目そのまま」で PDF 化するのは結構難しいです。

一番実直な方法は一枚づつスクリーンキャプチャを取る方法ですが、面倒です。
自分も色々試したんですが、なかなかうまくいかず。
しかし、 @ さんが同じことに取り組んでおり、ヒントをもらったところ、成功したので紹介します。


今回使った方法は PhantomJS を使ってキャプチャ取得を自動で行い、
PDF 化して SlideShare にそのままの見た目であげられるようにする方法です。
この方法で色々と便利になると思います。眠ってる HTML スライドがあるかたも、貴重な資料の共有手段の一つとして是非。
ちなみに以下で使用した環境は Mac です。

流れ

  1. HTML スライドの各ページに URI をふる(ハッシュでも可)
  2. PhantomJS のラスタライズ機能を使って、全ページを PNG 化する
  3. PNGsum2p で PDF に直す
  4. 個々の PDF を pdftk で一つの PDF にまとめる。
  5. SlideShare にあげる。

PhantomJS

PhantomJS はヘッドレスブラウザツールです。
WebKit のエンジンが入っており、云々。

PhantomJS のインストール

PhantomJS のインストールには、 Homebrew で入れる方法と、ソースからビルドする方法があります。
Homebrew で入れると Qt のビルドから始まり時間がかかるそうです。


そこで今回は Qt のビルド済みバイナリを入れて、 PhantomJS はソースからビルドしました。
(Qt のバイナリを入れてから Homebrew すればいけるのかもと後で気づきました。今度ためそう。)

Qt のインストール

PhantomJS を入れるためには Xcode と Qt が必要です。
Xcode は入ってますが、 Qt は入ってないので入れます。


下記ページの中にある
http://qt.nokia.com/downloads/qt-for-open-source-cpp-development-on-mac-os-x

これを入れます。

Cocoa: Mac binary package for Mac OS X 10.5 - 10.6 (32-bit and 64-bit)
http://get.qt.nokia.com/qt/source/qt-mac-opensource-4.7.4.dmg (211 MB, includes build and interface tools)


PhantomJS のビルド

PhantomJS のビルドは、下記の通り。

$ git clone git://github.com/ariya/phantomjs.git && cd phantomjs
$ qmake -spec macx-g++ && make

すると、

phantomjs/bin/phantomjs.app/Contents/MacOS/phantomjs

にバイナリができます。
これをパスの通ったところに移すなり、シンボリックリンクを貼るなりします。

$ ln -s ./bin/phantomjs /usr/local/bin/phantomjs

これで、 PhantomJS コマンドが使えるようになります。

PhantomJS の実行
$ phantomjs 
Usage: phantomjs [options] script.[js|coffee] [script argument [script argument ...]]

Options:
    --load-images=[yes|no]             Load all inlined images (default is 'yes').
    --load-plugins=[yes|no]            Load all plugins (i.e. 'Flash', 'Silverlight', ...) (default is 'no').
    --proxy=address:port               Set the network proxy.
    --upload-file fileId=/file/path    Upload a file by creating a '<input type="file" id="foo" />'
                                       and calling phantom.setFormInputFile(document.getElementById('foo'), 'fileId').

動いてるようです。

とりあえず example を実行してみるとこんな感じ。

$ cd example
$ phantom hello.js
Hello, world!

単純に、仮想ブラウザを通して実行した結果が出力されます。

ラスタライズ

ラスタライズは、仮想ブラウザで実行した結果のスクリーンショットを保存するような機能です。
オプションでは PNG と PDF のフォーマットが選べます。


ならここで PDF にすればいいと思うんですが、 PDF の方はスクショではなく、
ブラウザの印刷プレビュー時の形式(print css 適応のやつ)での保存になるので、
HTML 見た目そのままにしたい場合は、一旦 PNG にします。


phantom.js についてる example の rasterize.js あたりがほぼそのまま使えるんですが、
今回、自分は下記のようなファイルを作成しました。

var page = new WebPage(),
    address, output, size, width, height, paperwidth, paperheight;

address = phantom.args[0];
output = phantom.args[1];
width = phantom.args[2];
height = phantom.args[3];
paperwidth = phantom.args[4];
paperheight = phantom.args[5];

page.viewportSize = { width: width, height: height };
page.paperSize = { width: paperwidth, height: paperheight, border: '0px' }

page.open(address, function (status) {
  console.log(status);
  if (status !== 'success') {
    console.log('Unable to load the address!');
  } else {
    window.setTimeout(function () {
      page.render(output);
      phantom.exit();
    }, 200);
  }
});


これを rasterize.js で保存して、直で実行する場合のコマンドは以下です。


引数は

URI
スライドのアドレス
output
出力ファイル名
windowwidth
ブラウザの Window サイズ幅
windowheight
ブラウザの Window サイズ縦
viewportwidth
出力の横サイズ(cm)
viewporthheight
出力の縦サイズ(cm)
$ phantomjs rasterize.js URI output windowwidth windowheight viewportwidth viewporthheight


対象のスライドはローカルでサーバを立ち上げていて http://localhost:3000/slide.html#slide-0 だったとします。
自分が使ったのはこれ。
サイズはこちらを参考にしました。

$ phantomjs rasterize.js http://localhost:3000/slide.html#slide-0 slide-0.png 1366 768 48.77cm 17.43cm

これで一枚の PNG ができます。
パラメータを引数指定できるようにしてるのは、色々試したいからです。
決まってれば直で rasterize.js に書いても良いですが rasterize.js を直接いじるよりは、
これを呼び出す Make などを書く方がいいかもしれません。


自分は Cake (CoffeeScript で書く Make) でタスクを書いてやりました。


注意点としては、沢山のページを同時にラスタライズしようとすると、うまくいかない場合があります。
その辺もうまいこと工夫してみてください。

sam2p

sam2p はフォーマット変換ツールです。
これで生成した PNG を PDF に変換します。

sam2p のインストール

http://code.google.com/p/sam2p/downloads/list ここにバイナリがあるんですが、
Mac 用はなく、他のディストリ版はやはり動きませんでした。
ソースからビルドします。

ソースはこれ
http://sam2p.googlecode.com/files/sam2p-0.49.tar.gz

$ ./configure --enable-lzw --enable-gif 
$ . make

なんか最後の方色々怪しげなメッセージができつつも sam2p バイナリが完成。
しかし実行してもエラーがでました。

$ sam2p-0.49/sam2p example.png example.pdf
This is sam2p 0.49.
Available Loaders: PS PDF JAI PNG JPEG TIFF PNM BMP GIF LBM XPM PCX TGA.
Available Appliers: XWD Meta Empty BMP PNG TIFF6 TIFF6-JAI JPEG-JAI JPEG PNM GIF89a+LZW XPM PSL1C PSL23+PDF PSL2+PDF-JAI P-TrOpBb.
sh: png22pnm: command not found
sh: pngtopnm: command not found
sam2p: Error: Filter::PipeE: system() failed: (png22pnm -rgba /var/folders/qg/...

png22pnm, pngtopnm がないとのこと。


http://code.google.com/p/pdfsizeopt/wiki/InstallationInstructions によると

pngtopnm は png22pnm の代理らしいです。

pngtopnm: free (this is an replacement if you can't install png22pnm)


で、さがしてみたら pnp22pnm の max 版バイナリがありました。

http://code.google.com/p/pdfsizeopt/downloads/list

この中の png22pnm-darwin です。

png22pnm-darwin を npg22pnm にリネームして
chmod +x して /usr/local/bin にシンボリックリンクしました。

再度 sam2p を実行したら成功!

フォーマット変換

使い方は簡単。

$ sam2p slide-0.png sliede-0.pdf

PNG が PDF になりました。
これも一気に変換する cake を書いてやりました。

pdftk

pdftk(pdf tool kit) は PDF 関連の機能が詰まった便利ツールです。
これを用いて、バラバラの PDF を一つのファイルに連結します。

pdftk のインストール

http://www.pdflabs.com/docs/install-pdftk/Macintosh OS X Snow Leopard Installer
がありました、すごい!


dmg を落としてきて普通にインストール。

pdftk コマンドが使えるようになります。


PDF の連結

pdftk cat で連結します。

$ pdftk slide-0.pdf slide-1.pdf .... cat output slide.pdf

これで一つに連結されます。

コマンドのまとめ

$ phantomjs rasterize.js http://localhost:3000/slide.html#slide-0 slide-0.png 1366 768 48.77cm 17.43cm
$ sam2p slide-0.png sliede-0.pdf
$ pdftk slide-0.pdf slide-1.pdf .... cat output slide.pdf

Cakefile

Rake でもいいですが Cake でタスクを書いてみました。
まだ coffeescript は初心者なので、あまり色々な機能は使えていないと思います。
CoffeeScript は賛否有るけど、少なくともこういうツールをさくっと書きたい場面では手軽で良いと思います。


サーバはローカルで立てている前提です。。
PNG の生成で一旦確認したいので、二つのタスクに分けています。
また、ラスタライズは同時にたくさん起動するとうまくいかないページがあったので、少しインターバルを入れています。


使い方は -p でページ数を指定するだけです。
その都合からスライドのハッシュは連番にしています。

option '-p', '--page [page]'
task 'png', 'build png', (options) ->
  page = options.page
  command = 'phantomjs'
  script = 'rasterize.js'
  uri = ''
  width = 1366
  height = 768
  paperwidth = '48.77cm'
  paperheight = '17.43cm'
  commands = []

  for i in [0..page]
    uri = "http://localhost:3000/slide.html#slide-#{i}"
    output = uri.split('#')[1] + '.png'
    commands.push "#{command} #{script} #{uri} #{output} #{width} #{height} #{paperwidth} #{paperheight}"

  build = (c) ->
    exec c.shift() , (err, stdout, stderr)->
      throw err if err
      log stdout + stderr

    if c.length isnt 0
      setTimeout () ->
        build(c)
      , 1000
  build(commands)


option '-p', '--page [page]'
task 'pdf', 'build pdf', (options) ->
  slides = []
  page = options.page
  for i in [0..page]
    exec "echo 'sam2p slide-#{i}.png sliede-#{i}.pdf'" , (err, stdout, stderr)->
      throw err if err
      log stdout + stderr
    slides.push "slide-#{i}.pdf"


  slides = slides.join(' ')
  commands =  "pdftk #{slides} cat output slide.pdf"

  exec commands, (err, stdout, stderr)->
    throw err if err
    log stdout + stderr


実行はこんなかんじ。

$ cake -p 30 png
$ cake -p 30 pdf


我ながら恥ずかしいくらい汚いw もうちょっときれいに書きたいですw
あとスーパーpreってまだ coffeescript に対応していないんですね。
いずれ対応される事を見越して coffeescriptマークアップしておきます。

完成

これで完成です。
サンプルとして以下のファイルと、変換結果の slideshare にアップしたものを貼っておきます。


HTML:
PDF: http://www.slideshare.net/Jxck/nodefest2011live


HTML: HTML5 Presentation
SlideShare: http://www.slideshare.net/Jxck/test-it-9845589

雑感

PhantomJS に読ませた時点で、若干フォントなどが変わったりは有りますが、
まあ資料として見る分には特に問題ないでしょう。
もしくはその辺の CSS を修正すればいいだけです。


もとのスライドのスタイルの作り方によっては、うまくいかなかったりすることがあります。
うまくいかないページだけ取り直しながら、とにかく各ページの PNG が揃えばあとはこっちのものです。
(そこだけスクショでもいいかも)


結構面倒な手順を踏みましたが、ここまで全部コマンドラインから出来るため、
一度環境ができれば、あとは好きなように自動化できると思います。
画像ではなく文字をコピペしたいなどの場合は、別途 OCR をかませればいいでしょう。


また方法自体が、数ある HTML スライドツールのどれを使うかに依存しないあたりがうれしいですね。自作ツールでもなんでも使えます。
自分はこれで、 KeyNote や PTT から、いよいよ HTML ベースでのプレゼンに置き換えていくつもりです。


また、ツールが HTML なので JS や CSS3 の機能をつかったプレゼンが存分にでき、image, iframe も張れるのでスライドから他リソースの引用も HTML の流儀にそって行えるし、
その上で PDF にもできるのは自分にとっては大きいです。


ちなみに iframe を使用した場合は、 PhantomJS のラスタライズでメモリを食うので、そのページだけ別でやるとかが必要かもしれません。
もしくは画像にしておくとか。自分はそうしたページは、なんとなく iframe を消してリンクだけ書いておく感じでやりました。


ちなみに最近自分は deck.js というツールを使っています。
軽くてごてごてしてない、プラグインが書ける、ブラウザでの印刷用 CSS も入ってる、などが気に入ってます。


長くなりました。多少面倒かもしれませんが、是非ためしてみてください。
また他にもいい方法があったら(きっと出てくるはず)教えてください。