Műszaki cikk

PDFium Library Config: amikor a Brotli elcseréli a Skiát

A Delphihez készült PDFium Componentben a TPdfLibraryConfiguration-ban a BrotliEnabled vagy az IsolatePerDocument bekapcsolása hibajelzés nélkül átállította a mellékelt Skia buildet az AGG rendererre, mert mindkét opció olyan verzióra emeli az FPDF_LIBRARY_CONFIG-ot, ahol a PDFium szó szerint olvassa az m_RendererType mezőt. v3.123.0 óta a default renderer a DLL saját defaultja marad, v3.125.0 óta pedig az olyan Skia- vagy Fontations-kérés, amit a DLL nem tud teljesíteni, megfogható EPdfError-t dob ahelyett, hogy kilőné a folyamatot

Egyik hiba sem jelezte magát. Az első olyan oldalakat produkált, amik rendben néztek ki, csak egy másik raszterizáló rajzolta őket, kicsit más élsimítással és szövegélekkel, mint az a build, amit szállítottál és teszteltél. A második viszont jelezte, méghozzá hangosan: a natív inicializálásból lőtte le a gazdafolyamatot. Mindkettő ugyanonnan jön: egy verziózott C struktúra kettős természetéből, aminek a mezői csak attól számítanak, hogy a verziószám azt mondja, számítanak, és aminek a nulla értékei nem a „be nem állított” állapotot jelentik, hanem valódi döntéseket

Hogyan dönti el az FPDF_LIBRARY_CONFIG, melyik renderert használja a PDFium?

Az FPDF_InitLibraryWithConfig csak akkor nézi az m_RendererType-ot, ha a struktúra Version mezője 4 vagy magasabb, és attól a verziótól kezdve pontosan úgy használja az értéket, ahogy íródott. 4 alatti verziónál a PDFium figyelmen kívül hagyja a mezőt, és a build defaultját választja, ami PDF_USE_SKIA-val fordított buildekben Skia, mindenhol másutt AGG

Minden későbbi mező ugyanazt a mintát követi. A struktúra egyszerre egy képességgel nőtt, és minden képesség új verziószámmal érkezett. A PDFium Component a LoadLibrary-ban a te TPdfLibraryConfiguration-odból építi fel a natív struktúrát, és csak addig emeli a verziót, amennyire az általad beállított opciók megkövetelik

StruktúraverzióÚj mezőMi állítja be
2m_pIsolate, m_v8EmbedderSlotMindig írva; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform nem nil
4m_RendererTypeRenderer más, mint prpDefault
5m_FontLibraryTypeFontBackend más, mint pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

A csapda az utolsó két sorban van. A verziók kumulatívak: egy 6-os verziójú struktúra egyben 4-es és 5-es verziójú is, így a PDFium olvassa az m_RendererType-ot és az m_FontLibraryType-ot is, hiába kértél csak Brolit. Ami abban a pillanatban ebben a két mezőben áll, az lesz a renderer és a font backend, akár szándékosan választottad őket, akár nem

PDFium Component FPDF_LIBRARY_CONFIG verziólétra a 2-es verziótól a 7-esig, megmutatva, melyik TPdfLibraryConfiguration opció adja hozzá az m_RendererType, m_FontLibraryType, m_BrotliEnabled és m_IsolatePerDocument mezőket, és miért teszi a kumulatív verzió a kinullázott renderer mezőt szándékos AGG választássá a be nem állított érték helyett bármely builden
Minden opció emeli a struktúra verzióját, és minden korábbi mező életben marad, így az m_RendererType-ban álló nulla explicit AGG kérésként érkezik meg a PDFiumhoz

Miért váltott AGG-re a Brotli bekapcsolása a renderert?

v3.123.0 előtt a PDFium Component prpDefault esetén FPDF_RENDERERTYPE_AGG-t írt az m_RendererType-ba, így minden olyan konfiguráció, ami 6-os vagy 7-es verzióra tolta a struktúrát, AGG-t erőltetett Skia builden. A komponenshez mellékelt pdfium.dll és pdfium.v8.dll runtimeok Skia buildek, tehát ez a default telepítést ütötte, nem valami egzotikusat

