PDFを閲覧＋αするクロム拡張の楽な作り方③

# 連載一覧
- **[PDFを閲覧＋αするクロム拡張の楽な作り方](https://qiita.com/largetownsky/items/1cb016598824b7ea1b1e)**
  - クロム拡張でWeb上のPDFファイルを閲覧できる様になる
  - ただし、URLは謎形式で手入力する必要あり
- **[PDFを閲覧＋αするクロム拡張の楽な作り方②](https://qiita.com/largetownsky/items/fe854ea78960816538e5)**
  - 普通のURL入力やリンククリックのPDF閲覧に対応
  - 普通にPDFビューアとして使える様になる
- **[PDFを閲覧＋αするクロム拡張の楽な作り方③](https://qiita.com/largetownsky/items/e61624ba8be7e1d50fed)**
  - PDF閲覧に独自処理を追加

拙作のWebにリンク＆ノートを追加できるクロム拡張も、PDF対応する予定ですので、よろしくお願いします。

https://qiita.com/largetownsky/items/b7826f63b0cf7d8626b3

# 前回のまとめ
Web上でPDF閲覧にスクリプト処理を追加するのは、[pdf.jsライブラリ](https://mozilla.github.io/pdf.js/)のおかげで想像よりはるかに簡単に済みます。前回まで、 **ほぼノーコードの以下の手順で、PDFファイルビューア** ができました。
- [host_permissions](https://qiita.com/largetownsky/items/1cb016598824b7ea1b1e#%E3%82%AF%E3%83%AD%E3%83%A0%E6%8B%A1%E5%BC%B5%E3%81%AE%E8%A8%AD%E5%AE%9A)・[declarativeNetRequest](https://qiita.com/largetownsky/items/fe854ea78960816538e5#url%E3%81%AE%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E8%A8%AD%E5%AE%9A)・[web_accessible_resources](https://qiita.com/largetownsky/items/fe854ea78960816538e5#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AE%E8%A7%A3%E9%99%A4)のクロム拡張設定
- [pdf.jsのダウンロード・解凍・配置](https://qiita.com/largetownsky/items/1cb016598824b7ea1b1e#pdfjs%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A8%E9%85%8D%E7%BD%AE)
- pdf.jsライブラリ内のコードを[3行だけコメントアウト](https://qiita.com/largetownsky/items/1cb016598824b7ea1b1e#pdfjs%E3%81%AE%E5%BE%AE%E4%BF%AE%E6%AD%A3)

たったこれだけで、普通に使う分には全く問題の無いPDFビューアの完成です。唯一気になるのは、PDFファイル閲覧時のURL欄が **chrome-extension://[クロム拡張ID]/pdfjs-2.14.305-dist/web/viewer.html?file=[PDFのURL]** と謎形式なので、コピペして他人と共有しにくいこと位ですが、実はほとんどのクロム拡張PDFビューアも同じ状態です。知る限り[PDF Viewer for Vimium C](https://chrome.google.com/webstore/detail/pdf-viewer-for-vimium-c/nacjakoppgmdcpemlfnfegmlhipddanj)だけがiframe内にviewer.htmlを配置することで解決しています。


# 今回の概要
今回、 **作ったPDFビューアに独自処理を追加** する方法をまとめます。

- PDFビューア独自機能を介せずDOMを直接操作する方法
- PDFビューア独自機能を利用する方法

の2本立てです。


## PDFビューア独自機能を介せずDOMを直接操作する方法
作ったPDFビューアは、viewer.htmlが内部でviewer.jsなどを呼んで、以下の要素をDOMに追加することで、PDFを描画しています。

- HTML5の **CANVAS** 要素内にテキスト以外を描画
- テキストだけは別に **SPAN** の集合として描画

つまり、PDFビューアを無視して、DOM上のこれらの要素にアクセスすれば、PDFファイルに応じた処理ができます。実現するには、以下の手順が簡単でしょう。

### viewer.htmlに独自スクリプト呼び出し追加
- クロム拡張フォルダ
  - pdfjs-2.14.305-dist
    - web
      - viewer.html

の中に、以下の様な記述を追加して、PDF閲覧時に独自スクリプトも呼び出すようにします。
![my_script.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747214/c3d561c8-77a1-7cc8-1780-48a8e48f9529.png)

### 独自スクリプト追加
クロム拡張フォルダの直下に、以下のスクリプトを **文字コードUTF-8で** 追加します。処理の内容はコメントを見てください。
```my_script.js
const observer = new MutationObserver(() => {
    // 変更があったら、body以下のテキスト全文にNULLが含まれるかのチェック
    const text = document?.body?.textContent
    if (typeof text === "string" && text.indexOf("NULL") >= 0) {
        // NULLがあったらツッコむ
        alert("ヽ( ･∀･)ﾉ┌┛ｶﾞｯΣ(ﾉ`Д´)ﾉ")
    }
})

addEventListener("load", () => {
    // body以下の変更を監視
    observer.observe(document.body, {
        subtree: true,
        childList: true
    })
})
```
クロム拡張をリロードして、[これ](https://www.sra.co.jp/Portals/0/files/gsletter/pdf/GSLetterNeoVol154.pdf)とか[これ](http://www.hpcs.cs.tsukuba.ac.jp/~msato/lecture-note/prog2-2012/prog2-lecture5-print.pdf)を開くとツッコまれて、想定通り大変ウザいです。[これ](http://www2.rku.ac.jp/ikawa/y2003/twcu/c2-04.pdf)がツッコまれないのは、「N U L L」と間に空白文字が入っているからです。 ~~まだ修行が足りなかったか。~~

勘のよい方は、my_script.jsではDOM変化のたびに最大一度しかツッコんでいないのに
- ファイルを開いただけで何度もツッコまれた
- ページをめくっていったら、追加でツッコまれた
- [後の方のページにNULLと書いてある](http://www.newves.org/~mori/17Programming/PDFs/170616slide.pdf)のに、開いただけではツッコまれなかった

という点に気づかれたかもしれません。PDF.js付属のビューアは、巨大PDFファイルでも快適に閲覧できるよう、現在表示している周辺しかDOMに実体化せず、ページ移動などをトリガに **必要に応じて追加で実体化＆不要そうなDOM要素の開放** を自動でしてくれます。何という上げ膳据え膳…

ただしこのため、 PDFファイル全体を対象に処理をしたい場合、DOMだけ相手にしていてはダメ、ということになります。 **DOMに実体化していない部分へのアクセスには、PDFビューア独自機能へのアクセスが必要** です。


## PDFビューア独自機能を利用する方法
ここからはJavaScriptではやりにくいので、TypeScriptで書きます。

### 必要なパッケージのインストール
1. (nvm+)npm
    - 未導入なら、[この辺](https://qiita.com/akipon0821/items/eaeffe79221cfcd4d258)などを参考に、導入してください
1. TypeScript
    - 未導入なら、クロム拡張フォルダで **npm install --save-dev typescript** などを実行して、導入してください
1. pdfjs-dist
    - 未導入なら、クロム拡張フォルダで **npm install --save-dev pdfjs-dist** などを実行して、導入してください

pdfjsは連載一回目で[配置](https://qiita.com/largetownsky/items/1cb016598824b7ea1b1e#pdfjs%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A8%E9%85%8D%E7%BD%AE)していますが、なぜここで再度パッケージを追加するかというと、

- 導入済みの方は、webサーバなどで閲覧機能だけ利用する想定のパッケージ
  - 閲覧の便宜のため、viewer.htmlが付属している
  - TypeScriptの開発に重要な型情報が付いていない
- 今回導入する方は、pdfjsライブラリをプログラム上で利用する想定のパッケージ
  - viewer.htmlが付属してない
  - TypeScriptの開発に重要な型情報が付いている

という違いがあります。バージョンが同じなら、内部のviewer.jsなどは同じようですが、違うなら新しい方に合わせるなど調整しておくのが無難です。

### TypeScriptで処理実装
クロム拡張フォルダの直下に、以下のスクリプトを **文字コードUTF-8で** 追加します。処理の内容はコメントを見てください。VSCodeなどIDEで作業すると、**as PDFDocumentProxy** などで型を付与したあとは、入力補助も効いていい感じです。型の詳細は、[この辺](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFDocumentProxy.html)を参考にしてください。

```my_script.ts
import {type PDFDocumentProxy, type PDFPageProxy} from "pdfjs-dist"
import {type TextItem} from "pdfjs-dist/types/src/display/api"

let pdfViewerInitializeMonitor

addEventListener("load", () => {
    pdfViewerInitializeMonitor = setInterval(() => {
        // viwer.jsがwindowオブジェクトに追加しているAPIアクセスオブジェクトを取得
        const viewer = (window as any)["PDFViewerApplication"]
        // HTMLとしてのロード後も続くPDFのロードを待つ
        if (viewer && viewer.pdfDocument && viewer.downloadComplete) {
            // ロードが終わったら、１秒ごとのロード完了モニタを解除して処理本体を呼ぶ
            clearInterval(pdfViewerInitializeMonitor)
            myFunc(viewer.pdfDocument as PDFDocumentProxy)
        }
    }, 1000)
})

async function myFunc(doc: PDFDocumentProxy): Promise<void> {
    const pageCount = doc.numPages
    for (let i = 1; i <= pageCount; ++i) {
        // 全ページをチェックして、NULLを見つけたらツッコむ
        if (await pageHasText(doc.getPage(i), "NULL")) {
            alert("ヽ( ･∀･)ﾉ┌┛ｶﾞｯΣ(ﾉ`Д´)ﾉ")
            return
        }
    }
}

async function pageHasText(page: Promise<PDFPageProxy>, text: string): Promise<boolean> {
    // ページのテキストを得る
    const textContent = await (await page).getTextContent()
    for (const item of textContent.items) {
        const textItem = item as TextItem
        const t = textItem.str
        // 指定のテキストが見つかればtrueを返す
        if (t.indexOf(text) >= 0) {
            return true
        }
    }
    return false
}
```

### TypeScriptのトランスパイル
クロム拡張フォルダで **npx tsc my_script.ts --target es2015 --moduleResolution node** を実行するだけで、トランスパイルされたmy_script.jsができます。targetなどは、トランスパイルを成功させるためのおまじないです。

クロム拡張をリロードして、[後の方のページにNULLと書いてある](http://www.newves.org/~mori/17Programming/PDFs/170616slide.pdf)PDFを開くと、きちんと最初にツッコまれます。PDFビューア独自機能により、DOM範囲外の情報にアクセスできたことが分かります。

なお、実行時にエラーが出た場合、viewer.htmlへのmy_script.js登録時にtype="module"を忘れているためかもしれません。

# 最後に
PDFビューアへの独自処理追加、いかがだったでしょうか？ 思ったより簡単にPDFを扱えることが分かって頂ければ幸いです。