Tehnički članak

PDFium Library Config: kad Brotli tiho zameni Skiu

U PDFium Component-u za Delphi, uključivanje BrotliEnabled-a ili IsolatePerDocument-a u TPdfLibraryConfiguration prebacivalo je uključeni Skia build na AGG renderer bez ijedne greške, jer obe opcije podižu FPDF_LIBRARY_CONFIG na verziju na kojoj PDFium čita m_RendererType doslovno. Od v3.123.0 podrazumevani renderer ostaje sopstveni podrazumevani DLL-a, a od v3.125.0 Skia ili Fontations zahtev koji DLL ne može da ispuni podiže uhvatljiv EPdfError umesto da obara proces

Nijedan bag se nije najavio. Prvi je proizvodio stranice koje su izgledale dobro, samo renderovane drugim rasterizatorom, s mrvicu drugačijim glatkanjem ivica i tekstom od builda koji ste isporučili i testirali. Drugi se najavio, glomazno, obaranjem procesa domaćina iz unutrašnjosti nativne inicijalizacije. Oba dolaze s istog mesta: verzionisana C struktura čija se polja računaju tek kad broj verzije kaže da se računaju, i čije nulte vrednosti nisu „nepostavljeno“ nego prave odluke

Kako FPDF_LIBRARY_CONFIG odlučuje koji renderer PDFium koristi?

FPDF_InitLibraryWithConfig pita m_RendererType samo kad je polje Version strukture 4 ili više, i od te verzije koristi vrednost tačno onakvom kakva je upisana. Ispod verzije 4 PDFium ignoriše polje i bira podrazumevani build-a, što je Skia u buildovima kompajliranim s PDF_USE_SKIA i AGG svuda drugde

Svako kasnije polje prati isti obrazac. Struktura je rasla po jednoj sposobnosti, i svaka sposobnost je stigla zajedno s novim brojem verzije. PDFium Component gradi nativnu strukturu u LoadLibrary-ju iz vašeg TPdfLibraryConfiguration-a i podiže verziju samo toliko koliko opcije koje ste postavili traže

Verzija strukturePolje koje dodajePostavlja se
2m_pIsolate, m_v8EmbedderSlotUvek se piše; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform nije nil
4m_RendererTypeRenderer različit od prpDefault
5m_FontLibraryTypeFontBackend različit od pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Zamka je u poslednja dva reda. Verzije su kumulativne: struktura verzije 6 je i struktura verzije 4 i verzije 5, pa PDFium čita m_RendererType i m_FontLibraryType iako ste tražili samo Brotli. Šta god stoji u ta dva polja u tom trenutku postaje renderer i font bekend, da li ste ih nameravali birati ili ne

PDFium Component FPDF_LIBRARY_CONFIG merdevine verzija od verzije 2 do verzije 7 pokazuju koja TPdfLibraryConfiguration opcija dodaje m_RendererType, m_FontLibraryType, m_BrotliEnabled i m_IsolatePerDocument, i zašto kumulativne verzine čine polje renderera nule namernim AGG izborom umesto nepostavljene vrednosti na bilo kom buildu
Svaka opcija podiže verziju strukture i svako ranije polje ostaje živo, pa nula u m_RendererType stiže do PDFium-a kao eksplicitan AGG zahtev

Zašto je uključenje Brotli-ja zamenilo renderer s AGG-om?

Pre v3.123.0 PDFium Component je upisivao FPDF_RENDERERTYPE_AGG u m_RendererType za prpDefault, pa je svaka konfiguracija koja je gurala strukturu na verziju 6 ili 7 nameće AGG na Skia buildu. Runtimei pdfium.dll i pdfium.v8.dll koji se isporučuju s komponentom su Skia buildovi, pa je ovo pogodilo podrazumevano raspoređivanje, ne neki egzotičan slučaj

Preslikavanje je delovalo bezopasno kad je napisano. Na verziji 2 ili 3 polje se nikad ne čita, pa je prpDefault zaista značio „šta god DLL radi“. Trenutak kad je BrotliEnabled (verzija 6) ili IsolatePerDocument (verzija 7) ušao u sliku, isti kod pretvorio je „bez preferencije“ u eksplicitan AGG zahtev. Ništa nije palo. PDFium se inicijalizovao normalno, renderovao svaku stranicu i vratio se bez koda greške, jer je iz njegove tačke gledišta pozivaoc tražio AGG i dobio AGG

Heš piksela čini zamenu vidljivom tamo gde snimci ekrana ne vidi. Renderovanje prve stranice istog oglednog dokumenta pod tri konfiguracije dalo je:

  • Podrazumevana konfiguracija: heš 502D77C3711B4ACF
  • BrotliEnabled = True uz Renderer ostavljen na prpDefault: heš F75B5EB4728ADE87
  • Eksplicitan prpAgg: heš F75B5EB4728ADE87, identičan Brotli pokretanju

