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 |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Mindig írva; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform nem nil |
| 4 | m_RendererType | Renderer más, mint prpDefault |
| 5 | m_FontLibraryType | FontBackend más, mint pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = 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
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,RendererprpDefault-on hagyva: hashF75B5EB4728ADE87- Explicit
prpAgg: hashF75B5EB4728ADE87, 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
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
ConfigurePdfLibraryelutasí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_RenderPageSkiaexport meglétéből detektáljuk, ugyanabból a jelből, amit aPdfNativeRendererTypeis 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 aread-fonts(illetveread_fonts) crate neveket. A vizsgálat csak akkor fut, hapfbpFontations-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ó
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
- 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 aLoadLibrary–, ne beégetett értékként - 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
FillCharazm_RendererType-otFPDF_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 azIsolatePerDocument-et, és Skia kimenetet vársz a mellékelt runtimeoktól - Hagyd a
Renderer-tprpDefault-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áldGetSkiaRenderCapabilities.PageRender-rel arra, hogy naplózd, melyik renderer fut ténylegesen - v3.125.0-tól
EPdfError-t várj, nem crasht, haprpSkia-t kérsz AGG-only DLL-től, vagypfbpFontations-t nem Fontations DLL-től - Egy képesség-elutasítás után a
PdfLibraryConfigurationSealedFalse, é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 aPDFium.Loaded-t unit névvel írd, hogy elkerüld a Win32-vel és aTComponent-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