Articol tehnic

Detectarea glifelor PDF lipsă la momentul desenării

O glifă lipsă într-un PDF nu este o eroare. Producătorul cere un caracter pe care fontul selectat nu îl poate cartografia, fontul returnează indicele de glifă zero, iar fișierul care iese este structural valid, se deschide oriunde și arată o cutie goală acolo unde ar trebui un nume sau o sumă. Nimeni din pipeline-ul de generare nu află. Destinatarul află. HotPDF închide această buclă cu TrackUnresolvedGlyphs: activați-o și calea de desenare a textului înregistrează fiecare punct de cod a cărui căutare de glifă se rezolvă la indicele zero, declanșând OnUnresolvedGlyph o dată per constatare unică, cu punctul de cod, fontul pe care a eșuat, scriptul căruia îi aparține și o sugestie de fonturi care l-ar acoperi

Detectarea este jumătate din răspuns. Cealaltă jumătate este SetFontFallbackChain, care înregistrează o listă ordonată de fonturi per script, astfel încât cazurile comune se rezolvă singure, iar doar lacunele reale ajung la handlerul dumneavoastră. Împreună transformă o clasă de defecte care erau raportate de clienți într-o verificare la momentul build

De ce nu declanșează nimic o glifă lipsă?

Pentru că ISO 32000 nu impune niciun producătorului obligația de a verifica acoperirea, iar indicele de glifă zero este o glifă legitimă. Este .notdef, a cărui contur o alege designerul fontului: de obicei un dreptunghi gol sau vid, uneori nimic deloc. Un vizualizator care o desenează se comportă corect. Extragerea de text poate chiar returna caracterele corecte, deoarece cartografierea /ToUnicode este scrisă din textul sursă, nu din contururi, deci o verificare automată de dus-întors va trece fericit un document al cărui text vizibil are găuri

Diagramă a motivului pentru care o glifă PDF lipsă rămâne tăcută: glifa zero desenează o cutie goală, în timp ce extracția ToUnicode trece verificările de dus-întors
Glifa zero este un răspuns .notdef legitim, iar /ToUnicode este scris din textul sursă, deci nimic din pipeline nu este informat despre lacună

Consecința practică este că acoperirea trebuie verificată la momentul desenării, când biblioteca mai știe încă ce punct de cod a fost cerut și ce glifă a oferit efectiv fontul. După aceea informația a dispărut

Detectorul trebuie să observe starea de subset, nu contextul de dispozitiv

Aici a greșit prima implementare, iar motivul merită înțeles, deoarece se aplică oricărei verificări de acoperire montată pe un pipeline de text. HotPDF are două căi de text. Una emite printr-un font TrueType Unicode înregistrat cu o hartă de caractere în memorie construită la momentul înregistrării. Cealaltă este o cale GDI legacy care creează un context de dispozitiv nou și un handle de font per șir de caractere

A judeca acoperirea din calea GDI este fără speranță. Cartografierea ei nu este cartografierea care ajunge în content stream-ul emis, iar cele două nu sunt sincronizate, deci un detector care citește rezultatele GDI raportează tot intervalul ASCII imprimabil ca nerezolvat. Răspunsul autoritar trăiește în fontul înregistrat: harta de caractere pe care RegisterUnicodeTTF o parsează, interogată prin GetUnicodeGlyphForCodepoint. Detectorul este prin urmare filtrat pe starea de subset pregătit, nu pe nicio condiție GDI, și pur și simplu nu rulează pe documentele care nu au înregistrat niciodată un font Unicode, ceea ce este corect, deoarece acele documente sunt oricum restricționate la codificările standard

O a doua capcană stă lângă ea. Numele de familie GDI al unui font și numele PostScript extras din binarul fontului la înregistrare sunt șiruri diferite, și nu într-un mod pe care îl puteți normaliza: o familie numită Arial Unicode MS poartă numele PostScript ArialMT. Orice poartă scrisă ca „este fontul selectat curent cel pe care l-am înregistrat", comparată după nume, este cod mort care nu se declanșează niciodată. Filtrați pe stare, niciodată pe nume de fonturi

Fluxul de detectare a glifelor nerezolvate HotPDF, arătând poarta pe starea de subset, interogarea GetUnicodeGlyphForCodepoint și conectarea evenimentului OnUnresolvedGlyph
Acoperirea este judecată din harta fontului Unicode înregistrat, nu din GDI, iar fiecare punct de cod unic declanșează un eveniment cu o sugestie de font

Nu testați un detector de glife cu emoji

