技術記事

Delphiで既存のPDFに未埋め込みのフォントを埋め込んでPDF/Aに対応する

losLab PDF Libraryは、すでに読み込まれているPDFの不足しているフォントプログラムを、一度の呼び出しで埋め込むことができます。EmbedMissingFontsはドキュメント内のすべてのフォント辞書をスキャンし、そのBaseFont名に基づいて一致するインストール済みのシステムフォントを特定し、フォントプログラムをファイルに書き戻します。フォントの埋め込みが原因でPDF/A検証に失敗するサードパーティのドキュメントを修復している開発チームにとって、これはプリフライトエラー00030を解消するための解決策となります

このような状況は非常によく見られます。アーカイブの取り込みパイプラインが仕入先、顧客、またはスキャン業者からPDFを受信したとします。ドキュメントは社内のすべてのPCで問題なくレンダリングされますが、PDF/A検証ツールが「少なくとも1つのフォントが埋め込まれていません」という同じ警告をファイルごとに繰り返し、バッチ全体を拒否します。送信元がファイルを再生成することはないため、パイプライン側でこれらを修復する必要があります。本記事では、その修復プロセスについて解説します。これは、PDF/AおよびPDF/UA違反 of 検出について説明したプリフライトの記事の姉妹編であり、あちらがどのドキュメントが破損しているかを特定するのに対し、本記事では最も頻繁に発生する破損状態を修復します

なぜPDF/Aではすべてのフォントを埋め込む必要があるのか?

ISO 19005-1 §6.3.4では、適合ドキュメントで使用されるすべてのフォントが、ファイル内部にそのフォントプログラムを保持していることを要求しています。これは、PDF/Aが約束する核心が「再現性」にあるためです。つまり、ドキュメントを作成したマシンとフォントを共有していない50年後のマシンでも、まったく同じようにレンダリングされなければなりません。埋め込まれていないフォントは、表示システム上のどこかからArialを探すようにという指示を意味しますが、標準規格の立場では「表示システム上のどこか」はアーカイブの保証にはなりません。代替フォントが持つグリフ、メトリクス、文字範囲がそのままリーダーに表示されるため、著者が意図した表示とは異なってしまう可能性があります

歴史的な原因は「Standard 14」の慣例にあります。PDF 1.0では、すべてのビューアがHelvetica、Times、Courier、Symbol、およびZapfDingbatsを搭載していることを保証していたため、PDF生成ツールはそれらのフォントを名前で参照し、何も埋め込まない方法を採用しました。そして、30年にわたるツール群が現在も全く同じことを行っています。losLab PDF Libraryはこの要件を非常に重視しており、PDF/A作成モードではAddStandardFontが意図的に何もしない(no-op)ようになっています。ライブラリはStandard 14のフォントプログラムを同梱していないため、持っていないものは埋め込めず、適合性を主張するドキュメントに未埋め込みの参照を書き込むことを拒否します。これはフォントを選択せずに0を返すため、PDF/Aドキュメントでは代わりに埋め込みを有効にしたAddTrueTypeFontを使用する必要があり、PDF/Aモードが有効な間、すべてのEmbed=0要求は暗黙的にEmbed=1に昇格されます。これが書き込み側の処理です。より困難なのは読み込み側、すなわち、自分自身が作成したのではないフォント辞書が満載された、他人が作成したドキュメントです

EmbedMissingFontsは読み込まれたドキュメントをどのように修復するのか?

losLab PDF Libraryは、フォントを再構築するのではなく、その場で修復します。PDF生成ツールが埋め込まれていないTrueTypeフォントを書き込むとき、それが生成するFontDescriptor辞書はすでに完成しています(FontName、FontBBox、Flags、Ascent, Descent、StemVなどがすべて存在します)。埋め込みフォントとの唯一の違いは、実際のフォントプログラムを保持する/FontFile2ストリーム参照という1つのエントリがないことです。したがって、EmbedMissingFontsは、フォント辞書、エンコーディング、文字幅配列、またはリソース名でフォントを参照するコンテンツストリームには一切触れません。システムから一致するフォントプログラムを読み込み、新しいストリームオブジェクトに圧縮して、すでに存在するFontDescriptorに単一の/FontFile2参照(またはCIDFontType0フォントの場合は/FontFile3)を追加します。ドキュメントのページが指しているものはすべて元の位置に維持されるため、制御できないファイルに対しても安全に実行できます

サポート範囲には、実際に遭遇する両方のフォントアーキテクチャが含まれます。すなわち、シンプルなTrueTypeフォントと、CJKテキストや現代的なUnicode出力向けに生成される複合Type0/CIDフォントです。トラバーサル処理は、ページごとのリソースのスキャンに依存するのではなく、ドキュメントのオブジェクトツリー内のすべてのFont辞書を意図的にリストアップするため、注釈から参照されているフォントやページ間で共有されているフォントも確実に検出されます。APIは、読み込まれたドキュメントに対する1回の呼び出しです

var
  PDF: TPDFlib;
  Repaired: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('supplier-invoice.pdf', '') <> 1 then
      raise Exception.Create('Could not load PDF');

    // Walks every Font dictionary; returns how many fonts
    // gained a font program. Fonts whose program cannot be
    // found on the system are skipped, not failed.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

    PDF.SaveToFile('supplier-invoice-repaired.pdf');
  finally
    PDF.Free;
  end;
end;

名前の一致処理が単純な文字列比較よりも効果的に機能する理由を説明する上で、知っておくべき詳細があります。ライブラリは検索前にBaseFont名を正規化します。サブセット接頭辞(6文字の大文字とプラス記号からなるABCDEF+パターン)は削除され、ArialMTのようなPostScriptスタイルの接尾辞はArialに解決されます。また、TrueTypeコレクションファイルが検出されて解凍されるため、.ttcファイル内に存在するフォントフェイスも正しく埋め込まれます

