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 with HarfBuzz — the same engine browsers and LibreOffice 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 (▲ ▼ ◆ ✓ ★ and the full U+2000–U+2FFF block) with no configuration.
- 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 (▲ ▼ ◆ ✓ ★, U+2000–U+2FFF) | Noto Sans Symbols 2 |
| Math operators, arrows (→ ≥ ≤ ≠ ∞) | Noto Sans Math |
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.
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.