One IT Thing

IT業界を楽しむ為の学習系雑記

html5

IndexedDBにblob保存されたPDFファイルを外部アプリに頼らずにJavascriptで表示(後編)

投稿日:2019年6月11日 更新日:

はじめに

前回の続きです。

PDFをJavascriptでブラウザに表示するライブラリはMozillaの「PDF.js」がデファクトスタンダード、PDF内容をパースしてcanvasにレンダリングしていてテキスト選択も可能です。公式デモも素晴らしいです。


上記のデモを作成しているPDF.jsをnpmモジュールとして使うことが出来るのが同じくMozilla公式がリリースしているpdfjs-distです。

ただしこのモジュールはPDF処理を制御するクラスのみの提供になっていて、デモで行われているような全ページ表示、テキスト分離、選択、検索他諸々の機能は自分で実装することになります。

最低限の機能を実装する分には時間も掛からないのですが、公式デモレベルの機能レベルまで実装しようとすると時間が掛かります。出来るだけ楽をしたい怠惰な人間としては公式デモのようなビューワ機能を提供してくれるモジュールを使わせて頂きたいです。

ビューワ機能を提供してくれるnpmモジュールを選別する

「ng2-pdfjs-viewer」と「ng2-pdf-viewer」から選別します。
似た名前でまぎらわ(ry

npm trendsで比較してみると後者のng2-pdf-viewerの方がダウンロードされているようです。

https://www.npmtrends.com/ng2-pdfjs-viewer-vs-ng2-pdf-viewer

ただ今回は以下の理由から前者のng2-pdfjs-viewerを使うことにしました。

  • 最近の更新頻度がかなり多め
  • 使えるAngularのバージョンが広い
  • 公式デモのビューワ機能をそのまま組み込める

実装開始

ng2-pdfjs-viewerを使えるようにする

npmインストールします。3.2.9が入りました。

C:\src\ionic\pdfdl>npm install ng2-pdfjs-viewer
npm WARN ng2-pdfjs-viewer@3.2.9 requires a peer of @angular/core@^8.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ ng2-pdfjs-viewer@3.2.9
added 1 package in 10.902s

次にnode_modules\ng2-pdfjs-viewer\pdfjsディレクトリを
src/assetsにコピーします。公式デモのWebリソースはこの中に入っています。

PDFビューワコンポーネントを作る

ionic-cliでpdf-viewerページを追加。angularのComponentになります。

C:\src\ionic\pdfdl>ionic g page pdf-viewer
> ng.cmd generate page pdf-viewer
CREATE src/app/pdf-viewer/pdf-viewer.module.ts (559 bytes)
CREATE src/app/pdf-viewer/pdf-viewer.page.html (129 bytes)
CREATE src/app/pdf-viewer/pdf-viewer.page.spec.ts (713 bytes)
CREATE src/app/pdf-viewer/pdf-viewer.page.ts (271 bytes)
CREATE src/app/pdf-viewer/pdf-viewer.page.scss (0 bytes)
UPDATE src/app/app-routing.module.ts (536 bytes)
[OK] Generated page!

src/app/pdf-viewer/pdf-viewer.page.htmlを編集。

<ion-header>
    <ion-toolbar color="dark">
        <ion-title>pdf-viewer</ion-title>
    </ion-toolbar>
</ion-header>

<ion-content>
    <ng2-pdfjs-viewer #pdfViewer></ng2-pdfjs-viewer>
</ion-content>

src/app/pdf-viewer/pdf-viewer.page.tsを編集

import { Component, ViewChild } from "@angular/core";
import { ActivatedRoute, ParamMap } from "@angular/router";

@Component({
    selector: "app-pdf-viewer",
    templateUrl: "./pdf-viewer.page.html",
    styleUrls: ["./pdf-viewer.page.scss"]
})
export class PdfViewerPage {

    // ng2-pdf-viewer
    @ViewChild("pdfViewer") pdfViewer;

    // 表示するPDFのBlobデータ
    content;

    constructor(private route: ActivatedRoute) {
        // 遷移してきたHome画面からのPDFのBlobパラメータを取得
        route.queryParamMap.subscribe((params: ParamMap) => {
            this.content = this.route.snapshot.queryParamMap.get("content");
        });
    }

    // PDFビューワページが表示されたらPDFを表示
    ionViewDidEnter() {
        this.pdfViewer.pdfSrc = this.content;
        this.pdfViewer.refresh();
    }
}

src/app/pdf-viewer/pdf-viewer.module.tsを編集。pdf-viewerモジュール内で先ほどインストールしたng2-pdfjs-viewerを使えるようにします。

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { Routes, RouterModule } from "@angular/router";

import { IonicModule } from "@ionic/angular";

import { PdfViewerPage } from "./pdf-viewer.page";
import { PdfJsViewerModule } from "ng2-pdfjs-viewer";  ← 追加

const routes: Routes = [
    {
        path: "",
        component: PdfViewerPage
    }
];

@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        IonicModule,
        RouterModule.forChild(routes),
        PdfJsViewerModule              ← 追加
    ],
    declarations: [PdfViewerPage]
})
export class PdfViewerPageModule {}

