ことの発端
pdf.js
を vue で使おうと思ったのだが、メジャーなコンポーネントを見つけることができなかった。
それじゃぁ~ってことで、pdfjs-dist を直接使おうと思ったら、webpack
でエラーになってしまう。
まぁ、babel
君のバージョンが古くて async
やawait
が理解できないのはエラーを見ると明らかだったが、古いプロジェクトなんで、この辺更新したら返り血を浴びそうなので、何とかならんか考えた。
方針
- 最新の
pdf.js
を使いたい。 -
pdf.js
は CDN から拝借する。 - 型情報は
pdfjs-dist
から拝借するのが良いんじゃないかな。
実装
Rx
ばかり使っているのでこんなコードになりました。
import type * as pdfjs from "pdfjs-dist";
type state_t = "none" | "loading" | "loaded";
const ready_for_pdfjs = new BehaviorSubject<state_t>("none");
declare const pdfjsLib: typeof pdfjs;
function loadScript() {
return ready_for_pdfjs
.pipe(mergeMap((state) => {
switch(state){
case "none": {
ready_for_pdfjs.next("loading");
const script = document.createElement('script');
script.type = "text/javascript";
script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.13.216/pdf.min.js";
script.onload = () => {
ready_for_pdfjs.next("loaded");
}
const firstScript = document.getElementsByTagName('script')[ 0 ];
if(!firstScript.parentNode){
return throwError("cannot find parentNode");
}
firstScript.parentNode.insertBefore(script, firstScript);
return NEVER;
}
case "loading": {
return NEVER;
}
case "loaded": {
return of(pdfjsLib);
}
}
}))
.pipe(take(1));
}
肝になるの下記のコード。
import type * as pdfjs from "pdfjs-dist";
pdfjs-dist
から型情報を import
しています。
これを Type-Only Imports
と呼ぶらしく、詳しくは下記を参照。
んで、pdf.js
が読み込まれたら pdfjsLib
というグローバルオブジェクトが初期化されるので下記のコードで、その pdfjsLib
を参照できるようにする。
declare const pdfjsLib: typeof pdfjs;
ここまでできれば、煮るなり焼くなりなんとでもできる。
これで表示まではできたので、あとはページ移動や拡大率を設定できるように props を増やしてあげれば良いかな。
実装(イメージ)
<template>
<canvas ref="refCanvas"></canvas>
</template>
<script lang="ts">
import { defineComponent, onUnmounted, ref, watch } from "@vue/composition-api";
import { BehaviorSubject, from, NEVER, of, throwError } from "rxjs";
import { rx } from "@/code/common/utils";
import * as LOG from "@/code/common/utils/log";
import { mergeMap, take } from "rxjs/operators";
const log = LOG.Console;
/**
* pdf.js は npm でimportしているが、そのまま使うと webpack でエラーとなる。
* そこで、pdfjs-dist は型のみ読み込んで、実体はスクリプトを動的に読み込む方針とした。
*/
/**
* 型のみ読み込んで実体は取り込まない
* https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
*/
import type * as pdfjs from "pdfjs-dist";
type state_t = "none" | "loading" | "loaded";
const ready_for_pdfjs = new BehaviorSubject<state_t>("none");
/**
* pdj.js が読み込まれると pdfjsLib というグローバルオブジェクトが生成されるので
* Typescriptで使えるように定義する
*/
declare const pdfjsLib: typeof pdfjs;
function loadScript() {
return ready_for_pdfjs
.pipe(mergeMap((state) => {
switch(state){
case "none": {
ready_for_pdfjs.next("loading");
const script = document.createElement('script');
script.type = "text/javascript";
script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.13.216/pdf.min.js";
script.onload = () => {
ready_for_pdfjs.next("loaded");
}
const firstScript = document.getElementsByTagName('script')[ 0 ];
if(!firstScript.parentNode){
return throwError("cannot find parentNode");
}
firstScript.parentNode.insertBefore(script, firstScript);
return NEVER;
}
case "loading": {
return NEVER;
}
case "loaded": {
return of(pdfjsLib);
}
}
}))
.pipe(take(1));
}
export default defineComponent({
props: {
url: {
type: String,
required: true
}
},
setup(props) {
const refCanvas = ref<HTMLCanvasElement>();
const canvas_subject = new BehaviorSubject<HTMLCanvasElement | undefined>(undefined);
const sg = new rx.SubscriptionGroup(log);
onUnmounted(() => {
sg.unsubscribeAll();
});
watch(
() => refCanvas.value,
() => {
if (!refCanvas.value) return;
canvas_subject.next(refCanvas.value);
}
);
function getCanvas() {
// prettier-ignore
return canvas_subject.asObservable()
.pipe(mergeMap((canvas) => {
if(!canvas) return NEVER;
return of(canvas);
}));
}
function load(url: string){
// prettier-ignore
sg.append(
`load pdf: ${url}`,
loadScript()
.pipe(mergeMap((pdfjs) => {
return from(pdfjs.getDocument(props.url).promise)
}))
.pipe(mergeMap((pdf) => {
return from(pdf.getPage(55))
.pipe(mergeMap((page) => {
return getCanvas()
.pipe(mergeMap((canvas) => {
const ctx = canvas.getContext("2d");
if(!ctx) return throwError("ctx is null");
const viewport = page.getViewport({scale: 1});
canvas.width = viewport.width;
canvas.height = viewport.height;
return from(page.render({
canvasContext: ctx,
viewport: viewport
}).promise);
}));
}));
}))
);
}
load(props.url);
return {
refCanvas
};
}
});
</script>