Developer's Blog

PDF.js の導入方法と苦労した話

pdfjs

こんにちは。
島根支社の原田です。

JavaScript で PDF を表示する際にライブラリとして PDF.js が話題にあがると思います。
PDF.js は名前の通り JavaScript で実装されているライブラリです。
HTML5 の技術を使っているため、最新のブラウザであればほぼ問題無く使うことができます。

本稿では、PDF.js を利用した PDF の表示までの導入部分と、実装で苦労した話をしていきます。

下準備

最初に、プロジェクトのディレクトリを作成し、npm init します。

$ mkdir fenrir-pdfjs
$ cd fenrir-pdfjs
$ npm init

次に、最低限必要なパッケージをインストールします。

$ npm install --save-dev browserify
$ npm install --save-dev pdfjs-dist

これでライブラリ一式を引っ張ってきたので、必要なディレクトリとタスクを作ります。

$ mkdir src
$ mkdir dist
$ vi package.json

+ "scripts": {
+   "build": "browserify src/app.js > dist/app.js"
+ }

これで、下準備は完了です。

PDF を表示してみる

PDF を表示するために必要なコードを記述していきます。

まずは HTML 側から作ります。

[/index.html]

<!DOCTYPE html>
<html>
  <head>
    <title> PDF Viewer</title>
    <script src="dist/app.js"></script>
  </head>
  <body>
    <div id="pdf-container">
    </div>
  </body>
</html>

次に、JavaScript 側を作ります。

[/src/app.js]

require('pdfjs-dist');

PDFJS.workerSrc = "node_modules/pdfjs-dist/build/pdf.worker.js";

PDFJS.getDocument('test.pdf').then(function(pdf){
  // PDF ドキュメント全体に関する処理を記述

  pdf.getPage(1).then(function(page){
    // 読み込んだ各ページの処理を記述

    // 描画時の設定を記述
    var scale = 1.5;
    var rotate = 0;
    var viewport = page.getViewport(scale, rotate);
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    var renderContext = {
      canvasContext: ctx,
      viewport: viewport
    };
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    document.getElementById('pdf-container').appendChild(canvas);

    // 作成した canvas に読み込んだ pdf を描画
    page.render(renderContext);
  });

});

そしてタスクを実行します。

$ npm run build

最後に、test.pdf を配置して、index.html にアクセスすると PDF の最初の 1 ページが表示されます。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-13-18-12-55

src/app.js 内の処理は、公式サイトの Examples に記載されていますので、そちらをご参照ください。 workerSrc のパスについては pdf.worker.js を別ファイルで切り出して、dist に配置して、そのファイルパスをセットすると綺麗になると思います。

実装で苦労した話

スクロールで全ページを表示するのではなく、1 ページずつ表示する。

PDF.js のデモを見るとスクロールして全ページを表示していますが
場合によっては 1 ページずつ表示したいこともあると思います。
そのために、下記のように HTML 側と JavaScript 側のコードを修正します。

[/index.html]

<!DOCTYPE html>
<html>
  <head>
    <title> PDF Viewer</title>
    <script src="dist/app.js"></script>
  </head>
  <body>
    <button id="prev">prev</button>
    <button id="next">next</button>
    <div id="pdf-container">
      <canvas id="pdf-canvas"></canvas>
    </div>
  </body>
</html>

[/dist/app.js]

require('pdfjs-dist');
PDFJS.workerSrc = "node_modules/pdfjs-dist/build/pdf.worker.js";

window.onload = function() {

  var currentPage = 1;
  var pdfDoc;

  PDFJS.getDocument('test.pdf').then(function(pdf){
    pdfDoc = pdf;
    renderPage(pdfDoc);
  });

  function renderPage(pdf){
    pdf.getPage(currentPage).then(function(page){
      var scale = 1.0;
      var rotate = 0;
      var viewport = page.getViewport(scale, rotate);
      var canvas = document.getElementById('pdf-canvas');
      var ctx = canvas.getContext('2d');
      var renderContext = {
        canvasContext: ctx,
        viewport: viewport
      };
      canvas.height = viewport.height;
      canvas.width = viewport.width;

      document.getElementById('pdf-container').appendChild(canvas);
      page.render(renderContext);
    });
  }

  document.getElementById("next").onclick = function() {
    currentPage++;
    renderPage(pdfDoc);
  }

  document.getElementById("prev").onclick = function() {
    currentPage--;
    renderPage(pdfDoc);
  }
}

