En designer väljer ett typsnitt med ett enfaldigt a för rubriker, eller ett snedstreck-nolla för tabeller, eller ett set med swash-versaler för ett omslag. Dessa glyfer finns redan i typsnittet. De är helt enkelt inte standard. Standard-a mappas från tecknet via cmap-tabellen till en glyph, och alternativet sitter några glyph-id:n bort, nåbart bara via en substitutionsregel. Att producera det alternativet i en PDF innebär att läsa regeln och skicka ut den ersättande glyfen i innehållsströmmen. Den här artikeln handlar om att läsa dessa regler, enkla-substitutionssorten, i Object Pascal utan något inbyggt formningsbibliotek under
Omfånget är avsiktligt smalt. Stilistiska set och alternativ är substitutioner av typen en-glyph-in, en-glyph-ut. De är den del av OpenType-layout som du kan lösa med en liten, deterministisk tabellgenomgång, vilket gör dem lämpliga 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å "forma den här texten", och för fullständig bidirektiontell, Indic- eller arabisk formning är det rätt svar. Det är också ett C-bibliotek. Att binda in det i en Delphi- eller C++Builder-produkt innebär att leverera ett inbyggt objekt för varje målplattform och arkitektur, matcha dess anropskonvention, följa dess utgivningstakt och läsa dess licensvillkor mot dina egna. Inget av det är svårt isolerat. Allt av det är friktion som aldrig försvinner, och det köper ingenting när det faktiska kravet är "ge mig ss01-formen av den här bokstaven"
Enkel substitution behöver inte en formningsmotor. Den behöver en parser för en handfull GSUB-subtabellformat och en binär sökning eller två. Att skriva det i Pascal håller hela verktygskedjan i en kompilator. Den ärliga gränsen är att detta tillvägagångssätt hanterar glyph-substitutionsuppslag och ingenting annat. Det är inte bidi-upplösning, det är inte Indic-omordning och det är inte automatisk kontextuell formning. Där dessa behövs, behövs de, och ett per-glyph enkelt-substitutionsuppslag kan inte ersätta dem
GSUB-hierarkin, uppifrån och ned
Glyph-substitutionstabellen är organiserad som en kedja av indirekter, och en substitutionsfråga går igenom kedjan uppifrån. Längst upp finns ScriptList. En skript-tagg som latn väljer en post, och specialtaggen DFLT är standardskriptet som gäller 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 sådana för språk som behöver olika beteende. Turkiska är det vanliga exemplet, där den prickade och oprickade i kräver sin egen hantering
LangSys namnger en uppsättning funktionsindex. Varje index pekar in i FeatureList, där en funktionspost bär en fyra-byte-tagg, ss01 bland dem, och en lista med uppslagsindex. Dessa index pekar slutligen in i LookupList, där de faktiska substitutionssubtabellerna bor. Att lösa ss01 innebär alltså: hitta skriptet, hitta dess LangSys, hitta funktionen vars tagg är ss01, samla de uppslag 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 levererar, och exponerar ett sätt att åsidosätta skripttaggen när ett typsnitt kopplar sina funktioner under ett specifikt skript istället
Täckningstabeller avgör vem som deltar
Varje substitutionssubtabell börjar med samma fråga: tar den här ingångsglyfen del i den här regeln, och om så är fallet, var sitter den i regelns eget indexering? Den frågan besvaras av en täckningstabelle, och svaret är ett täckningsindex, ett litet ordningstal som resten av subtabellen använder för att slå upp vad glyfen blir
Täckning kommer i två format. Format 1 är en lista med glyph-id:n sorterade i stigande ordning. Du hittar en glyph med en binär sökning, och dess position i listan är dess täckningsindex. Format 2 är en lista med intervalsposter, var och en ett startglyph, ett slutglyph och det täckningsindex som startglyfen mappas till. En glyph inuti ett intervall får sitt täckningsindex genom att förskjuta från intervallets start. Format 1 är kompakt när de deltagande glyferna är utspridda, Format 2 när de faller i sammanhängande körningar. Båda är sorterade, så båda söks i logaritmisk tid, och båda returnerar antingen ett täckningsindex eller ett rent "inte täckt" som låter motorn lämna glyfen ensam
Enkel substitution, de två formaten
Enkel substitution är LookupType 1, och den mappar en glyph till exakt en ersättning. Den har också två format, och uppdelningen är en utrymmesoptimering. Format 1 lagrar ett enda signerat delta. Utgångsglyph-id:t är ingångsglyph-id:t plus det deltat, modulo 65536. Så här kodar ett typsnitt en substitution där varje deltagande glyph sitter på samma fasta förskjutning från sitt alternativ, till exempel ett block med lining-siffror placerade ett konstant avstånd från de matchande oldstyle-siffrorna. Täckningstabellen säger vilka glyfer som kvalificerar, och det ena deltat gäller för dem alla
Format 2 lagrar en explicit array med ersättningsglyph-id:n. Täckningsindexet från täckningstabellen är indexet i den arrayen, så glyph 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 är på en enhetlig förskjutning, vilket är det vanliga fallet för handbyggda stilistiska set. Frågan är densamma från anroparens sida oavsett. Ta ingångsglyfen, kör den genom täckning och om den täcks, tillämpa deltat eller läs arrayplatsen
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;
Kontraktet värt att notera är genomgången. GetSingleSubstituteGlyph returnerar ingångsglyph-id:t oförändrat vid varje miss: inget typsnitt, ingen GSUB-tabell, ingen matchande funktion, inget täckningsträff. Det innebär att anropet är säkert att göra ovillkorligt. Du frågar efter alternativet, och om det inte finns något, får du tillbaka exakt det du satte in, så anropskoden behöver aldrig specialhantera ett typsnitt som saknar funktionen
Vad de stilistiska funktionstaggar betyder
Funktionstaggen är hela vokabulären för vilket alternativ du frågar efter, och de taggar som är relevanta för stilistiskt arbete är en kort lista. Rubrikparet är salt, stilistiska alternativ, allomfattande tillgång till en glyfs alternativa former, och ss01 till ss20, de tjugo numrerade stilistiska set som ett typsnitt kan definiera, var och en ett namngivet paket med substitutioner som designern grupperar ihop. Ett typsnitt kan till exempel lägga ett enfaldigt a och ett rakt-ben-R under ss03, så att aktivera det enda setet stilar om båda
Runt dessa sitter flera fler enkla-substitutionstaggar. aalt är tillgång-till-alla-alternativ, unionen av varje alternativ en glyph har, vanligtvis presenterat som en glyph-palettfunktion. titl väljer rubrikversaler skurna för stora storlekar. subs och sups byter in sanna subskript- och superskripsiffror snarare än nedskalade standarder. ordn producerar ordinala former, de upphöjda bokstäverna i 1:a och 2:a. frac bygger bråk, men fullständiga diagonala bråk lutar sig också mot ligaturs- och kontextuell logik som går utanför vanlig enkel substitution. För enkelt-glyph-fallen är mekanismen identisk med ss01: skicka taggen till substitutionsfrågan och läs tillbaka alternativglyfen
// 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 substitution kan köras måste ett tecken bli en glyph, och det är cmap-tabellens jobb. Substitutionsfrågan börjar från ett glyph-id, så sökvägen är alltid tecken till glyph via cmap, sedan glyph till alternativ via GSUB. Den intressanta delen av cmap är dess räckvidd. En format 4-subtabell täcker det grundläggande flerspråkliga planet, de första 65536 kodpunkterna, och det räcker för de flesta latinska texter. Det räcker inte för kodpunkter från U+10000 uppåt, de kompletterande planen, vilket är där matematiska alfanumeriska tecken, många symboler och flera levande skrifter nu bor
Format 12 är subtabellen som täcker hela intervallet U+0000 till U+10FFFF. Det är en sorterad lista med grupper, varje grupp en startkodpunkt, en slutkodpunkt och ett startglyph-id, så en sammanhängande körning av kodpunkter mappas till en sammanhängande körning av glyfer. HotPDF löser kodpunkter med en hybridstrategi som matchar hur data är formad. Kodpunkter i BMP serveras från en direkt array indexerad efter kodpunkten, ett enda uppslag utan sökning. Kodpunkter i de kompletterande planen serveras från en gles tabell sorterad efter kodpunkt och sökt med en binär sökning. Resultatet är att GetUnicodeGlyphForCodepoint tar en fullständig Cardinal och svarar korrekt över hela intervallet, returnerar glyph-id 0, .notdef-glyfen, för valfri kodpunkt 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 frågor slutar
API:erna för enkel substitution svarar på en form av fråga, och det är värt att vara tydlig med vad de inte svarar på. LookupType 1 är en av åtta substitutionstyper. Frågan hanterar inte LookupType 2 multipel substitution, där en glyph blir flera, inte heller LookupType 4 ligaturssubstitution, där flera glyfer blir en. Den hanterar inte de kontextuella och kedjekontextuella typerna, LookupType 5 och 6, som bara aktiveras när en glyph visas i ett visst grannskap, inte heller utvidgnings- och omvänd-kedjningstyperna. Ett diagonalt bråk, ett devanagari-konjunkt eller en arabisk initial-medial-final-kaskad är ett sekvensproblem, och ett per-glyph enkelt-substitutionsuppslag kan inte uttrycka det
Det utför inte heller automatisk formning. Ingenting här inspekterar en körning av text, bestämmer vilka funktioner som ska slås på och tillämpar dem i den ordning skriptet kräver. Anroparen väljer funktionstaggen och tillämpar den glyph för glyph. Det är precis rätt verktyg för stilistiska set och alternativ, som är vallfria och lokala, och precis fel verktyg för ett skript som behöver omordning. Att hålla gränsen skarp är det som låter substitutionssökvägen förbli liten och förutsägbar
För de fall som verkligen behöver sekvensnivåarbete tas den komplexa skriptberättelsen upp i vår artikel om formning av komplex-skript-text i Delphi. Om dina substitutioner ingår i ett större rapporteringsjobb 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-substitutionsfrågorna bredvid inbäddnings-, deluppsättnings- och text-API:erna som täcks på annan plats på den här bloggen