Technical Article

Rendering Non-Embedded PDF Fonts with System Fonts in Delphi

When a PDF does not embed a font, the HotPDF component renders that text with an installed Windows font chosen by HPDFMapBaseFontToSystem: it decodes the /BaseFont name, strips the style part, tries several spellings until GDI confirms the family is installed, measures missing standard 14 widths from metric-compatible fonts, and converts single-byte codes to Unicode before drawing. Every one of those steps exists because the naive version failed on real files. The RenderLoadedPageToBitmap page renderer handles embedded programs well; this is the story of the fonts that are not in the file at all

Why does GDI silently draw the wrong typeface for a non-embedded font?

GDI never reports a missing font: hand CreateFontIndirect a face name it does not know and it quietly selects a substitute, often a different serif without a bold weight. The early renderer passed the PDF name almost verbatim, so TimesNewRoman,Bold, TimesNewRomanPS-BoldMT and SegoeUI-Semibold all matched nothing and came out in whatever GDI picked. Names can be worse than that. ISO 32000-1 §7.3.5 lets a name write any byte as #xx, and CJK producers routinely spell font names as escaped UTF-8 or legacy code-page bytes; before v2.766.69 the escapes themselves became the face name. HPDFMapBaseFontToSystem now decodes the escapes first, returns a valid UTF-8 byte sequence as its characters, and reads any other high bytes in the system code page

Cutting the style off is where heuristics bite. A comma always ends the family (Arial,Bold gives Arial), but a hyphen only does when the word after it is a style: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra or Condensed. That rule keeps MS-Mincho whole while turning Calibri-Light into Calibri. HotPDF then tries spellings of the family, followed by spellings of the family with a PSMT, MT or PS suffix removed. Since v2.768.18 those spellings cover every choice of spaces at the places a word can start: before a capital that follows a lower-case letter (MyriadPro becomes Myriad Pro), at the last capital of a run that a lower-case letter follows (UIGothic), and after a leading MS (MSPGothic), from the fully spaced form down to the name as written; past four such places only the fully spaced and the joined name are tried. Spacing cannot be applied blindly, because Windows keeps some words joined: SimSun is installed under exactly that spelling, while MicrosoftYaHei, MicrosoftJhengHei and MSPGothic belong to Microsoft YaHei, Microsoft JhengHei and MS PGothic. Before v2.768.18 the mapper put a space before every inner capital, so MicrosoftYaHei was looked up as Microsoft Ya Hei and never found. A candidate counts as installed when CreateFontIndirect followed by GetTextFace returns the name that was requested or, since v2.768.18, when the selected font's name table lists it as a family, full or typographic family name in any language; the answer is cached per name, so documents with many fonts that are not installed no longer probe Windows for each name on every page

The HotPDF pipeline that renders non-embedded PDF fonts with system fonts in Delphi: HPDFMapBaseFontToSystem decodes #xx escaped bytes in the /BaseFont name, strips Bold, Italic and Light style suffixes while keeping MS-Mincho whole, builds candidate spellings such as Myriad Pro and Microsoft YaHei, and accepts one only when GetTextFace or the font name table confirms the installed name
GDI never reports a missing font, it silently substitutes — the mapping tries its candidates in order and trusts only a name that GDI hands back or the selected font's name table lists

Because the mapping function is public in the HPDFRenderFontMetrics unit, a preflight report can show which installed family each unembedded font will render with, using the font enumeration that THotPDF already exposes for loaded documents:

uses
  HPDFDoc, HPDFRenderFontMetrics;

procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
  Page, I: Integer;
  Info: THPDFLoadedFontInfo;
begin
  for Page := 0 to Pdf.LoadedPageCount - 1 do
    for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
      if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
        Log.Add(Format('page %d  /%s  %s -> %s',
          [Page + 1, string(Info.ResourceName), string(Info.FontName),
           HPDFMapBaseFontToSystem(Info.FontName)]));
end;

How does HotPDF measure standard 14 fonts that have no /Widths?

HotPDF measures the missing advances on the installed font with the same metrics, because ISO 32000-1 §9.6.2.2 allows the standard 14 fonts to omit /Widths and the library ships no AFM tables. Arial carries Helvetica metrics, Times New Roman carries Times and Courier New carries Courier, so HPDFMeasureBaseFontWidths creates the matching face at lfHeight = -1000 and calls GetCharWidth32W; at that height the result is already in the 1/1000 em units PDF widths use. The renderer first turns each code into Unicode through /Encoding, /BaseEncoding and /Differences, defaulting to StandardEncoding. SVG export and text extraction hit one more trap: a standard Type 1 font with no /Encoding at all produced a decoder with no encoding information, SVG export never registered it, and every measured width went unused. Supplying the implied StandardEncoding fixed it, provided it is marked as a predefined encoding; routing it down the CMap-name path decodes every code as 0 and every width follows

Bold, italic and an off-by-one in the font descriptor flags

