Technical Article

OpenType GSUB stilistiske alternativer i ren Delphi

En designer vælger en skrifttype med et et-etagers a til overskrifter, eller et nul med skråstreg til tabeller, eller et sæt swash-versaler til et bogomslag. Disse glyffer findes allerede i skrifttypen. De er blot ikke standarden. Standard-a afbildes fra tegnet via cmap-tabellen til én glyf, og alternativet sidder et par glyf-ID'er væk, kun tilgængeligt via en erstatningsregel. At producere dette alternativ i en PDF kræver, at man læser reglen og udsender erstatningsglyffen i indholdsstrømmen. Denne artikel handler om at læse disse regler af typen enkelt erstatning (single substitution) i Object Pascal uden et indfødt formningsbibliotek (shaping library) nedenunder.

Omfanget er bevidst snævert. Stilistiske sæt og alternativer er enkelt-glyf-ind, enkelt-glyf-ud erstatninger. De er den del af OpenType-layoutet, du kan løse med en lille, deterministisk tabelgennemgang, hvilket gør dem velegnede til en Pascal-motor, der ønsker at forblive fri for C-afhængigheder.

Hvorfor ren Delphi frem for HarfBuzz

HarfBuzz er det oplagte svar på at "forme denne tekst", og til fuld tovejs, indisk eller arabisk formning (shaping) er det det rigtige svar. Det er også et C-bibliotek. At binde det ind i et Delphi- eller C++Builder-produkt betyder, at man skal levere et indfødt objekt til hver målplatform og arkitektur, matche dets kaldekonvention, spore dets frigivelsestakt og læse dets licensbetingelser i forhold til dine egne. Intet af det er svært i sig selv. Men det skaber alt sammen en friktion, der aldrig forsvinder, og det giver intet, når det faktiske krav blot er "giv mig ss01-formen af dette bogstav".

Enkelt erstatning (single substitution) kræver ikke en formningsmotor. Det kræver en parser til en håndfuld GSUB-undertabelformater og en binær søgning eller to. At skrive det i Pascal holder hele værktøjskæden inden i én compiler. Den reelle begrænsning er, at denne tilgang håndterer opslag af glyferstatninger og intet andet. Det er ikke bidi-opløsning, det er ikke indisk omorganisering, og det er ikke automatisk kontekstuel formning. Hvor der er brug for disse ting, er der brug for dem, og en enkelt erstatningsforespørgsel kan ikke erstatte dem.

GSUB-hierarkiet, fra top til bund

Glyferstatningstabellen (Glyph Substitution table) er organiseret som en kæde af indirektioner, og en erstatningsforespørgsel gennemgår kæden fra toppen. Øverst er ScriptList. Et script-tag såsom latn vælger en post, og det specielle tag DFLT er standardscriptet, der gælder, når intet mere specifikt script matcher. Scriptposten peger på et LangSys, sprog-systemet, med et standard-LangSys til det almindelige tilfælde og valgfrie navngivne til sprog, der kræver en anden adfærd. Tyrkisk er det sædvanlige eksempel, hvor det prikkede og prikløse i kræver deres egen håndtering.

LangSys navngiver et sæt funktionsindeks (feature indices). Hvert indeks peger ind i FeatureList, hvor en funktionspost bærer et tag på fire byte, herunder ss01, og en liste over opslagsindeks. Disse indeks peger endelig ind i LookupList, hvor de faktiske erstatningsundertabeller bor. Så at løse ss01 betyder: Find scriptet, find dets LangSys, find den funktion, hvis tag er ss01, saml de opslag, den navngiver, og anvend dem. HotPDF bruger som standard DFLT-scriptet og standard-LangSys, hvilket er det, langt de fleste latinske tekstskrifttyper leveres med, og det tilbyder en måde at tilsidesætte script-tagget på, når en skrifttype i stedet forbinder sine funktioner under et bestemt script.

Dækningstabeller bestemmer, hvem der deltager

