Skip to main content

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

PropertyTypeRequiredDescription
regularstringYesPath to the regular weight font file
boldstringNoPath to the bold weight font file
italicstringNoPath to the italic font file
boldItalicstringNoPath 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 / categoryVendored font
Arabic, PersianNoto Sans Arabic
HebrewNoto Sans Hebrew
Devanagari (Hindi, Sanskrit)Noto Sans Devanagari
BengaliNoto Sans Bengali
TamilNoto Sans Tamil
ThaiNoto Sans Thai
Chinese, Japanese, KoreanNoto Sans CJK SC
Greek, Cyrillic (Russian, Ukrainian, Bulgarian…), Latin ExtendedNoto 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.

Script detection is suppressed by an explicit fontFamily

When 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.