7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js + ReactでのPDF生成ライブラリ比較と実装方法

Posted at

こんにちは、花王株式会社の @TsuchiyaK です。

Next.js + Reactで構築したWebアプリケーションに画面をPDF出力する機能を実装する際にPDF生成ライブラリを比較検討した結果と実装手順を紹介します。

背景

今回実装したのは、Webアプリケーションの画面をPDF出力する機能です。画面内にはMUIのUIコンポーネントreact-chartjs-2で作成したチャートも含まれており、それらも含めて正確にPDF化する必要がありました。

PDF生成ライブラリの調査

まずは、どのようなライブラリがあるか情報収集を行いました。以下のサイトが非常に参考になりました。

PDF生成ライブラリnpmパッケージ比較

このサイトでは、主要なPDF生成ライブラリの特徴やダウンロード数、人気度などが比較されています。

断念した方法: @react-pdf/renderer

情報収集の結果、Reactとの親和性が高いという観点から、まず@react-pdf/rendererを試してみましたが、結論から言うと今回この方法は断念しました。

以下に@react-pdf/rendererの特徴と断念した理由を説明します。

@react-pdf/rendererの特徴

  • Reactコンポーネントを使用してPDFを作成できる
  • Web画面を作るような感覚でPDFを作成できる
  • ドキュメント作成に必要なコンポーネントが一通り揃っている

上記のような点から、ReactによるWebアプリにおけるPDF生成ライブラリとしては有力な候補になるのではないかと思います。

import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: { padding: 30 },
  section: { margin: 10, padding: 10 }
});

const MyDocument = () => (
  <Document>
    <Page size="A4" style={styles.page}>
      <View style={styles.section}>
        <Text>Hello React PDF!</Text>
      </View>
    </Page>
  </Document>
);

課題と断念理由

今回はPDF化するWeb画面内にMUIのUIコンポーネントやreact-chartjs-2で作成したチャートが含まれており、これらのコンポーネントが正しくPDFにレンダリングされないケースがありました。

例えば以下のようにPDFViewerの中にMUIやreact-chartjs-2のコンポーネントを含めるとエラーが出てしまいます。

'use client';

import { Typography } from '@mui/material';
import { Document, PDFViewer, Page, View } from '@react-pdf/renderer';

import {
  BarElement,
  CategoryScale,
  Chart as ChartJs,
  Legend,
  LinearScale,
  Title,
  Tooltip,
} from 'chart.js';
import { Chart } from 'react-chartjs-2';

ChartJs.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
);

export default function MyDocument() {
  const data = {
    labels: ['Jan', 'Feb'],
    datasets: [{ label: 'Example', data: [10, 20] }],
  };

  return (
    <PDFViewer>
      <Document>
        <Page>
          <View>
            <Typography variant="h6">Hello React PDF!</Typography>
          </View>
          <View>
            <Chart type="bar" data={data} />
          </View>
        </Page>
      </Document>
    </PDFViewer>
  );
}

error.png

error2.png

上記のような理由から@react-pdf/rendererの使用を断念しました。

採用した方法: html2canvas + jsPDF

今回は以下のアプローチを採用しました:

  1. html2canvasでWeb画面を画像化
  2. jsPDFで画像をPDF化

採用理由

  • 見た目をそのまま再現できる: 画面を画像化するため、チャートを含めてWeb画面の見た目をそのままPDFに反映できる
  • シンプルな実装: jsPDFは基本的なPDF生成に適しており、画像をPDFに埋め込むだけのシンプルな処理に最適
  • react-chartjs-2との相性: Canvas要素で描画されたチャートも問題なく画像化できる

実装方法

以下に実装した方法を記載します。
なお、最終的にPDFをA4サイズの用紙に印刷することを想定しています。

ライブラリのバージョン

{
  "chart.js": "^4.4.8",
  "html2canvas": "^1.4.1",
  "jspdf": "^3.0.1",
  "next": "15.3.0",
  "react": "^19.0.0",
  "react-chartjs-2": "^5.3.0"
}

PDF出力用のカスタムフック

import html2canvas from 'html2canvas';
import jsPdf from 'jspdf';

// 高解像度PDF生成用のスケール係数
const HIGH_RESOLUTION_SCALE = 3; // 288dpi相当

// JPEG画像の品質設定
const JPEG_QUALITY = 0.8;

/**
 * HTML要素をPDFファイルとしてエクスポートするカスタムフック
 */
