Odborný článok

Konfigurácia knižnice PDFium: keď Brotli poticho vymení Skia

V PDFium Component pre Delphi zapnutie BrotliEnabled alebo IsolatePerDocument v TPdfLibraryConfiguration kedysi preplo dodávané Skia zostavenie na AGG renderer bez akejkoľvek chyby, lebo obe voľby zdvihnú FPDF_LIBRARY_CONFIG na verziu, pri ktorej PDFium číta m_RendererType doslovne. Od v3.123.0 zostáva predvolený renderer DLL vlastným predvoleným a od v3.125.0 vyvolá žiadosť o renderer Skia alebo Fontations, ktorú DLL nedokáže splniť, chytiteľnú EPdfError namiesto zabitia procesu

Ani jeden bug sa neohlásil. Prvý vyrábal strany, ktoré vyzerali dobre, len vykreslené iným rasterizérom, s mierne odlišným anti-aliasingom a okrajmi textu než zostavenie, ktoré ste expedovali a testovali. Druhý sa ohlásil, a to nahlas, tým, že zrazil proces hostiteľa zvnútra natívnej inicializácie. Oba prichádzajú z rovnakého miesta: verziovaná C štruktúra, ktorej polia sa počítajú, až keď o tom rozhodne číslo verzie, a ktorej nulové hodnoty nie sú „nenastavené“, ale skutočné voľby

Ako FPDF_LIBRARY_CONFIG rozhoduje, ktorý renderer PDFium použije?

FPDF_InitLibraryWithConfig zohľadňuje m_RendererType len vtedy, keď pole Version štruktúry je 4 alebo vyššie, a od tej verzie používa hodnotu presne tak, ako je zapísaná. Pod verziou 4 pole PDFium ignoruje a vyberie predvolenú zostavenia, ktorou je Skia v zostaveniach kompilovaných s PDF_USE_SKIA a AGG všade inde

Každé neskoršie pole nasleduje ten istý vzor. Štruktúra rástla po jednej schopnosti a každá schopnosť prišla spolu s novým číslom verzie. PDFium Component stavia natívnu štruktúru v LoadLibrary z vašej TPdfLibraryConfiguration a zdvíha verziu len tak ďaleko, ako vyžadujú nastavené voľby

Verzia štruktúryPole, ktoré pridávaNastavuje sa
2m_pIsolate, m_v8EmbedderSlotVždy zapísané; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform nie nil
4m_RendererTypeRenderer iné než prpDefault
5m_FontLibraryTypeFontBackend iné než pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Pasca je v posledných dvoch riadkoch. Verzie sú kumulatívne: štruktúra verzie 6 je aj štruktúra verzie 4 a verzie 5, takže PDFium číta m_RendererType aj m_FontLibraryType, aj keď ste si žiadali len Brotli. Čokoľvek v tom okamihu sedí v týchto dvoch poliach sa stane rendererom a font backendom, či ste ich chceli zvoliť, alebo nie

Verziový rebrík FPDF_LIBRARY_CONFIG v PDFium Component od verzie 2 po verziu 7 ukazujúci, ktorá voľba TPdfLibraryConfiguration pridáva m_RendererType, m_FontLibraryType, m_BrotliEnabled a m_IsolatePerDocument a prečo kumulatívne verzie robia z vynulovaného poľa rendereru zámernú voľbu AGG, nie nenastavenú hodnotu na ľubovoľnom zostavení
Každá voľba zdvíha verziu štruktúry a každé skoršie pole ostáva živé, takže nula v m_RendererType dorazí do PDFia ako explicitná žiadosť o AGG

Prečo zapnutie Brotli preplo renderer na AGG?

Pred v3.123.0 zapisoval PDFium Component FPDF_RENDERERTYPE_AGG do m_RendererType pre prpDefault, takže každá konfigurácia, ktorá pretlačila štruktúru na verziu 6 alebo 7, vynútila AGG na Skia zostavení. Runtimy pdfium.dll a pdfium.v8.dll, ktoré sú súčasťou komponentu, sú Skia zostavenia, takže to zasiahlo predvolené nasadenie, nie nejaké exotické

Mapovanie vyzeralo neškodne, keď ho niekto napísal. Pri verzii 2 alebo 3 sa pole nikdy nečíta, takže prpDefault naozaj znamenalo „čokoľvek, čo DLL robí“. V momente, keď do hry vstúpilo BrotliEnabled (verzia 6) alebo IsolatePerDocument (verzia 7), ten istý kód zmenil „bez preferencie“ na explicitnú žiadosť o AGG. Nič nezlyhalo. PDFium sa inicializovalo normálne, vykreslilo každú stranu a nevrátilo žiadny chybový kód, lebo z jeho pohľadu volajúci o AGG požiadal a AGG dostal

Pixel hash spraví výmenu viditeľnou tam, kde snímky obrazovky nie. Vyrenderovanie prvej strany toho istého vzorového dokumentu pod tromi konfiguráciami dalo:

  • Predvolená konfigurácia: hash 502D77C3711B4ACF
  • BrotliEnabled = True s Renderer ponechaným na prpDefault: hash F75B5EB4728ADE87
  • Explicitný prpAgg: hash F75B5EB4728ADE87, identický s behom Brotli

Oprava vo v3.123.0 je verejná funkcia PdfNativeRendererType, ktorá rozlíši TPdfRendererPreference na hodnotu zapísanú do m_RendererType. prpAgg a prpSkia sa mapujú jedna na jednu. prpDefault sa teraz mapuje na Skia, keď načítané DLL exportuje FPDF_RenderPageSkia, a na AGG inak. Tento export sa kompiluje pod tou istou podmienkou PDF_USE_SKIA ako samotná Skia predvolená, čo z neho robí jedinú vlastnosť zostavenia, ktorú možno pozorovať zvonka z DLL. Po oprave dáva konfigurácia Brotli ten istý hash ako predvolená

Porovnanie pixel hash v PDFium Component ukazujúce hash vykreslenia Skia 502D77C3711B4ACF pre predvolené, konfiguráciu BrotliEnabled z čias pred v3.123.0 sediacu s explicitným behom prpAgg s hashom F75B5EB4728ADE87 a opravený wrapper rozlišujúci prpDefault cez export FPDF_RenderPageSkia späť na pôvodný hash Skia
Pixel hash odchytí, čo snímky obrazovky skrývajú: zapnutie Brotli kedysi vykreslilo každú stranu cez AGG a opravená predvolená sa teraz zhoduje s nedotknutou konfiguráciou

Font backend nikdy nemal ten istý problém. m_FontLibraryType sa číta od verzie 5 a jeho nulová hodnota, FPDF_FONTBACKENDTYPE_FREETYPE, je zároveň predvolenou PDFia, keď sa pole vôbec nečíta. Zapísanie FreeType pre pfbpDefault preto reprodukuje natívnu predvolenú presne. Nulové hodnoty nie sú vždy zlé, len nikdy nie sú automaticky správne

S v3.123.0 a novšou robí štartovací kód, ktorý by ste prirodzene napísali, presne to, čo hlási:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Musí bežať skôr, než čokoľvek načíta natívnu knižnicu
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // zdvíha FPDF_LIBRARY_CONFIG na verziu 6
  // Renderer ostáva prpDefault: rozlíši sa na Skia na zostaveniach, ktoré exportujú
  // FPDF_RenderPageSkia a na AGG na zostaveniach len pre AGG
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Pamätajte, že BrotliEnabled spraví dekódovateľnými streamy /BrotliDecode PDF 2.0 len vtedy, keď bolo samotné DLL zostavené s PDF_ENABLE_BROTLI. Príznak je žiadosť a na zostavení bez podpory Brotli nemá žiadny účinok. TPdfLibraryConfiguration.Hardened je to isté ako Default, lenže AllowMachineTime je False, čo zablokuje dokumentovému JavaScriptu čítanie skutočných hodín; je to rozumný štartovací bod pre server-side spracovanie nedôveryhodných súborov

Čo sa stane, keď si vyžiadate backend, ktorý DLL neobsahuje?

PDFium nevracia chybu pre renderer alebo font backend, ktorý zostavenie neobsahuje: FPDF_InitLibraryWithConfig failne natívny CHECK, ktorý sa na Windowse prejaví ako výnimka breakpointu a bez štruktúrovaného handlera výnimiek okolo volania ukončí proces. Hlavička to hovorí priamo, varuje, že nepodporovaná hodnota „zlyhá podobne okamžitým pádom“

Dva konkrétne prípady sú zostavenie len pre AGG, ktoré dostane FPDF_RENDERERTYPE_SKIA, a zostavenie bez Fontations, ktoré dostane FPDF_FONTBACKENDTYPE_FONTATIONS. Dodávaný Skia runtime je v druhej skupine: vykresľuje cez Skia, ale písma berie z FreeType. Žiadosť prpSkia spolu s pfbpFontations proti nemu vyprodukovala na strane Delphi External exception 80000003. Keď to ladiaci nástroj alebo handler výnimiek náhodou chytí, situácia je stále nezáchraná:

  • PDFium ostane napoly inicializované
  • Konfigurácia celého procesu je už zapečatená, takže ConfigurePdfLibrary odmietne opravenú konfiguráciu
  • Opakovanie s inou konfiguráciou v tom istom procese už nie je možné

Toto je zlyhanie opačné k bugu Brotli. Tam pole držalo hodnotu, ktorú si nikto nezvolil, a PDFium ju poticho prijalo. Tu pole drží hodnotu, ktorú volajúci zvolil zámerne, a PDFium o nej neprijme žiadnu diskusiu. Obe sú problémy, ktoré musí wrapper vyriešiť pred natívnym volaním, lebo po ňom nezostáva nič na chytanie

Ako PDFium Component vopred preveruje Skia a Fontations

Od v3.125.0 validuje LoadLibrary konfiguráciu po nadviazaní exportov DLL a pred volaním FPDF_InitLibraryWithConfig a mení nepodporovaný renderer alebo font backend na EPdfError so správou, ktorá menuje problematické nastavenie a alternatívy. DLL sa vyloží a konfigurácia sa odpečiatkuje, takže volajúci si môže zvoliť iné nastavenia a načítať znova

Samotné rozhodnutie býva v čistej funkcii PdfLibraryConfigurationSupportError, ktorá berie konfiguráciu plus dva Booleany popisujúce zostavenie a vracia prázdny reťazec, keď je kombinácia bezpečná. Keďže sa nedotýka žiadneho natívneho stavu, môžete ju volať z vlastných testov s ľubovoľnou kombináciou schopností. Vo vnútri LoadLibrary pochádzajú tie dva Booleany z rôznych druhov dôkazov a zasluhujú si rôznu mieru dôvery:

  • Skia sa rozpozná z prítomnosti exportu FPDF_RenderPageSkia, toho istého signálu, ktorý používa PdfNativeRendererType. Export aj Skia renderer sa kompilujú pod jednou podmienkou, takže kontrola je presná
  • Fontations nemá vlastný export. Jediná stopa, ktorú zanecháva, sú Rust font crates, ktoré vtiahne do binárky, takže PDFium Component prehľadá načítaný súbor knižnice po menách crate skrifa a read-fonts (tiež read_fonts). Sken beží len vtedy, keď je vyžiadané pfbpFontations a súbor, ktorý sa nedá prečítať, sa počíta ako „žiadne Fontations“

Kontrola Fontations je heuristika a môže sa mýliť jedným smerom: zostavenie Fontations zbavené každého z tých reťazcov by bolo odmietnuté, hoci by mohlo fungovať. Ten kompromis bol urobený zámerne. Falošné odmietnutie vás stojí výnimku, ktorú môžete chytiť, a fallback na FreeType. Falošné prijatie vás stojí proces

Odpečiatkovanie má rovnaký význam ako kontrola. LoadLibrary pečiatkuje konfiguráciu hneď na začiatku načítavania, takže bez resetu by odmietnutie schopnosti nechalo ConfigurePdfLibrary odpovedať na každý pokus EPdfError „konfigurácia knižnice PDFium už je zapečatená“. Cesta odmietnutia volá najprv UnloadLibrary; jej volanie FPDF_DestroyLibrary je v tom okamihu bezpečné, lebo PDFium ešte nebolo inicializované a vracia sa okamžite. Iné zlyhania načítania, ako chýbajúce DLL alebo nezhoda architektúry, pečiatku držia, takže slučka opakovaní musí obe odlíšiť:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // s kvalifikátorom jednotky: Windows.LoadLibrary má to isté meno
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Odmietnutie schopnosti vyloží DLL a odpečiatkuje konfiguráciu.
      // DLL, ktoré sa nepodarilo vôbec načítať, ostáva zapečatené: opakovanie nepomôže
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Všimnite si explicitné PDFium.LoadLibrary. V jednotke, ktorá tiež používa Windows alebo Winapi.Windows, sa nekvalifikované LoadLibrary rozlíši na jednotku, ktorá v uses zozname prichádza posledná; keď je to Win32 funkcia, volanie bez parametrov sa nepreloží s chybou počtu argumentov, ktorá o PDFiu nehovorí nič

Tok preverovania LoadLibrary v PDFium Component, kde ConfigurePdfLibrary pečiatkuje konfiguráciu, kontrola schopností testuje export FPDF_RenderPageSkia a dôkaz reťazca skrifa, nepodporovaná žiadosť vyvolá chytiteľnú EPdfError a odpečiatkuje na opakovanie, zatiaľ čo DLL, ktoré sa nikdy nenačíta, drží PdfLibraryConfigurationSealed true
Validácia beží po nadviazaní exportov a pred inicializáciou, takže chýbajúci backend zlyhá ako EPdfError, ktorý môžete chytiť, namiesto natívneho CHECK, ktorý zabije proces

Validácia, ktorá prichádza ešte skôr

ConfigurePdfLibrary odmieta niektoré kombinácie skôr, než sa vôbec zapojí nejaké DLL, všetky cez EPdfError. Explicitný FontBackend, vrátane pfbpFreeType, vyžaduje Renderer = prpSkia, lebo PDFium zohľadňuje font backend len pri rendereri Skia. IsolatePerDocument vyžaduje, aby V8Isolate bolo nil, keďže PDFium si vytvára vlastný isolate na dokument a failne natívny CHECK, ak mu jeden podáte navyše. Prázdne reťazce v UserFontPaths sa odmietajú. A každé volanie po prvom pokuse o načítanie zlyhá s „konfigurácia knižnice PDFium už je zapečatená“

To posledné pravidlo má praktický dôsledok: DLL nemožno najprv preskúmať a nastaviť ho potom. GetSkiaRenderCapabilities, V8FeaturesAvailable, otvorenie dokumentu a väčšina ďalších vstupných bodov volá LoadLibrary interne, čo pečiatkuje konfiguráciu hneď na mieste. Neskoršie volanie UnloadLibrary ju tiež znovu neotvorí. Najprv nakonfigurujte, potom načítajte, potom sa pýtajte, presne v tomto poradí, aké má nasledovať diagnostická rutina:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kópia, bezpečná na nahliadnutie
  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;
  // To isté rozlíšenie, ktoré aplikovalo LoadLibrary pri stavbe 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;

Zalogovať ten riadok raz pri štarte je lacné a je to prvá vec, ktorú chcete v support lístku hovoriacom „na serveri vyzerá text inak“. PDFium.Loaded je s kvalifikátorom z rovnakého dôvodu ako LoadLibrary: vo vnútri metódy formulára alebo komponentu sa holé Loaded zviaže na TComponent.Loaded

Dva spôsoby, akými sa verziovaná C konfiguračná štruktúra pokazí

Každá verziovaná konfiguračná štruktúra, či už je to FPDF_LIBRARY_CONFIG, Win32 záznam cbSize alebo plugin ABI, zlyháva dvomi symetrickými spôsobmi a wrapper sa musí brániť obojím. Prvý je naplnenie poľa pri ponechaní verzie priveľa nízkej; druhý je zdvihnutie verzie pri ponechaní poľa na nulovej hodnote, ktorú knižnica číta ako zámernú voľbu

  1. Pole nastavené, verzia priveľa nízka. Zápis m_BrotliEnabled = 1 do štruktúry verzie 2 a PDFium sa naň nikdy nepozrie. Volanie uspeje a streamy Brotli ostanú nedekódovateľné. Obranou je odvodiť verziu z polí skutočne používaných, čo robí LoadLibrary, namiesto natvrdo zapísanej jednej
  2. Verzia dosť vysoká, nulové pole znamená niečo. Zdvihnite verziu na 6 a každé pole po verziu 6 je teraz živé. FillChar vynuluje m_RendererType na FPDF_RENDERERTYPE_AGG, čo je skutočný renderer, nie „nenastavené“. Obranou je zapísať každé pole, ktoré zvolená verzia pokrýva, zámernou hodnotou a rozlíšiť „predvolené“ proti skutočnému zostaveniu, namiesto predpokladu

Tretie pravidlo nasleduje pre hodnoty, ktoré môžu zraziť volaného: validujte ich voči tomu, čo binárka dokáže, pred volaním, s najsilnejším dostupným dôkazom, a buďte poctiví v kóde aj v dokumentácii, keď je ten dôkaz heuristikou. Exportovaný symbol je dôkaz. Meno crate v tabuľke reťazcov je dobrý tip

Rýchly prehľad: konfigurácia knižnice PDFium Component

  • Zavolajte ConfigurePdfLibrary raz, skôr než čokoľvek načíta DLL; každý dopyt po schopnostiach alebo načítanie dokumentu ju pečiatkuje
  • Prejdite na v3.123.0 a novšiu, ak nastavujete BrotliEnabled alebo IsolatePerDocument a očakávate výstup Skia z dodávaných runtime
  • Nechajte Renderer na prpDefault, pokiaľ nepotrebujete konkrétny rasterizér; teraz sa rozlíši na predvolenú zostavenia pri každej verzii štruktúry
  • Použite PdfNativeRendererType s GetSkiaRenderCapabilities.PageRender na zalogovanie, ktorý renderer je skutočne aktívny
  • Čakajte EPdfError, nie pád, pre prpSkia na DLL len pre AGG alebo pfbpFontations na DLL bez Fontations vo v3.125.0 a novšej
  • Po odmietnutí schopnosti je PdfLibraryConfigurationSealed False a môžete prekonfigurovať; po zlyhaní načítania DLL ostáva True
  • Berite rozpoznávanie Fontations ako heuristiku a majte fallback na FreeType
  • Píšte PDFium.LoadLibrary a PDFium.Loaded s menom jednotky, aby ste sa vyhli kolízií mien s Win32 a TComponent

Ak DLL zlyhá skôr, než vôbec začne konfigurácia niečo znamenať, začnite s diagnostikou zlyhaní načítania PDFium DLL v Delphi a to, ako komponent nájde správnu binárku na každej platforme, popisuje načítanie natívnej knižnice PDFium na ľubovoľný cieľ. Keď je renderer vyriešený, taktiky render cache a plynulého zoomu popisujú, ako udržať vykresľovanie strán v prehliadači rýchle

PDFium Component obaluje engine PDFium pre Delphi a C++Builder s kontrolami konfigurácie ako tieto, takže natívna inicializácia zlyhá ako Pascal výnimka, ktorú môžete obslúžiť, a nie ako koniec procesu. Detaily o produkte a stiahnutie sú na stránke produktu PDFium Component pre Delphi