HotPDF renders embedded PDF fonts in Delphi without installing anything on the machine: the embedded glyph rendering pipeline in HPDFGlyphRender.pas parses the font programs stored inside the PDF itself — TrueType glyf outlines from FontFile2, CFF Type 2 charstrings from FontFile3, and Type 3 glyph content streams — and replays them as filled GDI vector paths. This article is the font-fidelity deep dive behind rendering PDF pages to a TBitmap with HotPDF: that piece covers the renderer as a whole, this one covers how the text on those pages gets its exact shapes
Why does a PDF render with boxes instead of text?
Boxes, blanks, or subtly wrong characters in rendered PDF output almost always mean the renderer asked the operating system for a font instead of using the one embedded in the file. The complaint arrives the same way every time: the document looks perfect on the machine that produced it, then a customer opens it on a clean server or a locked-down desktop and the Japanese invoice shows tofu squares, or a substituted lookalike face shifts every line break. The fonts were never on that machine — only inside the PDF — and a renderer that stops at system-font substitution cannot see them. Subset fonts make it worse: a subset may carry forty glyphs under character codes assigned privately to that one file, an assignment no installed font shares
ISO 32000-1 §9.9 defines three carriers for an embedded font program in the font descriptor: FontFile holds an original Type 1 program, FontFile2 a TrueType program, and FontFile3 a bare CFF (Type1C or CIDFontType0C) or an OpenType wrapper. A fourth flavour, the Type 3 font of ISO 32000-1 §9.6.5, embeds nothing binary at all — each glyph is a small PDF content stream executed in place. The three carriers differ in outline mathematics (quadratic B-splines versus cubic charstrings versus arbitrary page operators), so a faithful renderer needs a separate interpreter for each, plus an encoding layer that turns character codes into the right glyph index before any outline is touched
How does HotPDF convert TrueType glyf outlines to GDI paths?
THPDFEmbeddedTTF in HPDFGlyphRender.pas reads the loca table to locate each glyph record, walks the glyf contours point by point, and emits a GDI path. Two TrueType conventions need explicit handling. First, consecutive off-curve points imply an on-curve point at their midpoint, and a contour whose points are all off-curve starts at the midpoint of its last and first points — skip either rule and rounded glyphs grow flat facets or collapse. Second, TrueType curves are quadratic Béziers while GDI's PolyBezierTo takes cubics, so every quadratic segment is degree-elevated exactly rather than flattened into line segments
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
Degree elevation is lossless: the cubic traces the identical curve, so the rendered outline matches what a conforming viewer draws from the same table, at any zoom. The remaining work is placement. Each glyph is authored in font units (typically a 1000 or 2048 units-per-em grid), and the renderer composes the scale matrix, the text matrix, and the current transformation matrix into one glyph-to-device transform before the path is filled. Order matters here more than it looks: compose the same three matrices backwards and every glyph collapses toward the origin — a wrong-looking page whose actual bug is one line of matrix algebra
How the Type 2 charstring interpreter handles CFF fonts
THPDFEmbeddedCFF gives FontFile3 programs a genuine Type 2 charstring interpreter: it parses the CFF INDEX structures, the Top DICT and Private DICT, then executes each charstring and emits path segments straight to GDI. An OpenType wrapper (the OTTO container) is stripped first to reach the bare CFF table; naked CIDFontType0C and Type1C streams are consumed directly. Charstrings are a compact stack language, and three of its conventions decide whether the interpreter stays in sync with the byte stream. The optional width prefix means the first stack-clearing operator may carry one extra leading operand. The hintmask operator implies a vstemhm when operands are still on the stack, and the number of mask bytes to skip depends on the accumulated stem count — get the count wrong once and every subsequent opcode is misread. And subroutine calls add a bias to their index (107, 1131, or 32768 depending on subroutine count) before lookup, so an unbiased call lands on the wrong subroutine entirely
CID-keyed CFF adds one indirection that trips up naive implementations: the character code selects a CID, but the charstring index is a GID, and the font's charset maps GID to CID — so the renderer builds the inverse CID-to-GID mapping before drawing, and selects the per-glyph Private DICT through FDSelect for fonts that carry several. Name-keyed Type1C programs, the usual carrier for simple Type 1 fonts, instead resolve one-byte codes through the CFF program's built-in encoding or through the PDF-level encoding machinery described next. One honest caveat: the interpreter reads hint operators to keep the stream synchronized but does not execute hinting, a boundary discussed at the end
What is a Type 3 font and how is it drawn?
A Type 3 glyph is not an outline at all — ISO 32000-1 §9.6.5 defines it as a content stream, so HotPDF renders it by pushing the graphics state, composing the font matrix, font size, and text matrix onto the CTM, and executing the glyph procedure through the same operator interpreter that draws pages, with the font's own /Resources in scope. Two spec details matter for correctness. Type 3 /Widths are expressed in glyph space rather than the 1/1000 text space every other font type uses, so advances must pass through /FontMatrix — barcode fonts with a 0.01 matrix otherwise step wrong by an order of magnitude. And a glyph procedure that opens with the d1 operator makes two promises the renderer enforces: painting is clipped to the declared bounding box, and per ISO 32000-1 §9.6.5.2 the glyph ignores its own colour operators and paints with the caller's current fill colour, so rg, g, k and their stroking twins inside the procedure are suppressed for the duration of that glyph. Skip the colour rule and a d1 barcode font stamped in blue by the page comes out black; skip the clip and a malformed glyph paints outside its cell
How character codes become glyph IDs
Outline interpreters are only half the job, because the bytes in a PDF text string are character codes, not glyph indices, and ISO 32000-1 dedicates two subclauses to the mapping. For simple fonts, §9.6.6 prescribes a strict priority: a /Differences array overrides the base encoding (WinAnsiEncoding, MacRomanEncoding, or StandardEncoding), which overrides the font program's own map. HotPDF resolves that chain into a 256-entry code-to-GID table, translating glyph names to glyph indices by three routes: exact charset match inside a CFF program, numeric gNN/glyphNN names taken as literal indices, and Adobe Glyph List name-to-Unicode translation followed by a cmap lookup for TrueType programs. For composite fonts, §9.7 puts CIDToGIDMap in charge: the common case is /Identity, but the entry may be a stream of big-endian pairs indexed by CID — and HotPDF's own Unicode output uses exactly that stream form for compact subsets, so the stream path is not an exotic corner
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
When a TrueType cmap lookup is needed, HotPDF walks a fallback chain rather than trusting one subtable: the Windows Unicode subtables (format 4, then format 12 for supplementary planes) come first, then the (3,0) symbol subtable with its F000 private-use-area convention mirrored down to the low byte — the reason a symbol font like Wingdings answers for plain ASCII codes — then legacy formats 6 and 0. Format 2 subtables are deliberately not interpreted: they map legacy multi-byte code pages such as Shift-JIS and Big5, not Unicode, and modern CJK fonts invariably carry a format 4 or format 12 subtable anyway. Any code that survives none of these routes falls back to GDI drawing for that single glyph, so one unmappable character degrades one glyph, not the whole text run
What the embedded path does not do
The boundaries are worth stating plainly. Hinting is not executed — outlines are filled as authored, which is indistinguishable from hinted output at 150 DPI and above but can differ by a pixel from a hinted rasteriser at very small sizes. Original Type 1 programs in FontFile (eexec-encrypted charstrings) are not interpreted, and OpenType variable-font axes are not applied; both cases, like a damaged font program or a glyf table without any usable cmap, fall back to system-font drawing rather than failing the page. The same fidelity-first approach extends elsewhere in the renderer — axial and radial shading patterns get the same treatment gradients deserve — and the generation side has its own font-subtlety story in how EndDoc orders font subsets
Using the pipeline requires no font-specific code at all — every mechanism above engages automatically inside the page render call
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
The practical consequence is the one your support inbox cares about: a PDF that carries its fonts renders with those fonts, on a build server, in a Windows container, or on a customer desktop that has never seen the typeface. The embedded glyph rendering pipeline ships as part of the HotPDF Component for Delphi and C++Builder — a native VCL library covering PDF creation, editing, text extraction, and page rendering with no external DLL dependencies