pdf.jsを使いブラウザで見られるPDFスライド表示ツールを作った
追記: ライブラリとして切り出しました
どういうもの
https://azu.github.io/slide/DOMQuery/sourcemap.pdf というPDFファイルを読み込んで表示しています。
普通のHTMLスライドのようにウェブページとして公開することも出来ます。
例) https://azu.github.io//slide/DOMQuery/
作った経緯
mozilla/pdf.jsを使えばPDFをブラウザ上で表示出来るので、これを使ったプレゼンテーションツールとかあると面白そうな気がしたのが始まりです。
こういうのが欲しい理由としては以下のような感じでした。
- Deckset みたいにPDFしか配布用のフォーマットがないものがある
- Deckset自体は便利なので使いたい
- でもSlideShareやSpeaker Deckにロックインされたくない
- そもそもSpeaker Deckは素のPDFを見たほうがましという状況がよくあるのが問題…
- 今までGitHubにスライドおいてきた - 今まで発表したスライド一覧 azu/slide @ GitHub
- GitHub Pagesにスライドを置くという同じスタイルを取れる
- 直接PDFだとはやっぱり避けられる所がある + ブラウザによって表示形態がスライドに適してない
- PDFを出力できるアプリは多いので、意外と汎用的に対応できる気がする
- PowerPoint、KeyNote、DeckSet、Reveal.jsなど
なので、pdf.jsのLearningをみて作ったのが始まりです。
slide-pdf.jsの構成
slide-pdf.js がpdf.jsを使ってpdfスライドを読み込んでスライド的に表示する本体。
という感じURLにpdfファイルを指定するとスライド表示してくれるようにした。same originでpdfファイルを読み込めるなら何でも行ける。
pdf.jsは自動でpdfファイルをちょっとづつ取得して表示するので意外大きなファイルでも大丈夫そうだった。
これを<iframe>
を使って埋め込んだページ公開するのを意図した感じで作ってあります。(なので https://azu.github.io//slide-pdf.js/ 単体は画面ピッタリ)
<iframe id="main-slide"
src="https://azu.github.io/slide-pdf.js/?slide=https://azu.github.io//slide/DOMQuery/sourcemap.pdf"
scrolling="no"
allowtransparency="true"
width="100%"
height="100%"
style="border:0;">
</iframe>
という感じですね。
埋め込み例: https://azu.github.io//slide/DOMQuery/
埋め込み例をスクロールしてみるとHTMLとしても読めるようになっています。(検索等がしやすくなる)
DecksetなどはMarkdownからスライドを作るので、MarkdownをHTMLにしたものを一緒に配置するようにしてあります。 (SlideShareでやってる、スライド内容のテキスト出力をインスパイアしてつけた。)
上記のページでは以下のような事をしています。
- iframeの埋め込み
- markdownからメタ情報を取得
- markdownをhtmlにしたものを埋め込み
- failbackとしてpdfを直接ダウンロード出来るように
- noscriptでも読むことができる
さすがに毎回こういうのを手動でやるのは面倒なだけなので、PDFのURLやMarkdownファイルを指定したら自動的に生成してくれるジェネレーターも書きました。
$ pdf-slide-html -h
Usage:pdf-slide-html [options]
-h, --help displays help
--base-url String slide base-url
--pdf-url String pdf file path
--markdown String markdown file path
-o, --output String output file path
--base-url
には、iframeで埋め込むURLのベースを指定しますが ( https://azu.github.io//slide-pdf.js/
)、same origin policyがあるため使う場合は自分でcloneして、pdfがsame origin的に読み込める位置に配置する必要があります。
pdf-slide-html --pdf-url https://azu.github.io/slide/DOMQuery/power-assert-in-browser.pdf --base-url https://azu.github.io//slide-pdf.js/ --markdown power-assert-in-browser.md -o index.html
という感じの長い指定をすれば、自動的に先ほどのindex.htmlを作ってくれます。
Decksetとか組み合わせると、 HTML自体もいい感じに読めるし検索性も高まるので便利な気がしますが自分用感はありますね。
一応、iOSなどでもpdf.jsは動くので読めると思います。
pdf.jsについて
pdf.js ではpdfをレンダリングすることが出来ます。
Canvasによるレンダリングですが、pdfのテキストを取得して透明テキストをCanvasの上に配置することが可能です。
またPDF内のリンクをクリック出来るように、PDFからメタ情報を取得したボタンを配置することも出来ます(speakerdeckはこれができてないのが辛い)
ただし、用意されてるサンプルにはCanvasのレンダリングの例しかないので、選択出来るテキストの配置とリンクの配置は自分でコードを書いて載せる必要があります。
また、npmやbowerからインストールすることができるmozilla/pdfjs-distというビルド済みのものが公開されています。 しかし、このdistにはテキストレイヤーの作成に必要なものが含まれてないので別途mozilla/pdf.js自体も取ってくる必要があります。
簡単に仕組みを書くと、pdf.jsではpageごとに情報を取得することができ、さらにそのpageからテキストコンテンツを取得することができます。
そのテキストをpdf.js/web/text_layer_builder.js
にあるTextLayerBuilder
で組み立てると座標と文字列を組み合わせたものを作って配置してくれます。
pdfDoc.getPage(pageNum).then(function (page) {
page.getTextContent().then(function (textContent) {
var textLayerBuilder = new TextLayerBuilder({
textLayerDiv: domMapObject.textLayer,
viewport: viewport,
pageIndex: 0
});
textLayerBuilder.setTextContent(textContent);
})
});
リンクをクリック出来るようにするのも同様に、page.getAnnotations()
でリンクを含めたアノテーション情報を取得できるので、これ得た座標にクリックできる要素を配置する感じです。
function setupAnnotations(page, viewport, annotationArea) {
return page.getAnnotations().then(function (annotationsData) {
viewport = viewport.clone({
dontFlip: true
});
for (var i = 0; i < annotationsData.length; i++) {
var data = annotationsData[i];
if (!data || !data.hasHtml) {
continue;
}
var element = PDFJS.AnnotationUtils.getHtmlElement(data);
var rect = data.rect;
var view = page.view;
rect = PDFJS.Util.normalizeRect([
rect[0],
view[3] - rect[1] + view[1],
rect[2],
view[3] - rect[3] + view[1]
]);
element.style.left = (rect[0]) + 'px';
element.style.top = (rect[1]) + 'px';
element.style.position = 'absolute';
var transform = viewport.transform;
var transformStr = 'matrix(' + transform.join(',') + ')';
CustomStyle.setProp('transform', element, transformStr);
var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px';
CustomStyle.setProp('transformOrigin', element, transformOriginStr);
if (data.subtype === 'Link' && !data.url) {
continue;
}
annotationArea.appendChild(element);
}
});
}
やや複雑なのでデフォルトで用意して欲しい感じもしますが、テンプレ的な感じで一度動かせれば結構色々出来そうな感じです。
pdf.jsについては公式のサンプルと以下がとても参考になります。(一部そのままでは動かなかった気がする)
pdf.js 思っている以上に普通に動いて、APIもPromiseベースだったり面白い感じなのでPDFを使った何かを作りたい時はいじってみると楽しいと思います。
FAQ
Q. PDF選択出来るようになったけど文字化けする
おそらくbcmapsファイルが読み込めてないのだと思います。ネットワークパネルで/bcmaps/が404になっていたらそれです。
その場合は、以下でcmapsのディレクトリに対してのパスを定義すれば読み込んでくれます。
// define lang
PDFJS.cMapUrl = "../cmaps/"; // /をつける
PDFJS.cMapPacked = true; // 拡張子にbcmapを付けるか
これはPDFの内容によって動的に読み込むため(全部読み込むと数MBあるので)、そのリクエストするURLパスが解決出来てないことが原因だと思います。
まとめ
- slide-pdf.js
- pdf.jsを使ったiframeで埋め込みPDF表示ツール
- azu/pdf-slide-html
- 上記の埋め込みツールを使ったページを吐き出すジェネレータ
今使おうと思うと、自分でslide-pdfをcloneして配置する必要があったりちょっと面倒だと思います。( azu/pdf-slide-htmlも雑なので使いにくい)
もっと使いやすくする pull requestなど待ってます!
追記: ライブラリとして切り出しました
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。