OpenType GSUB Substitution Engine

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

 

Arabic Shaping  CFF / OpenType Subsetting

GSUB-движок OpenType (Glyph SUBstitution) внутри HotPDF позволяет вызывающим запрашивать, вести и встраивать любые подстановки глифов, какие объявляет шрифт OpenType — лигатуры, стилистические альтернативы, контекстные варианты, формы арабского / Indic шейпинга, альтернативные формы CJK и так далее. Реализован и открыт каждый OpenType GSUB LookupType с 1-го по 8-й как поверхность «только запрос возможностей»; вывод текста ведёт вызывающий и сам решает, какой substitute-глиф писать в контентный поток страницы.

 

Публичный API

type

TGSUBStringArray = array of AnsiString;

 

// LookupType 1 — Single Substitution (один глиф -> один глиф)

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

 

// LookupType 2 — Multiple Substitution (один глиф -> последовательность глифов)

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

  var OutGIDs: array of Word): Boolean;

 

// LookupType 3 — Alternate Substitution (один глиф -> один из N альтернативных)

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

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

AlternateIndex: Integer): Word;

 

// LookupType 4 — Ligature Substitution (N глифов -> одна лигатура)

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 (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-сабсеттера (Phase 9)

procedure MarkUnicodeGlyphUsed(GID: Word);

 

Описание

Движок активируется после того, как RegisterUnicodeTTF разберёт шрифт и закэширует его таблицы GSUB / GDEF / cmap. Каждый запрос подстановки идёт по цепочке ScriptList / LangSysList / FeatureList / LookupList шрифта и диспетчеризуется подходящему обработчику LookupType. Двенадцать методов выше — полный публичный охват; всё остальное (обход cmap, разбор ScriptList, поиск в Coverage, разрешение ClassDef, почтение LookupFlag, разворачивание Extension-обёрток, вложенная диспетчеризация SequenceLookupRecord) живёт за этой поверхностью.

 

Защитный контракт везде: шрифты без таблицы GSUB, теги фич не из 4 байтов, фичи, которых не объявляет выбранный скрипт / язык, GID вне покрытия сабтаблиц и игнорируемые по LookupFlag входные глифы — всё возвращает безопасный no-op (False / OutGID = InputGID / пустые OutGIDs / ConsumedCount = 1), так что вызывающие никогда не видят исключений в рутинных случаях «подстановка не применилась».

 

Матрица LookupType

LookupType 1 (Single Substitution) — один глиф отображается в один substitute. Канонические фичи: salt, ss01-ss20, smcp, onum, liga при подключении через LookupType 1, плюс init / medi / fina / isol арабские позиционные формы в шрифтах, ведущих их через GSUB. Используйте GetSingleSubstituteGlyph.

LookupType 2 (Multiple Substitution) — один глиф разбивается на последовательность substitute-глифов. Канонический потребитель: ccmp Glyph Composition / Decomposition (прекомпозированные латинские буквы с акцентами разбиваются на базу + комбинируемые знаки для последующего позиционирования знаков). Используйте GetMultipleSubstituteGlyphs.

LookupType 3 (Alternate Substitution) — один глиф отображается в один из N альтернативных. Канонические фичи: aalt (Access All Alternates), salt при разводке как Type 3, titl (Titling Alternates), ss01-ss20 стилистические наборы, когда дизайнер шрифта даёт больше одной альтернативы на слот. Используйте GetAlternateGlyphCount + GetAlternateGlyph.

LookupType 4 (Ligature Substitution) — N входных глифов сворачиваются в одну лигатуру. Канонические фичи: 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) — сопоставляет последовательность входных глифов и диспетчеризует вложенные lookups в конкретных позициях внутри совпадения. Реализованы все три варианта Format (1 литеральная последовательность, 2 последовательность ClassDef, 3 последовательность Coverage); диспетчер SequenceLookupRecord повторно входит в LookupList и обрабатывает вложенные lookups Single / Multiple / Alternate (первый) / Ligature с живым отслеживанием MatchPositions. Канонические фичи: rclt (Required Contextual Alternates — арабские init/medi/fina/isol при ведении через GSUB), clig, calt, Indic-шейпинг pres / blws / psts / half / pstf / cjct. Используйте ApplyContextualSubst (одна точка входа покрывает и LookupType 5, и 6).

LookupType 7 (Extension Substitution) — чистый слой косвенности, который спецификация OpenType определяет для шрифтов, чья сабтаблица подстановок лежит за 16-битной досягаемостью LookupList. Каждый публичный API прозрачно идёт по 32-битной косвенности Offset32 к настоящей сабтаблице LookupType 1 / 2 / 3 / 4 / 5 / 6 / 8. Снимает блокировку с тяжёлых CJK / Indic шрифтов (Noto Sans CJK, Noto Sans Devanagari), чей GSUB превышает 64 KB. Отдельного API нет — разворачивание автоматически.

LookupType 8 (Reverse Chained Contextual Single Substitution) — контекстно-зависимая подстановка 1:1, чья особенность в том, что применять её нужно в ОБРАТНОМ порядке сканирования по run-у из нескольких глифов (конец -> начало), потому что каждая подстановка может зависеть от ПОСЛЕДУЮЩЕГО lookahead-контекста, который ещё не должен быть подставлен. Каноническое применение: контекстные альтернативы арабского / сирийского / N'Ko / Indic, чья финальная форма зависит от следующего глифа. Используйте ApplyReverseChainedContextualSubst; обратный цикл сканирования ведёт вызывающий.

 

