【React】html2canvas と jsPDF を使ってWebページのデザインを複数ページのPDFファイルにする

# はじめに
この記事では、html2canvas と jsPDF を使ってWebページのデザインを複数ページのPDFファイルにする方法を記載します。

https://html2canvas.hertzen.com/

https://artskydj.github.io/jsPDF/docs/jsPDF.html

以下の記事では、html2canvas と jsPDF を使ってWebページのデザインを1ページのPDFファイルにする方法を記載しました。

https://qiita.com/Yasushi-Mo/items/dc65a76351c063dbe884

ただ、PDF化する対象のWebページのサイズによってはA4サイズに収まらないことがあります。特に、長いリストや表を含む場合には、1ページに収まらず、想定しない見た目になる可能性があります。

この問題を解決するために、PDFを複数ページに分割する手法を実装します。

# 開発環境
開発環境は以下の通りです。

- html2canvas 1.4.1
- jspdf 2.5.2
- Windows11
- React 18.3.1
- TypeScript 5.7.3
- Node.js 22.13.1
- npm 11.0.0

# 実装方針
PDF1ページ分の高さごとにWebページ画像を描画します。

- `pdfHeigh`: PDF1ページ分の高さ
- `position`: 描画開始位置


| ページ1                |
|-----------------------|
|  長いコンテンツの一部     |
|  (position = 0)         |

+---------------------------+
|  ページ2                 |
|  -----------------------  |
|  続きのコンテンツ        |
|  (position = pdfHeight) |


+---------------------------+
|  ページx                 |
|  -----------------------  |
|  続きのコンテンツ        |
|  (position = (x-1) * pdfHeight) |


PDFのページ追加は jsPDF の [addPage()](https://artskydj.github.io/jsPDF/docs/jsPDF.html#addPage) を利用します。

これを描画開始位置がWebページ画像の高さになるまで繰り返します。

# コード
コードにすると以下のようになります。

```PdfExportWithPageSplitting.tsx
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
import { useRef } from "react";

export function PdfExportWithPageSplitting() {
  /** PDF化する対象の要素を参照するための useRef */
  const contentRef = useRef<HTMLDivElement>(null);

  /** PDFダウンロード処理 */
  const handleDownloadPdf = async () => {
    if (!contentRef.current) return;

    try {
      // 1️. 指定した要素をキャプチャしてCanvasに変換
      const canvas = await html2canvas(contentRef.current);

      // 2️. Canvasを画像として取得（Base64のPNGデータ）
      const imageData = canvas.toDataURL("image/png");

      // 3️. jsPDF インスタンスを作成（A4縦向き）
      const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });

      // 4️. PDFの幅と高さを取得
      const pdfWidth = pdf.internal.pageSize.getWidth();
      const pdfHeight = pdf.internal.pageSize.getHeight();

      // 5️. 画像の幅と高さを取得
      const imageWidth = canvas.width;
      const imageHeight = canvas.height;

      // 6️. 画像をPDFにフィットさせるための比率を計算
      const ratio = pdfWidth / imageWidth;
      // 7. 画像全体をPDFに合わせた場合の高さを計算
      const scaledHeight = imageHeight * ratio;
      /** 現在の描画開始位置（スケール後の画像上でのY座標） */
      let position = 0;

      // 8. 画像全体が描画されるまでループ
      // 条件：position（描画開始位置）がscaledHeight（スケール後の画像全体の高さ）より小さい間
      while (position < scaledHeight) {
        // 9. 画像を追加
        pdf.addImage({
          imageData: imageData,
          format: "PNG",
          x: 0,
          // 前ページで描画した分を上方向へずらし、新しいページに適切な部分が表示されるように調整
          y: -position,
          width: pdfWidth,
          height: scaledHeight,
        });
        // 10. 1ページ分の高さを加算（ページごとに pdfHeight 分だけ増やす）
        position += pdfHeight;
        // 11. まだ描画していない画像部分が残っている場合は新しいページを追加
        if (position < scaledHeight) {
          pdf.addPage();
        }
      }
      // 12. PDFを保存
      pdf.save("document.pdf");
    } catch (error) {
      console.error("PDF生成中にエラーが発生しました:", error);
    }
  };

  return (
    <div>
      {/* PDFに変換する対象のエリア */}
      <div
        ref={contentRef}
        style={{ padding: 24, backgroundColor: "blue", width: 500 }}
      >
        <h2>PDF化するコンテンツ</h2>
        <p>この部分がPDFとして出力されます。</p>
        {[...Array(100)].map((_, i) => (
          <p key={i}>
            さらに長いコンテンツを追加して、ページ分割が発生することを確認します。
          </p>
        ))}
      </div>

      {/* PDFダウンロードボタン */}
      <button onClick={handleDownloadPdf} style={{ backgroundColor: "blue" }}>
        PDFをダウンロード
      </button>
    </div>
  );
}
```

# 動作確認
動作確認をします。「PDFをダウンロード」ボタンをクリックすると、実装時に指定した範囲のWebページが複数ページのPDFファイルとして出力されます。

![Screen-Recording-2025-02-14-083631.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/4380c072-48c4-4949-bf6e-7ca3e3a68441.gif)


:::note warn
PDF化対象のWebページが増えるほど、PDFのダウンロードには時間がかかります。また、PDFダウンロー自体ができないこともあります。
ちなみに今回確認した環境（Windows11、Core i7、16GB）では、PDF50ページ分まではダウンロードできました。

![Screen-Recording-2025-02-14-084553.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/1fd931f1-43f5-4186-a6c5-97c2c144c824.gif)

ただ、それ以上のサイズだとエラーになり、PDFダウンロードができませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/934818df-c256-435b-b194-8cd84dd02704.png)

:::

# 参考
- [html2canvas](https://html2canvas.hertzen.com/)
- [jsPDF](https://artskydj.github.io/jsPDF/docs/jsPDF.html)