Articolo tecnico

Alternati stilistici GSUB OpenType in puro Delphi

Un designer sceglie un font con una a single-story per i titoli, o uno zero barrato per le tabelle, o un set di maiuscole swash per una copertina. Quei glifi sono già nel font. Semplicemente non sono il default. La a predefinita mappa dal carattere attraverso la tabella cmap a un glifo, e l'alternativo sta a pochi glyph id di distanza, raggiungibile solo tramite una regola di sostituzione. Produrre quell'alternativo in un PDF significa leggere la regola ed emettere il glifo sostituto nello stream di contenuto. Questo articolo parla di leggere quelle regole, del tipo a sostituzione singola, in Object Pascal senza librerie di shaping native sotto

La portata è ristretta di proposito. Gli stylistic set e gli alternati sono sostituzioni da un glifo a un glifo. Sono la parte del layout OpenType che si risolve con una piccola camminata deterministica delle tabelle, il che li rende adatti a un motore Pascal che vuole restare libero da dipendenze C

Perché Delphi puro invece di HarfBuzz

HarfBuzz è la risposta ovvia a "fai lo shaping di questo testo", e per lo shaping completo bidirezionale, indiano o arabo è la risposta giusta. È anche una libreria C. Legarla in un prodotto Delphi o C++Builder significa spedire un oggetto nativo per ogni piattaforma e architettura target, rispettare la sua calling convention, seguire il suo ritmo di release e leggere i termini della licenza contro i vostri. Nulla di ciò è difficile in sé. Tutto è attrito che non passa mai, e non compra nulla quando il requisito vero è "dammi la forma ss01 di questa lettera"

La single substitution non ha bisogno di un motore di shaping. Ha bisogno di un parser per una manciata di formati di subtable GSUB e di un paio di ricerche binarie. Scriverlo in Pascal tiene l'intera toolchain dentro un solo compilatore. La query risponde a una domanda, quale glifo sostituisce questo glifo sotto questa feature, ed è esattamente la domanda che il lavoro stilistico pone; la risoluzione bidi, il riordino indiano e lo shaping contestuale sono problemi di sequenza che appartengono a un passaggio di shaping

La gerarchia GSUB, dall'alto verso il basso

La tabella Glyph Substitution è organizzata come una catena di indirezioni, e una query di sostituzione percorre la catena dall'alto. In cima c'è la ScriptList. Un tag script come latn seleziona una voce, e il tag speciale DFLT è lo script di default che si applica quando nessuno script più specifico corrisponde. La voce script punta a una LangSys, il sistema linguistico, con una LangSys di default per il caso comune e altre nominate per le lingue che hanno bisogno di comportamenti diversi. Il turco è l'esempio usuale, dove la i con il punto e quella senza punto esigono una gestione dedicata

La LangSys nomina un insieme di indici di feature. Ogni indice punta nella FeatureList, dove un record di feature porta un tag di quattro byte, ss01 compreso, e una lista di indici di lookup. Questi a loro volta puntano nella LookupList, dove vivono le subtable di sostituzione vere e proprie. Quindi risolvere ss01 significa: trovare lo script, trovare la sua LangSys, trovare la feature il cui tag è ss01, raccogliere i lookup che nomina e applicarli. HotPDF usa di default lo script DFLT e la LangSys di default, che è ciò con cui arrivano la stragrande maggioranza dei design di testo latini, ed espone un modo per superare il tag script quando un font collega le sue feature a uno script specifico

Diagramma della gerarchia della tabella GSUB OpenType con una richiesta ss01 in Delphi che si risolve attraverso ScriptList, LangSys, FeatureList e LookupList, e le query non coperte passate attraverso
Ogni richiesta ss01 percorre la catena GSUB OpenType da ScriptList attraverso LangSys e FeatureList fino alla subtable della LookupList. Qualsiasi mancanza lungo la strada restituisce il glyph id di input invariato

Le tabelle Coverage decidono chi partecipa

Ogni subtable di sostituzione inizia con la stessa domanda: questo glifo di input prende parte a questa regola e, in tal caso, dove si colloca nell'indice proprio della regola. La domanda la risponde una tabella Coverage, e la risposta è un indice di coverage, un piccolo ordinale che il resto della subtable usa per cercare ciò che il glifo diventa

