Designér vybere písmo s jednoúrovňovým a pro nadpisy, přeškrtnutou nulou pro tabulky nebo sadou zdobných verzálek (swash capitals) pro obálku. Tyto glyfy se v písmu již nacházejí. Akorát nejsou výchozí volbou. Výchozí a se mapuje ze znaku přes tabulku cmap na jeden glyf, zatímco alternativa leží o několik ID glyfů dále, přístupná pouze prostřednictvím substitučního pravidla. Získat tuto alternativu do PDF znamená načíst pravidlo a vygenerovat náhradní glyf do obsahového toku (content stream). Tento článek pojednává o tom, jak číst tato pravidla – konkrétně pro jednoduchou substituci – v Object Pascalu bez závislosti na nativní knihovně pro shaping (tvarování textu)
Záběr je záměrně úzký. Stylistické sady (stylistic sets) a alternativy jsou substituce typu jeden-glyf-dovnitř, jeden-glyf-ven. Jsou to ty části OpenType layoutu, které dokážete vyřešit pomocí krátkého deterministického procházení tabulek, což z nich dělá ideální cíl pro Pascal engine, který se chce vyhnout závislostem v jazyce C
Proč čisté Delphi místo HarfBuzz
HarfBuzz je zřejmá odpověď na otázku „natvaruj tento text“, a pro plně obousměrný text, indické nebo arabské tvarování je to ta správná odpověď. Je to ovšem knihovna v C. Její napojení do Delphi nebo C++Builderu znamená dodávat nativní objekt pro každou cílovou platformu a architekturu, ladit calling konvence, sledovat její tempo vydávání nových verzí a porovnávat její licenční podmínky s těmi vašimi. V izolaci není nic z toho těžké. Ale všechno dohromady to tvoří tření, které nikdy nezmizí, a přitom to nepřináší nic navíc, když aktuálním požadavkem je pouze: „dej mi ss01 formu tohoto písmene“
Jednoduchá substituce nepotřebuje engine pro tvarování. Potřebuje parser pro hrstku formátů podtabulek (subtables) GSUB a jedno nebo dvě binární vyhledávání. Zapsání tohoto kódu v Pascalu udrží celou vývojářskou vrstvu v jednom kompilátoru. Omezením – abychom byli upřímní – je, že tento přístup řeší pouze jednodouchou substituci glyfů a nic jiného. Neřeší bidi směrování, indické přeskupování znaků ani automatické kontextuální tvarování. Kde jsou tyto funkce potřeba, tam zkrátka chybí, a pouhý dotaz na substituci jednoho znaku je nenahradí
Hierarchie GSUB, shora dolů
Tabulka Glyph Substitution (GSUB) je uspořádána jako řetězec nepřímých odkazů a substituční dotaz prochází řetězcem odshora. Nahoře se nachází ScriptList. Skriptová značka, například latn, vybírá odpovídající položku; k dispozici je také speciální značka DFLT, což je výchozí skript, který se použije v případě, že žádný specifičtější skript nevyhovuje. Skriptová položka odkazuje na LangSys, jazykový systém, s výchozím LangSys pro běžné použití a případnými pojmenovanými systémy pro jazyky, které vyžadují odlišné chování. Obvyklým příkladem je turečtina, kde tečkované a beztečkové i vyžaduje vlastní zpracování
LangSys jmenuje sadu indexů vlastností (feature indices). Každý index ukazuje do FeatureList, kde záznam (feature record) nese čtyřbajtovou značku, například ss01, a seznam indexů vyhledávání (lookup indices). Tyto indexy konečně ukazují do LookupList, kde žijí samotné substituční podtabulky. Vyřešení ss01 tedy znamená: najít skript, najít jeho LangSys, najít feature, jejíž značka je ss01, posbírat lookupy, které jmenuje, a aplikovat je. HotPDF se ve výchozím nastavení spoléhá na skript DFLT a výchozí LangSys, což je nastavení, které dodává naprostá většina latinských písem, a poskytuje způsob, jak značku skriptu přepsat, když písmo namísto toho svoje sady umisťuje pod konkrétní skript
Coverage tabulky rozhodují, kdo se účastní
Každá substituční podtabulka začíná stejnou otázkou: účastní se tento vstupní glyf daného pravidla, a pokud ano, jaké je jeho místo v indexování pravidla samotného? Odpovědí na tuto otázku je tabulka Coverage a jejím výsledkem je coverage index (index pokrytí), malé pořadové číslo, které zbytek podtabulky používá k dohledání, čím se daný glyf stane
Coverage se dodává ve dvou formátech. Formát 1 je seznam ID glyfů setříděný vzestupně. Glyf se najde pomocí binárního vyhledávání a jeho pozice v seznamu je zároveň jeho coverage indexem. Formát 2 je seznam záznamů rozsahů (range records), přičemž každý obsahuje počáteční glyf, koncový glyf a coverage index, na který se počáteční glyf mapuje. Glyf uvnitř rozsahu získá svůj coverage index tak, že se k počátku rozsahu přičte jeho odstup. Formát 1 je kompaktní, když jsou zúčastněné glyfy rozptýlené, Formát 2 naopak, když spadají do souvislých běhů. Oba formáty jsou setříděné, takže se oba dají prohledávat v logaritmickém čase a oba vracejí buď coverage index, nebo čisté „nepokryto“, což enginu umožňuje glyf prostě přeskočit a neměnit
Jednoduchá substituce (Single Substitution) a její dva formáty
Single Substitution odpovídá LookupType 1 a mapuje jeden glyf přesně na jednu náhradu. Má rovněž dva formáty a jejich dělení je otázkou optimalizace prostoru. Formát 1 ukládá jedinou signovanou deltu. ID výstupního glyfu se rovná ID vstupního glyfu plus tato delta, modulo 65536. Takto písmo kóduje substituci v situaci, kdy každý účastnící se glyf leží ve stejném fixním odstupu od své alternativy, například blok proporčních číslic umístěný ve stejné vzdálenosti od odpovídajících skákavých (oldstyle) číslic. Tabulka Coverage říká, které glyfy vyhovují, a jediná delta se aplikuje na všechny z nich
Formát 2 uchovává explicitní pole náhradních ID glyfů. Index pokrytí z tabulky Coverage poslouží jako index do tohoto pole, takže glyf s coverage indexem 0 se stane prvním záznamem v poli, coverage index 1 se stane druhým a tak dále. Formát 2 se používá, když alternativy neleží na jednotném odstupu, což je běžný případ ručně sestavovaných stylistických sad. Z pohledu volajícího je ale dotaz stejný v obou případech. Převezmi vstupní glyf, prožeň ho přes Coverage, a pokud spadá do pokrytí, aplikuj deltu nebo si přečti slot z pole
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;
Smlouva (contract), které stojí za to si všimnout, je chování při minutí (pass-through). GetSingleSubstituteGlyph vrátí vstupní ID glyfu nezměněné v každém případě, kdy nedojde ke shodě: ať už písmo nebo tabulka GSUB neexistují, feature nebyla nalezena, nebo v pokrytí došlo k minutí. To znamená, že volání můžete bezpečně používat bezpodmínečně. Požádáte o alternativu, a pokud žádná není, dostanete zpět přesně to, co jste zadali, takže volající kód nikdy nemusí speciálně řešit případ písma, kterému daná vlastnost chybí
Co znamenají stylistické feature značky
Feature značka představuje celý slovník definující, o kterou alternativu žádáte, a značky relevantní pro stylistickou práci jsou součástí krátkého seznamu. K těm nejdůležitějším patří salt, stylistické alternativy (stylistic alternates) tvořící univerzální přístup k alternativním tvarům glyfu, a ss01 až ss20, tedy dvacet očíslovaných stylistických sad (stylistic sets), které může písmo definovat, přičemž každá z nich představuje pojmenovaný balík substitucí, jež návrhář seskupuje dohromady. Písmo může například zahrnout jednoúrovňové a a rovné písmeno R pod sadu ss03, takže povolením jedné sady změníte styl obou
Okolo nich existuje ještě několik značek pro jednoduchou substituci. aalt (access-all-alternates) představuje sjednocení všech alternativ, které glyf má, obvykle prezentované formou palety. titl vybírá takzvané titling kapitálky vyřezané speciálně pro velké velikosti textu. subs a sups prohodí glyfy za skutečné dolní a horní indexy namísto pouhých zmenšených výchozích variant. ordn vytváří ordinální formy (zvýšená písmena například u 1st a 2nd). frac dokáže sestavovat zlomky, i když plnohodnotné zlomky s lomítkem se také spoléhají na ligatury a kontextuální logiku jdoucí nad rámec obyčejné substituce. Ale pro jednopísmenné zlomky je mechanismus identický s ss01: předat značku do substitučního dotazu a načíst náhradní glyf
// 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;
Formát cmap 12 a doplňkové roviny (supplementary planes)
Před jakoukoli substitucí se ze znaku musí nejprve stát glyf a o to se stará tabulka cmap. Substituční dotaz začíná s ID glyfu, takže cesta je vždy ze znaku na glyf prostřednictvím tabulky cmap a teprve poté z glyfu na alternativu prostřednictvím GSUB. Zajímavou částí na tabulce cmap je její rozsah. Podtabulka Formátu 4 pokrývá Basic Multilingual Plane, což je prvních 65536 kódových bodů, a to pro většinu latinského textu postačuje. Nestačí to však pro kódové body od U+10000 výše, což jsou tzv. supplementary planes, kde sídlí například matematické alfanumerické symboly, mnoho speciálních znaků a dokonce i několik existujících písemných systémů
Formát 12 je podtabulka, která pokrývá celý rozsah od U+0000 do U+10FFFF. Je tvořena uspořádaným seznamem skupin, přičemž každá skupina má svůj počáteční kódový bod, koncový kódový bod a počáteční ID glyfu, takže souvislý sled kódových bodů se zmapuje na souvislý sled glyfů. HotPDF zpracovává kódové body pomocí hybridní strategie odpovídající způsobu uspořádání dat. Kódové body v oblasti BMP se obsluhují přes přímé pole indexované samotným kódovým bodem, což znamená vyhledávání na jeden přístup bez jakéhokoliv hledání. Kódové body v supplementary planes se naopak zpracovávají v řídké tabulce setříděné podle kódového bodu a vyhledává se v nich pomocí binárního hledání. Výsledkem je, že funkce GetUnicodeGlyphForCodepoint může přijímat plnohodnotný Cardinal a bezpečně odpovídá přes celou oblast, přičemž vrací ID glyfu 0 (.notdef) pro každý kódový bod, který v písmu není k dispozici
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;
Kde tyto dotazy končí
API funkce pro jednoduchou substituci odpovídají na jeden konkrétní typ otázky a měli bychom si ujasnit i to, na co odpovědět nedokážou. LookupType 1 je jen jedním z osmi typů substitucí. Zmíněný dotaz se nepostará o LookupType 2 (multiple substitution), kde se jeden glyf rozpadá na několik, a neporadí si ani s LookupType 4 (ligature substitution), kdy se několik glyfů sloučí do jednoho. Zcela také chybí kontextuální substituce a tzv. chaining-contextual formáty (LookupTypes 5 a 6), které se aktivují pouze za podmínky, kdy se glyf objevuje ve specifickém okolí znaků; nezvládá ani další rozšiřující či reverzní formáty substituce. Zlomek s šikmou lomící čárou, dévanágarské spojky (conjunct) nebo arabská iniciálně-mediálně-finální kaskáda už jsou problémem sekvencí a jednoduché vyhledávání glyfu (single-substitution) pro něj nemá vyjadřovací schopnost
Tyto funkce také neprovádí automatické tvarování (automatic shaping). Nic zde nedohlíží na průběh běhu textu (run of text), aby rozhodlo, které vlastnosti se mají povolit a v jakém pořadí se mají podle daného skriptu aplikovat. Samotný volající si musí zvolit značku vlastnosti a následně ji aplikovat znak po znaku. Jde ale o naprosto ideální nástroj pro použití se stylistickými sadami a alternativami, které se volí explicitně (opt-in) a navíc lokálně, ačkoli na skripty potřebující přeskládání znaků jsou tyhle nástroje zcela nevhodné. Udržování ostrých hranic však zaručuje, že postup substituce zůstává malý a plně předvídatelný
V případech, kdy je skutečně potřebná práce na úrovni sekvencí znaků, najdete příběh o složitých skriptech v našem článku o tvarování složitého textu v Delphi. Pokud jsou pak vaše substituce součástí větších reportovacích úloh, do nichž spadá i vkládání obrázků a dalších písem, problematiku skládání jednotlivých prvků pokrývá náš průvodce výstupem reportů obsahujících písma a obrázky. Všechny tyto prvky používají společný engine pocházející z komponenty HotPDF Component pro Delphi a C++Builder, jenž tak přináší substituční dotazy GSUB navrch k možnostem jako vkládání, subsettování písem a k textovým API rozhraním pokrývaným v dalších příspěvcích našeho blogu