Vue.js で pdf.js を使う

## ことの発端

`pdf.js` を vue で使おうと思ったのだが、メジャーなコンポーネントを見つけることができなかった。
それじゃぁ～ってことで、[pdfjs-dist](https://www.npmjs.com/package/pdfjs-dist) を直接使おうと思ったら、`webpack` でエラーになってしまう。
まぁ、`babel` 君のバージョンが古くて `async`や`await`が理解できないのはエラーを見ると明らかだったが、古いプロジェクトなんで、この辺更新したら返り血を浴びそうなので、何とかならんか考えた。


## 方針

* 最新の `pdf.js` を使いたい。
* `pdf.js` は CDN から拝借する。
* 型情報は `pdfjs-dist` から拝借するのが良いんじゃないかな。


## 実装

`Rx` ばかり使っているのでこんなコードになりました。

```ts
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));
}
```

肝になるの下記のコード。
```ts
import type * as pdfjs from "pdfjs-dist";
```

`pdfjs-dist` から型情報を `import` しています。
これを `Type-Only Imports` と呼ぶらしく、詳しくは下記を参照。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export

んで、`pdf.js` が読み込まれたら `pdfjsLib` というグローバルオブジェクトが初期化されるので下記のコードで、その `pdfjsLib` を参照できるようにする。

```ts
declare const pdfjsLib: typeof pdfjs;
```

ここまでできれば、煮るなり焼くなりなんとでもできる。
これで表示まではできたので、あとはページ移動や拡大率を設定できるように props を増やしてあげれば良いかな。

```vue:実装（イメージ）
<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>
```