Technical Article

Alternative stilistice OpenType GSUB în Delphi pur

Un designer alege un font cu un a cu un singur etaj pentru antete, sau un zero tăiat pentru tabele, sau un set de majuscule ornamentate (swash) pentru o copertă. Aceste glife sunt deja în font. Pur și simplu nu sunt cele implicite. Glifa implicită a se mapează de la caracter prin tabelul cmap la o singură glifă, iar alternativa se află la câteva ID-uri de glifă distanță, accesibilă numai printr-o regulă de substituție. Producerea acelei alternative într-un PDF înseamnă citirea regulii și emiterea glifei de înlocuire în fluxul de conținut. Acest articol este despre citirea acestor reguli, de tipul substituție simplă, în Object Pascal fără nicio bibliotecă nativă de modelare (shaping) dedesubt.

Domeniul de aplicare este restrâns în mod intenționat. Seturile și alternativele stilistice sunt substituții de tip o singură glifă la intrare, o singură glifă la ieșire. Acestea sunt acea parte a structurii OpenType pe care o puteți rezolva cu o parcurgere mică și deterministă a tabelului, ceea ce le face potrivite pentru un motor Pascal care dorește să rămână liber de dependențe C.

De ce Delphi pur în loc de HarfBuzz

HarfBuzz este răspunsul elegant pentru modelarea textului (text shaping) și pentru modelarea completă bidirecțională, indică sau arabă este răspunsul corect. Este, de asemenea, o bibliotecă C. Integrarea sa într-un produs Delphi sau C++Builder înseamnă livrarea unui obiect nativ pentru fiecare platformă și arhitectură țintă, potrivirea convenției sale de apelare, urmărirea ritmului său de lansare și compararea termenilor săi de licențiere cu ai dumneavoastră. Nimic din toate acestea nu este dificil în mod izolat. Însă toate acestea reprezintă o fricțiune care nu dispare niciodată și nu aduce niciun beneficiu când cerința reală este pur și simplu "dă-mi forma ss01 a acestei litere".

Substituția simplă nu are nevoie de un motor de modelare. Are nevoie de un parser pentru câteva formate de sub-tabele GSUB și de una sau două căutări binare. Scrierea acestora în Pascal menține întregul lanț de instrumente în interiorul unui singur compilator. Limita reală este că această abordare gestionează interogările de substituție a glifelor și nimic altceva. Nu este rezoluție bidirecțională, nu este reordonare indică și nu este modelare contextuală automată. Acolo unde acestea sunt necesare, sunt necesare, iar o interogare de substituție simplă nu le va putea înlocui.

Ierarhia GSUB, de la vârf la bază

Tabelul de substituție a glifelor (Glyph Substitution) este organizat ca un lanț de redirecționări, iar o interogare de substituție parcurge lanțul de la vârf. În vârf se află ScriptList. O etichetă de script precum latn selectează o intrare, iar eticheta specială DFLT este scriptul implicit care se aplică atunci când nu se potrivește niciun alt script specific. Intrarea de script indică o structură LangSys, sistemul de limbă, cu un LangSys implicit pentru cazul comun și unele numite opționale pentru limbile care au nevoie de un comportament diferit. Turca este exemplul obișnuit, unde i cu punct și fără punct necesită propria lor gestionare.

LangSys numește un set de indici de caracteristici (features). Fiecare index indică în FeatureList, unde o înregistrare de caracteristică poartă o etichetă de patru octeți, printre care și ss01, și o listă de indici de căutare (lookups). Acești indici indică în cele din urmă în LookupList, unde se află sub-tabelele efective de substituție. Deci, rezolvarea ss01 înseamnă: găsiți scriptul, găsiți LangSys-ul său, găsiți caracteristica a cărei etichetă este ss01, colectați căutările pe care le numește și aplicați-le. HotPDF folosește în mod implicit scriptul DFLT și LangSys-ul implicit, ceea ce este exact ceea ce livrează marea majoritate a designurilor de text latin, și expune o modalitate de a suprascrie eticheta de script atunci când un font își conectează caracteristicile sub un script specific în schimb.

Tabelele de acoperire decid cine participă

