PDF has no concept of a variable font. A font embedded in a PDF file is a fixed set of outlines with fixed metrics, so a variable font must be reduced to one static instance before it can go into a document. HotPDF performs that instancing internally: you inspect the axes of a variable font, select coordinates such as weight 620 or width 87.5, and the library bakes those values into a complete, self-contained font program that any conforming PDF reader can render
The reason this matters is practical rather than theoretical. Type foundries increasingly ship one variable file instead of a dozen static weights, and design teams pick values that no named instance provides. Without instancing, a report generator either falls back to the default instance, which throws away the design decision, or embeds the whole variable font and hopes the viewer honours axis coordinates it has no way to know about, which no reader is required to do
What does instancing actually have to rebuild?
An OpenType variable font stores one default outline per glyph plus a set of deltas indexed by position in the design space. Applying an axis coordinate is not a matter of writing a number into a header; it means walking the gvar table, interpolating deltas for the requested location, moving points, and then recomputing everything that was derived from those points. HotPDF rebuilds the glyph outlines, the long loca table, the complete horizontal and vertical metrics, the global font bounding box and the sfnt checksum adjustment
Just as important is what gets removed. A static instance must not keep fvar, avar, gvar, HVAR, VVAR, MVAR, STAT or cvar, and a stale DSIG has to go as well, since the signed bytes no longer exist. Leaving any of these behind produces a font that claims to be variable while carrying outlines that have already been moved, and readers that do apply variations will then apply them a second time
Phantom points, and the double-application trap
The subtlest rule in the whole process concerns metrics. In gvar, the point count for a glyph covers the outline points, or the component points for a composite glyph, plus four phantom points that encode the left side bearing, the advance width and their vertical equivalents. Those phantom points are themselves subject to deltas
So when a font has a gvar table, HotPDF derives horizontal and vertical metrics from the interpolated phantom points and does not additionally apply HVAR or VVAR. Adding both is the classic error: the same variation gets applied twice and every advance width comes out slightly too wide, which shows up as text that gradually drifts rightward across a justified line. Only when a font has no gvar does the library bake the metric variation store directly into hmtx or vmtx
Two further details keep the geometry honest. Phantom points never take part in contour interpolation, so points not explicitly listed for a simple glyph are inferred with IUP per contour, with the phantom points excluded. And composite glyphs get their deltas applied to component offsets that use XY parameters, after which child bounds are recomputed recursively. That recursion is bounded in depth and cycle-checked, because a hostile or merely broken component graph can otherwise recurse without end
Inspecting the design space before you choose
The first call in any instancing workflow is InspectVariableFont, which reports the axes and the named instances the foundry defined. Axis records carry the four-byte tag, the minimum, default and maximum values, flags and a name ID; named instances carry a subfamily name ID, flags, an optional PostScript name ID and one coordinate per axis:
var
Pdf: THotPDF;
Axes: THPDFVariableFontAxisArray;
Instances: THPDFVariableFontNamedInstanceArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
begin
for I := 0 to High(Axes) do
Writeln(Format('%s min=%.1f default=%.1f max=%.1f',
[string(Axes[I].Tag), Axes[I].MinimumValue,
Axes[I].DefaultValue, Axes[I].MaximumValue]));
Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
end
else
Writeln('not a variable font - embed it as an ordinary TrueType face');
finally
Pdf.Free;
end;
end;
Reporting the axis range matters because axis values are clamped to the range the font declares, not to the range your UI offers. A slider that lets a user request weight 1000 on a font whose wght axis stops at 900 should be corrected in the interface, not silently at the font layer, or the printed output will disagree with the preview
Selecting coordinates and emitting the document
Axis selection is stateful and applies to fonts registered afterwards. SetVariableFontAxis takes a four-byte printable ASCII tag and a finite value, and rejects anything else with an exception rather than quietly ignoring it. ClearVariableFontAxes resets the selection, and GetVariableFontAxisSelections reports what is currently pending, which is worth logging in report engines where several code paths may have touched the same document object. The family itself is selected by name through SetFont, exactly as any other embedded TrueType face would be:
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.SetVariableFontAxis('wght', 620); // semibold, not a named instance
Pdf.SetVariableFontAxis('wdth', 87.5); // slightly condensed
Pdf.CurrentPage.SetFont('Inter', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');
Pdf.ClearVariableFontAxes; // back to the default instance
Pdf.CurrentPage.SetFont('Inter', [], 10);
Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
The OnVariableFontInstance event fires as each instance is produced and reports the axis values that were used, which is the cheapest way to prove in a log what a given PDF actually contains. Because each distinct coordinate set yields a distinct font program, treat axis selections as part of your font cache key; the caching mechanics are described in the persistent font subset cache
How instancing interacts with subsetting and shaping
Instancing runs before subsetting, and that order is the right one. The instanced font is a normal static TrueType face, so the ordinary subsetter treats it like any other: it computes the glyph closure, keeps the glyphs the document actually uses and drops the rest. The interaction to be aware of is that two different axis selections of the same family are two different font programs, so a document that mixes weight 400 and weight 620 embeds two subsets, not one shared face with two instances
Shaping is unaffected in principle and worth checking in practice. Layout features live in GSUB and GPOS, which instancing preserves, so ligatures and stylistic alternates continue to work as described in OpenType GSUB stylistic alternates. What changes is positioning: a condensed instance has narrower advances than the default, so any layout that measured text before instancing measured the wrong widths. Measure with the same axis selection you will render with, and the discrepancy disappears
One last defensive note from the implementation, useful for anyone extending this path. Fonts without vertical metrics still evaluate dynamic array arguments at the Delphi call site, so parse-time arrays are always allocated rather than relying on a HasVerticalMetrics check to short-circuit past an empty index. It is the kind of language-level detail that turns an apparently guarded branch into an access violation on exactly the fonts you did not test with
Variable font support fits into the same font pipeline as embedding, subsetting and glyph closure, which is described in more depth in font subset closure and shaped glyphs. The full typography feature set for Delphi and C++Builder is listed on the HotPDF Delphi PDF component page