Tehnički članak

Kontrola zamjene PDF fontova u Delphiju uz PDFium

PDFium Component omogućuje Delphi aplikaciji da odluči koji se bajtovi fonta koriste kada PDF referencira font koji ne ugrađuje. ConfigureSystemFontProvider instalira implementaciju IPdfSystemFontProvider koja prima svaki zahtjev za mapiranje fonta koji PDFium uputi, potpun s nazivom pisma, debljinom, oznakom kurziva, znakovnim skupom i obitelji razmaka, te odgovara TrueType, TrueType Collection ili OpenType bajtovima koje treba koristiti

Ovo postoji jer su neugrađeni fontovi lutrija renderiranja. PDF koji imenuje Arial i ništa ne ugrađuje renderira se Arialom na radnoj stanici, metrički kompatibilnom zamjenom na Linux poslužitelju, i s čime god domaćinov mapper pronađe na zaključanoj slici kontejnera. Isti račun izgleda drugačije na svakom, prijelomi redaka se pomiču, a kupac dobiva dokument koji se ne podudara s arhiviranom kopijom

Zašto jednostavno ne instalirati fontove na poslužitelj?

Ponekad je to odgovor, i kada jest, prihvatite ga. No zakazuje u tri uobičajene situacije. Licenciranje može zabraniti instalaciju fonta na poslužitelj za automatizirano renderiranje. Slike kontejnera često se ponovno grade, a font instaliran ručno nestaje sa sljedećim deploymentom. A regulirani tijekovi rada zahtijevaju da mehanizam renderiranja bude reproducibilan iz artefakata pod kontrolom verzija, što instalacija fonta na razini stroja nije

Pružatelj usluge rješava sva tri prebacivanjem odluke u vašu aplikaciju. Fontovi se isporučuju kao resursi koje kontrolirate, politika mapiranja je kod koji možete pregledati, a isti binarni program renderira identično posvuda jer ništa ne ovisi o tome što je slučajno instalirano

Instaliranje pružatelja usluge

Konfiguracija se mora dogoditi prije nego što se knjižnica učita. PDFium prihvaća strukturu informacija o sistemskom fontu pri inicijalizaciji i zadržava ručke koje potom izdaje, pa bi zamjena pružatelja usluge dok su dokumenti otvoreni poništila ručke fontova koje PDFium još drži; komponenta to izravno odbacuje umjesto da dopusti da to ošteti renderiranje:

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
  // Deterministično mapiranje: naziv pisma plus debljina i kurziv odlučuju
  // koju datoteku isporučujemo za ovaj zahtjev
  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);   // potpuni sfnt ili TTC bajtovi
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // indeks unutar kolekcije
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // domaćin odlučuje o svemu
  Policy.AllowFaceSubstitution := False;  // odbaci drugačiji naziv pisma
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Tek sada učitajte knjižnicu i otvorite dokumente
end;

Rastavljanje se odvija obrnutim redoslijedom: pružatelj usluge prvo se odvaja od PDFium-a, a zatim se knjižnica uklanja iz memorije. Preskakanje odvajanja ostavlja izvorne ručke fontova koje pokazuju na Pascal objekte koji su na rubu oslobađanja, što je klasično kršenje pristupa memoriji pri gašenju u kodu koji miješa sučelja s brojanjem referenci s C knjižnicom

Što zastavice politike zapravo odlučuju

AllowDefaultFallback je prekidač između dva načina rada. Kada je isključen, zahtjev koji pružatelj usluge odbije jednostavno zakaže, što je ono što želite dok dokazujete da je svaki font u korpusu obuhvaćen: svaki propust postaje odmah vidljiv umjesto da bude prekriven. Kada je uključen, nerazriješeni zahtjevi delegiraju se mapperu koji vraća FPDF_GetDefaultSystemFontInfo, dok vanjski svijet i dalje vidi jedan jedinstveni omot ručke, s nazivom pisma, znakovnim skupom, podacima tablice i brisanjem fonta ispravno usmjerenima prema podrijetlu

AllowFaceSubstitution upravlja time smije li pružatelj usluge odgovoriti drugačijim nazivom pisma od zatraženog. Isključivanje te opcije čini zamjenu izričitom odlukom umjesto slučajnošću, što je važno kada dokument imenuje font čije se metrike razlikuju dovoljno da promijene paginaciju

