技術記事

Pure DelphiによるOpenType GSUBスタイリスティック代替

デザイナーは見出し用にシングルストーリーのaを持つフォントを選んだり、表組み用にスラッシュゼロを使ったり、表紙用にスワッシュ大文字のセットを使ったりします。それらのグリフはすでにフォント内に存在しています。ただし、デフォルトではないだけです。デフォルトのacmapテーブルを通じて文字から1つのグリフにマッピングされており、代替グリフは数個のグリフIDの先に置換ルールを通じてのみアクセスできる場所にあります。PDFでその代替グリフを生成するということは、そのルールを読み取り、コンテンツストリームに代替グリフを出力することを意味します。この記事は、Object Pascalでネイティブのシェイプライブラリを使用せずにそれらのルール(シングル置換型)を読み取ることについて解説します

スコープは意図的に絞り込まれています。スタイリスティックセットと代替グリフは、1グリフ入力・1グリフ出力の置換です。これらは、小さく決定論的なテーブルウォークで解決できるOpenTypeレイアウトの一部であり、C依存なしに保ちたいPascalエンジンに適しています

HarfBuzzではなくPure Delphiを選ぶ理由

HarfBuzzは「このテキストをシェイプする」という問いへの明白な答えであり、完全な双方向テキスト、インド系文字、アラビア語のシェイピングにはまさに正しい答えです。しかし、それはCライブラリです。DelphiまたはC++Builderの製品にバインドするには、ターゲットプラットフォームとアーキテクチャごとにネイティブオブジェクトを配布し、その呼び出し規約を合わせ、リリースサイクルを追跡し、ライセンス条項を自社のものと照合する必要があります。これらはいずれも単体では難しくありません。しかし、これらすべてはずっと消えることのない摩擦であり、実際の要件が「この文字のss01形式をくれ」だけなら何も得られません

シングル置換にシェイプエンジンは不要です。いくつかのGSUBサブテーブル形式のパーサーと1、2回の二分探索があれば十分です。それをPascalで書けば、ツールチェーン全体を1つのコンパイラ内に収めることができます。正直な限界は、このアプローチはグリフ置換ルックアップのみを処理するということです。bidi解決ではなく、インド系文字の並べ替えでもなく、自動的なコンテキストシェイピングでもありません。それらが必要な場合は確かに必要であり、シングル置換クエリはそれらの代替にはなりません

GSUBの階層構造(上から下へ)

グリフ置換テーブルは間接参照のチェーンとして構成されており、置換クエリは上からチェーンをたどります。最上位はScriptListです。latnのようなスクリプトタグがエントリを選択し、特別なタグDFLTは、より具体的なスクリプトが一致しない場合に適用されるデフォルトスクリプトです。スクリプトエントリはLangSys(言語システム)を指し示し、一般的なケース用のデフォルトLangSysと、異なる動作を必要とする言語用のオプション付き名前付きLangSysがあります。トルコ語が典型例で、ドットあり・ドットなしのiにはそれぞれ専用の処理が必要です

LangSysはフィーチャインデックスのセットを指定します。各インデックスはFeatureListを指し、フィーチャレコードには4バイトのタグ(ss01など)とルックアップインデックスのリストが含まれます。これらのインデックスは最終的にLookupListを指し、実際の置換サブテーブルが存在します。したがってss01を解決するには、スクリプトを見つけ、そのLangSysを見つけ、タグがss01のフィーチャを見つけ、それが指定するルックアップを収集して適用します。HotPDFはデフォルトとしてDFLTスクリプトとデフォルトLangSysを使用します。これは大多数のラテンテキストデザインが採用するものであり、フォントが特定のスクリプト下にフィーチャを配線している場合にスクリプトタグをオーバーライドする方法も提供します

カバレッジテーブルが参加者を決定する

すべての置換サブテーブルは同じ問いから始まります。この入力グリフはこのルールに参加するか、そしてそうであれば、ルール自身のインデックス付けの中でどこに位置するかという問いです。この問いはカバレッジテーブルによって答えられ、答えはカバレッジインデックス(サブテーブルの残りの部分がグリフが何になるかを検索するために使用する小さな序数)です

カバレッジには2つの形式があります。形式1は昇順にソートされたグリフIDのリストです。二分探索でグリフを見つけ、リスト内のその位置がカバレッジインデックスになります。形式2は範囲レコードのリストで、それぞれが開始グリフ、終了グリフ、開始グリフがマップするカバレッジインデックスです。範囲内のグリフは範囲の開始点からのオフセットによってカバレッジインデックスを取得します。形式1は参加グリフが分散している場合にコンパクトであり、形式2は連続したランに収まる場合に効率的です。どちらもソートされているため対数時間で検索でき、カバレッジインデックスまたはエンジンがグリフをそのままにしておくことができる「未カバー」をクリーンに返します

シングル置換の2つの形式

シングル置換はLookupType 1で、1つのグリフをちょうど1つの置換にマッピングします。これにも2つの形式があり、分割はスペースの最適化です。形式1は1つの符号付きデルタを格納します。出力グリフIDは入力グリフIDにそのデルタを加えた値(65536を法として)です。これは、参加するすべてのグリフがその代替から同じ固定オフセットにある置換をフォントがエンコードする方法です。例えば、対応するオールドスタイル数字から一定の距離に配置されたライニング数字のブロックがそれにあたります。カバレッジテーブルがどのグリフが対象かを示し、1つのデルタがすべてに機能します