修正したページは、prev ボタンを押すと 1 つ前のページ、next ボタンを押すと 1 つ次のページを表示する、簡単な作りをしています。
PDFJS.getDocument メソッドの内容を大きく変更しました。

  PDFJS.getDocument('test.pdf').then(function(pdf){
    pdfDoc = pdf;
    renderPage(pdfDoc);
  });

メソッド内に記述されていた getPage メソッド移行の処理を丸ごと別メソッド化しています。

function renderPage(pdf){
    pdf.getPage(currentPage).then(function(page){
      var scale = 1.0;
      var rotate = 0;
      var viewport = page.getViewport(scale, rotate);
      var canvas = document.getElementById('pdf-canvas');
      var ctx = canvas.getContext('2d');
      var renderContext = {
        canvasContext: ctx,
        viewport: viewport
      };
      canvas.height = viewport.height;
      canvas.width = viewport.width;

      document.getElementById('pdf-container').appendChild(canvas);
      page.render(renderContext);
    });
  }

これによって、getDocument で読み込んだ PDF のオブジェクトさえあれば、ページ番号を指定して 1 ページずつ表示されるようになっています。

1ページずつ順番にページを読み込んでいく

本来の PDF.js の動きから考えると、あまりされない実装だとは思いますが、苦心した話として書こうと思います。
結論から言いますと、 canvas へ描画する処理が完了した際に、次ページの canvas の描画を呼び出すようにすることで、画面に順番に表示されていきます。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-13-23-39-50

実際のコードは以下のようになります。

[/dist/app.js]

require('pdfjs-dist');
PDFJS.workerSrc = "node_modules/pdfjs-dist/build/pdf.worker.js";

var totalPage;
var currentPage = 1;
var pdfDoc;

function renderPDF(page){
  var scale = 0.5;
  var rotate = 0;
  var viewport = page.getViewport(scale, rotate);
  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  var renderContext = {
    canvasContext: ctx,
    viewport: viewport
  };
  canvas.height = viewport.height;
  canvas.width = viewport.width;

  document.getElementById('pdf-container').appendChild(canvas);
  currentPage++;
  page.render(renderContext).then(function(){
    if (currentPage <= totalPage) {
      pdfDoc.getPage(currentPage).then(renderPDF)
    }
  });
}

PDFJS.getDocument('test.pdf').then(function(pdf){
  totalPage = pdf.numPages;
  pdfDoc = pdf;

  pdfDoc.getPage(currentPage).then(renderPDF);
});

注目の箇所は、page.render メソッドの部分ですね。

page.render(renderContext).then(function(){
    if (currentPage <= totalPage) {
      pdfDoc.getPage(currentPage).then(renderPDF)
    }
  });

render メソッドも getDocument メソッドや getPage メソッドと同様に Promise オブジェクトが返却されます。
それを利用して描画が成功した場合の処理として次のページを描画していき、PDF の総ページ数に到達したら読み込み終了という形で実装しています。

おわりに

いかがでしたでしょうか。
PDF.js は非常に強力なライブラリであり、使いこなせば PDF 文書内の文字検索など様々な機能を使うことができます。
本稿で紹介した導入方法、苦労話が読んでくださった方の力に僅かでも貢献できれば幸いです。

エンジニア募集

フェンリル島根支社ではウェブエンジニアを募集しています。また、プロジェクトマネージャやスマートフォンアプリ開発エンジニアも募集しています。

島根支社の採用ページが公開されているのでぜひ一度ご覧になってください!

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

 

Copyright © 2019 Fenrir Inc. All rights reserved.