A leképezés ártalmatlannak nézett ki, amikor megírták. 2-es vagy 3-as verziónál a mezőt soha nem olvassa senki, tehát a prpDefault tényleg azt jelentette: „amit a DLL csinál”. Abban a pillanatban, ahogy képbe került a BrotliEnabled (6-os verzió) vagy az IsolatePerDocument (7-es verzió), ugyanez a kód a „nincs preferencia” állapotból explicit AGG kérést csinált. Semmi nem hibázott. A PDFium normálisan inicializált, kirajzolta az összes oldalt, és nem adott vissza hibakódot, mert a saját nézőpontjából a hívó AGG-t kért, és AGG-t kapott

Egy pixel hash ott is láthatóvá teszi a cserét, ahol a screenshotok nem. Ugyanannak a mintadokumentumnak az első oldala három konfigurációval kirajzolva ezt adta:

  • Default konfiguráció: hash 502D77C3711B4ACF
  • BrotliEnabled = True, Renderer prpDefault-on hagyva: hash F75B5EB4728ADE87
  • Explicit prpAgg: hash F75B5EB4728ADE87, azonos a Brotli futással

A v3.123.0-beli javítás a publikus PdfNativeRendererType függvény, ami egy TPdfRendererPreference-t felold arra az értékre, ami az m_RendererType-ba íródik. A prpAgg és a prpSkia egy az egyhez képeződik. A prpDefault mostantól akkor Skiára képeződik, ha a betöltött DLL exportálja az FPDF_RenderPageSkia-t, egyébként AGG-re. Ez az export ugyanazon a PDF_USE_SKIA feltétel alatt fordul, mint maga a Skia default, ami a DLL-en kívülről megfigyelhető egyetlen buildtulajdonsággá teszi. A javítás után a Brotli konfiguráció ugyanazt a hash-t adja, mint a default

PDFium Component pixel hash összehasonlítás: a default Skia render hash 502D77C3711B4ACF, a v3.123.0 előtti BrotliEnabled konfiguráció, ami explicit prpAgg futással egyezik F75B5EB4728ADE87 hash-sel, meg a kijavított wrapper, ami az FPDF_RenderPageSkia exporton át a prpDefault-ot visszaoldja az eredeti Skia hashre
A pixel hash megkapja, amit a screenshot elrejt: a Brotli bekapcsolása korábban minden oldalt AGG-vel rajzolt, a kijavított default pedig most az érintetlen konfigurációval egyezik

A font backend soha nem volt ilyen bajban. Az m_FontLibraryType-ot 5-ös verziótól olvassák, és a nulla értéke, az FPDF_FONTBACKENDTYPE_FREETYPE egyben a PDFium defaultja is arra az esetre, amikor a mezőt egyáltalán nem olvassák. A pfbpDefault-hoz FreeType-t írni ezért pontosan a natív defaultot adja vissza. A nulla értékek nem mindig rosszak, csak soha nem automatikusan jók

v3.123.0-val vagy újabbal az indító kód, amit amúgy is természetesen írnál, mostantól azt csinálja, amit mond:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Még azelőtt fusson le, hogy bármi betöltené a natív libraryt
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // 6-os verzióra emeli az FPDF_LIBRARY_CONFIG-ot
  // A Renderer prpDefault marad: Skiára oldódik fel azokon a buildeken,
  // amik exportálják az FPDF_RenderPageSkiát, AGG-re az AGG-only buildeken
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Ne feledd: a BrotliEnabled csak akkor teszi dekódolhatóvá a PDF 2.0 /BrotliDecode streameket, ha maga a DLL PDF_ENABLE_BROTLI-vel készült. A flag egy kérés, és Brotli-támogatás nélküli builden nem csinál semmit. A TPdfLibraryConfiguration.Hardened megegyezik a Default-tal, kivéve, hogy az AllowMachineTime False, ami megakadályozza, hogy a dokumentum JavaScriptje olvassa a valódi órát; megbízhatatlan fájlok szerveroldali feldolgozásához ésszerű kiindulópont

