Technical Article

OpenType GSUB stilisztikai alternatívák tiszta Delphi-ben

Egy tervező kiválaszt egy betűtípust egyemeletes a betűvel a fejlécekhez, áthúzott nullával a táblázatokhoz, vagy kacskaringós nagybetűkkel (swash capitals) egy borítóhoz. Ezek a glifák (glyphs) már benne vannak a betűtípusban. Egyszerűen csak nem ezek az alapértelmezettek. Az alapértelmezett a a karakterből a cmap táblázaton keresztül képeződik le egy glifára, az alternatíva pedig néhány glifa-azonosítóval (glyph id) arrébb helyezkedik el, és csak egy helyettesítési szabályon keresztül érhető el. Ennek az alternatívának a PDF-ben történő előállítása a szabály elolvasását és a helyettesítő glifa kibocsátását jelenti a tartalomfolyamban. Ez a cikk ezeknek a szabályoknak a beolvasásáról szól – a szimpla helyettesítés típusúakról – Object Pascalban, anélkül, hogy natív formázó (shaping) könyvtár lenne alatta.

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. Az OpenType elrendezésnek ez az a része, amelyet egy kis, determinisztikus táblabejárással fel lehet oldani, ami alkalmassá teszi őket egy olyan Pascal motorhoz, amely mentes szeretne maradni a C függőségektől.

Miért a tiszta Delphi a HarfBuzz helyett

A HarfBuzz a kézenfekvő válasz a „szöveg formázása” kérdésre, és a teljes kétirányú (bidi), indiai (Indic) vagy arab formázás esetén ez a helyes válasz. De ez egy C könyvtár. Annak beépítése egy Delphi vagy C++Builder termékbe azt jelenti, hogy minden célplatformhoz és architektúrához natív objektumot kell szállítani, igazodva annak hívási konvencióihoz, követve a kiadási ciklusát, és ellenőrizve a licencfeltételeit a sajátunkkal szemben. Önmagában ezek egyike sem nehéz. De mindez folyamatos súrlódást jelent, ami soha nem múlik el, és semmit sem nyújt, ha a tényleges követelmény csupán annyi: „add meg nekem ennek a betűnek az ss01 formáját”.

Az egyszeri helyettesítéshez nincs szükség formázómotorra (shaping engine). Ehhez egy elemző (parser) szükséges néhány GSUB al-táblázat formátumhoz, valamint egy-két bináris keresés. Ennek megírása Pascalban a teljes eszközláncot egyetlen fordítón belül tartja. A tiszta határ az, hogy ez a megközelítés csak a glifa-helyettesítési lekérdezéseket kezeli, és semmi mást. Ez nem bidi feloldás, nem indiai újrarendezés és nem automatikus környezetfüggő formázás. Ahol ezekre szükség van, ott szükség van rájuk, és az egyszeri helyettesítési lekérdezés nem fogja helyettesíteni őket.

A GSUB hierarchia fentről lefelé

A Glyph Substitution (glifa-helyettesítési) táblázat közvetett hivatkozások láncolataként van megszervezve, és a helyettesítési lekérdezés a láncot a tetejétől indulva járja be. A legtetején a ScriptList található. Egy olyan írásrendszer-tag (script tag), mint a latn, kiválaszt egy bejegyzést, a speciális DFLT tag pedig az alapértelmezett írásrendszer, amely akkor érvényes, ha nincs konkrétabb egyező írásrendszer. Az írásrendszer-bejegyzés egy LangSys-re, azaz nyelvi rendszerre mutat, amely tartalmaz egy alapértelmezett LangSys-t az általános esetre, és opcionális nevesítetteket azon nyelvekhez, amelyek eltérő viselkedést igényelnek. Török a szokásos példa, ahol a ponttal rendelkező és pont nélküli i saját kezelést igényel.

A LangSys megnevezi a funkció-indexek (feature indices) halmazát. Minden index a FeatureList-re mutat, ahol a funkciórekord egy négybájtos taget hordoz (többek között az ss01-et), valamint a lekérdezési indexek listáját. Ezek az indexek végül a LookupList-re mutatnak, ahol a tényleges helyettesítési al-táblázatok találhatók. Tehát az ss01 feloldása a következőt jelenti: keresse meg az írásrendszert, keresse meg annak LangSys-ét, keresse meg azt a funkciót, amelynek tagje az ss01, gyűjtse össze az általa megnevezett lekérdezéseket, és alkalmazza őket. A HotPDF alapértelmezés szerint a DFLT írásrendszert é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 ad az írásrendszer-tag felülírására, ha egy betűtípus a funkcióit egy adott írásrendszer alá huzalozza be.

