著作一覧 |
HTMLからPDFを生成する必要があって、以下の方法を考えた。
Edgeに該当URIを叩かせて、プリンタドライバにMS純正のPDF出力を設定しておいてJavaScriptでprintを呼び出す。
で、これが確実に動くのはわかっているのだが、実行環境がGNU/Linuxなのでどうにもならない。
Firefoxはapt-getで入れられるのでメンテナンスが楽そうだが、PDFに出力する簡単な方法が考え付かない。
しょうがないので、ヘッドレスChrome(頭があって手足がないが正解な気がするが本人がヘッドレスを名乗っているのだからまあ良いのだろう)を使うことにした。
Chromeはapt-getはできないが、debが用意されているから我慢する(のだが、今、あらためてみると、どうやってdebをダウンロードしたのかまったくわからない。どうあってもWindows版をダウンロードさせようとするのだが、キャンセルしまくると小さな文字のリンクがあって、Linuxを選択すると、またダイアログが出てくる……。しかし1週間くらい前にはまともなページにたどり着けて、そのURIをcurlしたんだよなぁ)。
説明を読もうとすると、なぜか頼みもしないのに日本語のページ(ヘッドレス Chrome ことはじめ)に飛ばされて、かつ情報が古い(Windowsでは待てとなっているが、Windowsでも--headlessで動作するので古い。書いてあるChromeは59についてだが、Windows版は65になっているし)が、まあ、このページからリンクされているところはそれっぽいから問題なかろう。
・このメモを書くためにあらためて「ヘッドレス Chrome ことはじめ」のページを眺めていたら、一番最後にドロップダウンリストで言語を選択できるようになっていて、US Englishにしたら、Windowsは60からのサポートとバージョン番号付きで書いてあった。なら65で動いて当然だ。というか、なんで先頭で言語を選択できるようにしていないのだろうか(または常に同期を取らないのだろうか)。なんか本当に面倒くさいベンダーだなぁ。
で、PDFの出力方法として chrome --headless --disable-gpu --print-to-pdf https://www.chromestatus.com/
というのが出ているのだが、これが全然だめだ。
コンテンツに重なって頼みもしないヘッダとフッタが出力される。どうしろと。
で、調べるとStack Overflowにやはり同じ問題で困った人たちが質問しているが、有効な解答が、マージンなしにして出力すればヘッダとフッタが下になって隠されるというやつで、なんだそれ。コンテンツとして印刷用のマージンを入れることになるわけだが、それだとWebとPDFでスタイルを変えなければならないし、しかも、PDF出力時のメディアタイプがわからないから話にならない(@media PDFかな? とか思ったがall、print、screen、speech以外に何が使えるか調べるのもあほらしくてやらなかった)。
が、DevTool(Chromeのデバッガインターフェイス)を眺めたら、Page#printToPDFというメソッドがあって、そのパラメータに、displayHeaderFooter
というまさにおれが欲しいものがあるではないか。
この場合、Chromeを起動しておいて外部からWebSocketで操作することになる。WebSocketの口はhttpで/jsonから取得する、おお、こういうインターフェイスは大好きだ。こういうところは素晴らしい。
で、起動方法はリモートデバッグオプションを指定して、chrome --headless --disable-gpu --remote-debug-port=9222
とすれば良いらしい(というか、これで動いた)。ポートの9222はデフォルトらしいので変えたければ別の値を指定すれば良いのだろうけど、いずれにしてもクライアントが最初にHTTPでアクセスするからウェルノウンにしておく必要はある。あと、disable-gpuは無くても動くようだが、バグを突くといやだから(バグ回避用の設定っぽい)指定する。これをnohupしておいて別口のクライアントからアクセスすることになる。
ぱっと見、DevToolを操作するためのnode用のライブラリがあるようだが、そんなもの使いたくないので、自分でopen-uriを使ってポート9222の/jsonを叩いてPageのURIを取ってWebSocketを使ってと書き始めたが、ふと、こんな程度の処理なら誰か作っているのではと気づいた。で、さらに探すと(しかしドキュメント性が低いのでやたらと時間が食われるのには閉口した)、chrome_remoteというGemがあることがわかったので、それを利用することした。
えらく簡単だ。
require 'chrome_remote' require 'base64' class PdfWriter def initialize() @chrome = ChromeRemote.client @chrome.send_cmd 'Network.enable' # Navigateに必要 @chrome.send_cmd 'Page.enable' # Pageオブジェクトの操作に必要 @chrome.send_cmd 'Runtime.enable' # JavaScript実行に必要 end def to_pdf(uri) @chrome.send_cmd 'Page.navigate', url:uri @chrome.wait_for 'Page.loadEventFired' # ロード後に数秒実行に必要なJavaScriptが動くとしたらここで待機するか、またはイベントをチェックする必要がある。 ==begin JavaScriptの呼出しには、Runtime.evaluateを使う ret = @chrome.send_cmd 'Runtime.evaluate', expression: "document.getElementById("foobar').baz()" ==end ret = @chrome.send_cmd 'Page.printToPDF', dispalyHEaderFooter:false, # これがやりたかったことだ printBackground: false, paperHeight: 11.7, paperWidth: 8.3 # A4: 11.7inch * 8.3inch ret['data'] # JSONが返されてdataプロパティにBase64エンコードされたPDFが格納されている end def to_pdf_file(file, uri) data = to_pdf(uri) File.open(file, 'wb') do |fout| fout.write(Base64.decode64(data)) end end end
実際は同じChromeのインスタンスを使いまわしたいので、Mutex使って排他制御したり他にもいろいろするが、単発ならこれでOK。
ジェズイットを見習え |