2026年4月22日

ブラウザだけでPDFを生成する:履歴書ジェネレーター実装で詰まった3つの問題

ブラウザでのPDF生成実装:選択と失敗の記録

utily.net の履歴書ジェネレーターは、すべての処理をブラウザ内で完結させています。氏名・住所・職歴などの個人情報がサーバーに送られることは一切ありません。

これを実現するための技術選定と実装で、思いのほか苦労しました。「ブラウザでPDFを生成する」というのは、やったことがない人から見ると簡単そうに聞こえますが、実際には複数の落とし穴があります。この記事では、試した方法・失敗した理由・最終的に採用した解決策を記録します。

この記事の対象読者:ブラウザだけで動くPDF生成を実装したいエンジニア、または utily.net の履歴書ツールがどう作られているか気になる方。

なぜサーバーレスのPDF生成にこだわったか

履歴書には、個人情報がすべて詰まっています。氏名、生年月日、住所、学歴、職歴、写真。これらのデータを外部サーバーに送信することへの抵抗感は、バックエンドエンジニアとして日々データを扱う立場から来ています。

「SSL通信だから安全」「信頼できるサービスだから大丈夫」という前提は理解しつつも、「そもそも送らなければいい」という設計を選びたかった。ブラウザ内だけで完結すれば、通信経路のリスクはゼロです。

ただ、これは同時に「PDF生成の複雑さをすべてクライアント側に持ち込む」ということでもあります。サーバーで puppeteer や wkhtmltopdf を動かす選択肢が消え、ブラウザだけで高品質なPDFを作る必要がありました。

試した方法と、それぞれがダメだった理由

1. window.print() でブラウザの印刷機能を使う

❌ 却下:レイアウトが制御できない

最初に検討したのが、CSSの @media printwindow.print() を組み合わせる方法です。実装コストが最も低く、「ブラウザが持っている印刷機能をそのまま使う」という発想は悪くありませんでした。

しかし問題が山積みでした。ブラウザごとに余白の扱いが微妙に異なる。ページヘッダー・フッター(URL やページ番号)を消すのにユーザー側の設定変更が必要。A3 と A4 の切り替えがユーザーの印刷ダイアログ操作に依存する。何より、「これはアプリが制御するものではなく、ユーザーが制御するもの」という根本的な問題があります。

2. html2canvas + jsPDF

❌ 却下:テキストが選択できない PDF になる

次に試したのが html2canvas と jsPDF を組み合わせる定番の手法。html2canvas でHTMLをキャンバスに描画し、そのキャンバス画像を jsPDF に貼り付けてPDFにします。

動くことは動きます。ただ、生成されるPDFはHTMLをスクリーンショットした画像の貼り付けなので、テキストが選択できません。採用担当者がメールアドレスをコピーしようとしても、画像なのでできない。履歴書として根本的に使いにくい。

また、日本語の細かいフォントレンダリングが画像化の過程で劣化することもありました。印刷したときに文字がぼやける場合があります。

3. jsPDF 単体(テキストAPI使用)

❌ 却下:日本語フォントの問題が解決困難

jsPDF には画像を使わずにテキストをPDFに直接描画するAPIがあります。こちらを使えばテキスト選択可能なPDFになります。しかし、日本語の対応が鬼門でした。

jsPDF のデフォルトフォントは欧文フォントのみで、日本語を直接渡すと文字化けか、最悪何も表示されません。日本語フォントを追加する方法があることはわかりましたが、設定が煩雑で、フォントファイルの扱いも複雑でした。また、複雑なレイアウト(写真の配置、複数カラム、細かいポジショニング)をjsPDFのAPIで実現しようとすると、かなりの低レベルな座標計算が必要になり、Reactのコンポーネントモデルと噛み合いませんでした。

採用した解決策:@react-pdf/renderer

✅ 採用:React コンポーネントでPDFレイアウトを記述できる

最終的に採用したのが @react-pdf/renderer です。このライブラリは、React のコンポーネントとして PDF のレイアウトを記述できます。JSXで書いたコンポーネントが、そのままPDFのページレイアウトになります。

採用の決め手になったのは以下の点です。

方法 テキスト選択 日本語 レイアウト制御 Reactとの相性
window.print() △(ブラウザ依存)
html2canvas + jsPDF ✕(画像)
jsPDF 単体 ✕(要対応) △(座標計算)
@react-pdf/renderer ○(フォント設定後) ○(JSX)

壁1:日本語フォントが表示されない

@react-pdf/renderer を使い始めて最初にハマったのが、日本語フォントの問題です。

初めてビルドして確認したとき、入力した日本語テキストがすべて「□□□□□」という豆腐になっていました。英字は表示されるのに、日本語だけが文字化けする。原因はすぐわかりました。PDF ライブラリが内蔵しているデフォルトフォントは欧文フォントのみで、日本語グリフが含まれていないためです。

解決策は、日本語フォントファイルをライブラリに渡して登録することです。Google Fonts の Noto Sans JP を使うことにしました。Noto(No Tofu:豆腐なし)という名前が皮肉ではありますが、Google が日本語を含む多言語に対応したフォントとして公開していて、ライセンスもクリアです。

フォントファイルサイズの問題:

日本語フォントは、欧文フォントと比べてファイルサイズが桁違いに大きい。Noto Sans JP の Regular ウェイトだけで 数MB になります。これをビルドに含めると、JavaScriptバンドルが一気に肥大化します。