A lefedettségi táblázatok döntik el a részvételt

Minden helyettesítési al-táblázat 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 lefedettségi táblázat (Coverage table) ad választ, a válasz pedig egy lefedettségi index (coverage index), egy kis sorszám, amelyet az al-táblázat többi része arra használ, hogy kikeresse, mivé válik a glifa.

Coverage comes in two formats. Format 1 is a list of glyph ids sorted in ascending order. You find a glyph with a binary search, and its position in the list is its coverage index. Format 2 is a list of range records, each a start glyph, an end glyph, and the coverage index that the start glyph maps to. A glyph inside a range gets its coverage index by offsetting from the range's start. Format 1 is compact when the participating glyphs are scattered, Format 2 when they fall into contiguous runs. Both are sorted, so both are searched in logarithmic time, and both return either a coverage index or a clean "not covered" that lets the engine leave the glyph alone.

Egyszeri helyettesítés (Single Substitution), a két formátum

Az egyszeri helyettesítés a LookupType 1, és pontosan egy glifát képez le pontosan egy cserére. Ennek is két formátuma van, és a felosztás helyoptimalizálás. A Format 1 egyetlen előjeles deltát tárol. A kimeneti glifa-azonosító a bemeneti glifa-azonosító plusz ez a delta modulo 65536. A betűtípus így kódolja azt a helyettesítést, ahol minden részt vevő glifa azonos fix eltolással helyezkedik el az alternatívájához képest – például a helyi számjegyek blokkja, amely állandó távolságra van elhelyezve a megfelelő régi stílusú számjegyektől. A lefedettségi táblázat megadja, hogy mely glifák jogosultak, és az egyetlen delta mindegyiküket kiszolgálja.

A Format 2 a helyettesítő glifa-azonosítók explicit tömbjét tárolja. A lefedettségi táblázatból származó lefedettségi index a tömb indexe, így a 0-s lefedettségi indexű glifa a tömb első bejegyzése lesz, az 1-es a második, és így tovább. A Format 2-t akkor használják, ha az alternatívák nem egyenletes eltolásban vannak, 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 azonos. Vegye a bemeneti glifát, futtassa át a lefedettségen, és ha lefedett, alkalmazza a deltát vagy olvassa be 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;

Érdemes megfigyelni az átjárhatósági (pass-through) szerződést. A GetSingleSubstituteGlyph minden hiba esetén változatlanul adja vissza a bemeneti glifa-azonosítót: nincs betűtípus, nincs GSUB táblázat, nincs egyező funkció, nincs lefedettségi találat. Ez azt jelenti, hogy a hívás biztonságosan elvégezhető feltétel nélkül. Kéri az alternatívát, és ha nincs, pontosan azt kapja vissza, amit megadott, így a hívó kódnak soha nem kell különleges esetet képeznie arra a betűtípusra vonatkozóan, amelyből hiányzik a funkció.

Mit jelentenek a stilisztikai funkció-tagek

A funkció-tag (feature tag) határozza meg, hogy melyik alternatívát kéri, és a stilisztikai munkához kapcsolódó tagek listája meglehetősen rövid. A legfontosabb páros a salt, a stilisztikai alternatívák (stylistic alternates), amely általános hozzáférést biztosít a glifák alternatív formáihoz, valamint az ss01 és ss20 közötti tagek, a húsz számozott stilisztikai készlet, amelyet a betűtípus definiálhat, és amelyek a tervező által csoportosított helyettesítések nevesített csomagjai. Egy betűtípus például az egyemeletes a és az egyenes szárú R betűket az ss03 alá helyezheti, így ennek az egyetlen készletnek az engedélyezése mindkettőt átformálja.

