Teknisk artikkel

OpenType GSUB-stilsalternativer i ren Delphi

En designer velger en skrift med en én-etasjes a for overskrifter, eller en null med skråstrek for tabeller, eller et sett med swash-store bokstaver for et omslag. Disse glyfene er i skriften allerede. De er bare ikke standarden. Standard-a-en kartlegges fra tegnet gjennom cmap-tabellen til én glyf, og alternativet sitter noen glyf-ID-er unna, og kan bare nås gjennom en substitusjonsregel. Å produsere dette alternativet i en PDF betyr å lese regelen og sende ut erstatningsglyfen i innholdsstrømmen. Denne artikkelen handler om å lese disse reglene, av typen for enkeltsubstitusjon, i Object Pascal uten noe opprinnelig formingsbibliotek under

Omfanget er snevert med vilje. Stilsett og alternativer er substitusjoner av typen én-glyf-inn, én-glyf-ut. De er den delen av OpenType-layouten du kan løse med en liten, deterministisk tabellgjennomgang, noe som gjør dem til en god passform for en Pascal-motor som ønsker å holde seg fri for C-avhengigheter

Hvorfor ren Delphi i stedet for HarfBuzz

HarfBuzz er det åpenbare svaret på "form 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 opprinnelig objekt for hver målplattform og arkitektur, matche anropskonvensjonen, spore utgivelsesrytmen og lese lisensvilkårene mot dine egne. Ingenting av dette er vanskelig isolert sett. Alt dette er friksjon som aldri forsvinner, og det kjøper ingenting når det faktiske kravet er "gi meg ss01-formen til denne bokstaven"

Enkeltsubstitusjon trenger ikke en formingsmotor. 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 inne i én kompilator. Den ærlige grensen er at denne tilnærmingen håndterer glyfsubstitusjonsoppslag og ingenting annet. Det er ikke bidi-oppløsning, det er ikke indisk omorganisering, og det er ikke automatisk kontekstuell forming. Der disse er nødvendige, er de nødvendige, og en enkeltsubstitusjonsspørring vil ikke fungere som en erstatning for dem

GSUB-hierarkiet, fra topp til bunn

Glyph Substitution-tabellen er organisert som en kjede av indireksjoner, og en substitusjonsspørring går gjennom kjeden fra toppen. På toppen er ScriptList. En skript-tag som latn velger en oppføring, og spesial-taggen DFLT er standard-skriptet som gjelder når ingen mer spesifikke skript stemmer overens. Skriptoppføringen peker på et LangSys, språksystemet, med et standard LangSys for det vanlige tilfellet og valgfrie navngitte for språk som trenger forskjellig oppførsel. Tyrkisk er det vanlige eksemplet, 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 bærer en tag på fire byte, ss01 blant dem, og en liste over oppslagsindekser. Disse indeksene peker til slutt inn i LookupList, der de faktiske substitusjons-undertabellene bor. Så å løse ss01 betyr: finn skriptet, finn dens LangSys, finn funksjonen hvis tag er ss01, samle oppslagene den navngir, og bruk dem. HotPDF går som standard til DFLT-skriptet og standard LangSys, noe som er det de aller fleste latinske tekstdesign leveres med, og den eksponerer en måte å overstyre skript-taggen når en skrift kobler funksjonene sine under et spesifikt skript i stedet

Coverage-tabeller bestemmer hvem som deltar

Hver substitusjons-undertabell begynner med det samme spørsmålet: deltar denne inndataglyfen i denne regelen, og hvis ja, hvor sitter den i regelens egen indeksering. Dette spørsmålet besvares av en Coverage-tabell, og svaret er en dekningsindeks (coverage index), et lite ordenstall som resten av undertabellen bruker for å slå opp hva glyfen blir

Dekning 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 posisjonen dens i listen er dens dekningsindeks. Format 2 er en liste over områdeoppføringer, der hver oppføring er en startglyf, en sluttglyf og dekningsindeksen som startglyfen kartlegges til. En glyf inne i 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 i sammenhengende løp. Begge er sortert, så begge søkes i logaritmisk tid, og begge returnerer enten en dekningsindeks eller et rent "ikke dekket" som lar motoren la glyfen være i fred

Enkeltsubstitusjon, de to formatene

Enkeltsubstitusjon er LookupType 1, og den kartlegger én glyf til nøyaktig én erstatning. Den har også to formater, og oppdelingen er en plassoptimalisering. Format 1 lagrer en enkelt delta med fortegn. Utdata-glyf-ID-en er inndata-glyf-ID-en pluss denne deltaen, modulo 65536. Dette er hvordan en skrift koder en substitusjon der hver deltakende glyf sitter med samme faste forskyvning fra sitt alternativ, for eksempel en blokk med "lining"-tall plassert i en konstant avstand fra de matchende "oldstyle"-tallene. Coverage-tabellen sier hvilke glyfer som kvalifiserer, og den ene deltaen betjener dem alle

