このページの本文へ

最新JSフレームワークAureliaとPDF.jsでPDFビューワーが自作できた!

2016年11月24日 08時58分更新

文●Jedd Ahyoung

  • この記事をはてなブックマークに追加
本文印刷
次世代フレームワークとして注目されているAurelia。PDF.jsを組み合わせることで、JavaScriptで自在にコントロールできるPDFビューアーを作っちゃいました。どういうことかって? 詳しくはこのチュートリアルで。

Webアプリケーション内でPDFファイルを扱うのにはいつも苦労します。もし運が良ければ、ユーザーはただ単にファイルをダウンロードするだけで済むでしょう。しかしときには、それ以上のことが必要です。これまで私はラッキーでした。しかし、先日のWebアプリケーションでは、PDFドキュメントを表示する必要があり、各ページに関連するメタデータを保存しなければなりませんでした。

以前はAdobe Readerのような、ブラウザー内で動作する高負荷なプラグインを使えばよかったかもしれません。しかし、これまでの経験と試行錯誤によって、WebアプリケーションにPDFビューワーを埋め込むためのもっと良い方法を見つけました。AureliaPDF.jsを使ってPDFの取り扱いをシンプルにする方法を説明します。

ゴールの概要

ゴールは、ビューワーとアプリケーションの間でデータをやりとりできるPDFビューワー部分をAureliaで作成することです。要求されることは3つです。

  1. ユーザーがPDFドキュメントを閲覧できて、適度な性能でスクロール、ズームイン、ズームアウトができる
  2. ビューワーとWebアプリケーションとの間で、プロパティ(現在開いているページやズームの度合いなど)を双方向に関連付ける
  3. ビューワーを再利用できるコンポーネントにする。このWebアプリケーションでは複数のビューワーを、お互いが干渉することなく簡単に、同時に使えるようにする

記事で使ったコードはGitHubレポジトリにあります。最終的なコードのデモはこちらです。

PDF.jsとは

PDF.jsはMozilla Foundationによって作成されたJavaScriptのライブラリーです。PDFを表示したり、ファイルとそのメタデータを解析(parse)したり、ページをDOMのノード(よくあるのは<canvas>要素)へ出力できたりします。ライブラリーに含まれている標準ビューワーは、ChromeとFirefoxの標準PDFビューワーにも使われており、単体のページとしても使えますし、リソースとしても使えます(HTMLのiframe要素の中に埋め込む)。

確かに、すごいことです。問題は、標準ビューワーはたくさんの機能がある反面、単体のWebページとして動作する設計になっていることです。つまり、Webアプリケーションに組み込めても、基本的にはiframeの枠の中で操作しなければならないということです。標準ビューワーは文字列のクエリーによって設定値を入力する仕様になっています。しかし、最初に表示が完了したあとで設定値を変更できず、ビューワーから情報やイベントの取得も困難です。このビューワーをAureliaのWebアプリケーションに組み込むには、つまりイベントが取得できるようにし、データの双方向バインディングをするには、Aureliaのカスタム要素を作る必要があります。

:もしPDF.jsのおさらいが必要でしたら『Dropboxも採用!JSだけでPDFをレンダリングできるPDF.jsにしびれた!』を参照してください。

実装のしかた

ゴールを達成するために、Aureliaのカスタム要素(HTML要素の拡張)を作ります。しかし、標準のビューワーを組み込むわけではありません。関連付けるプロパティやPDFレンダリングを最大限に操作するため、PDF.jsのコアとビューワーのライブラリーを利用した独自のビューワーを作成します。実現が可能なことを示すため、Aureliaアプリケーションのスケルトンから始めます。

ボイラープレート(定型文)

