Дизайнер выбирает шрифт с однорядной a для заголовков, или перечёркнутым нулём для таблиц, или набором росчерковых прописных для обложки. Все эти глифы уже содержатся в шрифте. Они просто не являются значениями по умолчанию. Стандартная a отображается через таблицу cmap в один глиф, а её вариант расположен на несколько идентификаторов дальше и доступен только через правило подстановки. Чтобы получить этот вариант в PDF, необходимо прочитать правило и вывести замещающий глиф в поток содержимого. Настоящая статья посвящена чтению подобных правил - подстановок одного глифа - на Object Pascal без использования встроенных библиотек шейпинга
Область рассмотрения намеренно сужена. Стилистические наборы и альтернативы представляют собой подстановки по схеме «один глиф на входе - один на выходе». Именно эта часть разметки OpenType допускает разрешение с помощью небольшого детерминированного обхода таблицы, что делает её подходящей для Pascal-движка, который хочет оставаться независимым от C-зависимостей
Почему чистый Delphi, а не HarfBuzz
HarfBuzz - очевидный ответ на вопрос «как выполнить шейпинг этого текста», и для полноценного двунаправленного, индийского или арабского шейпинга он действительно является правильным выбором. Вместе с тем это C-библиотека. Её интеграция в продукт на Delphi или C++Builder означает необходимость поставлять нативный объект для каждой целевой платформы и архитектуры, согласовывать соглашения о вызовах, следить за ритмом выпусков и изучать условия лицензии на предмет совместимости с собственными. Ни один из этих пунктов сам по себе не составляет труда. Однако в совокупности они создают постоянную нагрузку, которая ничего не даёт, когда реальное требование звучит всего лишь как «дайте мне форму ss01 этой буквы»
Одиночная подстановка не требует движка шейпинга. Ей нужен парсер для нескольких форматов подтаблиц GSUB и один-два бинарных поиска. Реализация этого на Pascal сохраняет весь инструментарий внутри одного компилятора. Честный предел состоит в том, что такой подход обрабатывает только поисковые запросы подстановки глифов и ничего более. Это не двунаправленное разрешение, не переупорядочение для индийских шрифтов и не автоматический контекстный шейпинг. Там, где они необходимы - они необходимы, и запрос одиночной подстановки их не заменит
Иерархия GSUB сверху вниз
Таблица замены глифов организована как цепочка косвенных указателей, и запрос подстановки проходит её сверху вниз. На вершине находится ScriptList. Тег скрипта, например latn, выбирает соответствующую запись, а специальный тег DFLT - это скрипт по умолчанию, применяемый при отсутствии более конкретного совпадения. Запись скрипта указывает на LangSys, языковую систему: с LangSys по умолчанию для общего случая и дополнительными именованными записями для языков, требующих особой обработки. Классический пример - турецкий, где точечная и беспунктная i требуют собственной обработки
LangSys содержит перечень индексов функций. Каждый индекс указывает в FeatureList, где запись функции несёт четырёхбайтовый тег, среди которых ss01, и список индексов поисковых запросов. Эти индексы наконец указывают в LookupList, где находятся фактические подтаблицы подстановки. Таким образом, разрешение ss01 означает: найти скрипт, найти его LangSys, найти функцию с тегом ss01, собрать именуемые ею поисковые запросы и применить их. HotPDF по умолчанию использует скрипт DFLT и LangSys по умолчанию - это именно то, с чем поставляется подавляющее большинство латинских шрифтов, - и предоставляет возможность переопределить тег скрипта в случаях, когда шрифт привязывает свои функции к конкретному скрипту
Таблицы покрытия определяют участников
Каждая подтаблица подстановки начинается с одного и того же вопроса: участвует ли данный входной глиф в этом правиле, и если да - какое место он занимает в собственной индексации правила? Ответ на этот вопрос даёт таблица Coverage, а сам ответ является индексом покрытия - небольшим порядковым номером, который использует остальная часть подтаблицы для поиска замены глифа
Coverage существует в двух форматах. Формат 1 представляет собой список идентификаторов глифов, отсортированных по возрастанию. Глиф находится методом бинарного поиска, а его позиция в списке является его индексом покрытия. Формат 2 представляет собой список диапазонных записей, каждая из которых содержит начальный глиф, конечный глиф и индекс покрытия, на который отображается начальный глиф. Глиф внутри диапазона получает свой индекс покрытия смещением от начала диапазона. Формат 1 компактен, когда участвующие глифы разбросаны; Формат 2 - когда они образуют непрерывные последовательности. Оба отсортированы, поэтому оба допускают поиск за логарифмическое время, и оба возвращают либо индекс покрытия, либо чёткий сигнал «не покрыт», позволяющий движку оставить глиф без изменений
Одиночная подстановка: два формата
Одиночная подстановка - это LookupType 1, отображающий один глиф ровно в одну замену. Она также существует в двух форматах, и разделение является оптимизацией пространства. Формат 1 хранит единственную знаковую дельту. Идентификатор замещающего глифа равен идентификатору входного глифа плюс эта дельта по модулю 65536. Так шрифт кодирует подстановку, при которой каждый участвующий глиф находится на одном фиксированном смещении от своего альтернативного варианта - например, блок прямых цифр, расположенный на постоянном расстоянии от соответствующих изначальных. Таблица Coverage указывает, какие глифы подходят, и одна дельта обслуживает их все
Формат 2 хранит явный массив идентификаторов замещающих глифов. Индекс покрытия из таблицы Coverage является индексом в этом массиве: глиф с индексом покрытия 0 становится первым элементом массива, с индексом 1 - вторым, и так далее. Формат 2 используется, когда альтернативы не находятся на равномерном смещении, что является общим случаем для вручную составленных стилистических наборов. С точки зрения вызывающей стороны запрос одинаков в обоих случаях: взять входной глиф, пропустить через Coverage, и если он покрыт - применить дельту или прочитать элемент массива
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, нет совпадающей функции, нет попадания в Coverage. Это означает, что вызов безопасен при любых условиях. Вы запрашиваете альтернативу, и если её нет - получаете обратно именно то, что передали; вызывающий код никогда не нуждается в специальной обработке шрифта без данной функции
Что означают теги стилистических функций
Тег функции - это весь словарный запас для обозначения того, какую альтернативу вы запрашиваете, и теги, актуальные для стилистической работы, составляют короткий список. Главная пара - это salt (стилистические альтернативы), универсальный доступ к альтернативным формам глифа, и ss01 - ss20, двадцать пронумерованных стилистических наборов, которые шрифт может определять, каждый из которых является именованным пакетом подстановок, сгруппированных дизайнером. Например, шрифт может поместить однорядную a и R с прямой ногой под ss03, так что включение этого набора изменит стиль обоих символов
Вокруг этих тегов расположены ещё несколько тегов одиночной подстановки. aalt - доступ ко всем альтернативам, объединение всех вариантов глифа, обычно представленное как функция цветовой палитры. titl выбирает заглавные буквы для написания, вырезанные для крупных размеров. subs и sups заменяют цифры настоящими подстрочными и надстрочными, а не масштабированными уменьшенными версиями. ordn создаёт порядковые формы, поднятые буквы в словах «1-й» и «2-й». 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 покрывает базовую многоязычную плоскость, первые 65 536 кодовых точек, и этого достаточно для большинства латинских текстов. Но этого недостаточно для кодовых точек от 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 является одним из восьми типов подстановки. Запрос не обрабатывает LookupType 2 (множественная подстановка), где один глиф становится несколькими, а также LookupType 4 (подстановка лигатур), где несколько глифов становятся одним. Он не обрабатывает контекстные и цепочно-контекстные типы, LookupTypes 5 и 6, срабатывающие только тогда, когда глиф появляется в определённом окружении, а также типы расширения и обратной цепочки. Диагональная дробь, конъюнкт деванагари или арабский каскад «начальная-средняя-конечная» форма - это задача последовательности, и поиск одиночной подстановки на уровне глифа не может её выразить
Он также не выполняет автоматического шейпинга. Ничто здесь не проверяет последовательность текста, не решает, какие функции включить, и не применяет их в порядке, требуемом письменностью. Вызывающая сторона выбирает тег функции и применяет её глиф за глифом. Это именно подходящий инструмент для стилистических наборов и альтернатив, которые являются необязательными и локальными, и именно неподходящий инструмент для письменности, требующей переупорядочения. Чёткое соблюдение этой границы позволяет пути подстановки оставаться компактным и предсказуемым
Случаи, требующие работы на уровне последовательностей, рассматриваются в нашей статье о шейпинге текста сложных письменностей в Delphi. Если ваши подстановки являются частью более крупной задачи по подготовке отчётов, включающей размещение изображений и других шрифтов на странице, руководство по выводу отчётов со шрифтами и изображениями объясняет, как эти части сочетаются. Всё это работает на одном и том же движке - HotPDF Component для Delphi и C++Builder, - который несёт запросы подстановки GSUB наряду с API встраивания шрифтов, создания подмножеств и работы с текстом, рассматриваемыми в других статьях этого блога