In PDFium Component for Delphi, turning on BrotliEnabled or IsolatePerDocument in TPdfLibraryConfiguration used to switch the bundled Skia build to the AGG renderer without any error, because both options raise FPDF_LIBRARY_CONFIG to a version where PDFium reads m_RendererType literally. Since v3.123.0 the default renderer stays the DLL's own default, and since v3.125.0 a Skia or Fontations request the DLL cannot honor raises a catchable EPdfError instead of killing the process
Neither bug announced itself. The first one produced pages that looked fine, just rendered by a different rasterizer, with slightly different anti-aliasing and text edges than the build you shipped and tested. The second one did announce itself, loudly, by taking the host process down from inside native initialization. Both come from the same place: a versioned C structure whose fields only count once the version number says they do, and whose zero values are not "unset" but real choices
How does FPDF_LIBRARY_CONFIG decide which renderer PDFium uses?
FPDF_InitLibraryWithConfig consults m_RendererType only when the structure's Version field is 4 or higher, and from that version on it uses the value exactly as written. Below version 4 PDFium ignores the field and picks the build default, which is Skia in builds compiled with PDF_USE_SKIA and AGG everywhere else
Each later field follows the same pattern. The structure grew one capability at a time, and each capability arrived together with a new version number. PDFium Component builds the native structure in LoadLibrary from your TPdfLibraryConfiguration and raises the version only as far as the options you set require
| Structure version | Field it adds | Set by |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Always written; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform not nil |
| 4 | m_RendererType | Renderer other than prpDefault |
| 5 | m_FontLibraryType | FontBackend other than pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
The trap is in the last two rows. Versions are cumulative: a version 6 structure is also a version 4 and a version 5 structure, so PDFium reads m_RendererType and m_FontLibraryType even though you only asked for Brotli. Whatever sits in those two fields at that moment becomes the renderer and the font backend, whether you meant to choose them or not
Why did enabling Brotli switch the renderer to AGG?
Before v3.123.0, PDFium Component wrote FPDF_RENDERERTYPE_AGG into m_RendererType for prpDefault, so any configuration that pushed the structure to version 6 or 7 forced AGG on a Skia build. The pdfium.dll and pdfium.v8.dll runtimes that ship with the component are Skia builds, so this hit the default deployment, not some exotic one
The mapping looked harmless when it was written. At version 2 or 3 the field is never read, so prpDefault really did mean "whatever the DLL does". The moment BrotliEnabled (version 6) or IsolatePerDocument (version 7) entered the picture, the same code turned "no preference" into an explicit AGG request. Nothing failed. PDFium initialized normally, rendered every page, and returned no error code, because from its point of view the caller had asked for AGG and received AGG
A pixel hash makes the swap visible where screenshots do not. Rendering the first page of the same sample document under three configurations gave:
- Default configuration: hash
502D77C3711B4ACF BrotliEnabled= True withRendererleft atprpDefault: hashF75B5EB4728ADE87- Explicit
prpAgg: hashF75B5EB4728ADE87, identical to the Brotli run
The fix in v3.123.0 is the public function PdfNativeRendererType, which resolves a TPdfRendererPreference to the value written into m_RendererType. prpAgg and prpSkia map one to one. prpDefault now maps to Skia when the loaded DLL exports FPDF_RenderPageSkia and to AGG otherwise. That export is compiled under the same PDF_USE_SKIA condition as the Skia default itself, which makes it the one build property you can observe from outside the DLL. After the fix the Brotli configuration produces the same hash as the default one
The font backend never had the same problem. m_FontLibraryType is read from version 5 on, and its zero value, FPDF_FONTBACKENDTYPE_FREETYPE, is also PDFium's default when the field is not read at all. Writing FreeType for pfbpDefault therefore reproduces the native default exactly. Zero values are not always wrong, they are just never automatically right
With v3.123.0 or later, the startup code you would naturally write now does what it says:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Must run before anything loads the native library
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // raises FPDF_LIBRARY_CONFIG to version 6
// Renderer stays prpDefault: resolved to Skia on builds that export
// FPDF_RenderPageSkia and to AGG on AGG-only builds
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Remember that BrotliEnabled only makes PDF 2.0 /BrotliDecode streams decodable when the DLL itself was built with PDF_ENABLE_BROTLI. The flag is a request, and on a build without Brotli support it has no effect. TPdfLibraryConfiguration.Hardened is the same as Default except that AllowMachineTime is False, which blocks document JavaScript from reading the real clock; it is a reasonable starting point for server-side processing of untrusted files
What happens when you request a backend the DLL does not contain?
PDFium does not return an error for a renderer or font backend that is missing from the build: FPDF_InitLibraryWithConfig fails a native CHECK, which on Windows surfaces as a breakpoint exception and, without a structured exception handler around the call, terminates the process. The header says as much, warning that an unsupported value "will similarly fail with an immediate crash"
The two concrete cases are an AGG-only build that receives FPDF_RENDERERTYPE_SKIA, and a build without Fontations that receives FPDF_FONTBACKENDTYPE_FONTATIONS. The bundled Skia runtime is in the second group: it renders with Skia but uses FreeType for fonts. Requesting prpSkia together with pfbpFontations against it produced External exception 80000003 on the Delphi side. When the debugger or an exception handler happens to catch that, the situation is still unrecoverable:
- PDFium is left half-initialized
- The process-wide configuration is already sealed, so
ConfigurePdfLibraryrefuses a corrected configuration - Retrying with a different configuration in the same process is no longer possible
This is the opposite failure to the Brotli bug. There the field held a value nobody chose and PDFium silently accepted it. Here the field holds a value the caller chose deliberately and PDFium accepts no discussion about it at all. Both are problems that a wrapper has to solve before the native call, because after it there is nothing left to catch
How PDFium Component prechecks Skia and Fontations
Since v3.125.0, LoadLibrary validates the configuration after binding the DLL exports and before calling FPDF_InitLibraryWithConfig, and turns an unsupported renderer or font backend into an EPdfError with a message that names the offending setting and the alternatives. The DLL is unloaded and the configuration is unsealed, so the caller can pick other settings and load again
The decision itself lives in the pure function PdfLibraryConfigurationSupportError, which takes the configuration plus two Booleans describing the build and returns an empty string when the combination is safe. Because it touches no native state, you can call it from your own tests with any capability combination. Inside LoadLibrary the two Booleans come from different kinds of evidence, and they deserve different levels of trust:
- Skia is detected from the presence of the
FPDF_RenderPageSkiaexport, the same signalPdfNativeRendererTypeuses. The export and the Skia renderer are compiled under one condition, so the check is exact - Fontations has no export of its own. The only trace it leaves is the Rust font crates it pulls into the binary, so PDFium Component scans the loaded library file for the crate names
skrifaandread-fonts(alsoread_fonts). The scan runs only whenpfbpFontationsis requested, and a file that cannot be read counts as "no Fontations"
The Fontations check is a heuristic, and it can be wrong in one direction: a Fontations build stripped of every one of those strings would be rejected even though it could have worked. That trade was made on purpose. A false rejection costs you an exception you can catch and a fallback to FreeType. A false acceptance costs you the process
Unsealing matters as much as the check. LoadLibrary seals the configuration at the very start of loading, so without the reset a capability rejection would leave ConfigurePdfLibrary answering every retry with EPdfError "PDFium library configuration is already sealed". The rejection path calls UnloadLibrary first; its FPDF_DestroyLibrary call is safe at that point because PDFium has not been initialized yet and returns immediately. Other load failures, such as a missing DLL or an architecture mismatch, keep the seal, so a retry loop has to tell the two apart:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // unit-qualified: Windows.LoadLibrary has the same name
Result := prpSkia;
except
on E: EPdfError do
begin
// A capability rejection unloads the DLL and unseals the configuration.
// A DLL that failed to load at all stays sealed: retrying cannot help
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Note the explicit PDFium.LoadLibrary. In a unit that also uses Windows or Winapi.Windows, an unqualified LoadLibrary resolves to whichever unit appears last in the uses clause; when that is the Win32 function, the parameterless call fails to compile with an argument-count error that says nothing about PDFium
Validation that happens even earlier
ConfigurePdfLibrary rejects some combinations before any DLL is involved, all with EPdfError. An explicit FontBackend, including pfbpFreeType, requires Renderer = prpSkia, because PDFium only consults the font backend for the Skia renderer. IsolatePerDocument requires V8Isolate to be nil, since PDFium creates its own isolate per document and fails a native CHECK if you also hand it one. Empty strings in UserFontPaths are rejected. And any call after the first load attempt fails with "PDFium library configuration is already sealed"
That last rule has a practical consequence: you cannot probe the DLL first and configure it afterwards. GetSkiaRenderCapabilities, V8FeaturesAvailable, opening a document, and most other entry points call LoadLibrary internally, which seals the configuration on the spot. Calling UnloadLibrary later does not reopen it either. Configure first, then load, then ask questions, which is exactly the order a diagnostic routine should follow:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // a copy, safe to inspect
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;
// Same resolution LoadLibrary applied when it built 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;
Logging that line once at startup is cheap, and it is the first thing you want in a support ticket that says "the text looks different on the server". PDFium.Loaded is qualified for the same reason as LoadLibrary: inside a form or component method, a bare Loaded binds to TComponent.Loaded
Two ways a versioned C configuration struct goes wrong
Every versioned configuration structure, whether it is FPDF_LIBRARY_CONFIG, a Win32 cbSize record, or a plugin ABI, fails in two symmetric ways, and a wrapper has to guard against both. The first is filling a field while leaving the version too low; the second is raising the version while leaving a field at a zero value that the library reads as a deliberate choice
- Field set, version too low. Write
m_BrotliEnabled= 1 into a version 2 structure and PDFium never looks at it. The call succeeds and Brotli streams stay undecodable. The defense is to derive the version from the fields actually in use, which is whatLoadLibrarydoes, rather than hard-coding one - Version high enough, zero field means something. Raise the version to 6 and every field up to version 6 is now live.
FillCharzeroesm_RendererTypetoFPDF_RENDERERTYPE_AGG, which is a real renderer, not "unset". The defense is to write every field the chosen version covers with an intentional value, and to resolve "default" against the actual build instead of assuming it
A third rule follows for values that can crash the callee: validate them against what the binary can do before the call, using the strongest evidence available, and be honest in the code and the documentation when that evidence is a heuristic. An exported symbol is proof. A crate name in a string table is a good guess
Quick reference: PDFium Component library configuration
- Call
ConfigurePdfLibraryonce, before anything loads the DLL; any capability query or document load seals it - Upgrade to v3.123.0 or later if you set
BrotliEnabledorIsolatePerDocumentand expect Skia output from the bundled runtimes - Leave
RendereratprpDefaultunless you need a specific rasterizer; it now resolves to the build default at every structure version - Use
PdfNativeRendererTypewithGetSkiaRenderCapabilities.PageRenderto log which renderer is actually active - Expect
EPdfError, not a crash, forprpSkiaon an AGG-only DLL orpfbpFontationson a non-Fontations DLL in v3.125.0 or later - After a capability rejection,
PdfLibraryConfigurationSealedis False and you may reconfigure; after a failed DLL load it stays True - Treat Fontations detection as heuristic and keep a FreeType fallback
- Write
PDFium.LoadLibraryandPDFium.Loadedwith the unit name to avoid Win32 andTComponentname clashes
If the DLL fails before configuration even matters, start with diagnosing PDFium DLL load failures in Delphi, and for how the component finds the right binary on each platform see loading the PDFium native library on any target. Once the renderer is settled, render cache and smooth zoom tactics covers how to keep page rendering fast in a viewer
PDFium Component wraps the PDFium engine for Delphi and C++Builder with configuration checks like these, so native initialization fails as a Pascal exception you can handle rather than a process exit. Product details and downloads are on the PDFium Component for Delphi product page