EmbedPDF

Fonts

Text the engine draws (a free text annotation, a filled-in form field, a redaction label) is written in the font you name in fontFamily. Characters that font doesn’t have come from fallback fonts, so text in any language shows, and the PDF looks the same in every PDF app:

await page.annotations.create({
  subtype: 'free-text',
  box: { x: 60, y: 132, width: 300, height: 60 },
  contents: 'Order 66 שולם',
  fontFamily: 'helvetica',
});

“Order 66” is drawn in Helvetica, the Hebrew in a Hebrew fallback font, right to left.

The fonts you can use#

doc.fonts.list() returns every font you can name in a document:

const { fonts } = await doc.fonts.list();
 
for (const font of fonts) {
  console.log(font.name, font.source, font.canChoose);
  // 'helvetica' 'standard' true
  // 'noto-sc' 'engine' true
  // 'MinionPro-Regular' 'document' false
}
sourceThe fontsTheir name
'standard'The 14 standard PDF fonts. They only cover Latin text.'helvetica', 'times-bold', …
'engine'The engine’s fonts: the ones you register locally, your server’s on the cloud.the key they were given
'document'Fonts the PDF already has.its PostScript name, such as 'MinionPro-Regular'; without one, its family and style, such as 'Inter,Medium'

A PDF can hold one font several times: each editor that wrote text in it may have added its own copy. Every copy under one name is one entry, with the copies in its programs, so no two fonts in the list share a name.

Pass a font’s name as fontFamily to write with it. A font with canChoose: false can’t be named for new text; restrictions says why (see What a font allows). A name that isn’t in the list fails with InvalidArg: the engine doesn’t quietly swap in another font.

FieldWhat it is
nameWhat you pass as fontFamily.
source'standard', 'engine' or 'document'.
familyNameThe family, such as 'Minion Pro'.
weight100 to 900.
italicWhether it’s an italic.
postScriptNameThe font’s PostScript name, such as 'MinionPro-Regular'; null when it has none.
canChooseWhether new text can be written in it.
restrictionsWhy it can’t be chosen, or why its program can’t be downloaded; empty when neither applies.
programsThe font files behind it; the first is the one new text uses. See Programs.

Next to fonts, the list has fallbacks, the names of the fonts that fill in missing characters in the order they were added, and lastResort, the program that draws a box for a character no font has.

The list changes when a write adds a font to the document, and, on the local engine, when you register fonts. Each change raises the document event fonts.changed:

doc.events.on('fonts.changed', async () => {
  const { fonts } = await doc.fonts.list();
});

Register a font#

To write Chinese, Cyrillic, Greek or your brand typeface, register the font with the engine and use it by a key you choose:

const response = await fetch('/fonts/NotoSansSC-Regular.otf');
 
await engine.fonts.register({
  key: 'noto-sc',
  familyName: 'Noto Sans SC',
  data: new Uint8Array(await response.arrayBuffer()),
});
 
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 60, y: 132, width: 300, height: 60 },
  contents: '请检查这一部分',
  fontFamily: 'noto-sc',
});

register() takes the font file (TTF or OTF) and a key:

FieldWhat it isDefault
keyYour name for the font. You use it wherever you pick a font.required
dataThe font file’s bytes, a Uint8Array or an ArrayBuffer.required
familyNameThe family the PDF names the font by.read from the file
weight100 to 900.read from the file
italicWhether it’s the italic style.read from the file

Registered fonts belong to the engine, not to one document: every document opened on the engine can use them, until you clear them or destroy the engine. Registering a key a second time does nothing and returns the first font, so it’s safe to call on every page load. A file the engine can’t read fails with InvalidArg, and so does one of the 14 standard names as a key.

To load fonts while the engine starts, before the first document opens, pass them to localEngine() with a url or with data:

const engine = localEngine({
  fonts: [{ key: 'brand-sans', url: '/fonts/BrandSans-Regular.ttf' }],
  fallbackFonts: [{ key: 'noto-sc', url: '/fonts/NotoSansSC-Regular.otf' }],
});

fonts are only used when you name them; fallbackFonts also fill in missing characters.

engine.fonts.list(); // the fonts you registered, in order (no await: it's in memory)
await engine.fonts.clearFallbacks(); // empty the fallback list; the fonts stay registered
await engine.fonts.clear(); // unregister every font and empty the fallback list

The engine keeps every registered font in memory, and a CJK font is several megabytes. Register the few fonts you use rather than a whole library.

Text in any language#

The engine picks a font for each character:

  • the font you named, when it has the character;
  • otherwise the first fallback font that has it. Yours come before the ones the engine comes with; among each, the closest in style (weight, then italic) first, then the order they were added in;
  • otherwise the standard font that stands in for the font you named, in its weight and italic: Times for a Times New Roman, Courier for a Courier New, Helvetica for any other. So Latin is never lost, even in a font that has none: “Hello” in Noto Sans Hebrew is written in Helvetica, in a bold font in Helvetica Bold.

It then lays the text out the way the language needs: Hebrew and Arabic right to left, mixed with numbers and Latin words in the right order; Arabic letters joined, Devanagari letters combined. The PDF holds the text in the order it was typed, so copying and searching it works.

Hebrew and Arabic work with no setup: the engine has Noto Sans Hebrew and Noto Naskh Arabic as fallback fonts. For other languages, add a fallback font:

await engine.fonts.register({ key: 'noto-sc', familyName: 'Noto Sans SC', data });
await engine.fonts.addFallback('noto-sc');
 
// "Hello " is drawn in Helvetica, "世界" in Noto Sans SC.
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 60, y: 192, width: 300, height: 60 },
  contents: 'Hello 世界',
});

clearFallbacks() removes the Hebrew and Arabic fonts along with yours, and localEngine({ defaultFallbackFonts: false }) never loads them.

A character no font has is drawn as a box, a small empty rectangle, so the text never silently loses a character. The write tells you which ones:

const { meta } = await page.annotations.create({
  subtype: 'free-text',
  box: { x: 60, y: 252, width: 300, height: 60 },
  contents: 'Hello 世界',
});
 
meta.missingCharacters; // ['世', '界'] when no font has them

Every write that draws text says so in meta.missingCharacters: creating or changing a free text annotation or a redaction’s label, filling in or resetting a form field, applying a redaction whose label wasn’t drawn yet. It’s left out when every character was drawn. In a change (doc.apply), each item’s meta says what its op drew, and the change’s meta what they all did.

Fonts the document already has#

doc.fonts.list() includes every font the document uses, on its pages, in its annotations and in its form. Text in a document’s font can be added when the document holds the whole font and its licence allows editing:

const { fonts } = await doc.fonts.list();
const minion = fonts.find((font) => font.familyName === 'Minion Pro' && font.canChoose);
 
if (minion) {
  await doc.forms.updateWidget(widget.ref, { fontFamily: minion.name });
}

Most PDFs carry only part of a font, the characters their text uses (a subset). Such a font can’t be chosen for new text (restrictions: ['subset']): a character it lacks has nowhere to come from.

A box that already uses a font keeps it, whatever the font is, as long as the font has the characters you type: filling in a form field whose font is a subset of Minion Pro writes Minion Pro. When you type a character the font lacks, the box moves to another font as a whole: the same family from the engine’s fonts if it has it, then the whole font in the document, then the standard font closest to it. resolve() tells you before the write.

Show typed text in a browser#

The viewer shows text being typed in the font the engine will write it in, and so do the headless adapters’ layers with the fonts plugin, which also gives you a font’s CSS in one call. If you build your own editor without them, ask the engine the same question the viewer does: which fonts will it draw this text in?

const { runs, missingCharacters } = await doc.fonts.resolve({
  annotation: annotation.ref, // the box being edited; leave it out for a new one
  runs: [{ text: 'Order 66 שולם', fontFamily: 'helvetica' }],
});

Each run you send comes back with the font the engine will use for it:

FieldWhat it is
fontThe font’s name: the one you asked for, or the one the box moves to.
weightIts weight.
italicWhether it’s an italic.
substitutedtrue when it isn’t the font you asked for.
programThe font file the engine will use, by hash; null when a browser can’t get one.
fallbacksThe files of the fonts that fill in what the run’s font lacks, by hash, in the order the engine tries them: the fallback fonts for this run’s style, then the standard font that stands in for its font.

missingCharacters lists the characters no font has, which the engine will draw as boxes.

A run’s text can also name its font by family, the way rich text does: { text: '世界', family: 'Noto Sans SC', weight: 400, italic: false }.

Then download each file, add it to the browser under a family of its own, and list the families in the same order, ending with the last-resort font that draws the boxes:

const { lastResort } = await doc.fonts.list();
const [run] = runs;
const hashes = [run.program, ...run.fallbacks, lastResort.displayHash].filter((hash) => hash !== null);
 
for (const hash of hashes) {
  const face = new FontFace(`font-${hash}`, await doc.fonts.downloadProgram(hash));
  document.fonts.add(await face.load());
}
 