The /Flags entry of a font descriptor numbers its bits from 1, not 0, so ForceBold is bit 19 ($40000) and Italic is bit 7 ($40), per ISO 32000-1 Table 123. The old code tested $20000, which is bit 18, SmallCap. The mistake survived from v2.345.0 to v2.766.53 because /FontDescriptor is almost always an indirect reference and the font builder only read direct objects, so the whole flags branch never ran, and the same blindness ignored /Widths 12 0 R and laid text out at a 500-unit fallback advance. Once v2.766.53 began resolving indirect references through the renderer, the bit had to be corrected in the same change, otherwise every small-caps face would suddenly have rendered bold:

const
  // ISO 32000-1 Table 123 counts bit positions from 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, not a weight
  FD_FORCEBOLD  = $40000;  // bit 19

procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
  if (Flags and FD_FORCEBOLD) <> 0 then
    LF.lfWeight := FW_BOLD;
  if (Flags and FD_ITALIC) <> 0 then
    LF.lfItalic := 1;
end;
Bit numbering of the PDF font descriptor /Flags entry: ISO 32000-1 Table 123 counts from bit 1, making Italic $40 at bit 7, SmallCap $20000 at bit 18 and ForceBold $40000 at bit 19, so HotPDF's descriptor test of $20000 targeted SmallCap and stayed harmless only while indirect /FontDescriptor references were never resolved
The flag branch was dead code for forty versions because the descriptor was indirect — once references resolved, the off-by-one bit became visible small-caps text

Why do CJK fonts with a UCS2 CMap get the wrong widths?

CJK text with a predefined UCS2 CMap draws the right glyphs but the wrong spacing when a renderer treats the code as the CID, because /W is indexed by CID, not by character code. With STSong-Light and UniGB-UCS2-H, the code happens to equal the Unicode value, so GDI draws the correct characters and the bug hides in the advances: lowercase letters arrive as codes 97 and up, fall outside a /W entry such as [1 95 500], and all receive the default width /DW of 1000. Since v2.766.56 the HotPDF renderer reads codes through the CMap codespace ranges (ISO 32000-1 §9.7.6.2) and maps them to CIDs before looking up widths. Only the built-in UCS2 and UTF16 tables and embedded CMap streams are used; an identity approximation for something like GBK-EUC-H would merely look supported while producing wrong output, so the renderer does not pretend

Why CJK text with a UCS2 CMap draws the right glyphs at the wrong advances: /W is indexed by CID while the codes are Unicode values, so with STSong-Light and UniGB-UCS2-H lowercase codes 97 and up miss the /W entry [1 95 500] and take the /DW default, fixed in HotPDF by mapping codes to CIDs through CMap codespace ranges
The bug hides because code equals Unicode here — glyphs look right while every advance silently defaults, so judge CJK rendering by the spacing, not the shapes

Why do accented characters turn into question marks on Chinese Windows?

Single-byte codes must never reach the ANSI ("A") GDI functions, because GetGlyphOutlineA and GetGlyphIndicesA interpret bytes in the system code page while TextOutA uses the selected font character set. On a Chinese system, Arial byte $A9 (the copyright sign in Windows-1252) became a GBK lead byte and rendered as "?", a trap the unhinted outline path added in v2.766.83 walked straight into. v2.767.3 asks the realized font for its character set with GetTextCharset, converts that to a code page through TranslateCharsetInfo, runs the byte through MultiByteToWideChar and calls the W functions; symbol fonts use U+F000 plus the code instead. Encodings that disagree with Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — are mapped to Unicode before any system font sees them

What are the limits of drawing with system fonts?

System-font rendering is an approximation, and the HotPDF component is honest about where it stops. Before v2.768.18 the install check compared only the name GetTextFace returns, and on localized Windows that function reports the family name in the system language, so Microsoft YaHei on Chinese Windows or Yu Mincho on Japanese Windows was judged missing and drawn in a GDI substitute; since v2.768.18 a face that comes back under another name is also looked up in the font's name table, and such fonts are found. Metric compatibility is guaranteed only for the Helvetica, Times and Courier families; Symbol maps to Symbol and ZapfDingbats to Wingdings, which is a stopgap rather than a match. The preflight above also sees only fonts in the /Resources dictionary of each page, not those referenced from inside form XObjects. When a code still cannot be drawn, the draw-time unresolved glyph tracking reports it, which is a better signal than eyeballing thumbnails

The durable fix sits on the authoring side. HotPDF itself writes with FontEmbedding set to True by default, substituting an embedded Arial even when code calls SetFont with Helvetica, and embedded text goes through the embedded font glyph renderer instead of any of the guesswork above. A cheap guard for incoming files is to warn before rendering when a mapped family is not in the screen font list:

// VCL: Screen.Fonts lists installed family names (Forms unit)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

For the full component, including page rendering, text extraction and font subsetting on the writing side, see the HotPDF Delphi PDF component product page