1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Vue.js で pdf.js を使う

Posted at

ことの発端

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

方針

  • 最新の 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>
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?