Tehnični članak

Konfiguracija knjižnice PDFium: ko Brotli tiho zamenja Skia

V komponenti PDFium Component za Delphi je vklop BrotliEnabled ali IsolatePerDocument v TPdfLibraryConfiguration preklopil zbirno gradnjo Skia na izrisovalnik AGG brez vsakršne napake, ker obe možnosti dvigneta FPDF_LIBRARY_CONFIG na različico, pri kateri PDFium bere m_RendererType dobesedno. Od v3.123.0 privzeti izrisovalnik ostane lastna privzetost DLL, od v3.125.0 pa zahteva po Skia ali Fontations, ki je DLL ne more izpolniti, sproži ulovljiv EPdfError namesto da bi ubil proces

Noben od hroščev se ni naznanil. Prvi je izdelal strani, ki so izgledale v redu, le izrisane z drugim rasterizatorjem, s rahlo drugačnim glajenjem robov in robovi črk kot gradnja, ki ste jo razposlali in testirali. Drugi se je naznanil — glasno — tako, da je spravil gostiteljski proces dol od znotraj izvorne inicializacije. Oba prihajata z istega mesta: verzionirana struktura C, katere polja štejejo šele, ko to pove številka različice, in katere ničelne vrednosti niso »nenastavljeno«, ampak prave izbire

Kako FPDF_LIBRARY_CONFIG odloči, kateri izrisovalnik PDFium uporabi?

FPDF_InitLibraryWithConfig posvetuje m_RendererType samo, kadar je polje Version strukture 4 ali višje, od te različice naprej pa uporabi vrednost točno tako, kot je zapisana. Pod različico 4 PDFium polje ignorira in izbere privzetost gradnje, kar je Skia v gradnjah, prevedenih s PDF_USE_SKIA, drugje pa AGG

Vsako poznejše polje sledi istemu vzorcu. Struktura je rasla po eni zmožnosti naenkrat in vsaka zmožnost je prispela skupaj z novo številko različice. Komponenta PDFium Component zgradi izvorno strukturo v LoadLibrary iz vaše TPdfLibraryConfiguration in dvigne različico samo toliko, kot zahtevajo nastavljene možnosti

Različica strukturePolje, ki ga dodaNastavi
2m_pIsolate, m_v8EmbedderSlotVedno zapisano; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform ni nil
4m_RendererTypeRenderer drugačen od prpDefault
5m_FontLibraryTypeFontBackend drugačen od pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Past je v zadnjih dveh vrsticah. Različice so kumulativne: struktura različice 6 je tudi struktura različice 4 in različice 5, zato PDFium bere m_RendererType in m_FontLibraryType, čeprav ste zahtevali samo Brotli. Karkoli sedi v teh dveh poljih v tistem trenutku postane izrisovalnik in zaledje pisav, ne glede na to, ali ste jih nameravali izbrati

PDFium Component lestvica različic FPDF_LIBRARY_CONFIG od različice 2 do različice 7, ki prikazuje, katera možnost TPdfLibraryConfiguration doda m_RendererType, m_FontLibraryType, m_BrotliEnabled in m_IsolatePerDocument, in zakaj kumulativne različice naredijo ničelno polje izrisovalnika namerno izbiro AGG namesto nenastavljene vrednosti na vsaki gradnji
Vsaka možnost dvigne različico strukture in vsako zgodnejše polje ostane živo, zato ničla v m_RendererType prispe k PDFiumu kot izrecna zahteva po AGG

Zakaj je vklop Brotli preklopil izrisovalnik na AGG?

Pred v3.123.0 je komponenta PDFium Component zapisala FPDF_RENDERERTYPE_AGG v m_RendererType za prpDefault, tako da je vsaka konfiguracija, ki je strukturo potisnila na različico 6 ali 7, vsilila AGG na gradnji Skia. Pogona pdfium.dll in pdfium.v8.dll, ki jih dostavlja komponenta, sta gradnji Skia, zato je to zadelo privzeto razporeditev, ne kako eksotično

Preslikava se je ob pisanju zdelo neškodljiva. Pri različici 2 ali 3 polje ni nikoli brano, zato je prpDefault res pomenil »karkoli dela DLL«. Trenutek, ko sta v sliko vstopila BrotliEnabled (različica 6) ali IsolatePerDocument (različica 7), je ista koda spremenila »brez preferenc« v izrecno zahtevo po AGG. Nič ni spodletelo. PDFium se je inicializiral normalno, izrisal vsako stran in vrnil nobeno kodo napake, ker je iz njegovega zornega kota klicatelj zahteval AGG in dobil AGG

Pikseljski hash naredi zamenjavo vidno tam, kjer je posnetki zaslonov ne pokažejo. Izris prve strani istega vzorčnega dokumenta pod tremi konfiguracijami je dal:

  • Privzeta konfiguracija: hash 502D77C3711B4ACF
  • BrotliEnabled = True z Renderer puščenim na prpDefault: hash F75B5EB4728ADE87
  • Izrecen prpAgg: hash F75B5EB4728ADE87, identičen pognanemu Brotli

Popravek v v3.123.0 je javna funkcija PdfNativeRendererType, ki razreši TPdfRendererPreference v vrednost, zapisano v m_RendererType. prpAgg in prpSkia se preslikata ena-v-enega. prpDefault se zdaj preslika v Skia, kadar naloženi DLL izvaža FPDF_RenderPageSkia, sicer pa v AGG. Ta izvoz je preveden pod istim pogojem PDF_USE_SKIA kot sama privzetost Skia, kar ga naredi edino lastnost gradnje, ki jo lahko opazujete od zunaj DLL. Po popravku konfiguracija Brotli izda isti hash kot privzeta

PDFium Component primerjava pikseljskih hashov, ki prikazuje privzeti izris hash Skia 502D77C3711B4ACF, konfiguracijo BrotliEnabled pred v3.123.0, ki se ujema z izrecnim zagonom prpAgg s hashom F75B5EB4728ADE87, in popravljeni ovijalnik, ki razreši prpDefault skozi izvoz FPDF_RenderPageSkia nazaj na izvirni hash Skia
Pikseljski hash ujame, kar posnetki zaslonov skrivajo: vklop Brotli je izrisoval vsako stran z AGG, popravljeni privzeti pa se zdaj ujema z nedotaknjeno konfiguracijo

Zaledje pisav nikoli ni imelo iste težave. m_FontLibraryType se bere od različice 5 naprej, njegova ničelna vrednost, FPDF_FONTBACKENDTYPE_FREETYPE, pa je tudi privzetost PDFiuma, kadar polja sploh ne bere. Zapis FreeType za pfbpDefault torej točno ponovi izvorno privzetost. Ničelne vrednosti niso vedno narobe, samo nikoli niso samodejno prave

Z v3.123.0 ali novejšim koda ob zagonu, ki bi jo naravno napisali, zdaj dela, kar pravi:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Zaženi se pred tem, kar koli naloži izvorno knjižnico
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // dvigne FPDF_LIBRARY_CONFIG na različico 6
  // Renderer ostane prpDefault: razreši se v Skia na gradnjah, ki izvažajo
  // FPDF_RenderPageSkia, in v AGG na gradnjah samo-AGG
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Upoštevajte, da BrotliEnabled tokove /BrotliDecode PDF 2.0 naredi dekodirljive samo, kadar je bil sam DLL zgrajen s PDF_ENABLE_BROTLI. Zastavica je zahteva in na gradnji brez podpore Brotli nima učinka. TPdfLibraryConfiguration.Hardened je isto kot Default, le da je AllowMachineTime False, kar dokumentnemu JavaScriptu blokira branje prave ure; razumna izhodiščna točka za obdelavo nezaupanja vrednih datotek na strani strežnika

