The PDF opens, the English lines look fine, and every Chinese character is an empty box, a question mark or accented Latin junk such as ä½ å¥½. In the HotPDF Delphi component the cure is the same three steps: hand TextOut a Unicode string, select a font that really contains CJK glyphs, and embed that font as a subset
Each symptom points at a different layer, which is why swapping fonts at random rarely fixes anything. The string can be damaged before HotPDF ever sees it, the font can lack the glyphs, the reading machine can lack the font, or the text can look right on the page and still copy out as garbage. Match what you see to the layer before touching code:
- Empty boxes or blank gaps: the code points arrived intact, but the selected face has no glyph for them, so the viewer draws glyph 0, the
.notdefbox - Question marks: a Unicode string passed through an
AnsiStringon a machine whose ANSI code page has no Chinese, and every unmappable character became?before anything was drawn - Accented Latin junk such as
ä½ å¥½orÄãºÃin place of 你好: bytes written in one encoding were decoded with another, UTF-8 read as Windows-1252 in the first case, GBK read as Windows-1252 in the second - Correct on screen, garbage when copied or searched: the page shows glyphs, but the file has no usable map from glyph codes back to Unicode, which is the job of a ToUnicode CMap (ISO 32000-1 §9.10.3)
- Correct on your PC, broken on the customer machine: the font was referenced instead of embedded, or the machine that generated the file did not have the face you named
Why does Chinese text in a HotPDF PDF turn into question marks?
Because somewhere between your data and TextOut the text was squeezed through an 8-bit ANSI string, and the machine doing the squeezing used a Western code page. On Delphi 7 and the other pre-2009 compilers, string is AnsiString, so a Chinese literal is just bytes in whatever code page the unit was saved in; when it reaches the WideString parameter of THPDFPage.TextOut, the RTL decodes those bytes with the ANSI code page of the computer running the program. Built and tested on Chinese Windows (code page 936), the output looks perfect. Run the same executable on an English server (code page 1252) and 你好 becomes ÄãºÃ. Delphi 2009 and later store string as UTF-16, which removes that trap and adds a quieter one: a unit saved without a byte order mark is read by the compiler in the build machine's ANSI code page, so UTF-8 source compiled on a Western build agent bakes ä½ å¥½ straight into the executable. The question-mark variant runs in the opposite direction, a Unicode string narrowed to AnsiString, and the compiler reports it as warning W1058, implicit string cast with potential data loss. Promote that warning to an error in every unit that builds PDFs
Unicode strings from source code to the PDF page
The safe route keeps the text in UnicodeString or WideString all the way into HotPDF and uses only calls whose text parameter is WideString. THPDFPage.TextOut and its alias UnicodeTextOut qualify. For wrapped paragraphs, use WideTextOutBox in place of TextOutBox: the older call takes AnsiString and measures byte by byte, while WideTextOutBox measures each character with the current font, breaks between any two Han, Kana or Hangul characters (these scripts put no spaces between words) and still honors CR and LF. Measure with GetWideTextWidth instead of TextWidth when you right-align a Chinese total, for the same reason. The AnsiString parameters you cannot avoid are font names, in SetFont and in the fallback chains, so write face names in English: 'SimSun' and 'Microsoft YaHei' survive any code page, while the localized 宋体 arrives as ?? on a Western one. The example below registers a font file that ships with the application, which matters on servers for reasons covered further down:
// Save this unit as UTF-8 with a byte order mark (Delphi 2009 or later)
uses
System.SysUtils, Winapi.Windows, Vcl.Graphics, HPDFDoc;
procedure WriteShippingNotice(const OutFile: string);
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.AutoLaunch := False;
PDF.FileName := OutFile;
PDF.EnableFontSubsetting := True;
PDF.BeginDoc;
// Installed privately for this process; raises if the file is missing
PDF.RegisterUnicodeTTF(ExtractFilePath(ParamStr(0)) + 'Fonts\NotoSansSC-VF.ttf');
PDF.CurrentPage.SetFont('Noto Sans SC', [], 16, DEFAULT_CHARSET);
PDF.CurrentPage.TextOut(60, 60, 0, '发货通知');
PDF.CurrentPage.SetFont('Noto Sans SC', [], 11, DEFAULT_CHARSET);
// X, Y, extra line spacing, box width, box height, text, alignment
PDF.CurrentPage.WideTextOutBox(60, 96, 6, 475, 300,
'您的订单已于今天下午从上海仓库发出,预计三个工作日内送达。' +
'如需修改收货地址,请在发货后二十四小时内联系客服。', jtLeft);
PDF.EndDoc;
finally
PDF.Free;
end;
end;
Delphi 7 has no Chinese string literal that survives a change of machine, so the robust pattern there is to keep CJK text out of the source file entirely. Store it in a UTF-8 text file or resource, read the bytes into an AnsiString without letting anything reinterpret them, and decode once with UTF8Decode, which returns a WideString without consulting the Windows code page at all. The CJKUnicode console demo that ships with HotPDF takes another code-page-proof route and builds its strings from code points ($4E2D, $6587 and so on), which is clumsy for prose but ideal for a regression test that has to produce the same PDF on any build machine. Whichever you choose, the value you hand to TextOut is a WideString from the moment it exists, and the code page of the machine that runs the program never gets a vote:
// Delphi 7: string is AnsiString, so CJK text never appears as a literal
function LoadUtf8Text(const FileName: string): WideString;
var
Stream: TFileStream;
Raw: AnsiString;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
SetLength(Raw, Stream.Size);
if Stream.Size > 0 then
Stream.ReadBuffer(Raw[1], Stream.Size);
finally
Stream.Free;
end;
if Copy(Raw, 1, 3) = #$EF#$BB#$BF then
Delete(Raw, 1, 3); // drop the UTF-8 byte order mark
Result := UTF8Decode(Raw); // no ANSI code page involved
end;
// ... inside the report code, after BeginDoc
Body := LoadUtf8Text(ExtractFilePath(ParamStr(0)) + 'notice.zh-CN.txt');
PDF.CurrentPage.SetFont('SimSun', [], 11, DEFAULT_CHARSET);
PDF.CurrentPage.WideTextOutBox(60, 96, 6, 475, 600, Body, jtLeft);
Picking a CJK font that covers the text and the language
Boxes mean the code points arrived and the font could not draw them, and a common reason is that the face you asked for was never the face that got used. THPDFPage.SetFont asks Windows whether the named face is installed; when it is not, and the name is not one of the built-in Latin faces such as Arial or Times New Roman, HotPDF substitutes Arial Unicode MS, which ships with Microsoft Office rather than with Windows. A developer PC with Office installed therefore renders the page correctly, while a server without Office has neither face, so on servers register the font by path instead of naming it. Coverage also stops at script borders inside CJK: the CJKUnicode demo notes that Chinese faces such as SimHei carry no Hangul, which is why it switches to Malgun Gothic for its Korean line. Pass DEFAULT_CHARSET as the fourth SetFont argument so Windows picks the face by name, as the HotPDF demos do; both RegisterUnicodeTTF and the system-font path accept .ttc collections, which is how most CJK faces ship
The second font decision is about language rather than coverage. Unicode unifies Chinese, Japanese and Korean ideographs, so 直 or 骨 is a single code point whose correct shape differs between a Simplified Chinese, a Traditional Chinese and a Japanese typeface. A PDF carries the glyph outlines of the face you chose, so a Japanese contract drawn in Microsoft YaHei shows Chinese forms to a Japanese reader, and no viewer setting can change that afterward. Choose the face from the language of the text, not from the characters you happen to see: Microsoft YaHei or SimSun for zh-CN, Microsoft JhengHei or MingLiU for zh-TW, Yu Gothic or MS Gothic for Japanese, Malgun Gothic for Korean. When records arrive with a language tag, the selection takes a few lines, and fallback chains catch stray characters from other scripts:
type
TCatalogItem = record
Lang: string; // 'zh-CN', 'zh-TW', 'ja' or 'ko'
Title: WideString;
end;
function FaceForLanguage(const LangTag: string): AnsiString;
begin
if SameText(LangTag, 'ja') then
Result := 'Yu Gothic'
else if SameText(LangTag, 'ko') then
Result := 'Malgun Gothic'
else if SameText(LangTag, 'zh-TW') then
Result := 'Microsoft JhengHei'
else
Result := 'Microsoft YaHei';
end;
// Call after BeginDoc
procedure WriteCatalogLines(PDF: THotPDF; const Items: array of TCatalogItem);
var
I: Integer;
Missing: THPDFUnresolvedGlyphInfoArray;
begin
PDF.SetFontFallbackChain(hfsKana, ['Yu Gothic', 'MS Gothic']);
PDF.SetFontFallbackChain(hfsHangul, ['Malgun Gothic']);
PDF.SetFontFallbackChain(hfsCommon, ['Microsoft YaHei', 'Segoe UI Symbol']);
for I := 0 to High(Items) do
begin
PDF.CurrentPage.SetFont(FaceForLanguage(Items[I].Lang), [], 11, DEFAULT_CHARSET);
PDF.CurrentPage.TextOutWithFallback(60, 80 + I * 20, 0, Items[I].Title);
end;
Missing := PDF.GetUnresolvedGlyphs;
if Length(Missing) > 0 then
raise Exception.CreateFmt('U+%.4X has no glyph in %s; candidates: %s',
[Missing[0].CodePoint, string(Missing[0].FontName),
string(Missing[0].SuggestedFonts)]);
end;
Fallback chains are keyed by script: Han ideographs fall under hfsCJK, kana under hfsKana and Hangul under hfsHangul, but no script chain claims the full-width comma U+FF0C or the ideographic full stop U+3002, so the hfsCommon chain, tried after any script-specific chain, is the one that catches them when the primary face lacks them. TextOutWithFallback records every code point that neither the primary face nor any chain covers, so calling GetUnresolvedGlyphs after the loop turns a silent box into an exception with font suggestions attached. The draw-time variant of that check, TrackUnresolvedGlyphs, is covered in detecting missing PDF glyphs at draw time. Vertical Chinese or Japanese uses the same fonts through the IsVertical argument of SetFont, and the column and punctuation rules are in the CJK vertical text layout guide
Why should a server embed font subsets instead of trusting installed fonts?
Because the PDF is read on a machine you will never see, and the only glyphs you control are the ones inside the file. A PDF that merely names SimSun leaves the reader to substitute: Acrobat may offer to download an Asian font pack, other viewers pick whatever CJK face they have, and some draw nothing. HotPDF embeds the glyphs for Unicode text. Apart from plain ASCII runs when you opt into SimpleLatinFonts, every TextOut run is written as a Type 0 composite font (ISO 32000-1 §9.7) with Identity-H encoding, or Identity-V for vertical text, a CIDFontType2 descendant carrying the embedded TrueType program, and a ToUnicode CMap; FontEmbedding and the NotEmbeddedFonts list govern simple fonts and leave this path alone. Whether the whole face or only the used glyphs go in is your call through EnableFontSubsetting, and with CJK fonts the difference is large. Built without subsetting, the HotPDF HelloWorld demo embeds all of Arial Unicode MS and comes to 15,288,386 bytes for eleven greeting lines; the vertical-text demo embeds the face twice, once under Identity-H and once under Identity-V, and reaches 46,579,235 bytes. For CJK output, turn subsetting on
Our position for server-side generation is firm: set EnableFontSubsetting to True, ship the font file with the service, and register it by path. Subsetting keeps only the glyphs the document actually draws and marks the font name with the six-letter tag described in ISO 32000-1 §9.6.4, so the font program grows with the characters on the page instead of with the size of the typeface. A face registered through RegisterUnicodeTTF is subset at EndDoc automatically and installed privately for the running process, so the server needs no system-wide font installation and two servers with different font folders still produce the same glyphs. Registering by path also changes what failure looks like: a missing file raises an exception at RegisterUnicodeTTF, in your logs, instead of yielding a perfectly valid PDF full of boxes that a customer reports a week later. Check that the license of the font you ship permits embedding. If the output has to be PDF/A, ISO 19005 requires every font to be embedded anyway, so the habit costs nothing; the HotPDF Hello World walkthrough lists the other document defaults worth changing at the same time
How do you verify CJK output on a machine without the font?
Start with the font table, because it answers the embedding and subsetting questions without opening a viewer. pdffonts from Poppler prints one line per font with its type, its encoding and three yes/no columns. For a document built like the first example, expect the Noto face listed as CID TrueType with Identity-H encoding and yes in all three: emb (embedded), sub (subset, recognized from the tag on the name) and uni (a ToUnicode map is present). The old HelloWorld.pdf shows the pattern its file size already gave away: ArialUnicodeMS, CID TrueType, Identity-H, emb yes, sub no, uni yes. After that, open the file on a machine or a clean virtual machine that has none of the fonts you used. Screenshots from your own PC prove little here, since a viewer that finds the font locally can hide a missing embed
Last, test the text layer, which is what search, copy and screen readers use. Select a Chinese line in the viewer, paste it into a UTF-8 editor and compare it with the source string, or run pdftotext -enc UTF-8 on the file. HotPDF writes the ToUnicode CMap from the characters you passed in, so the extracted text should match exactly, full-width punctuation included. One trap sits outside the PDF: a Windows console whose code page cannot show CJK characters can make extracted lines look empty or garbled, which is easy to mistake for a missing ToUnicode map. Writing the output to a UTF-8 file with -enc UTF-8 sidesteps the console; run against the HelloWorld demo, that showed 你好,Delphi PDF世界 and the Japanese and Korean greetings intact
The calls used here, RegisterUnicodeTTF, WideTextOutBox, TextOutWithFallback and the per-script fallback chains, together with the CJKUnicode demo, are part of the HotPDF Delphi PDF Component for Delphi and C++Builder, and the CJKUnicode demo is the quickest way to check your target fonts before wiring them into a report