Műszaki cikk

OpenType GSUB stilisztikai alternatívák tiszta Delphiben

Egy tervező kiválaszt egy betűtípust egyszintes a betűvel a fejlécekhez, vagy áthúzott nullával a táblázatokhoz, vagy suhintásos (swash) nagybetűkkel a borítóhoz. Ezek a glifák már benne vannak a betűtípusban. Csak éppen nem az alapértelmezettek. Az alapértelmezett a a karakterről a cmap táblán keresztül egy glifára képeződik le, az alternatíva pedig néhány glifa-azonosítóval arrébb ül, és csak egy helyettesítési szabályon keresztül érhető el. Ezen alternatíva előállítása a PDF-ben azt jelenti, hogy el kell olvasni a szabályt, és ki kell bocsátani a helyettesítő glifát a tartalomfolyamban. Ez a cikk ezeknek a szabályoknak – az egyszeres helyettesítésű fajtának – az Object Pascalban történő olvasásáról szól, anélkül, hogy alatta natív formázó (shaping) könyvtár lenne

A hatókör szándékosan szűk. A stilisztikai készletek és alternatívák egy-glifa-be, egy-glifa-ki helyettesítések. Ez az OpenType elrendezés azon része, amelyet egy kis, determinisztikus táblabejárással fel lehet oldani, ami jól illeszkedik egy olyan Pascal motorhoz, amely mentes akar maradni a C függőségektől

Miért tiszta Delphi a HarfBuzz helyett

A HarfBuzz a kézenfekvő válasz a "formázd ezt a szöveget" kérdésre, és a teljes kétirányú, indiai vagy arab formázáshoz ez a helyes válasz. Ez egyben egy C könyvtár is. A Delphi vagy C++Builder termékbe való kötése azt jelenti, hogy minden célplatformhoz és architektúrához natív objektumot kell szállítani, egyeztetni kell a hívási konvencióját, nyomon kell követni a kiadási ütemét, és össze kell vetni a licencfeltételeit a sajátoddal. Ezen dolgok egyike sem nehéz önmagában. Mindez azonban olyan súrlódás, amely soha nem múlik el, és semmit sem nyerünk vele, ha a tényleges követelmény az, hogy "add ide ennek a betűnek az ss01 formáját"

Az egyszeres helyettesítéshez nincs szükség formázó motorra. Szüksége van egy értelmezőre egy maroknyi GSUB altábla formátumhoz, és egy-két bináris keresésre. Ha ezt Pascalban írjuk meg, a teljes eszköztár egyetlen fordítón belül marad. Az őszinte korlát az, hogy ez a megközelítés a glifahelyettesítési kereséseket kezeli, és semmi mást. Ez nem bidi feloldás, nem indiai újrarendezés, és nem automatikus kontextuális formázás. Ahol ezekre szükség van, ott szükség van rájuk, és egy egyszeres helyettesítésű lekérdezés nem fogja helyettesíteni őket

A GSUB hierarchia, fentről lefelé

A Glyph Substitution (glifahelyettesítési) tábla indirekciók láncolataként van megszervezve, és egy helyettesítési lekérdezés fentről halad végig a láncon. Legfelül a ScriptList található. Egy olyan írásmód-címke (script tag), mint a latn, kiválaszt egy bejegyzést, és a speciális DFLT címke az alapértelmezett írásmód, amely akkor érvényes, ha egyetlen konkrétabb írásmód sem egyezik. Az írásmód-bejegyzés egy LangSys-re, a nyelvi rendszerre mutat, amely egy alapértelmezett LangSys-t tartalmaz az általános esethez, és opcionális elnevezetteket az eltérő viselkedést igénylő nyelvekhez. A török a szokásos példa, ahol a pontozott és pont nélküli i saját kezelést igényel

A LangSys funkcióindexek halmazát nevezi meg. Minden index a FeatureList-be mutat, ahol egy funkciórekord egy négybájtos címkét hordoz – köztük az ss01-et –, valamint a keresési indexek listáját. Ezek az indexek végül a LookupList-be mutatnak, ahol a tényleges helyettesítési altáblák élnek. Az ss01 feloldása tehát a következőt jelenti: keresd meg az írásmódot, keresd meg a LangSys-ét, keresd meg azt a funkciót, amelynek a címkéje ss01, gyűjtsd össze az általa megnevezett kereséseket, és alkalmazd őket. A HotPDF alapértelmezés szerint a DFLT írásmódot és az alapértelmezett LangSys-t használja, amelyet a latin szöveges tervek túlnyomó többsége szállít, és módot biztosít az írásmód-címke felülbírálására, ha egy betűtípus ehelyett egy adott írásmód alá köti a funkcióit

A Coverage táblák döntik el, ki vesz részt