Komponenta provjerava svaki odgovor pružatelja usluge prije nego što dosegne PDFium: prazni podaci se odbacuju, prevelike fontove se odbacuje prema MaxFontBytes, provjerava se TTC indeks, a pojedinačne sfnt tablice poslužuju se iz direktorija fonta kada PDFium zatraži tablicu umjesto cijele datoteke. Ta posljednja mogućnost znači da pružatelj usluge može predati potpunu datoteku fonta i pustiti komponentu da odgovara na upite na razini tablice, umjesto izlaganja sirovih Pascal objekata preko C ABI-ja

Predmemoriranje bez visećih podataka fonta

Zahtjevi za mapiranje fonta neprestano se ponavljaju tijekom renderiranja, pa se odgovori predmemoriraju s ključem koji obuhvaća svaki parametar odabira fonta, izbacuju se ograničenim redoslijedom najduže nekorišteno (LRU). Suptilnost je životni vijek: PDFium možda još čita bajtove fonta čiji je unos u predmemoriji tek izbačen

Predmemorija pohranjuje dinamička polja s brojanjem referenci, a svaka izvorna ručka drži vlastiti snimak, pa izbacivanje smanjuje referencu umjesto da oslobodi memoriju koja je u uporabi. Povratni poziv brisanja oslobađa ručku i održava aktivni brojač. U praksi to znači da se MaxCacheEntries može podešavati radi memorije bez ikakvog rizika povlačenja podataka ispod renderiranja koje je u tijeku

Poziva li se pružatelj usluge na mojoj niti?

Ne, ne nužno. PDFium može pozvati mapper iz vlastitih radnih niti, pa implementacija mora biti sigurna za niti (thread safe). Dijeljeni brojači, predmemorija i praćenje konfiguracije svaki su zaštićeni unutar komponente vlastitom kritičnom sekcijom, no kod unutar ResolveFont vaš je da ga učinite sigurnim

Najsigurniji oblik pružatelj je usluge koji ne dira nikakvo promjenjivo dijeljeno stanje: čitajte iz tablice izgrađene pri pokretanju, učitajte bajtove iz datoteke ili resursa, vratite rezultat. Ako pretraga treba vlastitu dijeljenu predmemoriju, zaštitite je. I zadržite iznimke unutar svoje implementacije, jer Pascal iznimka nikada ne smije odmotati kroz PDFium stog; komponenta ih hvata na granici C ABI-ja i pretvara u neuspjeh ili neobavezni zadani fallback, no oslanjanje na to kao na uobičajeni tok kontrole košta performanse i skriva greške. Pravila niti za ostatak komponente slijede ista načela kao ona u disciplini zaključavanja renderiranja

Dokazivanje mapiranja u produkciji

Statistika pretvara zamjenu fonta iz nagađanja u nešto što možete provjeriti. GetSystemFontProviderStatistics izvještava je li pružatelj usluge konfiguriran i instaliran, koliko je zahtjeva za mapiranje upućeno i kako su zadovoljeni, podijeljeno na pogotke predmemorije, pogotke pružatelja usluge i pogotke zadanog fallbacka, zajedno s odbačenim odgovorima, neuspjelim zahtjevima, aktivnim ručkama i predmemoriranim fontovima:

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]));

  // U provjeri usklađenosti s isključenim fallbackom, svaki pogodak
  // fallbacka ili neuspjeli zahtjev znači da je dokument referencirao font koji ne isporučujemo
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

Rastući broj RejectedProviderResponses signal je da pružatelj usluge odgovara podacima koje politika odbija, obično prevelikom datotekom ili zamijenjenim pismom, i vrijedi na to postaviti upozorenje jer ti zahtjevi tiho degradiraju na fallback ili neuspjeh. Za dijagnosticiranje koje fontove dokument doista treba prije nego što izgradite tablicu mapiranja, put inspekcije u analizi svojstava PDF fontova navodi ugrađene i neugrađene fontove po dokumentu

Opskrba fontovima, renderiranje i izdvajanje teksta dijele istu instancu knjižnice u Delphiju, C++Builderu i Lazarusu; detalji implementacije opisani su na stranici PDFium Component za Delphi