Popravka u v3.123.0 jeste javna funkcija PdfNativeRendererType, koja razrešava TPdfRendererPreference u vrednost upisanu u m_RendererType. prpAgg i prpSkia se preslikavaju jedan u jedan. prpDefault se sada preslikava u Skia kad učitani DLL izvozi FPDF_RenderPageSkia i u AGG inače. Taj izvoz se kompajlira pod istim uslovom PDF_USE_SKIA kao i sama Skia podrazumevana, što ga čini jedinim svojstvom builda koje možete posmatrati spolja iz DLL-a. Posle popravke Brotli konfiguracija daje isti heš kao podrazumevana

PDFium Component poređenje heševa piksela pokazuje heš podrazumevanog Skia renderovanja 502D77C3711B4ACF, konfiguraciju BrotliEnabled pre v3.123.0 koja se poklapa s eksplicitnim prpAgg pokretanjem s hešem F75B5EB4728ADE87, i ispravljeni omotač koji razrešava prpDefault kroz FPDF_RenderPageSkia izvoz nazad na prvobitni Skia heš
Heš piksela hvata ono što snimci ekrana kriju: uključen Brotli renderovao je svaku stranicu s AGG-om, a ispravljena podrazumevana sada se poklapa s nedirnutom konfiguracijom

Font bekend nikad nije imao isti problem. m_FontLibraryType se čita od verzije 5, i njegova nulta vrednost, FPDF_FONTBACKENDTYPE_FREETYPE, jeste i PDFium-ov podrazumevani kad se polje uopšte ne čita. Pisanje FreeType-a za pfbpDefault zato reprodukuje nativnu podrazumevanu tačno. Nulte vrednosti nisu uvek pogrešne, samo nikad nisu automatski ispravne

S v3.123.0 ili kasnijom, startni kod koji biste prirodno napisali sada radi ono što kaže:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Mora da radi pre nego što bilo šta učita nativnu biblioteku
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // podiže FPDF_LIBRARY_CONFIG na verziju 6
  // Renderer ostaje prpDefault: razrešuje se u Skia na buildovima koji izvoze
  // FPDF_RenderPageSkia i u AGG na samo-AGG buildovima
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Zapamtite da BrotliEnabled čini PDF 2.0 /BrotliDecode tokove dekodirljivim samo kad je i sam DLL buildovan s PDF_ENABLE_BROTLI-om. Flag je zahtev, i na buildu bez Brotli podrške nema efekta. TPdfLibraryConfiguration.Hardened je isto što i Default osim što je AllowMachineTime False, što sprečava JavaScript dokumenta da čita pravi sat; razumna je početna tačka za serversku obradu nepoverljivih fajlova

Šta se dešava kad zahtevate bekend koji DLL ne sadrži?

PDFium ne vraća grešku za renderer ili font bekend kojeg nema u buildu: FPDF_InitLibraryWithConfig ne uspeva nativni CHECK, što se na Windowsu pokazuje kao breakpoint izuzetak i, bez strukturiranog handlera izuzetaka oko poziva, prekida proces. Zaglavlje to i kaže, uz upozorenje da nepodržana vrednost „će slično pasti s trenutnim crash-om“

Dva konkretna slučaja su samo-AGG build koji primi FPDF_RENDERERTYPE_SKIA, i build bez Fontations-a koji primi FPDF_FONTBACKENDTYPE_FONTATIONS. Uključeni Skia runtime je u drugoj grupi: renderuje s Skiom ali koristi FreeType za fontove. Traženje prpSkia uz pfbpFontations nad njim dalo je External exception 80000003 na Delphi strani. Kad to slučajno uhvati debugger ili handler izuzetaka, situacija je i dalje neopoziva:

  • PDFium je ostavljen poinicijalizovan
  • Konfiguracija za ceo proces je već zalepljena, pa ConfigurePdfLibrary odbija ispravljenu konfiguraciju
  • Ponavljanje s drugačijom konfiguracijom u istom procesu više nije moguće

Ovo je suprotni neuspeh od Brotli baga. Tamo je polje nosilo vrednost koju niko nije izabrao i PDFium ju je tiho prihvatio. Ovde polje nosi vrednost koju je pozivaoc svesno izabrao i PDFium o njoj ne prima nikakvu raspravu. Oba su problemi koje omotač mora rešiti pre nativnog poziva, jer posle njega nema više ničega što se može uhvatiti

Kako PDFium Component prethodno proverava Skiu i Fontations

Od v3.125.0 LoadLibrary validira konfiguraciju posle vezivanja izvoza DLL-a i pre poziva FPDF_InitLibraryWithConfig, i pretvara nepodržan renderer ili font bekend u EPdfError s porukom koja imenuje pogrešnu postavku i alternative. DLL se istovaruje i konfiguracija se odlepljuje, pa pozivaoc može birati druge postavke i učitati ponovo

