Two invoices rendered from the same template with the same TrueType face, differing only in the customer's surname, leave two separate files in a font subset cache. The surname adds a capital Q and a lowercase z that the first invoice never used, so the set of kept glyphs changes, and with it the subset and the cache key. A persistent subset cache pays for itself once the glyph repertoire of your documents repeats, and the counters it exposes tell you within a single batch whether yours does
Turning on the disk cache in HotPDF
HotPDF keeps the subset cache off until you give it a folder. Two properties on THotPDF control it: FontSubsetCacheFolder names the directory, and FontSubsetCacheMaxBytes caps how much it may hold, by default 268435456 bytes (256 MiB). The cache is active only while the folder is non-empty and the budget sits above zero. Assigning the folder creates the directory if it is missing and immediately trims whatever is already there down to the budget
Subsetting is worth caching because it is real work. Building a TrueType subset means walking the composite-glyph closure, rewriting loca and glyf, rebuilding the tables that index glyphs, and producing a CID mapping the PDF can address. For a single document the cost disappears into the noise. In a report server that renders thousands of statements in a loop with the same three faces, the same subsets get built again and again, and that is the CPU the cache hands back
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// ASCII-only path: the property is an AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB instead of the 256 MiB default
// ... BeginDoc / draw / EndDoc for each document in the batch ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt is your own logging routine
LogFmt('subset cache: %d hits, %d misses, %d bytes in %d files',
[Info.HitCount, Info.MissCount, Info.CurrentBytes, Info.FileCount]);
finally
Pdf.Free;
end;
end;
The cache sits behind both subsetting paths: faces subset through EnableFontSubsetting, which covers TrueType and OpenType-CFF fonts, and the face registered with RegisterUnicodeTTF. Nothing else in your drawing code changes. The folder path is declared as AnsiString, so a directory name carrying characters outside the system code page is asking for trouble; a plain ASCII path under ProgramData or a service data directory sidesteps the question entirely
What goes into a cache key
Four inputs decide whether a stored subset can be reused: the exact bytes of the source font, the set of glyphs the document keeps, the subset mode, and the cache format version. Each key is built from a SHA-256 hash of the font bytes and a SHA-256 hash of a bitmap of used glyph IDs, prefixed with v1- and suffixed with -s for a sparse subset or -c for a compact one. Entries land on disk as <key>.hpdfsubset, so a directory listing tells you at a glance which mode your batch is producing
Hashing the font content instead of its file name has a practical consequence. Upgrading Arial or swapping in a newer build of a corporate face produces new keys automatically, and the old entries simply stop being hit until eviction removes them. You do not have to clear the cache after a font update to get correct output. Clearing merely reclaims the space sooner
The subset mode is the input people forget. CompactFontSubsetting renumbers the kept glyphs into a dense range and writes an explicit /CIDToGIDMap (ISO 32000-1 §9.7.4.2), and the registered face always gets a compact subset when PDFACompliance is set. CompressDocument also turns compact subsets on for as long as the document is open. So a service that writes the same statement once as plain PDF and once as PDF/A fills two sets of entries for identical glyphs, one ending in -s and one in -c. OpenType-CFF faces always take the sparse path
The glyph set is where the hit rate is decided. Statements from a fixed template, forms whose variable data is mostly digits, catalogues drawn from one product database: these reuse the same glyphs document after document. A workload where every document pulls a different slice of a CJK face with tens of thousands of glyphs mostly misses, because almost every document's bitmap is new. Run one representative batch and read the counters before deciding which kind of workload you have
How do you know the cache is doing anything?
GetFontSubsetCacheInfo returns a THPDFFontSubsetCacheInfo record with nine fields, and HitCount against MissCount answers the question directly. WriteCount and EvictionCount show whether entries survive long enough to be reused or are pushed out by a budget set too tight. CurrentBytes and FileCount describe the folder as it stands right now
Two details matter when you wire these into monitoring. The seven event counters belong to the THotPDF instance and start at zero when the component is created, so a service that creates one component per document sees a single document's worth of activity per reading. CurrentBytes and FileCount, by contrast, come from enumerating the folder at the moment you call, which makes them reflect every process using that folder — and makes each call cost a directory scan as well. Read the record once per batch, at the end, and keep the per-document loop clear of it
The remaining three counters are the ones worth an alert. CorruptCount counts entries that failed validation on load and were deleted; each one also counts as a miss. A handful after a power cut is expected, and a steady trickle points at the storage. RejectedCount counts subsets the cache refused to store, typically because a single subset is larger than the whole budget. WriteFailureCount counts writes that never completed, which in practice almost always means a folder the service account cannot write to. None of the three interrupts document generation, so a cache with a write permission problem produces exactly the same PDFs as no cache at all. It just never gets any faster
// SubsetCacheNeedsAttention is a helper you add yourself;
// the thresholds are examples, tune them to your batch size
function SubsetCacheNeedsAttention(const Info: THPDFFontSubsetCacheInfo;
out Reason: string): Boolean;
begin
Reason := '';
if Info.WriteFailureCount > 0 then
Reason := 'cache folder is not writable for this account'
else if Info.RejectedCount > Info.WriteCount then
Reason := 'subsets exceed FontSubsetCacheMaxBytes, raise the budget'
else if (Info.EvictionCount > Info.WriteCount div 2) and (Info.HitCount < Info.MissCount) then
Reason := 'entries are evicted before they are reused'
else if Info.CorruptCount > 10 then
Reason := 'repeated damaged entries, check the storage';
Result := Reason <> '';
end;
Budgets, eviction, and the switch that empties the folder
Eviction is least-recently-used by file modification time. Every successful load touches the entry's timestamp, every store is followed by a trim, and the oldest files go first until the folder fits the budget again. Lowering FontSubsetCacheMaxBytes at runtime trims on the spot instead of waiting for the next write, so a service reacting to a disk-space alert can shrink the cache the moment it decides to
That immediacy has a sharp edge. Setting the budget to zero while a folder is configured trims to zero, which deletes every .hpdfsubset file in it. Setting FontSubsetCacheFolder to an empty string, on the other hand, turns the cache off and leaves the files exactly where they are. Both switches disable caching for the next document; only one of them keeps a week's worth of warm entries. For troubleshooting, empty the folder property and leave the budget alone
// Pause caching for a diagnostic run, keep every stored entry
Pdf.FontSubsetCacheFolder := '';
// Danger: with a folder set, a zero budget trims the folder to nothing
// Pdf.FontSubsetCacheMaxBytes := 0;
// Deliberate reset: deletes *.hpdfsubset and leftover temp files,
// the event counters keep their values
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
A diagnostic run with the cache paused is the cleanest way to rule it out. The six-letter subset tag in /BaseFont is derived from the kept glyph set, so the same glyphs produce the same tag with or without the cache, and the embedded font programs ought to match. Whole-file hashes will still differ because of creation dates and the document ID, so compare the font streams, not the files
What happens to a damaged or half-written entry?
A damaged entry is deleted and the subset is built normally, before anything from that entry reaches a PDF stream. On load HotPDF checks the file signature, the format version, the subset mode flag, the glyph count against the source font, the declared lengths against the actual file size, and the sfnt header of the stored font program. Any mismatch throws the entry away, bumps CorruptCount, and falls through to in-memory subsetting. Putting a broken font program into a customer's PDF would surface weeks later in a viewer on somebody else's machine, far from its cause — and that is the failure this validation exists to prevent
Half-written entries do not appear in the first place. A store writes to a temporary file whose name carries the process ID and the component instance, then moves it over the final name with a single replace-and-flush rename. A crash mid-write leaves a stray .tmp- file, which ClearFontSubsetCache sweeps up, and the real entry is either the complete old version or the complete new one. Compact entries also store the old-to-new glyph map alongside the font program, so a cached compact subset rebuilds the same /CIDToGIDMap and PDF/A output can use the cache without bypassing it
Where should the folder live?
Three conditions decide it: the service account must be able to write there, the folder should sit on local storage, and no deployment step should wipe it. A network share turns every lookup into a round trip and puts rename semantics in the hands of the file server. A folder inside the application directory that the installer recreates means the cache starts cold after every update, which quietly erases the benefit on precisely the day you are watching performance most closely
Several components and several processes can share one local folder. Temporary names never collide, the final rename is atomic, and a lost race costs nothing worse than a miss or a write that has to be retried on the next document. Sharing is the better default on a single machine, because every instance warms the cache for the others. The one visible side effect is that each instance's counters describe only its own traffic, while CurrentBytes and FileCount describe everyone's, and the occasional WriteFailureCount tick can come from two processes replacing the same entry at once. A read-only folder still serves existing entries; it just cannot refresh their timestamps or add new ones
When the cache is the wrong lever
The cache removes repeated work. It does nothing for the first document of a batch and little for workloads whose glyph sets never repeat. If output is dominated by one large CJK face used across unpredictable text, look at which glyphs get pulled into each subset and why, covered in the notes on font subset closure and shaping glyphs. If file size rather than CPU is the goal, the article on CompressDocument and compact font subsets explains what the compact mode changes in the embedded font. When a batch turns out to be slow for reasons unrelated to fonts, the walkthrough of report output with fonts and images shows where the rest of the time usually goes, and the write-up on EndDoc font subset ordering covers when subsets are built during document finalisation
Because the subset cache lives inside the component, a Delphi or C++Builder report server picks it up simply by setting a folder path and a budget, with no extra service to deploy or monitor. Font embedding, subsetting and the rest of the PDF generation features are listed on the HotPDF Delphi component page