エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

PDF.js を使って PDF をブラウザに表示するための覚書

仕事で PDF をブラウザのページ中に埋め込んで表示したいことがあり、そのときに PDF.js を利用しました。

のちのち再び使いたくなったときのために、骨格を抜き出してまとめたものです。

サーバには Elixir の Phoenix framework を使っていますが、ほぼ静的なページで Phoenix らしいことは特に何もしていないので、Rails などでも同じように使える、はずです。

mozilla.github.io

assets/js/MyPdf.js

PDF.js の利用をこの一つのファイルに分離しています。

内容としては PDF.js の example にあるものと同じですが、class を使った記述に書き換えています。

class MyPdf {
  constructor(canvas, afterRendered) {
    this.canvas = canvas
    this.context = this.canvas.getContext('2d')
    this.pageNumber = 1
    this.rendering = false
    this.afterRendered = afterRendered
  }

  setAfterRendered(afterRendered) {
    this.afterRendered = afterRendered
  }

  loadDocument(path) {
    const renderPdf = (pdf) => {
      this.pdf = pdf
      this.getPage()
    }

    pdfjsLib
      .getDocument(path)
      .promise
      .then(renderPdf)
  }

  setPage(pageNumber) {
    if (1 <= pageNumber && pageNumber <= this.pdf.numPages && !this.rendering) {
      this.pageNumber = pageNumber
      this.getPage()
    }
  }

  nextPage() {
    this.setPage(this.pageNumber + 1)
  }

  prevPage() {
    this.setPage(this.pageNumber - 1)
  }

  getPage() {
    if (!this.rendering) {
      this.rendering = true
      this.pdf.getPage(this.pageNumber).then(this.renderPage.bind(this))
    }
  }

  renderPage(page) {
    const viewport = page.getViewport({scale: 1})

    this.canvas.height = viewport.height
    this.canvas.width = viewport.width

    page
      .render({ canvasContext: this.context, viewport })
      .promise
      .then(() => {
        if (this.afterRendered) { this.afterRendered(this.pdf.numPages, this.pageNumber) }
        this.rendering = false
      })
  }
}

export { MyPdf }

assets/js/app.js

MyPdf を利用します。 HTML の要素の操作はこちらで済ませて MyPdf ではそれらを意識せずに済むようにしています。

import "../css/app.scss"
import "phoenix_html"

import { MyPdf } from './MyPdf'

const myPdf = new MyPdf(document.getElementById('the-canvas'))

myPdf.setAfterRendered((numPages, pageNumber) => {
  document.getElementById('page').innerText = `${pageNumber} / ${numPages}`
})

document.getElementById('next').addEventListener('click', myPdf.nextPage.bind(myPdf))
document.getElementById('prev').addEventListener('click', myPdf.prevPage.bind(myPdf))

myPdf.loadDocument('sample.pdf')

lib/pdfjs_web/templates/layout/app.html.eex

レイアウトテンプレートです。

CDN から PDF.js を読み込んでいます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Pdfjs · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
    <script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js" integrity="sha512-U5C477Z8VvmbYAoV4HDq17tf4wG6HXPC6/KM9+0/wEXQQ13gmKY2Zb0Z2vu0VNUWch4GlJ+Tl/dfoLOH4i2msw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  </head>
  <body>
    <main role="main" class="container">
      <%= @inner_content %>
    </main>
  </body>
</html>

lib/pdfjs_web/templates/page/index.html.eex

PDF の表示領域とページ送りのボタンを配置したテンプレートです。

先に書いた通り、このテンプレートの要素と MyPdf を、 assets/js/app.js が結びつけています。

<button id="prev">&lt;&lt;</button>
<span id="page"></span>
<button id="next">&gt;&gt;</button>

<canvas id="the-canvas"></canvas>