Articol tehnic

Controlează substituirea fonturilor PDF în Delphi cu PDFium

PDFium Component permite unei aplicații Delphi să decidă ce octeți de font sunt folosiți atunci când un PDF referă un font pe care nu-l încorporează. ConfigureSystemFontProvider instalează o implementare IPdfSystemFontProvider care primește fiecare cerere de mapare de font pe care PDFium o face, completă cu numele feței, greutatea, indicatorul italic, setul de caractere și familia de pitch, și răspunde cu octeții TrueType, TrueType Collection sau OpenType de folosit

Aceasta există deoarece fonturile neîncorporate sunt o loterie de randare. Un PDF care numește Arial și nu încorporează nimic randează cu Arial pe o stație de lucru, cu un substitut compatibil metric pe un server Linux, și cu orice găsește mapatorul gazdă pe o imagine de container blocată. Aceeași factură arată diferit pe fiecare, întreruperile de rând se mută, iar un client primește un document care nu se potrivește cu copia arhivată

De ce să nu instalezi pur și simplu fonturile pe server?

Uneori acesta este răspunsul, iar când e cazul, alege-l. Dar eșuează în trei situații comune. Licențierea poate interzice instalarea unui font pe un server pentru randare automatizată. Imaginile de container sunt reconstruite frecvent, iar un font instalat manual dispare la următoarea implementare. Iar fluxurile de lucru reglementate au nevoie ca stiva de randare să fie reproductibilă din artefacte sub control de versiune, ceea ce o instalare de font la nivel de mașină nu este

Un furnizor rezolvă toate trei mutând decizia în aplicația ta. Fonturile se livrează ca resurse pe care le controlezi, politica de mapare este cod pe care îl poți revizui, iar același binar randează identic pretutindeni deoarece nimic nu depinde de ce se întâmplă să fie instalat

Instalarea unui furnizor

Configurarea trebuie să se întâmple înainte ca biblioteca să fie încărcată. PDFium acceptă o structură de informații de font de sistem la inițializare și păstrează handle-urile pe care le distribuie ulterior, așa că schimbarea unui furnizor cât timp documentele sunt deschise ar invalida handle-uri de font pe care PDFium încă le deține; componenta respinge direct acest lucru, în loc să lase corupt un randare:

uses
  PDFium;

type
  TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
  public
    function ResolveFont(const Request: TPdfSystemFontRequest;
      out Font: TPdfSystemFontData): Boolean;
  end;

function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
  out Font: TPdfSystemFontData): Boolean;
var
  Path: string;
begin
  // Mapare determinist: numele feței plus greutatea și italicul decid
  // ce fișier livrăm pentru această cerere
  Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
    Request.Italic, Request.Charset);
  Result := Path <> '';
  if not Result then
    Exit;
  Font.FaceName := Request.FaceName;
  Font.FontData := LoadFileBytes(Path);   // octeți sfnt sau TTC compleți
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // index în interiorul unei colecții
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // gazda decide totul
  Policy.AllowFaceSubstitution := False;  // respinge un nume de față diferit
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Abia acum încarcă biblioteca și deschide documente
end;

Demontarea rulează în ordine inversă: furnizorul este detașat de PDFium mai întâi, apoi biblioteca este descărcată. Săritul peste detașare lasă handle-uri de font native care indică spre obiecte Pascal pe cale să fie eliberate, care e clasica încălcare de acces la închidere în cod care amestecă interfețe cu numărare de referințe cu o bibliotecă C

Ce decid efectiv indicatorii de politică

AllowDefaultFallback este comutatorul între două moduri de operare. Cu el dezactivat, o cerere pe care furnizorul o refuză pur și simplu eșuează, ceea ce vrei cât timp dovedești că fiecare font dintr-un corpus este contabilizat: orice gol devine vizibil imediat, în loc să fie mascat. Cu el activat, cererile nerezolvate sunt delegate mapatorului returnat de FPDF_GetDefaultSystemFontInfo, în timp ce lumea exterioară tot vede un singur înveliș de handle uniform, cu numele feței, setul de caractere, datele de tabel și rutina de ștergere a fontului direcționate corect după origine

AllowFaceSubstitution guvernează dacă un furnizor poate răspunde cu un nume de față diferit de cel cerut. Dezactivarea ei transformă substituirea într-o decizie explicită, nu un accident, ceea ce contează când un document numește un font ale cărui metrici diferă suficient încât să schimbe paginarea