const input = document.querySelector('textarea')!; // your editor
input.style.fontFamily = hashes.map((hash) => `"font-${hash}"`).join(', ');
input.style.fontSynthesis = 'none'; // never fake a bold or italic the engine won't draw

Ask again when the text gets a character outside what its fonts cover; doc.fonts.getProgram() says what a file covers. After the write, the engine’s own picture of the box is exact.

Programs#

A font’s file is its program. programs on each font, and getProgram(hash), describe them:

FieldWhat it is
displayHashSHA-256 of the file a browser loads. You download and name programs by it.
sourceHashSHA-256 of the file the engine lays text out with and puts in the PDF; null when the document doesn’t hold one.
embedded'whole', 'subset', or 'none' when the PDF only names the font.
format'truetype', 'opentype', 'cff', 'type1' or 'type3'; null when nothing is embedded.
embeddingPermissionWhat the font’s licence says: 'installable', 'editable' or 'preview-and-print'; null when it says nothing.
canDownloadWhether downloadProgram() hands out the file.

getProgram(hash) adds what an editor needs to lay text out the way the engine does:

FieldWhat it is
coversThe characters the engine can write with it, as ranges of code points: { start, count }.
ascentHow far the font reaches above the baseline, in thousandths of the font size.
descentHow far below, as a negative number.

downloadProgram(hash) refuses a program with canDownload: false with InvalidArg, and a hash the document’s fonts don’t name with NotFound.

Document font settings#

Two settings per document shape the text the engine draws from then on. They’re not saved in the file:

await doc.fonts.setEmbeddingPolicy('full');
await doc.fonts.setTypographicFeatures(true);
SettingWhat it doesDefault
setEmbeddingPolicyHow much of a font goes into the PDF: 'subset' (only the characters used), 'full' (the whole font), or 'default'.'default'
setTypographicFeaturesKerning and ligatures for Latin text in rich text.off

'default' embeds only the characters used for annotation text, and the whole font for form fields. A font whose licence doesn’t allow a partial copy is always embedded whole, and a font already in the document is never added again.

Kerning and ligatures are off by default because Acrobat draws annotation text without them, and the engine matches what Acrobat draws.

Good to know#

What a font allows#

Three questions, answered separately for every font:

  • What its licence says (embeddingPermission). A font file says whether it may be embedded for editing or only for showing.
  • Whether new text can be written in it (canChoose).
  • Whether its program can go to a browser (canDownload).
The fontcanChoosecanDownloadrestrictions
A standard fontyesyes
An engine fontyesyes
An engine font whose licence is preview-and-printonce you authorize ityes'preview-and-print' until then
A server font your server keeps to itselfyesno'not-served'
A whole TrueType or OpenType font in the documentyesyes
The same, preview-and-printnono'preview-and-print'
A subset in the documentnoyes'subset'
A Type 1 or CFF font in the documentnoyes'unsupported-format'
A font whose characters can’t be told apart (no Unicode mapping)nono'no-unicode-map'
A Type 3 font (its letters are drawings)nono'unsupported-format'
A font the PDF only namesnono'not-embedded'

A font you can’t choose is still kept by a box that has it (see Fonts the document already has). A program with canDownload: false is shown while typing in a similar font, and the engine’s picture follows the write.

A font whose licence is preview-and-print can show text a document already has, but can’t be used for new text until you say your licence allows it:

await engine.fonts.authorizeEditing('brand-serif');

A font whose licence forbids embedding is refused when you register it.

The standard fonts look the same everywhere#

The engine draws the 14 standard fonts with its own built-in faces, on every computer and every server, whatever fonts are installed. Their programs come from the same faces, so a browser shows them exactly as the engine draws them.

Fonts a browser can’t load#

PDFs, forms especially, often carry their fonts as Type 1 or CFF, which browsers can’t load. For those the engine builds a program a browser can load, with the same letters, the same widths and the same characters, and displayHash names it. That’s why a program has two hashes: they’re the same for a TrueType or OpenType font and differ for these.

Two of a PDF’s fonts can read one such file with other widths or character codes. Each gets a program of its own, so the font lists the file twice under one sourceHash, and resolve() gives a box the program its own font reads.

A hash is not a permission#

The same file in two documents has the same hash, but each document decides what may be done with it: a font allowed in one document isn’t allowed in another because its hash matches.

Variable fonts#

A variable font is fixed to one style when you register it: the weight you give, or the font’s default. The PDF then carries an ordinary font of that style, the font’s instanced field is true, and its program is that fixed font.

Several fonts at once#

registerAll([...]) registers a list of fonts in order and returns them in the same order.

Was this page helpful?

Your feedback goes directly to the documentation team.