Home画面からPDFビューワに遷移させる

src/app/home/home.page.tsに以下を追加します。前編で「表示」ボタンがクリックされたらshowPdfメソッドを呼ぶようにしてあるのでshowPdfメソッドを実装します。

import { PdfViewerPage } from "../pdf-viewer/pdf-viewer.page"; ← 追加

@Component({
    selector: "app-home",
    templateUrl: "home.page.html",
    styleUrls: ["home.page.scss"]
})
export class HomePage {

    (snip)

    // 追加
    public showPdf(content: Blob) {
        // PDFのBlobデータをパラメータにしてpdf-viewerに遷移
        this.navController.navigateForward("/pdf-viewer", {
            queryParams: { content: content }
        });
    }

実装終わりです。

動作確認してみる

PC & Chromeで確認してみます。表示ボタンをクリックすると、PDF.jsのデモで使われているビューワでPDFが見れるようになりました。

スマホのブラウザでも同様の確認が出来ます。が、しかし・・・

Androidでピンチズームが出来ない

スマホだとズームはピンチイン、ピンチアウトで行うのが直感的ですよね。ですがPDF.jsのWebビューワリソースにはピンチ系のイベントハンドラが実装されていない為、ズームしようとしてもスマホで操作するには小さい「-」「+」をなんとか押すしかありません。これでは使い勝手が悪いです。

iPhone & Safariだとズームしますが、これはブラウザ自体のズームなのでPDF.jsビューワのコントロール部分までズームされてしまいます。

PDFを描画しているcanvas部分だけレンダリングするようにしたいです。

しょうがないので自前でピンチ処理を実装する

src/app/pdf-viewer/pdf-viewer.page.tsのionViewDidEnterメソッドを編集。ピンチイン、アウトを検知して画面上の「-」「+」ボタンを押してあげることでズームイン、アウトするようにしてみます。

    ionViewDidEnter() {
        // viewer.htmlのiframe
        let iframe = this.pdfViewer["iframe"].nativeElement;
        this.pdfViewer.pdfSrc = this.content;
        this.pdfViewer.refresh();

        // PDF読込完了
        iframe.onload = () => {
            // viewer.htmlのbody
            let iframeBody = iframe.contentWindow.document.body;

            // ピンチ操作記録オブジェクト
            let behavior = { ratio: 1, start: 0, end: 0 };

            // true : ピンチ操作中
            let active: boolean = false;

            // 二本指間の距離を算出
            let getDistance = function(e: TouchEvent) {
                return Math.sqrt(
                    Math.abs(
                        Math.pow(e.touches[0].pageX - e.touches[1].pageX, 2) -
                        Math.pow(e.touches[0].pageY - e.touches[1].pageY, 2)
                    )
                );
            };

            iframeBody.addEventListener(
                "touchstart",
                (e: TouchEvent) => {
                    // 二本指でタッチされたらピンチ方向検知開始
                    if (e.touches.length > 1) {
                        behavior.start = behavior.end = Math.round(
                            getDistance(e)
                        );
                        active = true;
                    }
                },
                false
            );

            iframeBody.addEventListener(
                "touchmove",
                (e: TouchEvent) => {
                    if (active && e.touches.length > 1) {
                        behavior.end = Math.round(getDistance(e));
                        behavior.ratio = behavior.end / behavior.start;
                    }
                },
                false
            );

            iframeBody.addEventListener(
                "touchend",
                (e: TouchEvent) => {
                    if (behavior.ratio < 1) {
                        // ピンチインはズームアウト
                        iframe.contentWindow.document
                            .querySelector("#zoomOut")
                            .click();
                    } else if (behavior.ratio > 1) {
                        // ピンチアウトはズームイン
                        iframe.contentWindow.document
                            .querySelector("#zoomIn")
                            .click();
                    }
                    console.log(behavior);
                    // ピンチ操作記録をクリア
                    behavior.start = behavior.end = 0;
                    behavior.ratio = 1;
                },
                false
            );
        };
    }

ピンチ操作によるズームイン、アウト処理の実装が終わりました。

ピンチズームの動作確認

ピンチ操作してみます。

canvasへの再レンダリングが走る為スムーズには行きません。画像としてズームすればピンチ操作にズームが追従するように出来ますが、ジャギが目立ってしまうようになるのでPDF.jsの機能でズームするようにしています。

まとめ

外部PDFアプリに頼らずにブラウザだけでPDFを表示出来るようになりました。ハイブリッドアプリをPWAにしたりするとネイティブ機能が使えなくなってブラウザだけでなんとかしなければならなくなります。代替手段を確保しておけば安心ですね。

補足

当初、ピンチ処理の実装はIonicに組み込まれているHammer.jsで以下のように実装しました。