Componenta validează fiecare răspuns de furnizor înainte de a ajunge la PDFium: datele goale sunt respinse, fonturile supradimensionate sunt respinse comparativ cu MaxFontBytes, indexul TTC este verificat, iar tabelele sfnt individuale sunt servite din directorul de fonturi când PDFium cere un tabel, nu întregul fișier. Acea ultimă capacitate înseamnă că un furnizor poate preda un fișier de font complet și lăsa componenta să răspundă la interogări la nivel de tabel, în loc să expună obiecte Pascal brute peste ABI-ul C

Cache fără date de font suspendate

Cererile de mapare de font se repetă constant în timpul randării, așa că răspunsurile sunt puse în cache cu o cheie ce acoperă fiecare parametru de selecție de font, evacuate în ordine LRU (cel mai puțin recent folosit) mărginită. Subtilitatea este durata de viață: PDFium poate încă citi octeții unui font a cărui intrare de cache tocmai a fost evacuată

Cache-ul stochează array-uri dinamice cu numărare de referințe, iar fiecare handle nativ deține propriul instantaneu, astfel încât evacuarea elimină o referință, nu eliberează memorie în uz. Callback-ul de ștergere eliberează handle-ul și menține un contor activ. Practic, asta înseamnă că MaxCacheEntries poate fi ajustat pentru memorie fără niciun risc de a smulge date de sub o randare în desfășurare

Este furnizorul apelat pe firul meu?

Nu, nu neapărat. PDFium poate apela mapatorul din propriile fire de execuție worker, așa că o implementare trebuie să fie sigură pentru fire multiple (thread-safe). Contoarele partajate, cache-ul și observarea configurației sunt fiecare protejate în interiorul componentei prin propria secțiune critică, dar codul din interiorul ResolveFont este responsabilitatea ta de a-l face sigur

Forma cea mai sigură este un furnizor care nu atinge nicio stare partajată mutabilă: citește dintr-un tabel construit la pornire, încarcă octeți dintr-un fișier sau o resursă, returnează. Dacă o căutare are nevoie de un cache partajat propriu, protejează-l. Și păstrează excepțiile în interiorul implementării tale, deoarece o excepție Pascal nu trebuie niciodată să se propage prin stiva PDFium; componenta captează la granița ABI-ului C și convertește într-un eșec sau o soluție de rezervă opțională implicită, dar a te baza pe asta ca flux de control normal costă performanță și ascunde bug-uri. Regulile de threading pentru restul componentei urmează aceleași principii ca cele din disciplina de blocare la randare

Dovedirea maparii în producție

Statisticile transformă substituirea de fonturi din ghicit în ceva pe care poți afirma. GetSystemFontProviderStatistics raportează dacă un furnizor este configurat și instalat, câte cereri de mapare au fost făcute și cum au fost satisfăcute, împărțite în hit-uri de cache, hit-uri de furnizor și hit-uri de soluție de rezervă implicită, împreună cu răspunsuri respinse, cereri eșuate, handle-uri active și fonturi puse în cache:

var
  Stats: TPdfSystemFontStatistics;
begin
  Stats := GetSystemFontProviderStatistics;
  Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
    [Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
     Stats.DefaultFallbackHits]));
  Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
    [Stats.RejectedProviderResponses, Stats.FailedRequests,
     Stats.ActiveHandles, Stats.CachedFonts]));

  // Într-o rulare de conformitate cu soluția de rezervă dezactivată, orice hit
  // de rezervă sau cerere eșuată înseamnă că un document a referit un font pe care
  // nu-l livrăm
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Un contor RejectedProviderResponses în creștere este semnalul că un furnizor răspunde cu date pe care politica le refuză, de obicei un fișier supradimensionat sau o față substituită, și merită alertă, deoarece acele cereri degradează tăcut la soluția de rezervă sau la eșec. Pentru diagnosticarea fonturilor de care un document are efectiv nevoie înainte de a construi tabelul de mapare, ruta de inspecție din analizarea proprietăților de font PDF listează fonturile încorporate și neîncorporate per document

Provizionarea de fonturi, randarea și extragerea de text împart aceeași instanță de bibliotecă în Delphi, C++Builder și Lazarus; detaliile de implementare sunt descrise pe pagina componentei PDFium pentru Delphi