このあとのリンクをたどるとわかるように、アプリの骨組みにはたくさんのファイルが入っていますが、その多くは不要です。わかりやすくするため、不要なものを削ったスケルトンを用意しました。これに、必要なものを加えています。

  • PDFファイルをdistフォルダー(Aureliaがバンドルするために使う)にコピーするための、Gulpのタスク
  • PDF.jsの依存オブジェクト(dependency)をpackage.jsonに追加
  • アプリのルートディレクトリでindex.htmlindex.cssがスタイルの初期値を取得
  • これから作業するファイルとして空のファイルを追加
  • src/resources/elements/pdf-document.cssというファイルに、今回のカスタム要素のスタイルを記述

アプリを起動して実行します。

始めに、gulpとjspmがグローバルインストールされていることを確認してください。

npm install -g gulp jspm

次にスケルトンのクローンを作成し、cdでディレクトリを移動します。

git clone git@github.com:sitepoint-editors/aurelia-pdfjs.git -b skeleton
cd aurelia-pdfjs

続いて、必要な依存オブジェクト(dependency)をインストールします。

npm install
jspm install -y

最後に、gulp watchを実行して、「http://localhost:9000」を見てください。すべてがうまくいけば、Welcomeメッセージが表示されます。

さらにセットアップを続ける

次にやるべきことは、2つほどPDFを見つけてきてsrc/documentsに置くことです。これらのPDFにそれぞれone.pdftwo.pdfと名前を付けてください。カスタム要素を最大限にテストするためには、PDFのうち1つはとても長いものがよいでしょう。たとえば、トルストイの「戦争と平和」はGutenberg Projectから入手できます。

PDFが用意できたら、src/app.htmlsrc/app.jsを開き(慣例としてAppはルートもしくはAureliaアプリです)、こちらのsrc/app.htmlsrc/app.jsの中にあるコードと入れ替えます。記事ではこれらのファイルの解説はしませんが、中のコードには分かりやすいコメントが付いています。

Gulpは加えた変更を自動で検出し、アプリのUIがレンダリングされるはずです。セットアップは以上です。さあ、いよいよ始めましょう。

Aureliaのカスタム要素を作成する

どのAureliaのビューでも使える汎用的なカスタム要素を作ります。AureliaのビューはHTML5の<template>タグに囲まれたHTMLの断片なので、例を挙げると下のようになります。

<template>
  <require from="resources/elements/pdf-document"></require>
  <pdf-document url.bind="document.url"
                page.bind="document.pageNumber"
                lastpage.bind="document.lastpage"
                scale.bind="document.scale">
  </pdf-document>
</template>

<pdf-document>タグは、カスタム要素の例です。<pdf-document>要素と属性(pagescaleなど)は本来のHTMLにはありませんが、Aureliaカスタム要素を使うと自分で作れます。カスタム要素の作り方は分かりやすく、Aureliaの基本の構成要素であるビュー(Views)とビューモデル(ViewModels)を使います。つまり次のように、 pdf-document.jsと名付けたビューモデルで足場作りから始めます。

// src/resources/elements/pdf-document.js

import {customElement, bindable, bindingMode} from 'aurelia-framework';

@customElement('pdf-document')