Mi történik, ha olyan backendet kérsz, ami nincs a DLL-ben?

A PDFium nem hibakóddal reagál arra, ha a renderer vagy a font backend hiányzik a buildből: az FPDF_InitLibraryWithConfig megbukik egy natív CHECK-en, ami Windowson breakpoint kivételként tör fel, és ha nincs strukturált kivételkezelő a hívás körül, lezárja a folyamatot. A header pontosan erre figyelmeztet: a nem támogatott érték szerinte ugyanígy, azonnali crashtel hibázik el

A két konkrét eset: egy AGG-only build, ami FPDF_RENDERERTYPE_SKIA-t kap, meg egy Fontations nélküli build, ami FPDF_FONTBACKENDTYPE_FONTATIONS-t kap. A mellékelt Skia runtime a második csoportba tartozik: Skiával renderel, de FreeType-ot használ a fontokhoz. A prpSkia és a pfbpFontations együttes kérése ellene External exception 80000003-ot adott a Delphi oldalon. Ha a debugger vagy egy kivételkezelő épp elkapja, a helyzet így is helyrehozhatatlan:

  • A PDFium félig inicializálva marad
  • A folyamat szintű konfiguráció már le van zárva, így a ConfigurePdfLibrary elutasítja a javított konfigurációt
  • Újrapróbálkozás más konfigurációval ugyanabban a folyamatban többé nem lehetséges

Ez a Brotli hiba ellentéte. Ott a mező egy olyan értéket hordozott, amit senki nem választott, és a PDFium néma csendben elfogadta. Itt a mező egy olyan értéket hordoz, amit a hívó szándékosan választott, és a PDFium róla semmilyen vitát nem fogad el. Mindkettő olyan probléma, amit a wrappernek a natív hívás előtt kell megoldania, mert utána már nincs mit elkapni

Hogyan ellenőrzi előre a PDFium Component a Skiát és a Fontationst?

v3.125.0 óta a LoadLibrary a DLL exportjainak bekötése után, de még az FPDF_InitLibraryWithConfig hívása előtt validálja a konfigurációt, és a nem támogatott renderert vagy font backendet megfogható EPdfError-ré alakítja, olyan üzenettel, ami megnevezi a hibás beállítást és az alternatívákat. A DLL unloadolódik, a konfiguráció lezárása feloldódik, így a hívó választhat más beállításokat, és újra betölthet

Maga a döntés a tiszta PdfLibraryConfigurationSupportError függvényben lakik, ami a konfiguráción felül két, a buildet leíró Boolean-t vesz át, és üres stringet ad vissza, ha a kombináció biztonságos. Mivel natív állapotot nem érint, a saját tesztjeidből bármilyen képességkombinációval meghívhatod. A LoadLibrary-n belül a két Boolean különböző jellegű bizonyítékból jön, és megérdemlik a különböző megbízhatósági fokozatot:

  • Skia: az FPDF_RenderPageSkia export meglétéből detektáljuk, ugyanabból a jelből, amit a PdfNativeRendererType is használ. Az export és a Skia renderer egyetlen feltétel alatt fordul, tehát az ellenőrzés egzakt
  • Fontations: saját exportja nincs. Az egyetlen nyom, amit hagy, az a Rust font crate-ek sorozata, amiket a binárisba húz, ezért a PDFium Component a betöltött library fájlban keresi a skrifa és a read-fonts (illetve read_fonts) crate neveket. A vizsgálat csak akkor fut, ha pfbpFontations-t kérnek, és ha a fájlt nem lehet olvasni, az annak számít, hogy nincs benne Fontations

A Fontations ellenőrzés heurisztika, és csak egy irányba tévedhet: egy Fontations build, amiből mindezeket a stringeket kiszedték, elutasításra kerülne, pedig működhetne. Ez a kompromisszum szándékos volt. Egy téves elutasítás ára egy megfogható kivétel és egy fallback a FreeType-ra. Egy téves elfogadás ára maga a folyamat

