Technical Article

OpenType GSUB stilističke alternative u čistom Delphi-ju

Dizajner bira font sa jednostavnim znakom a za naslove, ili nulu sa kosom crtom za tabele, ili set dekorativnih velikih slova za koricu. Ti glifovi su već u fontu. Oni jednostavno nisu podrazumevani. Podrazumevano a se mapira sa karaktera kroz tabelu cmap na jedan glif, a alternativa se nalazi nekoliko ID-ova glifova dalje, dostupna samo preko pravila zamene. Proizvodnja te alternative u PDF-u znači čitanje tog pravila i emitovanje zamenskog glifa u toku sadržaja. Ovaj članak se bavi čitanjem tih pravila, i to onih o jednostavnoj zameni, u Object Pascal-u bez ikakve izvorne biblioteke za oblikovanje ispod njih.

Obim je namerno uzak. Stilistički setovi i alternative su zamene tipa jedan-glif-unutra, jedan-glif-spolja. Oni su deo OpenType rasporeda koji možete rešiti malim, determinističkim prolaskom kroz tabelu, što ih čini pogodnim za Pascal mehanizam koji želi da ostane bez C zavisnosti.

Zašto čist Delphi umesto HarfBuzz-a

HarfBuzz is the obvious answer to "shape this text", and for full bidirectional, Indic, or Arabic shaping it is the right answer. It is also a C library. Binding it into a Delphi or C++Builder product means shipping a native object for every target platform and architecture, matching its calling convention, tracking its release cadence, and reading its licence terms against your own. None of that is hard in isolation. All of it is friction that never goes away, and it buys nothing when the actual requirement is "give me the ss01 form of this letter".

Jednostavna zamena ne zahteva mehanizam za oblikovanje. Potreban joj je parser za nekoliko formata GSUB podtabela i jedna ili dve binarne pretrage. Pisanje toga u Pascal-u drži ceo lanac alata unutar jednog kompajlera. Iskrena granica je da ovaj pristup rešava pretrage zamene glifova i ništa više. To nije bidi rezolucija, nije indijsko preslaganje, niti automatsko kontekstualno oblikovanje. Tamo gde su te funkcije potrebne, one su neophodne, i upit o jednostavnoj zameni ih ne može zameniti.

GSUB hijerarhija, od vrha do dna

Tabela zamene glifova je organizovana kao lanac indirekcija, a upit za zamenu prolazi kroz lanac počevši od vrha. Na vrhu je ScriptList. Oznaka pisma kao što je latn bira unos, a posebna oznaka DFLT je podrazumevano pismo koje se primenjuje kada se nijedno specifičnije pismo ne podudara. Unos pisma ukazuje na LangSys, jezički sistem, sa podrazumevanim LangSys-om za uobičajene slučajeve i opcionim imenovanim za jezike koji zahtevaju drugačije ponašanje. Turski jezik je klasičan primer, gde slova i sa tačkom i bez tačke zahtevaju sopstvenu obradu.

LangSys navodi skup indeksa funkcija. Svaki indeks ukazuje na FeatureList, gde zapis funkcije nosi četvorobajtnu oznaku, uključujući ss01, i listu indeksa pretrage. Ti indeksi na kraju ukazuju na LookupList, gde žive stvarne podtabele zamene. Dakle, rešavanje ss01 znači: pronađite pismo, pronađite njegov LangSys, pronađite funkciju čija je oznaka ss01, sakupite pretrage koje ona navodi i primenite ih. HotPDF se podrazumevano oslanja na DFLT pismo i podrazumevani LangSys, što isporučuje velika većina latiničnih fontova, i izlaže način za prepisivanje oznake pisma kada font povezuje svoje funkcije pod određenim pismom.

Tabele pokrivenosti odlučuju ko učestvuje

Svaka podtabela zamene počinje istim pitanjem: da li ovaj ulazni glif učestvuje u ovom pravilu i, ako da, gde se nalazi u sopstvenom indeksiranju pravila. Na to pitanje odgovara tabela pokrivenosti, a odgovor je indeks pokrivenosti, mali redni broj koji ostatak podtabele koristi za traženje onoga u šta se glif pretvara.

