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 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 / 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 (▲ ▼ ◆ ✓ ★)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.

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.

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:

StyleRangeCharacters
Outlined0–50⓪ ① ② … ⑳ ㉑ … ㊿ (U+2460U+2473, U+3251U+325F, U+32B1U+32BF)
Filled (negative)0–20⓿ ❶ ❷ … ❿ ⓫ … ⓴
Sans-serif circled1–10➀ ➁ … ➉ outlined, ➊ ➋ … ➓ filled
Parenthesised1–20⑴ ⑵ ⑶ … ⒇ (U+2474U+2487)
Circled lettersa–z, A–Zⓐ … ⓩ, Ⓐ … Ⓩ, ⒜ … ⒵
Squared units㎏ ㎡ ㎥ ㎜ ㎞ ㎝ … (U+3300U+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.