技術記事

PDFのフォントとテキスト: グリフが四角形になる理由

自分のマシンでは完璧に見えるのに、他の人のマシンでは空の四角形の列としてレンダリングされるPDFは、ドキュメントソフトウェアにおいて最も一般的なフォントの欠陥ですが、テキストが間違っていることを意味することはほとんどありません。文字は無傷で、エンコーディングも問題ありません。単にグリフが存在しないだけです。2つのマシン間で変わったのは、オペレーティングシステムにインストールされているフォントであり、ポータブルなファイルと壊れやすいファイルのギャップは、ページが書き込まれたときに行われた1つの決定です。つまり、フォントがPDF内に保存されたか、または相手側にあると想定されたか、です

それがなぜ起こるのか、そして別の失敗によって、検索可能に見えるテキストが文字化けとしてコピーされてしまう理由を理解するには、PDFがテキストをどのように保存するかを見る必要があります。PDFは文を保存しません。グリフコードとフォントプログラム、そしてそれらを相互にマッピングするテーブルを保存しており、すべてのレンダリングや抽出のバグはその3つの間のギャップに存在します。以下は、ISO 32000に基づいたそのメカニズムのツアーであり、それを制御する重要なDelphiの呼び出しを添えています

文字、コード、グリフは3つの異なるものです

日常会話では3つの明確な概念を「文字」という言葉に集約してしまうため、この語彙は人々を混乱させます。キャラクター(文字)とは、Unicode で U+0041 として識別される大文字のAの概念など、記述の抽象的な単位です。グリフとは描画された形であり、特定のフォントがその文字を描写するために使用する曲線とステムのアウトラインです。それらの間にはコードがあります。これは、現在のフォントのどのグリフを描画するかをビューアに伝える、コンテンツストリーム内の1つまたは複数のバイトです

PDFはコードで機能します。コンテンツストリームに文字列が表示されている場合、それらのバイトはUnicodeではなく、アクティブなフォントへのインデックスです。フォントのエンコーディングは、65というコードが「65にファイルされているグリフを描画する」ことを意味すると決定し、その操作のどこにも、結果が人間にAのように見えることは認識されていません。これが、PDFがグリフを見つけられる場所であればどこでも同じようにレンダリングされる理由であり、抽出が表示とは別の問題である理由でもあります。描画にはコードからグリフへの変換のみが必要であり、読み取りにはコードからUnicodeへの変換が必要であり、これらは独立して不一致になったり欠落したりする可能性のある2つの異なるテーブルです

実際に遭遇するフォントの種類

ISO 32000ではいくつかのフォント辞書タイプが定義されており、実際に受信または生成するドキュメントはそのうちの3つのいずれかを使用します。どれを見ているかを知ることで、問題の大半を説明できます

Type 1 は、3次ベジェ曲線で構築された、AdobeのオリジナルのPostScriptアウトラインフォーマットです。すべての準拠リーダーが提供しなければならない14の標準フォント(Helvetica、Times、Courier、Symbol、およびZapfDingbatsファミリー)は Type 1 であり、それらのいずれかを示すフォント辞書は、法的にフォントプログラムを省略できます。これは、フォントを埋め込まないことが、偶然ではなく仕様によって安全である唯一のケースです。他のすべての Type 1 書体では、プログラムを埋め込む必要があります。そうしないと、ビューアが何か(通常はメトリクスは似ているが視覚的には異なるフォント)を代用します

TrueType は2次曲線を使用し、AppleとMicrosoftの世界から来ました。ほとんどのシステムフォントがこれであり、最も頻繁に埋め込むことになります。PDFの単純なTrueTypeフォントは1バイトコードに制限されているため、そのようなフォント1つで一度に最大256個のグリフを処理できます。この上限が、CJKやその他の大きなスクリプトが単純なフォントに乗せられない構造的な理由です

Type 0(複合フォントまたはCIDキーフォント) は、その制限に対する答えです。マルチバイトコードと CMap を使用して、アウトライン自体が TrueType または CFF/Type 1 のいずれかである子孫の CIDFont にルーティングします。これは数千のグリフを保持できる唯一のフォントタイプであるため、中国語、日本語、韓国語、または幅広い多言語が混在するPDFは、作成者がそれを意識したかどうかに関係なく、Type 0 を使用しています。トレードオフは複雑さです。より多くの可動部分があり、そのうちのより多くの部分がレンダリングと抽出の両方で正しく機能する必要があります

