Un glifo mancante in un PDF non è un errore. Il produttore chiede un carattere che il font selezionato non sa mappare, il font restituisce l'indice di glifo zero, e il file che ne esce è strutturalmente valido, si apre ovunque, e mostra una casella vuota dove dovrebbe stare un nome o un importo. Nessuno nella pipeline di generazione se ne accorge. Lo fa il destinatario. HotPDF chiude quel cerchio con TrackUnresolvedGlyphs: attivarlo e la via di disegno del testo registra ogni code point la cui ricerca di glifo si risolve nell'indice zero, generando OnUnresolvedGlyph una volta per riscontro unico con il code point, il font su cui è fallito, lo script a cui appartiene e un suggerimento di font che lo coprirebbero
Il rilevamento è metà della risposta. L'altra metà è SetFontFallbackChain, che registra una lista ordinata di font per script, così i casi comuni si risolvono da soli e solo i vuoti autentici raggiungono il gestore di Lei. Insieme trasformano una classe di difetto che veniva riportata dai clienti in un controllo in fase di build
Perché un glifo mancante non genera nulla?
Perché ISO 32000 non impone alcun obbligo al produttore di verificare la copertura, e l'indice di glifo zero è un glifo legittimo. È .notdef, il cui contorno è scelto dal progettista del font: di solito un rettangolo vuoto o cavo, a volte niente affatto. Un viewer che lo disegna si comporta correttamente. L'estrazione del testo può perfino restituire i caratteri giusti, perché la mappatura /ToUnicode è scritta dal testo sorgente anziché dai contorni, così un controllo automatico di round-trip passerà felicemente un documento il cui testo visibile ha dei buchi
La conseguenza pratica è che la copertura va verificata nel momento del disegno, quando la libreria sa ancora quale code point fosse stato richiesto e quale glifo il font abbia effettivamente offerto. Dopo quella informazione è perduta
Il rilevatore deve osservare lo stato del subset, non il device context
È qui che la prima implementazione è andata storta, e la ragione vale la pena essere compresa perché si applica a qualsiasi controllo di copertura imbullonato a una pipeline di testo. HotPDF ha due vie di testo. Una emette attraverso un font TrueType Unicode registrato con una mappa caratteri in memoria costruita alla registrazione. L'altra è una via GDI legacy che crea un device context e un handle di font freschi per ogni sequenza di caratteri
Giudicare la copertura dalla via GDI è senza speranza. La sua mappatura non è quella che finisce nello stream di contenuto emesso, e le due non sono sincronizzate, quindi un rilevatore che legge i risultati GDI segnala l'intero intervallo ASCII stampabile come non risolto. La risposta autorevole vive nel font registrato: la mappa caratteri che RegisterUnicodeTTF analizza, interrogata attraverso GetUnicodeGlyphForCodepoint. Il rilevatore è quindi condizionato sullo stato subset-ready, non su alcuna condizione GDI, e semplicemente non gira sui documenti che non hanno mai registrato un font Unicode, il che è corretto perché quei documenti sono comunque limitati alle codifiche standard
Una seconda trappola gli sta accanto. Il nome di famiglia GDI di un font e il nome PostScript estratto dal binario del font alla registrazione sono stringhe diverse, e non in un modo normalizzabile: una famiglia chiamata Arial Unicode MS porta il nome PostScript ArialMT. Qualsiasi gate scritto come "il font attualmente selezionato è quello che abbiamo registrato", confrontato per nome, è codice morto che non scatta mai. Condizionare sullo stato, mai sui nomi dei font
Non testare un rilevatore di glifi con emoji
Il caso di test ovvio è una faccina, e la convincerà che il rilevatore sia rotto. I code point emoji comuni nei piani astrali si risolvono attraverso una via di sintesi a uso privato che li mappa direttamente a un indice di glifo, così non raggiungono mai il ramo generale di copertura. Il rilevatore si comporta correttamente e il test sta misurando la via sbagliata
Usare invece un code point non assegnato. U+0378 è permanentemente non allocato in Unicode, così nessun font può mapparlo legittimamente, ed esercita esattamente il ramo che si vuole verificare. Questa distinzione tra "la funzione è rotta" e "il test ha scelto un input che aggira la funzione" costa ore vere, e i code point non assegnati sono il modo più economico di evitarlo
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// Scatta una volta per code point unico, non una volta per occorrenza
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// Cablarlo in un lavoro di generazione
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// Fallire il lavoro invece di spedire una pagina con caselle
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Le catene di fallback sono per script, non per font
La ragione per cui il fallback è delimitato per script anziché per font sorgente è che i vuoti di copertura si raggruppano per sistema di scrittura. A un font di testo latino mancano Devanagari, thai, Han ed emoji, tutti insieme, e il sostituto per ciascuno è un font diverso. Dichiarare una catena per script descrive quindi il deployment reale: un font latino per il testo corrente, un font CJK, un font emoji, un catch-all
// THPDFFontScript copre hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji e hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
Fallback e rilevamento sono complementari anziché alternativi. Le catene gestiscono la copertura prevista; il rilevatore riporta quella non prevista, che su un sistema che elabora dati clienti arbitrari è la metà interessante. Si noti che sostituire un font cambia le metriche, così un paragrafo che ricade può rifluire; se il layout conta, il comportamento di closure e subsetting del font sostituito merita una lettura nell'articolo sulla closure del subset dei font, e gli script che richiedono riordinamento o giunzione sono gestiti dalla fase di shaping descritta in shaping del testo a script complessi
Come applicare un retrofit del comportamento senza rischiare la via esistente
La stessa release ha aggiunto un fallback sulla tabella kern legacy per la spaziatura delle coppie, e il modo in cui è stato delimitato è uno schema da copiare. Invece di aggiungere un nuovo punto decisionale alla logica di kerning, il fallback pende dal ramo di uscita anticipata che esisteva già per i font senza tabella GPOS. Un font moderno con GPOS non lo raggiunge mai, così il suo comportamento è invariato per costruzione anziché per testing. Le vie che non registrano un font Unicode producono due offset zero, così restano invariate anch'esse
Questa è la forma generale di un retrofit a basso rischio in una libreria di rendering matura: trovare il ramo che oggi non produce nulla e mettere lì il nuovo comportamento. Trasforma "crediamo che questo non abbia fatto regredire nulla" in "questo non può aver fatto regredire nulla", che è una cosa molto migliore da dire di un motore di testo per cui passano le fatture degli altri
Farne un gate, non un log
I riscontri di copertura sono utili solo se qualcosa fallisce su di essi. In un servizio di generazione documenti l'assetto produttivo è tenere il tracciamento attivo nel lavoro di regressione notturno su un corpus di veri nomi, indirizzi e descrizioni di prodotto dei clienti, e far fallire il lavoro su qualsiasi riscontro. Poiché l'evento scatta una volta per code point unico anziché una volta per occorrenza, l'output resta abbastanza piccolo da leggersi anche quando manca un intero script
In produzione lo stesso gestore è meglio impiegato come telemetria: registrare il code point e il font, continuare a servire il documento, e lasciare che l'aggregato dica quale script aggiungere al set di font di deployment dopo. Il comportamento di rendering per font incorporati e sostituiti è approfondito in rendering dei glifi dei font incorporati, e l'elenco completo delle proprietà incluso TrackUnresolvedGlyphs è documentato sulla pagina di prodotto di HotPDF Delphi PDF component