Een ontwerper kiest een lettertype met een a met één verdieping voor koppen, of een doorgestreepte nul voor tabellen, of een set "swash" kapitalen voor een omslag. Die glyphs zitten al in het lettertype. Ze zijn simpelweg niet de standaard. De standaard a mapt van het teken door 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 betekent het lezen van de regel en het uitzenden van de vervangende glyph in de content stream. Dit artikel gaat over het lezen van die regels, van het single-substitution soort, in Object Pascal zonder een onderliggende native shaping bibliotheek
De scope is met opzet smal. Stilistische sets en alternatieven zijn single-glyph-in, single-glyph-out substituties. Ze vormen het deel van OpenType layout dat u kunt oplossen met een kleine, deterministische tabelwandeling, wat ze een goede match maakt voor 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 (shape) deze tekst", en voor volledige bidirectionele, Indische of Arabische shaping is het het juiste antwoord. Het is ook een C-bibli কমপক্ষে C-bibliotheek. Het binden ervan in een Delphi- of C++Builder-product betekent het verzenden van een native object voor elk doelplatform en architectuur, het matchen van de aanroepconventie, het bijhouden van het release-ritme en het lezen van de licentievoorwaarden tegen uw eigen voorwaarden. Niets daarvan is moeilijk op zichzelf. Maar het is allemaal frictie die nooit weggaat, en het levert niets op wanneer de eigenlijke vereiste is: "geef me de ss01-vorm van deze letter"
Single substitution heeft geen shaping engine nodig. Het heeft een parser nodig voor een handvol GSUB-subtabelformaten en een of twee binaire zoekopdrachten. Dat in Pascal schrijven houdt de hele toolchain binnen één compiler. De eerlijke grens is dat deze aanpak glyph-substitutie-lookups afhandelt en niets anders. Het is geen bidi-resolutie, het is geen Indische herordening en het is geen automatische contextuele shaping. Waar die nodig zijn, zijn ze nodig, en een single-substitution query zal daar niet voor in de plaats komen
De GSUB-hiërarchie, van boven naar beneden
De Glyph Substitution tabel is georganiseerd als een keten van indirecties, en een substitutiequery wandelt de keten van bovenaf af. Bovenaan bevindt zich de ScriptList. Een script-tag zoals latn selecteert een vermelding, en de speciale tag DFLT is het standaardscript dat van toepassing is wanneer er geen specifieker script overeenkomt. De scriptvermelding wijst naar een LangSys, het taalsysteem, met een standaard LangSys voor het algemene geval en optionele benoemde voor talen die een ander gedrag nodig hebben. Turks is het gebruikelijke voorbeeld, waar de i met en zonder punt hun eigen afhandeling vereisen
De LangSys benoemt een set feature-indices. Elke index wijst naar de FeatureList, waar een feature-record een vier-byte tag draagt, waaronder ss01, en een lijst met lookup-indices. Die indices wijzen uiteindelijk naar de LookupList, waar de eigenlijke substitutie-subtabellen leven. Dus het oplossen van ss01 betekent: vind het script, vind zijn LangSys, vind de feature waarvan de tag ss01 is, verzamel de lookups die deze benoemt, en pas ze toe. HotPDF gebruikt standaard het DFLT-script en de standaard LangSys, wat is wat de overgrote meerderheid van de ontwerpen voor Latijnse teksten meelevert, en het biedt een manier om de script-tag te overschrijven wanneer een lettertype zijn features in plaats daarvan onder een specifiek script bedraadt
Coverage-tabellen bepalen wie deelneemt
Elke substitutie-subtabel begint met dezelfde vraag: neemt deze invoerglyph deel aan deze regel, en zo ja, waar bevindt het zich in de eigen indexering van de regel. Die vraag wordt beantwoord door een Coverage-tabel, en het antwoord is een coverage-index, een klein ordinaal getal dat de rest van de subtabel gebruikt om op te zoeken wat de glyph wordt
Coverage komt in twee formaten. Formaat 1 is een lijst van glyph-id's, gesorteerd in oplopende volgorde. U vindt een glyph met een binaire zoekopdracht, en zijn positie in de lijst is zijn coverage-index. Formaat 2 is een lijst met bereikrecords, elk bestaande uit een startglyph, een eindglyph, en de coverage-index waar de startglyph naar mapt. Een glyph binnen een bereik krijgt zijn coverage-index door te compenseren 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 doorzocht in logaritmische tijd, en beide retourneren ofwel een coverage-index ofwel een schone "niet gedekt" waardoor de engine de glyph met rust kan laten
Single Substitution, de twee formaten
Single Substitution is LookupType 1, en mapt één glyph naar precies één vervanging. Het heeft ook twee formaten, en de splitsing is een ruimteoptimalisatie. Formaat 1 slaat een enkele getekende delta op. Het uitvoerglyph-id is het invoerglyph-id plus die delta, modulo 65536. Dit is hoe een lettertype een substitutie codeert waarbij elke deelnemende glyph zich op dezelfde vaste verschuiving van zijn alternatief bevindt, bijvoorbeeld een blok uithangende cijfers die op een constante afstand van de bijbehorende oude-stijl cijfers zijn geplaatst. De Coverage-tabel zegt welke glyphs in aanmerking komen, en de ene delta bedient 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 de eerste arrayvermelding, coverage-index 1 de tweede, enzovoort. Formaat 2 wordt gebruikt wanneer de alternatieven niet op een uniforme verschuiving zitten, wat het meest voorkomende geval is voor handgebouwde stilistische sets. De query is hoe dan ook hetzelfde vanaf de kant van de aanroeper. Neem de invoerglyph, laat deze door Coverage lopen, en als deze gedekt is, pas de delta toe of lees de array-slot
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 moeite waard is om op te merken is de "pass-through". GetSingleSubstituteGlyph retourneert de invoerglyph-id ongewijzigd bij elke mis: geen lettertype, geen GSUB-tabel, geen overeenkomende feature, geen coverage-hit. Dat betekent dat de aanroep onvoorwaardelijk veilig kan worden gedaan. U vraagt om het alternatief, en als er geen is, krijgt u precies terug wat u erin stopt, zodat de aanroepende code nooit een uitzondering hoeft te maken voor een lettertype dat de feature mist
Wat de stilistische feature-tags betekenen
De feature-tag is het hele vocabulaire van welk alternatief u aanvraagt, en de tags die relevant zijn voor stilistisch werk vormen een korte lijst. Het belangrijkste paar is salt, stilistische alternatieven, de allesomvattende toegang tot de alternatieve vormen van een glyph, en ss01 tot en met ss20, de twintig genummerde stilistische sets die een lettertype kan definiëren, elk een benoemde bundel substituties die de ontwerper groepeert. Een lettertype kan bijvoorbeeld een a met één verdieping en een R met een rechte poot onder ss03 plaatsen, zodat het inschakelen van die ene set beide een nieuwe stijl geeft
Daaromheen zitten nog diverse andere single-substitution tags. aalt is access-all-alternates, de vereniging van elk alternatief dat een glyph heeft, meestal gepresenteerd als een glyph-paletfunctie. titl selecteert titelkapitalen die gesneden zijn voor grote formaten. subs en sups wisselen echte subscript en superscript cijfers in, in plaats van teruggeschaalde standaardcijfers. ordn produceert ordinale vormen, de verhoogde letters in 1st en 2nd. frac bouwt breuken, hoewel volledige diagonale breuken ook leunen op ligatuur en contextuele logica die verder gaat dan eenvoudige single substitution. Voor de single-glyph gevallen is het mechanisme identiek aan ss01: geef de tag door aan de substitutiequery en lees de alternatieve glyph terug
// 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 en de supplementary planes
Voordat enige substitutie kan worden uitgevoerd, moet een teken een glyph worden, en dat is de taak van de cmap-tabel. De substitutiequery start vanaf een glyph-id, dus het pad is altijd teken naar glyph via cmap, dan glyph naar alternatief via GSUB. Het interessante deel van cmap is het bereik. Een formaat 4-subtabel dekt het Basic Multilingual Plane, de eerste 65536 codepoints, en dat is genoeg voor de meeste Latijnse tekst. Het is niet genoeg voor codepoints vanaf U+10000 en hoger, de "supplementary planes", de plek waar wiskundige alfanumerieke tekens, veel symbolen en diverse levende scripts zich nu bevinden
Formaat 12 is de subtabel die het volledige bereik van U+0000 tot U+10FFFF dekt. Het is een gesorteerde lijst van groepen, elke groep bestaande uit een startcodepoint, een eindcodepoint en een startglyph-id, zodat een aaneengesloten reeks codepoints mapt naar een aaneengesloten reeks glyphs. HotPDF lost codepoints op met een hybride strategie die overeenkomt met de manier waarop de data is gevormd. Codepoints in de BMP worden bediend vanuit een directe array, geïndexeerd op de codepoint, een enkele lookup zonder te zoeken. Codepoints in de supplementary planes worden bediend vanuit een schaarse tabel, gesorteerd op codepoint en doorzocht met een binaire zoekopdracht. Het resultaat is dat GetUnicodeGlyphForCodepoint een volledige Cardinal aanneemt en over het hele bereik correct antwoordt, en glyph-id 0, de .notdef glyph, retourneert voor elk codepoint dat het lettertype 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 query's stoppen
De single-substitution API's beantwoorden één vorm van vraag, en het is de moeite waard om duidelijk te zijn over wat ze niet beantwoorden. LookupType 1 is een van de acht substitutietypes. De query behandelt noch LookupType 2 meervoudige substitutie, waarbij één glyph er meerdere wordt, noch LookupType 4 ligatuursubstitutie, waarbij meerdere glyphs er één worden. Het behandelt niet de contextuele en chaining-contextuele types, LookupTypes 5 en 6, die alleen vuren wanneer een glyph in een bepaalde buurt verschijnt, noch de extensie- en reverse-chaining-types. Een diagonale breuk, een Devanagari-conjunct of een Arabische begin-midden-eind-cascade is een sequentieprobleem, en een per-glyph single-substitution lookup kan dit niet uitdrukken
Het voert ook geen automatische shaping uit. Niets hier inspecteert een reeks tekst, beslist welke features moeten worden ingeschakeld, en past ze toe in de volgorde die het script vereist. De aanroeper kiest de feature-tag en past deze per glyph toe. Dat is precies de juiste tool voor stilistische sets en alternatieven, die opt-in en lokaal zijn, en precies de verkeerde tool voor een script dat herordening nodig heeft. De grens scherp houden is wat het substitutiepad in staat stelt klein en voorspelbaar te blijven
Voor de gevallen die wél werk op sequentieniveau nodig hebben, wordt het verhaal van het complexe script opgenomen in ons artikel over complex-script text shaping in Delphi. Als uw substituties deel uitmaken van een grotere rapportagetaak die ook afbeeldingen en andere lettertypen op de pagina plaatst, behandelt de gids voor rapportuitvoer met lettertypen en afbeeldingen hoe die stukjes in elkaar passen. Al deze draaien op dezelfde engine, de HotPDF Component voor Delphi en C++Builder, die de GSUB-substitutiequery's bevat naast de lettertype-insluiting, subsetting en tekst-API's die elders op deze blog worden behandeld