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, 즉 ligature, stylistic alternate, contextual variant, Arabic / Indic shaping form, CJK alternate form 등을 호출자가 조회하고 구동하며 포함할 수 있게 합니다. OpenType GSUB LookupType 1부터 8까지 모두 구현되어 capability-only query surface로 노출됩니다. 텍스트 방출은 호출자가 구동하며, page content stream에 어떤 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);

 

설명

engine은 RegisterUnicodeTTF가 font를 파싱하고 GSUB / GDEF / cmap table을 캐시한 뒤 활성화됩니다. 각 substitution query는 font의 ScriptList / LangSysList / FeatureList / LookupList 체인을 따라가고 적절한 LookupType handler로 dispatch합니다. 위의 12개 메서드가 전체 public surface이며, 나머지 cmap walk, ScriptList parsing, Coverage table lookup, ClassDef resolution, LookupFlag honor, Extension wrapper unwrapping, SequenceLookupRecord nested dispatch는 모두 그 surface 뒤에 있습니다

 

전체 defensive contract: GSUB table이 없는 font, 4바이트가 아닌 feature tag, 선택한 script / language가 광고하지 않는 feature, 어떤 subtable도 덮지 않는 GID, LookupFlag로 ignore된 input glyph는 모두 안전한 no-op(False / OutGID = InputGID / empty OutGIDs / ConsumedCount = 1)을 반환합니다. 따라서 호출자는 일반적인 "적용할 substitution 없음" 상황에서 예외를 보지 않습니다

 

LookupType matrix

LookupType 1(Single Substitution) - 하나의 glyph가 하나의 substitute로 매핑됩니다. 대표 feature는 salt, ss01-ss20, smcp, onum, LookupType 1로 연결된 liga, 그리고 GSUB로 구동하는 font의 init / medi / fina / isol Arabic positional form입니다. GetSingleSubstituteGlyph를 사용합니다

LookupType 2(Multiple Substitution) - 하나의 glyph가 substitute glyph sequence로 분리됩니다. 대표 사용자는 ccmp Glyph Composition / Decomposition으로, precomposed accented Latin letter를 downstream mark positioning을 위해 base + combining mark로 분해합니다. GetMultipleSubstituteGlyphs를 사용합니다

LookupType 3(Alternate Substitution) - 하나의 glyph가 N개 alternate 중 하나로 매핑됩니다. 대표 feature는 aalt(Access All Alternates), Type 3로 연결된 salt, titl(Titling Alternates), font designer가 slot당 여러 alternate를 제공하는 ss01-ss20 stylistic set입니다. GetAlternateGlyphCountGetAlternateGlyph를 사용합니다

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.

