Дизайнер обирає шрифт з однорівневим a для заголовків, або перекресленим нулем для таблиць, або набором рядкових ініціальних накреслень для обкладинки. Ці гліфи вже є у шрифті. Просто вони не є типовими. Типова a відображається через таблицю cmap на один гліф, а варіант знаходиться через кілька ідентифікаторів гліфів далі, доступний лише через правило підстановки. Відтворення цього варіанту в PDF означає читання правила та відображення замінника гліфа в потоці вмісту. Ця стаття присвячена читанню таких правил – правил одиничної підстановки – на Object Pascal без підлеглої бібліотеки шейпінгу
Обсяг навмисно вузький. Стилістичні набори та варіанти – це підстановки «один гліф на вхід, один гліф на виході». Це та частина розкладки OpenType, яку можна вирішити за допомогою невеликого детермінованого обходу таблиці, що робить їх придатними для рушія Pascal, який прагне залишатися без залежностей від C
Чому чистий Delphi, а не HarfBuzz
HarfBuzz – очевидна відповідь на питання «зформуй цей текст», і для повного двонаправленого, індійського або арабського шейпінгу це правильна відповідь. Це також бібліотека C. Підключення її до продукту Delphi або C++Builder означає постачання нативного об'єкта для кожної цільової платформи та архітектури, узгодження угоди виклику, відстеження ритму випусків і перевірку умов ліцензії щодо власних. Нічого з цього не складно окремо. Разом це тертя, яке ніколи не зникає, і воно нічого не додає, якщо реальна вимога полягає в «дай мені форму ss01 цієї літери»
Одинична підстановка не потребує рушія шейпінгу. Їй потрібен парсер для декількох форматів підтаблиць GSUB та кілька бінарних пошуків. Написання цього на Pascal тримає весь ланцюжок інструментів усередині одного компілятора. Чесне обмеження полягає в тому, що цей підхід обробляє лише пошукові запити підстановки гліфів і більше нічого. Це не розв'язання bidi, не переупорядкування Devanagari і не автоматичний контекстний шейпінг. Там, де вони потрібні, вони потрібні, і одиничний запит підстановки не замінить їх
Ієрархія 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, немає відповідної функції, немає попадання в покриття. Це означає, що виклик безпечно здійснювати безумовно. Ви запитуєте варіант, і якщо його немає, отримуєте назад саме те, що ввели, тому код виклику ніколи не потребує особливої обробки шрифту, у якого відсутня ця функція
Що означають теги стилістичних функцій
Тег функції – це весь словник того, який варіант ви запитуєте, і теги, що стосуються стилістичної роботи, складають короткий список. Основна пара – це 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 охоплює Базову багатомовну площину, перші 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 – це один із восьми типів підстановки. Запит не обробляє LookupType 2 – множинну підстановку, де один гліф стає кількома, ні LookupType 4 – лігатурну підстановку, де кілька гліфів стають одним. Він не обробляє контекстні та ланцюгово-контекстні типи – LookupTypes 5 та 6, що спрацьовують лише тоді, коли гліф з'являється в певному оточенні, ні типи розширення та зворотного ланцюга. Діагональний дріб, сполучний знак деванагарі або арабський початково-медіально-кінцевий каскад – це проблема послідовності, і пошук одиничної підстановки по гліфу не може її висловити
Він також не виконує автоматичний шейпінг. Тут ніщо не перевіряє серію тексту, не вирішує, які функції ввімкнути, і не застосовує їх у порядку, якого вимагає скрипт. Викликач вибирає тег функції та застосовує його гліф за гліфом. Це саме правильний інструмент для стилістичних наборів та варіантів, які є необов'язковими і локальними, і саме невірний інструмент для скрипту, що потребує переупорядкування. Чіткість меж дозволяє шляху підстановки залишатися малим і передбачуваним
Для випадків, що потребують роботи на рівні послідовності, складна скриптова частина розглядається в нашій статті про шейпінг тексту зі складними скриптами в Delphi. Якщо ваші підстановки є частиною більшого завдання з формування звітів, що також розміщує зображення та інші шрифти на сторінці, посібник з виводу звітів зі шрифтами та зображеннями охоплює, як ці частини поєднуються. Все це працює на одному рушії – HotPDF Component для Delphi та C++Builder, що несе запити підстановки GSUB разом із API вбудовування шрифтів, підмножин та тексту, розглянутими в інших матеріалах цього блогу