OpenType GSUB Substitution Engine

Glyph substitution capability surface (v2.119.43 - v2.119.50)

 

Arabic Shaping  CFF / OpenType Subsetting

HotPDF 內的 OpenType GSUB(Glyph SUBstitution)engine 讓呼叫端查詢、驅動並嵌入 OpenType font 宣告的各類 glyph substitution,包括 ligatures、stylistic alternates、contextual variants、Arabic / Indic shaping forms、CJK alternate forms 等。OpenType GSUB LookupType 1 到 8 均已實作,並以 capability-only query surface 形式公開;呼叫端負責驅動文字輸出,並決定要將哪個 substitute glyph 寫入 page content stream

 

Public API

type

  TGSUBStringArray = array of AnsiString;

 

// LookupType 1 - Single Substitution (one glyph -> one glyph)

function GetSingleSubstituteGlyph(InputGID: Word; const FeatureTag: AnsiString): Word;

 

// LookupType 2 - Multiple Substitution (one glyph -> sequence of glyphs)

function GetMultipleSubstituteGlyphs(InputGID: Word; const FeatureTag: AnsiString;

  var OutGIDs: array of Word): Boolean;

 

// LookupType 3 - Alternate Substitution (one glyph -> one of N alternates)

function GetAlternateGlyphCount(InputGID: Word; const FeatureTag: AnsiString): Integer;

function GetAlternateGlyph(InputGID: Word; const FeatureTag: AnsiString;

  AlternateIndex: Integer): Word;

 

// LookupType 4 - Ligature Substitution (N glyphs -> one ligature)

function ApplyLigatureSubstitution(const InputGIDs: array of Word;

  StartIndex: Integer; const FeatureTag: AnsiString;

  out OutGID: Word; out ConsumedCount: Integer): Boolean;

 

// LookupType 5 + 6 - Contextual / Chained Contextual Substitution

function ApplyContextualSubst(const InputGIDs: array of Word;

  StartIndex: Integer; const FeatureTag: AnsiString;

  var OutGIDs: array of Word;

  out ConsumedLen: Integer): Boolean;

 

// LookupType 8 - Reverse Chained Contextual Single Substitution

function ApplyReverseChainedContextualSubst(const InputGIDs: array of Word;

  StartIndex: Integer; const FeatureTag: AnsiString;

  out OutGID: Word): Boolean;

 

// Script / LangSys selection (Phase 7)

procedure SetGSUBScript(const ScriptTag: AnsiString);

procedure SetGSUBLanguage(const LangTag: AnsiString);

function GetGSUBScripts: TGSUBStringArray;

function GetGSUBLanguages(const ScriptTag: AnsiString): TGSUBStringArray;

function GetGSUBFeatures(const ScriptTag, LangTag: AnsiString): TGSUBStringArray;

 

// TTF subsetter closure (Phase 9)

procedure MarkUnicodeGlyphUsed(GID: Word);

 

說明

RegisterUnicodeTTF 解析字型並快取其 GSUB / GDEF / cmap tables 後,engine 便會啟用。每次 substitution query 都會走訪字型的 ScriptList / LangSysList / FeatureList / LookupList chain,並分派到適當的 LookupType handler。上方 12 個 methods 是完整 public surface;其他細節(cmap walk、ScriptList parsing、Coverage table lookup、ClassDef resolution、LookupFlag honor、Extension wrapper unwrapping、SequenceLookupRecord nested dispatch)都位於此 surface 後方RegisterUnicodeTTF has parsed a font and cached its GSUB / GDEF / cmap tables. Each substitution query walks the font's ScriptList / LangSysList / FeatureList / LookupList chain and dispatches to the appropriate LookupType handler. The 12 methods above are the complete public surface; everything else (cmap walk, ScriptList parsing, Coverage table lookup, ClassDef resolution, LookupFlag honor, Extension wrapper unwrapping, SequenceLookupRecord nested dispatch) lives behind that surface.

LookupType matrix

LookupType 1(Single Substitution)- 一個 glyph 對應到一個 substitute。典型 features:salt、ss01-ss20、smcp、onum、以 LookupType 1 連接的 liga,以及透過 GSUB 驅動這些 forms 的字型中的 init / medi / fina / isol Arabic positional forms。使用 GetSingleSubstituteGlyph

