Articolo tecnico

Rilevare i glifi PDF mancanti al momento del disegno

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

Diagramma di perché un glifo PDF mancante resta silenzioso mentre il glifo zero disegna una casella vuota e l'estrazione ToUnicode supera i controlli di round-trip
Il glifo zero è una risposta .notdef legittima e /ToUnicode è scritto dal testo sorgente, così nulla nella pipeline viene avvisato del vuoto

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

Flusso di rilevamento dei glifi non risolti di HotPDF che mostra il gate sullo stato subset, la ricerca con GetUnicodeGlyphForCodepoint e il cablaggio dell'evento OnUnresolvedGlyph
La copertura è giudicata dalla mappa del font Unicode registrato anziché da GDI, e ogni code point unico genera un evento con un suggerimento di 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

Diagramma di fallback font per script che mappa gli script hfsCJK, hfsArabic, hfsEmoji e hfsOther a catene ordinate di font sostitutivi in HotPDF
Ogni script riceve la propria catena ordinata, così un font latino per il testo mancante di Han, arabo o emoji ricade su un font che lo copre
// 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