---
title: "Fonts: defaults, CJK, emoji and Noto script packs"
url: https://recursiveai.org/onerender/cookbook/fonts/
description: "Find out which characters the bundled fonts can’t draw, add the package that covers them (CJK, emoji or one of 29 Noto script packs), and load your own…"
version: "1.0.0"
---

# Fonts: defaults, CJK, emoji and Noto script packs

Find out which characters the bundled fonts can’t draw, add the package that covers them (CJK, emoji or one of 29 Noto script packs), and load your own font with `@font-face`.

OneRender never uses the fonts installed on the machine unless you pass a provider for them, so a page renders the same everywhere. `RecursiveAI.OneRender.Fonts`, which `.Image` and `.Pdf` bring in, has Liberation Sans, Serif and Mono and Carlito and Caladea. They also answer to Arial, Helvetica, Times New Roman, Courier New, Calibri and Cambria, with the same metrics. They cover Latin, Greek, Cyrillic and most of Hebrew.

## Find what’s missing

A character no font covers is drawn as an empty box, and the font provider records which package would cover it:

```csharp
using OneRender.Fonts;
using OneRender.Image;
using OneRender.Pdf;
using OneRender.Text;

string html = """
<!DOCTYPE html>
<html lang="en"><head><title>Greetings</title>
<style>body { font: 22px/1.6 sans-serif; margin: 24px } .serif { font-family: "Times New Roman", serif } code { font-family: "Courier New", monospace }</style></head>
<body>
<p>Hello, Grüße, Привет, Γειά σου, שלום</p>
<p class="serif">Times New Roman maps to Liberation Serif. <code>Courier New</code> too.</p>
<p>日本語のテキスト, 中文, 한국어</p>
<p>مرحبا بالعالم · नमस्ते दुनिया · สวัสดีชาวโลก</p>
<p>Emoji: 🎉 👩‍💻 🇺🇸</p>
</body></html>
""";

TextFontProvider fonts = FontProviders.CreateDefault();
HtmlPdf.ToPdf(html, File.Create("greetings.pdf"), new HtmlPdfOptions { Fonts = fonts });

// Which packages would cover what the default fonts lack:
foreach (string package in fonts.Catalog.Diagnostics.Items
             .Where(d => d.Kind == TextDiagnosticKind.MissingGlyph)
             .Select(d => d.Package ?? "(no package)").Distinct())
    Console.WriteLine(package);
foreach (OptionalFontPackage p in OptionalFonts.Packages)
    Console.WriteLine($"registered: {p.PackageId}, {p.Faces.Count} faces");

File.WriteAllBytes("greetings.png", HtmlRender.ToImage(html, new HtmlRenderOptions { Width = 720, Height = 330 }).ToPng());
```

With only the default fonts, it prints the packages to add:

```text
RecursiveAI.OneRender.Fonts.Cjk
RecursiveAI.OneRender.Fonts.Noto.Arabic
RecursiveAI.OneRender.Fonts.Noto.Devanagari
RecursiveAI.OneRender.Fonts.Noto.Thai
RecursiveAI.OneRender.Fonts.Emoji
```

The diagnostics live on the provider you pass in `Fonts`, so create one per job when you want to read them. Each `TextDiagnostic` also has the code point and a `Message`.

## Add the packages

```shell
dotnet add package RecursiveAI.OneRender.Fonts.Cjk --version 1.0.0
dotnet add package RecursiveAI.OneRender.Fonts.Noto.Arabic --version 1.0.0
dotnet add package RecursiveAI.OneRender.Fonts.Noto.Devanagari --version 1.0.0
dotnet add package RecursiveAI.OneRender.Fonts.Noto.Thai --version 1.0.0
dotnet add package RecursiveAI.OneRender.Fonts.Emoji --version 1.0.0
```

There’s no code to change. Each package registers itself when the app starts, through a module initializer its build adds, with no reflection, so it works with NativeAOT and trimming. Run the same program again:

```text
registered: RecursiveAI.OneRender.Fonts.Cjk, 10 faces
registered: RecursiveAI.OneRender.Fonts.Noto.Arabic, 2 faces
registered: RecursiveAI.OneRender.Fonts.Noto.Devanagari, 2 faces
registered: RecursiveAI.OneRender.Fonts.Noto.Thai, 2 faces
registered: RecursiveAI.OneRender.Fonts.Emoji, 1 faces
```

No missing glyphs are left, and every line is drawn: Arabic shaped right to left, Devanagari conjuncts, Thai, the Japanese, Chinese and Korean text, and the emoji in color, including the ZWJ sequence and the flag. In the PDF the emoji stay text you can copy, with the color bitmaps drawn over it.

## The packages

- **`.Fonts.Noto.<Script>`**, 29 packages: Arabic, Armenian, Balinese, Bengali, Cherokee, Devanagari, Ethiopic, Georgian, Gujarati, Gurmukhi, Hebrew, Javanese, Kannada, Khmer, Lao, Malayalam, Math, Mongolian, Myanmar, Nko, Oriya, Sinhala, Symbols, Syriac, Tamil, Telugu, Thaana, Thai and Tibetan. Each has the regular and, where the release has one, bold weight.
- **`.Fonts.Cjk`**: Noto Sans CJK in regular and bold, about 32 MB. Name a region with `font-family: "Noto Sans CJK SC"` (also `JP`, `KR`, `TC`, `HK`) to get its glyph forms; without a family name the Japanese forms are used, whatever the `lang`.
- **`.Fonts.Emoji`**: Noto Color Emoji, about 10 MB.

These are fallback fonts: they supply only characters that the families you list and the default fonts lack. They ship the upstream files unmodified, under the SIL Open Font License, with each file’s source and SHA-256 in the package.

## Your own fonts

`@font-face` works as in a browser, with TrueType, OpenType, WOFF and WOFF2. The file comes through the same `Resources` callback as everything else:

```csharp
using OneRender.Image;
using OneRender.Text;

string html = """
<style>
  @font-face { font-family: Brand; src: url("/fonts/brand.ttf") format("truetype") }
  body { margin: 16px; font: 28px Brand, sans-serif }
</style>
<p>Set in the brand font</p>
""";
TextFontProvider fonts = FontProviders.CreateDefault();
RgbaImage img = HtmlRender.ToImage(html, new HtmlRenderOptions
{
    Width = 420, Height = 80, Fonts = fonts,
    BaseUrl = "https://example.com/",
    Resources = url => url == "https://example.com/fonts/brand.ttf" ? File.ReadAllBytes("brand.ttf") : null,
});
File.WriteAllBytes("brand.png", img.ToPng());
Console.WriteLine($"{fonts.Catalog.Diagnostics.Items.Count} font diagnostics");
```

```text
0 font diagnostics
```

A font file that is missing, damaged or over a size limit becomes a diagnostic of kind `FontResourceMissing` or `FontLoadFailed`, and the text falls back to the next family.

## Good to know

- System fonts are used only through an `ISystemFontProvider` you pass to `FontProviders.CreateDefault`, and `local()` in `@font-face` asks only that provider.
- On browser WebAssembly, WOFF2 needs decoded bytes from the host, because the platform has no Brotli.
- Tagged PDF/UA-1 output with whole sentences in complex scripts (Arabic, Devanagari, Thai) can fail veraPDF rule 7.21.7. The text still extracts correctly.

Matching, fallback and shaping are described in [Fonts and text](https://recursiveai.org/onerender/docs/text/).