Enhver erstatningsundertabel begynder med det samme spørgsmål: Deltager denne inputglyf i denne regel, og i givet fald, hvor sidder den i reglens egen indeksering. Det spørgsmål besvares af en dækningstabel (Coverage table), og svareet er et dækningsindeks, en lille ordinal, som resten af undertabellen bruger til at slå op, hvad glyffen bliver til.

Dækning (coverage) findes i to formater. Format 1 is en liste over glyf-ID'er sorteret i stigende rækkefølge. Du finder en glyf med en binær søgning, og dens position i listen er dens dækningsindeks. Format 2 er en liste over områdeposter, hver især en startglyf, en slutglyf og det dækningsindeks, som startglyffen afbilder til. En glyf inden for et område får sit dækningsindeks bygget fra områdets start. Format 1 er kompakt, når de deltagende glyffer er spredt, Format 2 når de falder i sammenhængende forløb. Begge er sorteret, så der søges i begge i logaritmisk tid, og begge returnerer enten et dækningsindeks eller et rent "ikke dækket", der lader motoren lade glyffen være.

Enkelt erstatning, de to formater

Enkelt erstatning (Single Substitution) er LookupType 1, og den afbilder én glyf til præcis én erstatning. Den har også to formater, og opdelingen er en pladsoptimering. Format 1 gemmer en enkelt delta med fortegn. Outputglyf-ID'et er inputglyf-ID'et plus denne delta, modulo 65536. Det er sådan, en skrifttype koder en erstatning, hvor hver deltagende glyf sidder med samme faste forskydning (offset) fra sit alternativ, for eksempel en blok af lining-tal placeret i konstant afstand fra de tilsvarende oldstyle-tal. Coverage-tabellen angiver, hvilke glyffer der kvalificerer sig, og den ene delta betjener dem alle.

Format 2 gemmer et eksplicit array af erstatningsglyf-ID'er. Dækningsindekset fra Coverage-tabellen er indekset i dette array, så glyffen ved dækningsindeks 0 bliver til det første element i arrayet, dækningsindeks 1 til det andet osv. Format 2 bruges, når alternativerne ikke har en ensartet forskydning, hvilket er det mest almindelige for håndbyggede stilistiske sæt. Forespørgslen er den samme fra kalderens side i begge tilfælde. Tag inputglyffen, kør den gennem Coverage, og hvis den er dækket, skal du anvende deltaet eller læse array-pladsen.

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, der er værd at bemærke, er direkte videregivelse (pass-through). GetSingleSubstituteGlyph returnerer inputglyf-ID'et uændret ved ethvert fejlslagent forsøg: ingen skrifttype, ingen GSUB-tabel, ingen matchende funktion, intet dækningshit. Det betyder, at kaldet er sikkert at foretage ubetinget. Du beder om alternativet, og hvis der ikke er noget, får du præcis det tilbage, du puttede ind, så den kaldende kode behøver aldrig at behandle en skrifttype, der mangler funktionen, som et specialtilfælde.

Hvad de stilistiske funktionstags betyder

Funktionstagget (feature tag) er hele ordforrådet for, hvilket alternativ du beder om, og de tags, der er relevante for stilistisk arbejde, er en kort liste. Hovedparret er salt, stilistiske alternativer, den generelle adgang til en glyfs alternative former, og ss01 til ss20, de tyve nummererede stilistiske sæt, en skrifttype kan definere, hver især en navngiven pakke af erstatninger, som designeren grupperer sammen. En skrifttype kan for eksempel lægge et et-etagers a og et R med lige ben under ss03, så aktivering af dette ene sæt ændrer stilen på begge.

Omkring disse sidder flere andre enkelt erstatningstags. aalt er access-all-alternates, foreningen af alle alternativer, en glyf har, normalt præsenteret som en glyf-palet-funktion. titl vælger titling-versaler skåret til store størrelser. subs og sups skifter til ægte sænkede og hævede tal frem for skalerede standarder. ordn producerer ordinalformer, de hævede bogstaver i 1st og 2nd. frac bygger brøker, selvom fulde diagonale brøker også læner sig op ad ligatur- og kontekstuel logik, der går ud over almindelig enkelt erstatning. For enkelt-glyf-tilfælde er mekanismen identisk med ss01: Send tagget til erstatningsforespørgslen og læs den alternative glyf tilbage.