Format 2 lagrer en eksplisitt matrise (array) med erstatnings-glyf-ID-er. Dekningsindeksen fra Coverage-tabellen er indeksen inn i denne matrisen, slik at glyfen ved dekningsindeks 0 blir den første matriseoppføringen, dekningsindeks 1 den andre, og så videre. Format 2 brukes når alternativene ikke har en enhetlig forskyvning, noe som er det vanlige tilfellet for håndbygde stilsett. Spørringen er den samme fra anroperens side uansett. Ta inndata-glyfen, kjør den gjennom Coverage, og hvis den er dekket, bruk deltaen eller les matriseposisjonen

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 som er verdt å legge merke til er gjennomslippet. GetSingleSubstituteGlyph returnerer inndata-glyf-ID-en uendret ved hvert bom treff: ingen skrift, ingen GSUB-tabell, ingen matchende funksjon, ingen dekningstreff. Det betyr at kallet er trygt å gjøre betingelsesløst. Du ber om alternativet, og hvis det ikke er noe, får du tilbake nøyaktig det du la inn, slik at anropskoden aldri trenger å spesialbehandle en skrift som mangler funksjonen

Hva stil-funksjonstaggene betyr

Funksjonstaggen er hele ordforrådet for hvilket alternativ du ber om, og taggene som er relevante for stilarbeid er en kort liste. Overskriftsparet er salt, stilistiske alternativer, den generelle tilgangen til en glyfs alternative former, og ss01 til ss20, de tjue nummererte stilsettene en skrift kan definere, hver en navngitt pakke med substitusjoner som designeren grupperer sammen. En skrift kan plassere en én-etasjes a og en R med rett ben under ss03, for eksempel, slik at aktivering av dette ene settet endrer stilen for begge

Rundt disse sitter flere andre enkeltsubstitusjons-tagger. aalt er access-all-alternates, foreningen av hvert alternativ en glyf har, vanligvis presentert som en glyfpalett-funksjon. titl velger titler-store bokstaver skåret for store størrelser. subs og sups bytter inn ekte senkede og hevdede tall i stedet for nedskalerte standarder. ordn produserer ordenstallsformer, de hevede bokstavene i 1st og 2nd. frac bygger brøker, selv om fulle diagonale brøker også lener seg på ligatur- og kontekstuell logikk som går forbi ren enkeltsubstitusjon. For enkel-glyf-tilfellene er mekanismen identisk med ss01: send taggen til substitusjonsspø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 de supplerende planene

Før noen substitusjon kan kjøre, må et tegn bli til en glyf, og det er cmap-tabellens jobb. Substitusjonsspørringen starter fra en glyf-ID, så banen er alltid tegn til glyf gjennom cmap, deretter glyf til alternativ gjennom GSUB. Den interessante delen av cmap er rekkevidden. 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, de supplerende planene (supplementary planes), som er der matematiske alfanumeriske tegn, mange symboler og flere levende skript 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 sluttkodepunkt og en start-glyf-ID, slik at et sammenhengende løp av kodepunkter kartlegges til et sammenhengende løp av glyfer. HotPDF løser kodepunkter med en hybridstrategi som matcher hvordan dataene er formet. Kodepunkter i BMP serveres fra en direkte matrise indeksert av kodepunktet, et enkelt oppslag uten søk. Kodepunkter i de supplerende planene serveres fra en glissen tabell sortert etter kodepunkt og søkt 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 skriften ikke kartlegger

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

API-ene for enkeltsubstitusjon svarer på én form for spørsmål, og det er verdt å være tydelig på hva de ikke svarer på. LookupType 1 er én av åtte substitusjonstyper. Spørringen håndterer ikke LookupType 2 multippel substitusjon, der én glyf blir til flere, og heller ikke LookupType 4 ligatursubstitusjon, der flere glyfer blir til én. Den håndterer ikke de kontekstuelle og kjedende-kontekstuelle typene, LookupTypes 5 og 6, som bare utløses når en glyf vises i et bestemt nabolag, ei heller utvidelses- og omvendt-kjedende typene. En diagonal brøk, en devanagari-konjunkt eller en arabisk innledende-medial-avsluttende kaskade er et sekvensproblem, og et enkeltsubstitusjonsoppslag per glyf kan ikke uttrykke det

Det utfører heller ikke automatisk forming. Ingenting her inspiserer et løp av tekst, bestemmer hvilke funksjoner som skal slås på, og bruker dem i den rekkefølgen skriptet krever. Anroperen velger funksjonstaggen og bruker den glyf for glyf. Det er akkurat det rette verktøyet for stilsett og alternativer, som er valgfrie og lokale, og akkurat feil verktøy for et skript som trenger omorganisering. Å holde grensen skarp er det som lar substitusjonsbanen holde seg liten og forutsigbar

For tilfellene som faktisk trenger arbeid på sekvensnivå, tas historien om komplekse skript opp i vår artikkel om kompleks skripttekstforming i Delphi. Hvis substitusjonene dine er en del av en større rapporteringsjobb som også plasserer bilder og andre skrifter på siden, dekker veiledningen til rapportutdata med skrifter og bilder hvordan disse brikkene passer sammen. Alle disse kjører på den samme motoren, HotPDF Component for Delphi og C++Builder, som bærer GSUB-substitusjonsspørringene sammen med skriftinnbygging, delsetting og tekst-API-ene som dekkes andre steder på denne bloggen