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
}source | The fonts | Their 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.
| Field | What it is |
|---|---|
name | What you pass as fontFamily. |
source | 'standard', 'engine' or 'document'. |
familyName | The family, such as 'Minion Pro'. |
weight | 100 to 900. |
italic | Whether it’s an italic. |
postScriptName | The font’s PostScript name, such as 'MinionPro-Regular'; null when it has none. |
canChoose | Whether new text can be written in it. |
restrictions | Why it can’t be chosen, or why its program can’t be downloaded; empty when neither applies. |
programs | The 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();
});Your server’s fonts#
On the cloud the server decides which fonts exist, for every user and every document. They’re
in doc.fonts.list() with source: 'engine', named by the key your server gives them:
await page.annotations.create({
subtype: 'free-text',
box: { x: 60, y: 132, width: 300, height: 60 },
contents: '请检查这一部分',
fontFamily: 'noto-sc',
});See Fonts to give your server fonts, and to choose which of them it may send to browsers.
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:
Your server’s fonts are fallback fonts unless your server says otherwise; see Fonts.
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 themEvery 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:
| Field | What it is |
|---|---|
font | The font’s name: the one you asked for, or the one the box moves to. |
weight | Its weight. |
italic | Whether it’s an italic. |
substituted | true when it isn’t the font you asked for. |
program | The font file the engine will use, by hash; null when a browser can’t get one. |
fallbacks | The 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 drawAsk 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:
| Field | What it is |
|---|---|
displayHash | SHA-256 of the file a browser loads. You download and name programs by it. |
sourceHash | SHA-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. |
embeddingPermission | What the font’s licence says: 'installable', 'editable' or 'preview-and-print'; null when it says nothing. |
canDownload | Whether downloadProgram() hands out the file. |
getProgram(hash) adds what an editor needs to lay text out the way the engine does:
| Field | What it is |
|---|---|
covers | The characters the engine can write with it, as ranges of code points: { start, count }. |
ascent | How far the font reaches above the baseline, in thousandths of the font size. |
descent | How 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.
Downloading a program needs a token that may write text in the document: one with
doc.annotate.modify, doc.forms.fill, doc.forms.modify or doc.redact. Without one it fails
with Forbidden, and details.anyOf lists them.
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 font | canChoose | canDownload | restrictions |
|---|---|---|---|
| A standard font | yes | yes | |
| An engine font | yes | yes | |
| An engine font whose licence is preview-and-print | once you authorize it | yes | 'preview-and-print' until then |
| A server font your server keeps to itself | yes | no | 'not-served' |
| A whole TrueType or OpenType font in the document | yes | yes | |
| The same, preview-and-print | no | no | 'preview-and-print' |
| A subset in the document | no | yes | 'subset' |
| A Type 1 or CFF font in the document | no | yes | 'unsupported-format' |
| A font whose characters can’t be told apart (no Unicode mapping) | no | no | 'no-unicode-map' |
| A Type 3 font (its letters are drawings) | no | no | 'unsupported-format' |
| A font the PDF only names | no | no | '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.
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.