全デバイス・全ブラウザで PDF を読みたい

TL;DR

  • PDF を画面に埋め込む方法は、iframe, object, embed, Viewer(3rd party library の利用)がある。
  • ブラウザネイティブの PDF 表示機能はブラウザ差異が大きいため、PDF を canvas や svg に変換して表示するライブラリやビューアーを利用した方が安定する。
  • しかし 3rd party library / service の利用はバンドルサイズやランタイムでの変換にコストがかかるため、なるべくブラウザネイティブなやり方で PDF を開きつつ、一部ブラウザ向けに対してのみ 3rd party library/service 経由で表示するように分岐させたい。
  • どのブラウザならブラウザネイティブの機能が使えるかを調べるために、サポート範囲の全端末・全ブラウザで PDF の描画結果を比較・調査した。

はじめに

業務委託エンジニアの井手(@sadnessOjisan)です。 今回は KAIZEN Sales の開発で作った "全ブラウザ・全端末対応の PDF コンポーネント" を作るにあたって、PDF の埋め込み方法について調査した結果を紹介します。

先に結果を書くと以下の通りです。

- Chrome
(PC)
Safari
(PC)
Firefox
(PC)
Safari
(SP)
Chrome
(SP)
IE
iframe △(no toolbar) ×(1 枚目のみ) × ×
object △(no toolbar) ×(1 枚目のみ) × ×
embed △(no toolbar) ×(1 枚目のみ) × ×
Google Drive △(不安定) △(不安定)
PDF.js(2.3.2~) ×
PDF.js legacy(2.3.2~) ×
PDF.js(~2.3.2)

KAIZEN Sales の説明と PDF コンポーネントの要件

KAIZEN Sales は企業が商談や営業に使う動画販促資料を管理するプラットフォームです。 その中に顧客企業がリンクを発行して、利用者にそのリンクの中にある動画や画像や PDF を見せてアンケートを取る機能があります。 今回僕が開発していたのはこのリンクで開かれるアンケートページと、そのリンク先にあるアンケートを顧客 HP にそのまま埋め込める 3rd party script です。

big buck bunny
KAIZEN sales の例

(※ Big Buck Bunney is licensed under CC-BY.)

この案件ではユーザーがコンテンツを視聴するページを開発し、その中で PDF を閲覧できるようにしました。ただこの PDF(コンテンツ)表示ページは外部リンクや 3rd party script として広く配布されるため PV も大きくなりやすく、また PC/Mobile や サポート対象の全ブラウザへの対応が必須となりました。

そこで、どの方法を使ってどの端末でアクセスすると PDF が表示されないかを調べるために、実験用の web サイトを作って、いろいろな方法や端末やブラウザを実験していました。 こちらがその実験用のサイトです。

FYI: https://github.com/ojisan-toybox/universal-pdf-component

PDF をページに埋め込むための方法

今回僕は下記の方法を実験しました。

  • iframe tag に埋め込む
  • object tag に埋め込む
  • embed tag に埋め込む
  • Viewer を呼び出す

iframe tag に埋め込む

iframe は

The <iframe> HTML element represents a nested browsing context, embedding another HTML page into the current one.

とある通り、別 webpage のコンテンツをそのまま埋め込めます。

FYI: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe

多くのブラウザではブラウザ自体が PDF の Viewer としても使えるため、pdf の src を iframe で指定すれば、iframe 越しに PDF を読めます。

iframe で PDF を表示させる例
iframe で PDF を表示させる例

object tag に埋め込む

object は

The <object> HTML element represents an external resource, which can be treated as an image, a nested browsing context, or a resource to be handled by a plugin.

FYI: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object

とあり、外部リソースの読み込みに利用できるタグです。その気になれば img 要素のように画像を読み込むことができます。

<object data="/path/to/image.png" width="250" height="200"></object>

PDF も同じように読み込めます。

<object
  data="https://ojisan-toybox.github.io/universal-pdf-component/example.pdf"
  type="application/pdf"
  width="800px"
  height="400px"
></object>

object で PDF を開く
object で PDF を開く

embed tag に埋め込む

embed も

The <embed> HTML element embeds external content at the specified point in the document.

FYI: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed

とのことで、object のようにコンテンツをブラウザに埋め込めます。

<embed
  width="800"
  height="400"
  src="https://ojisan-toybox.github.io/universal-pdf-component/example.pdf"
  type="application/pdf"
>

embed で PDF を表示する
embed で PDF を表示する

iframe, object, embed の使い分け

ではこれらのタグをどのようにして使い分けるとよいでしょうか。 実際のところPDFを表示させるにあたっては機能的な差異はほとんどないため、悩みます。

MDN には外部リソースの読み込みタグの歴史について解説したページがあり、embed, object, iframe の歴史や変遷について解説されています。 それによると embed, object は Java アプレットや Flash などのプラグイン技術を想定しており、今現在において使う機会はあまりないと言及されています。

