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 字体声明的各类 glyph substitution,包括 ligatures、stylistic alternates、contextual variants、Arabic / Indic shaping forms、CJK alternate forms 等。OpenType GSUB LookupType 1 到 8 均已实现,并作为 capability-only 查询表面公开;调用方负责驱动文本输出,并决定向页面内容流写入哪个 substitute glyph

 

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 链,并分派到对应 LookupType 处理器。上方 12 个方法就是完整公共表面;其余 cmap walk、ScriptList parsing、Coverage table lookup、ClassDef resolution、LookupFlag honor、Extension wrapper unwrapping 和 SequenceLookupRecord nested dispatch 都隐藏在该表面之后

 

整体防御性契约:没有 GSUB table 的字体、非 4-byte feature tags、所选 script / language 未公开的 feature、没有 subtable 覆盖的 GID、以及被 LookupFlag 忽略的输入 glyph,都会返回安全 no-op(False / OutGID = InputGID / empty OutGIDs / ConsumedCount = 1),因此调用方不会因为常规“没有适用替换”的情况看到异常

 

LookupType matrix

LookupType 1(Single Substitution) - 一个 glyph 映射到一个 substitute。典型 feature 包括 salt、ss01-ss20、smcp、onum、接线为 LookupType 1 的 liga,以及经 GSUB 驱动的字体中的 init / medi / fina / isol 阿拉伯位置形式。使用 GetSingleSubstituteGlyph。

LookupType 2(Multiple Substitution) - 一个 glyph 拆分为一串 substitute glyphs。典型使用者是 ccmp Glyph Composition / Decomposition(预组 accented Latin 字母拆成 base + combining marks,供下游 mark positioning 使用)。使用 GetMultipleSubstituteGlyphs。

LookupType 3(Alternate Substitution)——一个字形映射到 N 个备选字形之一。典型特性: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 - Arabic LAM-ALEF 等)以及 Indic script 连字(akhn、pres、blws、psts)。使用 ApplyLigatureSubstitution。

LookupType 5(Contextual Substitution) + LookupType 6(Chained Contextual Substitution) - 匹配输入 glyph 序列,并在匹配内部的特定位置分派 nested lookups。三种 Format 变体(1 字面序列、2 ClassDef 序列、3 Coverage 序列)均已实现;SequenceLookupRecord dispatcher 会重新进入 LookupList,并通过实时 MatchPositions 跟踪处理 Single / Multiple / Alternate(first)/ Ligature nested lookups。典型 feature 包括 rclt(Required Contextual Alternates - GSUB 驱动时的 Arabic init/medi/fina/isol)、clig、calt,以及 Indic shaping 的 pres / blws / psts / half / pstf / cjct。使用 ApplyContextualSubst(一个入口同时覆盖 LookupType 5 和 6)。

LookupType 7(Extension Substitution) - OpenType 规范为 substitution subtable 位于 LookupList 16-bit 范围之外的字体定义的纯间接层。每个公共 API 都会透明跟随 32-bit Offset32 间接引用到真实 LookupType 1 / 2 / 3 / 4 / 5 / 6 / 8 subtable。该机制解锁 GSUB 超过 64 KB 的重型 CJK / Indic 字体,例如 Noto Sans CJK 和 Noto Sans Devanagari。不需要单独 API,unwrap 自动完成。

LookupType 8(Reverse Chained Contextual Single Substitution) - 上下文感知的 1:1 替换,其特殊之处在于调用方必须对多 glyph run 以反向扫描顺序应用(end -> start),因为每个替换可能依赖尚不能被替换的“未来” lookahead 上下文。典型用途:Arabic / Syriac / N'Ko / Indic 上下文 alternates,其 final form 取决于后续 glyph。使用 ApplyReverseChainedContextualSubst;反向扫描循环由调用方驱动

 

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.,四字节标签用尾随空格补齐) 可将查询固定到特定 script / language pair。空字符串恢复默认路径。选择会跨查询保持,并在 RegisterUnicodeTTF('', nil) 时清除

 

strict 与 fallback 语义:未知 ScriptTag 会使后续查询返回空 no-op 结果,调用方可据此检测所选 script 不可用;未知 LangTag 会按 OpenType 约定回退到该 script 的 default LangSys。GetGSUBScripts / GetGSUBLanguages / GetGSUBFeatures 会枚举已加载字体实际公开的内容

 

