PuppeteerでPDF出力する

はじめに

Headless Chrome を Node.jsで使用できるライブラリ PuppeteerでPDFを出力する紹介です。

サーバサイドで、HTML出力したものをPDFにして、帳票としてクライアント側に出力するなんてことありますよね・・・。あんまりないかな・・・。
どちらにしても、僕はよくやるんです。そういうときPuppeteerを使用するんですが、ハマったポイントとかもあるので、合わせて紹介します。

簡単な使い方。

サンプルは本家にあるとおりです。

GoogleChrome/puppeteer

注意点として、NodeJSのバージョンはv7.6.0以降を選んでください。それ以下だと、サンプロプログラムが動かないです。

まずは、puppeteerをインストールします。

$ npm i --save puppeteer

それから、プログラムを書きます。ここでは、index.jsとしましょう。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp', {waitUntil: 'networkidle2'});
  await page.pdf({path: 'hn.pdf', format: 'A4'});

  await browser.close();
})();

そして、実行

$ node index.js

そうすると、hn.pdfが出力されていると思います。googleのトップ画面がpdf出力されているはずです。

いちいちpuppeteerをインストールして試すのが面倒な方は、

を使ってみて、Javascriptのasyncメソッドの中だけコピーして実行してみてください。その場で、pdfを確認できるはずです。

即値のHTMLをPDFにする。

前のサンプルでは、URLを指定していましたが、即値のHTMLを出力するには、gotoメソッドの代わりにsetContentメソッドを使用します。
たとえば、こんな感じです。
asyncメソッドの中だけ記載します。

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  let html = '<html><body><h1>Hello World</h1></body></html>'
  await page.setContent(html);
  await page.pdf({path: 'hn.pdf', format: 'A4'});

  await browser.close();

描画したいHTMLをsetContentに渡してあげるだけで、即値のHTMLを出力することができます。

ただし外部リンクを含まないHTMLだけです。ここが僕がはまったところです。

setContentメソッドのあとPDF出力すると、外部リンクのコンテンツを取得する前にPDF出力がされてしまいます。
そうすると、<head>タグ内で外部CSSの読み込みをすると、読み込みが完了する前にPDF出力されてしまうため、PDFが真っ白になります。

駄目な例

const browser = await puppeteer.launch();
const page = await browser.newPage();
let html = '<html>';
html += '<head><link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"> <</head>'
html +='<body><h1>Hello World</h1></body></html>'
await page.setContent(html);
await page.pdf({path: 'hn.pdf', format: 'A4'});

await browser.close();

このサンプルは、Hello Worldが描画されることなく、真っ白な出力になってしまいます。CSSを読み込み前にPDFが出力されてしまうためです。

外部リンクを含む即値HTMLの出力

真っ白な出力を回避するためには、外部リンクを読み込んだ後でPDFを出力しなければなりません。でも、setContentには、ウエイトをしてくれるオプションは存在しません。
それで、代わりに最初に使ったgotoメソッドを使用します。
gotoメソッドでは、外部リンク先のコンテンツを取得し、ネットワークを使用しなくなるまでウエイトをかけるオプションがあります。
ただし、単にgotoメソッドを使うのではなく、HTMLをDATA URLの形式にします。その方法はとても簡単で、HTMLのソースのはじめにdata:text/html,の文字列を追加するだけです。

さきほどのサンプルをgotoメソッドで書き直したものを記載します。

大丈夫な例

const browser = await puppeteer.launch();
const page = await browser.newPage();
let html = '<html>';
html += '<head><link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"> <</head>'
html +='<body><h1>Hello World</h1></body></html>'
await page.goto("data:text/html," + html, { waitUntil: 'networkidle0' });
await page.pdf({path: 'hn.pdf', format: 'A4'});

await browser.close();

これで、即値のHTMLもPDF出力できるようになったはずです。もちろん、pdfメソッドをscreenshotメソッドに変更すればPNG出力も可能です。

まとめ

いかがでしたでしょうか。

HTMLを即値で出力するとき、gotoメソッドを使用するという技を使用しました。今のところこれしか解決方法がありません。
僕は、だいぶこれハマってしまいました。

似たようなことをしたいときに役立ててください。