FYI: https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Other_embedding_technologies

しかし私は iframe もしくはobject を使うべきだと思っています。 なぜなら iframe, object は fallback できるからです。

<object data="data/test.pdf" type="application/pdf" width="300" height="200">
  表示されない場合はこちらからDLしてください:
  <a href="data/test.pdf">test.pdf</a>
</object>

そして、iframe は第三者のコンテンツをウェブサイトに組み込む場合に使われているイメージがあるので、自分でホスティングしたPDFの表示にはobjectを使いました。 object の利用が推奨されていない理由はプラグインに頼るのを推奨しないという意味であり、PDFの閲覧という目的においては問題がない認識です。 iframe, object はどちらを使っても良く、好みの問題だと思っています。

ブラウザネイティブな方法の弱点

このように、ブラウザには PDF を読み込む方法が用意されているので、ブラウザの機能を使えばPDFを埋め込めるコンポーネント作成の要件を満たせそうです。 しかし、いざ使ってみると次のような問題に出会うでしょう。

モバイル Safari で paging できない

モバイル Safari では、iframe, object, embed を利用した際どのタグでも表示されますが、PDF のページ数が 2 ページ以上あるときに 1 枚目の内容しか表示されず、スクロールできません。

Safari ではスクロールできない
Safari ではスクロールできない

同様の問題は stackoverflow でも紹介されており、ブラウザ側の不具合もしくは仕様とみてよさそうです。

https://stackoverflow.com/questions/15480804/problems-displaying-pdf-in-iframe-on-mobile-safari

Android Chrome では表示されない

さらに Android の Mobile Chrome では 1 枚目すらも表示されませんでした。

Android Chrome では表示されない
Android Chrome では表示されない

ちなみに PC の Chrome では正常に表示されます。

IE も表示されない

PC においても IE だと表示されません。

iframe の場合

iframe が IE で表示されない
iframe が IE で表示されない

embed の場合

embed が IE で表示されない
embed が IE で表示されない

Safari(PC) ではツールバーの仕様が異なる

ツールバーの代わりにダウンロードリンクや PDF Viewer への導線が表示されます。

Safari ではツールバーが異なる
Safari ではツールバーが異なる

ブラウザの実装依存な機能に頼るのは怖い

このようにブラウザネイティブな機能に頼るとブラウザの差異が大きく、さらに利用するブラウザによってはユーザーからすれば機能不備と感じられるものもあります。(※ 個人的には、PDF の扱い方が何かで規定されているわけではないのでブラウザベンダの機能不備とは思っていません) そのため全端末・全ブラウザでの安定運用を考えると、ブラウザに頼るのは安定しなさそうです。

Viewer を用意して対応する

そこで Viewer と呼ばれる概念を使った PDF 閲覧の方法を紹介します。

Google Drive

裏技的な方法ですが、Google Drive が持つ PDF Viewer が、クエリパラメータを使って任意の PDF を読めることを利用して、その URL を iframe に埋め込んで閲覧する方法があります。

FYI: https://stackoverflow.com/questions/15480804/problems-displaying-pdf-in-iframe-on-mobile-safari

例えば、 https://ojisan-toybox.github.io/universal-pdf-component/example.pdf という PDF を表示させたければ、

<embed
  src="https://drive.google.com/viewerng/viewer?embedded=true&url=https://ojisan-toybox.github.io/universal-pdf-component/example.pdf"
  }
/>

とするだけで良いです。

この方法では、アプリ開発者側では実装が不要なので、作業工数やバンドルサイズの節約ができます。 しかし、アプリ開発者側で挙動の制御はできないので、細かい要件に対応するには向きません。 またこの方法は IE やモバイルでも対応できる方法ですが、なぜかたまに表示されないという問題があるので、少し安定さには欠けます。

FYI: https://stackoverflow.com/questions/26934520/google-drive-pdf-viewer-does-not-work-anymore-on-android

PDF.js

PDF.jsは Mozilla が開発している、Web 上で PDF を閲覧可能にするツールです。

PDF.js
PDF.js

公式の説明では、

A general-purpose, web standards-based platform for parsing and rendering PDFs.

とあり、PDF の parse と rendering を行ってくれます。 解釈された PDF は canvas もしくは svg に変換されます。 オプション次第では、テキストやリンク情報も保持できるので、テキストコピーやリンクを使った遷移も可能です。

また公式がクエリパラメータを使って任意の PDF を読めるビルド済みサイトを配布しているため、それを自前でホスティングすれば先ほどの Google Drive のような使い方が可能となります。

<embed
  src={`https://ojisan-toybox.github.io/pdfjs-file-viewer/?file=https://ojisan-toybox.github.io/universal-pdf-component/example.pdf`}
  width="500"
  height="375"
>

PDF.js の例
PDF.js の例

IE サポートはあるのか

