Техническа статия

Стилистични алтернативи на OpenType GSUB в чист Delphi

Дизайнер избира шрифт с едноетажно a за заглавия, или нула с наклонена черта за таблици, или набор от swash главни букви за корица. Тези глифове вече са в шрифта. Те просто не са по подразбиране. Стойността по подразбиране a се картографира от знака през таблицата cmap до един глиф, а алтернативата седи на няколко id-та на глифове разстояние, достъпна само чрез правило за замяна. Създаването на тази алтернатива в PDF означава прочитане на правилото и излъчване на заместващия глиф в потока от съдържание. Тази статия е за четенето на тези правила, от вида с единична замяна, в Object Pascal без библиотека за нативно оформяне (shaping) отдолу

Обхватът е тесен нарочно. Стилистичните набори и алтернативи са замени от тип един глиф на входа, един глиф на изхода. Те са частта от OpenType оформлението, която можете да разрешите с малко, детерминистично обхождане на таблица, което ги прави подходящи за Pascal двигател, който иска да остане свободен от C зависимости

Защо чист Delphi вместо HarfBuzz

HarfBuzz е очевидният отговор на "оформи този текст" и за пълно двупосочно, индийско или арабско оформяне той е правилният отговор. Той също така е C библиотека. Свързването му в Delphi или C++Builder продукт означава доставяне на нативен обект за всяка целева платформа и архитектура, съвпадение на неговата конвенция за извикване, проследяване на неговия ритъм на издаване и четене на неговите лицензни условия спрямо вашите собствени. Нито едно от тези не е трудно само по себе си. Всичко това е триене, което никога не изчезва, и не купува нищо, когато действителното изискване е „дай ми формата ss01 на тази буква“

Единичната замяна не се нуждае от двигател за оформяне. Тя се нуждае от парсер за шепа формати на подтаблици GSUB и едно или две двоични търсения. Написването на това в Pascal задържа цялата верига от инструменти в един компилатор. Честното ограничение е, че този подход обработва търсения за замяна на глифове и нищо друго. Това не е bidi резолюция, не е индийско пренареждане и не е автоматично контекстуално оформяне. Където са необходими, те са необходими, и заявка за единична замяна няма да ги замести

Йерархията на GSUB, отгоре надолу

Таблицата Glyph Substitution е организирана като верига от индирекции и заявката за замяна обхожда веригата от върха. На върха е ScriptList. Таг за скрипт като latn избира запис, а специалният таг DFLT е скриптът по подразбиране, който се прилага, когато няма съвпадение с по-специфичен скрипт. Записът на скрипта сочи към LangSys, езиковата система, с LangSys по подразбиране за общия случай и незадължителни именувани такива за езици, които се нуждаят от различно поведение. Турският е обичайният пример, където i с точка и без точка изискват своя собствена обработка

LangSys назовава набор от индекси на функции. Всеки индекс сочи към FeatureList, където запис на функция носи четирибайтов таг, сред които ss01, и списък с индекси за търсене. Тези индекси най-накрая сочат към LookupList, където живеят действителните подтаблици за замяна. Така че разрешаването на ss01 означава: намерете скрипта, намерете неговия LangSys, намерете функцията, чийто таг е ss01, съберете търсенията, които назовава, и ги приложете. HotPDF използва по подразбиране скрипта DFLT и LangSys по подразбиране, което е това, което доставя огромното мнозинство от дизайни на латински текст, и излага начин за отмяна на тага на скрипта, когато шрифт свързва своите функции под специфичен скрипт вместо това

Таблиците Coverage решават кой участва

Всяка подтаблица за замяна започва със същия въпрос: участва ли този входен глиф в това правило и ако да, къде седи той в собственото индексиране на правилото. На този въпрос отговаря таблица Coverage и отговорът е индекс на покритие, малко поредно число, което останалата част от подтаблицата използва, за да потърси в какво се превръща глифът

Coverage идва в два формата. Формат 1 е списък от идентификатори на глифове, сортирани във възходящ ред. Намирате глиф с двоично търсене и неговата позиция в списъка е неговият индекс на покритие. Формат 2 е списък от записи на диапазони, всеки от които е начален глиф, краен глиф и индекс на покритие, към който се картографира началният глиф. Глиф вътре в диапазон получава своя индекс на покритие чрез изместване от началото на диапазона. Формат 1 е компактен, когато участващите глифове са разпръснати, Формат 2, когато попадат в съседни серии. И двата са сортирани, така че и двата се претърсват в логаритмично време и двата връщат или индекс на покритие, или чисто "не е покрито", което позволява на двигателя да остави глифа на мира

Единична замяна (Single Substitution), двата формата

Единичната замяна е 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;

Договорът, който си струва да се отбележи, е пропускането (pass-through). GetSingleSubstituteGlyph връща идентификатора на входния глиф непроменен при всяко пропускане: без шрифт, без GSUB таблица, без съвпадаща функция, без попадение в покритието. Това означава, че извикването е безопасно да се направи безусловно. Вие искате алтернативата и ако няма такава, получавате обратно точно това, което сте въвели, така че извикващият код никога не се нуждае от специален случай за шрифт, на който липсва функцията

Какво означават таговете на стилистичните функции

Тагът на функцията е целият речник за това коя алтернатива искате, а таговете, свързани със стилистичната работа, са кратък списък. Водещата двойка е salt, стилистични алтернативи, всеобхватният достъп до алтернативните форми на глифа, и от ss01 до ss20, двадесетте номерирани стилистични набора, които един шрифт може да дефинира, всеки от които е именуван пакет от замени, които дизайнерът групира заедно. Един шрифт може да постави едноетажно a и R с прав крак под ss03, например, така че активирането на този един набор променя стила и на двете

Около тях седят още няколко тага за единична замяна. aalt е достъп до всички алтернативи (access-all-alternates), обединението на всяка алтернатива, която има даден глиф, обикновено представяно като функция на палитрата с глифове. 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), първите 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-та, разгледани на други места в този блог