Teknisk artikel

OpenType GSUB stilistiske alternativer i ren Delphi

En designer vælger en skrifttype med et et-etages a til overskrifter, eller et streget nul til tabeller, eller et sæt svirpede store bogstaver til et omslag. Disse glyffer er allerede i skrifttypen. De er simpelthen bare ikke standard. Standard a afbildes fra tegnet gennem cmap-tabellen til én glyph, og alternativet sidder et par glyph-ids væk, og kan kun nås gennem en substitutionsregel. At producere det alternativ i en PDF betyder at læse reglen og udsende erstatningsglyffen i indholdsstrømmen. Denne artikel handler om at læse disse regler, af typen enkeltsubstitution, i Object Pascal uden noget native formningsbibliotek nedenunder

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

Hvorfor ren Delphi i stedet for HarfBuzz

HarfBuzz er det åbenlyse svar på at "forme denne tekst", og for fuld tovejs, indisk eller arabisk formning er det det rigtige svar. Det er også et C-bibliotek. At binde det ind i et Delphi- eller C++Builder-produkt betyder at levere et native objekt til hver målplatform og arkitektur, matche dets kaldekonvention, spore dets udgivelsesrytme og læse dets licensvilkår mod dine egne. Intet af dette er svært isoleret set. Det er alt sammen gnidning, der aldrig forsvinder, og det køber ingenting, når det faktiske krav er "giv mig ss01-formen af dette bogstav"

Enkeltsubstitution har ikke brug for en formningsmotor. Det har brug for 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 inde i én compiler. Den ærlige grænse er, at denne tilgang håndterer glyph-substitutionsopslag og intet andet. Det er ikke bidi-opløsning, det er ikke indisk omorganisering, og det er ikke automatisk kontekstuel formning. Hvor de er nødvendige, er de nødvendige, og en enkeltsubstitutionsforespørgsel vil ikke træde i stedet for dem

GSUB-hierarkiet, fra top til bund

Glyph Substitution-tabellen er organiseret som en kæde af indirektioner, og en substitutionsforespø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 det standardscript, der gælder, når der ikke er noget mere specifikt script, der matcher. Script-posten peger på et LangSys, sprogsystemet, med et standard-LangSys for det almindelige tilfælde og valgfrie navngivne for sprog, der kræver en anden adfærd. Tyrkisk er det sædvanlige eksempel, hvor det prikkede og uprikkede i kræver deres egen håndtering

LangSys navngiver et sæt funktionsindekser. Hvert indeks peger ind i FeatureList, hvor en funktionspost bærer et fire-byte-tag, blandt dem ss01, og en liste over opslagsindekser. Disse indekser peger endelig ind i LookupList, hvor de faktiske substitutionsundertabeller lever. Så at løse ss01 betyder: find scriptet, find dets LangSys, find den funktion, hvis tag er ss01, saml de opslag, det navngiver, og anvend dem. HotPDF standardiserer til DFLT-scriptet og standard-LangSys, hvilket er det, langt de fleste latinske tekstdesign leverer, og det afslører en måde at tilsidesætte script-tagget, når en skrifttype i stedet forbinder sine funktioner under et specifikt script

Dækningstabeller bestemmer, hvem der deltager

Hver substitutionsundertabel begynder med det samme spørgsmål: deltager denne input-glyph i denne regel, og hvis ja, hvor sidder den i reglens egen indeksering. Det spørgsmål besvares af en dækningstabel (Coverage table), og svaret er et dækningsindeks, et lille ordenstal, som resten af undertabellen bruger til at slå op, hvad glyffen bliver til

Dækning kommer i to formater. Format 1 er en liste over glyph-ids sorteret i stigende rækkefølge. Du finder en glyph med en binær søgning, og dens position på listen er dens dækningsindeks. Format 2 er en liste over intervalposter, hver især en start-glyph, en slut-glyph og det dækningsindeks, som start-glyffen knyttes til. En glyph inden for et interval får sit dækningsindeks ved at forskyde fra intervallets 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å begge gennemsøges på logaritmisk tid, og begge returnerer enten et dækningsindeks eller en ren "ikke dækket", der lader motoren lade glyffen være i fred

Enkeltsubstitution, de to formater

Enkeltsubstitution er LookupType 1, og den afbilder én glyph til nøjagtig én erstatning. Den har også to formater, og opdelingen er en pladsoptimering. Format 1 gemmer et enkelt signeret delta. Output-glyph-iddet er input-glyph-iddet plus det delta, modulo 65536. Sådan koder en skrifttype en substitution, hvor hver deltagende glyph sidder på samme faste forskydning fra sit alternativ, for eksempel en blok af linjefigurer placeret i en konstant afstand fra de matchende gammeldags figurer. Dækningstabellen fortæller, hvilke glyffer der kvalificerer sig, og det ene delta tjener dem alle

