En designer velger en font med en en-etasjes a for overskrifter, eller en null med skråstrek for tabeller, eller et sett med swash-versaler for et omslag. Disse glyfene finnes allerede i fonten. De er bare ikke standardvalget. Standard a avbildes fra tegnet gjennom cmap-tabellen til én glyf, og alternativet sitter noen få glyf-ID-er unna, kun tilgjengelig gjennom en erstatningsregel. Å produsere det alternativet i en PDF betyr å lese regelen og sende ut erstatningsglyfen i innholdsstrømmen. Denne artikkelen handler om å lese disse reglene, av typen enkelt-erstatning (single-substitution), i Object Pascal uten noe innebygd formingsbibliotek (shaping library) i bunnen.
Omfanget er snevert med vilje. Stilistiske sett og alternativer er enkelt-glyf-inn, enkelt-glyf-ut-erstatninger. De er den delen av OpenType-oppsettet du kan løse med en liten, deterministisk tabellvandring, noe som gjør at de passer godt for en Pascal-motor som ønsker å forbli fri for C-avhengigheter.
Hvorfor ren Delphi i stedet for HarfBuzz
HarfBuzz is det opplagte svaret på å "forme denne teksten", og for full toveis, indisk eller arabisk forming er det det riktige svaret. Det er også et C-bibliotek. Å binde det inn i et Delphi- eller C++Builder-produkt betyr å levere et maskinspesifikt objekt for hver målplattform og arkitektur, matche dets kallekonvensjon, spore dets oppdateringstakt og lese lisensvilkårene mot dine egne. Ingenting av det er vanskelig i seg selv. Alt er friksjon som aldri forsvinner, og det gir ingenting når det faktiske kravet er "gi meg ss01-formen av denne bokstaven".
Enkelt-erstatning trenger ikke en formingsmotor (shaping engine). Den trenger en parser for en håndfull GSUB-undertabellformater og et binærsøk eller to. Å skrive det i Pascal holder hele verktøykjeden innenfor én kompilator. Den ærlige begrensningen er at denne tilnærmingen håndterer glyferstatningsoppslag og ingenting annet. Det er ikke toveis (bidi) løsning, det er ikke indisk rekkefølgeendring, og det er ikke automatisk kontekstuell forming. Der disse trengs, trengs de, og et enkelt-erstatningsspørsmål kan ikke erstatte dem.
GSUB-hierarkiet, fra topp til bunn
Glyph Substitution-tabellen er organisert som en kjede av indireksjoner, og et erstatningsspørsmål går gjennom kjeden fra toppen. På toppen er ScriptList. En skript-tagg som latn velger en oppføring, og den spesielle taggen DFLT er standardskriptet som gjelder når ingen mer spesifikk skript matcher. Skriptoppføringen peker på et LangSys, språksystemet, med et standard LangSys for det vanlige tilfellet og valgfrie navngitte for språk som trenger ulik adferd. Tyrkisk er det vanlige eksempelet, der i med og uten prikk krever sin egen håndtering.
LangSys navngir et sett med funksjonsindekser. Hver indeks peker inn i FeatureList, der en funksjonsoppføring har en fire-byte tagg, blant annet ss01, og en liste over oppslagsindekser. Disse indeksene peker til slutt inn i LookupList, der de faktiske erstatningsundertabellene lever. Så å løse ss01 betyr: finn skriptet, finn dets LangSys, finn funksjonen som har ss01 som tagg, samle oppslagene den navngir, og ta dem i bruk. HotPDF bruker som standard DFLT-skriptet og standard LangSys, som er det de aller fleste latinske tekstdesign leveres med, og det eksponerer en måte å overstyre skript-taggen på når en font kobler funksjonene sine under et spesifikt skript i stedet.
Dekningstabeller bestemmer hvem som deltar
Hver erstatningsundertabell begynner med det samme spørsmålet: deltar denne inngangsglyfen i denne regelen, og i så fall, hvor sitter den i regelens egen indeksering. Det spørsmålet besvares av en dekningstabell (Coverage table), og svaret er en dekningsindeks, et lite ordenstall som resten av undertabellen bruker for å slå opp hva glyfen blir til.
Dekning (Coverage) kommer i to formater. Format 1 er en liste over glyf-ID-er sortert i stigende rekkefølge. Du finner en glyf med et binærsøk, og dens posisjon i listen er dens dekningsindeks. Format 2 er en liste over områdeposter (range records), der hver er en startglyf, en endeglyf, og dekningsindeksen som startglyfen avbildes til. En glyf innenfor et område får sin dekningsindeks ved å forskyve seg fra områdets start. Format 1 er kompakt når de deltakende glyfene er spredt, Format 2 når de faller inn i sammenhengende sekvenser. Begge er sortert, så det søkes i begge på logaritmisk tid, og begge returnerer enten en dekningsindeks eller en ren "not covered" (ikke dekket) som lar motoren la glyfen være i fred.
Enkelt-erstatning, de to formatene
Enkelt-erstatning (Single Substitution) er LookupType 1, og den avbilder én glyf til nøyaktig én erstatning. Den har også to formater, og oppdelingen er en plassoptimalisering. Format 1 lagrer en enkelt signert delta. Utgangsglyf-ID-en er inngangsglyf-ID-en pluss dette deltaet, modulo 65536. Dette er hvordan en font koder en erstatning der hver deltakende glyf sitter med samme faste forskyvning (offset) fra sitt alternativ, for eksempel en blokk med proporsjonale tall plassert i en konstant avstand fra de tilsvarende klassiske tallene (oldstyle figures). Dekningstabellen angir hvilke glyfer som kvalifiserer, og det ene deltaet betjener alle sammen.
Format 2 lagrer et eksplisitt array med erstatningsglyf-ID-er. Dekningsindeksen fra dekningstabellen er indeksen inn i det arrayet, slik at glyfen på dekningsindeks 0 blir det første elementet i arrayet, dekningsindeks 1 det andre, og så videre. Format 2 brukes når alternativene ikke har en ensartet forskyvning, som er det vanlige tilfellet for manuelt oppbygde stilistiske sett. Spørringen er den samme fra kallerens side uansett. Ta inngangsglyfen, kjør den gjennom dekningen (Coverage), og hvis den dekkes, bruk deltaet eller les array-plassen.
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;
Kontrakten verdt å merke seg er gjennomgangen. GetSingleSubstituteGlyph returnerer inngangsglyf-ID-en uendret ved hver bom: ingen font, ingen GSUB-tabell, ingen samsvarende funksjon, ingen dekningtreff. Det betyr at kallet er trygt å gjøre ubetinget. Du ber om alternativet, og hvis det ikke finnes noe, får du tilbake akkurat det du la inn, slik at den kallende koden aldri trenger å spesialbehandle en font som mangler funksjonen.
Hva de stilistiske funksjonstaggene betyr
Funksjonstaggen er hele ordforrådet for hvilket alternativ du ber om, og taggene som er relevante for stilistisk arbeid er en kort liste. Hovedparet er salt, stilistiske alternativer (stylistic alternates), som er samleoppføringen for tilgang til en glyfs alternative former, og ss01 til ss20, de tjue nummererte stilistiske settene en font kan definere, der hvert er en navngitt pakke med erstatninger som designeren grupperer sammen. En font kan for eksempel legge en en-etasjes a og en R med rett ben under ss03, slik at aktivering av dette settet endrer utseendet på begge.
Rundt disse sitter flere tagger for enkelt-erstatning. aalt er tilgang til alle alternativer (access-all-alternates), unionen av alle alternativer en glyf har, vanligvis presentert som en glyf-palettfunksjon. titl velger tittelsignaturer (titling capitals) kuttet for store størrelser. subs og sups bytter inn ekte senkede og hevede tall i stedet for nedskalerte standardtall. ordn produserer ordenstallformer, de hevede bokstavene i 1. og 2. frac bygger brøker, selv om fulle diagonale brøker også lener seg på ligatur- og kontekstuell logikk som går utover ren enkelt-erstatning. For tilfellene med enkeltglyf er mekanismen identisk med ss01: send taggen til erstatningsspørringen og les tilbake den alternative 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 og tilleggsplanene
Før noen erstatning kan kjøre, må et tegn bli en glyf, og det er cmap-tabellens jobb. Erstatningsspørringen starter fra en glyf-ID, så stien er alltid tegn til glyf gjennom cmap, og deretter glyf til alternativ gjennom GSUB. Den interessante delen av cmap er dens rekkevidde. En format 4 undertabell dekker Basic Multilingual Plane, de første 65536 kodepunktene, og det er nok for det meste av latinsk tekst. Det er ikke nok for kodepunkter fra U+10000 og oppover, tilleggsplanene (supplementary planes), der matematisk alfanumerikk, mange symboler og flere levende skrifter nå lever.
Format 12 er undertabellen som dekker hele området U+0000 til U+10FFFF. Det er en sortert liste over grupper, der hver gruppe er et startkodepunkt, et endekodepunkt og en startglyf-ID, slik at en sammenhengende rekke med kodepunkter avbildes til en sammenhengende rekke med glyfer. HotPDF løser kodepunkter med en hybrid strategi som samsvarer med hvordan dataene er utformet. Kodepunkter i BMP betjenes fra et direkte array indeksert etter kodepunktet, et enkelt oppslag uten søk. Kodepunkter i tilleggsplanene betjenes fra en spredt tabell sortert etter kodepunkt og søkt i med et binærsøk. Resultatet er at GetUnicodeGlyphForCodepoint tar en full Cardinal og svarer riktig over hele området, og returnerer glyf-ID 0, .notdef-glyfen, for ethvert kodepunkt fonten ikke avbilder.
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;
Hvor disse spørringene stopper
Enkelt-erstatnings-API-ene svarer på én form for spørsmål, og det er verdt å være tydelig på hva de ikke svarer på. LookupType 1 er en av åtte erstatningstyper. Spørringen håndterer ikke LookupType 2 fler-erstatning (multiple substitution), der én glyf blir til flere, og heller ikke LookupType 4 ligaturerstatning (ligature substitution), der flere glyfer blir til én. Den håndterer ikke de kontekstuelle og kjedede kontekstuelle typene, LookupTypes 5 og 6, som bare utløses når en glyf vises i et bestemt nabolag, og heller ikke utvidelses- og omvendt-kjedede typer. En diagonal brøk, en Devanagari-konjunkt eller en arabisk innledende-midtre-endelig kaskade er et sekvensproblem, og et enkelt-erstatningsoppslag per glyf kan ikke uttrykke det.
Det utfører heller ikke automatisk forming (shaping). Ingenting her inspiserer en tekstsekvens, bestemmer hvilke funksjoner som skal slås på, og bruker dem i den rekkefølgen skriptet krever. Kalleren velger funksjonstaggen og bruker den glyf for glyf. Det er akkurat det rette verktøyet for stilistiske sett og alternativer, som er valgfrie og lokale, og akkurat feil verktøy for et skript som trenger rekkefølgeendring. Å holde grensen skarp er det som gjør at erstatningsstien forblir liten og forutsigbar.
For tilfellene som krever arbeid på sekvensnivå, er historien om komplekse skrifter tatt opp i vår artikkel om forming av tekst med komplekse skrifter i Delphi. Hvis erstatningene dine er en del av en større rapportjobb som også plasserer bilder og andre fonter på siden, dekker guiden for rapportutdata med fonter og bilder hvordan disse delene passer sammen. Alle disse kjører på den samme motoren, HotPDF Component for Delphi og C++Builder, som har GSUB-erstatningsspørringene sammen med fontinnbygging, subsetting og tekst-API-er som er dekket andre steder på denne bloggen.