A lezárás feloldása ugyanolyan fontos, mint maga az ellenőrzés. A LoadLibrary a betöltés leg elején lezárja a konfigurációt, így reset nélkül egy képesség-elutasítás után a ConfigurePdfLibrary minden újrapróbálkozásra EPdfError-rel válaszolna: „PDFium library configuration is already sealed”. Az elutasítási ág előbb meghívja az UnloadLibrary-t; az FPDF_DestroyLibrary hívása ilyenkor biztonságos, mert a PDFium még nincs inicializálva, és azonnal visszatér. A többi betöltési hiba, például a hiányzó DLL vagy az architektúra-eltérés, megtartja a lezárást, tehát egy retry ciklusnak meg kell tudnia különböztetni a kettőt:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // unit-minősített: a Windows.LoadLibrary ugyanazt a nevet viseli
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // A képesség-elutasítás unloadolja a DLL-t, és feloldja a konfiguráció lezárását.
      // Az egyáltalán fel nem töltött DLL lezárva marad: az újrapróbálkozás nem segít
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Vedd észre az explicit PDFium.LoadLibrary-t. Egy unitban, ami egyben a Windows-t vagy a Winapi.Windows-t is használja, a minősítés nélküli LoadLibrary arra a unitra oldódik fel, ami utoljára szerepel a uses záradékban; ha az a Win32 függvény, a paraméter nélküli hívás argumentumszám-hibával nem fordul le, és ebből a hibából semmi nem árulja el, hogy PDFiumról van szó

PDFium Component LoadLibrary precheck folyamata: a ConfigurePdfLibrary lezárja a konfigurációt, a képességellenőrzés vizsgálja az FPDF_RenderPageSkia exportot és a skrifa string bizonyítékot, a nem támogatott kérés megfogható EPdfError-t dob és feloldja a lezárást az újrapróbálkozáshoz, míg a soha fel nem töltött DLL esetén a PdfLibraryConfigurationSealed true marad
A validáció az exportok bekötése után, de az inicializálás előtt fut, így a hiányzó backend megfogható EPdfError-ként hibázik, nem olyan natív CHECK-ként, ami kilövi a folyamatot

Még korábban lezajló validáció

A ConfigurePdfLibrary bizonyos kombinációkat már azelőtt elutasít, hogy bármely DLL szóba jönne, mindet EPdfError-rel. Egy explicit FontBackend, a pfbpFreeType-ot is beleértve, megköveteli, hogy Renderer = prpSkia legyen, mert a PDFium a font backendet csak a Skia renderernél kérdezi le. Az IsolatePerDocument azt követeli meg, hogy a V8Isolate nil legyen, mert a PDFium dokumentumonként saját isolate-t készít, és ha te is adsz neki egyet, megbukik egy natív CHECK-en. A UserFontPaths-ban az üres stringek elutasításra kerülnek. És minden hívás az első betöltési kísérlet után elbukik a „PDFium library configuration is already sealed” hibával

Ennek az utolsó szabálynak gyakorlati következménye van: nem vizsgálgathatod meg előbb a DLL-t, és konfigurálhatod utána. A GetSkiaRenderCapabilities, a V8FeaturesAvailable, egy dokumentum megnyitása és a legtöbb más belépési pont belül meghívja a LoadLibrary-t, ami a helyszínen lezárja a konfigurációt. A későbbi UnloadLibrary hívás sem nyitja újra. Előbb konfigurálj, aztán tölts be, aztán kérdezz – pontosan ezt a sorrendet kell követnie egy diagnosztikai rutinnak:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // másolat, nyugodtan megnézhető
  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;
  // Ugyanaz a feloldás, amit a LoadLibrary alkalmazott, amikor felépítette az FPDF_LIBRARY_CONFIG-ot
  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;

Ezt a sort indításkor egyszer naplózni olcsó, és ez az első, amit egy „a szöveg másképp néz ki a szerveren” tartalmú support jegyben szeretnél látni. A PDFium.Loaded ugyanabból az okból minősített, mint a LoadLibrary: form vagy komponens metódusán belül a csupasz Loaded a TComponent.Loaded-hoz kötődik

