Tehnički članak

Kontrola zamene fontova u PDF-u u Delphiju pomoću PDFium-a

PDFium Component omogućava Delphi aplikaciji da odluči koji bajtovi fonta se koriste kada PDF referencira font koji ne ugrađuje. ConfigureSystemFontProvider instalira implementaciju IPdfSystemFontProvider koja prima svaki zahtev za mapiranje fonta koji PDFium napravi, kompletan sa imenom lica, debljinom, italik zastavicom, skupom znakova i porodicom koraka, i odgovara sa bajtovima TrueType, TrueType kolekcije ili OpenType koje treba koristiti

Ovo postoji jer su neugrađeni fontovi lutrija renderovanja. PDF koji imenuje Arial, a ne ugrađuje ništa, renderuje se sa Arial-om na radnoj stanici, sa metrički kompatibilnom zamenom na Linux serveru, i sa čim god mapper domaćina pronađe na zaključanoj kontejnerskoj slici. Ista faktura izgleda drugačije na svakoj, prelomi redova se pomeraju, a kupac dobija dokument koji se ne poklapa sa arhiviranom kopijom

Zašto jednostavno ne instalirati fontove na server?

Ponekad je to odgovor, i kada jeste, prihvatite ga. Ali to ne uspe u tri česte situacije. Licenciranje može zabraniti instaliranje fonta na server za automatizovano renderovanje. Kontejnerske slike se često ponovo grade, a ručno instaliran font nestaje sa sledećim raspoređivanjem. A regulisani tokovi rada zahtevaju da pipeline renderovanja bude reprodukovan iz artefakata pod kontrolom verzija, što instalacija fonta na nivou mašine nije

Provajder rešava sva tri premeštanjem odluke u vašu aplikaciju. Fontovi se isporučuju kao resursi koje kontrolišete, politika mapiranja je kod koji možete pregledati, a isti binarni fajl renderuje identično svuda jer ništa ne zavisi od toga šta je slučajno instalirano

Instaliranje provajdera

Konfiguracija mora da se desi pre nego što se biblioteka učita. PDFium prihvata strukturu informacija o sistemskom fontu pri inicijalizaciji i zadržava ručke koje kasnije deli, tako da bi zamena provajdera dok su dokumenti otvoreni poništila ručke fonta koje PDFium i dalje drži; komponenta to potpuno odbija umesto da dozvoli da to pokvari renderovanje:

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čko mapiranje: ime lica plus debljina i italik odlučuju
  // koji fajl isporučujemo za ovaj zahtev
  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);   // kompletni 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 sve
  Policy.AllowFaceSubstitution := False;  // odbaci drugačije ime lica
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

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

Gašenje se izvodi obrnutim redosledom: provajder se prvo odvaja od PDFium-a, zatim se biblioteka rasterećuje. Preskakanje odvajanja ostavlja izvorne ručke fonta da pokazuju na Pascal objekte koji se upravo brišu, što je klasičan pristup pristupu pri gašenju u kodu koji meša interfejse sa brojanjem referenci sa C bibliotekom

Šta zastavice politike zapravo odlučuju

AllowDefaultFallback je prekidač između dva operativna režima. Kada je isključen, zahtev koji provajder odbije jednostavno ne uspe, što je ono što želite dok dokazujete da je svaki font u korpusu obuhvaćen: svaka praznina odmah postaje vidljiva umesto da se prekrije. Kada je uključen, nerazrešeni zahtevi se prosleđuju mapperu koji vraća FPDF_GetDefaultSystemFontInfo, dok spoljašnji svet i dalje vidi jedan jedinstven omotač ručke, sa imenom lica, skupom znakova, podacima tabele i rutinom brisanja fonta ispravno usmerenim po poreklu

AllowFaceSubstitution upravlja time da li provajder sme da odgovori drugačijim imenom lica od traženog. Isključivanje ovoga čini zamenu izričitom odlukom, a ne slučajnošću, što je bitno kada dokument imenuje font čije se metrike razlikuju dovoljno da promene paginaciju