LookupType 2(Multiple Substitution)- 一個 glyph 會拆成 substitute glyphs 序列。典型用途:ccmp Glyph Composition / Decomposition(預組 accented Latin letters 拆成 base + combining marks,供後續 mark positioning 使用)。使用 GetMultipleSubstituteGlyphs

LookupType 3(Alternate Substitution)- 一個 glyph 對應到 N 個 alternates 中的一個。典型 features:aalt(Access All Alternates)、以 Type 3 連接的 salt、titl(Titling Alternates),以及字型設計者為每個 slot 提供多個 alternate 時的 ss01-ss20 stylistic sets。使用 GetAlternateGlyphCount + GetAlternateGlyph

LookupType 4 (Ligature Substitution) - N input glyphs fold into one ligature. Canonical features: liga (Standard Ligatures: fi / fl / ffi / ffl), clig (Contextual Ligatures), dlig (Discretionary Ligatures), hlig (Historical Ligatures), rlig (Required Ligatures - Arabic LAM-ALEF and similar), Indic script ligatures (akhn, pres, blws, psts). Use ApplyLigatureSubstitution.GetSingleSubstituteGlyph。

LookupType 2(Multiple Substitution) - 一個 glyph 拆成一連串替代 glyph。典型案例:ccmp Glyph Composition / Decomposition(預先組合的拉丁字母拆成基礎字元 + 結合符,供後續的記號定位使用)。使用 GetMultipleSubstituteGlyphs。

LookupType 3(Alternate Substitution) - 一個 glyph 對應 N 個替代之一。典型 feature:aalt(Access All Alternates)、接成 Type 3 的 salt、titl(Titling Alternates),以及字型設計師為每個槽位提供多個替代時的 ss01-ss20 風格集。使用 GetAlternateGlyphCount + GetAlternateGlyph。

LookupType 4(Ligature Substitution) - N 個輸入 glyph 摺成一個連結字。典型 feature:liga(Standard Ligatures:fi / fl / ffi / ffl)、clig(Contextual Ligatures)、dlig(Discretionary Ligatures)、hlig(Historical Ligatures)、rlig(Required Ligatures - 阿拉伯文 LAM-ALEF 等)、Indic 文字連結字(akhn、pres、blws、psts)。使用 ApplyLigatureSubstitution。

LookupType 5(Contextual Substitution) + LookupType 6(Chained Contextual Substitution) - 比對輸入 glyph 序列,並在比對範圍內的特定位置派發巢狀 lookup。三種 Format 變體(1 字面序列、2 ClassDef 序列、3 Coverage 序列)都已實作;SequenceLookupRecord 分派器會重入 LookupList,並在即時追蹤 MatchPositions 的情況下處理 Single / Multiple / Alternate(第一個)/ Ligature 巢狀 lookup。典型 feature:rclt(Required Contextual Alternates - GSUB 驅動時的阿拉伯文 init/medi/fina/isol)、clig、calt,以及 Indic 塑形的 pres / blws / psts / half / pstf / cjct。使用 ApplyContextualSubst(單一入口同時涵蓋 LookupType 5 與 6)。

LookupType 7(Extension Substitution) - OpenType 規格為替代子表超出 LookupList 16-bit 定址範圍的字型定義的純間接層。每個公開 API 都會透明地跟隨 32-bit Offset32 間接,抵達真正的 LookupType 1 / 2 / 3 / 4 / 5 / 6 / 8 子表。解鎖 GSUB 超過 64 KB 的大型 CJK / Indic 字型(Noto Sans CJK、Noto Sans Devanagari)。沒有獨立 API - 解包裝自動完成。

LookupType 8(Reverse Chained Contextual Single Substitution) - 具上下文感知的 1:1 替代,特點是呼叫端必須以反向掃描順序(尾 -> 頭)套用到多位元組 glyph run,因為每個替代可能依賴尚未被替代的「未來」前視上下文。典型案例:最終形式取決於下一個 glyph 的阿拉伯文 / 敘利亞文 / N'Ko / Indic contextual alternates。使用 ApplyReverseChainedContextualSubst;反向掃描迴圈由呼叫端驅動。