LookupType 5(Contextual Substitution) + LookupType 6(Chained Contextual Substitution) - input glyph sequence를 매칭하고 match 내부 특정 위치에서 nested lookup을 dispatch합니다. 세 가지 Format variant(1 literal sequence, 2 ClassDef sequence, 3 Coverage sequence)를 모두 구현했습니다. SequenceLookupRecord dispatcher는 LookupList에 재진입하고 live MatchPositions 추적으로 Single / Multiple / Alternate(first) / Ligature nested lookup을 처리합니다. 대표 feature는 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) - substitution subtable이 LookupList의 16비트 범위를 넘어가는 font를 위해 OpenType spec이 정의한 순수 indirection layer입니다. 모든 public API는 32비트 Offset32 indirection을 투명하게 따라 실제 LookupType 1 / 2 / 3 / 4 / 5 / 6 / 8 subtable로 이동합니다. GSUB가 64KB를 넘는 무거운 CJK / Indic font(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 ApplyReverseChainedContextualSubst; the caller drives the reverse scan loop.

 

Script / LangSys selection

기본적으로 engine은 DFLT script(또는 font가 선언한 첫 script)와 default LangSys를 선호합니다. SetGSUBScript('latn' / 'arab' / 'cyrl' / 'hani' / 'kana' / 'deva' / 'beng' / 'taml' / etc.)SetGSUBLanguage('ENG ' / 'TUR ' / 'AZE ' / 'JAN ' / 'KOR ' / 'ARA ' / etc.)를 호출하면 query를 특정 script / language pair에 고정할 수 있습니다. language tag는 4바이트가 되도록 trailing-space padded입니다. 빈 문자열은 default-path baseline을 복원합니다. 선택은 query 사이에 유지되며 RegisterUnicodeTTF('', nil)에서 지워집니다

 

strict-vs-fallback 의미: 알 수 없는 ScriptTag는 이후 query가 빈 no-op 결과를 반환하게 하므로 호출자가 선택한 script가 없음을 감지할 수 있습니다. 알 수 없는 LangTag는 OpenType 관례에 따라 해당 script의 default LangSys로 fallback합니다. GetGSUBScripts / GetGSUBLanguages / GetGSUBFeatures는 로드된 font가 실제로 광고하는 항목을 열거합니다

 

LookupFlag honor and GDEF

모든 query는 각 Lookup table의 LookupFlag(useMarkFilteringSet가 설정된 경우 선택적 trailing markFilteringSet uint16 포함)를 읽고 ignore로 표시된 input glyph를 건너뜁니다. spec 정의 bit인 ignoreBaseGlyphs(0x0002, GDEF class 1 skip), ignoreLigatures(0x0004, class 2 skip), ignoreMarks(0x0008, class 3 skip), useMarkFilteringSet(0x0010), high byte markAttachmentType을 준수합니다. ClassDef Format 1과 2를 모두 파싱하고 GDEF v1.0 / v1.1 / v1.2 header를 모두 허용합니다. GDEF table이 없는 font는 "어떤 glyph도 ignore하지 않음"으로 fallback하므로 GDEF-less font를 쓰는 호출자의 출력은 byte-identical로 유지됩니다

 

TTF subsetter closure (MarkUnicodeGlyphUsed)

HotPDF v2.84.0 TTF subsetter는 cmap을 통해 FUnicodeUsedCps에서 used-glyph set을 도출합니다. GSUB substitute glyph(stylistic alternate, ligature, contextual variant 등 위 7개 query API가 반환하는 모든 항목)는 보통 cmap으로 도달하는 codepoint가 없기 때문에 이전에는 subsetter에 보이지 않았고, consumer reader는 그 자리에 .notdef를 렌더링했습니다

 

PDF text stream에 GetSingleSubstituteGlyph / GetMultipleSubstituteGlyphs / GetAlternateGlyph / ApplyLigatureSubstitution / ApplyContextualSubst / ApplyReverseChainedContextualSubst가 반환한 GID를 방출한 뒤에는, 방출된 GID마다 한 번 MarkUnicodeGlyphUsed(GID)를 호출하여 embedded subset에 포함시키십시오. 이 helper는 idempotent이며 defensive하고(out-of-range GID는 조용히 버림), v2.84.0 composite-glyph closure pass와 통합됩니다. 호출자는 top-level substitute GID만 표시하면 되며 composite component는 자동으로 끌려옵니다

 

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;

 

Typical workflow (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 query surface입니다. 즉 "여기에서 GSUB가 무엇을 할 것인가"에 답하지만, Harfbuzz급 layout, Indic cluster-aware reordering, BiDi resolution, GPOS positioning, mark attachment 같은 automatic shaping pipeline을 실행하지 않습니다. 호출자는 scan loop를 구동하고, 어떤 substitute / alternate를 방출할지 선택하며, 방출된 각 substitute GID에 대해 MarkUnicodeGlyphUsed를 호출하고, font가 요구하는 GPOS / mark positioning을 적용해야 합니다

 

Producer-side Arabic / Persian / Urdu shaping (LAM-ALEF mandatory ligature + Arabic Presentation Forms-A) is implemented as a separate built-in pipeline that runs automatically during text emission - see Arabic / Persian / Urdu Shaping.

 

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 matrix closed(Phase 6). v2.119.49 Script / LangSys selection API(Phase 7). v2.119.50 TTF subsetter closure via MarkUnicodeGlyphUsed(Phase 9; Phase 8은 producer-side shaping integration spike였고 이후 revision을 위해 8a-8f로 분리되었습니다)

 

참조: Arabic / Persian / Urdu Shaping, CFF / OpenType Font Subsetting Functions, THotPDF.EnableFontSubsetting