Návrhář vybere písmo s jednobříškovým a pro nadpisy, přeškrtnutou nulou pro tabulky nebo sadou zdobných kapitálek (swash capitals) pro obálku. Tyto glyfy v písmu již jsou. Pouze nejsou výchozí. Výchozí a se mapuje ze znaku přes tabulku cmap na jeden glyf a alternativa se nachází o několik ID glyfů dále, dostupná pouze prostřednictvím pravidla nahrazování. Vytvoření této alternativy v PDF znamená přečtení pravidla a zapsání náhradního glyfu do proudu obsahu. Tento článek pojednává o čtení těchto pravidel, konkrétně typu s jednoduchým nahrazováním (single-substitution), v Object Pascalu bez jakékoli nativní knihovny pro shaper pod ním.
Rozsah je záměrně úzký. Stylistické sady a alternativy představují nahrazení typu jeden glyf na vstupu za jeden na výstupu. Jsou tou částí rozvržení OpenType, kterou můžete vyřešit krátkým deterministickým průchodem tabulky, což z nich dělá vhodného kandidáta pro Pascal engine, který se chce vyhnout závislostem na C.
Proč čisté Delphi spíše než HarfBuzz
HarfBuzz je zřejmou odpovědí na úkol "vytvarovat tento text" a pro plnohodnotné obousměrné (bidi) tvarování nebo tvarování indických a arabských písem je to správná odpověď. Je to však také knihovna v C. Její začlenění do produktu v Delphi nebo C++Builderu znamená dodávání nativního objektu pro každou cílovou platformu a architekturu, přizpůsobení se její volací konvenci, sledování verzí a zkoumání licenčních podmínek. Nic z toho není izolovaně těžké. Všechno je to ale tření, které nikdy nezmizí, a nepřináší nic navíc, když skutečným požadavkem je pouze "dej mi podobu ss01 tohoto písmene".
Jednoduché nahrazování nepotřebuje shaper engine. Potřebuje parser pro několik formátů podtabulek GSUB a jedno nebo dvě binární vyhledávání. Zápis tohoto řešení v Pascalu udržuje celý toolchain uvnitř jediného kompilátoru. Upřímným limitem je, že tento přístup řeší pouze vyhledávání nahrazení glyfů a nic jiného. Nejedná se o řešení obousměrného textu, změnu pořadí indických písem ani o automatické kontextové tvarování. Tam, kde jsou tyto funkce vyžadovány, jsou nezbytné, a dotaz na jednoduché nahrazení je nemůže nahradit.
Hierarchie GSUB odshora dolů
Tabulka nahrazování glyfů (Glyph Substitution table) je organizována jako řetězec nepřímých odkazů a dotaz na nahrazení prochází tento řetězec shora. Na samém vrcholu je ScriptList. Značka písma (script tag), například latn, vybere položku a speciální značka DFLT představuje výchozí písmo, které se použije, pokud neodpovídá žádné specifičtější písmo. Položka písma ukazuje na LangSys, jazykový systém, s výchozím LangSys pro běžné případy a volitelnými pojmenovanými systémy pro jazyky vyžadující odlišné chování. Typickým příkladem je turečtina, kde tečkované a beztečkové i vyžadují vlastní zpracování.
LangSys jmenuje sadu indexů funkcí. Každý index ukazuje do FeatureList, kde záznam funkce nese čtyřbajtovou značku, mimo jiné ss01, a seznam indexů vyhledávání (lookup indices). Tyto indexy nakonec ukazují do LookupList, kde žijí skutečné podtabulky nahrazování. Vyřešení ss01 tedy znamená: najít písmo, najít jeho LangSys, najít funkci se značkou ss01, shromáždit vyhledávání, která jmenuje, a aplikovat je. HotPDF standardně používá písmo DFLT a výchozí LangSys, což je to, s čím se dodává naprostá většina latinkových písem, a nabízí způsob, jak značku písma přepsat, pokud písmo své funkce zapojuje pod konkrétním písmem.
Tabulky pokrytí určují, kdo se účastní
Každá podtabulka nahrazování začíná stejnou otázkou: účastní se tento vstupní glyf tohoto pravidla, a pokud ano, kde se nachází ve vlastním indexování pravidla. Na tuto otázku odpovídá tabulka pokrytí (Coverage table) a odpovědí je index pokrytí (coverage index), malé řadové číslo, které zbytek podtabulky používá k vyhledání toho, čím se glyf stane.
Pokrytí existuje ve dvou formátech. Formát 1 je seznam ID glyfů seřazený vzestupně. Glyf najdete binárním vyhledáváním a jeho pozice v seznamu je jeho indexem pokrytí. Formát 2 je seznam záznamů rozsahů, z nichž každý obsahuje počáteční glyf, koncový glyf a index pokrytí, na který se počáteční glyf mapuje. Glyf uvnitř rozsahu získá svůj index pokrytí posunem od začátku rozsahu. Formát 1 je kompaktní, když jsou zúčastněné glyfy rozptýlené, formát 2, když spadají do souvislých řad. Oba jsou seřazené, takže se v obou vyhledává v logaritmickém čase, a oba vracejí buď index pokrytí, nebo čisté "nepokryto", což umožní enginu ponechat glyf beze změny.
Jednoduché nahrazování, dva formáty
Jednoduché nahrazování (Single Substitution) je LookupType 1 a mapuje jeden glyf na přesně jednu náhradu. Má také dva formáty a toto rozdělení je optimalizací prostoru. Formát 1 ukládá jednu znaménkovou deltu. Výstupní ID glyfu je vstupní ID glyfu plus tato delta, modulo 65536. Takto písmo kóduje nahrazení, kde každý zúčastněný glyf leží ve stejném pevném posunu od své alternativy, například blok proporčních číslic umístěných v konstantní vzdálenosti od odpovídajících skákajících číslic (oldstyle figures). Tabulka pokrytí říká, které glyfy splňují podmínky, a tato jedna delta slouží všem.
Formát 2 ukládá explicitní pole ID náhradních glyfů. Index pokrytí z tabulky pokrytí je indexem do tohoto pole, takže glyf na indexu pokrytí 0 se stane první položkou pole, na indexu pokrytí 1 druhou atd. Formát 2 se používá, když alternativy nejsou v jednotném posunu, což je běžný případ ručně vytvořených stylistických sad. Dotaz je z pohledu volajícího v obou případech stejný. Vezme se vstupní glyf, projde se tabulkou pokrytí, a pokud je pokryt, použije se delta nebo se přečte hodnota 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, která stojí za povšimnutí, je propustnost (pass-through). GetSingleSubstituteGlyph vrací vstupní ID glyfu beze změny při každém neúspěchu: žádné písmo, žádná tabulka GSUB, žádná odpovídající funkce, žádný zásah v pokrytí. To znamená, že volání je bezpečné provádět bezpodmínečně. Požádáte o alternativu, a pokud žádná neexistuje, dostanete zpět přesně to, co jste vložili, takže volající kód nemusí nijak zvlášť ošetřovat písmo, kterému tato funkce chybí.
Co znamenají značky stylistických funkcí
Značka funkce (feature tag) je celým slovníkem toho, o jakou alternativu žádáte, a značky relevantní pro stylistickou práci tvoří krátký seznam. Hlavní dvojicí je salt, stylistické alternativy (stylistic alternates), což je univerzální přístup k alternativním formám glyfu, a ss01 až ss20, dvacet očíslovaných stylistických sad, které písmo může definovat, z nichž každá je pojmenovaným balíčkem nahrazení, které návrhář seskupil. Písmo může například umístit jednobříškové a a R s rovnou nohou pod ss03, takže povolení této jedné sady změní styl obou.
// 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
Předtím, než může proběhnout jakékoli nahrazení, se znak musí stát glyfem, což je úkolem tabulky cmap. Dotaz na nahrazení začíná od ID glyfu, takže cesta vede vždy od znaku ke glyfu přes cmap a poté od glyfu k alternativě přes GSUB. Zajímavou částí tabulky cmap je její dosah. Podtabulka formátu 4 pokrývá základní vícejazyčnou rovinu (Basic Multilingual Plane), tedy prvních 65536 kódových bodů, což stačí pro většinu latinkových textů. Nestačí to však pro kódové body od U+10000 výše, doplňkové roviny (supplementary planes), kde dnes žijí matematické alfanumerické znaky, mnoho symbolů a několik živých písem.
Formát 12 je podtabulka, která pokrývá celý rozsah U+0000 až U+10FFFF. Jedná se o seřazený seznam skupin, z nichž každá obsahuje počáteční kódový bod, koncový kódový bod a počáteční ID glyfu, takže souvislá řada kódových bodů se mapuje na souvislou řadu glyfů. HotPDF řeší kódové body hybridní strategií, která odpovídá tvaru dat. Kódové body v rovině BMP jsou obsluhovány z přímého pole indexovaného kódovým bodem (jediné vyhledání bez prohledávání). Kódové body v doplňkových rovinách jsou obsluhovány z řídké tabulky seřazené podle kódového bodu a prohledávané binárním vyhledáváním. Výsledkem je, že GetUnicodeGlyphForCodepoint přebírá plný Cardinal a odpovídá správně v celém rozsahu, přičemž vrací ID glyfu 0, glyf .notdef, pro jakýkoli kódový bod, který písmo nemapuje.
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čí
Rozhraní API pro jednoduché nahrazování odpovídají na jeden typ otázek a je dobré si ujasnit, na co neodpovídají. LookupType 1 je jedním z osmi typů nahrazování. Dotaz neřeší LookupType 2 (vícenásobné nahrazení, kde se jeden glyf stává několika), ani LookupType 4 (nahrazení ligaturou, kde se několik glyfů stává jedním). Neřeší kontextové a řetězené kontextové typy, LookupTypes 5 a 6, které se spouštějí pouze tehdy, když se glyf objeví v určitém sousedství, ani typy rozšíření a zpětného řetězení. Diagonální zlomek, dévanágarská spřežka nebo arabská kaskáda počátečních-středních-koncových znaků představují sekvenční problém a vyhledávání jednoduchého nahrazení na úrovni jednotlivých glyfů je nedokáže vyjádřit.
Neprovádí ani automatické tvarování. Nic zde nezkoumá text, nerozhoduje o tom, které funkce zapnout, a neaplikuje je v pořadí, které písmo vyžaduje. Volající zvolí značku funkce a aplikuje ji glyf po glyfu. To je přesně ten správný nástroj pro stylistické sady a alternativy, které jsou volitelné a lokální, a zcela nesprávný nástroj pro písmo, které vyžaduje změnu pořadí znaků. Udržování ostré hranice je to, co umožňuje cestě nahrazování zůstat malou a předvídatelnou.
Odtud téma složitých písem pokračuje v našem článku o tvarování složitých písem v Delphi. Pokud jsou vaše nahrazení součástí větší tiskové úlohy, která na stránku umisťuje také obrázky a jiná písma, průvodce výstupem sestav s písmy a obrázky popisuje, jak tyto části do sebe zapadají. Všechny běží na stejném enginu, HotPDF Component pro Delphi and C++Builder, který nese dotazy na nahrazování GSUB spolu s rozhraními API pro vkládání písem, vytváření podmnožin a text, o nichž se píše na jiných místech tohoto blogu.