LookupType 5(Contextual Substitution)+ LookupType 6(Chained Contextual Substitution)- 匹配 input glyph sequence,並在 match 內特定位置分派 nested lookups。三種 Format variants(1 literal sequence、2 ClassDef sequence、3 Coverage sequence)均已實作;SequenceLookupRecord dispatcher 會重新進入 LookupList,並透過即時 MatchPositions tracking 處理 Single / Multiple / Alternate(first)/ Ligature nested lookups。典型 features:rclt(Required Contextual Alternates;GSUB 驅動時的 Arabic init/medi/fina/isol)、clig、calt、Indic shaping pres / blws / psts / half / pstf / cjct。使用 ApplyContextualSubst(一個 entry point 同時涵蓋 LookupType 5 與 6)

LookupType 7(Extension Substitution)- OpenType spec 為 substitution subtable 超出 LookupList 16-bit 可達範圍的字型所定義的純 indirection layer。每個 public API 都會透明地跟隨 32-bit Offset32 indirection 到真正的 LookupType 1 / 2 / 3 / 4 / 5 / 6 / 8 subtable。這解除 GSUB 超過 64 KB 的大型 CJK / Indic fonts(Noto Sans CJK、Noto Sans Devanagari)限制。沒有獨立 API;unwrap 會自動進行

LookupType 8 (Reverse Chained Contextual Single Substitution) - context-aware 1:1 substitution whose distinguishing feature is that callers must apply it in REVERSE scan order over a multi-glyph run (end -> start) because each substitute may depend on FUTURE lookahead context that must not have been substituted yet. Canonical use: Arabic / Syriac / N'Ko / Indic contextual alternates whose final form depends on the following glyph. Use DFLT script(或字型宣告的第一個 script)與預設 LangSys。呼叫 SetGSUBScript('latn' / 'arab' / 'cyrl' / 'hani' / 'kana' / 'deva' / 'beng' / 'taml' / 等) 與 SetGSUBLanguage('ENG ' / 'TUR ' / 'AZE ' / 'JAN ' / 'KOR ' / 'ARA ' / 等,以尾端空白補足 4 位元組),即可把查詢固定到特定的 script / language 配對。空字串會還原預設路徑基線。選擇會跨查詢保留,並在 RegisterUnicodeTTF('', nil) 時清除。

 

嚴格與後備語意:未知的 ScriptTag 會讓後續查詢回傳空的無操作結果(呼叫端因此能察覺所選 script 不可用);未知的 LangTag 依 OpenType 慣例退回該 script 的預設 LangSys。GetGSUBScripts / GetGSUBLanguages / GetGSUBFeatures 可列舉已載入字型實際宣告的內容。

 

LookupFlag 與 GDEF

每次查詢都會讀取每個 Lookup 表的 LookupFlag(以及設定 useMarkFilteringSet 時選用的尾端 markFilteringSet uint16),並跳過被標記忽略的輸入 glyph。規格定義的位元都會遵守:ignoreBaseGlyphs(0x0002,跳過 GDEF 類別 1)、ignoreLigatures(0x0004,跳過類別 2)、ignoreMarks(0x0008,跳過類別 3)、useMarkFilteringSet(0x0010),以及高位元組的 markAttachmentType。ClassDef Format 1 與 2 都會解析;GDEF v1.0 / v1.1 / v1.2 檔頭全都接受。沒有 GDEF 表的字型退回「不忽略任何 glyph」,使用無 GDEF 字型的呼叫端因此得到位元組相同的輸出。

 

TTF 子集化閉包(MarkUnicodeGlyphUsed)

HotPDF v2.84.0 的 TTF 子集化器透過 cmap 從 FUnicodeUsedCps 推導已用 glyph 集。GSUB 替代 glyph(風格替代、連結字、上下文變體 - 也就是上面 7 個查詢 API 回傳的一切)通常沒有任何 codepoint 經由 cmap 抵達它們,因此過去子集化器看不見它們,消費端讀取器會轉譯出 .notdef。

 

把 GetSingleSubstituteGlyph / GetMultipleSubstituteGlyphs / GetAlternateGlyph / ApplyLigatureSubstitution / ApplyContextualSubst / ApplyReverseChainedContextualSubst 回傳的任何 GID 輸出進 PDF 文字 stream 之後,請呼叫 ApplyReverseChainedContextualSubst; the caller drives the reverse scan loop.

 

