【React】コンポーネントをそのまま表も含めてPDFに変換する

こんにちは、フリーランスエンジニアの太田雅昭です。

jsPDF

jsPDFは、クライアントサイドでPDFを作成できるライブラリです。

GitHub – parallax/jsPDF: Client-side JavaScript PDF generation for everyone.

Client-side JavaScript PDF generation for everyone. – parallax/jsPDF

htmlからも作成できるため、表などが簡単に作れます。

参考サイト

以下のサイトを参考にさせていただきました。

jsPDFで日本語対応したPDFを作成する方法(テーブルもあり) – Qiita

やりたいこと・ReactプロジェクトでDOM描画したものをそのままPDF化したい・画像PDFではなく、テキストのPDFにしたい参考にしたサイトhttps://blog.capilano-fw…

手順

フォントをセットアップする

以下より、フォントをjsに変換します。今回はNoto Sans JPを使用しました。

No Title

No Description

変換したjsを下記のようにインポートします。

import "./NotoSansJP-Regular-normal";

これでフォントセットアップ完了です。

コンポーネントを作る

再利用を考慮して、子要素全てをPDFにする形としました。

import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { Box, Button } from "@mui/material";
import { jsPDF } from "jspdf";
import { useRef } from "react";
import "./NotoSansJP-Regular-normal";

const FONT = "NotoSansJP-Regular";

type Props = {
    children: React.ReactNode;
}

export function PdfContainer(props: Props) {
    const { children } = props;
    const targetRef = useRef<HTMLDivElement>(null);
    const pdfRef = useRef(new jsPDF());

    function getFileName() {
        const timestamp = new Date().getTime();
        return `download_${timestamp}.pdf`;
    };

    function savePdf() {
        if (!targetRef.current) return;
        pdfRef.current.html(targetRef.current, {
            callback(doc) {
                const fileName = getFileName();
                doc.setFont(FONT, "normal");
                doc.setFontSize(12);
                doc.save(fileName);
            },
            x: 15,
            y: 15,
            width: 170,
            windowWidth: 775,
        });
    };

    return <Box sx={{ border: "1px solid #aaa", padding: 2 }}>
        <Button variant="contained" onClick={savePdf}>
            <PictureAsPdfIcon fontSize='small' />
            PDFを保存
        </Button>
        <Box sx={{ '& *': { fontFamily: FONT } }} ref={targetRef}>
            {children}
        </Box>
    </Box>
};

試してみる

以下のコードで試してみます。ChatGPT出力をそのまま使っているため、重複が半端ないです。実際にはMuiのstyledなどを使ってスリムにできます。

export function PdfContainerSample() {
    return <PdfContainer>
        <h1>PDF Container Sample</h1>
        <p>これはPDFコンテナのサンプルです。</p>
        <table style={{ borderCollapse: 'collapse', width: '100%' }}>
            <thead>
                <tr>
                    <th style={{ border: '1px solid black', padding: '8px' }}>項目</th>
                    <th style={{ border: '1px solid black', padding: '8px' }}>説明</th>
                    <th style={{ border: '1px solid black', padding: '8px' }}>値</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td style={{ border: '1px solid black', padding: '8px' }}>サンプル1</td>
                    <td style={{ border: '1px solid black', padding: '8px' }}>これはサンプル1の説明です。</td>
                    <td style={{ border: '1px solid black', padding: '8px' }}>100</td>
                </tr>
                <tr>
                    <td style={{ border: '1px solid black', padding: '8px' }}>サンプル2</td>
                    <td style={{ border: '1px solid black', padding: '8px' }}>これはサンプル2の説明です。</td>
                    <td style={{ border: '1px solid black', padding: '8px' }}>200</td>
                </tr>
                <tr>
                    <td style={{ border: '1px solid black', padding: '8px' }}>サンプル3</td>
                    <td style={{ border: '1px solid black', padding: '8px' }}>これはサンプル3の説明です。</td>
                    <td style={{ border: '1px solid black', padding: '8px' }}>300</td>
                </tr>
            </tbody>
        </table>
    </PdfContainer>
}

結果以下のようになりました。

ブラウザ表示

PDF表示

ちょっとずれてますね。本番では細かな修正が必要そうです。