つーさにブログ

つうさにのメモ用ブログ

EmacsでPDF.jsしたかった

WebViewの夢

環境: Emacs 28.0.50

はじめに

PDF.jsMozillaが開発しているブラウザでPDFを表示するためのJavaScriptライブラリ兼PDFビューアアプリケーション。Mozilla FirefoxのPDFビューアとして利用されているが、PDFビューアの実態はWebアプリケーションなので他のモダンWebブラウザからでも利用できる。

PDF.jsの利用例として、VSCode拡張機能vscode-pdfがある。VSCodeにはデフォルトでPDFビューア機能はないが、vscode-pdfはVSCodeのWebview上でPDF.jsのPDFビューアを表示することでPDFビューア機能を提供している。

...ム、WebView。筆者は最近GNU EmacsWebKitによるWebView機能に熱心で、いくつか記事を書いている(これとかこれ)。VSCodeがWebview APIとPDF.jsを使って高機能PDFビューアになっているなら、これをEmacsでも試してみたくなった。

ということで、今回はEmacsのWebViewでPDF.jsのPDFビューアするメモ。

EmacsでPDF.jsしてみる

以前の記事同様、--with-xwidgetsでビルドしたNS版Emacsを使う。macOSのバージョンはCatalinaである。

GNU Emacs 28.0.50 (build 1, x86_64-apple-darwin18.7.0, NS appkit-1894.60 Version 10.15.7 (Build 19H2))

動作確認

https://github.com/mozilla/pdf.js#online-demo にあるデモを試す。

現在、PDF.jsは「async/awaitなどのモダン機能をサポートしているブラウザ用」と「ES5互換にした古いブラウザ用」の二つが提供されている。ビルドしたEmacsでアクセスして動作確認する。

M-x widget-webkit-browse-url https://mozilla.github.io/pdf.js/web/viewer.htmlしてモダンブラウザ用のデモにアクセスすると、しっかり表示できた。

f:id:tsuu_mmj:20201009013841p:plain
Emacsからデモにアクセス

ということで、PDF.js WebサイトのDownloadからPrebuilt((ES5-compatible)でないほう)をローカルにダウンロードしていろいろ試す。

試したいこと

PDF.jsを使って、他のEmacsメジャーモードと同様、「PDFファイルを開くとpdfjs-modeみたいなメジャーモードになって、そのPDFファイルをWebViewで閲覧できる」といったことが可能か確認する。

PDF.jsのPDFビューアには右上の「ファイルを開きます」アイコンをクリックすることでマシン上の任意のPDFファイルを開ける機能があるが、今回は上記の理由でそれは使わずに即座に目的のPDFを開く方法を模索する1

HTTPアクセス編

ダウンロードしたzipを展開し、展開したディレクトリでHTTPサーバを建てる:

$ unzip pdfjs-2.6.347-dist.zip -d pdfjs-2.6.347-dist
$ cd pdfjs-2.6.347-dist/
$ python3 -m http.server 8000
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

PDF.jsはデフォルトでデモ用のPDFを付属している。試しに以下を評価してPDFに直接アクセスしてみる:

(let ((url "http://localhost:8000/web/compressed.tracemonkey-pldi-09.pdf"))
  (xwidget-webkit-browse-url url)
  (display-buffer (xwidget-buffer (xwidget-webkit-current-session))))

SafariでおなじみのPDFビューアで表示された。(GTKEmacsだとどうなるのだろう?)

f:id:tsuu_mmj:20201009022450p:plain
SafariのPDFビューアっぽい

以下を評価すればデモを見た時と全く同じにPDFビューアが表示される:

(let ((url "http://localhost:8000/web/viewer.html"))
  (xwidget-webkit-browse-url url)
  (display-buffer (xwidget-buffer (xwidget-webkit-current-session))))

次に、HTTPサーバを動かしているディレクトリのweb/以下にtl2020-sample.pdfという適当なPDFを設置してそれをPDF.jsのPDFビューアから閲覧する。 以下を評価すればPDF.jsのPDFビューアにtl2020-sample.pdfが表示される:

(let ((url "http://localhost:8000/web/viewer.html?file=./tl2020-sample.pdf"))
  (xwidget-webkit-browse-url url)
  (display-buffer (xwidget-buffer (xwidget-webkit-current-session))))

f:id:tsuu_mmj:20201009023249p:plain
ローカルのPDFを閲覧できた

file:// アクセス編

上ではHTTPサーバを建ててPDFファイルを閲覧した。しかしvscode-pdfではHTTPサーバを建てずにPDF.jsのPDFビューアを利用している。EmacsでもHTTPサーバを建てずにPDF.jsできないだろうか?

ということでサーバを建てずにfile://でアクセスしてみる。

まず、上でしたようにPDFファイルに直接アクセスしてみる。以下を評価する2