Minden helyettesítési altábla ugyanazzal a kérdéssel kezdődik: részt vesz-e ez a bemeneti glifa ebben a szabályban, és ha igen, hol helyezkedik el a szabály saját indexelésében. Erre a kérdésre egy Coverage (lefedettségi) tábla válaszol, a válasz pedig egy lefedettségi index, egy kis sorszám, amelyet az altábla többi része arra használ, hogy megkeresse, mivé válik a glifa

A lefedettség két formátumban érkezik. Az 1. formátum a glifa-azonosítók növekvő sorrendben rendezett listája. Egy glifát bináris kereséssel találsz meg, és a listában elfoglalt pozíciója a lefedettségi indexe. A 2. formátum tartományrekordok listája, amelyek mindegyike egy kezdő glifát, egy záró glifát és azt a lefedettségi indexet tartalmazza, amelyre a kezdő glifa leképeződik. A tartományon belüli glifa a lefedettségi indexét a tartomány kezdetétől való eltolással kapja meg. Az 1. formátum akkor kompakt, ha a részt vevő glifák szétszórtak, a 2. formátum pedig akkor, ha összefüggő sorozatokba esnek. Mindkettő rendezett, így mindkettőben logaritmikus idő alatt lehet keresni, és mindkettő vagy egy lefedettségi indexet ad vissza, vagy egy tiszta "nem fedett" jelzést, amely lehetővé teszi a motor számára, hogy békén hagyja a glifát

Egyszeres helyettesítés, a két formátum

Az egyszeres helyettesítés (Single Substitution) a LookupType 1, és egy glifát pontosan egy helyettesítésre képez le. Két formátuma is van, és a felosztás egy térbeli optimalizáció. Az 1. formátum egyetlen előjeles deltát tárol. A kimeneti glifa azonosítója a bemeneti glifa azonosítója plusz ez a delta, modulo 65536. A betűtípus így kódol egy olyan helyettesítést, ahol minden részt vevő glifa ugyanazon a rögzített eltoláson ül az alternatívájától, például az ugráló (oldstyle) számjegyektől állandó távolságra elhelyezett verzális (lining) számjegyek egy blokkja. A Coverage tábla megmondja, mely glifák felelnek meg, és az egyetlen delta mindegyiket kiszolgálja

A 2. formátum a helyettesítő glifa-azonosítók explicit tömbjét tárolja. A Coverage táblából származó lefedettségi index az ebbe a tömbbe mutató index, így a 0. lefedettségi indexnél lévő glifa lesz az első tömbbejegyzés, az 1. lefedettségi indexnél a második, és így tovább. A 2. formátumot akkor használják, ha az alternatívák nincsenek egyenletes eltoláson, ami a kézzel készített stilisztikai készletek gyakori esete. A lekérdezés a hívó oldaláról mindkét esetben ugyanaz. Fogd a bemeneti glifát, futtasd át a Coverage-en, és ha fedett, alkalmazd a deltát, vagy olvasd ki a tömbhelyet

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;

A figyelemre méltó szerződés (contract) az áteresztés (pass-through). A GetSingleSubstituteGlyph a bemeneti glifa azonosítóját változatlanul adja vissza minden találatnélküliség (miss) esetén: nincs betűtípus, nincs GSUB tábla, nincs egyező funkció, nincs lefedettségi találat. Ez azt jelenti, hogy a hívást biztonságosan meg lehet tenni feltétel nélkül. Kéred az alternatívát, és ha nincs, pontosan azt kapod vissza, amit betettél, így a hívó kódnak soha nem kell külön esetként (special-case) kezelnie azt a betűtípust, amelyből hiányzik a funkció

Mit jelentenek a stilisztikai funkció címkék

A funkció címke (feature tag) a teljes szókincse annak, hogy melyik alternatívát kéred, és a stilisztikai munkához releváns címkék listája rövid. A főszereplő páros a salt (stilisztikai alternatívák), amely az általános hozzáférés egy glifa alternatív formáihoz, és az ss01-től ss20-ig terjedő címkék, az a húsz számozott stilisztikai készlet, amelyet egy betűtípus definiálhat, mindegyik a helyettesítések egy megnevezett csomagja, amelyet a tervező csoportosít. Egy betűtípus például tehet egy egyszintes a betűt és egy egyenes lábú R betűt az ss03 alá, így ennek az egy készletnek az engedélyezése mindkettőt átstílusozza

Ezek körül még számos további egyszeres helyettesítésű címke található. Az aalt az access-all-alternates (hozzáférés minden alternatívához), ami egy glifa minden alternatívájának uniója, általában glifa-paletta funkcióként bemutatva. A titl a nagy méretekre vágott cím-nagybetűket választja ki. A subs és a sups a kicsinyített alapértelmezések helyett valódi alsó és felső indexű számjegyeket cserél be. Az ordn sorszámformákat (ordinal) hoz létre, a megemelt betűket az 1st-ben és a 2nd-ben. A frac törteket épít fel, bár a teljes átlós törtek szintén ligatúrákra és kontextuális logikára támaszkodnak, amelyek túlmutatnak az egyszerű egyszeres helyettesítésen. Az egyglifás eseteknél a mechanizmus megegyezik az ss01-gyel: add át a címkét a helyettesítési lekérdezésnek, és olvasd vissza az alternatív glifát

