HTMLからPDFへの変換は、理屈の上では簡単そうに見えます。洗練されたテンプレートを作成し、データを流し込めば、ウェブページとピクセル単位で一致するドキュメントが出来上がる……そう期待するものです。しかし現実には、クラッシュ、グリフの欠落、表示の崩れとの、日々続く戦いになりがちです。最近のプロジェクトでは、3つの問題が繰り返し発生しました。iTextが特定のSVGグラフィックスに当たると完全にクラッシュし、絵文字は空白の白い四角形へと消え去り、微妙な透明の背景は不透明な黒いブロックへと変わってしまいました。それぞれの失敗には明確な原因があり、これらすべてを解決するには、PDFエンジンにコンテンツが渡される前の、アプリケーション側での準備方法を再考する必要がありました。

SVGがパイプラインを破壊するとき

iTextには利便性のために内部SVGレンダラーが搭載されていますが、その統合には重大な弱点が隠されています。SVGに複雑なパス、重いCSSスタイリング、あるいは特定の座標変換が含まれている場合、組み込みのパーサーは整った例外を投げて処理を継続するのではなく、「爆発」します。これらは、警告なしにPDF生成スレッドを停止させる完全なシステムクラッシュであり、手元に残るのは不完全なファイルと、ベクトルパーサーの深い場所を指し示すスタックトレースだけです。

確実な解決策は、iTextにSVGのレンダリングを一切求めないことです。代わりに、その作業をスタンドアロンモードで動作するApache Batikに移行させます。Batikは、同じような脆さを見せることなく、複雑なパスやCSSルールを処理できます。また、切り離して運用することで、PDFエンジンをグラフィックス関連の不安定さから保護できます。ワークフローは単純です。ドキュメントの組み立てを開始する前に、SVGをBatikに通してPNGデータURLを生成します。生のベクトルマークアップではなく、そのラスタ形式の画像をiTextに渡します。スタンドアロンのBatikは、大きなライブラリの中にバンドルされ固定された組み込みレンダラーよりも、SVG仕様に忠実です。また、分離されているため、不正なグラフィックスによってドキュメント変換全体が停止することはありません。

チャートがプロフェッショナルに見えるか、バグレポートのように見えるかは、一つの小さな詳細によって決まります。SVGは、座標系とスケーリングの挙動を定義するために viewBox 属性に依存しています。変換コードが viewBox を無視すると、完全に正しいチャートであっても、読めないほど小さくなったり、歪んで引き伸ばされたりすることがあります。この属性を明示的に解析し、その寸法を出力サイズにマッピングしてください。このステップを飛ばすと、レンダリングの品質とは無関係で、座標宣言の欠落に起因するレイアウト問題のデバッグに何時間も費やすことになります。

見えないインクの問題

絵文字があるべき場所に空白の四角形が表示されるのは、単純な理由によります。現在のフォントがその「言語」を話せない(サポートしていない)のです。Helveticaなどの標準的なPDFフォントは、絵文字が広く普及する前に作られました。絵文字のUnicode範囲に対応するグリフが含まれていないため、iTextがそれらのコードポイントに遭遇すると、何も描画せずに次に進んでしまいます。その結果、ソーシャルセンチメントレポートやユーザーフィードバックのエクスポートが壊れているように見える、空のボックスだらけのドキュメントが出来上がります。

クライアントのOSがその欠落を埋めてくれることを期待することはできません。PDFは独自のフォントリソースを保持しており、ブラウザで正しく見えていても、ファイルがシステムのフォントから切り離されると意味をなしません。解決策は、明示的なフォントルーティングレイヤーを構築することです。絵文字のUnicodeブロックをカバーするモノクロシンボルを提供するSymbolaのような、絵文字対応の専用フォントを登録します。白黒のハートや警告シンボルは、光沢のあるカラーグリフセットのような洗練さには欠けるかもしれませんが、意味は伝わります。空の長方形は、失敗を伝えてしまいます。フルカラーの絵文字フォントは、PDFビューア内で一貫してレンダリングするのが依然として難しく、カラーサポートを追い求めると、解決するよりも多くの互換性の問題を引き起こすことがよくあります。