Komponenta validira svaki odgovor provajdera pre nego što stigne do PDFium-a: prazni podaci se odbacuju, prevelike fontove odbacuje MaxFontBytes, TTC indeks se proverava, a pojedinačne sfnt tabele se poslužuju iz direktorijuma fonta kada PDFium zatraži tabelu umesto ceo fajl. Ta poslednja mogućnost znači da provajder može predati kompletan fajl fonta i pustiti komponentu da odgovori na upite na nivou tabele, umesto da izlaže sirove Pascal objekte preko C ABI-ja

Keširanje bez visećih podataka fonta

Zahtevi za mapiranje fonta se stalno ponavljaju tokom renderovanja, pa se odgovori keširaju sa ključem koji pokriva svaki parametar izbora fonta, izbačen po omeđenom najmanje-nedavno-korišćenom redosledu. Suptilnost je vek trajanja: PDFium možda i dalje čita bajtove fonta čiji je unos keša upravo izbačen

Keš čuva dinamičke nizove sa brojanjem referenci, a svaka izvorna ručka drži sopstveni snimak, tako da izbacivanje odbacuje referencu umesto da oslobodi memoriju u upotrebi. Povratni poziv brisanja oslobađa ručku i održava aktivan brojač. Praktično, ovo znači da se MaxCacheEntries može podešavati za memoriju bez ikakvog rizika da se podaci povuku ispod renderovanja u toku

Da li se provajder poziva na mojoj niti?

Ne, ne nužno. PDFium može pozvati mapper iz sopstvenih radnih niti, pa implementacija mora biti bezbedna za niti. Deljeni brojači, keš i posmatranje konfiguracije su svaki zaštićeni unutar komponente sopstvenom kritičnom sekcijom, ali kod unutar ResolveFont je vaš da ga učinite bezbednim

Najbezbedniji oblik je provajder koji ne dodiruje deljeno promenljivo stanje: čitajte iz tabele izgrađene pri pokretanju, učitajte bajtove iz fajla ili resursa, vratite. Ako pretraga treba sopstveni deljeni keš, zaštitite ga. I držite izuzetke unutar sopstvene implementacije, jer Pascal izuzetak nikada ne sme da se odmota kroz PDFium stek; komponenta hvata na granici C ABI-ja i pretvara u otkaz ili opcioni podrazumevani fallback, ali oslanjanje na to kao normalan tok kontrole košta performanse i skriva greške. Pravila niti za ostatak komponente prate ista načela kao ona u disciplini brave renderovanja

Dokazivanje mapiranja u produkciji

Statistika pretvara zamenu fontova iz nagađanja u nešto na šta možete izneti tvrdnju. GetSystemFontProviderStatistics prijavljuje da li je provajder konfigurisan i instaliran, koliko je zahteva za mapiranje napravljeno, i kako su zadovoljeni, podeljeno na pogotke keša, pogotke provajdera i pogotke podrazumevanog fallback-a, uz odbačene odgovore, neuspele zahteve, žive ručke i keširane fontove:

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 testu usklađenosti sa isključenim fallback-om, bilo koji pogodak
  // fallback-a ili neuspeo zahtev 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 je signal da provajder odgovara podacima koje politika odbija, obično prevelikim fajlom ili zamenjenim licem, i vredi na to postaviti alarm jer ti zahtevi tiho degradiraju na fallback ili otkaz. Za dijagnostikovanje kojih fontova dokument zapravo treba pre nego što izgradite tabelu mapiranja, put inspekcije u analizi svojstava PDF fonta navodi ugrađene i neugrađene fontove po dokumentu

Nabavka fontova, renderovanje i izvlačenje teksta dele istu instancu biblioteke preko Delphija, C++Buildera i Lazarusa; detalji raspoređivanja su opisani na stranici PDFium Component za Delphi