Kaj se zgodi, kadar zahtevate zaledje, ki ga DLL ne vsebuje?

PDFium ne vrne napake za izrisovalnik ali zaledje pisav, ki v gradnji manjkata: FPDF_InitLibraryWithConfig spodleti pri izvornem CHECK, ki se na Windows pokaže kot izjema prekinitvene točke in brez upravljavca strukturiranih izjem okoli klica konča proces. Glava tudi sama opozarja, da bo nepodprta vrednost »podobno spodletela z neposrednim sesutjem«

Dva konkretna primera sta gradnja samo-AGG, ki dobi FPDF_RENDERERTYPE_SKIA, in gradnja brez Fontations, ki dobi FPDF_FONTBACKENDTYPE_FONTATIONS. Zbirni pogon Skia je v drugi skupini: izrisuje s Skia, za pisave pa uporablja FreeType. Zahteva prpSkia skupaj s pfbpFontations proti njemu je na strani Delphi izdelala External exception 80000003. Ko jo vnosnik ali upravljavec izjem po naključju ujame, je stanje še vedno neozdravljivo:

  • PDFium ostane pol inicializiran
  • Konfiguracija po vsem procesu je že zapečatena, zato ConfigurePdfLibrary zavrne popravljeno konfiguracijo
  • Ponovni poskus z drugačno konfiguracijo v istem procesu ni več mogoč

To je nasproten spodlet od hrošča Brotli. Tam je polje nosilo vrednost, ki je ni izbral nihče, PDFium pa jo je tiho sprejel. Tu polje nosi vrednost, ki jo je klicatelj izbral namenoma, PDFium pa o njej ne sprejme nikakršne razprave. Oboje so težave, ki jih mora ovijalec rešiti pred izvornim klicem, ker za njim ni več ničesar, kar bi lahko ujel

Kako komponenta PDFium Component vnaprej preveri Skia in Fontations

Od v3.125.0 LoadLibrary preveri konfiguracijo po vezavi izvozov DLL in pred klicem FPDF_InitLibraryWithConfig ter spremeni nepodprtega izrisovalnika ali zaledja pisav v EPdfError s sporočilom, ki poimenuje krivo nastavitev in alternative. DLL se razloži in konfiguracija se odpri, tako da klicatelj lahko izbere druge nastavitve in naloži znova

Odločitev sama živi v čisti funkciji PdfLibraryConfigurationSupportError, ki sprejme konfiguracijo plus dve logični vrednosti, ki opisujeta gradnjo, in vrne prazen niz, kadar je kombinacija varna. Ker se ne dotika nobenega izvornega stanja, jo lahko pokličete iz svojih preskusov s katero koli kombinacijo zmožnosti. Znotraj LoadLibrary prihajata ti logični vrednosti iz različnih vrst dokazov in zaslužita različne stopnje zaupanja:

  • Skia se zazna iz navzočnosti izvoza FPDF_RenderPageSkia, istega signala, ki ga uporablja PdfNativeRendererType. Izvoz in izrisovalnik Skia sta prevedena pod enim pogojem, zato je preizkus točen
  • Fontations nima lastnega izvoza. Edini sled, ki ga pusti, so Rust okvari pisav, ki jih vleče v binarno datoteko, zato komponenta PDFium Component preišče naloženo datoteko knjižnice po imenih okvari skrifa in read-fonts (tudi read_fonts). Pregled teče samo, kadar je zahtevan pfbpFontations, datoteka, ki je ni mogoče brati, pa šteje kot »brez Fontations«

Preizkus Fontations je hevristika in lahko zgreši v eno smer: gradnja Fontations, olupljena vseh teh nizov, bi bila zavrnjena, čeprav bi lahko delovala. Ta izmenjava je bila sklenjena namenoma. Napačna zavrnitev vas stane izjeme, ki jo lahko ujamete, in rezerve na FreeType. Napačno sprejetje vas stane procesa

Odpiranje zapečatke šteje enako kot preizkus. LoadLibrary zapečati konfiguracijo na samem začetku nalaganja, zato bi zavrnitev zmožnosti brez ponastavitve pustila ConfigurePdfLibrary, da vsak ponovni poskus odgovori z EPdfError »PDFium library configuration is already sealed«. Pot zavrnitve najprej pokliče UnloadLibrary; njen klic FPDF_DestroyLibrary je na tej točki varen, ker PDFium še ni bil inicializiran in se vrne takoj. Druge spodletitve nalaganja, kot je manjkajoča DLL ali neskladje arhitektur, zapečatko obdržijo, zato mora zanka ponavljanja vedeti, kako ju ločiti:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // kvalificirano z enoto: Windows.LoadLibrary ima isto ime
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Zavrnitev zmožnosti razloži DLL in odpri konfiguracijo.
      // DLL, ki sploh ni naložil, ostane zapečaten: ponavljanje ne pomaga
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Upoštevajte izrecni PDFium.LoadLibrary. V enoti, ki uporablja tudi Windows ali Winapi.Windows, se nekvalificiran LoadLibrary razreši v enoto, ki se pojavi zadnja v klavzuli uses; kadar je to funkcija Win32, klic brez parametrov ne prevede s napako o številu argumentov, ki o PDFium ne pove nič

PDFium Component potek vnaprejšnjega preverjanja LoadLibrary, kjer ConfigurePdfLibrary zapečati konfiguracijo, preizkus zmožnosti preizkusi izvoz FPDF_RenderPageSkia in dokaze nizov skrifa, nepodprta zahteva sproži ulovljiv EPdfError in se odpri za ponovitev, DLL, ki se sploh ne naloži, pa obdrži PdfLibraryConfigurationSealed true
Preverjanje teče, ko so se izvozi že vezali in pred inicializacijo, zato manjkajoče zaledje spodleti kot EPdfError, ki ga lahko ujamete, namesto izvornega CHECK, ki ubije proces

Preverjanje, ki se zgodi še prej

ConfigurePdfLibrary zavrne nekatere kombinacije, preden je vpletena katera koli DLL, vse z EPdfError. Izrecen FontBackend, vključno s pfbpFreeType, zahteva Renderer = prpSkia, ker PDFium zaledje pisav posvetuje samo za izrisovalnik Skia. IsolatePerDocument zahteva, da je V8Isolate nil, ker PDFium ustvari svoj izolat na dokument in spodleti pri izvornem CHECK, če mu ročno podate še enega. Prazni nizi v UserFontPaths so zavrnjeni. Vsak klic po prvem poskusu nalaganja pa spodleti z »PDFium library configuration is already sealed«

Zadnje pravilo ima praktično posledico: DLL ne morete najprej povohati in ga šele nato nastaviti. GetSkiaRenderCapabilities, V8FeaturesAvailable, odpiranje dokumenta in večina drugih vstopnih točk interni kličejo LoadLibrary, ki konfiguracijo zapečati na mestu. Poznejši klic UnloadLibrary je tudi ne ponovno odpre. Najprej nastavite, nato naložite, šele nato vprašajte — točno ta vrstni red naj sledi diagnostična rutina:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kopija, varna 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 razrešitev, ki jo je LoadLibrary uporabil, ko je zgradil 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;

Ta vrstica v dnevnik enkrat ob zagonu je poceni in to je prva stvar, ki jo želite v podpornem listku, ki pravi »besedilo na strežniku izgleda drugače«. PDFium.Loaded je kvalificiran iz istega razloga kot LoadLibrary: znotraj metode obrazca ali komponente se gol Loaded veže na TComponent.Loaded

Dva načina, kako verzionirana struktura konfiguracije C zgreši

Vsaka verzionirana struktura konfiguracije, naj bo to FPDF_LIBRARY_CONFIG, zapis Win32 cbSize ali ABI vtičnika, spodleti na dva simetrična načina in jih mora ovijalec pokriti oba. Prvi je polnjenje polja, medtem ko različica ostane prenizka; drugi je dvig različice, medtem ko polje ostane na ničelni vrednosti, ki jo knjižnica bere kot namerno izbiro

  1. Polje nastavljeno, različica prenizka. Zapišite m_BrotliEnabled = 1 v strukturo različice 2 in PDFium se nanjo nikoli ne pogleda. Klic uspe in tokovi Brotli ostanejo nedekodirljivi. Obramba je izpeljati različico iz polj, ki so dejansko v rabi, kar dela LoadLibrary, namesto da bi jo trdo kodiral
  2. Različica dovolj visoka, ničelno polje nekaj pomeni. Dvignite različico na 6 in vsako polje do različice 6 je zdaj živo. FillChar ničelira m_RendererType na FPDF_RENDERERTYPE_AGG, kar je pravi izrisovalnik, ne »nenastavljeno«. Obramba je zapisati vsako polje, ki ga izbrana različica pokriva, z namerno vrednostjo in razrešiti »privzeto« proti dejanski gradnji, namesto da bi ga domnevali

Za vrednosti, ki lahko sesujejo klicanega, sledi tretje pravilo: preverite jih proti temu, kar binarna datoteka zmore, pred klicem, z najmočnejšim dokazom, ki je na voljo, in bodite iskreni v kodi in dokumentaciji, kadar je ta dokaz hevristika. Izvožen simbol je dokaz. Ime okvari v tabeli nizov je dober ugib

Hiter pregled: konfiguracija knjižnice PDFium Component

  • Pokličite ConfigurePdfLibrary enkrat, preden kar koli naloži DLL; vsaka poizvedba po zmožnostih ali nalaganje dokumenta jo zapečati
  • Nadgradite na v3.123.0 ali novejši, če nastavljate BrotliEnabled ali IsolatePerDocument in od zbirnih pogonov pričakujete izris Skia
  • Pustite Renderer na prpDefault, razen če potrebujete konkreten rasterizator; zdaj se razreši v privzetost gradnje pri vsaki različici strukture
  • Uporabite PdfNativeRendererType s GetSkiaRenderCapabilities.PageRender, da zapišete v dnevnik, kateri izrisovalnik je dejansko aktiven
  • Pričakujte EPdfError, ne sesutja, za prpSkia na DLL samo-AGG ali pfbpFontations na DLL brez Fontations v v3.125.0 ali novejšem
  • Po zavrnitvi zmožnosti je PdfLibraryConfigurationSealed False in smešte se znova nastaviti; po spodleteli naložitvi DLL ostane True
  • Zaznavo Fontations obravnavajte kot hevristiko in obdržite rezervo na FreeType
  • Zapišite PDFium.LoadLibrary in PDFium.Loaded z imenom enote, da se izognete trkom imen z Win32 in TComponent

Če DLL spodleti, še preden konfiguracija sploh pride poštev, začnite pri diagnosticiranju spodletelih naložitev DLL PDFium v Delphiju, kako komponenta najde pravo binarno datoteko na vsaki platformi pa je opisano v nalaganju izvorne knjižnice PDFium na kateri koli cilj. Ko je izrisovalnik urejen, predpomnilnik izrisa in taktike tekočega povečevanja pokrije, kako ostane izrisovanje strani v pregledovalniku hitro

Komponenta PDFium Component ovije pogon PDFium za Delphi in C++Builder s takšnimi preverjanji konfiguracije, tako da izvorna inicializacija spodleti kot Pascalova izjema, ki jo lahko obravnavate, namesto kot izhod procesa. Podrobnosti o izdelku in prenosi so na strani izdelka PDFium Component za Delphi