ReactでPDF帳票作ってみた (react-pdf vs. Puppeteer)

みなさんこんにちは帳票弱いマンです。

フロントエンドライブラリはやっぱりReactが好きです。帳票を作る時もReactを使いたかったので、色々やった結果を書いてみます。

## 出力方式

2つの方式を検討・利用しました。

### [React-PDF](https://react-pdf.org/)

- https://react-pdf.org/  （※私が利用したバージョンは ~~4.x~~ → 1.6 表示する方のreact-pdfと間違えてました）
- 専用のReact要素 ＋ react-pdf用レンダラーを用いることによってPDFのBlobが出る方式。

```typescript
  <Document>
    <Page size="A4" style={styles.page}>
      <View style={styles.section}>
        <Text>Section #1</Text>
      </View>
      <View style={styles.section}>
        <Text>Section #2</Text>
      </View>
    </Page>
  </Document>
```

- flexレイアウト的な要素の柔軟な配置が可能。
- 複数ページにまたがる印刷も可能。
- 日本語もワードラップの問題などありつつも、対策すればちゃんと使える。
- クライアントサイド（ブラウザ）でも動作可。
    - ただし、PDF中で使うTrueTypeフォントについては、クライアントサイドでDLしないといけないので、通信的にそこそこ重い。
- もちろんNode.js上で動作可。
    - **ただ、巨大な表をレンダリングすると、結構重い。**
        - ちょっとしょぼいサーバでレンダリングすると10秒ぐらいかかったりする…
            - 完全にCPUがボトルネックになっているので、純粋にサーバをスケールアップすれば解決するといえば解決する
        - これがネックで私は検討から外しました。
        - あとメモリリークも観測しました。これについては、[定期的にプロセスを再起動させればいい](https://qiita.com/knjname/items/2e97b9753530819c4f80)ので、そこまで問題だとは思いません。

### [Puppeteer](https://github.com/puppeteer/puppeteer)

- https://github.com/puppeteer/puppeteer
- Node.jsによるChromeオートメーションライブラリ
    - Node.jsからChromeを立ち上げてIPC(プロセス間通信)で操作するだけなので、別にJSでChromeが動いているわけじゃない。
    - Seleniumみたいなものと思えばOK
- これ自体は別にPDFを出すソフトウェアではないが、下記のようにして、Reactを用いてPDFを出力することができる。

```typescript
const html = renderToString(<MyReactApp />);
await page.setContent(html);
const blob = await page.pdf();
```

- **以下の流れでPuppeteerにPDFを出力させる想定。**
  1. Reactで帳票となるHTMLをレンダリングする
  2. そのHTMLをPuppeteerで立ち上げたブラウザのページに送りつけて表示させる
  3. ブラウザのページにPDFを出力させる
- **レンダリングに用いられたフォントはちゃんとPDFに組み込まれる。**
    - これ大事！
- 基本的にHTML + CSSで出来る装飾を用いることができる。
    - SVGも利用可能。最悪SVG使えばなんでもレンダリング可能。
    - PDFに出さなくてもHTMLである程度出力をチェック可能。
    - ReactDOM用のコードを帳票に流用可能。
        - 個人的にはこれが結構デカかった。
- Node.js 上のみで動く。
    - **クライアントサイドで動作不可。**
        - あまりPDFをクライアントで出したいというケースがないのでそんなに困ることはないかと。
    - ヘッドレス（画面なし）で動作可能。
    - Dockerでも動く。Fargateなどのミニマルな環境でも動作可。

## 結局、PuppeteerからPDFを出力する方式を採用

当初はReact-PDFで帳票を作っていたんですが、下記の理由でPuppeteerを選びました。

- 複雑な帳票はHTMLで作ったほうが早い
- HTMLとレンダリングコードを共用できる
- レンダリングのトラブルシューティングはDevToolsを使って解決することができる
- Puppeteerの方がパフォーマンスがいい

考えてみれば、Google Chromeという人類最強最大のレンダリングソフトウェアを使わない理由はないですね。


## Puppeteerで帳票出力する際のあれこれ

### 基本的には Print CSS の世界

PDFの内容については(Google Chromeが対応している)Print CSS仕様を用いることができます。

ここらへんについては下記のエントリが詳しいので、一読したほうがいいです。

[そろそろ真面目に、HTMLで帳票を描く話をしようか](https://qiita.com/cognitom/items/d39d5f19054c8c8fd592)

### 用紙のサイズが混在できない

たとえば用紙サイズはPuppeteerでPDF出力時に直接指定できます。

```typescript
await page.pdf({ format: "A4" });
```

ただ、これだとページによって用紙サイズが違う場合困りますね。

それなら、Print CSSを使えば仕様上は用紙サイズを複数指定できそうだと思ったのですが、私が確認した限りだとページごとに違う用紙サイズというのはサポートされてないように見えました。

なので結局は、**用紙サイズごとに分割してPDFを出して、後から結合すればOK**です。 

JS上でPDF結合するなら、 [pdf-lib](https://github.com/Hopding/pdf-lib) がおすすめです。

### フォントが小さくならない

**Chromeには実は最小フォントサイズの設定があります。CSS上でいくら小さな文字サイズを指定しようが、ブラウザの設定以上には縮まりません。**

Puppeteerでも同様なので、8ptぐらいがフォントサイズ最小になっていると思います。

私は `zoom: 50%` がついている `<div>` の中に帳票をレンダリングすることで回避しました。これだと小さい文字は出ます。

（ブラウザのプロファイルをいじれば、こういう小細工しなくてもいける気がする）

### 線が1px以上細くならない

あきらめましょう。

（ちなみにSVGでレンダリングされた線はきちんと細くなります。）

### Docker化したい

Puppeteerは普通にDocker化できます。Fargateみたいな環境でもきちんと動きます。

ただ、その際フォントをDockerイメージに入れておかないとフォントが効かないので、インストールしましょう。 ([`~/.fonts`にフォント入れて、`fc-cache`でフォントキャッシュ更新すればOK](https://www.google.com/get/noto/help/install/))

### 最新バージョンのPuppeteerのPDFレンダリングが遅い

今（2020/09/18）のところ新しいバージョン(`5.x`)のPuppeteerはPDFの出力が何故かすごく遅いです。 [issue にもあがっています。](https://github.com/puppeteer/puppeteer/issues/4649)

私が試したところ、 `1.11.0` など古いバージョンだとPDFレンダリングが遅い問題には当たりませんでした。バージョンを下げてみることをおすすめします。

### PuppeteerのHTMLレンダリングを高速にしたい

PDF出力抜きのHTMLレンダリングまでの工程(ブラウザ立ち上げ → ページ作成 → HTML読み込み → レンダリング完了) でも時間を結構浪費するので、ブラウザ立ち上げ・ページ作成まで、実際のPDFレンダリングの事前に完了させておくと速いです。

一度帳票レンダリングに使ったページオブジェクトはキャッシュさせておいてもいいと思います。

その上で、ページへのHTML読み込みについては、[`.innerHTML = ...` が一番速かった](https://github.com/knjname/2020-07-23_PuppeteerFastPDFGenerationTest) です。

```typescript
await page.evaluate((html) => (document.body.innerHTML = html), html);
```

（もちろんこの辺の高速化Trickはセキュリティに配慮して行いましょう。）

## まとめ

今のところPuppeteerで帳票出力という自由度の高い方式に満足しています。