Technical Article

Стилістичні альтернативи OpenType GSUB на чистому Delphi

Дизайнер вибирає шрифт з одноярусною літерою a для заголовків, перекресленим нулем для таблиць або набором декоративних великих літер (swash capitals) для обкладинки. Ці гліфи вже є у шрифті. Вони просто не є значеннями за замовчуванням. Символ a за замовчуванням відображається через таблицю cmap в один гліф, а альтернативний варіант знаходиться за іншим ідентифікатором гліфа, доступним лише через правило підстановки. Отримання цього альтернативного варіанта в PDF означає зчитування правила та виведення заміненого гліфа в потік вмісту. Ця стаття присвячена зчитуванню цих правил — типу простої підстановки (single-substitution) — в Object Pascal без використання вбудованої бібліотеки формування тексту.

Область дослідження навмисно обмежена. Стилістичні набори та альтернативи — це підстановки типу 'один гліф на вході, один гліф на виході'. Це та частина макета OpenType, яку ви можете вирішити за допомогою невеликого детермінованого обходу таблиці, що робить її чудовим рішенням для двигуна Pascal, який хоче залишатися незалежним від залежностей на мові C.

Чому чистий Delphi, а не HarfBuzz

HarfBuzz є очевидною відповіддю на завдання 'сформувати цей текст', і для повноцінного двоспрямованого формування, індійського або арабського письма це правильне рішення. Але це бібліотека C. Її зв'язування з продуктом Delphi або C++Builder означає постачання нативного об'єкта для кожної цільової платформи та архітектури, узгодження її угод про виклики, відстеження випусків нових версій та вивчення умов ліцензії порівняно з вашими власними. Нічого з цього не є складним саме по собі. Але це створює постійні незручності у розробці та не дає жодної користі, коли реальна вимога полягає лише в отриманні форми ss01 для цієї літери.

Проста підстановка не потребує двигуна формування тексту. Їй потрібен лише парсер для кількох форматів підтаблиць GSUB та один-два двійкових пошуки. Написання цього на Pascal дозволяє тримати весь інструментарій всередині одного компілятора. Чесне обмеження полягає в тому, що цей підхід обробляє лише запити на заміну гліфів і нічого більше. Це не двоспрямоване відображення, не зміна порядку індійських символів і не автоматичне контекстне формування гліфів. Там, де вони дійсно потрібні, вони необхідні, і проста підстановка не зможе їх замінити.

Ієрархія GSUB від верху до низу

Таблиця підстановки гліфів (Glyph Substitution) організована як ланцюжок непрямих посилань, і запит на заміну проходить цей ланцюжок з самого верху. Нагорі знаходиться ScriptList. Тег скрипту, наприклад latn, вибирає запис, а спеціальний тег DFLT є скриптом за замовчуванням, який застосовується, коли немає більш точного збігу. Запис скрипту вказує на LangSys — систему мови, яка містить LangSys за замовчуванням для загальних випадків та додаткові іменовані системи для мов, що вимагають іншої поведінки. Типовим прикладом є турецька мова, де символи i з крапкою та без крапки вимагають власної обробки.

LangSys вказує на набір індексів функцій. Кожен індекс веде до FeatureList, де запис функції містить чотирьохбайтовий тег, зокрема ss01, та список індексів пошуку (lookups). Ці індекси вказують на LookupList, де і знаходяться фактичні підтаблиці підстановки. Отже, розв'язання ss01 означає: знайти скрипт, знайти його LangSys, знайти функцію з тегом ss01, зібрати вказані нею пошуки та застосувати їх. HotPDF за замовчуванням використовує скрипт DFLT та LangSys за замовчуванням, з якими постачається переважна більшість шрифтів для латиниці, і надає можливість перевизначити тег скрипту, якщо шрифт пов'язує свої функції з певним конкретним скриптом.

Таблиці охоплення визначають учасників

Кожна підтаблиця підстановки починається з одного питання: чи бере участь цей вхідний гліф у даному правилі, і якщо так, то де саме він знаходиться у власній індексації правила. На це питання відповідає таблиця охоплення (Coverage table), а відповіддю є індекс охоплення (coverage index) — невелике порядкове число, яке решта підтаблиці використовує для пошуку того гліфа, на який він перетворюється.

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

Одиночне заміщення, два формати

Проста підстановка (Single Substitution) — це тип LookupType 1, і вона відображає один гліф точно в одну заміну. Вона також має два формати, і цей поділ є оптимізацією дискового простору. Формат 1 зберігає одне знакове зміщення (delta). Ідентифікатор вихідного гліфа — це ідентифікатор вхідного гліфа плюс це зміщення за модулем 65536. Саме так шрифт кодує підстановку, коли кожен гліф знаходиться на однаковій відстані від свого альтернативного варіанта, наприклад, блок маюскульних цифр на постійній відстані від відповідних мінускульних цифр. Таблиця охоплення визначає, які гліфи підходять, і одна дельта обслуговує їх усі.

Формат 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 створює порядкові форми, наприклад, підняті літери в англійських скороченнях 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 покриває базову багатомовну площину (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 є лише одним із восьми типів підстановок. Запит не обробляє множинну підстановку LookupType 2, де один гліф перетворюється на кілька, або лігатурну підстановку LookupType 4, де кілька гліфів стають одним. Він не обробляє контекстні типи підстановок та ланцюжки контекстів (LookupTypes 5 та 6), які запускаються лише тоді, коли гліф з'являється у певному оточенні, а також типи розширення та зворотного зв'язку. Діагональний дріб, лігатури деванагарі або каскад початкових-серединних-кінцевих форм в арабському письмі — це проблеми послідовності, і пошук простої підстановки для окремого гліфа не здатний їх виразити.

Він також не виконує автоматичного формування тексту. Ніщо в цьому процесі не аналізує послідовність тексту, не вирішує, які функції увімкнути, і не застосовує їх у порядку, якого вимагає писемність. Користувач сам вибирає тег функції та застосовує його гліф за гліфом. Це саме той інструмент, який потрібен для стилістичних наборів та альтернатив (які є локальними та підключаються за вибором), і абсолютно невідповідний інструмент для писемностей, що вимагають зміни порядку символів. Збереження цієї чіткої межі дозволяє шляху підстановки залишатися невеликим та передбачуваним.

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