// 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;

A cmap 12-es formátuma és a kiegészítő síkok

Mielőtt bármilyen helyettesítés lefuthatna, a karakternek glifává kell válnia, és ez a cmap tábla feladata. A helyettesítési lekérdezés egy glifa-azonosítóból indul, így az út mindig a karaktertől a glifáig a cmap-en keresztül, majd a glifától az alternatíváig a GSUB-on keresztül vezet. A cmap érdekes része a hatóköre. A 4-es formátumú altábla lefedi az Alap Többnyelvű Síkot (Basic Multilingual Plane), az első 65536 kódpontot, és ez elegendő a legtöbb latin szöveghez. Nem elég a U+10000-től felfelé lévő kódpontokhoz, a kiegészítő síkokhoz (supplementary planes), ahol a matematikai alfanumerikus karakterek, sok szimbólum és több élő írásmód ma már él

A 12-es formátum az az altábla, amely lefedi a teljes U+0000 és U+10FFFF közötti tartományt. Ez a csoportok rendezett listája, ahol minden csoport egy kezdő kódpont, egy záró kódpont és egy kezdő glifa-azonosító, így a kódpontok egy összefüggő sorozata a glifák egy összefüggő sorozatára képeződik le. A HotPDF a kódpontokat egy olyan hibrid stratégiával oldja fel, amely illeszkedik az adatok formájához. A BMP-ben (alapsíkban) lévő kódpontokat egy közvetlen tömbből szolgálja ki, amelyet a kódpont indexel – ez egyetlen keresés nélküli lekérdezés. A kiegészítő síkokban lévő kódpontokat egy kódpont szerint rendezett ritka táblából (sparse table) szolgálja ki, és bináris kereséssel keresi meg. Az eredmény az, hogy a GetUnicodeGlyphForCodepoint egy teljes Cardinal értéket vesz fel, és az egész tartományban helyesen válaszol, visszaadva a 0-s glifa-azonosítót (a .notdef glifát) minden olyan kódpontra, amelyet a betűtípus nem képez le

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;

Hol állnak meg ezek a lekérdezések

Az egyszeres helyettesítésű API-k egy bizonyos alakú kérdésre válaszolnak, és érdemes tisztázni, mire nem válaszolnak. A LookupType 1 a nyolc helyettesítési típus egyike. A lekérdezés nem kezeli a LookupType 2 többszörös helyettesítést, ahol egy glifából több lesz, sem a LookupType 4 ligatúra helyettesítést, ahol több glifából lesz egy. Nem kezeli a kontextuális és láncoló-kontextuális (chaining-contextual) típusokat (LookupType 5 és 6), amelyek csak akkor aktiválódnak, ha egy glifa egy bizonyos környezetben jelenik meg, sem a kiterjesztési (extension) és fordított láncolású (reverse-chaining) típusokat. Egy átlós tört, egy dévanágari kötés vagy egy arab kezdő-középső-záró kaszkád szekvenciaprobléma (sequence problem), és egy glifánkénti egyszeres helyettesítésű keresés nem tudja kifejezni

Nem végez automatikus formázást sem. Itt semmi nem vizsgál meg egy szövegsorozatot, nem dönti el, hogy mely funkciókat kapcsolja be, és nem alkalmazza őket az írásmód által megkövetelt sorrendben. A hívó választja ki a funkció címkét, és glifáról glifára alkalmazza azt. Ez pontosan a megfelelő eszköz a stilisztikai készletekhez és alternatívákhoz, amelyek választhatók (opt-in) és lokálisak, és pontosan a rossz eszköz egy újrarendezést igénylő írásmódhoz. A határvonal éles tartása az, ami lehetővé teszi, hogy a helyettesítési útvonal kicsi és kiszámítható maradjon

Azokra az esetekre, amelyeknél szükség van szekvenciaszintű munkára, a komplex írásmódok történetét a komplex írásmódok szövegformázásáról szóló, Delphiben írt cikkünkben dolgozzuk fel. Ha a helyettesítéseid egy nagyobb jelentéskészítő feladat részét képezik, amely képeket és egyéb betűtípusokat is elhelyez az oldalon, a betűtípusokkal és képekkel kiegészített jelentéskimenetről szóló útmutató bemutatja, hogyan illeszkednek össze ezek a részek. Mindezek ugyanazon a motoron, a Delphihez és C++Builderhez készült HotPDF komponensen futnak, amely a blogon máshol tárgyalt betűtípus-beágyazási, részhalmazképzési (subsetting) és szöveg API-k mellett a GSUB helyettesítési lekérdezéseket is hordozza