Een ontwerper kiest een lettertype met een 'enkeldeks' a voor koppen, of een nul met een schuine streep voor tabellen, of een set sierkapitalen (swash capitals) voor een omslag. Die glyphs zitten al in het lettertype. Ze zijn simpelweg niet de standaard. De standaard-a mapt van het teken via de cmap-tabel naar één glyph, en het alternatief bevindt zich een paar glyph-ID's verderop, alleen bereikbaar via een substitutieregel. Het produceren van dat alternatief in een PDF vereist het lezen van de regel en het verzenden van de vervangende glyph in de inhoudsstroom. Dit artikel gaat over het lezen van die regels, van het type enkele substitutie (single-substitution), in Object Pascal zonder onderliggende native shaping-bibliotheek.
De reikwijdte is bewust smal gehouden. Stylistische sets en alternatieven zijn substituties van één glyph in, één glyph uit. Ze vorming het deel van de OpenType-indeling dat u kunt oplossen met een kleine, deterministische tabeltocht. Hierdoor passen ze goed bij een Pascal-engine die vrij wil blijven van C-afhankelijkheden.
Waarom pure Delphi in plaats van HarfBuzz
HarfBuzz is het voor de hand liggende antwoord op 'vorm deze tekst', en voor volledige bidirectionele, Indische of Arabische vormgeving is het de juiste oplossing. Het is echter ook een C-bibliotheek. Het koppelen ervan in een Delphi- of C++Builder-product betekent dat u een native object moet meeleveren voor elk doelplatform en elke architectuur, rekening moet houden met de aanroepende conventie, de updates moet bijhouden en de licentievoorwaarden moet toetsen aan de uwe. Niets daarvan is op zichzelf moeilijk, maar het zorgt voor constante frictie die nooit verdwijnt. Bovendien levert het niets op wanneer de daadwerkelijke eis louter is: 'geef me de ss01-vorm van deze letter'.
Enkele substitutie heeft geen shaping-engine nodig. Het vereist een parser voor een handvol GSUB-subtabelformaten en een of twee binaire zoekopdrachten. Door dat in Pascal te schrijven, blijft de hele toolchain binnen één compiler. De eerlijke beperking is dat deze benadering alleen glyph-substitutiezoekopdrachten afhandelt en niets anders. Het is geen bidi-resolutie, geen Indische herordening en geen automatische contextuele vormgeving. Waar die nodig zijn, zijn ze nodig, en een opvraging van het type enkele substitutie is daarvoor geen vervanging.
De GSUB-hiërarchie, van boven naar beneden
De Glyph Substitution-tabel (GSUB) is georganiseerd als een keten van indirecte verwijzingen, en een substitutie-opvraging doorloopt de keten vanaf de bovenkant. Bovenaan staat de ScriptList. Een script-tag zoals latn selecteert een vermelding, en de speciale tag DFLT is the standaardscript dat van toepassing is wanneer er geen specifieker script overeenkomt. De scriptvermelding verwijst naar een LangSys, het taalsysteem, met een standaard LangSys voor het algemene geval en optionele benoemde systemen voor talen die ander gedrag vereisen. Turks is het gebruidelijke voorbeeld, waarbij de i met en zonder punt hun eigen afhandeling vereisen.
De LangSys benoemt een set feature-indexen. Elke index verwijst naar de FeatureList, waar een feature-record een vier-byte tag bevat (waaronder ss01) en een lijst met lookup-indexen. Die indexen verwijzen uiteindelijk naar the LookupList, waar de daadwerkelijke substitutie-subtabellen zich bevinden. Dus het oplossen van ss01 betekent: vind het script, vind de LangSys ervan, vind de feature met de tag ss01, verzamel de lookups die deze benoemt en pas ze toe. HotPDF maakt standaard gebruik van het DFLT-script en de standaard LangSys, wat de overgrote meerderheid van Latijnse tekstontwerpen gebruikt, en biedt een manier om de script-tag te overschrijven wanneer een font zijn features onder een specifiek script registreert.
Coverage-tabellen bepalen wie deelneemt
Elke substitutie-subtabel begint met dezelfde vraag: neemt deze invoer-glyph deel aan deze regel, en zo ja, waar bevindt deze zich in de indexering van de regel zelf. Die vraag wordt beantwoord door een Coverage-tabel, en het antwoord is een coverage-index, een klein rangtelwoord dat de rest van de subtabel gebruikt om op te zoeken wat de glyph wordt.
Coverage is beschikbaar in twee formaten. Formaat 1 is een lijst van glyph-ID's die in oplopende volgorde zijn gesorteerd. U vindt een glyph via een binaire zoekopdracht, en de positie in de lijst is de bijbehorende coverage-index. Formaat 2 is een lijst van bereikrecords, elk bestaande uit een start-glyph, een eind-glyph en de coverage-index waarnaar de start-glyph mapt. Een glyph binnen een bereik krijgt zijn coverage-index door te compenseren (offsetting) vanaf het begin van het bereik. Formaat 1 is compact wanneer de deelnemende glyphs verspreid zijn, Formaat 2 wanneer ze in aaneengesloten reeksen vallen. Beide zijn gesorteerd, dus beide worden in logaritmische tijd doorzocht, en beide retourneren ofwel een coverage-index ofwel een duidelijke 'not covered' waardoor de engine de glyph ongemoeid laat.
Enkele substitutie, de twee formaten
Enkele substitutie (Single Substitution) is LookupType 1, en mapt één glyph naar exact één vervanging. Dit heeft ook twee formaten, en de splitsing is een ruimteoptimalisatie. Formaat 1 slaat een enkele signed delta op. Het uitgangs-glyph-ID is het ingangs-glyph-ID plus die delta, modulo 65536. Dit is hoe een font een substitutie codeert waarbij elke deelnemende glyph zich op dezelfde vaste afstand (offset) van zijn alternatief bevindt, bijvoorbeeld een blok gelijnde cijfers (lining figures) dat op een constante afstand van de bijbehorende oude cijfers (oldstyle figures) is geplaatst. De Coverage-tabel geeft aan welke glyphs in aanmerking komen, en de enkele delta dient voor ze allemaal.
Formaat 2 slaat een expliciete array van vervangende glyph-ID's op. De coverage-index uit de Coverage-tabel is de index in die array, dus de glyph op coverage-index 0 wordt het eerste array-element, coverage-index 1 het tweede, enzovoort. Formaat 2 wordt gebruikt wanneer de alternatieven zich niet op een uniforme afstand bevinden, wat het gebruikelijke geval is voor handmatig gebouwde stylistiche sets. De opvraging is aan de kant van de beller in beide gevallen hetzelfde. Neem de invoer-glyph, leid deze door de Coverage, en als deze is gedekt, pas dan de delta toe of lees de array-positie.
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;
Het contract dat de aandacht verdient is de pass-through. GetSingleSubstituteGlyph retourneert het ingevoerde glyph-ID ongewijzigd bij elke mislukking: geen font, geen GSUB-tabel, geen overeenkomende feature, geen coverage-treffer. Dit betekent dat de aanroep veilig onvoorwaardelijk kan worden gedaan. U vraagt om het alternatief, en als dat er niet is, krijgt u precies terug wat u hebt ingevoerd, zodat de aanroepende code nooit een speciaal geval hoeft te maken voor een font dat de feature mist.
Wat de stylistiche feature-tags betekenen
De feature-tag vormt de hele definitie van welk alternatief u aanvraagt, en de tags die relevant zijn voor stylistisch werk vormen een korte lijst. Het belangrijkste paar is salt, stylistiche alternatieven, de algemene toegang tot de alternatieve vormen van een glyph, en ss01 tot en met ss20, de twintig genummerde stylistiche sets die een font kan definiëren, elk een benoemde bundel substituties die de ontwerper groepeert. Een font kan bijvoorbeeld een 'enkeldeks' a en een R met een rechte poot onder ss03 plaatsen, zodat het inschakelen van die ene set beide aanpast.
Daaromheen bevinden zich nog enkele tags voor enkele substitutie. aalt staat voor access-all-alternates, de unie van elk alternatief dat een glyph heeft, meestal gepresenteerd als een glyph-paletfunctie. titl selecteert titelkapitalen die zijn ontworpen voor grote formaten. subs en sups wisselen in voor echte sub- en superscriptcijfers in plaats van geschaalde standaardcijfers. ordn produceert ordinale vormen, de verhoogde letters in 1st en 2nd. frac bouwt breuken, hoewel volledige diagonale breuken ook leunen op ligaturen en contextuele logica die verder gaat dan gewone enkele substitutie. Voor de gevallen met één glyph is het mechanisme identiek aan ss01: geef de tag door aan de substitutie-opvraging en lees de alternatieve glyph uit.
// 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-formaat 12 en de aanvullende vlakken
Voordat een substitutie kan worden uitgevoerd, moet een teken een glyph worden, en dat is de taak van de cmap-tabel. De substitutie-opvraging begint bij een glyph-ID, dus het pad is altijd van teken naar glyph via cmap, en vervolgens van glyph naar alternatief via GSUB. Het interessante deel van cmap is het bereik. Een formaat 4-subtabel dekt het Basic Multilingual Plane (BMP), de eerste 65.536 codepunten, en dat is voldoende voor de meeste Latijnse tekst. Het is niet voldoende voor codepunten vanaf U+10000 en hoger, de aanvullende vlakken (supplementary planes), waar wiskundige alfanumerieke tekens, veel symbolen en diverse levende schriften zich nu bevinden.
Formaat 12 is the subtabel die het volledige bereik van U+0000 tot U+10FFFF dekt. Het is een gesorteerde lijst van groepen, waarbij elke groep bestaat uit een start-codepunt, een eind-codepunt en een start-glyph-ID, zodat een aaneengesloten reeks codepunten mapt naar een aaneengesloten reeks glyphs. HotPDF lost codepunten op met een hybride strategie dat aansluit bij de vorm van de gegevens. Codepunten in het BMP worden bediend vanuit een directe array die is geïndexeerd op basis van het codepunt, een enkele opvraging zonder zoekopdracht. Codepunten in de aanvullende vlakken worden bediend vanuit een sparse-tabel die is gesorteerd op codepunt en doorzocht met een binaire zoekopdracht. Het resultaat is dat GetUnicodeGlyphForCodepoint een volledige Cardinal accepteert en correct reageert over het hele bereik, waarbij glyph-ID 0, de .notdef-glyph, wordt geretourneerd voor elk codepunt dat het font niet mapt.
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;
Waar deze opvragingen ophouden
De API's voor enkele substitutie beantwoorden één soort vraag, en het is goed om duidelijk te zijn over wat ze niet beantwoorden. LookupType 1 is een van de acht substitutietypen. De opvraging handelt geen LookupType 2 meervoudige substitutie af, waarbij één glyph er meerdere wordt, noch LookupType 4 ligaturensubstitutie, waarbij meerdere glyphs één worden. Het handelt ook niet de contextuele en keten-contextuele typen af (LookupTypes 5 en 6), die alleen worden geactiveerd wanneer een glyph in een bepaalde omgeving verschijnt, noch de extensie- en omgekeerde-ketentypen. Een diagonale breuk, een Devanagari-conjunct of een Arabische initiaal-mediaal-finaal-cascade is een volgordeprobleem, en een zoekopdracht voor enkele substitutie per glyph kan dit niet uitdrukken.
Het voert ook geen automatische vormgeving (shaping) uit. Niets hier inspecteert een reeks tekst, beslist welke features moeten worden ingeschakeld en past ze toe in de volgorde die het schrift vereist. De aanroeper kiest de feature-tag en past deze glyph voor glyph toe. Dat is precies het juiste hulpmiddel voor stylistiche sets en alternatieven, die opt-in en lokaal zijn, en precies het verkeerde hulpmiddel voor een schrift dat herordening vereist. Door de grens scherp te houden, blijft het substitutiepad klein en voorspelbaar.
Voor de gevallen die wel werk op reeksniveau vereisen, de complexe-schriften-structuur wordt behandeld in ons artikel over tekstvormgeving voor complexe schriften in Delphi. Als uw substituties deel uitmaken van een grotere rapportageopdracht die ook afbeeldingen en andere fonts op de pagina plaatst, de gids voor rapportuitvoer met lettertypen en afbeeldingen covers how those pieces fit together. Al deze functies draaien op dezelfde engine, the HotPDF Component voor Delphi en C++Builder, die de GSUB-substitutie-opvragingen bevat naast de API's voor font-inbedding, subsetting en tekst die elders op deze blog worden behandeld.