// 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 de supplerende planer

Før en erstatning kan køre, skal et tegn blive til en glyf, og det er cmap-tabellens opgave. Erstatningsforespørgslen starter fra et glyf-ID, så stien er altid tegn til glyf via cmap, derefter glyf to alternativ via GSUB. Den interessante del af cmap er dens rækkevidde. En format 4-undertabel dækker Basic Multilingual Plane (BMP), de første 65536 kodepunkter, og det er nok til det meste latinske tekst. Det er ikke nok til kodepunkter fra U+10000 og opefter, de supplerende planer (supplementary planes), hvor matematiske alfanumeriske tegn, mange symboler og flere nulevende skrifter nu bor.

Format 12 er den undertabel, der dækker hele intervallet U+0000 til U+10FFFF. Det er een sorteret liste over grupper, hvor hver gruppe har et startkodepunkt, et slutkodepunkt og et startglyf-ID, så et sammenhængende forløb af kodepunkter afbildes til et sammenhængende forløb af glyffer. HotPDF løser kodepunkter med en hybrid strategi, der matcher, hvordan dataene er formet. Kodepunkter i BMP betjenes fra et direkte array indekseret efter kodepunktet, et enkelt opslag uden søgning. Kodepunkter i de supplerende planer betjenes fra en spredt tabel, der er sorteret efter kodepunkt og søges i med en binær søgning. Resultatet er, at GetUnicodeGlyphForCodepoint tager en fuld Cardinal og svarer korrekt over hele intervallet, idet det returnerer glyf-ID 0, .notdef-glyffen, for ethvert kodepunkt, som skrifttypen ikke afbilder.

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 forespørgsler stopper

API'erne til enkelt erstatning (single-substitution) besvarer én form for spørgsmål, og det er vigtigt at være klar over, hvad de ikke besvarer. LookupType 1 is en ud af otte erstatningstyper. Forespørgslen håndterer ikke LookupType 2 multipel erstatning (multiple substitution), hvor én glyf bliver til flere, og heller ikke LookupType 4 ligatur-erstatning (ligature substitution), hvor flere glyffer bliver til én. Den håndterer ikke de kontekstuelle og kædede-kontekstuelle typer, LookupType 5 og 6, der kun udløses, når en glyf optræder i et bestemt nabolag, og heller ikke udvidelses- og omvendt-kædede typer. En diagonal brøk, en Devanagari-konjunkt eller en arabisk start-midt-slut-kaskade er et sekvensproblem, og et enkelt erstatningsopslag pr. glyf kan ikke udtrykke det.

Det udfører heller ikke automatisk formning (shaping). Intet her inspicerer en tekstblok, beslutter hvilke funktioner der skal aktiveres, og anvender them i den rækkefølge, som skriften kræver. Kalderen vælger funktionstagget og anvender det glyf for glyf. Det er præcis det rigtige værktøj til stilistiske sæt og alternativer, som er tilvalgte og lokale, og præcis det forkerte værktøj til en skrift, der kræver omorganisering. At holde grænsen skarp er det, der gør det muligt for erstatningsstien at forblive lille og forudsigelig.

For de tilfælde, der kræver arbejde på sekvensniveau, det komplekse skriftforløb tages op i vores artikel om tekstformning af komplekse skrifter i Delphi. Hvis dine erstatninger er en del af en større rapportopgave, der også placerer billeder og andre skrifttyper på siden, dækker vejledningen til rapportoutput med skrifttyper og billeder, hvordan disse dele passer sammen. Alle disse kører på den samme motor, HotPDF Component til Delphi og C++Builder, som bærer GSUB-erstatningsforespørgslerne sammen med API'erne til skrifttypeindlejring, subsetting og tekst, der er dækket andre steder på denne blog.