Technical Article

Pure DelphiによるOpenType GSUBの異体字(Stylistic Alternates)

デザイナーは、見出し用に1階建てのa、表用にスラッシュ付きゼロ、あるいはカバー用にスワッシュ(飾り)付き大文字を持つフォントを選択します。これらのグリフはフォントに既に含まれています。単にデフォルトになっていないだけです。デフォルトのaは、cmapテーブルを介してキャラクターから1つのグリフへとマッピングされ、異体字(alternate)はいくつかのグリフID離れた場所に配置されており、置換ルールを介してのみ到達できます。PDFでその異体字を出力するということは、そのルールを読み取り、置換されたグリフをコンテンツストリームに出力することを意味します。この記事では、下層にネイティブのシェイピングライブラリを持たずに、Object Pascalでこれらのルール(単一置換の種類)を読み取る方法について解説します。

範囲は意図的に絞っています。スタイルセット(Stylistic sets)および異体字は、単一のグリフの入力に対して単一のグリフを出力する置換(単一置換)です。これらは、外部のC言語依存関係を排除したいPascalエンジンに適した、小規模で決定論的なテーブル巡回によって解決できるOpenTypeレイアウトの一部です。

HarfBuzzではなくPure Delphiを選択する理由

「このテキストをシェイピングする」という要求に対する明白な答えはHarfBuzzであり、双方向テキスト(bidi)、インディック系、あるいはアラビア語の完全なシェイピングにおいてはそれが正しい選択です。しかし、それはCライブラリです。これをDelphiまたはC++Builder製品にバインドするということは、すべてのターゲットプラットフォームおよびアーキテクチャに対してネイティブオブジェクトを同梱し、その呼び出し規約に合わせ、リリースサイクルを追跡し、自身のライセンス条項と照らし合わせてそのライセンスを確認することを意味します。これらは個々には難しくありませんが、常につきまとう摩擦であり、「この文字のss01形式を提供してほしい」という実際の要件に対してはお買い得なメリットはありません。

単一置換(Single substitution)はシェイピングエンジンを必要としません。必要なのは、いくつかのGSUBサブテーブル形式のパーサーと、1つか2つのバイナリサーチ(二分探索)です。これをPascalで記述することにより、ツールチェーン全体を1つのコンパイラ内に収めることができます。限界として、このアプローチはグリフ置換ルックアップのみを処理し、それ以外は処理しません。双方向テキストの解決、インディック系の並べ替え、あるいは自動的な文脈依存シェイピング(Contextual shaping)を行うものではありません。それらが必要とされる場所ではそれらが必要であり、単一置換のクエリがその代わりを務めることはできません。

GSUBの階層構造:上から下まで

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

LangSysは機能インデックス(feature indices)のセットを指定します。各インデックスはFeatureListを指し、そこで機能レコードはss01などの4バイトのタグと、ルックアップインデックスのリストを保持します。それらのインデックスは、最終的に実際の置換サブテーブルが存在するLookupListを指します。したがって、ss01の解決とは、「スクリプトを見つけ、そのLangSysを見つけ、タグがss01である機能を見つけ、それが指定するルックアップを収集し、それらを適用する」ことを意味します。HotPDFはデフォルトでDFLTスクリプトとデフォルトのLangSysを使用します。これはラテン文字デザインの大部分が提供する仕様であり、フォントが特定のスクリプトの下に機能を定義している場合にスクリプトタグをオーバーライドする方法も提供しています。

Coverageテーブルによる対象グリフの決定

すべての置換サブテーブルは同じ質問から始まります。この入力グリフはこのルールに参加しているか、参加しているならルールの独自のインデックス内のどこに位置しているか、という質問です。この質問はCoverageテーブルによって回答され、その回答はCoverageインデックス(後続のサブテーブルがグリフの変換先を検索するために使用する小さな順序値)になります。

Coverageには2つのフォーマットがあります。フォーマット1は、昇順でソートされたグリフIDのリストです。バイリサーチでグリフを検索し、リスト内での位置がCoverageインデックスになります。フォーマット2は範囲レコード(range records)のリストであり、それぞれ開始グリフ、終了グリフ、および開始グリフがマッピングされる開始Coverageインデックスを持ちます。範囲内のグリフは、範囲の開始点からのオフセットによってCoverageインデックスを取得します。フォーマット1は参加するグリフが散在している場合にコンパクトになり、フォーマット2は連続した範囲に並んでいる場合に適しています。どちらもソートされているため、対数時間(logarithmic time)で検索され、Coverageインデックスまたはエンジンがグリフをそのままにする「カバーされていない」という結果のいずれかを返します。

単一置換の2つのフォーマット

単一置換(Single Substitution)はLookupType 1であり、1つのグリフを正確に1つの置換先にマッピングします。これにも2つのフォーマットがあり、スペース最適化のための分割です。フォーマット1は、1つの符号付きデルタ(差分値)を格納します。出力されるグリフIDは、入力されたグリフIDにそのデルタを加え、65536で割った余り(モジュロ)になります。これは、参加するすべてのグリフがその異体字から一定の固定オフセットに配置されている置換をフォントがエンコードする方法です。例えば、対応するオールドスタイル数字から一定の距離に配置されたライニング数字のブロックなどです。Coverageテーブルがどのグリフが対象であるかを示し、1つのデルタがそれらすべてに機能します。

