LoginSignup
2
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-28

連載一覧

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

前回のまとめ

Web上でPDF閲覧にスクリプト処理を追加するのは、pdf.jsライブラリのおかげで想像よりはるかに簡単に済みます。前回まで、 ほぼノーコードの以下の手順で、PDFファイルビューア ができました。

たったこれだけで、普通に使う分には全く問題の無いPDFビューアの完成です。唯一気になるのは、PDFファイル閲覧時のURL欄が chrome-extension://[クロム拡張ID]/pdfjs-2.14.305-dist/web/viewer.html?file=[PDFのURL] と謎形式なので、コピペして他人と共有しにくいこと位ですが、実はほとんどのクロム拡張PDFビューアも同じ状態です。知る限りPDF Viewer for Vimium Cだけが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

独自スクリプト追加

クロム拡張フォルダの直下に、以下のスクリプトを 文字コード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
    })
})

クロム拡張をリロードして、これとかこれを開くとツッコまれて、想定通り大変ウザいです。これがツッコまれないのは、「N U L L」と間に空白文字が入っているからです。 まだ修行が足りなかったか。

勘のよい方は、my_script.jsではDOM変化のたびに最大一度しかツッコんでいないのに

  • ファイルを開いただけで何度もツッコまれた
  • ページをめくっていったら、追加でツッコまれた
  • 後の方のページにNULLと書いてあるのに、開いただけではツッコまれなかった

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

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

PDFビューア独自機能を利用する方法

ここからはJavaScriptではやりにくいので、TypeScriptで書きます。

必要なパッケージのインストール

  1. (nvm+)npm
    • 未導入なら、この辺などを参考に、導入してください
  2. TypeScript
    • 未導入なら、クロム拡張フォルダで npm install --save-dev typescript などを実行して、導入してください
  3. pdfjs-dist
    • 未導入なら、クロム拡張フォルダで npm install --save-dev pdfjs-dist などを実行して、導入してください

pdfjsは連載一回目で配置していますが、なぜここで再度パッケージを追加するかというと、

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

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

TypeScriptで処理実装

クロム拡張フォルダの直下に、以下のスクリプトを 文字コードUTF-8で 追加します。処理の内容はコメントを見てください。VSCodeなどIDEで作業すると、as PDFDocumentProxy などで型を付与したあとは、入力補助も効いていい感じです。型の詳細は、この辺を参考にしてください。

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) {
            // ロードが終わったら、1秒ごとのロード完了モニタを解除して処理本体を呼ぶ
            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と書いてあるPDFを開くと、きちんと最初にツッコまれます。PDFビューア独自機能により、DOM範囲外の情報にアクセスできたことが分かります。

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

最後に

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

2
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
2
0