Technical Article

Стилистические альтернативы OpenType GSUB в чистом коде Delphi

Дизайнер выбирает шрифт с упрощенным начертанием буквы a для заголовков, перечеркнутым нулем для таблиц или набором декоративных прописных букв для обложки. Все эти глифы уже присутствуют в шрифте, но не используются по умолчанию. Стандартный символ a сопоставляется через таблицу cmap с одним глифом, тогда как альтернативный глиф находится на некотором расстоянии по идентификатору и доступен только по правилу подстановки. Вывод этой альтернативы в PDF требует чтения правила подстановки и записи измененного глифа в поток содержимого. В этой статье рассматривается чтение таких правил (а именно простых одиночных подстановок) на языке Object Pascal без подключения внешних библиотек верстки.

Границы темы очерчены намеренно. Стилистические наборы и альтернативы представляют собой простые подстановки типа «один глиф на входе, один глиф на выходе». Это часть разметки OpenType, которую можно обработать простым детерминированным обходом таблиц, что делает этот подход отличным выбором для движка на Pascal, стремящегося избежать зависимостей от библиотек на C.

Почему чистый код Delphi, а не HarfBuzz

Использование HarfBuzz является очевидным ответом на задачу верстки текста, и для полной поддержки двунаправленного письма, индийских или арабских шрифтов это правильный выбор. Однако это библиотека на C. Интеграция ее в продукт на Delphi или C++Builder означает необходимость поставки скомпилированных файлов под каждую целевую платформу и архитектуру, согласование соглашений о вызовах, отслеживание обновлений и проверку лицензионных условий. По отдельности эти задачи не вызывают сложностей, но вместе они создают постоянную дополнительную нагрузку, не принося пользы, если реальная задача состоит в получении формы ss01 конкретной буквы.

Простая одиночная подстановка не требует полноценного движка верстки. Для нее достаточно парсера нескольких форматов субтаблиц GSUB и пары бинарных поисков. Реализация этого алгоритма на Pascal позволяет держать весь процесс сборки в рамках одного компилятора. Честное ограничение состоит в том, что этот метод обрабатывает только поиск подстановок глифов и ничего больше. Он не поддерживает двунаправленное письмо, переупорядочивание индийских шрифтов или автоматическую контекстную верстку. Там, где эти функции необходимы, без них не обойтись, и простой запрос одиночной подстановки их не заменит.

Иерархия GSUB сверху вниз

Таблица подстановки глифов (Glyph Substitution) организована в виде цепочки перенаправлений, и запрос подстановки проходит по этой цепочке с самого верха. Вверху находится ScriptList. Тег письменности (например, latn) выбирает запись, а специальный тег DFLT служит письменностью по умолчанию, применяемой при отсутствии более точного совпадения. Запись письменности указывает на LangSys (языковую систему) с конфигурацией по умолчанию для общих случаев и дополнительными именованными системами для языков со специфическим поведением. Классический пример - турецкий язык, где символы i с точкой и без требуют особого обращения.

LangSys определяет набор индексов признаков (features). Каждый индекс указывает на FeatureList, где запись признака содержит четырехбайтовый тег (включая ss01) и список индексов таблиц поиска (lookups). Эти индексы указывают на LookupList, где находятся таблицы подстановок. Таким образом, разрешение ss01 означает: найти письменность, найти ее LangSys, найти признак с тегом ss01, собрать указанные таблицы поиска и применить их. HotPDF по умолчанию использует письменность DFLT и стандартный LangSys, что подходит для большинства шрифтов латиницы, но также предоставляет возможность переопределить тег письменности, если возможности шрифта привязаны к конкретной системе.

Таблицы покрытия определяют участников процесса

Каждая субтаблица подстановки начинается с одного вопроса: участвует ли входящий глиф в данном правиле и где именно он расположен в индексации этого правила. Ответ на этот вопрос дает таблица покрытия (Coverage table), возвращающая индекс покрытия - небольшое порядковое число, используемое остальной частью субтаблицы для поиска целевого глифа.

Таблица покрытия существует в двух форматах. Формат 1 представляет собой список идентификаторов глифов, отсортированный по возрастанию. Вы находите глиф с помощью бинарного поиска, и его позиция в списке становится его индексом покрытия. Формат 2 содержит список записей диапазонов, каждая из которых включает начальный глиф, конечный глиф и индекс покрытия, с которым сопоставляется начальный глиф. Глиф внутри диапазона получает свой индекс смещением относительно начала диапазона. Формат 1 эффективен при разрозненном расположении глифов, Формат 2 - при их непрерывной последовательности. Оба формата отсортированы, поэтому поиск в них выполняется за логарифмическое время, возвращая индекс покрытия либо статус отсутствия покрытия, позволяющий движку оставить глиф без изменений.

Одиночная подстановка и два ее формата

Одиночная подстановка относится к LookupType 1 и сопоставляет один глиф ровно с одной заменой. Она также имеет два формата, разделение между которыми служит для оптимизации дискового пространства. Формат 1 сохраняет одно знаковое смещение (дельта). Идентификатор выходного глифа рассчитывается как сумма идентификатора входного глифа и этой дельты по модулю 65536. Так кодируются подстановки, в которых все участвующие глифы имеют одинаковое смещение относительно своих альтернативных вариантов (например, мажорантные цифры, расположенные на фиксированном расстоянии от минускульных). Таблица покрытия определяет подходящие глифы, а одна общая дельта применяется ко всем.