Fiecare sub-tabel de substituție începe cu aceeași întrebare: participă această glifă de intrare la această regulă și, dacă da, unde se situează în indexarea proprie a regulii. La această întrebare răspunde un tabel de acoperire (Coverage table), iar răspunsul este un index de acoperire, o valoare ordinală mică pe care restul sub-tabelului o folosește pentru a căuta în ce se transformă glifa.

Acoperirea (Coverage) vine în două formate. Formatul 1 este o listă de ID-uri de glife sortate în ordine crescătoare. Găsiți o glifă cu o căutare binară, iar poziția sa în listă este indexul său de acoperire. Formatul 2 este o listă de înregistrări de intervale, fiecare fiind o glifă de început, o glifă de sfârșit și indexul de acoperire la care se mapează glifa de început. O glifă din interiorul unui interval își obține indexul de acoperire prin deplasare (offset) de la începutul intervalului. Formatul 1 este compact atunci când glifele participante sunt dispersate, Formatul 2 când acestea se încadrează în intervale contigue. Ambele sunt sortate, deci ambele sunt căutate în timp logaritmic, și ambele returnează fie un index de acoperire, fie un rezultat clar "neacoperit" (not covered) care permite motorului să lase glifa neschimbată.

Substituția simplă, cele două formate

Substituția simplă (Single Substitution) este LookupType 1 și mapează o glifă la exact o înlocuire. De asemenea, are două formate, iar divizarea reprezintă o optimizare a spațiului. Formatul 1 stochează o singură diferență (delta) cu semn. ID-ul glifei de ieșire este ID-ul glifei de intrare plus acea diferență, modulo 65536. Acesta este modul în care un font codifică o substituție în care fiecare glifă participantă se află la același offset fix față de alternativa sa, de exemplu un bloc de cifre uniforme plasate la o distanță constantă de cifrele în stil vechi (oldstyle) corespunzătoare. Tabelul de acoperire (Coverage) indică ce glife se califică, iar acea singură valoare delta le servește pe toate.

Formatul 2 stochează un tablou explicit de ID-uri de glife de înlocuire. Indexul de acoperire din tabelul de acoperire este indexul din acel tablou, astfel încât glifa de la indexul de acoperire 0 devine prima intrare din tablou, indexul de acoperire 1 a doua și așa mai departe. Formatul 2 este utilizat când alternativele nu se află la un offset uniform, ceea ce reprezintă cazul obișnuit pentru seturile stilistice create manual. Interogarea este aceeași din perspectiva apelantului în ambele cazuri. Luați glifa de intrare, treceți-o prin Coverage și, dacă este acoperită, aplicați diferența delta sau citiți poziția din tablou.

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;

Contractul care merită remarcat este trecerea directă (pass-through). GetSingleSubstituteGlyph returnează ID-ul glifei de intrare neschimbat la fiecare ratare: lipsă font, lipsă tabel GSUB, lipsă caracteristică potrivită, lipsă potrivire în tabelul de acoperire. Asta înseamnă că apelul poate fi făcut în siguranță în mod necondiționat. Solicitați alternativa și, dacă nu există una, primiți înapoi exact ceea ce ați introdus, astfel încât codul de apelare nu trebuie niciodată să trateze ca pe un caz special un font căruia îi lipsește acea caracteristică.

Ce înseamnă etichetele caracteristicilor stilistice

Eticheta caracteristicii (feature tag) reprezintă întregul vocabular al alternativei pe care o solicitați, iar etichetele relevante pentru lucrul stilistic sunt pe o listă scurtă. Perechea principală este salt, alternative stilistice (stylistic alternates), accesul general la formele alternative ale unei glife, și ss01 până la ss20, cele douăzeci de seturi stilistice numerotate pe care le poate defini un font, fiecare fiind un pachet numit de substituții pe care designerul le grupează împreună. Un font ar putea pune un a cu un singur etaj și un R cu picior drept sub ss03, de exemplu, astfel încât activarea acelui singur set le restilizează pe ambele.