(let ((url "file:///Users/(path/to)/pdfjs-2.6.347-es5-dist/web/compressed.tracemonkey-pldi-09.pdf"))
  (xwidget-webkit-browse-url url)
  (display-buffer (xwidget-buffer (xwidget-webkit-current-session))))

この場合、SafariなPDFビューアでしっかり閲覧できた。

では、PDF.jsを試す。以下を評価する:

(let ((url "file:///Users/(path/to)/pdfjs-2.6.347-es5-dist/web/viewer.html"))
  (xwidget-webkit-browse-url url)
  (display-buffer (xwidget-buffer (xwidget-webkit-current-session))))

この場合、以下のようになりPDF.jsのPDFビューアでデモPDFが表示されない。

f:id:tsuu_mmj:20201009024518p:plain
PDF.jsできない図

これは何故かというと、PDF.jsのPDFビューアがデモ用のPDFファイルを得るのにXHRを使うが、file://ではXHRが使えないため3Missing PDF Fileになってしまう。

file:// アクセスでもゴリ押し編

PDF.jsのPDFビューアで任意のPDFを閲覧するには「JavaScirptでwindow.PDFViewerApplication.openを呼ぶ」という方法もある。ただしwindow.PDFViewerApplication.open"file://~"のようなurl文字列を渡しても結局XHRされてしまうため開けない。

window.PDFViewerApplication.openは引数にurl文字列ではなく、Uint8Arrayといった型付き配列をPDFバイナリとして渡すことができる。ということで、PDFをEmacs Lispでバイナリ配列にしてブラウザに評価してもらい任意のPDFを開く。🙃

以下を評価する:

(progn
  (xwidget-webkit-browse-url "file:///Users/(path/to)/pdfjs-2.6.347-dist/web/viewer.html")
  (sit-for 0.1)
  (let* ((xw (xwidget-webkit-current-session))
         (filename "/Users/(path/to)/pdfjs-2.6.347-dist/web/compressed.tracemonkey-pldi-09.pdf")
         (pdf-bytes (vconcat
                     ;; from f-read-bytes
                     (with-temp-buffer
                       (set-buffer-multibyte nil)
                       (setq buffer-file-coding-system 'binary)
                       (insert-file-contents-literally filename)
                       (buffer-substring-no-properties (point-min) (point-max)))))
         (js-array (replace-regexp-in-string
                    " " ","
                    (format "%s" pdf-bytes))))
    (xwidget-webkit-execute-script
     xw
     (format "window.PDFViewerApplication.open(new Uint8Array(%s));" js-array))
    (display-buffer (xwidget-buffer xw))))

上を評価するとちょっと時間はかかるがPDFを表示できた。上で貼ったデモPDF表示と同様の表示のため画像は載せないが、バイナリ列を与えることでサーバを建てずに任意のPDFを表示できた。

と、調子に乗ってtl2020-sample.pdfも同様の手法で開いてみるとこの有様。

f:id:tsuu_mmj:20201009032343p:plain
日本語が消えた図

ToUnicode CMapが埋め込まれていないフォントを表示するのに必要なCMapファイルをXHRで取得できないため、日本語が表示できなかった。これは致命的なのでfile://アクセスでのゴリ押しも諦めて、これにて検証終了とする。

VSCodeのWebview API

VSCodeのWebview APIではローカルにHTTPサーバを建てずともちゃんとWebアプリケーションが動く。

VSCode拡張機能でWebview API利用時にlocalResourceRootsを設定し、それ以下のファイルに対してはしっかりXHRも働くようになっているのだと思われる4

VSCodeのWebviewとEmacsのWebView

EmacsのWebViewはEmacs内でWebブラウジングするための機能という感じである。対してVSCodeのWebviewはWebの力を使ってクロスプラットフォームな機能を実現するためのもので、そもそもWebブラウジングするためのものではなさそう。VSCodeはデフォルトでPNGなどの画像を表示できるが、これもWebviewを使っている。

また、Emacsのxwidget-webkit機能はまだまだ発展途上だ。Webの力を使ってEmacs上でクロスプラットフォームなアプリケーションの実現はまだまだ先になりそう(来ないかもしれない)。

まとめ

EmacsのWebViewでは(VSCodeのとは違って)マトモにWebアプリケーションを動作させるにはHTTPサーバ建てる必要がありそう。

EmacsのWebViewは発展途上で操作性に難アリなので、やはりEmacsでPDF見るなら pdf-tools だろうか。

おしまい。


  1. <input type="file">なので、頑張っても.click()呼んでユーザに選択してもらうところまでしかできないはず(多分)。

  2. (path/to) は省略したもの。

  3. 使えるようにするにはパッチを当てる必要がある swift - iOS - WKWebView Cross origin requests are only supported for HTTP - Stack Overflow

  4. VSCodeはelectoronのprotocol APIを使って、file://ではない独自のschemeを利用しているようだ。参考: https://github.com/microsoft/vscode/blob/6bebcbb58d95e59ffde0e372888c5eea3995bf82/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts