Tehnički članak

PDFium konfiguracija biblioteke: Brotli tiho mijenja Skiu

U PDFium Componentu za Delphi, uključivanje BrotliEnabled ili IsolatePerDocument u TPdfLibraryConfiguration prebacivalo je priloženi Skia build na AGG renderer bez ikakve greške, jer obje opcije podižu FPDF_LIBRARY_CONFIG na verziju na kojoj PDFium m_RendererType čita doslovno. Od v3.123.0 zadani renderer ostaje vlastiti zadani DLL-a, a od v3.125.0 Skia ili Fontations zahtjev koji DLL ne može ispuniti podiže uhvatljiv EPdfError umjesto da ubije proces

Nijedan bug nije najavio sebe. Prvi je proizvodio stranice koje su izgledale u redu, samo renderirane drugim rasterizatorom, s malo drugačijim anti-aliasingom i rubovima teksta od builda koji ste isporučili i testirali. Drugi se jest najavio, glasno, obaranjem host procesa iznutra iz nativne inicijalizacije. Oba dolaze s istog mjesta: verzionirana C struktura čija polja računaju se tek kad broj verzije kaže da se računaju, i čije su nulte vrijednosti ne "nepostavljeno" nego pravi izbori

Kako FPDF_LIBRARY_CONFIG odlučuje koji renderer PDFium koristi?

FPDF_InitLibraryWithConfig konzultira m_RendererType samo kad je polje Version strukture 4 ili više, i od te verzije koristi vrijednost točno onakvom kakva je zapisana. Ispod verzije 4 PDFium polje ignorira i bira zadani build, koji je Skia u buildovima kompajliranim s PDF_USE_SKIA i AGG svugdje drugdje

Svako kasnije polje slijedi isti obrazac. Struktura je rasla jednom sposobnošću odjednom, i svaka je sposobnost stigla zajedno s novim brojem verzije. PDFium Component gradi nativnu strukturu u LoadLibrary iz vašeg TPdfLibraryConfiguration i podiže verziju samo toliko koliko opcije koje ste postavili zahtijevaju

Verzija strukturePolje koje dodajePostavlja se kad
2m_pIsolate, m_v8EmbedderSlotUvijek zapisano; 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 zadnja dva retka. Verzije su kumulativne: struktura verzije 6 ujedno je struktura verzije 4 i verzije 5, pa PDFium čita m_RendererType i m_FontLibraryType iako ste tražili samo Brotli. Što god se u tom trenutku nalazilo u ta dva polja postaje renderer i font backend, bilo da ste ih namjeravali odabrati ili ne

PDFium Component FPDF_LIBRARY_CONFIG ljestev verzija od verzije 2 do verzije 7 pokazuje koja TPdfLibraryConfiguration opcija dodaje m_RendererType, m_FontLibraryType, m_BrotliEnabled i m_IsolatePerDocument, i zašto kumulativne verzije čine nulto polje renderera namjernim AGG izborom, a ne nepostavljenu vrijednost na bilo kojem buildu
Svaka opcija podiže verziju strukture i svako ranije polje ostaje živo, pa nula u m_RendererType stiže do PDFiuma kao izričit AGG zahtjev

Zašto je uključivanje Brotlija prebacilo renderer na AGG?

Prije v3.123.0 PDFium Component pisao je FPDF_RENDERERTYPE_AGG u m_RendererType za prpDefault, pa je svaka konfiguracija koja je strukturu gurnula na verziju 6 ili 7 forsirala AGG na Skia buildu. pdfium.dll i pdfium.v8.dll runtimei koji idu uz komponentu Skia su buildovi, pa je ovo pogodilo zadano uvođenje, ne neki egzotični

Mapiranje je izgledalo bezopasno kad je napisano. Na verziji 2 ili 3 polje se nikad ne čita, pa je prpDefault stvarno značilo "što god DLL radi". U trenutku kad su BrotliEnabled (verzija 6) ili IsolatePerDocument (verzija 7) ušli u sliku, isti je kod "bez preferencije" pretvorio u izričit AGG zahtjev. Ništa nije palo. PDFium se normalno inicijalizirao, renderirao svaku stranicu i vratio bez koda greške, jer je iz njegove točke gledišta pozivatelj tražio AGG i dobio AGG

Pikselni hash čini zamjenu vidljivom tamo gdje slike zaslona ne. Renderiranje prve stranice istog uzorčnog dokumenta pod tri konfiguracije dalo je:

  • Zadana konfiguracija: hash 502D77C3711B4ACF
  • BrotliEnabled = True uz Renderer ostavljen na prpDefault: hash F75B5EB4728ADE87
  • Izričit prpAgg: hash F75B5EB4728ADE87, identičan Brotli pokretanju

Popravak u v3.123.0 jest javna funkcija PdfNativeRendererType, koja razrješuje TPdfRendererPreference u vrijednost zapisanu u m_RendererType. prpAgg i prpSkia mapiraju se jedan-na-jedan. prpDefault sada mapira u Skiu kad učitani DLL izvozi FPDF_RenderPageSkia i u AGG inače. Taj se izvoz kompajlira pod istim PDF_USE_SKIA uvjetom kao i sama Skia zadana postavka, što ga čini jedinim svojstvom builda koje možete promatrati izvana iz DLL-a. Nakon popravka Brotli konfiguracija proizvodi isti hash kao zadana

PDFium Component usporedba pikselnih hashova pokazuje zadani Skia render hash 502D77C3711B4ACF, konfiguraciju BrotliEnabled prije v3.123.0 koja se poklapa s izričitim prpAgg pokretanjem s hashom F75B5EB4728ADE87, i popravljeni wrapper koji razrješuje prpDefault kroz FPDF_RenderPageSkia izvoz natrag na izvorni Skia hash
Pikselni hash hvata što slike zaslona kriju: uključivanje Brotlija renderiralo je svaku stranicu s AGG-om, a popravljeni zadani sada se poklapa s nedirnutom konfiguracijom

Font backend nikad nije imao isti problem. m_FontLibraryType čita se od verzije 5, a njegova nulta vrijednost, FPDF_FONTBACKENDTYPE_FREETYPE, istodobno je PDFiumov zadani kad se polje uopće ne čita. Pisanje FreeTypea za pfbpDefault zato reproducira nativni zadani točno. Nulte vrijednosti nisu uvijek krive, samo nikad nisu automatski točne

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

uses
  PDFium;

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

Upamtite da BrotliEnabled tokove /BrotliDecode PDF 2.0 čini dekodabilnima samo kad je i sam DLL građen s PDF_ENABLE_BROTLI. Zastavica je zahtjev, i na buildu bez Brotli podrške nema učinka. TPdfLibraryConfiguration.Hardened isto je što i Default, osim što je AllowMachineTime False, što JavaScriptu dokumenta blokira čitanje pravog sata; razumna je polazna točka za obradu nepovjerljivih datoteka na strani servera

Što se dogodi kad zatražite backend koji DLL ne sadrži?

PDFium ne vraća grešku za renderer ili font backend kojeg nema u buildu: FPDF_InitLibraryWithConfig provali nativni CHECK, koji se na Windowsu pokazuje kao iznimka breakpointa i, bez structured exception handlera oko poziva, terminira proces. Header to i kaže, upozoravajući da nepodržana vrijednost "slično pada s trenutačnim padom"

Dva konkretna slučaja jesu AGG-only build koji dobije FPDF_RENDERERTYPE_SKIA, i build bez Fontationsa koji dobije FPDF_FONTBACKENDTYPE_FONTATIONS. Priloženi Skia runtime u je drugoj grupi: renderira s Skiom, ali za fontove koristi FreeType. Traženje prpSkia zajedno s pfbpFontations protiv njega proizvelo je External exception 80000003 na Delphi strani. Kad to slučajno uhvati debugger ili exception handler, situacija je i dalje nepopravljiva:

  • PDFium ostaje napola inicijaliziran
  • Konfiguracija na razini procesa već je zaključana, pa ConfigurePdfLibrary odbija ispravljenu konfiguraciju
  • Ponavljanje s drugom konfiguracijom u istom procesu više nije moguće

Ovo je suprotni pad od Brotli buga. Tamo je polje držalo vrijednost koju nitko nije odabrao i PDFium ju je tiho prihvatio. Ovdje polje drži vrijednost koju je pozivatelj svjesno odabrao i PDFium o njoj uopće ne prihvaća raspravu. Oba su problemi koje wrapper mora riješiti prije nativnog poziva, jer poslije njega nema više ničega za uhvatiti

Kako PDFium Component provjerava unaprijed Skiu i Fontations

Od v3.125.0 LoadLibrary validira konfiguraciju nakon vezanja DLL izvoza i prije poziva FPDF_InitLibraryWithConfig, i nepodržani renderer ili font backend pretvara u EPdfError s porukom koja imenuje problematičnu postavku i alternative. DLL se istovaruje i konfiguracija se otključava, pa pozivatelj može odabrati druge postavke i učitati ponovno

Odluka sama živi u čistoj funkciji PdfLibraryConfigurationSupportError, koja prima konfiguraciju plus dva Booleana koji opisuju build i vraća prazan string kad je kombinacija sigurna. Budući da ne dira nikakvo nativno stanje, možete je zvati iz vlastitih testova s bilo kojom kombinacijom sposobnosti. Unutar LoadLibrary dva Booleana dolaze iz raznih vrsta dokaza, i zaslužuju razne razine povjerenja:

  • Skia se otkriva iz prisutnosti izvoza FPDF_RenderPageSkia, istog signala kojeg koristi PdfNativeRendererType. Izvoz i Skia renderer kompajliraju se pod jednim uvjetom, pa je provjera točna
  • Fontations nema vlastiti izvoz. Jedini trag koji ostavlja jesu Rust font crateovi koje vuče u binarnu datoteku, pa PDFium Component pretražuje učitanu bibliotečku datoteku za imenima crateova skrifa i read-fonts (također read_fonts). Pretraga radi samo kad se zatraži pfbpFontations, a datoteka koja se ne može čitati računa se kao "nema Fontationsa"

Fontations provjera jest heuristika i može pogriješiti u jednom smjeru: Fontations build ogoljen od svakog od tih stringova bio bi odbijen iako je mogao raditi. Taj kompromis namjerno je skrojen. Lažna odbijenica košta vas iznimku koju možete uhvatiti i fallback na FreeType. Lažno prihvaćanje košta vas proces

Otključavanje bitno je kao i provjera. LoadLibrary zaključava konfiguraciju na samom početku učitavanja, pa bez resetiranja bi odbijenica sposobnosti ostavila ConfigurePdfLibrary da na svako ponavljanje odgovara s EPdfError "PDFium library configuration is already sealed". Put odbijenice prvo zove UnloadLibrary; njegov poziv FPDF_DestroyLibrary u tom je trenutku siguran jer PDFium još nije inicijaliziran i vraća se odmah. Ostali neuspjesi učitavanja, poput nedostajućeg DLL-a ili nepoklapanja arhitekture, zadržavaju zaključanost, pa retry petlja mora razlikovati dvoje:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // kvalificirano jedinicom: Windows.LoadLibrary isto se zove
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Odbijenica sposobnosti istovaruje DLL i otključava konfiguraciju.
      // DLL koji uopće nije učitan ostaje zaključan: ponavljanje ne pomaže
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Obratite pozornost na izričit PDFium.LoadLibrary. U jedinici koja također koristi Windows ili Winapi.Windows, nekvalificirani LoadLibrary razrješuje se na jedinicu koja stoji zadnja u uses klauzuli; kad je to Win32 funkcija, poziv bez parametara ne prolazi kompajliranje s greškom broja argumenata koja o PDFiumu ne govori ništa

PDFium Component LoadLibrary tok provjere unaprijed u kojem ConfigurePdfLibrary zaključava konfiguraciju, provjera sposobnosti testira FPDF_RenderPageSkia izvoz i skrifa string dokaze, nepodržani zahtjev podiže uhvatljiv EPdfError i otključava za ponavljanje, dok DLL koji nikad ne učita drži PdfLibraryConfigurationSealed true
Validacija radi nakon što se izvozi vežu i prije inicijalizacije, pa nedostajući backend pada kao EPdfError koji možete uhvatiti umjesto nativnog CHECK-a koji ubija proces

Validacija koja se događa još ranije

ConfigurePdfLibrary odbija neke kombinacije prije nego bilo koji DLL uopće sudjeluje, sve s EPdfError. Izričit FontBackend, uključujući pfbpFreeType, zahtijeva Renderer = prpSkia, jer PDFium font backend konzultira samo za Skia renderer. IsolatePerDocument zahtijeva da V8Isolate bude nil, jer PDFium stvara vlastiti isolate po dokumentu i provali nativni CHECK ako mu i sami uručite jednog. Prazni stringovi u UserFontPaths odbijaju se. I svaki poziv nakon prvog pokušaja učitavanja pada s "PDFium library configuration is already sealed"