Ezek körül számos egyéb egyszeri helyettesítési tag található. Az aalt az összes alternatíva elérése (access-all-alternates), a glifa összes alternatívájának uniója, amely általában glifa-paletta funkcióként jelenik meg. A titl a nagy méretekre tervezett címsor-nagybetűket választja ki. A subs és sups valódi alsó indexű és felső indexű számjegyeket vált be a lekicsinyített alapértelmezettek helyett. Az ordn sorszámneveket állít elő (például az 1st és 2nd felső indexbe tett betűit). A frac törteket épít, bár a teljes átlós törtek olyan ligatúra- és környezeti logikára is támaszkodnak, amely túlmutat az egyszerű egyszeri helyettesítésen. Az egyedi glifás esetekben a mechanizmus megegyezik az ss01-gyel: adja át a taget a helyettesítési lekérdezésnek, és olvassa 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 Format 12 é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áblázat feladata. A helyettesítési lekérdezés egy glifa-azonosítóból indul ki, így az útvonal mindig a karakterből a glifába vezet a cmap-en keresztül, majd a glifából az alternatívába a GSUB-on keresztül. A cmap érdekes része a hatóköre. A Format 4 al-táblázat a Basic Multilingual Plane-t (alapvető többnyelvű sík), az első 65536 kódpontot fedi le, és ez elegendő a legtöbb latin szöveghez. De nem elegendő az U+10000 feletti kódpontokhoz, azaz a kiegészítő síkokhoz (supplementary planes), ahol a matematikai alfanumerikus karakterer, számos szimbólum és több ma is használt írásrendszer található.

A Format 12 az az al-táblázat, amely a teljes U+0000 és U+10FFFF közötti tartományt lefedi. Ez csoportok rendezett listája, ahol minden csoport egy kezdő kódpontból, egy záró kódpontból és egy kezdő glifa-azonosítóból áll, így a kódpontok folytonos futása leképeződik a glifák folytonos futására. A HotPDF hibrid stratégiával oldja fel a kódpontokat, igazodva az adatok formájához. A BMP kódpontjai közvetlenül az adott kódponttal indexelt tömbből kerülnek kiszolgálásra – ez egyetlen lekérdezés keresés nélkül. A kiegészítő síkok kódpontjai pedig egy kódpont szerint rendezett ritka (sparse) táblázatból származnak, bináris kereséssel. Az eredmény az, hogy a GetUnicodeGlyphForCodepoint egy teljes Cardinal értéket fogad el, és helyesen válaszol a teljes tartományban, a 0-s glifa-azonosítót (a .notdef glifát) adva vissza minden olyan kódponthoz, 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;

Ahol ezek a lekérdezések véget érnek

Az egyszeri helyettesítési API-k egy bizonyos fajta kérdésre válaszolnak, és érdemes tisztázni, mire nem adnak választ. 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 glifa több glifává válik), sem a LookupType 4 ligatúra-helyettesítést (ahol több glifa eggyé válik). Nem kezeli a környezetfüggő és láncolt-környezetfüggő típusokat (LookupType 5 és 6), amelyek csak akkor aktiválódnak, ha egy glifa egy bizonyos szomszédságban jelenik meg, sem a kiterjesztési és fordított láncolású típusokat. Az átlós tört, a devanagari konjunkció vagy az arab kezdeti-középső-végső kaszkád szekvenciális probléma, és a glifánkénti egyszeri helyettesítési lekérdezés ezt nem képes kifejezni.

Nem végez automatikus formázást (shaping) sem. Semmi sem vizsgálja itt a szövegfolyamot, döntve arról, hogy mely funkciókat kapcsolja be, és alkalmazza azokat az írásrendszer által megkövetelt sorrendben. A hívó választja ki a funkció-taget, és alkalmazza azt glifáról glifára. Ez pontosan a megfelelő eszköz a stilisztikai készletekhez és alternatívákhoz, amelyek opcionálisak és helyi jellegűek, és pontosan a rossz eszköz egy olyan írásrendszerhez, amely újrarendezést igényel. A határok élesen tartása teszi lehetővé, hogy a helyettesítési útvonal kicsi és kiszámítható maradjon.

Azokban az esetekben, amelyek szekvenciaszintű munkát igényelnek, a komplex írásrendszerek történetét a delphi komplex szövegformázásról szóló cikkünk folytatja. Ha a helyettesítései egy nagyobb jelentéskészítési munka 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 rendelkező jelentéskimenetről szóló útmutató bemutatja, hogyan illeszkednek össze ezek a darabok. Mindezek ugyanazon a motoron futnak, a Delphi és C++Builder platformokhoz készült HotPDF Component csomagban, amely a GSUB helyettesítési lekérdezéseket a betűtípus-beágyazási, részhalmaz-képzési (subsetting) és szöveg-API-kkal együtt tartalmazza, amelyeket a blogunkon máshol ismertetünk.