Documentation
Fonts
Out of the box you have the 14 standard PDF fonts, no setup required. Bring your own TrueType font when you need brand type or characters beyond Latin, and jasy embeds it, subsets it and compresses it for you.
The standard fonts
Three families ship with every PDF reader, so jasy embeds nothing for them. Pick one with font; the
default is Helvetica. bold and italic select the matching face.
Text("Helvetica is the default");
Text("A serif heading", { font: "Times-Roman", bold: true });
Text("Monospaced code", { font: "Courier" });
The family names are Helvetica, Times-Roman and Courier. They cover Latin text (Windows-1252),
including the Euro sign and accented characters.
Embedding your own font
Register a font on the document with addFont(name, source), then use that name as font on any
Text. The source is a path (jasy reads it on Node), the raw bytes, or a per-style family - and the
container does not matter: .ttf, .woff and .woff2 are the same thing at the call site.
import { writeFileSync } from "node:fs";
import { Document, Page, Column, Text, renderToBytes } from "@jasy/pdf";
async function build() {
const doc = Document([
Page({ size: "A4", margin: 56 }, [
Column({ gap: 10 }, [
Text("Set in Inter", { font: "Inter", size: 22 }),
Text("Full Unicode too: Привет, Γειά, čeština", { font: "Inter" }),
]),
]),
]);
doc.addFont("Inter", "Inter-Regular.ttf"); // a path - jasy reads it
writeFileSync("typed.pdf", await renderToBytes(doc));
}
build();
The name is what you pass to Text({ font }). A custom font also unlocks full Unicode (Cyrillic,
Greek, Latin Extended), which the standard fonts cannot show - and, because it carries its own
OpenType tables, it brings kerning and Latin ligatures with it (both on by default) and makes
Arabic shaping and skip-ink underlines possible. See Text.
Already hold the bytes (say, a browser upload)? Pass them instead of a path:
import { readFileSync } from "node:fs";
doc.addFont("Inter", readFileSync("Inter-Regular.ttf"));
From a URL
addFontFromUrl fetches it for you - the one form that also works in the browser, where there is no
filesystem to point at. It resolves at registration, exactly as addFont reads a path there and
then, so a dead link fails on the line that asked for the font rather than inside a later render.
await doc.addFontFromUrl("Inter", "https://example.com/Inter-Regular.woff2");
// a whole family in one call - the files are fetched in parallel
await doc.addFontFromUrl("Inter", {
normal: "https://example.com/Inter-Regular.woff2",
bold: "https://example.com/Inter-Bold.woff2",
});
It is guarded like any other network read: a 15 second timeout, a 32 MB ceiling enforced while the
body arrives, and every failure wrapped as a FontUrlError that says what the file actually was - a
404 HTML page is reported as such, not as a broken font table.
In a bundler, import url from "./Inter.woff2?url" gives you exactly the URL this wants. That is the
whole browser story: an import and one line.
Managing the registry
The document owns its fonts, so you can ask it what is registered - and you never have to deregister:
doc.getFonts(); // ["Inter"] - the registered names
doc.hasFont("Inter"); // true
doc.addFont("Inter", "..."); // re-adding a name overwrites it
A font you register but never use on a Text is dropped at render and costs nothing. Register
freely; only the faces you actually use are embedded.
Bold, italic and a whole family
Register the faces of a family under one name and jasy picks the right one for bold / italic. A
requested style you did not register falls back to the family's regular face, cleanly.
doc.addFont("Inter", {
normal: "Inter-Regular.ttf",
bold: "Inter-Bold.ttf",
italic: "Inter-Italic.ttf",
boldItalic: "Inter-BoldItalic.ttf",
});
Text("Bold Inter", { font: "Inter", bold: true });
Text("Italic Inter", { font: "Inter", italic: true });
Only normal is required; add bold / italic / boldItalic as you have them, each a path or bytes.
What jasy does for you
- Subsetting - only the glyphs you use are embedded (a 740 KB font drops to tens of KB).
- Compression - the embedded program is FlateDecode-compressed.
- Copy and search - a
ToUnicodemap ships with the font, so the text stays selectable and searchable in the reader.
Which files jasy reads
| Format | Supported |
|---|---|
.ttf (TrueType, and TrueType-flavoured OpenType) | yes |
.woff | yes - unwrapped for you |
.woff2 | yes - unwrapped for you |
.otf with CFF outlines | not yet, and it says so by name |
A WOFF is the same font behind a small wrapper; a WOFF2 is more than that - it does not merely compress the glyph table, it rewrites it, with the points spread across five separate streams. jasy unpacks both itself. The only dependency in that path is a Brotli decoder, and it is loaded only if a WOFF2 actually turns up, so a document without one never pays for it.
The reconstruction is checked against the source rather than assumed: all 6,253 glyph outlines of a
real font come back identical, and the rendered page is pixel-for-pixel the same as the one produced
from its .ttf.