対策として、PDF ダウンロード機能を使うときだけフォントを動的に読み込む(遅延ロード)ようにしました。履歴書の編集中にフォントをロードする必要はなく、「PDFをダウンロード」ボタンを押したときに初めてフォントファイルを取得し、PDF を生成する流れです。

壁2:A3横とA4縦の両対応

日本の履歴書フォーマットには A3 横(A4 2枚分を横長に並べたもの)と A4 縦の2種類があります。両方に対応する必要がありました。

ページサイズが変わると、レイアウトのほぼすべての数値が変わります。フォントサイズ、各フィールドの幅、写真のサイズ、余白の量。A3 横は縦に長いコンテンツを横2カラムに配置する必要があり、A4 縦は縦1カラムにすべてを収める必要があります。

これを「ひとつのコンポーネント」で処理しようとすると、条件分岐だらけになります。最終的に、A3横用のコンポーネントとA4縦用のコンポーネントを別々に作り、選択されたレイアウトに応じて切り替える方式を採りました。コードの重複は増えますが、それぞれのレイアウトを独立して調整できる利点があります。

ポイント:@react-pdf/renderer はピクセルではなく pt(ポイント)単位でサイズを指定します。A4 縦は 595pt × 842pt、A3 横は 1190pt × 842pt が PDF 標準の寸法です。印刷時のサイズ精度がピクセル指定より高く、これがPDF向けライブラリを使う利点のひとつです。

壁3:ライブラリ自体のバンドルサイズ

@react-pdf/renderer 自体も、そこそこのサイズがあります。PDF のレンダリングエンジンをブラウザで動かすわけですから、ある程度は仕方ありません。しかし、初回ロードでこのライブラリをすべてダウンロードするのは、ユーザー体験として良くない。

対策として、React の lazy()Suspense を使った動的インポートでコードスプリッティングを行いました。PDF 関連のコードは別チャンクとして切り出され、ユーザーが「PDFダウンロード」ボタンを押したときに初めてロードされます。

これにより、初回ページロード時は PDF エンジンのコードが読み込まれず、ページの表示速度に影響を与えません。PDF 機能を使うユーザーだけが、必要なタイミングで追加のコードをダウンロードする仕組みです。

実装して気づいた @react-pdf/renderer の注意点

通常のHTMLと書き方が違う

@react-pdf/renderer は、通常の HTML 要素(div、p、span)ではなく、ライブラリ独自の要素(View、Text、Page など)を使います。CSS も通常の CSS ではなく、StyleSheet.create() で作ったスタイルオブジェクトを props として渡します。Flexbox は使えますが、Grid は使えない制約もあります。

最初は「Reactで書けるんだ」と軽く考えていましたが、実際には「PDF向けの独自DSL」という感覚で覚え直す必要がありました。慣れると書きやすいですが、最初の数時間は戸惑いました。

リアルタイムプレビューはブラウザに任せた

「入力内容のリアルタイムプレビュー」機能は、@react-pdf/renderer ではなく通常の HTML/CSS で実装しています。PDF の生成は重い処理なので、入力のたびにPDFを生成してプレビューするのはパフォーマンス上の問題があります。

そのため、編集画面のプレビューは CSS でレイアウトした HTML、最終的な PDF 出力だけ @react-pdf/renderer を使うという二段構えにしました。両者のレイアウトに微妙なズレが出ることもありますが、許容範囲に収めています。

振り返って:ブラウザでのPDF生成は「できる」が「楽ではない」

サーバーを使わずにPDFを生成することは技術的に可能ですが、手軽ではありません。日本語対応、バンドルサイズ、レイアウトの精度、それぞれに対処が必要です。

もし今から同じことをやるなら、という観点での学びを整理します。

実際に使ってみる

ブラウザだけで動く履歴書ジェネレーターを無料で使えます。データはサーバーに送られません。

履歴書を作成する

よくある質問

Q. Google Drive への保存はサーバーを経由しますか?

Google Drive への保存機能は、Google の OAuth 認証を使ってブラウザから直接 Drive API を呼び出す仕組みです。utily.net のサーバーを経由しません。認証トークンもブラウザ内にのみ保持されます。

Q. PDF生成に時間がかかるのはなぜですか?

PDF生成時に日本語フォントファイルを読み込む遅延ローディングの仕組みを採用しています。初回は数秒かかることがありますが、2回目以降はブラウザキャッシュが効くため高速です。

Q. 印刷したときと PDF で見たときにズレが出ることがありますか?

PDF 内のレイアウトは印刷向けに固定されているため、印刷ダイアログから「PDFとして保存」しても問題なく使えます。ただし、プレビュー(HTML)と実際のPDF出力の間には微妙なズレが出る場合があります。ダウンロードしたPDFファイルの内容をご確認ください。

Q. 将来的にサーバー側でも処理するようになりますか?

今のところ予定はありません。「個人情報をサーバーに送らない」というのは utily.net の設計方針の核心です。この方針は今後も変えるつもりはありません。

まとめ

ブラウザだけでPDFを生成する実装は、「できる」と「楽にできる」の間に大きな溝があります。特に日本語を含む高品質なレイアウトが必要な場合、いくつかの壁を乗り越える必要があります。

ただ、乗り越えた先には「ユーザーの個人情報をサーバーに送らなくていい」という設計上の大きなメリットがあります。プライバシーを重視するユーザーに対して、それが伝わるといいと思っています。

raiyu
Webエンジニア / 個人開発者

都内を拠点に活動するWebエンジニア。リードエンジニア・バックエンドデベロッパーとしてシステムの設計から実装まで幅広く携わる。「長く愛され、使い心地の良いプロダクト」を信条に utily.net を個人開発・運営。