În jurul acestora se află alte câteva etichete de substituție simplă. aalt este acces la toate alternativele (access-all-alternates), reuniunea tuturor alternativelor pe care le are o glifă, prezentată de obicei ca o funcție de paletă de glife. titl selectează majusculele de titlu create pentru dimensiuni mari. subs și sups înlocuiesc cifrele cu indici inferiori și superiori reali, în loc de valorile implicite reduse la scară. ordn produce forme ordinale, literele ridicate din 1st și 2nd. frac construiește fracții, deși fracțiile diagonale complete se bazează, de asemenea, pe o logică de ligatură și contextuală care depășește simpla substituție. Pentru cazurile cu o singură glifă, mecanismul este identic cu ss01: transmiteți eticheta la interogarea de substituție și citiți glifa alternativă returnată.

// 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;

Formatul cmap 12 și planele suplimentare

Înainte ca orice substituție să poată rula, un caracter trebuie să devină o glifă, iar aceasta este sarcina tabelului cmap. Interogarea de substituție începe de la un ID de glifă, așa că traseul este întotdeauna de la caracter la glifă prin cmap, apoi de la glifă la alternativă prin GSUB. Partea interesantă a cmap este acoperirea sa. Un sub-tabel în formatul 4 acoperă planul multilingv de bază (Basic Multilingual Plane), primele 65536 de puncte de cod, iar acest lucru este suficient pentru majoritatea textelor latine. Nu este însă suficient pentru punctele de cod de la U+10000 în sus, planele suplimentare, unde se află acum caracterele alfanumerice matematice, multe simboluri și câteva scrieri active.

Formatul 12 este sub-tabelul care acoperă întregul interval U+0000 - U+10FFFF. Es o listă sortată de grupuri, fiecare grup având un punct de cod de început, un punct de cod de sfârșit și un ID de glifă de început, astfel încât un șir contiguu de puncte de cod se mapează la un șir contiguu de glife. HotPDF rezolvă punctele de cod cu o strategie hibridă care se potrivește cu modul în care sunt structurate datele. Punctele de cod din BMP sunt servite dintr-un tablou direct indexat de punctul de cod, o singură căutare fără efort. Punctele de cod din planele suplimentare sunt servite dintr-un tabel rar sortat după punctul de cod și căutat cu o căutare binară. Rezultatul este că GetUnicodeGlyphForCodepoint preia un Cardinal complet și răspunde corect pe întregul interval, returnând ID-ul de glifă 0, glifa .notdef, pentru orice punct de cod pe care fontul nu îl mapează.

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;

Unde se opresc aceste interogări

API-urile de substituție simplă răspund la un singur tip de întrebare și merită să fim clari cu privire la ceea ce nu rezolvă. LookupType 1 este unul dintre cele opt tipuri de substituție. Interogarea nu gestionează substituția multiplă LookupType 2, unde o glifă devine mai multe, și nici substituția de ligatură LookupType 4, unde mai multe glife devin una singură. Nu gestionează tipurile contextuale și contextuale în lanț, LookupType 5 și 6, care se declanșează numai când o glifă apare într-o anumită vecinătate, și nici tipurile de extensie și de înlănțuire inversă. O fracție diagonală, o conjunctură Devanagari sau o cascadă inițială-medială-finală arabă reprezintă o problemă de secvență, iar o căutare de substituție simplă per glifă nu o poate exprima.

De asemenea, nu efectuează modelare automată (shaping). Nimic de aici nu inspectează un bloc de text, nu decide ce caracteristici să activeze și nu le aplică în ordinea cerută de scriere. Apelantul alege eticheta caracteristicii și o aplică glifă cu glifă. Acesta este exact instrumentul potrivit pentru seturile și alternativele stilistice, care sunt opționale și locale, și exact instrumentul greșit pentru o scriere care are nevoie de reordonare. Menținerea clară a acestei granițe este ceea ce permite căii de substituție să rămână mică și predictibilă.

Pentru cazurile care necesită lucru la nivel de secvență, povestea scrierilor complexe este reluată în articolul nostru despre modelarea textului cu scrieri complexe în Delphi. Dacă substituțiile dumneavoastră fac parte dintr-o lucrare de raportare mai mare care plasează, de asemenea, imagini și alte fonturi pe pagină, ghidul pentru realizarea rapoartelor cu fonturi și imagini acoperă modul în care se potrivesc aceste piese. Toate acestea rulează pe același motor, HotPDF Component pentru Delphi și C++Builder, care include interogările de substituție GSUB alături de API-urile de încorporare a fonturilor, subsetting și text acoperite în alte părți ale acestui blog.