        iframe.onload = () => {
            var iframeBody = iframe.contentWindow.document.body;

            let hammer = new window['Hammer'](iframeBody);
            hammer.get('pinch').set({ enable: true });

            hammer.on("pinch", (event) => {
                if (event.type == 'pinch') {

                    if (event.additionalEvent == "pinchout" && event.eventType == 4) {
                        iframe.contentWindow.document.querySelector("#zoomIn").click();
                    }

                    if (event.additionalEvent == "pinchin" && event.eventType == 4) {
                        iframe.contentWindow.document.querySelector("#zoomOut").click();
                    }
                }
            });
        }

ところがiframe越しに操作している為か操作を続けているとピンチインとアウトがテレコになって帰ってくるような挙動をした為、Hammer.jsは使わずに自前で実装しました。Hammer.jsを使った方が簡単ですので解決出来る方はHammer.jsを使ってください。

-html5
-, , ,

執筆者:

関連記事

Angular、React、Vueプロジェクトを新規作成した時に生成されるリソース規模比較

アンチウィルスのディスクフルスキャンが日に日に遅くなっていく・・・ 一年後には丸一日掛かっても終わらなくなり、仕事終わりにかけて行ったフルスキャンが翌日まだ頑張っている。 この原因、ちょこちょこ作って …

IndexedDBにストアしたオブジェクトのキー値を部分的に更新する

目次1 はじめに2 課題3 より良い方法4 まとめ5 補足 はじめに IndexedDBは「key : value」でレコードを保存するキーバリューストアです。 バリューには「単値」または「Javas …

B2BスマホアプリをGooglePlay、AppStoreに公開することがお勧め出来ない7つの理由とその対策

商材の性質やシーンに応じてスマホアプリをGooglePlayやAppleStoreのようなストアに公開することがマイナスに働くこともあります。 商材として価値の有る電子データをお持ちの商社さんとアプリ …

キャッシュされているはずのServiceWorker資源にオフラインアクセス出来ない(Workbox + ionicons)

そのHTTPリクエストしたファイル資源、ひょっとしてURLパラメータついてたりしませんか? 目次1 事象2 原因3 対処 事象 ionic3(SPA)でWorkboxを使ったServiceWorker …

History APIを使ってIonic(3以前のSPA)でブラウザの戻るボタンやAndroidバックキーを押すと前サイトに戻ってしまう件に対応する

Ionic2や3ではまだAngular Routerを採用していなかったので、ページ遷移をしてもブラウザ履歴が積まれず、Androidのバックキーやブラウザの戻るボタンを押すとサイトに入ってくる前のペ …


shingo nakanishi。東京で消耗中の職歴20年越え中年ITエンジニアです。「生涯現役プログラマを楽しむ」ことができる働き方探しをライフワークにしています。

19歳(1996年)から書き始めた個人日記が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。