形式2は代替グリフIDの明示的な配列を格納します。カバレッジテーブルからのカバレッジインデックスがその配列へのインデックスとなるため、カバレッジインデックス0のグリフは最初の配列エントリになり、カバレッジインデックス1が2番目になります。形式2は代替グリフが均一なオフセットにない場合に使用されます。これは手作業で構築されたスタイリスティックセットの一般的なケースです。どちらの形式も呼び出し側からは同じクエリです。入力グリフを取得し、カバレッジに通して、カバーされていればデルタを適用するか配列スロットを読み取ります

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

注目に値するコントラクトはパススルーです。GetSingleSubstituteGlyphはすべてのミスで入力グリフIDをそのまま返します。フォントなし、GSUBテーブルなし、一致するフィーチャなし、カバレッジヒットなし、いずれの場合も同様です。つまり、この呼び出しは無条件に行っても安全です。代替グリフを要求して、存在しなければ入力したものがそのまま返ってくるため、呼び出し元のコードはフィーチャを持たないフォントを特別扱いする必要が一切ありません

スタイリスティックフィーチャタグの意味

フィーチャタグは要求する代替グリフの語彙全体であり、スタイリスティック作業に関連するタグは短いリストです。主要な2つはsalt(スタイリスティック代替)で、グリフの代替形式へのキャッチオール的なアクセスを提供するものと、ss01からss20(フォントが定義できる20個の番号付きスタイリスティックセット、それぞれデザイナーがまとめた置換の名前付きバンドル)です。例えば、フォントはシングルストーリーのaとストレートレッグのRss03の下に置くかもしれず、そのセットを有効にすると両方が変更されます

その周辺にはさらにいくつかのシングル置換タグがあります。aaltはアクセスオールオルタネート、グリフが持つすべての代替の和集合で、通常はグリフパレットフィーチャとして表示されます。titlは大きなサイズ向けにカットされたタイトルキャピタルを選択します。subssupsは縮小されたデフォルトではなく真の下付き・上付き数字に置き換えます。ordnは序数形式(1stや2ndの上付き文字)を生成します。fracは分数を構築しますが、完全な斜め分数はシングル置換の範囲を超えたリガチャとコンテキストロジックにも依存します。1グリフの場合のメカニズムはss01と同じです。タグを置換クエリに渡して代替グリフを読み取るだけです

// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

cmapフォーマット12と補助面

置換が実行される前に、文字がグリフになる必要があり、それがcmapテーブルの役割です。置換クエリはグリフIDから始まるため、パスは常にcmapを通じて文字からグリフへ、次にGSUBを通じてグリフから代替へとなります。cmapの興味深い部分はそのリーチです。フォーマット4のサブテーブルは基本多言語面(最初の65536コードポイント)をカバーし、ほとんどのラテンテキストには十分です。しかしU+10000以上のコードポイント(補助面)には不十分で、そこには数学英数字、多くの記号、いくつかの現用文字が存在します

フォーマット12は完全なU+0000からU+10FFFFの範囲をカバーするサブテーブルです。グループのソート済みリストで、各グループは開始コードポイント、終了コードポイント、開始グリフIDを持ち、連続するコードポイントの並びが連続するグリフの並びにマッピングされます。HotPDFはデータの形状に合わせたハイブリッド戦略でコードポイントを解決します。BMP内のコードポイントはコードポイントでインデックスされた直接配列から提供され、単一の検索でサーチなしです。補助面のコードポイントはコードポイントでソートされたスパーステーブルから提供され、二分探索で検索されます。その結果、GetUnicodeGlyphForCodepointは完全なCardinalを受け取り、全範囲にわたって正しく答え、フォントがマッピングしていないコードポイントに対してはグリフID 0(.notdefグリフ)を返します

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

これらのクエリが止まるところ

シングル置換APIは1つの形状の問いに答えるものであり、何に答えないかを明確にしておく価値があります。LookupType 1は8つの置換タイプのうちの1つです。このクエリはLookupType 2(複数置換、1つのグリフが複数になる)も、LookupType 4(リガチャ置換、複数のグリフが1つになる)も処理しません。コンテキストとチェーニングコンテキストタイプ(LookupType 5と6、特定の隣接環境にあるときだけ発動する)も、拡張や逆チェーニングタイプも処理しません。斜め分数、デーヴァナーガリー結合文字、またはアラビア語の語頭・語中・語末のカスケードはシーケンスの問題であり、グリフごとのシングル置換ルックアップでは表現できません

また、自動シェイピングも行いません。ここには、テキストの実行を検査して有効にするフィーチャを決定し、スクリプトが要求する順序で適用するものは何もありません。呼び出し元がフィーチャタグを選択し、グリフごとに適用します。これはスタイリスティックセットと代替グリフに対して正しいツールであり(オプトインでローカル)、並べ替えを必要とするスクリプトに対しては完全に間違ったツールです。境界を鮮明に保つことが、置換パスを小さく予測可能に保てる理由です

シーケンスレベルの作業が必要なケースについては、Delphiでの複雑なスクリプトテキストシェイピングに関する記事で扱っています。置換が画像や他のフォントもページに配置するより大きなレポートジョブの一部である場合は、フォントと画像を使ったレポート出力のガイドでそれらのピースがどのように組み合わさるかを解説しています。これらはすべて同じエンジン、DelphiとC++Builder向けのHotPDF Componentで動作し、このブログの他の場所で扱われているフォント埋め込み、サブセット化、テキストAPIとともにGSUB置換クエリを搭載しています