Web ページを開いて DOM を確認すると、こう思うかもしれません。
「HTML を取って PDF に変換すればいいだけだろう」
一見するとシンプルです。たった一行のコードで済みそうです。
document.documentElement.outerHTML
しかし現実は違います。
スタイルは崩れ、レイアウトは壊れ、JavaScript は動かず、PDF は元のページとは似ても似つかないものになります。
問題はコードではありません。ブラウザの HTML の本質に対する誤解です。HTML は単なる「文書」ではなく、レンダリングプロセスの結果なのです。
この記事では、.NET で HTML を PDF に変換する際に開発者が陥りがちな問題と、実際に有効なアプローチについて解説します。
なぜブラウザ外では HTML が壊れるのか
HTML→PDF 変換が壊れる理由を理解するには、まずブラウザが実際に何をしているのかを理解する必要があります。
ブラウザは単に HTML を読むだけではありません。リソース取得、スクリプト実行、スタイル適用、描画までをリアルタイムで行う複雑なレンダリングパイプラインを持っています。
HTML を取り出すということは、その一部しか取得していないということです。
ここから問題が始まります。
問題1:HTML は自己完結していない
ブラウザから取得した HTML は、単体では完全ではありません。CSS は外部ファイルに分離され、フォントは Google Fonts や CDN から読み込まれ、画像は絶対パスや相対パスで参照されています。
その HTML を .NET のコンバーターに渡すと、構造は認識されますが、見た目は一切再現されません。結果として得られるのは、1997 年のウェブページのような PDF です。
問題2:JavaScript はすでに実行済み
ここが多くの開発者が見落とすポイントです。
document.documentElement.outerHTML で取得できるのは「元の HTML」ではありません。
JavaScript 実行後の DOM の状態です。
つまり、.NET アプリケーションが受け取るのは JavaScript の出力であり、入力ではありません。そして、コンバーターがその HTML を異なる環境で再レンダリングしようとしても、JavaScript は二度と実行されません。動的コンテンツ、遅延読み込みされる要素、クライアントサイドレンダリング――そのすべてが音もなく消え去ります。
問題3:モダンフレームワークでさらに悪化する
React / Vue / Angular などの SPA ではこの問題がさらに深刻になります。
典型的な CSR(クライアントサイドレンダリング)の構成では、元の HTML ファイルはほぼ空であり、<div id="root"> があるだけです。画面に表示されるすべてのものは、実行時に JavaScript によって注入されたものです。outerHTML で取得するのは完全にレンダリングされた DOM ですが、それは壊れやすいスナップショットです。JavaScript のコンテキストを取り除けば、その大部分は PDF レンダラーにとって意味をなさなくなります。
SSR(サーバーサイドレンダリング)は役立ちますが、問題を完全に解決するわけではありません。スタイルとインタラクティブ性は、依然としてクライアントに結びついていることが多いのです。
問題4:環境そのものが違う
根本原因はここです。
ブラウザには、レイアウト計算、フォントのサブセット化、CSS カスケード解決、GPU コンポジットといった完全なレンダリングエンジンが搭載されています。あなたの .NET サーバーにはそのどれもありません。HTML を一方の環境から他方へ移すとき、あなたは根本的なアーキテクチャの境界を越えているのです。
これが、「HTML を変換するだけ」のアプローチが常にもう少しで動きそうに感じられるのに、決して完全には動かない理由です。
開発者が試す典型的な(しかし誤った)方法
HTML→PDF 変換がうまくいかないと、多くの開発者は修正を試みます。
しかし多くは別の問題を生むだけです。
🚫 方法1:HTML をそのまま渡す
最も単純な方法です。HTML を取得し、変換ライブラリに渡して、うまくいくことを願います。
結果は予想通りです。外部スタイルシートは読み込まれず、フォントはデフォルトにフォールバックし、JavaScript でレンダリングされたコンテンツは失われ、PDF は元のページとは似ても似つかないものになります。
「動く時もあるが、ほぼ壊れる」という典型例です。
🚫 方法2:CSS が同じように動作すると仮定する
次の手段は通常、スタイルを修正することです。開発者は CSS をインライン化し、見た目を改善し、問題は解決したと仮定します。
解決していません。
PDF レンダリングはブラウザと同じ CSS ルールに従いません。静かに壊れるもののいくつか:
-
@media printは明示的に処理されない限り完全に無視される -
flexboxとgridのサポートは変換ライブラリによって大きく異なる -
page-break-beforeとpage-break-afterはサポートされていないか、予期しない動作をする
画面上ではページは正常に見えますが、PDF は別の話です。
🚫 方法3:ピクセルベースのレイアウトをハードコードする
画面はピクセルを使用します。印刷は物理単位(pt、mm、cm)を使用します。これらは交換可能ではありません。
ピクセルで構築されたレイアウトは、1920 pxのモニターでは妥当に見えるかもしれませんが、A4 の PDF ではコンテンツがオーバーフローしたり、切り取られたり、ページの半分が空白になったりします。画像も同様です。画面上で鮮明に見える 72dpi の画像は、300dpi で印刷するとぼやけて見えます。
これは、本番環境でしか表面化しないバグの一つであり、通常は重要な締め切りの直前に発覚します。
この時点で、ほとんどの開発者は問題が単一のものではなく、パターンであることに気づきます。
そして、より重い解決策に手を伸ばし始めます。
🚫 方法4:ヘッドレスブラウザ依存
ある時点で、ほとんどの開発者は「正しい」解決策を発見します。Puppeteer や Playwright などのヘッドレスブラウザを使用して、ブラウザとまったく同じようにページをレンダリングし、PDF にエクスポートするのです。
これは機能しますが、実際のコストが伴います。
- Chromium はデプロイメントフットプリントに約 300MBを追加する
- サーバーレス環境ではコールドスタート時間が大幅に増加する
- 同時リクエスト時にメモリ使用量が急増する
- メンテナンスのオーバーヘッドが時間とともに増大する
時折の PDF 生成であれば管理可能ですが、アプリケーション内のすべての PDF に対するデフォルトのアプローチとしては、それは負債となります。
🚫 方法5:PDF 生成でスレッドをブロックする
これはレンダリングの問題ではなく、アーキテクチャの問題です。
PDF 生成は低速です。HTMLの解析、リソースの解決、ページのレイアウト――すべてに時間がかかります。これをメインスレッドで同期的に呼び出す開発者は、最終的に壁にぶつかります。1 つの遅い PDF が他のすべてをブロックするのです。
トラフィックの少ないアプリでは、これは見えません。実際の負荷がかかる本番環境では、サーバーをダウンさせる最初の要因となります。
本番環境で実際に起きる問題
ここからは開発ではなく「本番で起きる問題」です。
フォントが消える
ローカルマシンで PDF をレンダリングすると、正常に見えます。あなたのマシンにはフォントがインストールされています。サーバーにはありません。
次に何が起こるかはライブラリ次第です。警告なしに代替フォントに置き換えるものもあれば、文字があるべき場所に空白をレンダリングするものもあります。いずれにせよ、PDF は壊れたまま出荷され、誰かがすべての出力を手動でレビューしていない限り、気づかれません。
修正は複雑ではありませんが、意図的なアクションが必要です。フォントは参照するのではなく、PDF に埋め込む必要があります。サーバー環境でシステムフォントに依存することは、発生を待つ静かな失敗です。
CJK(中国語・日本語・韓国語)が崩れる
中国語、日本語、韓国語、その他の非ラテン文字は、さらに複雑さを加えます。正しいフォントがインストールされていても、エンコーディングの不一致により、文字が正しくレンダリングされなかったり、まったくレンダリングされなかったりする可能性があります。
これは、テストデータが ASCII のみを使用している場合に見逃しやすく、実際のユーザーが母国語でコンテンツを送信した最初の時点で発覚します。
ヘッダー・フッターが機能しない
よくあるパターン:CSS でヘッダーとフッターに position: fixed を使用し、PDF のすべてのページで繰り返されると仮定する。そうはなりません。
position: fixed はブラウザの概念であり、ビューポートに対して要素を固定します。PDF ページにはビューポートがありません。要素は一度だけ表示されるか、間違ったページに表示されるか、完全に消えます。
PDF で真に繰り返されるヘッダーとフッターには、ライブラリレベルのサポートが必要であり、CSS で偽装することはできません。
ページ番号が難しい
「Page X of Y」は些細なことに思えます。実際には、レンダラーがコンテンツのレイアウトを開始する前に総ページ数を知っている必要があり、これは 2 回のレンダリングパス、またはそれを明示的にサポートするライブラリを意味します。
ほとんどの開発者は、これが自動的に処理されると仮定しています。そうではありません。変換ライブラリによる明示的なサポートがなければ、動的なページ番号は単に機能しません。
ユーザー入力のセキュリティ問題
PDF にユーザー生成コンテンツ(名前、住所、コメント、フォームフィールドなど)が含まれており、そのコンテンツを生の HTML としてレンダリングしている場合、問題があります。
ユーザーは <script> タグ、悪意のあるスタイル、または一部のレンダリング環境ではデータを流出させるコンテンツを注入できます。これは、レンダリング中に JavaScript が実際に実行されるヘッドレスブラウザを内部で使用するライブラリでは特に危険です。
ここでは入力のサニタイズはオプションではありません。それは PDF ジェネレーターとセキュリティ脆弱性の違いです。
実際に機能するもの(シナリオに応じて)
重要なのは「HTML をどう扱うか」です。
方法1:入力 HTML を正規化する
どのライブラリに手を伸ばす前に、最も影響力のあることは、コンバーターに渡す HTML をクリーンアップすることです。
つまり:
-
すべてのスタイルをインライン化する。 外部スタイルシートはサーバー環境では確実に読み込まれません。すべてを
<style>タグまたはインラインのstyle属性に移動します。 -
印刷単位に切り替える。 レイアウトに影響するものは
pxをpt、mm、cmに置き換えます。これだけで多くのレイアウトバグが修正されます。 -
@media printルールを追加する。 ナビゲーション、サイドバー、インタラクティブな要素など、PDF に存在すべきでないものを非表示にします。 -
page-break-beforeとpage-break-afterを意図的に使用する。 レンダラーにページ区切りを任せてはいけません。
これですべてが解決するわけではありませんが、変換が始まる前に、壊れた出力の最も一般的な原因を排除します。
方法2:適切なライブラリを使う
HTML がクリーンになったら、残りを確実に処理できる変換ライブラリが必要です。
.NET エコシステムの主な選択肢は、それぞれ異なるトレードオフがあります。
- QuestPDF — コードファースト、HTML 入力なし、プログラムで定義されたレイアウトに最適
- IronPDF — 内部でヘッドレス Chromium を使用、高い忠実度だが重い
- Spire.PDF for .NET — ブラウザ依存なしで HTML から PDF への変換を処理し、フォント埋め込み、CJKテキスト、ヘッダー、フッター、ページ番号付けを標準でサポート
正しい選択は制約によって異なります。この記事で説明した正確な問題(フォント、レイアウトの不整合、ページネーション)に対処している場合、Spire.PDF はブラウザ依存を導入することなく、これらをライブラリレベルで処理します。
方法3:HTML を「捕まえる」のをやめる
これが最も重要な考え方の転換です。
この記事の問題のほとんどは、同じ根本原因から生じています。Web ページをドキュメントとして扱うことです。Web ページはブラウザで表示され、スクロールされ、リサイズされ、インタラクションされるように設計されています。PDF は固定され、ページ分割された、印刷可能な成果物です。これらは根本的に異なるものです。
Web ページを PDF としてキャプチャしようとするとき、あなたは一方のフォーマットを他方のように振る舞わせようと強制しています。データから直接 PDF を生成する場合――ライブラリのネイティブ API を使用してレイアウト、タイポグラフィ、構造を定義する場合――変換の問題を完全にスキップします。
これは初期投資が高くなりますが、スケールし、エッジケースを優雅に処理し、実際に信頼できる出力を生成するアプローチでもあります。
重要なポイント
一つ覚えておくべきことがあるとすれば、それはこれです。ブラウザの HTML はポータブルなドキュメントフォーマットではないということです。
それはレンダリングプロセスの結果であり、単純に抽出して .NET アプリケーションで再利用できるものではありません。ブラウザを離れると、その HTML を機能させていた環境は失われます。
だからこそ、適切なアプローチを選ぶことが重要なのです。
- ピクセルパーフェクトな出力が必要なら、レンダリングエンジンを使用する
- HTML を制御しているなら、HTML-to-PDF ライブラリを使用する
- 長期的な信頼性を重視するなら、ドキュメントを直接生成する
しかし最大の転換は技術的なものではなく、概念的なものです。
「このページをどうやって PDF に変換するか?」と問うのをやめ、「どのようなドキュメントを生成しようとしているのか?」と問い始めることです。
なぜなら、その転換を一度行えば、ブラウザと戦うのをやめ、意図的にドキュメントを構築し始めるからです。そして、Spire.PDF for .NET のようなツールが最も理にかなうのはそこです。回避策としてではなく、最初から正しい解決策として。
まとめ
ほとんどの HTML-to-PDF の失敗は同じ根本原因を共有しています。Web ページをドキュメントとして扱うことです。しかし、Web ページは動的でブラウザ依存の体験です。PDF は固定されたポータブルな成果物です。それらは交換可能ではありません。
ブラウザはドキュメント工場ではなく、生きた環境です。あなたが抽出するのは凍結された瞬間であり、それを機能させていた機構を剥ぎ取られたものです。
成功する開発者は、PDF 生成を後付けとして扱うのをやめます。彼らは意図的な選択をします。ページをキャプチャするのではなく、ドキュメントを生成するということです。その転換が、壊れやすい HTML スナップショットとの戦いから、意図的で信頼性のある出力の構築へとあなたを導きます。
ウェブはブラウジングのためのものです。PDF は印刷のためのものです。
この二つを混同するのはやめましょう。