LookupFlag honor and GDEF

每次查询都会读取每个 Lookup table 的 LookupFlag,以及 useMarkFilteringSet 置位时可选的尾随 markFilteringSet uint16,并跳过标记为 ignore 的输入 glyph。规范定义的 bit 均被遵守:ignoreBaseGlyphs(0x0002,跳过 GDEF class 1)、ignoreLigatures(0x0004,跳过 class 2)、ignoreMarks(0x0008,跳过 class 3)、useMarkFilteringSet(0x0010)以及高字节 markAttachmentType。ClassDef Format 1 + 2 均会解析;GDEF v1.0 / v1.1 / v1.2 header 均被接受。没有 GDEF table 的字体回退到“没有 glyph 被忽略”,因此使用无 GDEF 字体的调用方输出保持逐字节相同

 

TTF subsetter closure (MarkUnicodeGlyphUsed)

HotPDF v2.84.0 TTF subsetter 通过 cmap 从 FUnicodeUsedCps 派生 used-glyph set。GSUB substitute glyphs(stylistic alternates、ligatures、contextual variants,以及上方 7 个查询 API 返回的全部内容)通常没有 codepoint 可通过 cmap 到达,因此此前 subsetter 看不到它们,消费端阅读器会在其位置渲染 .notdef渲染 .notdef 代替。

 

把 GetSingleSubstituteGlyph / GetMultipleSubstituteGlyphs / GetAlternateGlyph / ApplyLigatureSubstitution / ApplyContextualSubst / ApplyReverseChainedContextualSubst 返回的任何 GID 写入 PDF 文本流之后,对每个已输出的 GID 调用一次 MarkUnicodeGlyphUsed(GID),把它拉入嵌入的子集。该辅助函数是幂等的,带防御性处理(越界的 GID 会被静默丢弃),并与 v2.84.0 的复合字形闭包处理集成:调用方只需标记顶层的替换 GID,复合字形的组件会自动拉入。

 

典型工作流 (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;

 

典型工作流 (Arabic LAM-ALEF ligature)

 

PDF.SetGSUBScript('arab');

Run := [LamGID, FathaGID, AlefGID];  // post-cmap GIDs

if PDF.ApplyLigatureSubstitution(Run, 0, 'rlig', LigGID, ConsumedCount) then

begin

  // emit LigGID + advance by ConsumedCount

  PDF.MarkUnicodeGlyphUsed(LigGID);

end;

 

Scope and limitations

该 engine 是 capability-only 查询表面:它回答“GSUB 在这里会做什么”,但不会运行自动 shaping pipeline(Harfbuzz 级布局、Indic 的 cluster-aware reordering、BiDi resolution、GPOS positioning、mark attachment)。调用方负责驱动扫描循环,选择要输出的 substitute / alternate,为每个输出的 substitute GID 调用 MarkUnicodeGlyphUsed,并应用字体所需的任何 GPOS / mark positioning

 

生产端阿拉伯文 / 波斯文 / 乌尔都语塑形(LAM-ALEF 强制连字 + Arabic Presentation Forms-A)作为独立内置流水线实现,在文本输出期间自动运行,详见 阿拉伯文 / 波斯文 / 乌尔都语塑形.

 

Version trace

v2.119.43 Single Substitution + Phase 1。v2.119.44 Multiple + Alternate(Phase 2)。v2.119.45 Ligature(Phase 3)。v2.119.46 Extension + GDEF + LookupFlag honor(Phase 4)。v2.119.47 Contextual + Chained Contextual + SequenceLookupRecord dispatcher(Phase 5)。v2.119.48 Reverse Chained Contextual,LookupType 1-8 矩阵闭合(Phase 6)。v2.119.49 Script / LangSys selection API(Phase 7)。v2.119.50 通过 MarkUnicodeGlyphUsed 实现 TTF subsetter closure(Phase 9;Phase 8 曾是 producer-side shaping integration spike,后续拆为 8a-8f)

 

另请参阅: Arabic / Persian / Urdu Shaping, CFF / OpenType Font Subsetting Functions, THotPDF.EnableFontSubsetting