PDFium Component lets a Delphi application decide which font bytes are used when a PDF references a font it does not embed. ConfigureSystemFontProvider installs an IPdfSystemFontProvider implementation that receives every font mapping request PDFium makes, complete with face name, weight, italic flag, charset and pitch family, and answers with the TrueType, TrueType Collection or OpenType bytes to use
This exists because non-embedded fonts are a rendering lottery. A PDF that names Arial and embeds nothing renders with Arial on a workstation, with a metric-compatible substitute on a Linux server, and with whatever the host mapper finds on a locked-down container image. The same invoice looks different on each, line breaks move, and a customer receives a document that does not match the archived copy
Why not just install the fonts on the server?
Sometimes that is the answer, and when it is, take it. But it fails in three common situations. Licensing may forbid installing a font on a server for automated rendering. Container images are rebuilt frequently and a font installed by hand disappears with the next deployment. And regulated workflows need the rendering stack to be reproducible from artefacts under version control, which a machine-wide font installation is not
A provider addresses all three by moving the decision into your application. Fonts ship as resources you control, the mapping policy is code you can review, and the same binary renders identically everywhere because nothing depends on what happens to be installed
Installing a provider
Configuration must happen before the library is loaded. PDFium accepts a system font info structure at initialisation and keeps handles it hands out afterwards, so swapping a provider while documents are open would invalidate font handles PDFium still holds; the component rejects that outright rather than letting it corrupt a render:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// Deterministic mapping: face name plus weight and italic decide
// which file we ship for this request
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // complete sfnt or TTC bytes
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // index inside a collection
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // host decides everything
Policy.AllowFaceSubstitution := False; // reject a different face name
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// Only now load the library and open documents
end;
Teardown runs in the opposite order: the provider is detached from PDFium first, then the library is unloaded. Skipping the detach leaves native font handles pointing at Pascal objects that are about to be freed, which is the classic shutdown access violation in code that mixes reference-counted interfaces with a C library
What the policy flags actually decide
AllowDefaultFallback is the switch between two operating modes. With it off, a request the provider declines simply fails, which is what you want while proving that every font in a corpus is accounted for: any gap becomes visible immediately instead of being papered over. With it on, unresolved requests are delegated to the mapper returned by FPDF_GetDefaultSystemFontInfo, while the outside world still sees one uniform handle wrapper, with face name, charset, table data and font deletion routed correctly by origin
AllowFaceSubstitution governs whether a provider may answer with a different face name than the one requested. Turning it off makes substitution an explicit decision rather than an accident, which matters when a document names a font whose metrics differ enough to change pagination
The component validates every provider response before it reaches PDFium: empty data is rejected, oversized fonts are rejected against MaxFontBytes, the TTC index is checked, and individual sfnt tables are served from the font directory when PDFium asks for a table rather than for the whole file. That last capability means a provider can hand over a complete font file and let the component answer table-level queries, instead of exposing raw Pascal objects across the C ABI
Caching without dangling font data
Font mapping requests repeat constantly during rendering, so responses are cached with a key covering every font selection parameter, evicted by bounded least-recently-used order. The subtlety is lifetime: PDFium may still be reading the bytes of a font whose cache entry has just been evicted
The cache stores reference-counted dynamic arrays and each native handle holds its own snapshot, so eviction drops a reference rather than freeing memory in use. The delete callback releases the handle and maintains an active count. Practically, this means MaxCacheEntries can be tuned for memory without any risk of pulling data out from under an in-flight render
Is the provider called on my thread?
No, not necessarily. PDFium may call the mapper from its own worker threads, so an implementation must be thread safe. Shared counters, the cache and configuration observation are each protected inside the component by their own critical section, but the code inside ResolveFont is yours to make safe
The safest shape is a provider that touches no mutable shared state: read from a table built at startup, load bytes from a file or a resource, return. If a lookup needs a shared cache of your own, guard it. And keep exceptions inside your implementation, since a Pascal exception must never unwind through the PDFium stack; the component catches at the C ABI boundary and converts to a failure or an optional default fallback, but relying on that as normal control flow costs performance and hides bugs. Threading rules for the rest of the component follow the same principles as those in render lock discipline
Proving the mapping in production
Statistics turn font substitution from guesswork into something you can assert on. GetSystemFontProviderStatistics reports whether a provider is configured and installed, how many mapping requests were made, and how they were satisfied, split into cache hits, provider hits and default fallback hits, along with rejected responses, failed requests, live handles and cached fonts:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// In a conformance run with fallback disabled, any fallback hit or
// failed request means a document referenced a font we do not ship
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
A rising RejectedProviderResponses count is the signal that a provider is answering with data the policy refuses, usually an oversized file or a substituted face, and it is worth alerting on because those requests silently degrade to fallback or failure. For diagnosing which fonts a document actually needs before you build the mapping table, the inspection route in analysing PDF font properties lists embedded and non-embedded fonts per document
Font provisioning, rendering and text extraction share the same library instance across Delphi, C++Builder and Lazarus; deployment details are described on the PDFium Component for Delphi page