PDFをブラウザ全画面モードにしてスライドショーができる、ビデオ会議で便利かもしれないツールを作ってみた

# 概要

Webブラウザで、手元のコンピュータの中にあるPDFを1つのタブのなかでスライドショー的に投影するためのツールを作ってみました。

デモページは[github.ioのこちら](https://hrkt.github.io/my-pdf-slideshow/)からどうぞ。

リポジトリは[GitHubのこちら](https://github.com/hrkt/my-pdf-slideshow)。

メニューはこんな感じです。

![menu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65002/27ad5582-fca0-5f26-d82c-ffb2178c594c.png)


PDFレンダラは[PDF.js](https://mozilla.github.io/pdf.js/)のサンプルそのまんまで、キーボード操作のあたりだけをメインに足しました。

## 追記

- 2021/01/12 PDF中のURLのリンクを開く機能を付けました。

# 動機

ブラウザでPDFを見るときに、ツールバーがないものを作ってみたかったので。

利用シーンとしては、以下のようなものです。

1. 特にビデオ会議での話なのですが、他人に自分の画面共有をしたいときで、デスクトップ全体ではなく特定のウインドウだけ見せたい、というシーンがあると思います。
2. 1つのブラウザの画面で、関連資料と、WEBページ上の何か(オンラインのVSCodeとか）をタブで開き、フルスクリーン(WindowsだとF11とかで、macOSだとFn + Fとかで)にして、相互に(Ctrl+TABとかのショートカットで)移動できると、画面がしゅっと切り替えられてかっこいい
3. 切り替えた先が、フルスクリーンのPDF資料になっているとかっこいい

上記で、3.だけ、2022年1月時点で、自分が使っているブラウザ(Chrome, Firefox, Safari)では実現できなさそうだったので、 [PDF.js](https://mozilla.github.io/pdf.js/) を使わせてもらって作れないかなと思いました。

# 実装

## 使ったもの

実行時のライブラリで依存したものは、以下の2つです。
- [PDF.js](https://mozilla.github.io/pdf.js/)
- [lodash](https://lodash.com/)

jQueryも、気の利いたロギングライブラリも使いませんでした。

## ページのレンダリング部分

ブラウザのウインドウのサイズに合わせて画面いっぱいにPDFの1ページを描くようにしました。

[PDF.jsのコードサンプルのページ](https://mozilla.github.io/pdf.js/examples/)を元に、元のPDFの縦横を見て、ぴちっと描けるようにしてみたものです。

MacとWindows10 PCで動かしてみて大丈夫そうだったので、これでいいやということにしています。
iOSやiPad OS、Androidでは試していません。

```javascript
  pageRendering = true
  // Using promise to fetch the page
  pdfDoc.getPage(num).then(function (page) {
    // treat scaling

    debug('window.devicePixelRatio' + window.devicePixelRatio)

    const desiredWidth = document.documentElement.clientWidth
    const desiredHeight = document.documentElement.clientHeight
    debug('desiredWidth: ' + desiredWidth + ' desiredHeight: ' + desiredHeight)

    const viewport = page.getViewport({ scale: 1 })
    const scaleW = desiredWidth / viewport.width
    const scaleH = desiredHeight / viewport.height

    const scale = Math.min(scaleW, scaleH)

    debug('scale:' + scale)
    const scaledViewport = page.getViewport({ scale: scale })

    debug('viewport: ' + viewport.width + ' x ' + viewport.height)
    debug('scaled: ' + scaledViewport.width + ' x ' + scaledViewport.height)
    canvas.height = Math.floor(scaledViewport.height)
    canvas.width = Math.floor(scaledViewport.width)

    // Render PDF page into canvas context
    const renderContext = {
      canvasContext: ctx,
      viewport: scaledViewport
    }
    const renderTask = page.render(renderContext)

    // Wait for rendering to finish
    renderTask.promise.then(function () {
      pageRendering = false
      if (pageNumPending !== null) {
        // New page rendering is pending
        renderPage(pageNumPending)
        pageNumPending = null
      }
    })
  })

  updatePageNumLabel(num)
}

```

## キーボードでの操作

### ページ移動

画面には操作ボタンを出さないので、ページの移動はキーボードでの操作になります。

Unixのlessコマンドに準じて、"SPACE"(か "f")で次のページ、"b"で前のページにしました。先頭"p"と末尾"G"へのジャンプも付けました。
(lessだと"G"なのですが、大文字小文字でなくキーだけ見ているので、"g"でも同じ効果)

ブラウザ側にはなるべくキーイベントを受け取ってほしくないので、event.preventDefault()を呼ぶようにしたのですが、「F」キーだけ、macOSでの「最大化(Fn + F)」とかぶっており、かつ、[JavaScriptのKeyEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)だけではどうにもならなさそうだったので、

```javascript
/**
 * handles "less" command style short cuts
 *
 * @param {*} evt
 * @returns true if event is handled in this function, otherwise false
 */
function handleLessStyleShortCut (evt) {
  const key = evt.keyCode || evt.charCode || 0
  debug(key, evt.metaKey, evt.keyCode, evt.charCode)
  switch (evt.code) {
    case 'KeyB': {
      evt.preventDefault()
      onPrevPage()
      break
    }
    case 'KeyF': {
      if (!shouldPassThroughKeyEvent(evt)) {
        evt.preventDefault()
      }
      onNextPage()
      return true
    }
    case 'Space': {
      evt.preventDefault()
      onNextPage()
      break
    }
    case 'KeyP': {
      evt.preventDefault()
      pageNum = 1
      queueRenderPage(pageNum)
      break
    }
    case 'KeyG': {
      evt.preventDefault()
      pageNum = pdfDoc.numPages
      queueRenderPage(pageNum)
      break
    }
    case 'KeyR': {
      evt.preventDefault()
      queueRenderPage(pageNum)
      break
    }
    default: {
      // do nothing
      return false
    }
  }
  return true
}

```

## ページの前後以外

以下も付けました。

- メニューを出したり消したり「.」
- ファイルを開く「o」
- 画面再描画「r」  (リサイズうまくいくようにしたので、使うことはないと思いますが)
- 数字キーでページを入れて、「Enter」を押すと指定したページにジャンプ
- リンクをクリックしたとき、新しいウインドウにリンク先を開く(後述)

## リンクを開く

pdf.jsでは、様々な「Annotation」が提供されており、URLを元にブラウザを開くものは「Link」という名前がついていました。

ページをレンダリングする際に、ページ内のリンク一覧を覚えておき、Canvasがクリックされたときに、位置が対応するリンクを開くようにしました。

覚えるところ：

```javascript
    page.getAnnotations().then((annotations) => {
      for (let i = 0; i < annotations.length; i++) {
        const annotation = annotations[i]
        console.dir(annotation)
        if (annotation.subtype !== 'Link') {
          // only handle link. ignore other types like line, rect, etc.
          continue
        }
        const url = annotation.url
        const x1 = annotation.rect[0]
        const y1 = annotation.rect[1]
        const x2 = annotation.rect[2]
        const y2 = annotation.rect[3]
        linksInCurrentPage.push(new LinkArea(x1 * scale, canvas.height - y2 * scale, x2 * scale, canvas.height - y1 * scale, url))
      }
    })
```

位置に応じてリンクを開くところ：

```javascript
/**
 * handles click event in the-canvas area
 * @param {*} evt evt
 */
function handleCanvasClickEvent (evt) {
  const offset = canvas.getBoundingClientRect()
  const x = evt.clientX - offset.left
  const y = evt.clientY - offset.top
  for (let i = 0; i < linksInCurrentPage.length; i++) {
    const l = linksInCurrentPage[i]
    if (
      l.x1 <= x &&
      x <= l.x2 &&
      l.y1 <= y &&
      y <= l.y2
    ) {
      const tgt = evt.ctrlKey ? '_blank' : 'mypdfslideshow'
      window.open(l.url, tgt)
    }
  }
}
```


## そのほか見た目

- ファイルをロードしたあとは、ウインドウのタイトルにファイル名を反映させています。
- PDFを移し始めると操作メニューは消すので、メニュー画面の出し方が画面に出ていないのをどうしよう、、、と思いましたが、プログラマであればきっと「.」も押すに違いない、なぜならばGitHubリポジトリでそのままVSCodeを開くショートカットも「.」だし、という謎の理由により、よしとしました。

## 動作

### 方法1. 

- [デモサイト](https://hrkt.github.io/my-pdf-slideshow/)、github.io上にあります。

### 方法2.

GitHubのリポジトリをクローンし、お手元でWEBサーバを上げるツール( VSCodeの機能拡張の[Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)や、 [Web Servrer for Chrome](https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb) )を使ってサーバを上げ、ブラウザから使えます。

## テスト

[手動で数パターンやっただけ](https://github.com/hrkt/my-pdf-slideshow/blob/main/README.md#test)です。PDFファイルは、46ページ、3MBytes程度のもので確認しています。

画面設定、OS側でのスケーリングを2種類ずつためして、くずれないであろうところまでは確認しました。2022年1月時点で自分が使いそうなものについては動いたのでよしとします。

|OS|Browser|Display Settings|
|:---|:---|:---|
|Windows10 21H1|Firefox 95.0.2|100%, 125%|
|Windows10 21H1|Chrome 97.0.4692.71|100%, 125%)|
|macOS 12.0.1 (Monterey)|Safari 15.1|normal, expand space|

注意：Windows環境で、画面のスケーリングを変更した後は、設定画面上にも注意がある通り、ブラウザの再起動が必要になることがあります。少なくとも2022年1月の筆者の環境では、ブラウザの再起動が必要でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65002/24f12671-ad22-7660-ef41-bd3d039ef3b2.png)


# おわりに

PDF.jsがとてもよくできているので、特に難しいことはしなくてもサクッとPDFがブラウザで描けて楽しかったです。

## future work

- もしかすると、キーボードのショートカットで、矢印キーでの移動も付けておくとよかったかも。。と思いました。誰かからつけてほしいと言われたら機能つけることにします。

- PDF.jsは、リモートにあるファイルも取り扱ってくれるのですが、異なるサイトからのPDFのロードでは、提供側でCORS対応のヘッダーつけてくれていないといけない、という縛りがあります。対応していないサイトからダウンロードしようとして「エラーがでるんだが。。」と言われても困ってしまうので、意図して機能を落としました。これは「知っている人モード」をつけたうえで、有効化してもよいかもしれません。

- メニューが若干しょぼいので見た目はちょっと変えたいかも。