Cazul de test evident este o față zâmbitoare, și vă va convinge că detectorul este stricat. Punctele de cod emoji comune în planurile astrale se rezolvă printr-o cale de sinteză private-use care le cartografiază direct la un indice de glifă, deci nu ajung niciodată la ramura generală de acoperire. Detectorul se comportă corect, iar testul măsoară calea greșită

Folosiți în schimb un punct de cod neatribuit. U+0378 este permanent nealocat în Unicode, deci niciun font nu îl poate cartografia legitim, și exercită exact ramura pe care vreți să o verificați. Distincția aceasta între „funcția este stricată" și „testul a ales o intrare care ocolește funcția" costă ore reale, iar punctele de cod neatribuite sunt calea cea mai ieftină de a o evita

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
  // Se declanșează o dată per punct de cod unic, nu o dată per apariție
  FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
    [Info.CodePoint, String(Info.FontName), Ord(Info.Script),
     String(Info.SuggestedFonts)]));
end;

// Conectarea lui într-o sarcină de generare
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
    // Faceți sarcina să eșueze în loc să livrați o pagină cu cutii
    raise Exception.Create(Audit.Findings.Text);
finally
  Pdf.Free;
end;

Lanțurile de rezervă sunt per script, nu per font

Motivul pentru care rezerva este delimitată per script, nu per font sursă, este că lacunele de acoperire se adună pe sisteme de scriere. Un font de text latin nu are Devanagari, thailandeză, Han și emoji, toate deodată, iar substitutul pentru fiecare este alt font. Declararea unui lanț per script descrie deci implementarea reală: un font latin pentru textul corpului, un font CJK, un font emoji, un catch-all

Diagramă de rezervă de fonturi per script, cartografiind scripturile hfsCJK, hfsArabic, hfsEmoji și hfsOther la lanțuri ordonate de fonturi substitut în HotPDF
Fiecare script primește propriul lanț ordonat, astfel încât un font de corp latin lipsit de Han, arabă sau emoji cade pe un font care îl acoperă
// THPDFFontScript acoperă hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji și 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']);

Rezerva și detectarea sunt complementare, nu alternative. Lanțurile gestionează acoperirea pe care ați anticipat-o; detectorul raportează acoperirea pe care nu ați anticipat-o, ceea ce, pe un sistem care procesează date arbitrare de la clienți, este jumătatea interesantă. Rețineți că înlocuirea unui font schimbă metricile, deci un paragraf care cade pe rezervă se poate rearanja; dacă layoutul contează, comportamentul de closure și subsetting al fontului substituit merită citit în articolul despre closure-ul subsetului de font, iar scripturile care au nevoie de reordonare sau legare sunt gestionate de etapa de shaping descrisă în shaping-ul de text pentru scripturi complexe

Cum să adăugați ulterior comportament fără a risca calea existentă

Aceeași versiune a adăugat o rezervă de tabelă kern legacy pentru spațierea perechilor, iar modul în care a fost delimitată este un tipar care merită copiat. În loc să adauge un nou punct de decizie logicii de kerning, rezerva atârnă de ramura de ieșire timpurie care exista deja pentru fonturile fără tabelă GPOS. Un font modern cu GPOS nu ajunge niciodată la ea, deci comportamentul lui este neschimbat prin construcție, nu prin testare. Căile care nu înregistrează un font Unicode produc două offset-uri zero, deci rămân și ele neschimbate

Aceasta este forma generală a unui retrofit cu risc scăzut într-o bibliotecă de randare matură: găsiți ramura care în prezent nu produce nimic și puneți comportamentul nou acolo. Transformă „credem că acest lucru nu a regresat nimic" în „acesta nu are cum să fi regresat nimic", ceea ce este mult mai bine de spus despre un motor de text prin care trec facturile altora

Faceți din ea o poartă, nu un jurnal

Constatările de acoperire sunt utile doar dacă ceva eșuează pe ele. Într-un serviciu de generare de documente aranjamentul productiv este să păstrați urmărirea activă în sarcina de regresie nocturnă contra unui corpus de nume, adrese și descrieri de produse reale de la clienți, și să faceți sarcina să eșueze la orice constatare. Deoarece evenimentul se declanșează o dată per punct de cod unic, nu o dată per apariție, rezultatul rămâne suficient de mic pentru a fi citit chiar și când lipsește un script întreg

În producție același handler este mai bine folosit ca telemetrie: înregistrați punctul de cod și fontul, continuați să serviți documentul, și lăsați agregatul să vă spună ce script să adăugați următorul în setul de fonturi de implementare. Comportamentul de randare pentru fonturile înglobate și substituite este tratat în continuare în randarea glifelor fonturilor înglobate, iar lista completă de proprietăți inclusiv TrackUnresolvedGlyphs este documentată pe pagina de produs HotPDF Delphi PDF component