La Coverage arriva in due formati. Il formato 1 è una lista di glyph id ordinata in senso crescente. Trovate un glifo con una ricerca binaria, e la sua posizione nella lista è il suo indice di coverage. Il formato 2 è una lista di record di intervallo, ognuno un glifo iniziale, un glifo finale e l'indice di coverage a cui mappa il glifo iniziale. Un glifo dentro un intervallo ottiene il suo indice di coverage con un offset dalla partenza dell'intervallo. Il formato 1 è compatto quando i glifi partecipanti sono sparsi, il formato 2 quando cadono in sequenze contigue. Entrambi sono ordinati, quindi entrambi si cercano in tempo logaritmico, ed entrambi restituiscono o un indice di coverage o un netto "non coperto" che lascia il motore libero di non toccare il glifo

Single Substitution, i due formati

La Single Substitution è la LookupType 1, e mappa un glifo su esattamente una sostituzione. Ha anche due formati, e la divisione è un'ottimizzazione di spazio. Il formato 1 memorizza un unico delta con segno. Il glyph id di output è il glyph id di input più quel delta, modulo 65536. È così che un font codifica una sostituzione in cui ogni glifo partecipante sta allo stesso offset fisso dal suo alternativo, per esempio un blocco di figure lining piazzate a distanza costante dalle oldstyle corrispondenti. La tabella Coverage dice quali glifi qualificano, e l'unico delta serve tutti

Il formato 2 memorizza un array esplicito di glyph id sostituti. L'indice di coverage della tabella Coverage è l'indice in quell'array, così il glifo all'indice di coverage 0 diventa la prima voce dell'array, l'indice di coverage 1 la seconda, e così via. Il formato 2 si usa quando gli alternati non sono a un offset uniforme, che è il caso comune per gli stylistic set costruiti a mano. La query è la stessa dal lato chiamante in entrambi i casi. Prendete il glifo di input, fatelo passare in Coverage, e se è coperto applicate il delta o leggete la cella dell'array

Formati uno e due delle tabelle Coverage che alimentano i formati della single substitution in OpenType, dove un motore Delphi somma un delta fisso o indicizza un array esplicito di sostituti
La Coverage assegna a ogni glifo qualificante un indice tramite lista ordinata o record di intervallo. La single substitution formato 1 applica poi un delta condiviso mentre il formato 2 legge il sostituto da una cella esplicita dell'array
var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.CurrentPage.SetFont('My Stylistic Face', [], 12);

    // Glifo di default per 'a' attraverso la cmap del font.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: risolve l'alternativo via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID significa che la feature non ha toccato questo glifo.
    if AltGID <> BaseGID then
      { emette AltGID nello stream di contenuto };
  finally
    Pdf.Free;
  end;
end;

Il contratto da notare è il pass-through. GetSingleSubstituteGlyph restituisce il glyph id di input invariato a ogni mancata copertura: niente font, niente tabella GSUB, niente feature corrispondente, niente hit in coverage. Significa che la chiamata è sicura da fare incondizionatamente. Chiedete l'alternativo e, se non c'è, vi torna esattamente ciò che avete dato, quindi il codice chiamante non ha mai bisogno di un caso speciale per un font privo della feature

Cosa significano i tag di feature stilistici

Il tag di feature è l'intero vocabolario di quale alternativo state chiedendo, e i tag rilevanti per il lavoro stilistico sono una lista corta. La coppia di punta è salt, gli alternati stilistici, l'accesso prendi-tutto alle forme alternative di un glifo, e ss01 fino a ss20, i venti stylistic set numerati che un font può definire, ognuno un pacchetto nominato di sostituzioni che il designer raggruppa insieme. Un font potrebbe mettere una a single-story e una R dalla gamba dritta sotto ss03, per esempio, così attivare quel solo set ridisegna entrambi