To zadnje pravilo ima praktičnu posljedicu: ne možete prvo sondirati DLL, a konfigurirati ga poslije. GetSkiaRenderCapabilities, V8FeaturesAvailable, otvaranje dokumenta i većina drugih ulaznih točaka interno zove LoadLibrary, koji konfiguraciju zaključava na licu mjesta. Kasnije zvanje UnloadLibrary je također ne otvara. Prvo konfigurirajte, zatim učitajte, pa postavljajte pitanja, što je točno red kojega bi se dijagnostička rutina trebala držati:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kopija, sigurno 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;
  // Ista razlučivost koju je LoadLibrary primijenio 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;

Logirati taj redak jednom pri pokretanju jeftino je, i to je prvo što želite u support tiketu koji kaže "tekst izgleda drugačije na serveru". PDFium.Loaded kvalificiran je iz istog razloga kao LoadLibrary: unutar metode forme ili komponente, goli Loaded veže se na TComponent.Loaded

Dva načina na koja verzionirana C konfiguracijska struktura krene krivo

Svaka verzionirana konfiguracijska struktura, bilo da je FPDF_LIBRARY_CONFIG, Win32 cbSize zapis ili plugin ABI, pada na dva simetrična načina, i wrapper se mora čuvati oba. Prvo je punjenje polja uz ostavljanje verzije preniskom; drugo je podizanje verzije uz ostavljanje polja na nultoj vrijednosti koju biblioteka čita kao svjesni izbor

  1. Polje postavljeno, verzija preniska. Upiredite m_BrotliEnabled = 1 u strukturu verzije 2 i PDFium nikad na njega ne pogleda. Poziv uspije i Brotli tokovi ostaju nedekodabilni. Obrana je izvoditi verziju iz polja koja su stvarno u upotrebi, što radi LoadLibrary, umjesto da se hard-coda
  2. Verzija dovoljno visoka, nulto polje nešto znači. Podignite verziju na 6 i svako polje do verzije 6 sada je živo. FillChar nulira m_RendererType u FPDF_RENDERERTYPE_AGG, što je pravi renderer, ne "nepostavljeno". Obrana je upisati svako polje koje odabrana verzija pokriva namjernom vrijednošću, i razrješiti "zadano" protiv stvarnog builda umjesto pretpostaviti ga

Treće pravilo slijedi za vrijednosti koje mogu srušiti pozvanoga: validirajte ih protiv onoga što binarna datoteka može prije poziva, koristeći najjači dostupni dokaz, i budite iskreni u kodu i dokumentaciji kad je taj dokaz heuristika. Izvezeni simbol dokaz je. Ime cratea u string tablici dobra je pretpostavka

Brza referenca: konfiguracija biblioteke PDFium Componenta

  • Zovite ConfigurePdfLibrary jednom, prije nego bilo što učita DLL; svaki upit o sposobnostima ili učitavanje dokumenta zaključava je
  • Nadogradite na v3.123.0 ili novije ako postavljate BrotliEnabled ili IsolatePerDocument i od priloženih runtimea očekujete Skia izlaz
  • Ostavite Renderer na prpDefault osim ako trebate konkretan rasterizator; sada se na svakoj verziji strukture razrješuje u zadani build
  • Koristite PdfNativeRendererType s GetSkiaRenderCapabilities.PageRender da logirate koji je renderer stvarno aktivan
  • Očekujte EPdfError, ne pad, za prpSkia na AGG-only DLL-u ili pfbpFontations na DLL-u bez Fontationsa u v3.125.0 ili novijem
  • Nakon odbijenice sposobnosti, PdfLibraryConfigurationSealed je False i smijete rekonfigurirati; nakon neuspjelog učitavanja DLL-a ostaje True
  • Detekciju Fontationsa tretirajte kao heuristiku i držite FreeType fallback
  • Pišite PDFium.LoadLibrary i PDFium.Loaded s imenom jedinice da izbjegnete sudare imena s Win32 i TComponent

Ako DLL padne prije nego konfiguracija uopće postane bitna, počnite s dijagnostikom neuspjeha učitavanja PDFium DLL-a u Delphiju, a za to kako komponenta na svakoj platformi nalazi pravu binarnu datoteku pogledajte učitavanje PDFium nativne biblioteke na bilo kojem cilju. Kad se renderer namjesti, taktike render cachea i glatkog zuma pokriva kako u pregledniku zadržati brzo renderiranje stranica

PDFium Component omotava PDFium motor za Delphi i C++Builder provjerama konfiguracije poput ovih, pa nativna inicijalizacija pada kao Pascal iznimka koju možete uhvatiti umjesto izlaza iz procesa. Detalji o proizvodu i preuzimanja na PDFium Component for Delphi stranici proizvoda