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

The handle you get back carries what the engine resolved — the identity a saved document
names the face by, and the licence:

```ts
const handle = await engine.fonts.register({ key: 'noto-sc', data });
handle.familyName; // 'Noto Sans SC' — as registered, else the font's own
handle.weight; // 400 (100..900)
handle.italic; // false
handle.embeddingPermission; // 'installable' | 'editable' | 'preview-and-print'
handle.editingAuthorized; // may new text be authored with it?
handle.instanced; // a static instance cut from a variable font?
```

Licences come from the font's `fsType` and are enforced: a font whose licence forbids
embedding (restricted, or bitmap-only) is **refused at registration**. A
*preview-and-print* font renders text a document already contains and may be embedded, but
does not author new text — a FreeText naming it fails and missing-glyph fallback skips it —
until your application asserts that it holds a licence permitting editing:

```ts
await engine.fonts.authorizeEditing('brand-serif'); // handle.editingAuthorized → true
```

A variable font is pinned to one instance at registration (`wght` to the weight you
gave, or its default), so what's embedded is a plain static program; the handle says so
with `instanced: true`. Fonts whose licence forbids subsetting are embedded whole.

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

Inside a [rich text](https://www.embedpdf.com/docs/engine/core-concepts/annotation-types#rich-text) document a run
names the face by family rather than by key — `{ text: '世界', style: { family: 'Noto Sans SC' } }`
— the way the PDF will; the engine resolves a registered family (or its key) either way.
Reading back, the annotation's `fontFamily` is your key whenever the font is registered
on the engine doing the reading, so a saved document round-trips to the same picker entry.

## Show the font in the browser too

The engine embeds the font in the PDF; the DOM knows nothing about it. The live text
editor and any vector rendering of the annotation set `font-family` to the **key**, so
mount the same bytes as a `@font-face` under that name and the two agree pixel for pixel:

```ts
import { mountWebFont } from '@embedpdf/web';

await engine.fonts.register({ key: 'noto-sc', familyName: 'Noto Sans SC', data });
const unmount = await mountWebFont('noto-sc', data); // @font-face { font-family: 'noto-sc' }
```

`mountWebFont` is refcounted per document and idempotent per key; call the returned
function to release the face. The full viewer does both steps for you from its
`annotations.fonts` option — see [Getting started](https://www.embedpdf.com/docs/viewer/getting-started#fonts-for-text-annotations).

## Document font settings

Two per-document switches shape the appearances the engine generates from then on. They
are session state of the handle, never written into the file, and exist on the local
engine only (`doc.fonts` is `undefined` elsewhere):

```ts
// How much of a registered font's program an appearance carries. 'default'
// subsets annotation text and embeds form-field text whole; 'subset' and
// 'full' apply to both. Programs already in the document are never re-embedded.
await doc.fonts?.setEmbeddingPolicy('full');

// Latin kerning and ligatures when shaping rich text. Off by default: Acrobat
// draws plain advance widths and no ligatures, and matching Acrobat wins.
await doc.fonts?.setTypographicFeatures(true);
```

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