ただし、IE でのサポートは注意が必要です。

#12602Frequently-Asked-Questions#faq-support を見るに、なんと今の PDF.js は IE サポートがありません。

IEがサポートされていないという比較表
IEがサポートされていないという比較表

PDF.js 公式が配布している Prebuilt (for older browsers) という legacy バージョンも IE サポートがされていません。ためしにその zip の中にある PDF.js で => と検索すると arrow function が見つかります。

legacy 版に arrow が含まれる
legacy 版に arrow が含まれる

ちなみに fgrep した箇所が webpack の変換が効いていそうな箇所であることと最新バージョンでは webpack5 でビルドされていることから、webpack5 がデフォルトで arrow function を吐くようになった ことによる影響かと思い、webpack のビルドオプションを切り替えてビルドしなおせば IE 対応できるのではと思ったのですが、IE サポートが切れる v2.4.456 は webpack4 でビルドされていたのでそれが理由ではなさそうでした。そもそも公式が IE 対応しないと言っているので、最新バージョンを使う以上は IE 対応を諦めるのが吉かと思います。

この arrow は v2.3.200 には入っていないので、もし IE 対応をしたいのであればこのバージョンを使いましょう。 このバージョンで IE 対応した Viewer を用意したので、もし興味がある方はこちらをお使いください。

FYI: https://github.com/ojisan-toybox/pdfjs-file-ie11-viewer

ツールバー

ちなみに PDF.js 公式が配布している Viewer のツールバーは、 Firefox が備えている Viewer のそれと同じデザイン・機能です。 Firefox が PDF.js を利用しているからなのか、PDF.js が Firefox の技術を利用しているのかはわかりませんが、同じ開発元だからと考えられます。

FireFox で開くPDF
FireFox で開くPDF

KAIZEN Sales での選択

さて、これらの実験結果を踏まえて KAIZEN Sales での技術選択を考えます。

要件

KAIZEN Sales では、PDF 閲覧機能を 3rd party script としても配布しなければいけなく、Mobile, IE でも動作させる必要があるため、

  • バンドルサイズを落とすためにブラウザネイティブな機能を使いたい
  • ランタイムでのパフォーマンス向上のためにブラウザネイティブな機能を使いたい
  • そのうえで IE, Mobile でも動作させたい

という要件があります。

実装

そこで、基本的には object タグを用いて PDF を埋め込み、UA が mobile, IE なら Google Viewer 越しに iframe で PDF を埋め込むといった実装をしました。そして表示されなかった場合の保険として、PDF のダウンロードリンクを用意しました。

KAIZEN Sales で見るPDF
KAIZEN Sales で見るPDF

万能ツールである PDF.js に寄せなかったのは、PDF の解釈・canvas/svg への変換をライブラリ越しで行われると、パフォーマンスが落ちるためです。また 3rd party script としても配布するため、PDF.js をバンドルに含めたくないという理由もあります。(※ これは iframe 超しに PDF.js の viewer を呼べば防げます。ただし別のサーバーにホスティングする必要があり、当時の開発リソース的な問題でこの選択をしませんでした。そのため開発リソースに余裕が出たフェーズで Google Drive から PDF.js への移行する予定です)

ツールバーの制御

Chrome では一定以上の width になるとツールバーが表示されます。 Chrome に備わっているツールバーは PDF の minimap のようなものが左側に表示されるため、PDF の閲覧領域が狭くなり、これを無くして欲しいと言う要望がありました。 これはブラウザネイティブな機能であるため難しそうに思えますが、実は PDF の URL へのクエリパラメータで制御できます。

たとえば URL に #view=FitV&pagemode=none&toolbar=0 をつけるだけでヘッダや minimap を消せます。 このような機能はブラウザの実装依存な機能であるため、この方法に気づくには苦労しました。

ちなみに PDF.js で同様のことをしたい場合は、PDF.js の Viewer の UI(PDF 除く)が DOM であることを利用して、PDF.js の Viewr それ自体の HTML・CSS を直接いじることで制御できます。 もっとも、PDF.js のツールバーは表示領域を圧迫しないのでこの対応は不要かと思います。

おわりに

iframe, object, embed を使った PDF の表示については各ブラウザの実装依存であるため、各ブラウザを使った時の差異の把握に苦労しました。 しかし、3rd party の code を頼らずに PDF を表示できるのがこの方法の強みではあるので、ブラウザの各挙動の勉強と追従を頑張り、このやり方を続けたいです。 もし私たちと同じようにブラウザネイティブなやり方で PDF を表示させたい方にとって本記事が役に立ってくれれば幸いです。

とはいえバンドルサイズ・ランタイムでの変換コストを考えずに全環境での PDF 表示をしたいのであれば、古い PDF.js を使って PDF を表示させるのが一番良いと思います。

We're hiring!

PDF や 動画の最高の視聴体験を一緒に作りませんか?