Un designer sceglie un font con una a a piano singolo per i titoli, o uno zero barrato per le tabelle, o un set di capitali con grazie ornamentali per una copertina. Quei glifi sono già presenti nel font. Semplicemente, non rappresentano l'opzione predefinita. La a predefinita viene mappata dal carattere tramite la tabella cmap a un glifo, e l'alternato si trova a pochi ID glifo di distanza, raggiungibile solo tramite una regola di sostituzione. Generare quell'alternato in un PDF significa leggere tale regola ed emettere il glifo sostitutivo nello stream dei contenuti. Questo articolo tratta della lettura di tali regole, nella fattispecie quelle di sostituzione singola, in Object Pascal senza alcuna libreria di modellazione (shaping) nativa sottostante.
L'ambito è volutamente ristretto. I set stilistici e gli alternati sono sostituzioni a singolo glifo in ingresso e in uscita. Rappresentano la parte del layout OpenType che è possibile risolvere con un semplice ciclo deterministico sulle tabelle, il che li rende ideali per un motore Pascal che desidera rimanere indipendente da librerie C.
Perché usare puro Delphi anziché HarfBuzz
HarfBuzz è la risposta scontata per "modellare questo testo" e, per una modellazione completa bidirezionale, Indic o araba, è la scelta corretta. Tuttavia, è una libreria C. Integrarla in un prodotto Delphi o C++Builder significa distribuire un oggetto nativo per ogni piattaforma e architettura di destinazione, gestirne la convenzione di chiamata, seguirne la cadenza di rilascio e verificare le condizioni di licenza rispetto alle proprie. Nessuna di queste cose è complessa di per sé. Si tratta però di un attrito continuo che non scompare mai, e non offre alcun vantaggio reale quando il requisito effettivo è semplicemente "fornire la forma ss01 di questa lettera".
La sostituzione singola non richiede un motore di modellazione. Necessita di un parser per una manciata di formati di sotto-tabelle GSUB e di una ricerca binaria o due. Scrivere questo in Pascal mantiene l'intera catena di strumenti all'interno di un unico compilatore. Il limite effettivo è che questo approccio gestisce le ricerche di sostituzione dei glifi e nulla più. Non si tratta di risoluzione bidirezionale, né di riordinamento Indic, né di modellazione contestuale automatica. Laddove queste ultime sono necessarie, devono essere implementate in altro modo, e una ricerca di sostituzione singola non può sostituirle.
La gerarchia GSUB, dall'alto in basso
La tabella Glyph Substitution è organizzata come una catena di indirezioni, e una ricerca di sostituzione scorre la catena a partire dall'alto. In cima si trova lo ScriptList. Un tag script come latn seleziona una voce, e il tag speciale DFLT rappresenta lo script predefinito applicato quando non vi sono corrispondenze più specifiche. La voce dello script punta a un LangSys (sistema linguistico), con un LangSys predefinito per i casi comuni e sistemi con nome opzionali per lingue che richiedono comportamenti differenti. Il turco è l'esempio tipico, dove la i con e senza punto richiede una gestione specifica.
Il sistema LangSys definisce un insieme di indici di funzionalità (feature indices). Ciascun indice punta al FeatureList, in cui un record di funzionalità porta un tag a quattro byte, tra cui ss01, e un elenco di indici di ricerca (lookup indices). Tali indici puntano infine al LookupList, dove risiedono le sotto-tabelle di sostituzione effettive. Pertanto, risolvere ss01 significa: trovare lo script, trovare il suo LangSys, trovare la funzionalità dotata di tag ss01, raccogliere le ricerche indicate e applicarle. HotPDF utilizza per impostazione predefinita lo script DFLT e il LangSys predefinito, la scelta con cui viene distribuita la stragrande maggioranza dei font per testi latini, ed espone un modo per sovrascrivere il tag dello script qualora un font colleghi le sue funzioni sotto uno script specifico.
Le tabelle Coverage determinano la partecipazione
Ogni sotto-tabella di sostituzione inizia con la stessa domanda: questo glifo di input prende parte a questa regola e, in caso affermativo, dove si colloca nell'indicizzazione della regola stessa. A questa domanda risponde una tabella Coverage, e la risposta è un indice di copertura (coverage index), un piccolo valore ordinale utilizzato dal resto della sotto-tabella per cercare la destinazione del glifo.
La copertura è disponibile in due formati. Il Formato 1 è un elenco di ID glifo ordinati in modo crescente. Si individua un glifo tramite ricerca binaria, e la sua posizione nell'elenco rappresenta il suo indice di copertura. Il Formato 2 è un elenco di record di intervalli, ciascuno composto da un glifo iniziale, un glifo finale e l'indice di copertura a cui mappa il glifo iniziale. Un glifo interno a un intervallo ottiene il suo indice di copertura applicando un offset rispetto all'inizio dell'intervallo. Il Formato 1 risulta compatto quando i glifi partecipanti sono sparsi, il Formato 2 quando si presentano in sequenze contigue. Entrambi sono ordinati, perciò le ricerche avvengono in tempo logaritmico, ed entrambi restituiscono un indice di copertura o un esplicito "not covered" che permette al motore di lasciare il glifo inalterato.
Sostituzione singola, i due formati
La sostituzione singola corrisponde al LookupType 1 e mappa un glifo esattamente a una sostituzione. Anch'essa presenta due formati, e la suddivisione è dettata da ragioni di ottimizzazione dello spazio. Il Formato 1 memorizza un singolo delta con segno. L'ID glifo di output è pari all'ID glifo di input più tale delta, modulo 65536. Questo è il modo in cui un font codifica una sostituzione in cui ogni glifo partecipante si trova allo stesso offset fisso rispetto al suo alternato, ad esempio un blocco di cifre allineate (lining figures) poste a distanza costante rispetto alle corrispondenti cifre vecchio stile (oldstyle figures). La tabella Coverage indica quali glifi sono idonei, e il singolo delta serve tutti quanti.
Il Formato 2 memorizza un array esplicito di ID glifi sostitutivi. L'indice di copertura ricavato dalla tabella Coverage rappresenta l'indice all'interno di quell'array, perciò il glifo all'indice di copertura 0 diventa la prima voce dell'array, l'indice di copertura 1 la seconda, e così via. Il Formato 2 viene utilizzato quando gli alternati non si trovano a un offset uniforme, caso comune nei set stilistici creati manualmente. La query dal lato del chiamante è identica in entrambi i casi. Si prende il glifo di input, lo si passa attraverso la tabella Coverage e, se risulta coperto, si applica il delta o si legge la relativa posizione nell'array.
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 glifo 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;
Il comportamento da notare è il pass-through. GetSingleSubstituteGlyph restituisce l'ID glifo di input invariato per ogni occorrenza non trovata: assenza di font, assenza di tabella GSUB, assenza di funzionalità corrispondente o mancata copertura. Ciò significa che la chiamata è sicura da eseguire in modo incondizionato. Si richiede l'alternato e, se non esiste, si riceve indietro esattamente lo stesso elemento fornito, perciò il codice chiamante non ha mai bisogno di gestire come caso speciale un font sprovvisto della funzionalità.
Il significato dei tag delle funzionalità stilistiche
Il tag della funzionalità costituisce l'intero dizionario dell'alternato richiesto, e i tag relativi all'aspetto stilistico formano un elenco ristretto. La coppia principale è costituita da salt (stylistic alternates), l'accesso globale alle forme alternate di un glifo, e da ss01 fino a ss20, i venti set stilistici numerati che un font può definire, ciascuno dei quali è un gruppo di sostituzioni coerente creato dal designer. Un font potrebbe ad esempio inserire una a a piano singolo e una R a gamba dritta sotto il set ss03, perciò l'abilitazione di tale set modifica lo stile di entrambe.
Attorno a questi risiedono diversi altri tag di sostituzione singola. aalt corrisponde ad access-all-alternates, ovvero l'unione di tutti gli alternati di un glifo, solitamente presentata come tavolozza dei glifi. titl seleziona le maiuscole per i titoli ottimizzate per grandi dimensioni. subs e sups inseriscono cifre reali in pedice e apice anziché versioni ridotte in scala di quelle predefinite. ordn produce forme ordinali, come le lettere rialzate in 1st e 2nd. frac crea frazioni, sebbene le frazioni diagonali complete si basino anche su logiche di legatura e contestuali che vanno oltre la semplice sostituzione singola. Per i casi a singolo glifo, il meccanismo è identico a ss01: si passa il tag alla ricerca di sostituzione e si legge il glifo alternato restituito.
// 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 formato 12 e i piani supplementari
Prima di poter eseguire qualsiasi sostituzione, un carattere deve essere mappato a un glifo, compito affidato alla tabella cmap. La ricerca di sostituzione inizia da un ID glifo, perciò il percorso prevede sempre il passaggio da carattere a glifo tramite cmap, e poi da glifo ad alternato tramite GSUB. La parte interessante di cmap è il suo raggio d'azione. Una sotto-tabella in formato 4 copre il Basic Multilingual Plane, ovvero i primi 65.536 punti di codice, ed è sufficiente per la maggior parte del testo latino. Non basta invece per punti di codice da U+10000 in su, ovvero i piani supplementari, che ospitano simboli matematici alfanumerici, vari simboli e diverse scritture correnti.
Il Formato 12 è la sotto-tabella che copre l'intero intervallo da U+0000 a U+10FFFF. Si tratta di un elenco ordinato di gruppi, ciascuno composto da un punto di codice iniziale, un punto di codice finale e un ID glifo iniziale, in modo che una sequenza contigua di punti di codice mappi a una sequenza contigua di glifi. HotPDF risolve i punti di codice con una strategia ibrida che rispecchia la struttura dei dati. I punti di codice nel BMP sono serviti da un array diretto indicizzato dal punto di codice, una ricerca singola senza necessità di scansioni. I punti di codice nei piani supplementari sono serviti da una tabella sparsa ordinata per punto di codice e scansionata tramite ricerca binaria. Il risultato è che GetUnicodeGlyphForCodepoint accetta un tipo Cardinal completo e risponde correttamente sull'intero intervallo, restituendo l'ID glifo 0 (il glifo .notdef) per qualsiasi punto di codice non mappato dal font.
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;
Limiti di queste ricerche
Le API di sostituzione singola rispondono a un tipo specifico di domanda, ed è bene chiarire cosa non sono in grado di gestire. Il LookupType 1 è uno degli otto tipi di sostituzione. La query non gestisce la sostituzione multipla di LookupType 2, in cui un glifo ne genera diversi, né la sostituzione di legatura di LookupType 4, in cui più glifi si uniscono in uno solo. Non gestisce i tipi contestuali e a catena contestuali (LookupType 5 e 6) che si attivano solo quando un glifo appare in un determinato contesto, né i tipi di estensione e a catena inversa. Una frazione diagonale, una combinazione Devanagari o una sequenza iniziale-mediana-finale araba sono problemi di sequenza, e una ricerca di sostituzione singola per singolo glifo non può esprimerli.
Inoltre, non viene eseguita alcuna modellazione (shaping) automatica. Nessuna parte di questo codice analizza una sequenza di testo per decidere quali funzionalità attivare e applicarle nell'ordine richiesto dalla scrittura. Il chiamante sceglie il tag della funzionalità e lo applica glifo per glifo. Questo è lo strumento corretto per i set stilistici e gli alternati, che sono opzionali e locali, e lo strumento del tutto errato per scritture che richiedono il riordinamento dei caratteri. Mantenere chiara questa distinzione è ciò che consente al percorso di sostituzione di rimanere leggero e prevedibile.
Per i casi che richiedono interventi a livello di sequenza, la gestione delle scritture complesse è trattata nel nostro articolo sulla modellazione di testi con scritture complesse in Delphi. Se le sostituzioni fanno parte di un processo di reportistica più ampio che inserisce anche immagini e altri font sulla pagina, la guida alla generazione di report con font e immagini illustra come si integrano questi elementi. Tutti questi si basano sullo stesso motore, l'HotPDF Component per Delphi e C++Builder, che include le ricerche di sostituzione GSUB insieme alle API di incorporamento font, creazione di sottoinsiemi e gestione testi trattate in altre sezioni di questo blog.