Custom Fonts
The fonts object registers custom font families from uploaded font files. Once registered, a font family can be referenced by name in any style definition using the fontFamily property.
Standard fonts (Helvetica, Times-Roman, Courier) are always available without registration.
Registering a font family
Declare fonts at the document level with a family name and paths to each variant:
{
"document": {
"fonts": {
"Roboto": {
"regular": "fonts/Roboto-Regular.ttf",
"bold": "fonts/Roboto-Bold.ttf",
"italic": "fonts/Roboto-Italic.ttf",
"boldItalic": "fonts/Roboto-BoldItalic.ttf"
}
},
"styles": {
"body": { "fontFamily": "Roboto", "fontSize": 10 }
},
"content": [
{ "p": "This text renders in Roboto.", "style": "body" }
]
}
}
Font files are resolved through the asset storage system — the same way images are loaded. Upload fonts via the tenant assets API before referencing them.
Font family properties
| Property | Type | Required | Description |
|---|---|---|---|
regular | string | Yes | Path to the regular weight font file |
bold | string | No | Path to the bold weight font file |
italic | string | No | Path to the italic font file |
boldItalic | string | No | Path to the bold-italic font file |
Only regular is required. Missing variants fall back to the regular file — the text renders but without the visual weight or slant.
Uploading font files
Upload font files to your tenant's asset storage:
POST /api/v1/{tenantId}/assets/fonts
Content-Type: multipart/form-data
file: Roboto-Regular.ttf
Supported formats: .ttf (TrueType) and .otf (OpenType). Fonts are embedded in the PDF with full Unicode support.
Using in styles
Reference the registered family name in any style's fontFamily property:
{
"fonts": {
"NotoSerif": {
"regular": "fonts/NotoSerif-Regular.ttf",
"bold": "fonts/NotoSerif-Bold.ttf",
"italic": "fonts/NotoSerif-Italic.ttf",
"boldItalic": "fonts/NotoSerif-BoldItalic.ttf"
}
},
"styles": {
"title": { "fontFamily": "NotoSerif", "fontSize": 22, "fontWeight": "bold" },
"body": { "fontFamily": "NotoSerif", "fontSize": 11 },
"bodyItalic": { "fontFamily": "NotoSerif", "fontSize": 11, "fontStyle": "italic" },
"caption": { "fontFamily": "NotoSerif", "fontSize": 8, "fontStyle": "italic" }
}
}
The bold, italic, and boldItalic style flags select the corresponding variant from the registered family.
Inline formatting
Shortcodes ([b], [i], [bi]) use the correct font variant automatically:
{ "p": "Regular text with [b]bold emphasis[/b] and [i]italic notes[/i].", "style": "body" }
If the style's font is NotoSerif, [b] renders with NotoSerif-Bold.ttf, [i] with NotoSerif-Italic.ttf, and so on.
Mixing custom and standard fonts
Custom and standard fonts coexist in the same document. Styles without a fontFamily property use the default Helvetica:
{
"fonts": {
"NotoSerif": { "regular": "fonts/NotoSerif-Regular.ttf" }
},
"styles": {
"body": { "fontFamily": "NotoSerif", "fontSize": 11 },
"footnote": { "fontSize": 8, "color": "#999999" }
},
"content": [
{ "p": "Serif body text.", "style": "body" },
{ "p": "Sans-serif footnote in default Helvetica.", "style": "footnote" }
]
}
Partial families
If you only have the regular weight, register just that file:
{
"fonts": {
"BrandFont": {
"regular": "fonts/BrandFont-Regular.ttf"
}
}
}
Bold, italic, and boldItalic all fall back to the regular file. The text renders but without visual weight or slant differences.
Full example
{
"document": {
"metadata": { "title": "Custom Font Report" },
"fonts": {
"NotoSerif": {
"regular": "fonts/NotoSerif-Regular.ttf",
"bold": "fonts/NotoSerif-Bold.ttf",
"italic": "fonts/NotoSerif-Italic.ttf",
"boldItalic": "fonts/NotoSerif-BoldItalic.ttf"
}
},
"styles": {
"title": { "fontFamily": "NotoSerif", "fontSize": 20, "fontWeight": "bold" },
"body": { "fontFamily": "NotoSerif", "fontSize": 11 },
"caption": { "fontFamily": "NotoSerif", "fontSize": 8, "fontStyle": "italic", "color": "#888888" }
},
"content": [
{ "h1": "Quarterly Report" },
{ "p": "This report uses Noto Serif throughout for a traditional, readable appearance.", "style": "body" },
{ "p": "Key metrics are highlighted with [b]bold[/b] and supplementary notes in [i]italic[/i].", "style": "body" },
{
"table": {
"widths": [3, 1, 1],
"rows": [
[{ "p": "Department", "style": "title" }, { "p": "Q1", "style": "title" }, { "p": "Q2", "style": "title" }],
[{ "p": "Engineering", "style": "body" }, { "p": "$1.2M", "style": "body" }, { "p": "$1.4M", "style": "body" }],
[{ "p": "Marketing", "style": "body" }, { "p": "$800K", "style": "body" }, { "p": "$950K", "style": "body" }]
]
}
},
{ "p": "Source: internal finance systems. Figures are unaudited.", "style": "caption" }
]
}
}
Multi-script & complex text
Registered fonts are shaped at render time by a full text-shaping engine — the same shaping technology browsers use. Complex scripts render correctly straight from JSON, with no client-side rendering:
- Right-to-left — Arabic, Persian, and Hebrew letters join into their initial/medial/final/isolated forms and set right-to-left automatically.
- Indic — Devanagari conjuncts (क्ष, श्र), Bengali, and Tamil render with correct ligature stacking and vowel positioning.
- Thai — above/below vowel marks and tone marks position correctly.
- CJK — Chinese, Japanese, and Korean render from a single Noto Sans CJK family.
- Latin / Cyrillic / Greek — Noto Sans covers extended Latin, Cyrillic, and Greek alongside the default font.
- Geometric symbols — Noto Sans Symbols 2 covers geometric shapes, dingbats, and misc Unicode symbols (▲ ▼ ◆ ✓ ★) with no configuration.
- Enclosed & circled numbers — ① ⑳ ㉑ ㊿ ❶ ➀ ⑴ and squared units (㎏ ㎥) render automatically — see Enclosed & circled numbers for the available ranges.
- Math & technical — Noto Sans Math covers arrows, math operators, and technical symbols (→ ← ≥ ≤ ≠ ∞ etc.) automatically.
The following fonts are bundled and applied automatically with no registration:
| Script / category | Vendored font |
|---|---|
| Arabic, Persian | Noto Sans Arabic |
| Hebrew | Noto Sans Hebrew |
| Devanagari (Hindi, Sanskrit) | Noto Sans Devanagari |
| Bengali | Noto Sans Bengali |
| Tamil | Noto Sans Tamil |
| Thai | Noto Sans Thai |
| Chinese, Japanese, Korean | Noto Sans CJK SC |
| Greek, Cyrillic (Russian, Ukrainian, Bulgarian…), Latin Extended | Noto Sans |
| Geometric symbols, dingbats (▲ ▼ ◆ ✓ ★) | Noto Sans Symbols 2 |
| Math operators, arrows (→ ≥ ≤ ≠ ∞) | Noto Sans Math |
| Enclosed & circled numbers, squared units (① ㉑ ❶ ㎏) | Noto Sans CJK SC |
Two additional fonts are vendored but explicitly invoked rather than automatic:
- Font Awesome icons — access glyphs via
[font, Icons]…[/font]inline - MICR E-13B — applied automatically to runs that contain MICR magnetic-ink characters (routing/account numbers on bank cheques)
For all automatic fonts, just write the text — script detection is automatic. No font registration needed unless you want a specific face.
When you do register a font and reference it via fontFamily in a style or [font, Family]…[/font] inline, it takes precedence over the vendored fallback for those runs.
fontFamilyWhen a style sets fontFamily, that family is used for every run in the paragraph — including scripts it cannot render. If you mix scripts within such a paragraph, register the additional families and switch explicitly with [font, Family]…[/font]:
{
"fonts": {
"Serif": { "regular": "fonts/NotoSerif-Regular.ttf" },
"Arabic": { "regular": "fonts/NotoSansArabic-Regular.ttf", "bold": "fonts/NotoSansArabic-Bold.ttf" }
},
"styles": {
"feat": { "fontFamily": "Serif", "fontSize": 13 }
},
"content": [
{ "p": "Filed by [font, Serif]$data.counsel.name[/font] · [font, Arabic]صالح عالميا[/font]", "style": "feat" }
]
}
Paragraphs whose style has no fontFamily always benefit from automatic script detection — no inline tags needed.
Embedding and subsetting
Fonts embed automatically, and large fonts stay cheap: a Noto Sans CJK file is ~16 MB on disk, but only the glyphs actually used are embedded — a document using a handful of CJK characters adds a few kilobytes, not megabytes. Scripts that depend on contextual substitution (Arabic, Indic, Thai, Hebrew) are embedded in full so every shaped glyph is preserved.
See it in action
The Multilingual Proclamation tutorial renders 25 languages across 8 writing systems in one document — multi-script seal, inline font switching, page breaks, and five barcode symbologies — entirely from declarative JSON.
The Multilingual Device Guide takes the harder case: full safety prose in Chinese, Japanese, Korean, Hindi and Thai — Devanagari conjuncts and Thai tone stacking in running sentences, not just labels — with the whole CJK family delivered by subsetting.
Enclosed & circled numbers
Numbered forms, legends and step lists often want a number inside a circle — ① ② ③ or ❶ ❷ ❸. These are real Unicode characters and render automatically, like any other script — write them directly in a plain paragraph, with no font registration and no inline switching:
{
"styles": {
"boxLabel": { "fontSize": 8, "fontWeight": "bold", "color": "#A93226" }
},
"content": [
{ "p": "① SENDER", "style": "boxLabel" },
{ "p": "⑱ CARRIER'S RESERVATIONS", "style": "boxLabel" }
]
}
Available ranges:
| Style | Range | Characters |
|---|---|---|
| Outlined | 0–50 | ⓪ ① ② … ⑳ ㉑ … ㊿ (U+2460–U+2473, U+3251–U+325F, U+32B1–U+32BF) |
| Filled (negative) | 0–20 | ⓿ ❶ ❷ … ❿ ⓫ … ⓴ |
| Sans-serif circled | 1–10 | ➀ ➁ … ➉ outlined, ➊ ➋ … ➓ filled |
| Parenthesised | 1–20 | ⑴ ⑵ ⑶ … ⒇ (U+2474–U+2487) |
| Circled letters | a–z, A–Z | ⓐ … ⓩ, Ⓐ … Ⓩ, ⒜ … ⒵ |
| Squared units | — | ㎏ ㎡ ㎥ ㎜ ㎞ ㎝ … (U+3300–U+33FF) |
Bold runs pick up the bold face automatically ([b]①[/b] renders a bold circled one). Only the glyphs you actually use are embedded, so a document with two dozen circled numerals adds kilobytes, not the font's full size. As with every script, an explicit fontFamily on the style or an inline [font, Family] tag takes precedence — register a family only when you want a different face for these characters.
When to draw instead. Unicode offers circles and parentheses, but no squared or diamond digits, and the outlined set stops at 50. For a number in a square, diamond or branded shape — or any number beyond the available range — draw it as SVG: a shape plus a text child, sized in points and placed in its own table cell. That stays vector in both PDF and Word and needs no font at all.
Special & display faces
Beyond text fonts, registered families can be domain-specific or display faces — handled identically (register, then reference by fontFamily or switch inline with [font, Family]):
- Blackletter — diploma and certificate mastheads, ornamental titles.
- Signature script — hand-signed names on letters, awards and approvals.
- Handwriting — reviewer annotations, informal callouts, margin notes.
- Monospace — code listings, checksums, column-aligned figures.
- OCR-B — passport machine-readable zone (MRZ) and OCR form fields.
- MICR E-13B — the magnetic-ink routing/account/check band on bank checks.
The Type Specimen tutorial shows each of these in a realistic context, all embedded straight from JSON.
Notes
- Font files are embedded in the generated PDF. Recipients do not need the fonts installed.
- Fonts are loaded once per document generation and cached for the duration of the render.
- If a font file is missing or unreadable, the family is skipped and styles referencing it fall back to Helvetica.
- Font family names are case-insensitive:
"NotoSerif"and"notoserif"resolve to the same family.