Attorno a quelli se ne addensano altri a sostituzione singola. aalt è access-all-alternates, l'unione di ogni alternativo che un glifo ha, di solito presentata come feature da palette di glifi. titl seleziona le maiuscole titling tagliate per i corpi grandi. subs e sups scambiano vere figure pedice e apice al posto dei default ridotti di scala. ordn produce le forme ordinali, le lettere sollevate in 1st e 2nd. frac costruisce le frazioni, anche se le frazioni diagonali complete si appoggiano anche a logica di legature e contesto che va oltre la semplice single substitution. Per i casi a glifo singolo il meccanismo è identico a ss01: passate il tag alla query di sostituzione e leggete il glifo alternativo

// Prova una feature di stylistic set, poi ripiega sugli alternati semplici.
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');
  // Ancora BaseGID se nessuna delle due feature copre questo glifo.
end;

cmap formato 12 e i piani supplementari

Prima che qualsiasi sostituzione possa girare, un carattere deve diventare un glifo, ed è compito della tabella cmap. La query di sostituzione parte da un glyph id, quindi il percorso è sempre da carattere a glifo attraverso cmap, poi da glifo a alternativo attraverso GSUB. La parte interessante di cmap è la sua portata. Una subtable formato 4 copre il Basic Multilingual Plane, i primi 65536 code point, e basta per la maggior parte del testo latino. Non basta per i code point da U+10000 in su, i piani supplementari, dove oggi vivono gli alfanumerici matematici, molti simboli e parecchie lingue viventi

Il formato 12 è la subtable che copre l'intero intervallo U+0000 a U+10FFFF. È una lista ordinata di gruppi, ogni gruppo un code point iniziale, un code point finale e un glyph id iniziale, così una sequenza contigua di code point mappa a una sequenza contigua di glifi. HotPDF risolve i code point con una strategia ibrida che rispecchia la forma dei dati. I code point nel BMP sono serviti da un array diretto indicizzato per code point, una singola lookup senza ricerca. I code point nei piani supplementari sono serviti da una tabella sparsa ordinata per code point e cercata con ricerca binaria. Il risultato è che GetUnicodeGlyphForCodepoint prende un Cardinal completo e risponde correttamente su tutto l'intervallo, restituendo glyph id 0, il glifo .notdef, per ogni code point che il font non mappa

Diagramma della pipeline di risoluzione cmap per HotPDF con gli hit diretti del formato 4 e la ricerca binaria del formato 12 che consegnano glyph id alla sostituzione GSUB OpenType in Delphi
I caratteri arrivano ai glifi attraverso la cmap — hit diretti da array per il BMP, gruppi formato 12 cercati a binario per i piani supplementari — e il glyph id risultante alimenta la stessa query GSUB invariato
var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // Un code point di piano supplementare: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // lookup formato 12
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // il font non ha glifo per questo code point
end;

Query per glifo contro shaping di sequenze

Le API a sostituzione singola rispondono a una sola forma di domanda: LookupType 1, un glifo dentro e un glifo fuori. Gli altri tipi di lookup GSUB lavorano su sequenze, dove un glifo diventa parecchi, parecchi diventano uno, o una regola scatta solo quando un glifo compare in un certo intorno. Una frazione diagonale, un congiunto devanagari o una cascata araba iniziale-mediale-finale è quel tipo di problema di sequenza, ed è compito di un passaggio di shaping, non di una lookup per glifo

La divisione del lavoro è deliberata. Qui il chiamante sceglie il tag di feature e lo applica glifo per glifo, senza nulla che ispezioni una sequenza di testo o decida l'ordine delle feature per conto suo. È esattamente lo strumento giusto per stylistic set e alternati, che sono opt-in e locali, e tenere il percorso per glifo libero da logica di sequenza è ciò che gli permette di restare piccolo e prevedibile

Per i casi che hanno davvero bisogno di lavoro a livello di sequenza, la storia degli script complessi è ripresa in il nostro articolo sullo shaping del testo a script complessi in Delphi. Se le vostre sostituzioni fanno parte di un job di report più grande che piazza anche immagini e altri font nella pagina, la guida all'output di report con font e immagini copre come i pezzi si incastrano. Tutto gira sullo stesso motore, l'HotPDF Delphi Component per Delphi e C++Builder, che porta le query di sostituzione GSUB accanto alle API di font embedding, subsetting e testo coperte altrove su questo blog