きちぽよ〜

ねむい

PDF.js で遊んでみた (ページの描画,テキスト・注釈の表示など)

修士論文の予備審査が終わって一段落した seikichi です.卒業できるといいなぁ…….

PDF.jsHTML5JavaScript を利用してPDFを描画するライブラリです. PDF.js を利用するとWebブラウザ上でPDFファイルを描画することができます. つい最近,このライブラリを利用して遊んでみた ので,そのときに得た知識とかを適当に書いてみます.

このエントリでは主に以下の内容について解説します.

  • PDF.js の取得,ビルド
  • ページの描画
  • テキストを選択可能な形式で描画
  • 注釈 (リンク等) を取り扱う
  • PDF.js の各種オプション

動作環境

PDF.js は Canvas や TypedArray など割とモダン(?)な技術を利用しています. 利用しているWebブラウザが,PDF.js の要求を満たしているかどうかは, このページ で確認できます. 正直なところ,全てのテストケースがをクリアしなくても,それなりの機能は使える (気がする) ので, そこそこ新しいブラウザなら結果は気にしなくても良い気がします.

PDF.js の取得,ビルド

取得するGithub のリポジトリ には ビルド済みのファイルが含まれていないので,自分でビルドする必要があります. ビルドには node.js が必要なので,事前にインストールしておきましょう.

  $ git clone git://github.com/mozilla/pdf.js.git
  $ cd pdf.js
  $ node make generic

上記のコマンド群を実行すると,build/generic/build ディレクトリに pdf.jspdf.worker.js という名前のファイルが生成されます. 生成された pdf.js を読み込めばライブラリを利用する準備は完了です. pdf.worker.jspdf.js から利用されるファイルなので,直接読み込む必要はありませんが, pdf.js と同じディレクトリに配置すると良いでしょう (他のディレクトリに配置する場合は後述する PDFJS.workerSrc の設定が必要). また build/generic/web/ ディレクトリ内の compatibility.jspdf.js の前に読み込んでおくと, ブラウザ間の差異を吸収してくれるので,是非とも利用しましょう.

ところで,これらのファイルの大きさを調べてみると

  $ ls -lh build/generic/build
  -rw-r--r--  1 seikichi  staff   231K 11 20 15:14 pdf.js
  -rw-r--r--  1 seikichi  staff   1.5M 11 20 15:14 pdf.worker.js

とどちらもかなりの大きさです (pdf.worker.js やべえなこれ...). README.md に 「公開する際は minify すべき」と書かれているので minify します.

  $ npm install uglify-js -g
  $ uglifyjs build/generic/web/compatibility.js build/generic/build/pdf.js -o pdf.min.js
  $ uglifyjs build/generic/build/pdf.worker.js -o pdf.min.worker.js

uglify-js のバージョンが古いと,生成された pdf.min.js を読み込んだ際に "Uncaught SyntaxError: Unexpected token ILLEGAL" と怒られたようなどうだっけ (うろ覚え). 以降の解説では,minify したファイルを利用しています.

PDFのページを描画する

準備も整ったので,いよいよ実際にPDFファイルを描画してみます (利用したPDFファイル). なおこのサンプルは PDF.js のリポジトリexamples/helloworld に含まれる内容とほぼ同じです.

JSFiddle のスクリプトからPDFデータを参照する上手い方法が思い浮かばなかったため,

  1. PDFファイルをbase64エンコードして文字列としてソースに埋め込み
  2. 実行時にPDFファイルのデータを Uint8Array に変換

という面倒な作業をしていますが,本質では無いのであまり気にしないで下さい.

例がシンプルすぎて,あまり解説する内容も無いですね. 強いて挙げるなら以下の4点でしょうか.

  • PDFJS.getDocument の第一引数に TypedArray を渡すとPDFファイルのデータだと解釈され, 文字列を渡すとPDFファイルへのURLだと解釈されます.
  • PDFJS.getDocument の第一引数にPDFファイルのURLを指定すると,内部では XMLHttpRequest (XHR) が利用されます. つまり異なるドメインに存在するPDFファイルを指定すると, CORS が有効でない場合にエラーとなります.
  • getPage メソッドの引数 (ページ番号) は 1-origin です (先頭ページの番号は0じゃなくて1ですよ,という話).
  • PDFJS.workerSrc に pdf.worker.js (もしくは pdf.worker.js を minify したファイル) へのパスを指定しないと, pdf.js 自身のパスの js$.worker.js に置き換えた文字列が pdf.worker.js へのパスとして利用されます.

ページの描画はこれでバッチリ,だと良かったのですが,よくよく結果を眺めてみると

  • テキストが選択可能でない (ドラッグしてテキストを選択してコピー,とかが出来ない)
  • リンク ("PDF.js" の部分です) を選択しても,何も起こらない

という問題があります.これらについては以降の節で解決します.

PDFのページに含まれるテキストを選択可能な形式で描画する