PDF内で12、18、24、36ポイントでレンダリングされた1つのTrueTypeフォント。1つの埋め込みアウトラインが任意のサイズにスケーリングできることを示しています。

その図の背後にある1つの詳細がファイルサイズを決定します。フォントは固定サイズのビットマップではなく、アウトラインのライブラリであるため、同じ埋め込みプログラムがページ上のすべてのポイントサイズを提供します。スケーリングは描画時に適用される変換です。これが、見出しと本文が1つの埋め込み書体を共有する理由であり、埋め込みコストがサイズごとではなくフォントごとである理由です

埋め込みはポータブルと壊れやすさの違いです

埋め込みとは、フォントプログラム、つまり実際のアウトラインデータがストリームとしてPDFに書き込まれることを意味します。あなたのフォントを聞いたことがないマシン上のリーダーは、ファイルからそれらのアウトラインを直接読み取り、正確なグリフを描画します。埋め込みをスキップすると、宛先に同じ名前のフォントがあるかどうかに賭けることになります。そうでない場合、ビューアは代用品にフォールバックします。標準の14フォントの場合、その代用は定義されており、無害です。それ以外のすべてのものについては、別の書体でのニアミスから、代用フォントがスクリプトをまったくカバーしていない場合の空の四角形の結果まで多岐にわたります

HotPDFでの制御は、ドキュメントが開く前に設定される単一のプロパティです。FontEmbedding は、描画に使用する書体をファイルにパックするようにライブラリに指示します:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Compression := cmFlateDecode;
    Pdf.FontEmbedding := True;          // outlines travel inside the file
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Calibri', [], 11);
    Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

この順序は表面的なものではありません。BeginDoc は HotPDF がドキュメント構造をコミットする場所であるため、その呼び出しの前に FontEmbedding を True にする必要があります。後で割り当てた場合、エラーも警告も表示されず、単にフォントが含まれないファイルが静かに出力されます。これは最悪の種類のバグです。フォントがたまたまインストールされている開発者のマシンではすべてのテストに合格し、インストールされていない顧客のマシンでのみ表面化します

また、埋め込みはライセンスとエンジニアリングが交差する場所でもあります。フォントプログラムには、自由に埋め込むことができるか、プレビューのみか、またはまったく埋め込むことができないかを示すフラグが含まれています。これらのフラグを尊重するのはあなたの責任であり、レンダラーの責任ではありません。「機能した」ことは「許可された」ことと同じではありません

サブセット化: 使用したグリフのみを埋め込む

完全な埋め込みでは、フォントプログラム全体がファイルに書き込まれます。CJKの大きなTrueType書体は数メガバイトに達する可能性があり、数文字を表示するために全体を埋め込むことは、複数ページのドキュメント全体で問題が悪化するほど無駄です。サブセット化は、ドキュメントが参照するグリフのみを書き込み、6文字のタグとプラス記号を付けてフォントの名前を変更すること(サブセット化されたPDFのフォントリストにある ABCDEF+Calibri の形式)でこれを解決します。これにより、リーダーは部分的な書体と同じ名前の完全なシステムフォントを混同することはありません

ほとんどの生成されたドキュメントにとって、サブセット化は適切なデフォルトです。ファイルサイズをソースフォントではなくコンテンツに比例させるように維持します。これは、ファイルサイズを支配してしまうような大規模な多言語フォントにとって最も重要です。1つの注意点は、サブセットには作成時に使用されたもののみが含まれるということです。後続のプロセスが後でサブセット化されたフォントにテキストを追加しようとした場合、必要なグリフがファイルにない可能性があり、これは他の人のPDFを増分編集する際の実際の制約となります

UnicodeフォントとCJKの四角形問題

テキストがプレーンなラテン文字でない場合、単純なフォントのパスは機能しなくなります。解決策は、Unicode対応のフォントを明示的に登録し、HotPDFにそこから Type 0 フォントを構築させることです。RegisterUnicodeTTF はパスによってTrueTypeファイルを読み込みます。その後、登録された名前は他のフォントと同様に SetFont で使用できます:

Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;