Script / LangSys selection

預設情況下,engine 會偏好 DFLT script(或字型宣告的第一個 script)與 default LangSys。呼叫 SetGSUBScript('latn' / 'arab' / 'cyrl' / 'hani' / 'kana' / 'deva' / 'beng' / 'taml' / etc.) 和 SetGSUBLanguage('ENG ' / 'TUR ' / 'AZE ' / 'JAN ' / 'KOR ' / 'ARA ' / etc.,以 trailing-space 補足 4 bytes),即可將 queries 固定到特定 script / language pair。空字串會還原 default-path baseline。選擇會跨 queries 保留,並在 RegisterUnicodeTTF('', nil) 時清除

 

Strict-vs-fallback semantics:未知 ScriptTag 會讓後續 queries 回傳 empty no-op results(讓呼叫端可偵測所選 script 不可用);未知 LangTag 則依 OpenType convention fallback 到該 script 的 default LangSys。GetGSUBScripts / GetGSUBLanguages / GetGSUBFeatures 會列舉已載入字型實際宣告的內容

 

LookupFlag honor and GDEF

每個 query 都會讀取各 Lookup table 的 LookupFlag(以及設定 useMarkFilteringSet 時選用的尾端 markFilteringSet uint16),並略過標記為 ignore 的 input glyphs。規格定義的 bits 都會遵守:ignoreBaseGlyphs(0x0002,略過 GDEF class 1)、ignoreLigatures(0x0004,略過 class 2)、ignoreMarks(0x0008,略過 class 3)、useMarkFilteringSet(0x0010),以及 high byte markAttachmentType。ClassDef Format 1 + 2 都會解析;GDEF v1.0 / v1.1 / v1.2 headers 都可接受。沒有 GDEF table 的字型會 fallback 到「沒有 glyph 被忽略」,因此使用 GDEF-less fonts 的呼叫端仍可保持 byte-identical output

 

TTF subsetter closure (MarkUnicodeGlyphUsed)

HotPDF 的 v2.84.0 TTF subsetter 會透過 cmap 從 FUnicodeUsedCps 推導 used-glyph set。GSUB substitute glyphs(stylistic alternates、ligatures、contextual variants,也就是上方 7 個 query APIs 回傳的一切)通常沒有可經由 cmap 抵達它們的 codepoint,因此過去對 subsetter 是不可見的,閱讀端會在其位置 render .notdef

 

將 GetSingleSubstituteGlyph / GetMultipleSubstituteGlyphs / GetAlternateGlyph / ApplyLigatureSubstitution / ApplyContextualSubst / ApplyReverseChainedContextualSubst 回傳的任何 GID 輸出到 PDF text stream 後,請對每個已輸出的 GID 呼叫一次 MarkUnicodeGlyphUsed(GID),將它拉入 embedded subset。此 helper 具冪等性且防禦式(out-of-range GIDs 會被靜默丟棄),並與 v2.84.0 composite-glyph closure pass 整合:呼叫端只需要標記 top-level substitute GID,composite components 會自動拉入

 

Typical workflow (Latin small caps)

 

PDF.RegisterUnicodeTTF('myFont', 'C:\\Windows\\Fonts\\arial.ttf');

PDF.SetGSUBScript('latn');

PDF.SetGSUBLanguage('');  // default LangSys

SmallCapGID := PDF.GetSingleSubstituteGlyph(InputGID, 'smcp');

if SmallCapGID <> InputGID then

begin

  // emit SmallCapGID into the page content stream...

  PDF.MarkUnicodeGlyphUsed(SmallCapGID);  // pull into subset

end;MarkUnicodeGlyphUsed 標記每個輸出的替代 GID,並套用字型需要的任何 GPOS / 記號定位。

 

Typical workflow (Arabic LAM-ALEF ligature)

 

PDF.SetGSUBScript('arab');

Run := [LamGID, FathaGID, AlefGID];  // post-cmap GIDsMarkUnicodeGlyphUsed (Phase 9; Phase 8 was a producer-side shaping integration spike and split into 8a-8f for future revisions).

 

請參閱: Arabic / Persian / Urdu Shaping, CFF / OpenType Font Subsetting Functions, THotPDF.EnableFontSubsetting