examples/text-selection にサンプルがあります. サンプルには "Minimal pdf.js text-selection demo" と書かれているのですが, pdf.jspdf.worker.js 以外に,ビューワに関するファイル (web/text_layer_builder.js など) が必要になったり JavaScript だけでなく CSS の設定も必要だったりで,一気に面倒になった感じがします.現実は厳しいですね…….

コードでは, canvas の上にテキストを表示するためのdiv要素を作成し, div要素のスタイルを適切に設定した後に, TextLayerBuilder オブジェクトを作成し, PDFPageProxy の getTextContent でページ内のテキストを取得し, PDFPageProxy の render メソッドの引数に TextLayerBuilder オブジェクトを渡す, という感じです.俺の説明能力が低すぎて,書いてて全然伝わる気がしない…….

さて,テキストが選択可能になったしページの描画はこれで完璧!と思いきや, ページ内のリンク ("PDF.js" の部分です) をクリックしても何も起こりません. 現実は厳しい……. ということで次は注釈を表示します.

PDFのページに含まれる注釈 (リンクなど) を描画する

PDF.js のリポジトリにサンプルはありません.悲しい……. というわけでビューワ (web/ 以下) のソースを読んで,必要最低限(?)の部分を抜き出してみました.

日本語で解説するのが面倒になってきたのでやりません.ソース参照.

API

いちいち解説してるとキリが無いですし,wiki にも まとまったエントリも見当たらないので,src/display/ ディレクトリ内の api.js(URL) を見るのが一番早いかと思われます.コメント充実してますし. 利用例を見たければ web ディレクトリ以下とかをどうぞ.

PDF.js のオプション

PDF.js にはいくつかのオプションがあります. 最初のサンプルで設定した,PDFJS.workerSrc もその1つですね. 詳細はこれもやっぱり src/display/ ディレクトリ内の api.js(URL) を読むのが一番早いと思うのですが,いくつか挙げてみると

  • PDFJS.workerSrc : ワーカーファイル (pdf.worker.js のことです) へのパス (文字列). 指定されなければ pdf.js 自身のパスの js$.worker.js に置き換えた文字列が利用されます デフォルト値は null
  • PDFJS.disableRange : PDFファイルのダウンロードに Range-Request を利用したくない場合は true を設定する. デフォルト値は false

Range-Request が有効になっていると (デフォルト),PDFファイルを表示するために, ファイルを全てダウンロードせず,必要な部分だけダウンロードするようになります. (「1ページ目を描画したいので,○○バイト目から□□バイト目までダウンロードさせて下さい〜」とサーバに要求するイメージ). ChromeFirefox の開発者ツールでネットワークの情報を眺めてみると面白いです. PDF.js めっちゃ便利ですごい (小学生並の感想)

いつ Range-Request するの?

DropboxGoogle Drive に置いた PDF ファイルを Range-Request で読み込みながら描画したいなぁ」などとわけわからんことを以前思いつきました (結果). DropboxGoogle Drive は公式ドキュメントで Range-Request をサポートしていると書いているのですが,PDFJS.getDocument で URL を指定しても,Range-Request が行われません. 何でだろうなぁと思いながら通信のログを眺めていると,... (疲れたので続きはまた今度書く)

まとまらないまとめ

!!PDF.js すごい!!

さんこうにしたじょうほう

補足: PDFビューワを利用する

PDF.js のリポジトリには,PDFビューワのプログラムが含まれています.Firefox で PDF ファイルを開くと出てくる アレですね. ビューワを試してみるにはリポジトリのルートディレクトリで以下のコマンドを実行し

  $ node make server

Webブラウザで http://localhost:8888/web/viewer.html を開いてみましょう. 面倒な人はこちらのデモをどうぞ. ページの表示のみならず,テキストの選択,検索,印刷,注釈の表示などなど, PDFビューワとして十分な(?)機能を備えているのが分かります.もじらぱない(小並感).

補足: Promise について

jQuery.Deferred や Q や JSDeferred などのライブラリを利用されたことがある方は, この節をスキップして大丈夫です.

ここまで挙げてきたサンプルコードには then というメソッドの呼び出しが何度も登場しました. PDF.js のAPIの多くは,Promise オブジェクトを結果として返します.

  var result = PDFJS.getDocument({url: 'path/to/pdf/file'});
  console.log(result); // => PDFのドキュメントに関するオブジェクトではなく,Promise という型のオブジェクトが表示される

これは PDF.js が「pdf ファイルを開く」,「ページを取得」や「ページの内容を描画」などといった処理を (Web Worker を使ったり使わなかったりして) 非同期に行っているからです.

さて,その Promise の使い方ですが,

  var promise = PDFJS.getDocument({url: 'path/to/pdf/file'});
  promise.then(function(pdf) {
    // 成功した場合の処理
  }, function(reason) {
    // 何らかの原因で処理が失敗したときの処理 (省略可)
  });

みたいな使い方をします. 更に,Promise.then の引数で別の Promise を返すと

  PDFJS.getDocument({url: 'path/to/pdf/file'}).then(function(pdf) {
    return pdf.getPage(1); // getPage は Promise を返します
  }).then(function(page) {
    // ページを使った何らかの処理
  });

みたいに書けます.便利ですね (ほんまか).