【ITニュース解説】A PDF Exporter With No PDF Library
2026年09月09日に「Dev.to」が公開したITニュース「A PDF Exporter With No PDF Library」について初心者にもわかりやすく解説しています。
ITニュース概要
Firefox拡張機能がPDFライブラリなしでPDF出力機能を実装。ブラウザのJPEGエンコードやzlib圧縮を流用し、336行で実現した。既存機能を活用すれば、依存なしで開発できる。ただし、依存が減るとリスクは他の部分に移行するため、厳密なテストが重要だ。
ITニュース解説
システムエンジニアを目指す初心者の皆さん、ブラウザの拡張機能でスクリーンショットをPDFにエクスポートする際、PDF生成のための特別なライブラリを使わずにその機能を実現した事例を紹介する。通常は複雑なPDF生成処理が、たった336行のコードで実現できたという驚くべき話である。
なぜこのようなことが可能になったのか。それは、PDFライブラリが通常担う主要な二つの機能が、現代のブラウザにすでに組み込まれていたからだ。一つはJPEG画像のエンコード、もう一つはデータの圧縮である。この二つの機能がブラウザに最初から備わっていたため、開発者はそれらを活用するだけで、PDF生成の大部分をまかなうことができたのだ。
PDFファイルは画像を特定の形式で格納する。「/DCTDecode」と「/FlateDecode」という二つのフィルターがその鍵だ。「/DCTDecode」は、そのデータストリームがJPEG形式であることを意味する。つまり、JPEG画像をPDFに格納する際、ブラウザがすでに生成したJPEGデータをそのままファイルに書き込むだけでよい。画像を再度エンコードしたり、特別な画像コーデックを組み込んだりする必要がない。ブラウザの「canvas.toBlob」という機能を使えば、HTMLのキャンバス要素からJPEG形式の画像データを直接取得できるため、この要件を満たせる。
次に「/FlateDecode」は、データストリームがzlibでラップされたdeflate形式で圧縮されていることを示す。これもまた、ブラウザのネイティブ機能が役立つ。「CompressionStream('deflate')」というWeb APIを使うと、まさにこのzlibでラップされたdeflate形式のデータが得られる。特別なzlibライブラリをプロジェクトに含める必要もない。
これらの事実は、PDF形式が1993年に標準化された際、すでに広く使われていた画像形式や圧縮形式を基に設計されたことに起因する。そして数十年の時を経て、現代のブラウザが偶然にもそれらの同じ形式をネイティブにサポートするようになった。開発者が意図的に調整したわけではないが、この幸運な偶然のおかげで、PDFライブラリに頼らずとも、PDFエクスポート機能が実装できるようになったのだ。
ただし、この「依存なし」のアプローチには明確な限界がある。一般的なPDFライブラリは、高度なフォント処理、ベクターグラフィックス、暗号化、フォーム機能、PDFの段階的な更新など、多岐にわたる機能を提供する。このエクスポート機能は、単に画像をPDFページに配置し、後述する見えないテキストレイヤーを追加するに過ぎない。そのため、「依存なし」という言葉が意味を持つのは、「JPEGエンコーダやzlib実装」といった、本来PDFライブラリが提供する特定の機能がブラウザで代替できた場合に限られることを理解しておく必要がある。
このエクスポート機能の実装において、開発者が特に気をつけなければならなかった点が一つある。それは、PDFファイルの構造の根幹をなす「クロスリファレンステーブル」(XRefテーブル)の管理だ。PDFファイルの末尾には、ファイル内の各オブジェクトがどこから始まるかを示すバイトオフセットが記録されたこのテーブルが存在する。PDFビューアは、このテーブルを最初に読み込み、指定されたオフセットに直接ジャンプして目的のオブジェクトを探し出す。もし、このテーブル内のオフセットが一つでも間違っていたら、ファイル自体は完全に無傷で、データも全て存在しているにもかかわらず、ビューアからはそのデータを見つけられなくなり、PDFファイル全体が読み込めなくなってしまうのだ。
このような間違いは、PDFエクスポート中にエラーとして表面化しないのが厄介な点だ。ファイルは正常に生成されたように見え、ユーザーが保存後、後日開こうとして問題が発覚する可能性がある。この種の「静かなエラー」を防ぐためには、正確なバイトオフセットの計算が極めて重要だ。安易に文字列の長さを元にオフセットを推定するのではなく、実際にエンコードされたバイト列を計測して正確なオフセットを算出する必要がある。
そして、この繊細な部分を確実に検証するためには、徹底したテストが不可欠だ。単にクロスリファレンステーブルが存在するか、エントリ数が正しいかを確認するだけでは不十分だ。より重要なのは、テーブル内の各エントリが指し示すオフセットに実際に目的のオブジェクトが存在するかどうかを検証するテストである。このエクスポーターでは、21個の検証項目のうち、オフセット検証のテストが最も重要だった。
このテストを効率的に行うための設計判断も興味深い。PDF生成のコアロジックは、ブラウザのキャンバスAPIに直接依存しないように分離された。もしキャンバスAPIに直接アクセスしていたら、そのテストには実際のブラウザ環境が必要となり、テストの実行が複雑でコストの高いものになっていただろう。しかし、バイト列の組み立てに特化した部分を分離したことで、Node.jsのような環境でも、ファイルのバイトレベルでの正確性を検証するテストが可能になった。この設計は、当初はコードの整理のために行われたものだが、結果的に「ファイルがサイレントに破損する可能性のあるコンポーネント」を、最も安価に検証できる環境に移動させるというメリットを生んだ。
他にも、使いやすさを考慮した細かい工夫がある。例えば、非常に長いスクリーンショットをPDFにする場合、一枚の巨大なページとして出力するのではなく、通常の印刷や閲覧に適した高さで自動的にページを分割するようにしている。PDFの仕様上、非常に大きな一枚のページも有効だが、ほとんどのビューアで表示が困難になったり、印刷できなかったりと、実用性に欠けるためだ。「有効であること」と「有用であること」は必ずしも一致しないという教訓がここにある。
また、スクリーンショット内のテキストを検索可能にするために、見えないOCR(光学文字認識)レイヤーを追加している。これはPDFのテキスト描画モード3を利用しており、画面には表示されないが、テキストの選択や検索は可能にする。選択したテキストが実際のピクセル位置に合うよう、水平スケーリングも適用されている。使用するフォントはベース14書体のHelveticaで、WinAnsiエンコード(ラテン文字のみ対応)だ。OCRの精度が低い場合、文字化けしたテキストを埋め込むよりも、認識できなかった文字は含めないという判断をしている。同様に、OCRで認識された単語のうち、信頼度が60%未満のものは破棄される。これは、検索結果が間違った単語を指し示すことで、かえってユーザーの利便性を損なうのを防ぐためだ。完全性よりも、ユーザーが実際に頼りにする「正確に検索できる」という特性を優先した設計と言える。
この話から得られる普遍的な教訓は多い。「依存なし」という主張は、その依存が具体的に何を担っていたのか、そしてそれが別の何で置き換えられたのかを明確に説明できるときに初めて意味を持つ。この事例では、PDFライブラリが提供するはずだったJPEGエンコーダとzlib実装がブラウザのネイティブ機能で代替され、残りの仕事は336行のファイル組み立てで済んだ。
さらに重要なのは、依存が既存のプラットフォーム機能に置き換えられたとしても、それに伴うリスクが消えるわけではなく、形を変えて別の場所に移動するということだ。このケースでは、リスクは冗長性がなく、自己チェックの手段もないPDFのクロスリファレンステーブルに移動した。だからこそ、その部分に最も手厚いテストを配置する必要があった。出力のどの部分が、下流のシステムに検知されずに間違った状態になりうるかを問いかけることで、本当に重要なテスト箇所を見つけることができる。これはシステム開発において重要な考え方となる。