Odluka sama živi u čistoj funkciji PdfLibraryConfigurationSupportError, koja prima konfiguraciju plus dva Booleana koji opisuju build i vraća prazan string kad je kombinacija bezbedna. Pošto ne dira nativno stanje, možete je zvati iz sopstvenih testova s bilo kojom kombinacijom sposobnosti. Unutar LoadLibrary-ja dva Booleana dolaze iz različitih vrsta dokaza, i zaslužuju različite nivoe poverenja:

  • Skia se otkriva po prisustvu izvoza FPDF_RenderPageSkia, istog signala kojim se služi PdfNativeRendererType. Izvoz i Skia renderer kompajliraju se pod jednim uslovom, pa je provera tačna
  • Fontations nema izvoz za sebe. Jedini trag koji ostavlja jesu Rust font crates koje povlači u binarni fajl, pa PDFium Component pregleda učitanu bibliotečku datoteku za imenima crates-a skrifa i read-fonts (takođe read_fonts). Pregled radi samo kad se traži pfbpFontations, i fajl koji se ne može pročitati računa se kao „bez Fontations-a“

Fontations provera je heuristika, i može pogrešiti u jednom smeru: Fontations build ogoljen od svih tih stringova bio bi odbijen iako je mogao da radi. Taj kompromis je sklopljen namerno. Lažno odbijanje košta vas izuzetak koji možete uhvatiti i pad na FreeType. Lažno prihvatanje košta vas proces

Odlepljivanje znači koliko i provera. LoadLibrary zalepljuje konfiguraciju na samom početku učitavanja, pa bez resetovanja bi odbijanje sposobnosti ostavilo ConfigurePdfLibrary da na svako ponavljanje odgovara s EPdfError „PDFium bibliotečka konfiguracija je već zalepljena“. Put odbijanja prvo zove UnloadLibrary; njegov poziv FPDF_DestroyLibrary je bezbedan u tom trenutku jer PDFium još nije inicijalizovan i vraća se odmah. Drugi neuspesi učitavanja, poput nedostajućeg DLL-a ili nepoklapanja arhitekture, zadržavaju zaptivač, pa petlja ponavljanja mora da ih razlikuje:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // kvalifikovano jedinicom: Windows.LoadLibrary nosi isto ime
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Odbijanje sposobnosti istovaruje DLL i odlepljuje konfiguraciju.
      // DLL koji uopšte nije učitan ostaje zalepljen: ponavljanje ne pomaže
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Primetite eksplicitno PDFium.LoadLibrary. U jedinici koja koristi i Windows ili Winapi.Windows, nekvalifikovano LoadLibrary razrešuje se u jedinicu koja stoji poslednja u uses klauzi; kad je to Win32 funkcija, poziv bez argumenata ne prolazi kompilaciju s greškom o broju argumenata koja ništa ne kaže o PDFium-u

PDFium Component LoadLibrary tok prethodne provere gde ConfigurePdfLibrary zalepljuje konfiguraciju, provera sposobnosti testira FPDF_RenderPageSkia izvoz i skrifa string dokaze, nepodržan zahtev podiže uhvatljiv EPdfError i odlepljuje za ponavljanje, dok DLL koji se nikad ne učita zadržava PdfLibraryConfigurationSealed true
Validacija radi posle što se izvozi vežu i pre inicijalizacije, pa nedostajući bekend pada kao EPdfError koji možete uhvatiti umesto nativnog CHECK-a koji obara proces

Validacija koja se dešava još ranije

ConfigurePdfLibrary odbija neke kombinacije pre nego što se bilo koji DLL uopšte pojavi, sve s EPdfError-om. Eksplicitan FontBackend, uključujući pfbpFreeType, traži Renderer = prpSkia, jer PDFium pita font bekend samo za Skia renderer. IsolatePerDocument traži da je V8Isolate nil, pošto PDFium sam pravi isolate po dokumentu i ne uspeva nativni CHECK ako mu i vi date jednog. Prazni stringovi u UserFontPaths se odbijaju. I svaki poziv posle prvog pokušaja učitavanja pada s „PDFium bibliotečka konfiguracija je već zalepljena“