この成否を分けるのは2つのことです。フォントは文字列内のスクリプトをカバーしている必要があります。ラテン文字のみのTrueTypeに要求したからといって中国語のグリフが増えるわけではなく、今回はその書体にグリフが純粋に存在しないため、結果は再び空の四角形になります。また、登録されたTTFから組み立てられた Type 0 フォントは、アウトラインを見つけることができないリーダーにとっては無意味であるため、埋め込みをオンにしておく必要があります。混在するコンテンツの場合、耐久性のある選択はカバー範囲の広い書体であり、通常は Noto および Arial Unicode MS ファミリーを埋め込んでサブセット化したものが答えになります

右から左への記述や複雑なスクリプトでは、カバー範囲の上にシェーピングレイヤーが追加されます。HotPDFはアラビア語とヘブライ語向けに RtLTextOut を公開しています。これは方向の並べ替えを処理するため、論理的な順序を渡してライブラリにレイアウトさせます。アラビア語を正しく表示することは、カバー範囲、シェーピング、方向の3つの個別のことであり、四角形が表示された場合は、それらのいずれかが失敗したことを意味する可能性があります

ToUnicodeテーブル: コピー&ペーストが存在する場所

上記はすべて描画に関するものです。抽出はその鏡像であり、独自の理由で失敗します。ビューアはフォントのコードからグリフへのマッピングを使用してページをレンダリングしますが、ユーザーがテキストを選択してコピーする場合、ビューアはそれらと同じコードをUnicodeに戻す必要があります。その逆マッピングが ToUnicode CMapであり、フォントに添付されるオプションのストリームです

これが存在し、正しい場合、コピーされたテキストは正しい文字として出力されます。これが存在しないか間違っている場合、またはフォントがカスタムのグリフコードでサブセット化され、ToUnicode が書き込まれなかった場合、ページは完璧に見えてもクリップボードは文字化けで埋め尽くされます。カスタムエンコードされたサブセットではそうではないにもかかわらず、グリフコードがUnicodeであるかのように読み取られるためです。これが、OCRテキストレイヤーを持つスキャン済みドキュメントが検索可能である一方、不注意なジェネレータから作成されたボーンデジタルのPDFが検索できない理由です。レンダリングと抽出は異なるテーブルを利用するため、ファイルは一方を満たし、他方を満たさない場合があります。抽出が出力にとって重要である場合は、正しい ToUnicode マップを要件として扱い、そこにあると信じるのではなく、サンプルからテキストをコピーして検証してください

フォントのバグをすばやく診断する方法

障害モードはどこを見るべきかを教えてくれます。別のマシンでの空の四角形は、ほとんどの場合、フォントが埋め込まれていなかったことを意味します。そのため、最初に埋め込みを確認し、次にグリフのカバー範囲を確認してください。自分のマシンにさえ表示される四角形は、カバー範囲を示しています。埋め込みに関係なく、フォントにそのスクリプトが含まれていません。正しくレンダリングされるのにコピーすると意味不明なテキストになるのは、ToUnicode の問題であり、レンダリングの問題ではありません。描画自体は壊れていないため、フォントや埋め込みをいじっても修正されません。完成したファイルを読み取るには、Acrobatで開き、「ドキュメントのプロパティ」の「フォント」を確認します。正常なエントリには、種類、埋め込みまたは埋め込みサブセットと表示され、エンコーディングの名前が示されています。埋め込まれるべきなのに埋め込まれていないフォントは、顧客が気付く前にそこで自らを示しています

文字、コード、グリフの分割が明確になれば、これらはどれもエキゾチックなものではありません。描画に使用するフォントを埋め込み、大きなフォントをサブセット化し、テキストがラテン文字を離れた瞬間にUnicode書体と RegisterUnicodeTTF を使用し、誰かがテキストを抽出する場合は正しい ToUnicode マップを保持します。これらを正しく行えば、四角形は表示されなくなります。周囲のメカニズムについては、最小のPDFの構造でフォント辞書がオブジェクトツリーのどこにあるかを示し、ドキュメント構造のウォークスルーでリソースがページ間でどのように共有されるかをカバーしています

ここに示されている SetFontFontEmbedding、および RegisterUnicodeTTF の呼び出しは、Delphi および C++Builder 向けの HotPDF Component の一部です