Knowledge(Webページ)をpuppeteerでPDF化

2022 年 5 月 16 日 by yasukuni

社内における技術共有(メモ)に「Knowledge」を使用しています。こちらに溜まった記事をPDFとしたく、試行錯誤した結果を記録します。

#1 検討

まずは(ブラウザで)表示されているものをPDFにしようと思い、Chromeの起動引数で「–print-to-pdf」指定を試してみました。

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --disable-gpu --no-sandbox --print-to-pdf=C:\PathToSave\knowledge.pdf http://kb.sys.local/open.knowledge/view/1

これが成功すれば、記事のIDの数分だけループすれば、PDF化される算段です。
がしかし・・・

左:PDF/右:ブラウザで表示

とでもチープな感じで微妙です。

・ブラウザで表示したものと異なるレイアウト
・色が無い/シンタックスハイライトが無い

#2 puppeteer

レイアウトが異なる問題ですが上部「ストック」などのボタン配置はスマホ用の表示に見えます。(knowledgeが)レスポンシブ対応している?

そこで印刷の用紙サイズを変更したいのですが、先程のコマンドでの起動では細かな設定変更ができないらしいです。そこで代わりに、nodejs の puppeteer という chromeをコマンドで操るツールを使うことにしました。

npm install puppeteerでライブラリ放り込み、以下のコードを実行します。
※「await page.pdf({~});」という部分で、ChromeのGUI印刷設定と同じものをコードで指定が可能。

const puppeteer = require('puppeteer');
(async() => {
    let browser;
    try {
        browser = await puppeteer.launch({
            headless: true,
            args: [
                '--no-sandbox',
                '--disable-setuid-sandbox',
                '--disable-gpu',
                '--hide-scrollbars',
                '--disable-web-security',
            ]
        });
        const page = await browser.newPage();
        await page.goto('http://kb.sys.local/open.knowledge/view/1');
        await page.pdf({
            path: './knowledge.pdf',
            displayHeaderFooter: false,
            printBackground: true,
            landscape: true,
            height: 500 + 'mm',
            width: 1000 + 'mm',
            margin: {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0,
            },
        });
        await browser.close();
    } catch (err) {
        if (browser) {
            await browser.close();
        }
        throw err;
    }
})();

下のキャプチャがPDF化の結果です。用紙サイズ変更によりレイアウトに関してはマシになりましたが、依然として味気ない感じです。

puppeteer で用紙サイズを指定

#3 印刷調整

できるだけブラウザでの表示に近づける為、以下の通り色々と試行錯誤しました。

▶ Chromeのウィンドウサイズ/ViewPort指定

var height = 4000;
var width = 1600;
browser = await puppeteer.launch({
    headless: true,
    args: [
        '--window-size=' + width + ',' + height
    ],
    defaultViewport: {
        width: width,
        height: height
    }
});

▶ 画像の遅延読み込み対処

画像の遅延ロードによりPDF上の画像が一部表示されない問題が発生。
この設定が施された画像は、画面をスクロールして可視領域に入ったタイミングで読み込みが開始されます。

Bootstrap Lazy loading – examples & tutorial (mdbootstrap.com)

このため、縦に長いページだと一部画像が表示領域外となり、初期設定されているローディング用のインジケータがPDF化(下図)されてしまいました。

読み込み中を示すインジゲータがPDF化されてしまう
DOM上では/images/loader.gifが設定されている⇒スクロールして可視領域に入ったら本来の画像に差し替わる動作

対応として

  1. ページロード後に一度、画面の末端までスクロールする
    ⇒ 画像の遅延ロードのイベントを強制的に発火させます。
  2. 全アバター画像のsrc属性がloader.gifから変更され かつ 画像のロードが完了するのを待機
    ⇒setIntervalを100ms 設定で、上記監視を行います。
  3. 全て読み終えたらPDF化する処理を開始

という処理を追加しました。

await page.goto('http://kb.sys.local/open.knowledge/view/' + i, { "waitUntil": "networkidle0" });

await page.evaluate(async() => {

    window.document.body.scrollIntoView(false);

    await Promise.all(Array.from($('div.question_image img'), image => {
        return new Promise((resolve, reject) => {
            let h = setInterval(() => {
                if (image.src != "http://kb.sys.local/images/loader.gif" && image.complete) {
                    clearInterval(h);
                    resolve();
                }
            }, 100);
        });
    }));
});

▶ スタイルタグ追加

・PDF(レポート)上、不要な要素を削除(操作ボタン類、フッタなどをdisplay:none)
・色が当たらない問題の対応のため。色を当てたいスタイルへ!important付きで再定義する。
(https://qiita.com/yuuuking/items/7c0f2d2b64cadf12b789)

印刷用CSSで設定でも良いのですが、knowledge側に手を加えたくなかったので、スタイルの変更は
スタイルヘッダを動的に追加して行いました。

await page.evaluate(async() => {
  const style = document.createElement('style');
  style.type = 'text/css';
  const content = `
    #footer {display:none;}
    /*任意のスタイルを設定*/
  `;
  style.appendChild(document.createTextNode(content));
  const promise = new Promise((resolve, reject) => {
      style.onload = resolve;
      style.onerror = reject;
  });
  document.head.appendChild(style);
  await promise;
});

▶印刷設定

改ページの回避の為、印刷用紙サイズをドキュメントの高さに合わせます。
px→mm変換の係数は適当な計算式で算出しました (1インチ = 25.4mm。25.4mm / 96dpi = 0.264583…)
※+20mmは余白の関係か、無駄な空白ページが追加されるケースがあり、面倒だったのでマジックナンバーで調整。

var actualHeight = await page.evaluate('document.body.scrollHeight');

await page.pdf({
    height: (width * 0.26458333) + 'mm',
    width: (actualHeight * 0.26458333 + 20) + 'mm', 
    margin: {
        top: 0,
        right: 0,
        bottom: 0,
        left: 0,
    },
});

#4 最終結果

色味が増えだいぶブラウザの見た目に寄ることができました。

左: puppeteerで用紙サイズのみ変更/右:印刷調整後(スタイル編集)

#5 まとめ

今回思ったようなPDFにしたく、色々と調整を行ってみましたが
DOM操作, スタイル操作, イベントの操作を行なうことで、情報を欲しい形で得ることができることが分かりました。

既存のシステムのWEB帳票作成、クラウドシステムのダッシュボードの定期的にレポート作成など
割りとコストをかけずとも簡易的にレポートを作成することができそうです。


以上、ご参考になれば幸いです。

TrackBack