Дизайнер избира шрифт с едноетажно a за заглавия, или зачертана нула за таблици, или набор от калиграфски главни букви (swash capitals) за корица. Тези глифове вече са в шрифта. Те просто не са по подразбиране. Глифът по подразбиране за a се съпоставя от знака през таблицата cmap към един глиф, а алтернативата се намира на няколко идентификатора на глифове разстояние, достъпна само чрез правило за заместване. Произвеждането на тази алтернатива в PDF означава прочитане на правилото и извеждане на заместващия глиф в потока от съдържание. Тази статия е за четенето на тези правила - от вида с единично заместване, в Object Pascal, без родна библиотека за оформяне (shaping library) отдолу.
Обхватът е нарочно тесен. Стилистичните набори и алтернативи са замествания "един глиф на входа - един глиф на изхода". Те са онази част от оформянето на OpenType, която можете да разрешите с малко, детерминирано обхождане на таблицата, което ги прави много подходящи за машина на Pascal, която иска да остане свободна от зависимости от C.
Защо чист Delphi вместо HarfBuzz
HarfBuzz е очевидният отговор на въпроса "оформи този текст" и за пълно двупосочно, индийско или арабско оформяне той е правилният отговор. Той обаче е и библиотека на C. Свързването му в продукт на Delphi или C++Builder означава доставка на роден обект за всяка целева платформа и архитектура, съобразяване с конвенцията му за извикване, проследяване на версиите му и четене на лицензионните му условия спрямо вашите собствени. Нищо от това не е трудно само по себе си. Всичко това обаче е триене, което никога не изчезва, и не носи нищо, когато действителното изискване е просто "дай ми формата ss01 на тази буква".
Единичното заместване не се нуждае от машина за оформяне. Нуждае се от парсер за няколко формата на подтаблици на GSUB и едно-две двоични търсения. Написването на това на Pascal запазва целия инструментариум в рамките на един компилатор. Честното ограничение е, че този подход се справя с търсенето на замествания на глифове и нищо друго. Това не е двупосочно (bidi) разрешаване, не е пренареждане на индийски скриптове и не е автоматично контекстно оформяне. Когато те са необходими, те са необходими, и заявката за единично заместване няма да ги замени.
Йерархията на GSUB, отгоре надолу
Таблицата за заместване на глифове (Glyph Substitution table) е организирана като верига от косвени връзки, а заявката за заместване обхожда веригата от самото начало. Най-отгоре е ScriptList. Етикет за скрипт (script tag) като latn избира запис, а специалният етикет DFLT е скриптът по подразбиране, който се прилага, когато никой по-специфичен скрипт не съвпада. Записът за скрипт сочи към LangSys - езиковата система, с LangSys по подразбиране за общия случай и незадължителни именовани системи за езици, които се нуждаят от различно поведение. Турският е обичайният пример, където буквата i с точка и без точка изисква собствена обработка.
LangSys идентифицира набор от индекси на функции. Всеки индекс сочи към FeatureList, където записът за функция носи четирибайтов етикет (сред тях и ss01) и списък с индекси за търсене. Тези индекси накрая сочат към LookupList, където живеят действителните подтаблици за заместване. Така че разрешаването на ss01 означава: намиране на скрипта, намиране на неговата LangSys езикова система, намиране на функцията, чийто етикет е ss01, събиране на търсенията, които тя именува, и прилагането им. HotPDF използва по подразбиране скрипта DFLT и LangSys по подразбиране, с които се доставя по-голямата част от латинските текстови дизайни, и излага начин за замяна на етикета за скрипт, когато шрифтът свързва функциите си под конкретен скрипт.
Таблиците с покритие (Coverage tables) решават кой участва
Всяка подтаблица за заместване започва с един и същ въпрос: участва ли този входен глиф в това правило и ако е така, къде се намира в собственото индексиране на правилото. Отговорът на този въпрос се дава от таблица с покритие (Coverage table), а отговорът е индекс на покритие (coverage index) - поредно число, което останалата часть от подтаблицата използва, за да потърси в какво се превръща глифът.
Покритието се предлага в два формата. Формат 1 е списък с идентификатори на глифове, сортирани във възходящ ред. Намирате глиф с двоично търсене и позицията му в списъка е неговият индекс на покритие. Формат 2 е списък със записи за диапазони, всеки с начален глиф, краен глиф и индекс на покритие, към който се съпоставя началният глиф. Глиф в рамките на диапазон получава своя индекс на покритие при отместване от началото на диапазона. Формат 1 е компактен, когато участващите глифове са разпръснати, а Формат 2 - когато попадат в непрекъснати последователности. И двата формата са сортирани, така че и двата се търсят за логаритмично време, и двата връщат или индекс на покритие, или чисто "не е покрит", което позволява на машината да остави глифа на мира.
Единично заместване, двата формата
Единичното заместване (Single Substitution) е LookupType 1 и съпоставя един глиф към точно един негов заместник. То също има два формата, като разделението е оптимизация на пространството. Формат 1 съхранява единична делта със знак (signed delta). Идентификаторът на изходния глиф е идентификаторът на входния глиф плюс тази делта, по модул 65536. Ето как даден шрифт кодира заместване, при което всеки участващ глиф се намира на едно и също фиксирано отместване от своя алтернативен вариант, например блок от подравнени цифри (lining figures), разположени на постоянно разстояние от съответните цифри в стар стил (oldstyle figures). Таблицата 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 е достъп до всички алтернативи (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;
Формат 12 на cmap и допълнителните равнини
Преди да може да се изпълни каквото и да е заместване, даден знак трябва да се превърне в глиф и това е задача на таблицата cmap. Заявката за заместване започва от идентификатор на глиф, така че пътят винаги е от знак към глиф през cmap, а след това от глиф към алтернатива през GSUB. Интересната част на cmap е нейният обхват. Подтаблица от формат 4 покрива основната многоезична равнина (Basic Multilingual Plane - BMP) - първите 65536 кодови точки, и това е достатъчно за повечето латински текстове. Това обаче не е достатъчно за кодови точки от U+10000 нагоре - допълнителните равнини (supplementary planes), където сега живеят математически буквено-цифрови символи, много други символи и няколко живи скрипта.
Формат 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 за вграждане на шрифтове, подмножества и текст, разгледани на други места в този блог.