フォーマット2は、置換グリフIDの明示的な配列を格納します。CoverageテーブルからのCoverageインデックスがその配列へのインデックスとなるため、Coverageインデックス0のグリフは配列の最初の要素になり、インデックス1は2番目の要素になり、以下同様に続きます。フォーマット2は、スタイルセットを手作業で構築する場合などのように、異体字が均一なオフセットに配置されていない場合に使用されます。クエリはどちらのフォーマットでも呼び出し側からは同一です。入力グリフを取得し、それをCoverageに通し、カバーされている場合はデルタを適用するか配列のスロットを読み取ります。

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は、一致しないすべてのケース(フォントがない、GSUBテーブルがない、一致する機能がない、Coverageにヒットしない)において、入力されたグリフIDをそのまま返します。これは、この呼び出しを無条件に実行しても安全であることを意味します。異体字を要求し、存在しない場合は入力したものがそのまま返されるため、呼び出し側のコードでその機能を欠くフォントを特別扱いする必要はありません。

スタイル機能タグの意味

機能タグは、要求する異体字の種類を決定する語彙であり、スタイル処理に関連するタグは短いリストです。主要なペアは、グリフの代替形状への汎用的なアクセスを提供するスタイル異体字(Stylistic alternates)のsaltと、フォントが定義できる20個のスタイルセット(Stylistic sets)ss01ss20です。それぞれデザイナーがグループ化する置換のバンドルです。例えば、フォントが1階建てのaと直線の脚を持つRss03の下に配置している場合、その1つのセットを有効にするだけで両方が再スタイルされます。

それらの周囲には、さらにいくつかの単一置換タグが存在します。aaltは、グリフが持つすべての異体字の和集合である全代替文字アクセス(access-all-alternates)で、通常はグリフパレット機能として提供されます。titlは、大きなサイズ向けにカットされたタイトルの大文字を選択します。subsおよびsupsは、デフォルトの数字を縮小するのではなく、本物の下付き文字および上付き文字に置き換えます。ordnは、1stや2ndで上付きになる文字などの序数形式を生成します。fracは分数を生成しますが、斜め分数の完全な処理には、プレーンな単一置換を超える合字(Ligature)や文脈依存のロジックも必要になります。単一グリフのケースについては、メカニズムは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のサブテーブルは、基本多言語面(BMP)である最初の65536コードポイントをカバーしており、これはほとんどのラテンテキストで十分です。しかし、数学用英数字記号、多くのシンボル、およびいくつかの現用文字が存在する、U+10000以上の追加面(補助面)のコードポイントをカバーするには十分ではありません。

フォーマット12は、U+0000からU+10FFFFまでの全範囲をカバーするサブテーブルです。これはソートされたグループのリストであり、各グループは開始コードポイント、終了コードポイント、および開始グリフIDを持つため、連続した範囲のコードポイントが連続した範囲のグリフにマッピングされます。HotPDFは、データがどのように形成されているかに合わせたハイブリッド戦略でコードポイントを解決します。BMP内のコードポイントは、コードポイントによってインデックス付けされた直接配列から提供され、検索を行わない単一のルックアップになります。補助面内のコードポイントは、コードポイントによってソートされ、バイリサーチで検索される疎なテーブルから提供されます。その結果、GetUnicodeGlyphForCodepointCardinalを受け取り、範囲全体で正しく応答し、フォントがマッピングしていないコードポイントに対してはグリフ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は特定の形式の質問に回答するものであり、何に回答しないのかを明確にしておくことが重要です。LookupType 1は8つの置換タイプの中の1つです。このクエリは、1つのグリフが複数に変換されるLookupType 2複数置換や、複数のグリフが1つに変換されるLookupType 4合字置換を処理しません。また、グリフが特定の文脈にある場合にのみ発生するLookupType 5および6の文脈依存および連鎖文脈依存のタイプ、さらには拡張タイプや逆連鎖タイプも処理しません。斜め分数、デーヴァナーガリーの結合文字、あるいはアラビア語の語頭・語中・語末の変形などはシーケンスの問題であり、グリフごとの単一置換ルックアップでは表現できません。

また、自動的なシェイピングも実行しません。ここでは、テキストのランを検査し、有効にする機能を決定し、スクリプトが要求する順序で適用する処理は行いません。呼び出し側が機能タグを選択し、それをグリフごとに適用します。これは、オプトインで局所的であるスタイルセットや異体字に対してはまさに適したツールですが、並べ替えを必要とするスクリプトに対してはまったく適していません。境界を明確にしておくことが、置換パスを小さく予測可能に保つ鍵となります。

シーケンスレベルの処理を必要とするケースについては、複雑なスクリプトに関する説明はDelphiでの複雑なスクリプトのテキストシェイピングに関する記事で扱っています。置換が、ページ上に画像や他のフォントを配置する大規模なレポート作成作業の一部である場合、フォントと画像を使用したレポート出力ガイドが、これらの要素がどのように適合するかをカバーしています。これらすべてが、このブログの他の場所で扱われているフォントの埋め込み、サブセット化、およびテキストAPIと並んで、GSUB置換クエリを実装しているDelphiおよびC++Builder向けのHotPDF Componentと同じエンジン上で動作します。