プリフライトレポートによる修復の検証

CreatePreflightReportは検証ステップであり、検証プロセスは自己完結しています。ファイルをエラーと判定した監査自体が、ファイルをクリアするものと同じであるべきです。エラーコード00030は、「少なくとも1つのフォントが埋め込まれていません(FontFile/FontFile2/FontFile3がない)」というPDF/Aのディープ監査の検出結果であり、ファイル全体に対して報告されるため、1つでもフォントが見落とされるとエラー状態が維持されます。ソースファイルに対してレポートを実行し、修復して保存してから、出力ファイルに対して再度レポートを実行してください

function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 selects the PDF/A checks
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

ファイルごとの判定ではなくフォントごとの状態を確認するには、修復されたドキュメントを再度読み込んで列挙処理を行います。FindFontsを実行した後にSelectFontGetFontIsEmbeddedを呼び出すことで、フォントごとに埋め込み状態が報告されます。これは、どのファイルのどのフォントフェイスが修復できなかったかをバッチ処理で正確にログ記録する必要がある場合に適したツールです。これと同じ列挙パターンは、読み込まれたPDFからテキスト、画像、フォントを抽出することに関する記事にも登場し、そこでは修復ではなく抽出処理に利用されています

フォントがシステムにインストールされていない場合はどうなるのか?

EmbedMissingFontsは、プログラムが見つからないフォントをスキップし、その結果を戻り値として報告します。カウントされた値が未埋め込みフォントの数より少ない場合、その差分はシステムに存在しないフォントです。これは適切なエラー処理の形であり、代替案よりも優れています。なぜなら、ドキュメントに指定された名前のフォントに対して代替のプログラムをでっち上げるとレンダリング結果が変化してしまい、アーカイブ修復で最も避けるべき事態を招くためです。このようなケースに備えて、losLab PDF LibraryはEmbedFontProgramFromFileを提供しています。これにより、呼び出し側が用意した.ttfまたは.otfを名前付きフォントに埋め込むことができるため、パイプラインで想定される企業指定フォントを配布し、それらに意図的にフォールバックさせることが可能になります

var
  I, FontID: Integer;
begin
  PDF.FindFonts;
  for I := 1 to PDF.FontCount do
  begin
    FontID := PDF.GetFontID(I);
    if (FontID > 0) and (PDF.SelectFont(FontID) = 1) then
      if PDF.GetFontIsEmbedded = 0 then
        // Try the installed system font first, then fall back
        // to a font file shipped alongside the application
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

明確にしておくべき制限事項が2つあります。第1に、現在の実装ではType1フォントは修復されません。それらの/FontFileエントリには、明示的な長さキーを持つ3セグメントのPFB構造が必要であり、ライブラリは不正なストリームを書き込む代わりにそれらをスキップします。これらは現代のドキュメントでは稀ですが、古いアーカイブには存在します。第2に、フォントの埋め込みはライセンス行為です。TrueTypeフォントの埋め込み許可はファウンドリに帰属し、ライセンスされたフォントプログラムを組織外に出るドキュメントに埋め込む修復パイプラインを運用する場合、フォントライセンスが実際にそれを許可しているか確認する必要があります。ライブラリは指示された通りに実行しますが、実行してよいかどうかはコンパイラではなく、法務部門に確認すべき問題です

埋め込みは必須だが、それだけでは十分ではない

フォントの修復により解消されるのはエラー00030のみです。暗号化、XMPメタデータの欠落、OutputIntentのないデバイス依存の色空間、あるいはToUnicodeマップの欠落などによってPDF/A検証に失敗するドキュメントは、すべてのフォントを埋め込んだ後でも依然として検証に失敗します。そのため、修復処理は単独で実行するのではなく、プリフライト主導のループ内に組み込むべきです。完全なレポートを実行し、示された問題を修正し、レポートが完了を告げるまでこのプロセスを繰り返してください。また、コストの側面もあります。完全なCJKフォントプログラムは数メガバイトに達することがあるため、それらを複数埋め込むと小さなドキュメントのサイズが劇的に膨張する可能性があります。これを抑えるのが、PDFファイルサイズの最適化とフォントのサブセット化に関する記事で説明されているサブセット化であり、埋め込まれる各プログラムをドキュメントが実際にレンダリングするグリフのみに削減します

新しいドキュメントのデグレードを防ぐ

SetEmbedAllFontsは、同じ機能の予防側の処理です。これは、本記事で修復対象としているようなドキュメントを自前のコードが生成するのを防ぐ書き込み側のガードです。SetEmbedAllFonts(1)を有効にすると、その後のAddTrueTypeFont呼び出しでEmbed=0が要求されても埋め込み参照に昇格され、PDF/Aモードがすでに強制している保証がすべてのドキュメントに適用されます。これは呼び出し後に追加されたフォントに影響し、すでに読み込まれているファイル内のフォントには影響しないため、役割分担は明確です。作成するドキュメントにはSetEmbedAllFontsを使用し、受け取ったドキュメントにはEmbedMissingFontsを使用します

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// From here on, AddTrueTypeFont(Name, 0) behaves
// like AddTrueTypeFont(Name, 1): no non-embedded
// reference can reach the output file

書き込み側のガードと、読み込み・修復・保存のプロセスの両方は、結果を検証するプリフライトエンジンとともに、Delphi、C#、およびVB.NET向けのlosLab PDF Libraryに含まれています。製品ページには、フォントごとの埋め込みやサブセット化の呼び出しを含む、完全なフォントAPIリファレンスが掲載されています