Custom fonts
The 14 standard PDF fonts only cover Latin text. To put CJK, Cyrillic, Arabic, an emoji, or your brand typeface into a FreeText annotation — or to fill in glyphs a document’s own fonts are missing — register a font with the engine and reference it by a key you choose.
This is a local-engine feature: engine.fonts exists on the engine you create
with @embedpdf/engine. On the cloud engine it’s undefined by design — fallback
fonts there are a server decision (see Fallback fonts).
Registered fonts are engine-global, not per-document: register once and every
document opened on that engine can use them. They live for the engine’s lifetime
(until you clear() them or destroy the engine).
Register a font#
Load the font bytes however you like (fetch, a bundler asset, a file input) and pass
them to register with a stable key you’ll reference later.
import { localEngine } from '@embedpdf/engine';
const engine = localEngine();
const data = new Uint8Array(
await (await fetch('/fonts/NotoSansSC-Regular.otf')).arrayBuffer(),
);
await engine.fonts.register({
key: 'noto-sc', // your stable id — reference this everywhere
familyName: 'Noto Sans SC',
data,
});Only key and data are required. familyName, weight, and italic refine how the
font is matched as a fallback; omit them and they’re inferred from the file.
register is idempotent — registering the same key again is a cheap no-op, so
it’s safe to call on every page load without re-uploading the bytes.
Use it on a FreeText annotation#
A FreeText annotation’s fontFamily accepts either a standard font name or a font key
you registered. Just pass the key:
const page = doc.page(pageObjectNumber);
await page.annotations.create({
subtype: 'free-text',
intent: 'free-text',
rect: { left: 60, bottom: 600, right: 360, top: 660 },
fontFamily: 'noto-sc', // ← your registered key
fontSize: 18,
textAlign: 'left',
contents: '这是一个测试',
color: { r: 0, g: 0, b: 0 },
});When you download the document, the engine embeds only the glyph subset the annotation actually used — so a multi-megabyte CJK font adds just a few kilobytes per annotation, and the text renders anywhere.
The 14 standard font names (helvetica, courier, times-roman, …) are reserved.
Don’t register a custom font under one of those keys, or fontFamily will resolve it
as the standard font. Referencing a key you never registered throws — there’s no
silent fall back to Helvetica.
Automatic fallback for missing glyphs#
Registering a font makes it available to name explicitly. If you also want it to fill in glyphs automatically — when a document’s own fonts (or a FreeText’s chosen font) don’t cover some characters — add it to the fallback chain:
await engine.fonts.register({ key: 'noto-sc', familyName: 'Noto Sans SC', data });
await engine.fonts.addFallback('noto-sc'); // also fills missing glyphs
// "Hello " draws in Helvetica; "世界" is filled from Noto automatically.
await page.annotations.create({
subtype: 'free-text',
intent: 'free-text',
rect: { left: 60, bottom: 540, right: 360, top: 600 },
fontFamily: 'helvetica',
fontSize: 18,
textAlign: 'left',
contents: 'Hello 世界',
color: { r: 0, g: 0, b: 0 },
});register and addFallback are deliberately separate: registering exposes a font for
explicit use, while addFallback also enrolls it for automatic substitution during
page rendering and appearance generation. The chain is ordered — call addFallback for
each font in the priority you want them tried.
Managing registered fonts#
engine.fonts.list(); // FontHandle[] — what's registered, in order
engine.fonts.clearFallbacks(); // drop the fallback chain; fonts stay registered
engine.fonts.clear(); // unregister every font and reset the chainEach registered font is held in memory for the engine’s lifetime. A CJK face is several
megabytes — register the few you actually need rather than a whole library, and reach
for clear() if you swap font sets at runtime.
Cloud parity#
Code that targets both engines should feature-detect, since the cloud engine omits the service:
if (engine.fonts) {
await engine.fonts.register({ key: 'noto-sc', familyName: 'Noto Sans SC', data });
}On the cloud, the server already ships its own fallback fonts and applies them to every render and save — your client doesn’t need to (and can’t) configure them. See Fallback fonts.
Your feedback goes directly to the documentation team.