📚

ReactでPDFを作る

2024/10/29に公開

はじめに

ハッカソンで開発している際にPDFダウンロードする機能を作る話になり、Next.jsで作ることにしました
ただ時間が厳しかったため、かなりAI頼りになってしまいました

必要な要件

  • ボタンを押したらPDFとしてダウンロードできる
  • 中身はバックエンドにfetchしたものを入れる
    • 全ての要素が表示の順序関係なく返される(json)
  • できているデザインを実装する

使ったライブラリ

  • html2canvas
  • jsPDF

最初調べた時はreact-pdfを使おうとしましたが、直接PDFを作っていくものなので独自の記法なこともありスタイリングなどもうまくできなかったので、断念しました

そこで、DOM → PNG → PDFというようにそれぞれ変換するためのライブラリを使って実装しました!

実装

基本的な作りはこの記事を参考にしました!
https://zenn.dev/k_kazukiiiiii/articles/8938bb1bd33985

グリッドレイアウトや順序バラバラに返されたデータをどうやるかなどは、Claudeに頼りました

ライブラリのインストール

npm install html2canvas jspdf

PDFにするコンポーネントを実装

実際にPDFにしたい中身をコンポーネントとして実装します!
16:9のスライドみたいな感じをイメージしています

PdfPartsというカード要素を縦2横4のグリッドレイアウトにしています

export const PDFContent = () => {
    return (
        <div
            id="pdf-id"
            className="w-[1200px] aspect-video py-16 px-8"
        >
            <h1>タイトル</h1>
            <ul className="divide-[#4f4f4f] divide-y-4 flex-1 content-center">
                <div className="col-span-4 row-span-1 grid grid-cols-4 gap-x-4 pb-14">
                    {contents.slice(0, 4).map((item, i) => (
                    <PdfParts
                        key={i}
                        color={item.color}
                        title={item.title}
                        content={item.content}
                      />
                    ))}
                </div>
                <div className="col-span-4 row-span-1 grid grid-cols-4 gap-x-4 pt-14">
                    {contents.slice(4, 8).map((item, i) => (
                      <PdfParts
                        key={i}
                        color={item.color}
                        title={item.title}
                        content={item.content}
                      />
                    ))}
                </div>
            </ul>
        </div>
    );
};

ボタンを押した時の関数

ダウンロードボタンを押した時に、html2canvasで画像にして、jsPDFでpdfファイルに変換する機能を実装する
この時、PDFのサイズを上手く指定しないとDOMに合ってくれません

const pdhDownloadHandler = () => {
    // PDFファイルに変換したいコンポーネントのidを検索してDOM要素を取得する
    const target = document.getElementById("pdf-id");
    if (target === null) return;

    html2canvas(target, { scale: 2.5 }).then((canvas) => {
        const imgData = canvas.toDataURL("image/png", 1.0);

        // A4横向きのPDFサイズ (単位: mm)
        const slideWidth = 320; // スライドの幅
        const slideHeight = 180; // スライドの高さ

        // コンポーネント画像がA4ページに収まるようにスケーリング
        const pdf = new jsPDF({
            orientation: "landscape",
            unit: "mm",
            format: [slideWidth, slideHeight],
        });

        // A4ページ全体に収まるように、画像をページ全体にリサイズ
        pdf.addImage(imgData, "PNG", 0, 0, slideWidth, slideHeight);

        // PDFを保存
        pdf.save(`judge-result.pdf`);
    });
};

ダウンロードボタン

ページコンポーネントにボタン要素をおいて、さらに先ほど作ったPDFの中身もおきます
画面としては表示したくないのですが、document.getElementById("pdf-id")で取得するので含める必要があります

透明度を0にして、画面幅が1200px以下になった際にもレイアウトが崩れないように実装しました

export const Page = () => {
    return (
        <>
            <button>ダウンロード</button>
            <div className="absolute top-0 left-0 w-[1200px] opacity-0 pointer-events-none" >
                <PDFContent />
            </div>
        </>
    );
}

まとめ

今回は時間がない中、PDFダウンロード機能を作った話でした
なかなか難しかったです

時間があればreact-pdfで構築できた方が良さそうです

Discussion