目次
はじめに
前回の続きです。
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を使ってください。