Két mód, ahogy egy verziózott C konfigurációs struktúra elromlik

Minden verziózott konfigurációs struktúra, legyen az FPDF_LIBRARY_CONFIG, egy Win32 cbSize rekord vagy egy plugin ABI, két szimmetrikus módon hibázik, és a wrappernek mindkettő ellen védekeznie kell. Az első: mezőt töltesz meg, a verziót viszont alacsonyan hagyod; a második: verziót emelsz, egy mezőt viszont nulla értéken hagysz, amit a library szándékos választásként olvas

  1. Mező beállítva, verzió túl alacsony. Írj m_BrotliEnabled = 1-et egy 2-es verziójú struktúrába, és a PDFium soha nem nézi meg. A hívás sikerül, és a Brotli streamek dekódolhatatlanok maradnak. A védekezés: a verziót a ténylegesen használt mezőkből származtasd – pontosan ezt teszi a LoadLibrary –, ne beégetett értékként
  2. Verzió elég magas, a nulla mező jelent valamit. Emeld a verziót 6-ra, és máris él minden mező a 6-os verzióig. A FillChar az m_RendererType-ot FPDF_RENDERERTYPE_AGG-ra nullázza, ami egy valódi renderer, nem a „be nem állított” állapot. A védekezés: a választott verzió által lefedett minden mezőt szándékos értékkel írj, és a „default”-ot a tényleges build alapján oldd fel, ne feltételezésből

Harmadik szabály az olyan értékekre, amik kilőhetik a hívott felet: a hívás előtt validáld őket arra, amit a bináris ténylegesen tud, a legerősebb elérhető bizonyítékot használva, és légy őszinte a kódban és a dokumentációban, amikor az a bizonyíték csak heurisztika. Egy exportált szimbólum bizonyíték. Egy crate név a string táblában jó találgatás

Gyorsreferencia: PDFium Component library konfiguráció

  • Hívd meg a ConfigurePdfLibrary-t egyszer, még mielőtt bármi betöltené a DLL-t; bármilyen képesség-lekérdezés vagy dokumentumbetöltés lezárja
  • Frissíts v3.123.0-ra vagy újabbra, ha beállítod a BrotliEnabled-t vagy az IsolatePerDocument-et, és Skia kimenetet vársz a mellékelt runtimeoktól
  • Hagyd a Renderer-t prpDefault-on, hacsak nem kell konkrét raszterizáló; mostantól minden struktúraverziónál a build defaultjára oldódik fel
  • A PdfNativeRendererType-ot használd GetSkiaRenderCapabilities.PageRender-rel arra, hogy naplózd, melyik renderer fut ténylegesen
  • v3.125.0-tól EPdfError-t várj, nem crasht, ha prpSkia-t kérsz AGG-only DLL-től, vagy pfbpFontations-t nem Fontations DLL-től
  • Egy képesség-elutasítás után a PdfLibraryConfigurationSealed False, és újrakonfigurálhatsz; sikertelen DLL-betöltés után True marad
  • A Fontations detektálást kezeld heurisztikaként, és tartogass FreeType fallbacket
  • A PDFium.LoadLibrary-t és a PDFium.Loaded-t unit névvel írd, hogy elkerüld a Win32-vel és a TComponent-tel való névütközést

Ha a DLL már azelőtt hibázik, hogy a konfigurációnak egyáltalán jelentése lenne, kezdd a Delphi-s PDFium DLL betöltési hibák diagnózisával, arról pedig, hogyan találja meg a komponens a megfelelő binárist platformonként, a PDFium natív library betöltése bármely targeten című írás szól. Ha a renderer már el van döntve, a render cache és smooth zoom taktikák mutatja meg, hogyan maradjon gyors az oldalrenderelés egy viewerben

A PDFium Component a PDFium engineet csomagolja Delphihez és C++Builderhez az ilyen konfiguráció-ellenőrzésekkel, így a natív inicializálás nem folyamatkilépéssel hibázik, hanem egy olyan Pascal kivétellel, amit kezelni tudsz. Termékrészletek és letöltések a PDFium Component for Delphi terméklapon