Pokrivenost dolazi u dva formata. Format 1 je lista ID-ova glifova sortiranih u rastućem redosledu. Glif pronalazite binarnom pretragom, a njegova pozicija u listi je njegov indeks pokrivenosti. Format 2 je lista zapisa opsega, gde je svaki definisan početnim glifom, krajnjim glifom i indeksom pokrivenosti na koji se početni glif mapira. Glif unutar opsega dobija svoj indeks pokrivenosti pomeranjem od početka opsega. Format 1 je kompaktan kada su glifovi koji učestvuju raštrkani, a Format 2 kada spadaju u neprekidne nizove. Oba su sortirana, tako da se oba pretražuju u logaritamskom vremenu, i oba vraćaju ili indeks pokrivenosti ili jasnu poruku „nije pokriveno” koja omogućava mehanizmu da glif ostavi netaknutim.

Jednostavna zamena, dva formata

Jednostavna zamena je LookupType 1 i ona mapira jedan glif na tačno jednu zamenu. Ona takođe ima dva formata, a podela je zapravo optimizacija prostora. Format 1 čuva jednu označenu deltu. Izlazni ID glifa je ulazni ID glifa plus ta delta, po modulu 65536. Ovako font kodira zamenu u kojoj se svaki glif koji učestvuje nalazi na istom fiksnom pomaku od svoje alternative, na primer blok proporcionalnih brojeva postavljenih na konstantnoj udaljenosti od odgovarajućih brojeva u starom stilu. Tabela pokrivenosti kaže koji glifovi ispunjavaju uslove, a jedna delta služi za sve njih.

Format 2 čuva eksplicitan niz ID-ova zamenskih glifova. Indeks pokrivenosti iz tabele pokrivenosti je indeks u tom nizu, tako da glif na indeksu pokrivenosti 0 postaje prvi unos u nizu, indeks pokrivenosti 1 drugi, i tako dalje. Format 2 se koristi kada alternative nisu na ujednačenom pomaku, što je uobičajen slučaj za ručno izrađene stilističke setove. Upit je isti sa strane pozivaoca u oba slučaja. Uzmite ulazni glif, provucite ga kroz Coverage i, ako je pokriven, primenite deltu ili pročitajte poziciju u nizu.

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;

Ugovor koji vredi primetiti jeste prolaz. GetSingleSubstituteGlyph vraća nepromenjen ulazni ID glifa pri svakom promašaju: bez fonta, bez GSUB tabele, bez odgovarajuće funkcije, bez pogotka u pokrivenosti. To znači da je poziv bezbedan za upućivanje bezuslovno. Tražite alternativu i, ako je nema, dobijate nazad tačno ono što ste uneli, tako da pozivni kod never mora posebno da tretira font koji nema tu funkciju.

Šta znače oznake stilskih funkcija

Oznaka funkcije je ceo vokabular alternative koju tražite, a oznake relevantne za stilski rad čine kratku listu. Glavni par je salt, stilističke alternative, sveobuhvatni pristup alternativnim oblicima glifa, i ss01 do ss20, dvadeset numerisanih stilističkih setova koje font može da definiše, od kojih je svaki imenovani paket zamena koje dizajner grupiše. Font bi mogao da stavi jednostavan a i R sa pravom nogom pod ss03, na primer, tako da omogućavanje tog jednog seta menja stil za oba slova.

Oko njih se nalazi još nekoliko oznaka za jednostavnu zamenu. aalt predstavlja pristup svim alternativama, uniju svake alternative koju glif ima, obično predstavljenu kao funkcija palete glifova. titl bira velika slova za naslove prilagođena velikim veličinama. subs i sups ubacuju prave indekse i eksponente umesto smanjenih podrazumevanih brojeva. ordn proizvodi ordinalne oblike, podignuta slova u oznakama reda (1st i 2nd). frac gradi razlomke, iako se puni dijagonalni razlomci takođe oslanjaju na ligaturnu i kontekstualnu logiku koja prevazilazi običnu jednostavnu zamenu. Za slučajeve sa jednim glifom, mehanizam je identičan sa ss01: prosledite oznaku upitu za zamenu i pročitajte nazad zamenski glif.