Poslednje pravilo ima praktičnu posledicu: ne možete prvo sondirati DLL pa ga konfigurisati posle. GetSkiaRenderCapabilities, V8FeaturesAvailable, otvaranje dokumenta i većina ostalih ulaza zovu LoadLibrary interno, što zalepljuje konfiguraciju na licu mesta. Poziv UnloadLibrary-ja kasnije je ni ne otvara. Konfigurišite prvo, pa učitajte, pa postavljajte pitanja, što je tačno red kojem bi dijagnostička rutina trebalo da sledi:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kopija, bezbedno za pregled
  if not PDFium.Loaded then
  begin
    if PdfLibraryConfigurationSealed then
      Exit('PDFium failed to load; configuration is sealed');
    Exit('PDFium not loaded; configuration can still change');
  end;
  // Isto razrešenje koje je LoadLibrary primenio kad je gradio FPDF_LIBRARY_CONFIG
  if PdfNativeRendererType(Config.Renderer,
    GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
    Renderer := 'Skia'
  else
    Renderer := 'AGG';
  Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
    [Renderer, BoolToStr(Config.BrotliEnabled, True),
     BoolToStr(Config.IsolatePerDocument, True)]);
end;

Logovanje te linije jednom pri startu je jeftino, i to je prvo što želite u support tiketu koji kaže „tekst na serveru izgleda drugačije“. PDFium.Loaded je kvalifikovan iz istog razloga kao LoadLibrary: unutar metode forme ili komponente, goli Loaded se veže za TComponent.Loaded

Dva načina da verzionisana C konfiguracijska struktura krene naopako

Svaka verzionisana konfiguracijska struktura, bila to FPDF_LIBRARY_CONFIG, Win32 cbSize zapis ili plugin ABI, pada na dva simetrična načina, i omotač mora da se čuva od obojice. Prvo je popunjavanje polja uz ostavljanje verzije preniskom; drugo je podizanje verzije uz ostavljanje polja na nultoj vrednosti koju biblioteka čita kao namernu odluku

  1. Polje postavljeno, verzija preniska. Upis m_BrotliEnabled = 1 u strukturu verzije 2 i PDFium nikad ne pogleda. Poziv uspeva i Brotli tokovi ostaju nedekodirljivi. Odbrana je izvođenje verzije iz polja koja zaista koristite, što LoadLibrary radi, umesto da se čvrsto ukucava jedno
  2. Verzija dovoljno visoka, nulta vrednost polja nešto znači. Podignite verziju na 6 i svako polje do verzije 6 je sada živo. FillChar nulira m_RendererType u FPDF_RENDERERTYPE_AGG, što je pravi renderer, ne „nepostavljeno“. Odbrana je pisanje svakog polja koje izabrana verzija pokriva s namernom vrednošću, i razrešavanje „podrazumevanog“ protiv stvarnog builda umesto pretpostavke

Treće pravilo sledi za vrednosti koje mogu oboriti pozvanog: validirajte ih protiv onoga što binarni fajl ume pre poziva, s najjačim dokazom koji je dostupan, i budite iskreni u kodu i dokumentaciji kad je taj dokaz heuristika. Izvezeni simbol je dokaz. Ime crate-a u tabeli stringova je dobra pretpostavka

Brzi podsetnik: konfiguracija biblioteke PDFium Component

  • Zovite ConfigurePdfLibrary jednom, pre nego što bilo šta učita DLL; svaki upit sposobnosti ili učitavanje dokumenta ga zalepljuje
  • Nadogradite na v3.123.0 ili kasniju ako postavljate BrotliEnabled ili IsolatePerDocument i očekujete Skia izlaz od uključenih runtime-a
  • Ostavite Renderer na prpDefault osim ako vam treba konkretan rasterizator; sada se razrešuje u podrazumevani build-a na svakoj verziji strukture
  • Koristite PdfNativeRendererType s GetSkiaRenderCapabilities.PageRender da zabeležite koji je renderer zaista aktivan
  • Očekujte EPdfError, ne crash, za prpSkia na samo-AGG DLL-u ili pfbpFontations na ne-Fontations DLL-u u v3.125.0 ili kasnijoj
  • Posle odbijanja sposobnosti, PdfLibraryConfigurationSealed je False i smete se prekonfigurisati; posle neuspelog učitavanja DLL-a ostaje True
  • Tretirajte Fontations detekciju kao heuristiku i držite FreeType rezervu
  • Pišite PDFium.LoadLibrary i PDFium.Loaded s imenom jedinice da izbegnete Win32 i TComponent sudare imena

Ako DLL padne pre nego što konfiguracija uopšte počne da znači, krenite od dijagnostikovanja neuspeha učitavanja PDFium DLL-a u Delphiju, a o tome kako komponenta nalazi pravi binarni fajl na svakoj platformi pogledajte učitavanje PDFium nativne biblioteke na bilo kom cilju. Kad se renderer sredi, render keš i taktike gladkog zumiranja pokriva kako da renderovanje stranica ostane brzo u pregledaču

PDFium Component omotava PDFium engine za Delphi i C++Builder s proverama konfiguracije poput ovih, pa nativna inicijalizacija pada kao Pascal izuzetak koji možete uhvatiti umesto izlaska procesa. Detalji proizvoda i preuzimanja su na stranici proizvoda PDFium Component for Delphi