PHPで出力されるWebページをサーバーサイドでPDFにしてダウンロードさせる


要件

  • とあるシステムの管理ダッシュボードでレポートをダウンロードしたい。
  • レポートは画面を印刷したものと同じでPDFにする。
  • 一覧のページに設置されたボタンクリックでその詳細ページをダウンロードする。

絵に書くとこんな感じ。

何をつかってやるか

ヘッドレスChromeを使う。ヘッドレスChromeはコマンドからChromeブラウザを操作することが出来るので、GUI操作をプログラムから実行することができる。

PDFを作成するには

google-chrome  --headless --no-sandbox --disable-gpu --print-to-pdf 'https://example.com/'

でよい。

いくつかの問題

  • 印刷対象のURLへのアクセス
  • 出力先
  • フォント

ヘッドレスChromeを使ってやるのは良いが実装する上でいくつかの問題が発生した。
印刷対象がログインしたあとのページのため、サーバーサイドから詳細ページにアクセスするにはログイン処理を通る必要があり、直接URLにアクセスできない。また出力先の指定方法がわからなかったりフォントがなかったりした。

1. 印刷対象へのアクセス

サーバーサイドのヘッドレスChromeから自身のページにHTTP経由でアクセスしようとしても、ログイン処理を通過していないため直接アクセスできない。

サーバーサイドから自分自身にHTTP経由でアクセスすることは難しいけど、出力予定のページのHTMLは作成出来る。ということで、ファイルにHTMLを出力してそれをヘッドレスChromeに読ませることで解決した。
HTMLをファイルに出力しているので以下のようにPDFにすることができる。

google-chrome  --headless --no-sandbox --disable-gpu --print-to-pdf 'file:///path/to/page.html'

2. 出力先の指定方法

先のコマンド例で実行した場合、PDFファイルはコマンドを実行したディレクトリに output.pdf という名前でPDFファイルが保存される。出力先を指定したくても google-chrome --help でヘルプがみれない。ということでソースコードを漁る。

// Default file name for pdf. Can be overriden by "--print-to-pdf" switch.
const char kDefaultPDFFileName[] = "output.pdf";

--print-to-pdf で上書き出来るということなので --print-to-pdf=origin-file-name.pdf で出力先を指定できることがわかる。

3. フォントのインストールが必要

HTMLファイルをPDF化しても日本語フォントがないと文字化けするのでインストールする必要がある。日本語フォントはIPAフォントからダウンロードできる。

どうやってやるか

1. ページの出力HTMLをサーバーに保存する

PHPでは出力制御関数を使って出力を操作できる。今回は、サーバーで作成したHTMLをクライアントに返さずにサーバーで取得したいのでこの関数を使う。

ob_start();
/*
ページを表示するための
いろいろな処理
 */
$html = ob_get_clean();

これで $html 変数に出力予定のHTMLが保持されるのであとはこれをサーバーに保存すればよい。
MVCモデルのフレームワークを使っている場合はこんな感じになる。

Class Awesome extends Base_Controller {
    public function detail($id) {
        // 詳細ページの処理
    }

    public function pdf($id) {
        // 詳細ページの内容をPDFにしてダウンロードさせたい
        ob_start();
        $this->detail($id);
        $html = ob_get_clean();

        $html_filepath = "/tmp/" .  md5($html) . ".html";
        file_put_contents($html_filepath, $html);
    }
}

2. 保存したHTMLをPDFにして保存

先の処理で保存したHTMLファイルをヘッドレスChromeを使ってPDF化する。ファイルへのアクセスは file:// プロトコルで指定すればヘッドレスChromeに読ませることが出来る。

google-chrome  --headless --no-sandbox --disable-gpu --print-to-pdf="/tmp/output.pdf" "file:///tmp/detail_page.html"

PHPのコードからは exec() 関数を使用すれば良い。

$html = ob_get_clean();

$html_filepath = "/tmp/" .  md5($html) . ".html";
$pdf_filepath = "/tmp/" . md5($html) . ".pdf";

file_put_contents($html_filepath, $html);

exec("google-chrome  --headless --no-sandbox --disable-gpu --print-to-pdf=$pdf_filepath file://$html_filepath");

3. IPAフォントをインストールする

apt-get install -y unzip
cd /usr/local/share/fonts/
curl 'https://ipafont.ipa.go.jp/IPAfont/IPAfont00303.zip' -o IPAfont00303.zip
unzip IPAfont00303.zip
fc-cache -fv
fc-list

4. ダウンロードさせるには

$pdf_filepath; // ダウンロードさせるPDFのファイルパス
$basename = basename($pdf_filepath);

header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . $basename .'"');//ファイルサイズを取得
header('Content-Length: '.filesize($pdf));
readfile($pdf);

おまけ

この環境をDockerで作るにはChromeのインストールやフォントのインストールが必要になる。Dockerfileはこんな感じになる。

FROM php:5.6-fpm


RUN cd /tmp
RUN apt install -y gnupg
RUN curl 'https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb' -o chrome.deb
RUN curl 'https://dl.google.com/linux/linux_signing_key.pub' -o chrome_key.pub
RUN apt-key add chrome_key.pub

# 依存関係でエラーになるのでそれをインストールする
RUN dpkg -i chrome.deb || apt-get install -f -y

RUN dpkg -i chrome.deb
RUN apt update
RUN apt-get install -y google-chrome-stable

# 日本語font install
RUN apt-get install -y unzip
RUN cd /usr/local/share/fonts/
RUN curl 'https://ipafont.ipa.go.jp/IPAfont/IPAfont00303.zip' -o IPAfont00303.zip
RUN unzip IPAfont00303.zip
RUN fc-cache -fv
RUN fc-list