@bindable({ name: 'url' })
@bindable({ name: 'page', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
@bindable({ name: 'scale', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
@bindable({ name: 'lastpage', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })

export class PdfDocument {
  constructor () {
    // Instantiate our custom element.
  }

  detached () {
    // Aurelia lifecycle method. Clean up when element is removed from the DOM.
  }

  urlChanged () {
    // React to changes to the URL attribute value.
  }

  pageChanged () {
    // React to changes to the page attribute value.
  }

  scaleChanged () {
    // React to changes to the scale attribute value.
  }

  pageHandler () {
    // Change the current page number as we scroll
  }

  renderHandler () {
    // Batch changes to the DOM and keep track of rendered pages
  }
}

注目してほしいのは@bindable修飾子です。defaultBindingMode: bindingMode.twoWayを設定することでバインドするプロパティを設定し、ビューモデルのイベントハンドラーメソッド(urlChangedpageChanged)を作ることで、カスタム要素に組み込んだ属性の値の変更を監視し、対処できるようにしました。

それではこのビューモデルとペアになる最初のビューを作ります。

// src/resources/elements/pdf-document.html

<template>
  <require from="./pdf-document.css"></require>

  <div ref="container" class="pdf-container">
    My awesome PDF viewer.
  </div>
</template>

PDF.jsを組み込む

PDF.jsは3つのパーツから成ります。1つは、PDFドキュメントの解析・読み込みを受け持つコアライブラリーです。2つ目は、表示(display)ライブラリーで、コアレイヤーの上に使用可能なAPIを構築します。最後は、Webビューワープラグインで、前述したひな形のWebページです。記事の目的、独自のビューワー作成を達成するためには、表示APIを通して、このコアライブラリーを使用します。

表示APIはPDFJSというライブラリーオブジェクトをエクスポートします。このオブジェクトは、いくつかの設定変数をセットでき、PDFJS.getDocument(url)メソッドによってドキュメントを読み込んでくれます。このAPIは完全に非同期でWeb Workerからのメッセージを送受信するので、JavaScriptのPromiseデザインパターンによって成り立っています。ここでは主に、PDFJS.getDocument()メソッドから非同期で返されたPDFDocumentProxyオブジェクトと、PDFDocumentProxy.getPage()メソッドから非同期で返されたPDFPageProxyオブジェクトについて取り上げます。

ドキュメント類がやや不足しているものの、PDF.jsで作る基本的なビューワーの例はこのコードデモこちらのコードデモを参照してください。これらの例を土台にしてカスタムビューワーを作ります。

Web Workerを組み込む

PDF.jsはレンダリング作業をバックグラウンド処理するためWeb Workerを使用します。ブラウザーでのWeb Workerの動作の仕組みは事実上サンドボックス化されているため、通常使うモジュールローダーではなく、直接JavaScriptファイルのファイルパスを使ってWeb Workerをロードする必要があります。アプリケーションをバンドルする際に変わるかもしれませんが、幸いAureliaは静的なファイルパスを参照しなくてもすむように、ローダーの抽象化を提供しています。

もし同じリポジトリのバージョンであれば、すでにpdfjs-distパッケージがインストールされているはずです。もしそうでなければ、 「jspm install npm:pdfjs-dist@^1.5.391」のようにjspmを使ってインストールしてください。以下のように、Aureliaの依存性注入(dependency injection)モジュールを使い、Aureliaの抽象化されたローダーを組み込み、このローダーを使ってWeb Workerファイルをコンストラクタに読み込みます。

// src/resources/elements/pdf-document.js

import {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework';
import {PDFJS} from 'pdfjs-dist';

@customElement('pdf-document')

... // all of our @bindables

@inject(Loader)
export class PdfDocument {
  constructor (loader) {
    // Let Aurelia handle resolving the filepath to the worker.
    PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js');

    // Create a worker instance for each custom element instance.
    this.worker = new PDFJS.PDFWorker();
  }
  detached () {
    // Release and destroy our worker instance when the the PDF element is removed from the DOM.
    this.worker.destroy();
  }
  ...
}

ページを読み込む

PDF.jsライブラリーはPDFドキュメントを読み込み、解析し、表示します。PDF.jsは標準で部分ダウンロードと認証に対応しています。PDF.jsに対象ドキュメントのURLさえ提供すれば、PDFドキュメントとそのメタデータをもつJavaScriptオブジェクトに対応するPromiseオブジェクトが返ります。

PDFはバインディングした属性に従って読み込み、表示されます。この場合、url属性を使います。原則的にはURLが変わると、カスタム要素はPDF.jsに対してファイルをリクエストします。それをurlChangedハンドラーで実行します。同時に、プロパティの初期化のためにコンストラクタを変更し、またクリーンアップ(削除)のためにdetachedメソッドも変更します。

ドキュメントの各ページに対応して、スクロール可能で高さ固定のコンテナ内に、DOMで<canvas>要素を作ります。実装するには、Aureliaの基本的なテンプレート機能「repeater」を使います。PDFの各ページにはそれぞれ異なるサイズと向きがあるかもしれないので、PDFページのviewportの指定に合わせてそれぞれの<canvas>要素に幅と高さを設定するからです。

作成したビューです。

// src/resources/elements/pdf-document.html

<template>
  <require from="./pdf-document.css"></require>

  <div ref="container" id.bind="fingerprint" class="pdf-container">
    <div repeat.for="page of lastpage" class="text-center">
      <canvas id="${fingerprint}-page${(page + 1)}"></canvas>
    </div>
  </div>
</template>

PDFドキュメントが読み込まれたら、各canvas要素のサイズをページに合わせるために、PDFの各ページのサイズを取得します。この時点でサイズを取得しておくのは各ページの正確な高さを知って、ビューワーのスクロール機能をセットアップするためです。各ページが読み込まれたあと、Aureliaのタスクキューの抽象化を使ってcanvas要素のサイズ変更のタスクをキューに追加します。これはDOMのパフォーマンス上の理由です。ここにマイクロタスクについての詳細が書かれています。

作成したビューモデルです。

// src/resources/elements/pdf-document.js

import {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework';
import {TaskQueue} from 'aurelia-task-queue';
import {PDFJS} from 'pdfjs-dist';

@customElement('pdf-document')

... // all of our @bindables

@inject(Loader, TaskQueue)
export class PdfDocument {
  constructor (loader, taskQueue) {
    PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js');
    this.worker = new PDFJS.PDFWorker();

    // Hold a reference to the task queue for later use.
    this.taskQueue = taskQueue;

    // Add a promise property.
    this.resolveDocumentPending;

    // Add a fingerprint property to uniquely identify our DOM nodes.
    // This allows us to create multiple viewers without issues.
    this.fingerprint = generateUniqueDomId();

    this.pages = [];
    this.currentPage = null;
  }

  urlChanged (newValue, oldValue) {
    if (newValue === oldValue) return;

    // Load our document and store a reference to PDF.js' loading promise.
    var promise = this.documentPending || Promise.resolve();
    this.documentPending = new Promise((resolve, reject) => {
      this.resolveDocumentPending = resolve.bind(this);
    });

    return promise
      .then((pdf) => {
        if (pdf) {
          pdf.destroy();
        }
        return PDFJS.getDocument({ url: newValue, worker: this.worker });
      })
      .then((pdf) => {
        this.lastpage = pdf.numPages;

        pdf.cleanupAfterRender = true;

        // Queue loading of all of our PDF pages so that we can scroll through them later.
        for (var i = 0; i < pdf.numPages; i++) {
          this.pages[i] = pdf.getPage(Number(i + 1))
            .then((page) => {
              var viewport = page.getViewport(this.scale);
              var element = document.getElementById(`${this.fingerprint}-page${page.pageNumber}`);

              // Update page canvas elements to match viewport dimensions. 
              // Use Aurelia's TaskQueue to batch the DOM changes.
              this.taskQueue.queueMicroTask(() => {
                element.height = viewport.height;
                element.width = viewport.width;
              });

              return {
                element: element,
                page: page,
                rendered: false,
                clean: false
              };
            });
        }

        // For the initial render, check to see which pages are currently visible, and render them.
        /* Not implemented yet. */

        this.resolveDocumentPending(pdf);
      });
  }

  detached () {
    // Destroy our PDF worker asynchronously to avoid any race conditions.
    return this.documentPending
      .then((pdf) => {
        if (pdf) {
          pdf.destroy();
        }
        this.worker.destroy();
      })
      .catch(() => {
        this.worker.destroy();
      });
  }
}

// Generate unique ID values to avoid any DOM conflicts and allow multiple PDF element instances.
var generateUniqueDomId = function () {
  var S4 = function() {
    return (((1 + Math.random()) * 0x10000) | 0)
      .toString(16)
      .substring(1);
  };

  return `_${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
}

ここまでの作業を保存すると、Gulpがページをレンダリングします。コンテナにそれぞれのPDFの正しいページが表示されているのに気づくでしょう。唯一の問題は、まだ空っぽだということです。では直していきます。

ページをレンダリングする

すでにページを読み込んだので、DOMの要素としてレンダリングする必要があります。レンダリングにはPDF.jsのレンダリング機能を使います。PDF.jsのビューワーライブラリーにはページのレンダリング専用の非同期通信APIがあります。どのようにrenderContextオブジェクトを作り、PDF.jsのレンダリング機能に渡すかを示す良い例がここにあります。この例からコードを取り出して、レンダリング機能に組み込みます。

src/resources/elements/pdf-document.js

...
export class PdfDocument { ... }

var generateUniqueDomId = function () { ... }

var render = function (renderPromise, scale) {
  return Promise.resolve(renderPromise)
    .then((renderObject) => {
      if (renderObject.rendered) return Promise.resolve(renderObject);
      renderObject.rendered = true;

      var viewport = renderObject.page.getViewport(scale);
      var context = renderObject.element.getContext('2d');

      return renderObject.page.render({
        canvasContext: context,
        viewport: viewport
      })
        .promise.then(() => {
          return renderObject;
        });
  });
};

PDF.jsのレンダリングはやや高負荷なので、抑えます。いま見える部分だけを描画すればよいので、すべてを一度にレンダリングするのではなく見えているページの境界内だけをレンダリングするようにします。viewportに表示される部分を調べるために、単純な計算をします。

// src/resources/elements/pdf-document.js

export class PdfDocument { ... }

var generateUniqueDomId = function () { ... }

var render = function (...) { ... }

var checkIfElementVisible = function (container, element) {
  var containerBounds = {
    top: container.scrollTop,
    bottom: container.scrollTop + container.clientHeight
  };

  var elementBounds = {
    top: element.offsetTop,
    bottom: element.offsetTop + element.clientHeight
  };

  return (!((elementBounds.bottom < containerBounds.top && elementBounds.top < containerBounds.top)
    || (elementBounds.top > containerBounds.bottom && elementBounds.bottom > containerBounds.bottom)));
}

最初にドキュメントを読み込んだとき、そしてスクロールしたとき、viewportのチェックが実行されます。いま読み込み、表示されている部分をシンプルに描画します。以下のとおりです。

// src/resources/elements/pdf-document.js

export class PdfDocument {
...
  urlChanged (newValue, oldValue) {
    ...
        // For the initial render, check to see which pages are currently visible, and render them.
        this.pages.forEach((page) => {
          page.then((renderObject) => {
            if (checkIfElementVisible(this.container, renderObject.element))
            {
              if (renderObject.rendered) return;
              render(page, this.scale);
            }
          });
        });

        this.resolveDocumentPending(pdf);
      });
  }

アプリケーションをリロードすると、各PDFの最初のページが表示されるはずです。

スクロールを実装する

見慣れている一貫性のある使い心地を実現するため、ビューワーはページを完全にスクロールできる独立したドキュメントとして表示しなければなりません。CSSで、高さは固定で、はみ出した部分はスクロールできるコンテナを作成して実現します。

より大きなドキュメントを扱う際のパフォーマンスを最大化するために、いくつかのことをします。始めに、Aureliaのタスクキューを利用して一括でDOMを変更します。次に、PDF.jsがすでにレンダリングしたページを把握して、同じ処理を繰り返さなくてよいようにします。最後に、Aureliaのdebounceというバインディングビヘイビア(反応の関連付け)を用いて、スクロールが止まったあとで見えているページだけを描きます。以下が、スクロールしたときに実行されるメソッドです。

// src/resources/elements/pdf-document.js

export class PdfDocument {
...
  renderHandler () {
    Promise.all(this.pages)
      .then((values) => {
        values.forEach((renderObject) => {
          if (!renderObject) return;

          if (!checkIfElementVisible(this.container, renderObject.element))
          {
            if (renderObject.rendered && renderObject.clean) {
              renderObject.page.cleanup();
              renderObject.clean = true;
            }

            return;
          }

          this.taskQueue.queueMicroTask(() => {
            if (renderObject.rendered) return;
            render(renderObject, this.scale);
          });
        });
    });
  }
...
}

こちらがビューです。Aureliaのイベントバインディングを活用し、scroll.triggerで定義したメソッドをバインディングビヘイビアdebounceと合わせて使用します。

// src/resources/elements/pdf-document.html

<template>
  <require from="./pdf-document.css"></require>

  <div ref="container" id.bind="fingerprint" class="pdf-container" scroll.trigger="pageHandler()" 
       scroll.trigger2="renderHandler() & debounce:100">
    <div repeat.for="page of lastpage" class="text-center">
      <canvas id="${fingerprint}-page${(page + 1)}"></canvas>
    </div>
  </div>
</template>

ビューワーに、pageを適切にバインディングします。ページが変更されたときは、そのページを表示するためにスクロール位置データを更新し、スクロールされたときは現在のページ番号データをいま見ているページの番号に更新するため、ビューモデルに以下の2つのメソッドを加えます。

export class PdfDocument {
...
  // If the page changes, scroll to the associated element.
  pageChanged (newValue, oldValue) {
    if (newValue === oldValue || 
        isNaN(Number(newValue)) || 
        Number(newValue) > this.lastpage || 
        Number(newValue) < 0) {
      this.page = oldValue;
      return;
    }

    // Prevent scroll update collisions with the pageHandler method.
    if (Math.abs(newValue - oldValue) <= 1) return;

    this.pages[newValue - 1]
      .then((renderObject) => {
        this.container.scrollTop = renderObject.element.offsetTop;
        render(this.pages[newValue - 1], this.scale);
      });
  }

...

  // Change the current page number as we scroll.
  pageHandler () {
    this.pages.forEach((page) => {
      page.then((renderObject) => {
        if ((this.container.scrollTop + this.container.clientHeight) >= renderObject.element.offsetTop
      && (this.container.scrollTop <= renderObject.element.offsetTop))
        {
          this.page = renderObject.page.pageNumber;
        }
      });
    });
  }
...
}

コンテナ内のscroll.triggerイベントの中で、pageHandlerメソッドをコールします。

:現状ではAureliaのテンプレート機能の制約のため、1つのイベントハンドラに対して、別々のバインディングビヘイビアをもつ複数のメソッドを宣言できません。この制約を回避するため、ビューモデルのトップに次のコードを加えます。

import {SyntaxInterpreter} from 'aurelia-templating-binding';
SyntaxInterpreter.prototype.trigger2 = SyntaxInterpreter.prototype.trigger;

そして、scroll.trigger2イベントに新しいメソッドを加えます。

Gulpはアプリケーションをリロードし、スクロールされるとPDFの新しいページが表示されます。やりました!

ズーム機能を実装する

ズームするとき、現在のズーム率データの更新をscaleChangedプロパティのハンドラーで実現します。基本的には、各ページの新たなviewportサイズを反映して、すべてのcanvas要素を設定されたスケールでサイズ変更します。同じサイクルを再度実行し現在のviewportに表示するページを再度レンダリングします。

// src/resources/elements/pdf-document.js

export class PdfDocument {
...
  scaleChanged (newValue, oldValue) {
    if (newValue === oldValue || isNaN(Number(newValue))) return;

    Promise.all(this.pages)
      .then((values) => {
        values.forEach((renderObject) => {
          if (!renderObject) return;

          var viewport = renderObject.page.getViewport(newValue);

          renderObject.rendered = false;

          this.taskQueue.queueMicroTask(() => {
            renderObject.element.height = viewport.height;
            renderObject.element.width = viewport.width;

            if (renderObject.page.pageNumber === this.page) {
              this.container.scrollTop = renderObject.element.offsetTop;
            }
          });
        });

      return values;
    })
    .then((values) => {
      this.pages.forEach((page) => {
        page.then((renderObject) => {
          this.taskQueue.queueMicroTask(() => {
            if (checkIfElementVisible(this.container, renderObject.element)) {
              render(page, this.scale);
            }
          });
        });
      });
    });
  }
...
}

最終的な結果

ここで再度ゴールをおさらいします。

  1. ユーザーがPDFドキュメントを閲覧できて、適度な性能でスクロール、ズームイン、ズームアウトができる
  2. ビューワーとWebアプリケーションとの間で、プロパティ(現在開いているページやズームの度合いなど)を双方向に関連付ける
  3. ビューワーを再利用できるコンポーネントにする。このWebアプリケーションでは複数のビューワーを、お互いが干渉することなく簡単に、同時に使えるようにする

最終的なコードはGitHubのリポジトリに、最終的なコードのデモとともにあります。まだ改善の余地はありますが、目的は達成しました!

プロジェクトの事後分析と改善

改善の余地はいつでも存在します。事後分析で以降に取り組むことを明らかにすることは良い習慣です。このPDFビューワーの実装について、改良したい部分がいくつかあります。

各ページの構成要素

この試作品ではviewportのスクロールのみを実現しました。理想はどのページをどこにでも、それがビューワーの外側であっても表示できる、たとえば、PDFのサムネイルを独立した要素として生成するなどです。カスタム要素<pdf-page>のようなもので実現できるかもしれませんし、ビューワーは単に構文内でこの要素を使うだけで済むかもしれません。

APIの最適化

PDF.jsは拡張可能なAPIです。すでにPDF.jsの優れた使用例はあるものの、さらに多くのドキュメントを扱うかもしれません。記事のゴールを達成するにあたり、このビューワーのAPIを使用した、もっとクリーンかつ最適化された方法があるかもしれません。

仮想スクロールと、パフォーマンスの最適化

現在はビューワーの中のcanvas要素の数は、ドキュメント内のページ数と同じです。すべてのcanvas要素がDOMの中にあるので、ページ数の多いドキュメントではとても負荷が大きくなります。

Aureliaのプラグイン、ui-virtualization pluginデモ)を使うと、アクティブなviewportにあわせてDOMの要素を動的に加えたり削除したりすることで、パフォーマンスが劇的に改善します。DOM内に数千ものcanvas要素があるとパフォーマンスは著しく落ちるのでそれを避けるため、できれば、このプラグインを組み込みたいものです。各ページの要素の最適化とあわせてこの最適化をすると、大きなサイズのドキュメントの取り扱いが本当に大きく改善します。

プラグインを作る

Aureliaはプラグインシステムを採用しています。この試作品をAureliaのプラグインにすれば、どのAureliaアプリケーションからでも使えるリソースになります。AureliaのGithubリポジトリには、開発の出発点として最適なプラグインのスケルトンプロジェクトが用意されています。これを使えば、ほかの人もゼロから作ることなく一連の機能を使えるのです!

さらに先へ

Webアプリケーションの中でPDFファイルを取り扱うのはいつも苦痛でした。しかし、最新のリソースを使えばライブラリーとその機能を組み合わせることで、以前よりもはるかに多くのことができます。記事では基本的なPDFビューワーの例を見てきました。開発者が完全にコントロールできるので、記事で作成した試作品を拡張して追加機能も持たせられます。可能性には限りがありません!

※本記事はVildan Softicが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれた査読担当者の皆さんに感謝します。

(原文:Adventures in Aurelia: Creating a Custom PDF Viewer

[翻訳:西尾健史/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事