iTextは、改行処理を通じて、さらに厄介な第2の問題を引き起こします。ライブラリが絵文字のサロゲートペアを誤った境界で分割し、単一の文字を2つの無効な半分に引き裂いてしまうことがあるのです。それが起こると、テキストストリームが破損し、本来1つのグリフがあるべき場所に、読み取り不可能な断片が残ることになります。これを防ぐには、サロゲートペアを認識し、それらを原子的な単位として扱うカスタムの ISplitCharacter を実装してください。これにより、レイアウトエンジンが絵文字の途中で改行を挿入するのを防ぎ、テキストの整合性を維持できます。

透明度が黒に変わるとき

柔らかいrgbaの背景や、レイヤー化されたfill-opacityエフェクトを持つSVGは、ブラウザでは洗練されて見えます。同じマークアップをiTextに渡すと、透明度が塗りつぶされた黒い長方形に崩壊してしまうことが頻繁にあります。エンジンがCSSのカラー関数やopacity属性を誤って処理し、不透明度を全密度のインクに置き換えてしまうのです。

コンバーターに到達する前にSVGをプリプロセスすることが、唯一の確実な防御策です。アルファブレンディングに依存する要素はすべて削除するか、置き換えてください。rgba() の値を不透明な rgb() カラーに変換します。もし不透明度の概念を維持する必要がある場合は、CSSのショートハンドから値を切り離し、標準の opacity 属性に移動させてください。ただし、透明性を完全に削除するのが最も安全な方法です。これらの変更はウェブデザインとしては後退しているように感じられますが、PDFは現代的なCSSの透明性よりも古い、異なるイメージングモデルを使用しています。PDF形式は具体的なカラー値を想定しており、曖昧な値を渡すとトラブルを招くことになります。

マークアップのサニタイズを行う際は、すべてのSVGに適切な xmlns 名前空間の宣言が含まれていることを再確認してください。生成されたHTMLやテンプレートエンジンは、ミニファイやDOMシリアライズの過程で名前空間属性を削除してしまうことがよくあります。その名前空間がないと、SVGパーサーが要素を誤認したり、エラーを出さずに処理を失敗したりして、パーサーエラーが発生するか、ページに表示されない不正なベクトルデータが生成される可能性があります。これは数秒で終わる基本的なチェックですが、それによって数時間の作業を節約できます。

1つのテンプレート、2つの世界

最悪の長期的解決策は、ブラウザ用とPDF用に別々のHTMLテンプレートを維持することです。ラベルがずれたり、余白が変わったりして、すぐにエクスポートされたレポートがダッシュボードと一致しなくなります。よりクリーンなアーキテクチャは、単一のテンプレートに依存し、context.isForPdf() のような単一のフラグでレンダリングロジックを分岐させるものです。

そのフラグが false の場合、テンプレートは完全なブラウザ体験を提供します。無限ズームのためのネイティブSVG、モダンなCSS、そしてブラウザがサポートするあらゆるカラーアセットを提供します。フラグが true の場合、同一のテンプレートがSVGアセットを事前にレンダリングされたPNGに置き換え、絵文字に対応したフォントスタックを有効にし、サポートされていない透明効果をすべて削除します。テキストと構造は変更されず、アセットパイプラインとスタイリングルールだけがターゲットとなる媒体に適応します。

このデュアルパス(二系統)のアプローチにより、コードベースの整合性が保たれます。コンテンツは1箇所で更新するだけで、ルーティングレイヤーが画面と紙の間の機械的な違いを処理します。また、テストもより簡単になります。ブラウザのデベロッパーツールを使用してテンプレートロジックを検証した後、PDFフラグを有効にして、同じデータがコンバーターをクラッシュさせることなく、きれいなドキュメントを生成することを確認できます。

PDF生成に関する厳しい現実

PDFがブラウザのように振る舞うことは決してありません。レンダリングモデルは根本的に異なり、iTextのようなライブラリは、速度、ファイルサイズ、仕様への準拠の間で意図的なトレードオフを行っています。成功は、エンジンと戦って最善を祈ることからは生まれません。境界を早期に受け入れ、その境界に合わせてパイプラインを設計することから生まれます。

PDFの段階に到達する前に、ベクトルを変換してください。すべてのグリフにフォールバックが用意されるよう、フォントを明示的にルーティングしてください。透明性を排除して単色に戻してください。テンプレートがどちらの世界に向けてレンダリングしているのかを判断するために必要なコンテキストを与えてください。これを一貫して行えば、ドキュメントがレンダラーと戦うことはなくなり、意図した通りの見た目になります。