Формат 2 хранит явный массив идентификаторов заменяющих глифов. Индекс из таблицы покрытия служит индексом в этом массиве, то есть глиф с индексом покрытия 0 заменяется первым элементом массива, глиф с индексом 1 заменяется вторым и так далее. Формат 2 применяется, когда альтернативные глифы расположены с разными смещениями, что характерно для созданных вручную стилистических наборов. С точки зрения вызывающего кода запрос в обоих случаях выглядит одинаково: взять входной глиф, прогнать его через таблицу покрытия и, если он найден, применить дельту или прочитать значение из массива.

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, нужный признак или совпадение в таблице покрытия. Благодаря этому вызов можно выполнять безопасно без предварительных проверок. Вы запрашиваете альтернативный глиф и, если его нет, получаете исходный символ, поэтому коду не требуется обрабатывать шрифты без поддержки данной функции как особые случаи.

Значение тегов стилистических признаков

Тег признака задает тип запрашиваемой альтернативы, и список тегов для стилистического оформления довольно короток. Основная пара включает тег salt (стилистические альтернативы для доступа ко всем альтернативным формам глифа) и теги с ss01 по ss20 (двадцать нумерованных стилистических наборов, объединяющих логически связанные подстановки). Например, шрифт может содержать упрощенную a и прямую букву R в наборе ss03, поэтому включение этого набора изменит отображение обоих символов.

Помимо них существует еще несколько тегов одиночных подстановок. Тег aalt (доступ ко всем альтернативам) представляет собой объединение всех возможных альтернатив глифа, часто используемое в палитрах глифов. Тег titl выбирает титульные прописные буквы для крупных кеглей. Теги subs и sups заменяют стандартные цифры на настоящие подстрочные и надстрочные знаки вместо простого масштабирования. Тег ordn создает порядковые числительные (буквы в верхнем регистре для обозначений вроде 1st и 2nd). Тег frac строит дроби, хотя сложные диагональные дроби требуют лигатур и контекстной логики, выходящей за рамки простой одиночной подстановки. В простых случаях механизм аналогичен 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. Запрос подстановки начинается с идентификатора глифа, поэтому путь всегда идет от символа к глифу через cmap, а затем от глифа к альтернативе через GSUB. Особенность таблицы cmap заключается в ее диапазоне охвата. Субтаблица формата 4 охватывает основную многоязычную плоскость (Basic Multilingual Plane, BMP), то есть первые 65536 кодовых точек, чего достаточно для большинства текстов на латинице. Однако ее недостаточно для символов с кодами от U+10000 и выше (дополнительных плоскостей), где располагаются математические символы, эмодзи и специфические виды письменности.

Субтаблица формата 12 охватывает весь диапазон от U+0000 до U+10FFFF. Она представляет собой отсортированный список групп, где каждая группа содержит начальную и конечную кодовые точки, а также начальный идентификатор глифа, благодаря чему непрерывный диапазон символов сопоставляется с непрерывным диапазоном глифов. HotPDF разрешает кодовые точки с помощью гибридного подхода, адаптированного под структуру данных. Символы из плоскости BMP обрабатываются через прямой массив по коду символа, что обеспечивает мгновенный поиск без сканирования. Символы дополнительных плоскостей запрашиваются из разреженной таблицы, отсортированной по кодовым точкам, с помощью бинарного поиска. В результате метод GetUnicodeGlyphForCodepoint принимает полный тип Cardinal и корректно работает во всем диапазоне кодов, возвращая 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 представляет собой лишь один из восьми типов подстановок в OpenType. Этот запрос не поддерживает множественную подстановку LookupType 2 (когда один глиф заменяется несколькими) или подстановку лигатур LookupType 4 (когда несколько глифов объединяются в один). Он также не обрабатывает контекстные правила LookupTypes 5 and 6, которые срабатывают только при наличии определенных соседних символов, а также правила расширения и обратного связывания. Создание диагональных дробей, лигатур деванагари или формирование арабского письма представляют собой задачи упорядочивания последовательностей, которые невозможно решить одиночным посимвольным поиском.

Данный метод также не выполняет автоматическую верстку текста. Он не анализирует текстовый поток, не определяет набор необходимых признаков и не применяет их в требуемом порядке. Вызывающий код сам выбирает тег признака и применяет его посимвольно. Это идеальный инструмент для стилистических наборов и альтернатив, которые выбираются вручную и локально, но он не подходит для систем письма с динамическим изменением порядка символов. Соблюдение этой границы позволяет коду подстановки оставаться компактным и предсказуемым.

Для сценариев, требующих работы на уровне последовательностей символов, сложные алгоритмы верстки рассматриваются в нашей статье о формировании сложного текста в Delphi. Если подстановки являются частью более крупной задачи по генерации отчетов с изображениями и шрифтами, руководство по выводу отчетов со шрифтами и изображениями описывает интеграцию этих компонентов. Все эти возможности реализованы в рамках движка HotPDF Component для Delphi и C++Builder, который поддерживает запросы подстановки GSUB наряду с механизмами внедрения шрифтов, создания их подмножеств и API для работы с текстом.