En formgivare väljer ett typsnitt med ett enplans-a för rubriker, eller en nolla med snedstreck för tabeller, eller en uppsättning dekorativa versaler (swash capitals) för ett omslag. De glyferna finns redan i typsnittet. De är helt enkelt inte standardvalet. Standard-a mappar från tecknet via cmap-tabellen till en glyf, och alternativet ligger några glyf-ID:n bort, nåbart endast via en ersättningsregel (substitution rule). Att producera det alternativet i en PDF innebär att läsa den regeln och skicka ut ersättningsglyfen i innehållsströmmen. Denna artikel handlar om att läsa dessa regler, av typen enkla ersättningar (single-substitution), i Object Pascal utan något inbyggt formgivningsbibliotek (shaping library) i botten.
Omfånget är medvetet smalt. Stilistiska uppsättningar och alternativ är ersättningar av typen en glyf in, en glyf ut. De är den del av OpenType-layouten som du kan lösa med en liten, deterministisk tabellvandring, vilket gör dem till ett bra val för en Pascal-motor som vill förbli fri från C-beroenden.
Varför ren Delphi snarare än HarfBuzz
HarfBuzz är det uppenbara svaret på att "forma denna text", och för fullständig dubbelriktad (bidirectional), indisk eller arabisk textformning är det det rätta svaret. Men det är också ett C-bibliotek. Att binda det till en Delphi- eller C++Builder-produkt innebär att leverera ett plattformsspecifikt objekt för varje målplattform och arkitektur, matcha dess anropskonvention, följa dess uppdateringstakt och läsa dess licensvillkor mot dina egna. Inget av det är svårt i sig. Men allt av det är friktion som aldrig försvinner, och det ger ingenting när det faktiska kravet är "ge mig ss01-formen av denna bokstav".
Enkla ersättningar behöver ingen formningsmotor (shaping engine). Det behövs en parser för en handfull GSUB-undertabellformat och en binärsökning eller två. Att skriva det i Pascal håller hela verktygskedjan inom en och samma kompilator. Den ärliga begränsningen är att detta tillvägagångssätt hanterar glyfersättningssökningar och ingenting annat. Det är inte bidi-lösning, det är inte indisk omordning, och det är inte automatisk kontextuell formning. Där sådant behövs, så behövs det, och en sökning för enkla ersättningar kan inte ersätta det.
GSUB-hierarkin, från topp till botten
Glyfersättningstabellen (Glyph Substitution table) är organiserad som en kedja av indirektioner, och en ersättningssökning vandrar genom kedjan från toppen. Längst upp finns ScriptList. En skripttagg som latn väljer en post, och den speciella taggen DFLT är standardskriptet som tillämpas när inget mer specifikt skript matchar. Skriptposten pekar på ett LangSys, språksystemet, med ett standard-LangSys för det vanliga fallet och valfria namngivna för språk som kräver ett annat beteende. Turkiska är det vanliga exemplet, där det punkterade och punktlösa i kräver sin egen hantering.
LangSys namnger en uppsättning funktionsindex (feature indices). Varje index pekar in i FeatureList, där en funktionspost bär en fyrabytestagg, bland dem ss01, och en lista över sökningsindex (lookup indices). Dessa index pekar slutligen in i LookupList, där de faktiska ersättningsundertabellerna (substitution subtables) bor. Att lösa ss01 innebär alltså: hitta skriptet, hitta dess LangSys, hitta funktionen vars tagg är ss01, samla de sökningar den namnger och tillämpa dem. HotPDF använder som standard DFLT-skriptet och standard-LangSys, vilket är vad den stora majoriteten av latinska textdesigner levereras med, och det exponerar ett sätt att åsidosätta skripttaggen när ett typsnitt kopplar sina funktioner under ett specifikt skript istället.
Coverage-tabeller avgör vilka som deltar
Varje ersättningsundertabell börjar med samma fråga: deltar denna indataglyf i denna regel, och i så fall, var i regelns egen indexering hamnar den. Den frågan besvaras av en täckningstabell (Coverage table), och svaret är ett täckningsindex (coverage index), ett litet ordningstal som resten av undertabellen använder för att slå upp vad glyfen blir.
Coverage finns i två format. Format 1 är en lista över glyf-ID:n sorterade i stigande ordning. Du hittar en glyf med en binärsökning, och dess position i listan är dess täckningsindex. Format 2 är en lista över intervallsposter (range records), där varje post är en startglyf, en slutglyf och det täckningsindex som startglyfen mappar till. En glyf inom ett intervall får sitt täckningsindex genom att förskjutas från intervallets start. Format 1 är kompakt när de deltagande glyferna är spridda, Format 2 när de faller i sammanhängande sekvenser. Båda är sorterade, så båda söks igenom på logaritmisk tid, och båda returnerar antingen ett täckningsindex eller ett tydligt "not covered" som låter motorn lämna glyfen ifred.
Single Substitution, de två formaten
Enkla ersättningar (Single Substitution) är LookupType 1, och den mappar en glyf till exakt en ersättning. Den har också två format, och uppdelningen är en utrymmeoptimering. Format 1 lagrar en enskild tecknad delta. Utdataglyfens ID är indataglyfens ID plus den deltan, modulo 65536. Det är så ett typsnitt kodar en ersättning där varje deltagande glyf ligger på samma fasta förskjutning (offset) från sitt alternativ, till exempel ett block av linjesiffror (lining figures) placerade på ett konstant avstånd från matchande medeltidssiffror (oldstyle figures). Coverage-tabellen anger vilka glyfer som är kvalificerade, och den enda deltan tjänar alla dessa.
Format 2 lagrar en explicit array av ersättningsglyf-ID:n. Täckningsindexet från Coverage-tabellen är indexet in i den arrayen, så glyfen vid täckningsindex 0 blir den första arrayposten, täckningsindex 1 den andra, och så vidare. Format 2 används när alternativen inte ligger på en enhetlig förskjutning, vilket är det vanliga fallet för handgjorda stilistiska uppsättningar. Frågan är densamma från anroparens sida i båda fallen. Ta indataglyfen, kör den genom Coverage, och om den täcks, tillämpa deltan eller läs av positionen i arrayen.
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;
Det kontrakt som är värt att notera är genomströmningen (pass-through). GetSingleSubstituteGlyph returnerar indataglyfens ID oförändrat vid varje miss: inget typsnitt, ingen GSUB-tabell, ingen matchande funktion, ingen träff i täckningen. Det innebär att anropet är säkert att göra villkorslöst. Du frågar efter alternativet, och om det inte finns något får du tillbaka exakt det du skickade in, så anropskoden behöver aldrig hantera ett typsnitt som saknar funktionen som ett specialfall.
Vad taggarna för de stilistiska funktionerna betyder
Funktionstaggen (feature tag) är hela ordförrådet för vilket alternativ du ber om, och taggarna som är relevanta för stilistiskt arbete är en kort lista. Det viktigaste paret är salt, stilistiska alternativ (stylistic alternates), den allmänna åtkomsten till en glyfs alternativa former, och ss01 till ss20, de tjugo numrerade stilistiska uppsättningar ett typsnitt kan definiera, där var och en är ett namngivet paket av ersättningar som formgivaren har grupperat. Ett typsnitt kan till exempel lägga ett enplans-a och ett R med rakt ben under ss03, så att aktivering av den uppsättningen ändrar stilen för båda.
Runt dessa finns flera andra taggar för enkla ersättningar. aalt är access-all-alternates, föreningen av varje alternativ en glyf har, vanligtvis presenterad som en glyfpalettfunktion. titl väljer rubrikversaler (titling capitals) anpassade för stora storlekar. subs och sups byter in riktiga nedsänkta och upphöjda siffror istället för nedskalade standardalternativ. ordn producerar ordningstal (ordinal forms), de upphöjda bokstäverna i 1:a och 2:a. frac bygger bråktal, även om avancerade diagonala bråk också lutar sig mot ligaturer och kontextuell logik som går utöver enkla ersättningar. För fallen med en enskild glyf är mekanismen identisk med ss01: skicka taggen till ersättningssökningen och läs tillbaka den alternativa glyfen.
// 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;
cmap format 12 och de kompletterande planen
Innan någon ersättning kan köras måste ett tecken bli en glyf, och det är cmap-tabellens jobb. Ersättningssökningen startar från ett glyf-ID, så vägen är alltid tecken till glyf via cmap, sedan glyf till alternativ via GSUB. Den intressanta delen av cmap är dess räckvidd. En format 4-undertabell täcker Basic Multilingual Plane, de första 65536 kodpunkterna, och det är tillräckligt för den mesta latinska texten. Det är inte tillräckligt för kodpunkter från U+10000 och uppåt, de kompletterande planen (supplementary planes), vilket är där matematiska alfanumeriska tecken, många symboler och flera levande skript nu bor.
Format 12 är undertabellen som täcker hela intervallet U+0000 till U+10FFFF. Det är en sorterad lista över grupper, där varje grupp är en startkodpunkt, en slutkodpunkt och ett start-glyf-ID, så att en sammanhängande sekvens av kodpunkter mappar till en sammanhängande sekvens av glyfer. HotPDF löser kodpunkter med en hybridstrategi som matchar hur datan är utformad. Kodpunkter i BMP serveras från en direkt array indexerad efter kodpunkten, en enda sökning utan sökning i träd. Kodpunkter i de kompletterande planen serveras från en gles tabell sorterad efter kodpunkt och söks med en binärsökning. Resultatet är att GetUnicodeGlyphForCodepoint tar en hel Cardinal och svarar korrekt över hela intervallet, och returnerar glyf-ID 0, .notdef-glyfen, för alla kodpunkter som typsnittet inte mappar.
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;
Var dessa sökningar stannar
API:erna för enkla ersättningar besvarar en viss typ av fråga, och det är viktigt att vara på det klara med vad de inte besvarar. LookupType 1 är en av åtta ersättningstyper. Sökningen hananerar inte LookupType 2 (multiple substitution), där en glyf blir flera, och inte heller LookupType 4 (ligature substitution), där flera glyfer blir en. Den hanterar inte de kontextuella och kedjade kontextuella typerna, LookupType 5 och 6, som endast utlöses när en glyf visas i ett visst grannskap, och inte heller typerna för förlängning och omvänd kedjad ersättning. Ett diagonalt bråktal, en devanagarisk konjunkt eller en arabisk initial-medial-final-kaskad är ett sekvensproblem, och en sökning för enkla ersättningar per glyf kan inte uttrycka det.
Det utför inte heller automatisk formning. Ingenting här inspekterar en textsekvens, bestämmer vilka funktioner som ska slås på, och tillämpar dem i den ordning som skriptet kräver. Anroparen väljer funktionstaggen och tillämpar den glyf för glyf. Det är exakt det rätta verktyget för stilistiska uppsättningar och alternativ, som är valbara (opt-in) och lokala, och exakt fel verktyg för ett skript som behöver omordning. Att hålla gränsen skarp är det som gör att ersättningssökvägen kan förbli liten och förutsägbar.
För de fall som faktiskt kräver arbete på sekvensnivå tas historien om komplexa skript upp i vår artikel om formning av text med komplexa skript i Delphi. Om dina ersättningar är en del av ett större rapportjobb som också placerar bilder och andra typsnitt på sidan, täcker guiden för rapportutdata med typsnitt och bilder hur dessa delar passar ihop. Alla dessa körs på samma motor, HotPDF Component för Delphi och C++Builder, som bär GSUB-ersättningssökningarna tillsammans med typsnittsinbäddning, delmängdsskapande (subsetting) och text-API:er som täcks på andra ställen i den här bloggen.