Выбор Script / LangSys

По умолчанию движок предпочитает скрипт DFLT (или первый объявленный шрифтом) и LangSys по умолчанию. Вызывайте SetGSUBScript('latn' / 'arab' / 'cyrl' / 'hani' / 'kana' / 'deva' / 'beng' / 'taml' / etc.) и SetGSUBLanguage('ENG ' / 'TUR ' / 'AZE ' / 'JAN ' / 'KOR ' / 'ARA ' / etc., trailing-space padded to 4 bytes), чтобы прибить запросы к конкретной паре скрипт / язык. Пустая строка возвращает базовую линию пути по умолчанию. Выборы переживают запросы и сбрасываются при RegisterUnicodeTTF('', nil).

 

Семантика строгости-против-фолбэка: неизвестный ScriptTag заставляет последующие запросы возвращать пустые no-op результаты (так вызывающие могут понять, что выбранный скрипт недоступен); неизвестный LangTag по конвенции OpenType откатывается к LangSys скрипта по умолчанию. GetGSUBScripts / GetGSUBLanguages / GetGSUBFeatures перечисляют, что загруженный шрифт реально объявляет.

 

Почтение LookupFlag и GDEF

Каждый запрос читает LookupFlag каждой таблицы Lookup (и опциональный замыкающий uint16 markFilteringSet, когда useMarkFilteringSet установлен) и пропускает помеченные к игнорированию входные глифы. Определённые спецификацией биты чтятся: ignoreBaseGlyphs (0x0002, пропуск класса 1 GDEF), ignoreLigatures (0x0004, пропуск класса 2), ignoreMarks (0x0008, пропуск класса 3), useMarkFilteringSet (0x0010) и старший байт markAttachmentType. Разбираются и ClassDef Format 1, и 2; принимаются заголовки GDEF v1.0 / v1.1 / v1.2. Шрифты без таблицы GDEF откатываются к «ни один глиф не игнорируется», так что вывод остаётся байт-в-байт для вызывающих со шрифтами без GDEF.

 

Замыкание TTF-сабсеттера (MarkUnicodeGlyphUsed)

TTF-сабсеттер HotPDF v2.84.0 выводит набор использованных глифов из FUnicodeUsedCps через cmap. Substitute-глифы GSUB (стилистические альтернативы, лигатуры, контекстные варианты — всё, что возвращают 7 query API выше) обычно не имеют кодовой точки, ведущей к ним через cmap, так что раньше они были невидимы сабсеттеру, и читающая сторона рисовала на их месте .notdef.

 

После вывода любого GID, возвращённого GetSingleSubstituteGlyph / GetMultipleSubstituteGlyphs / GetAlternateGlyph / ApplyLigatureSubstitution / ApplyContextualSubst / ApplyReverseChainedContextualSubst в текстовый поток PDF, вызывайте MarkUnicodeGlyphUsed(GID) по одному разу на выведенный GID, чтобы втянуть его во встраиваемый сабсет. Хелпер идемпотентен, защищён (GID вне диапазона молча отбрасываются) и интегрирован с проходом замыкания композитных глифов v2.84.0: вызывающим достаточно пометить substitute GID верхнего уровня — компоненты композитов подтягиваются сами.

 

Типичный рабочий процесс (латинские малые заглавные)

 

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

PDF.SetGSUBScript('latn');

PDF.SetGSUBLanguage('');// LangSys по умолчанию

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

if SmallCapGID <> InputGID then

begin

  // вывести SmallCapGID в контентный поток страницы...

PDF.MarkUnicodeGlyphUsed(SmallCapGID);// втянуть в сабсет

end;

 

Типичный рабочий процесс (арабская лигатура LAM-ALEF)

 

PDF.SetGSUBScript('arab');

Run := [LamGID, FathaGID, AlefGID];// GID после cmap

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

begin

  // вывести LigGID + продвинуться на ConsumedCount

PDF.MarkUnicodeGlyphUsed(LigGID);

end;

 

Область применения и ограничения

Движок — поверхность «только запрос возможностей»: он отвечает на вопрос «что здесь сделал бы GSUB», но не гоняет автоматический шейпинг-конвейер (вёрстка уровня HarfBuzz, кластерно-осведомлённая перестановка для Indic, разрешение BiDi, позиционирование GPOS, привязка знаков). За ведение цикла сканирования, выбор выводимого substitute / alternate, вызовы MarkUnicodeGlyphUsed для каждого выведенного substitute GID и применение любого требуемого шрифтом позиционирования GPOS / знаков отвечает вызывающий.

 

Producer-side шейпинг арабского / персидского / урду (обязательная лигатура LAM-ALEF + Arabic Presentation Forms-A) реализован отдельным встроенным конвейером, выполняющимся автоматически при выводе текста — см. Arabic / Persian / Urdu Shaping.

 

Трассировка версий

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 (Phase 4). v2.119.47 — Contextual + Chained Contextual + диспетчер SequenceLookupRecord (Phase 5). v2.119.48 — Reverse Chained Contextual, матрица LookupType 1-8 закрыта (Phase 6). v2.119.49 — API выбора Script / LangSys (Phase 7). v2.119.50 — замыкание TTF-сабсеттера через MarkUnicodeGlyphUsed (Phase 9; Phase 8 была спайком интеграции producer-side шейпинга и разбита на 8a-8f для будущих ревизий).

 

См. также: Arabic / Persian / Urdu Shaping, CFF / OpenType Font Subsetting Functions, THotPDF.EnableFontSubsetting