EmbedPDF

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 chain

Each 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.

Was this page helpful?

Your feedback goes directly to the documentation team.