export const useExportToPdf = () => {
  const exportToPdf = async (elementId: string, fileName: string) => {
    const element = document.getElementById(elementId);
    if (!element) {
      console.error(`Element with id "${elementId}" not found.`);
      return;
    }

    try {
      // HTML要素をcanvasに変換して画像として取得
      const canvas = await html2canvas(element, {
        scale: HIGH_RESOLUTION_SCALE,
        useCORS: true,
        allowTaint: false,
        backgroundColor: '#ffffff',
        width: element.offsetWidth,
        height: element.offsetHeight,
      });

      const imgData = canvas.toDataURL('image/jpeg', JPEG_QUALITY);

      // PDFを作成して画像を追加
      const pdf = new jsPdf('p', 'mm', 'a4');
      const pdfWidth = 210; // A4 width in mm
      const pdfHeight = 297; // A4 height in mm

      // キャンバスの実際のサイズを取得
      const canvasWidth = canvas.width / HIGH_RESOLUTION_SCALE;
      const canvasHeight = canvas.height / HIGH_RESOLUTION_SCALE;

      // A4サイズに収まるように画像サイズを計算
      const imgWidth = pdfWidth;
      const imgHeight = (canvasHeight * pdfWidth) / canvasWidth;

      // A4サイズを超える場合は縮小
      if (imgHeight > pdfHeight) {
        const ratio = pdfHeight / imgHeight;
        const finalWidth = imgWidth * ratio;
        const finalHeight = pdfHeight;
        pdf.addImage(imgData, 'JPG', 0, 0, finalWidth, finalHeight);
      } else {
        pdf.addImage(imgData, 'JPG', 0, 0, imgWidth, imgHeight);
      }

      // PDFを保存
      pdf.save(fileName);
    } catch (error) {
      console.error('Failed to export PDF:', error);
    }
  };

  return { exportToPdf };
};

コンポーネントでの使用例

'use client';

import { useExportToPdf } from '@/hooks/use-export-to-pdf';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';

export default function ReportPage() {
  const { exportToPdf } = useExportToPdf();
  const pdfElementId = 'pdf-content';
  const pdfFileName = 'report.pdf';

  // A4サイズに合わせたスタイル設定
  const pdfStyle = {
    width: '595px',   // A4の幅 (約72dpiポイント換算。96dpi環境では調整推奨)
    height: '842px',  // A4の高さ (同上)
    backgroundColor: '#FFFFFF',
  };

  return (
    <Box
      sx={{
        flexDirection: 'column',
        display: 'flex',
        my: 2,
        overflowY: 'auto',
      }}
    >
      <Button
        variant="contained"
        sx={{
          width: '90%',
          alignSelf: 'center',
          mb: 2,
        }}
        onClick={() => exportToPdf(pdfElementId, pdfFileName)}
      >
        Export PDF
      </Button>

      {/* PDF化する領域 */}
      <div id={pdfElementId} style={{ ...pdfStyle }}>
        {/* PDFコンテンツ */}
        <YourReportContent />
      </div>
    </Box>
  );
}

実装のポイント

1. 高解像度化

scale: HIGH_RESOLUTION_SCALE

scaleオプションを3に設定することで、ブラウザ標準想定の96dpi × 3 ≒ 288dpiの高解像度PDFを生成できます。文字やチャートが鮮明になりますが、値を大きくしすぎると処理時間が長くなります。

2. CORS対応

useCORS: true,
allowTaint: false

外部画像を含む場合でも正しくレンダリングできるよう、CORS対応を有効にしています。

3. A4サイズへの最適化

const pdfStyle = {
  width: '595px',   // A4の幅 (約72dpiポイント換算。必要に応じて余白/縮尺調整)
  height: '842px',  // A4の高さ (同上)
  backgroundColor: '#FFFFFF',
};

PDF化する要素のサイズをA4サイズ(72dpi換算)に設定することで、印刷時のレイアウト崩れを防ぎます。

4. アスペクト比の維持

if (imgHeight > pdfHeight) {
  const ratio = pdfHeight / imgHeight;
  const finalWidth = imgWidth * ratio;
  const finalHeight = pdfHeight;
  pdf.addImage(imgData, 'JPG', 0, 0, finalWidth, finalHeight);
}

コンテンツがA4サイズを超える場合でも、アスペクト比を維持しながら縮小して収めています。

制約と注意点

1. テキストのコピーができない

画像として埋め込むため、生成したPDF上でテキストをコピーすることができません。テキスト選択が必要な場合は、別の方法(例: PDFライブラリで直接テキストを描画)を検討する必要があります。

2. 複雑なレイアウトの再現性

html2canvasは非常に優秀なライブラリですが、以下のような場合は完全に再現できない可能性があります:

  • 複雑なCSSアニメーション
  • Shadow DOMを使用したコンポーネント
  • 特殊なフォント

今回のケースでは、MUIコンポーネントやreact-chartjs-2のチャートを含めて特に問題なくPDF化できました。実装時には実際の環境で動作確認することを推奨します。

まとめ

Next.js + ReactでのPDF生成ライブラリの選択基準として以下のように考慮すると良いでしょう:

ライブラリ 適している場合 不向きな場合
@react-pdf/renderer ・React経験者
・シンプルなドキュメント
・コンポーネントベースで構築したい
・既存のWeb画面をそのまま出力したい
・Canvasベースのコンテンツを含む
html2canvas + jsPDF ・既存のWeb画面をそのまま出力したい
・チャートや複雑なビジュアルを含む
・印刷用途
・PDFのテキストを選択/コピーしたい
・ファイルサイズを小さくしたい

今回の要件では html2canvas + jsPDF を採用しました。

同じような要件に直面している方の参考になれば幸いです!

参考リンク

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?