Lo shaping del testo nel componente PDFium passa attraverso un oggetto installabile. ConfigureTextShaper installa lo shaper attraverso cui ogni punto di ingresso di shaping instrada, sostituendo e liberando qualunque cosa ci fosse; ActiveTextShaper restituisce quello installato e crea il default della piattaforma al primo uso; ActiveTextShaperName riporta quale backend sia attivo; ClearTextShaper elimina l'installazione e lascia che il default venga ricreato. Su Windows il default è TPdfUniscribeTextShaper. Sotto Free Pascal c'è TPdfHarfBuzzTextShaper, che collega libharfbuzz a runtime così una libreria mancante è una condizione riportata anziché un fallimento di caricamento
Un'interfaccia, due backend che dividono il lavoro in modo completamente diverso. Comprendere quell'asimmetria è ciò che impedisce alla via portabile di produrre testo con shaping corretto e posizionamento sbagliato
Perché il backend Windows è una classe e quello portabile tre pezzi?
Perché Uniscribe è quattro API che fingono di essere una. ScriptItemize segmenta una stringa per script e risolve i livelli bidirezionali; ScriptShape mappa i caratteri in glifi; ScriptPlace calcola avanzamenti e offset; ScriptLayout mette i run risultanti in ordine visivo. Un backend costruito su di essa quindi non ha più nulla da aggiungere, ed è per questo che lo shaper Windows è una singola classe con un singolo metodo
HarfBuzz copre le due centrali. Dà shape e colloca un run la cui direzione e script il chiamante ha già deciso, e non ha opinioni su come un paragrafo si divida in run o in quale ordine quei run compaiano. Così il backend portabile fornisce il resto: l'algoritmo bidirezionale risolve i livelli di embedding, le funzioni Unicode di HarfBuzz segmentano il testo per script, e i run sono disposti nell'ordine visivo che la regola L2 di UAX #9 produce. La metà bidirezionale è abbastanza sostanziosa da essere un'unità propria, descritta nell'articolo sui livelli di embedding UAX #9
Lo shaper non risolve i font, ed è deliberato
Uniscribe legge il binario del font da un device context GDI. Non c'è equivalente portabile di questo, e inventarne uno dentro un'unità di shaping significherebbe decidere, per conto di ogni applicazione, se i font vengano da fontconfig, da CoreText, da una cartella font dell'applicazione o da un database. Così il backend HarfBuzz accetta un resolver: una callback che mappa un nome di font nei byte TrueType o OpenType. Restituire False fa fallire la richiesta di shaping allo stesso modo in cui un font GDI illeggibile la fa fallire su Windows
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// La politica di Lei: fontconfig, CoreText, una cartella font dell'app, un database
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// La proprietà passa all'unità; chiamare una volta all'avvio,
// prima che qualcosa dia shape al testo
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// Su Delphi il default della piattaforma (Uniscribe) è creato a richiesta,
// quindi non serve alcuna installazione
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
Tenere la scoperta dei font fuori dallo shaper ha un secondo beneficio che compare nei server: lo stesso processo può dare shape con un insieme di font incorporato che non c'entra nulla con ciò che è installato sulla macchina, che è ciò che si vuole quando l'output deve essere riproducibile al byte tra host. Il componente espone anche un provider di font del sistema ospite per i casi in cui si vogliono i font installati, trattato nell'articolo sul provider di font di sistema
Il record di risultato è neutrale rispetto al backend, e i cluster sono la ragione
Entrambi i backend riempiono lo stesso TPdfShapedText: il testo sorgente, il nome del font, la dimensione, i byte del font, un array di run, la larghezza totale, il numero di glifi e il numero di caratteri logici. Ogni TPdfShapedRun trasporta il proprio intervallo nel testo sorgente, la propria posizione X visiva, la propria larghezza, il proprio livello bidirezionale e un flag destra-verso-sinistra, più i propri glifi. Ogni TPdfShapedGlyph trasporta un identificatore di glifo, un avanzamento, offset X e Y, e il cluster a cui appartiene come inizio e lunghezza nel testo sorgente
Quei campi di cluster sono ciò che rende il record utilizzabile anziché semplicemente informativo. Lo shaping non è una mappatura uno a uno: una sillaba devanagari diventa un glifo da quattro caratteri, una legatura araba ne fonde due, e un singolo carattere può produrre segni multipli. Senza intervalli di cluster non si può collocare un cursore, fare hit-test di un clic o evidenziare una selezione, perché non si può dire a quali caratteri appartenga un glifo. Con essi, l'aritmetica è locale e lo stesso codice funziona per entrambi i backend
var
Shaped: TPdfShapedText;
R, G: Integer;
begin
if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
for R := 0 to High(Shaped.Runs) do
begin
// I run arrivano già in ordine visivo con VisualX compilato
X := Shaped.Runs[R].VisualX;
for G := 0 to High(Shaped.Runs[R].Glyphs) do
begin
EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
X + Shaped.Runs[R].Glyphs[G].OffsetX,
Shaped.Runs[R].Glyphs[G].OffsetY);
X := X + Shaped.Runs[R].Glyphs[G].Advance;
end;
end;
end;
I budget appartengono al record di opzioni
TPdfTextShapingOptions trasporta una direzione più tre tetti: caratteri massimi, glifi massimi e run massimi, con una funzione di classe Default che riempie valori sensati. I tetti non sono paranoia su input malformato; sono aritmetica. Lo shaping espande: un font con sostituzione contestuale aggressiva può emettere più glifi che caratteri di input, e un paragrafo che alterna script ogni pochi caratteri produce un run per commutazione. Un documento assemblato per massimizzare entrambi trasforma una stringa modesta in una grande allocazione, e un servizio che dà shape al testo da PDF non fidati ha bisogno di un limite scelto da sé anziché di un limite imposto dalla macchina
Impostare la direzione esplicitamente anziché lasciarla su automatico vale la pena farlo ogni volta che la si conosce già. L'automatico applica le regole della direzione di paragrafo per indovinare dal primo carattere forte, il che è giusto per testo libero e sbagliato per un campo modulo la cui direzione è una proprietà del campo anziché del valore che qualcuno ha digitato dentro
Binding a runtime, non una dipendenza di build
Il backend HarfBuzz carica la libreria dinamicamente. È una decisione di deployment con conseguenze reali: un solo binario gira su una macchina con HarfBuzz e su una senza, riportando capacità ridotte nel secondo caso invece di fallire l'avvio. Per una libreria spedita ad altri sviluppatori è l'unico assetto praticabile, perché non si può richiedere a ogni consumatore di un componente PDF di procurarsi e far coincidere le versioni di una libreria di shaping di cui potrebbe non avere bisogno
La regola corrispondente per i chiamanti è verificare. ActiveTextShaper restituisce nil quando la piattaforma non ha default e nessuno è stato configurato, e il punto di ingresso di shaping riporta quello come shaper non disponibile anziché come fallimento di shaping. Sono problemi diversi e meritano messaggi diversi: uno è un vuoto di deployment, l'altro è un problema di font o di testo
Installare una volta, prima che qualcosa dia shape
L'installazione sostituisce e libera lo shaper precedente, quindi chiamarla ripetutamente è sicuro ma inutile, e chiamarla mentre un altro thread sta dando shape non è affatto sicuro. Farlo durante l'avvio. Se occorre ripiegare sul default della piattaforma in seguito, passare nil, che è anche il modo di disfare un test double alla fine di un test
Una volta installato un backend, misurazione e wrapping si comportano allo stesso modo su entrambe le piattaforme, poiché consumano le metriche di run e glifi anziché chiamare la piattaforma direttamente; il modello di wrapping è descritto nell'articolo su misurazione del testo e word wrap. Le piattaforme e le toolchain supportate per il componente sono elencate nella pagina di prodotto di PDFium Delphi component