PDFlibPas resolves characters the selected font cannot draw by searching a fallback chain of installed faces, cluster by cluster, while preserving shaping and bidirectional run order. You enable it with SetAutomaticFontFallback, extend the chain with AddFontFallback, and only the fallback fonts actually used for output are embedded in the file
The problem it solves is one every document generator meets the first time a customer name arrives in a script the template font never anticipated. The failure is quiet, which is what makes it expensive
Why does unsupported text vanish instead of raising an error?
Because PDF has no concept of a font that cannot draw a character. A simple font maps byte codes to glyph names through an encoding; a composite font maps codes through a CMap to glyph indices. Ask for a glyph the face does not contain and you get glyph index zero, .notdef, which most faces draw as nothing or as an empty box. The file is structurally valid, the text operator is well formed, and the page renders. It is just blank where the name should be
Nothing in ISO 32000-1 requires a producer to notice. A generator that writes text without checking coverage produces a technically conforming PDF that has silently lost content, and the loss surfaces on a customer's screen weeks later. This is why the fallback feature and the missing-glyph report ship together: resolving what can be resolved is only half the job, and reporting what could not be resolved is the other half
Fallback happens per cluster, not per code point
Granularity is the detail that separates a working implementation from a plausible one. Text is not a sequence of independent characters. A Devanagari syllable, an emoji with a skin-tone modifier, a base letter with combining marks: each is one cluster that must be rendered by one font, because the shaping decisions inside it depend on tables in that face
PDFlibPas resolves clusters, so a cluster that a fallback face covers is drawn entirely by that face. Splitting mid-cluster and drawing half from the primary font and half from a fallback would produce a technically present but visibly broken result, which is arguably worse than the blank you started with. Run order is preserved as well, so a fallback inside a right-to-left run does not reorder the surrounding text; the same machinery underlies the vertical layout described in vertical writing for Japanese and Chinese
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// Search order: first match wins, so put the broadest faces last
Lib.AddFontFallback('Microsoft YaHei'); // Simplified Chinese
Lib.AddFontFallback('Meiryo'); // Japanese
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = embed the face
Lib.SetTextSize(11);
Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
Lib.DrawText(72, 700, 'Delivery status: on time');
Lib.SaveToFile('invoice.pdf');
finally
Lib.Free;
end;
end;
Order the chain deliberately. Resolution takes the first face that covers the cluster, so a broad pan-Unicode font placed first will win almost everything and your carefully chosen script-specific faces will never be consulted. Put the specific faces first and the catch-all last
Report or abort: which failure do you want?
SetMissingGlyphPolicy takes PDF_MISSING_GLYPH_REPORT, the compatible default, or PDF_MISSING_GLYPH_ABORT. Under the report policy the text operation proceeds, unresolvable code points are dropped as before, and each one is recorded. Under the abort policy the text operation is rejected before any content is written and LastErrorCode is set to 521
Choose by what the document is for. A batch of internal reports should keep rendering and log the gaps, because a slightly incomplete report today beats no report at all. A legally binding contract, an invoice, or anything with a name on it should abort, because a silently dropped character in a party name is a defect you want to discover in your own process rather than in a dispute. The abort policy fails before writing, so no half-formed content stream is left behind
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... build the document ...
if Lib.DrawText(72, 660, CustomerName) <> 1 then
if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
begin
Report := Lib.GetMissingGlyphReportJSON;
// {"valid":false,"policy":1,"eventCount":1,"events":[
// {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
// "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
// "fontType":"TrueType","operation":"DrawText"}]}
EscalateToOperator(Report);
end;
finally
Lib.Free;
end;
end;
The report is deliberately machine-readable and bounded. Each event carries the page, the UTF-16 index inside the string, the code point in both numeric and U+XXXX form, the font that was selected, its type and the operation that hit the problem, so a support ticket can name the exact character rather than describing a symptom. The tracker keeps the most recent 256 events, which is enough to diagnose a document and small enough that a pathological run cannot turn diagnostics into a memory problem
Measurement and drawing must agree
Width measurement uses the same cluster-aware fallback decisions as drawing. This sounds obvious and is the thing most home-grown fallback layers get wrong: they patch the drawing path, leave measurement on the primary font, and every text box, right alignment and table column ends up computed from widths that do not match what was rendered
Because both paths share the resolution, a string measured before drawing occupies the width it was measured at, including the fallback runs. That is what makes fallback safe to enable globally rather than only in the places you audited by hand
Only what you used gets embedded
Fallback fonts are embedded lazily: a face in the chain that never resolved a cluster contributes nothing to the output. A document containing one Chinese character and 5,000 Latin ones does not carry a full CJK face; it carries what the subsetting pass produced for that one glyph, which is the behaviour described in file size optimisation and font subsetting
That laziness makes a broad chain cheap to configure. Register the faces your document set might need across every locale you serve, and each individual PDF pays only for what it actually used. For documents you did not generate, where the missing faces are already inside an existing file, the repair path is different and is covered in embedding missing fonts into an existing PDF
One deployment caveat is worth stating plainly: fallback resolves against faces installed on the machine that runs the code. A server without CJK fonts installed has nothing to fall back to, and the report will tell you so on the first document rather than after the first complaint. Ship the fonts you depend on, and confirm licensing for embedding them
PDFlibPas is a Delphi, C++Builder and Lazarus PDF library with matching DLL and ActiveX interfaces, so the fallback and missing-glyph APIs are available from non-Pascal callers too. Full documentation is on the PDFlibPas Delphi PDF library page