HTMLをPOSTするとPDFに変換して返すDockerコンテナの作り方

先日公開したウェブサービスの制作過程で得た知見を、シリーズで発信していく記事の第4弾です。
個人開発でウェブサービスにトライしてみたいと考えている方の参考になりましたら嬉しいです。

公開したウェブサービスは、ウェブサイト制作業務を楽に進めるためのツールなのですが、そのツールには企画をまとめるためのメモ機能を搭載しています。

そのメモ機能は、マークダウン記法で記述できるようになっており、PlantUMLも使用できるよう拡張しています。このメモをPDFとして出力出来たら、そのまま企画書として提出できて便利だろうなと思いまして、HTMLをPDFに変換するDockerコンテナを作りました。

## wkhtmltopdfを利用してHTMLをPDFに変換する

HTMLをPDFに変換する方法については、OSSのwkhtmltopdfを利用することにしました。

HTMLソースをPOSTするだけでPDFが返却されるようにしたかったので、サーバとしての機能が必要です。今回はnode.jsとexpressを使ってお手軽にサーバを立てることにします。

Dockerfileはnode.jsイメージをベースとし、それにwkhtmltopdfと日本語フォントをインストールしたDockerイメージを作ることを目標にします。

## Dockerfile

```Docker
FROM node:12.16.2-stretch

ENV LANG C.UTF-8
ARG DEBIAN_FRONTEND=noninteractive

ENV HOST 0.0.0.0
EXPOSE 3000

RUN apt-get update -qq \
 && apt-get install -y \
      build-essential \
      xorg \
      libssl-dev \
      libxrender-dev \
      wget \
      unzip \
      gdebi \
 && apt-get autoremove \
 && apt-get clean

# wkhtmltopdf をインストールする。
RUN wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.stretch_amd64.deb \
     && apt-get install ./wkhtmltox_0.12.5-1.stretch_amd64.deb

# 日本語フォントとして Noto Fonts をインストールする。
RUN wget https://noto-website.storage.googleapis.com/pkgs/Noto-unhinted.zip \
 && unzip -d NotoSansJapanese Noto-unhinted.zip \
 && mkdir -p /usr/share/fonts/opentype \
 && mv -fv ./NotoSansJapanese /usr/share/fonts/opentype/NotoSansJapanese \
 && rm -rfv Noto-unhinted.zip \
 && fc-cache -fv

WORKDIR /usr/src

COPY ./package*.json ./

RUN npm install --no-optional

COPY . .

CMD ["node", "index.js"]
```

ポイントはstretchベースのnodeイメージを使用する点です。
そこに、stretchに対応したwkhtmltopdfと必要なライブラリをインストールしていきます。

ただし、それだけではPDFファイルの日本語が文字化けしますので、Notoフォントをインストールしてフォントキャッシュに反映させておきます。


## index.js

まずはnpmでexpressとwkhtmltopdfパッケージをインストールします。

```
npm i express wkhtmltopdf
```

当初、wkhtmltopdfの呼び出しは、child_processを通してコマンドラインで行おうとしたのですが、うまくいきませんでした。コンテナに入ってコマンドラインから呼び出すとPDFに変換できるのですが、node.jsのchild_processから呼び出すと、wkhtmltopdfがうまく機能しないようです。（Githubにも同様のやりとりが見つかりました。）

node.jsをやめてNginx + PHPのコンテナに変更しようかと迷っていたところ、「wkhtmltopdf」というnpmパッケージを発見し、そちらを試したところ無事に変換することができました。

以下のようなコードでOKです。
※最小限のエラー処理しか書いていませんので、用途に合わせて適宜追加してください。

```javascript:index.js
const express = require('express')
const wkhtmltopdf = require('wkhtmltopdf')

const app = express()
app.use(express.json())
app.listen(3000)

app.post('/', (req, res, next) => {
  try {
    const html = req.body.html
    wkhtmltopdf(html).pipe(res)
  } catch (err) {
    next(err)
  }
})

app.use((err, _req, res, next) => {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500).send('エラーが発生しました')
})
```
もしダウンロードするファイル名を指定したいときは、resのヘッダーにファイル名を追加すればOKです。
試してないですが、多分以下の感じでいけるのでは。（ファイル名に日本語を使うと、ブラウザによって挙動が異なることがあるので、できれば避けたいところ）

```
const fileName = 'hogehoge.pdf'
res.set('Content-Disposition', `attachment; filename=${fileName}`)
```

各ファイルを以下のように配置して、Dockerfileをビルドします。

> html2pdf/
　├ Dockerfile
　├ index.js
　├ package.json
　└ package-lock.json


コンテナを立ち上げれば、目的のサーバの完成です。（Docker最高！）

今回の例では、3000番ポートで待ち受けますので、HTMLソースを3000番ポートに向けてPOSTすれば、PDFファイルが返却されます。

```javascript:
const html = '< htmlソース >'
axios.post('http://localhost:3000', { html })
```

これまでは、PDF出力が必要な場合、PHPとTCPDFを使うことが多かったのですが、こちらの方が格段に楽で良いですね。HTMLとcssならば、凝ったレイアウトも比較的かんたんに実現できますし、このコンテナを起動しておけば、いろいろなところから呼び出して使いまわせて便利です。

## 最後に、宣伝です。。。
この記事の冒頭で触れたウェブサービスでも、これに近い方法でコンテナを走らせています。

WEBサイト制作者さん向けのサービスでして、お気軽にお試しいただけると幸いです。（無料で使えます）

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ウェブサイト制作に携わる人達に使っていただきたいウェブサービスをリリースしました。<br><br>ウェブサイトを作る際、大量の情報やデータを管理する必要がありますが、それをスッキリ整理整頓できて、簡単にチームで共有できます。<a href="https://t.co/kFOCip3eUD">https://t.co/kFOCip3eUD</a></p>&mdash; 橋本技研@売れるネットショップ研究所 WEB技術×販促 (@hashimotosubaru) <a href="https://twitter.com/hashimotosubaru/status/1246611675379191808?ref_src=twsrc%5Etfw">April 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>