// 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 i dopunske ravni

Pre nego što se bilo koja zamena pokrene, karakter mora postati glif, a to je posao tabele cmap. Upit za zamenu počinje od ID-a glifa, tako da je putanja uvek karakter-u-glif kroz cmap, a zatim glif-u-alternativu kroz GSUB. Zanimljiv deo tabele cmap je njen doseg. Podtabela formata 4 pokriva osnovnu višejezičnu ravan (BMP), prvih 65536 kodnih tačaka, i to je dovoljno za većinu latiničnih tekstova. To nije dovoljno za kodne tačke od U+10000 naviše, dopunske ravni, gde sada žive matematički alfanumerički znakovi, mnogi simboli i nekoliko živih pisama.

Format 12 je podtabela koja pokriva ceo opseg od U+0000 do U+10FFFF. To je sortirana lista grupa, gde je svaka grupa definisana početnom kodnom tačkom, krajnjom kodnom tačkom i početnim ID-om glifa, tako da se neprekidni niz kodnih tačaka mapira u neprekidni niz glifova. HotPDF rešava kodne tačke pomoću hibridne strategije koja odgovara načinu na koji su podaci oblikovani. Kodne tačke u BMP-u se poslužuju iz direktnog niza indeksiranog kodnom tačkom, što je jednokratna pretraga bez pretraživanja niza. Kodne tačke u dopunskim ravnima se poslužuju iz retke tabele sortirane po kodnoj tački i pretražuju binarnom pretragom. Rezultat je da GetUnicodeGlyphForCodepoint uzima pun Cardinal i odgovara ispravno na celom opsegu, vraćajući ID glifa 0, odnosno .notdef glif, za bilo koju kodnu tačku koju font ne mapira.

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;

Gde se ovi upiti zaustavljaju

API-ji za jednostavnu zamenu odgovaraju na jedno obličje pitanja, i vredi biti jasan u vezi sa onim na šta oni ne odgovaraju. LookupType 1 je jedan od osam tipova zamene. Upit ne rešava LookupType 2 višestruku zamenu, niti LookupType 4 zamenu ligatura. Takođe ne rešava kontekstualne i lančano-kontekstualne tipove, LookupTypes 5 i 6, koji se pokreću samo kada se glif pojavi u određenom okruženju, niti tipove proširenja i obrnutog lančanog povezivanja. Dijagonalni razlomak, devanagari konjunkt ili arapska kaskada početak-sredina-kraj su sekvencijalni problemi, a pretraga jednostavne zamene po pojedinačnom glifu to ne može izraziti.

Takođe ne vrši automatsko oblikovanje. Ništa ovde ne ispituje niz teksta, ne odlučuje koje funkcije treba uključiti i ne primenjuje ih redosledom koji pismo zahteva. Pozivalac bira oznaku funkcije i primenjuje je glif po glif. To je tačno pravi alat za stilističke setove i alternative, koji su opcioni i lokalni, i tačno pogrešan alat za pismo koje zahteva preslaganje redosleda. Održavanje te granice oštrom je ono što omogućava putanji zamene da ostane mala i predvidiva.

Za slučajeve koji zahtevaju rad na nivou sekvence, priča o složenim pismima se nastavlja u našem članku o oblikovanju teksta sa složenim pismima u Delphi-ju. Ako su vaše zamene deo većeg posla izveštavanja koji takođe postavlja slike i druge fontove na stranicu, vodič kroz izveštavanje sa fontovima i slikama pokriva kako se ti delovi uklapaju. Sve ovo radi na istom mehanizmu, HotPDF komponenti za Delphi i C++Builder, koja nosi GSUB upite za zamenu zajedno sa API-jima za ugradnju fontova, podskupove i tekst koji su pokriveni na drugim mestima na ovom blogu.