Format 2 gemmer et eksplicit array af erstatnings-glyph-ids. Dækningsindekset fra dækningstabellen er indekset i det array, så glyph ved dækningsindeks 0 bliver den første array-post, dækningsindeks 1 den anden og så videre. Format 2 bruges, når alternativerne ikke er på en ensartet forskydning, hvilket er det almindelige tilfælde for håndbyggede stilistiske sæt. Forespørgslen er den samme fra kalderens side uanset hvad. Tag input-glyffen, kør den gennem Dækning, og hvis den er dækket, skal du anvende deltaet eller læse array-sporet

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;

Den kontrakt, der er værd at bemærke, er gennemløbet. GetSingleSubstituteGlyph returnerer input-glyph-iddet uændret ved hver misser: 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 nøjagtigt det tilbage, som du lagde i det, så den kaldende kode behøver aldrig at lave en specialbehandling for en skrifttype, der mangler funktionen

Hvad de stilistiske funktionstags betyder

Funktionstagget 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, fang-alt-adgangen til en glyphs alternative former, og ss01 til ss20, de tyve nummererede stilistiske sæt en skrifttype kan definere, hver især et navngivet bundt af substitutioner, som designeren grupperer sammen. En skrifttype kan for eksempel sætte et et-etages a og et lige-benet R under ss03, så aktivering af det ene sæt ændrer stilen for dem begge

Omkring disse sidder flere andre tags for enkeltsubstitution. aalt er access-all-alternates, foreningen af ethvert alternativ, en glyph har, normalt præsenteret som en glyph-palette-funktion. titl vælger titler med store bogstaver, der er skåret til store størrelser. subs og sups bytter til sande sænkede og hævede figurer i stedet for nedskalerede standarder. ordn producerer ordenstal, 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 forbi almindelig enkeltsubstitution. For tilfældene med en-glyph er mekanismen identisk med ss01: send tagget til substitutionsforespørgslen og læs den alternative glyph 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 nogen substitution kan køre, skal et tegn blive til en glyph, og det er cmap-tabellens opgave. Substitutionsforespørgslen starter fra et glyph-id, så stien er altid tegn til glyph gennem cmap, og derefter glyph til alternativ gennem GSUB. Den interessante del af cmap er dens rækkevidde. En format 4-undertabel dækker Basic Multilingual Plane, de første 65536 kodepunkter, og det er nok til det meste latinske tekst. Det er ikke nok til kodepunkter fra U+10000 og opad, de supplerende planer, hvor matematiske alfanumeriske tegn, mange symboler og flere levende scripts nu bor

Format 12 er den undertabel, der dækker hele intervallet fra U+0000 til U+10FFFF. Det er en sorteret liste af grupper, hvor hver gruppe er et startkodepunkt, et slutkodepunkt og et start-glyph-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 serveres fra et direkte array indekseret efter kodepunktet, et enkelt opslag uden søgning. Kodepunkter i de supplerende planer serveres fra en sparsom tabel sorteret efter kodepunkt og gennemsøgt med en binær søgning. Resultatet er, at GetUnicodeGlyphForCodepoint tager en fuld Cardinal og svarer korrekt på tværs af hele spektret, og returnerer glyph-id 0, .notdef-glyffen, for ethvert kodepunkt, 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

De enkelte substitutions-API'er besvarer én form for spørgsmål, og det er værd at være klar over, hvad de ikke besvarer. LookupType 1 er en af otte substitutionstyper. Forespørgslen håndterer ikke LookupType 2 flersubstitution, hvor én glyph bliver til flere, og heller ikke LookupType 4 ligatursubstitution, hvor flere glyffer bliver til én. Den håndterer ikke de kontekstuelle og kædekontekstuelle typer, LookupType 5 og 6, der kun udløses, når en glyph optræder i et bestemt nabolag, ej heller forlængelses- og omvendt-kædetyperne. En diagonal brøk, en devanagari-konjunkt eller en arabisk kaskade fra start-midt-slut er et sekvensproblem, og et enkelt substitutionsopslag pr. glyph kan ikke udtrykke det

Det udfører heller ikke automatisk formning. Intet her inspicerer et forløb af tekst, beslutter, hvilke funktioner der skal slås til, og anvender dem i den rækkefølge, scriptet kræver. Kalderen vælger funktionstagget og anvender det glyph for glyph. Det er præcis det rigtige værktøj til stilistiske sæt og alternativer, som er valgfrie og lokale, og præcis det forkerte værktøj til et script, der skal omorganiseres. At holde grænsen skarp er det, der gør, at substitutionsstien forbliver lille og forudsigelig

For de tilfælde, der kræver arbejde på sekvensniveau, er komplekse script-historien taget op i vores artikel om kompleks script-tekstformning i Delphi. Hvis dine substitutioner er en del af en større rapporteringsopgave, der også placerer billeder og andre skrifttyper på siden, dækker guiden til rapportoutput med skrifttyper og billeder, hvordan de stykker passer sammen. Alle disse kører på den samme motor, HotPDF Component til Delphi og C++Builder, som bærer GSUB-substitutionsforespørgslerne sammen med indlejring af skrifttyper, underopsætning og tekst-API'er, der er dækket andre steder på denne blog