はじめに
Webアプリでユーザーにマニュアルや請求書のPDFを見せたい、という場面って結構あるんですよね。でも、ブラウザによってはプラグインが必要だったり、外部ビューアに飛ばしたりと、なかなかスマートにいかない。
そこで使えるのが PDF.js というライブラリ。Mozillaが開発していて、GitHubスターは52,000超え。Firefoxに標準搭載されているPDFビューアの中身そのものです。
正直なところ、PDFをWeb上でまともに扱いたいならこれ一択ですね。
とはいえ、説明を読むより先に実際に動かしてみた方が早いと思います。1ページの小さなPDFをBase64で埋め込み、canvasへの描画・ページ送り・テキスト抽出までその場で試せるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
PDF.jsとは
PDF.jsは、HTML5とJavaScriptだけでPDFを解析・レンダリングするライブラリです。Web標準技術のみで動作するので、プラグインもFlashも不要。
基本情報:
| 項目 | 内容 |
|---|---|
| 開発元 | Mozilla |
| ライセンス | Apache 2.0 |
| GitHubスター | 52,400+ |
| コントリビューター | 407人 |
| 初回リリース | 約15年前 |
FirefoxユーザーならすでにこのPDF.jsを毎日使っているはず。ブラウザ内蔵のPDFビューアがまさにこれなんですよね。
特徴・メリット
1. プラグイン不要
Flash PlayerやAdobe Readerといった外部プラグインが一切必要ない。純粋なJavaScriptとCanvas/SVGだけで動作する。これ、意外と大きなメリットで、企業のセキュリティポリシーでプラグインが禁止されている環境でも問題なく使える。
2. クロスブラウザ対応
Chrome、Firefox、Safari、Edgeなど主要ブラウザで動作する。モダンブラウザ向けとレガシーブラウザ向けの2種類のビルドが用意されていて、IE11をサポートする必要がある場合も対応可能。
3. カスタマイズ性
ビューアのUIを自由にカスタマイズできる。ページ送り、ズーム、検索、印刷など、標準機能を組み合わせて独自のPDFビューアを作れる。
4. 大規模な実績
Firefoxに統合されているということは、数億人のユーザーが日常的に使っているということ。バグが少なく、互換性も高い。これは個人的に重要視しているポイント。
インストール方法
npmで簡単にインストールできる。
npm install pdfjs-dist
yarnの場合:
yarn add pdfjs-dist
CDNを使う場合:
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.min.mjs" type="module"></script>
PDF.jsのサンプルを動かす
PDF.jsを試すには、表示対象のPDFファイルが必要です。とはいえ外部URLのPDFを毎回フェッチするのはこの手のサンプルには向かないので、ここでは1〜3ページだけのごく小さなPDFをBase64文字列としてコードに直接埋め込み、pdfjsLib.getDocument({ data })にそのバイト列を渡す形にしています。実際のアプリではここがfetchで取得したPDFやファイル入力(<input type="file">)から読み込んだArrayBufferに置き換わるだけで、以降のAPIの使い方は同じです。
いずれのサンプルもWeb Workerで重い処理を行う設計のため、pdfjsLib.GlobalWorkerOptions.workerSrcにワーカースクリプトのURLを指定しています。バージョンはnpmの最新版である6.2.108に固定し、esm.sh経由でpdf.worker.min.mjsを読み込むようにしました。
PDFをBase64データから読み込みcanvasに描画する
まずは最も基本的なパターンです。pdfjsLib.getDocument()にBase64をデコードしたUint8Arrayを渡してPDF文書(PDFDocumentProxy)を取得し、pdf.getPage(1)で1ページ目を取り出してpage.render()でcanvasに描画します。スライダーでscale(拡大率)を変えると、getViewport({ scale })が返すサイズに合わせてcanvasの解像度そのものが変わる様子が確認できます。
import * as pdfjsLib from 'pdfjs-dist'
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://esm.sh/pdfjs-dist@6.2.108/build/pdf.worker.min.mjs'
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise
const page = await pdf.getPage(1)
const viewport = page.getViewport({ scale: 1.5 })
const canvas = document.getElementById('pdf-canvas')
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({
canvasContext: canvas.getContext('2d'),
viewport,
}).promise
実際に動かせるものが下です。スライダーを右に動かすほどscaleの値が大きくなり、canvasに描かれるPDFのページも大きく再描画されます。
pdfjsLib.getDocument()はPDFの取得元をほとんど問いません。ここではdataにBase64起源のバイト列を渡していますが、URL文字列を渡せばPDF.js自身がfetchしてくれますし、ArrayBufferをそのまま渡すこともできます。scaleの値を0.5のように1未満にすると、逆にcanvasが小さく描画されることも確認してみてください。
ページ送りができるビューアを作る
複数ページのPDFでは、pdf.numPagesでページ数を取得し、ボタン操作に応じてpdf.getPage(n)を呼び直すだけでシンプルなビューアが作れます。1ページ分の描画処理(getViewport → page.render())は先ほどと同じで、表示するページ番号だけを変数として持ち回している点がポイントです。
let currentPage = 1
async function renderPage(num) {
const page = await pdfDoc.getPage(num)
const viewport = page.getViewport({ scale: 1.5 })
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({ canvasContext: ctx, viewport }).promise
}
document.getElementById('next').onclick = () => {
if (currentPage < pdfDoc.numPages) renderPage(++currentPage)
}
document.getElementById('prev').onclick = () => {
if (currentPage > 1) renderPage(--currentPage)
}
下のサンプルは3ページのPDFを埋め込んであります。「次へ」「前へ」ボタンを押すたびにpdfDoc.getPage(n)が呼ばれ、各ページの内容がcanvasに描き直されます。
「次へ」を2回押すと3ページ目まで進み、pageInfoの表示が3 / 3になった時点で「次へ」ボタンを押しても何も起きなくなります。これはcurrentPage < pdfDoc.numPagesのガードが効いているためで、実際のビューアではここに合わせて「次へ」ボタン自体をdisabledにするとより親切になります。
getTextContentでテキストを抽出する
PDF.jsはOCRなしで、PDF内部に埋め込まれたテキストデータをそのまま取得できます。使うAPIはpage.getTextContent()だけで、返ってくるitems配列の各要素が持つstrプロパティをつなげればページ全体のテキストになります。検索インデックスの作成や全文検索の対象データを作る用途で重宝するAPIです。
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise
let fullText = ''
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const textContent = await page.getTextContent()
fullText += textContent.items.map((item) => item.str).join(' ') + '\n'
}
ボタンを押すと、埋め込んだPDFからgetTextContent()でテキストを抜き出して画面に表示するサンプルです。
「テキストを抽出する」ボタンを押すと、canvasに描画したのと同じPDFからgetTextContent()でテキストレイヤーを取り出し、そのまま画面に表示します。ループの中でpdf.getPage(i)とpage.getTextContent()を呼んでいる部分を、fullText.includes(keyword)のような判定に差し替えれば、複数ページのPDFから特定のキーワードを含むページだけを探す簡易検索にも応用できます。
基本的な使い方
シンプルにPDFを表示する
まずは最小構成でPDFを表示してみる。
<!DOCTYPE html>
<html>
<head>
<title>PDF.js サンプル</title>
</head>
<body>
<canvas id="pdf-canvas"></canvas>
<script type="module">
import * as pdfjsLib from 'pdfjs-dist';
// ワーカーの設定
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.mjs';
// PDFを読み込む
const loadingTask = pdfjsLib.getDocument('/path/to/document.pdf');
loadingTask.promise.then(pdf => {
console.log('PDF読み込み完了: ' + pdf.numPages + 'ページ');
// 最初のページを取得
pdf.getPage(1).then(page => {
const scale = 1.5;
const viewport = page.getViewport({ scale });
const canvas = document.getElementById('pdf-canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// ページをレンダリング
page.render({
canvasContext: context,
viewport: viewport
});
});
});
</script>
</body>
</html>
React/Next.jsでの使用例
Reactで使う場合はこんな感じになる。
'use client';
import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
// ワーカーの設定
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.mjs`;
interface PDFViewerProps {
url: string;
}
export const PDFViewer = ({ url }: PDFViewerProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
useEffect(() => {
const loadPDF = async () => {
const pdf = await pdfjsLib.getDocument(url).promise;
setPdfDoc(pdf);
setNumPages(pdf.numPages);
};
loadPDF();
}, [url]);
useEffect(() => {
const renderPage = async () => {
if (!pdfDoc || !canvasRef.current) return;
const page = await pdfDoc.getPage(currentPage);
const scale = 1.5;
const viewport = page.getViewport({ scale });
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
if (!context) return;
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport,
}).promise;
};
renderPage();
}, [pdfDoc, currentPage]);
return (
<div className="pdf-viewer">
<div className="controls">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage <= 1}
>
前へ
</button>
<span>{currentPage} / {numPages}</span>
<button
onClick={() => setCurrentPage(p => Math.min(numPages, p + 1))}
disabled={currentPage >= numPages}
>
次へ
</button>
</div>
<canvas ref={canvasRef} />
</div>
);
};
テキスト抽出
PDFからテキストを抽出することも可能。OCRではなく、PDF内のテキストデータを直接取得できる。
const extractText = async (url: string): Promise<string> => {
const pdf = await pdfjsLib.getDocument(url).promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += pageText + '\n';
}
return fullText;
};
実践的なユースケース
請求書・見積書の表示
SaaSで請求書PDFをそのまま表示したいケース。ダウンロードリンクだけ置くより、その場で確認できたほうがUXは良い。
マニュアル・ドキュメントビューア
製品マニュアルをWebで公開する場合、PDF.jsを使えば目次機能や検索機能も実装できる。
PDFフォームの入力
PDF.jsはフォーム入力もサポートしている。ユーザーがブラウザ上でPDFフォームに入力し、そのデータを取得するといった使い方も可能。
ファイルアップロード時のプレビュー
ユーザーがPDFをアップロードする際、送信前にプレビュー表示したい場合にも使える。
const handleFileUpload = async (file: File) => {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
// プレビュー表示処理
};
パフォーマンスの考慮点
PDF.jsは優秀だが、大きなPDFを扱う場合はいくつか注意点がある。
- ページ単位でレンダリング: 100ページあるPDFを全部一度にレンダリングするのは避ける
- Web Worker: デフォルトでWorkerを使うので、メインスレッドをブロックしない
- メモリ管理: 不要になったページは
cleanup()で解放する
// 使い終わったらクリーンアップ
page.cleanup();
// ドキュメント全体を破棄
pdf.destroy();
まとめ
PDF.jsは、WebアプリでPDFを扱うなら真っ先に検討すべきライブラリですね。
- Mozillaが開発していて信頼性が高い
- プラグイン不要でどのブラウザでも動く
- カスタマイズ性が高い
- 52,000スター、407人のコントリビューターという実績
個人的には、PDFをWebで表示したいという要件が出てきたら、まずPDF.jsで実現できないか検討するようにしている。大抵のことはこれで解決する。
ちなみに、もっと高機能なビューアが必要なら、PDF.jsをベースにしたreact-pdfというラッパーライブラリもあるので、そちらも合わせてチェックしてみてください。