---
title: "Custom fonts"
description: "Register your own fonts at runtime to author FreeText annotations in any script and fill missing glyphs."
source: "https://www.embedpdf.com/docs/engine/core-concepts/custom-fonts"
---

# 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](https://www.embedpdf.com/docs/engine/core-concepts/annotation-types) —
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](https://www.cloudpdf.com/docs/server/configuration/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.

```ts
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:

```ts
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](https://www.embedpdf.com/docs/engine/core-concepts/downloading) 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**:

```ts
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

```ts
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:

```ts
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](https://www.cloudpdf.com/docs/server/configuration/fonts).
