docs

Custom Font Loading

Bundle fonts with your application and control font fallbacks with a custom FontLoader.

On this page

You can bundle fonts with an app so text renders identically across every platform, even on devices with no system fonts. When a page requests a font by CSS family, weight, and style, FontLoader turns that request into font data.

The Default Font Loader

App::Create() installs the default OS font loader to resolve fonts installed on the OS.

When you create the renderer directly with Renderer::Create(), no font loader is installed. A font loader is required— call GetPlatformFontLoader() to use the OS loader without App::Create().

Setting a custom loader on Platform before calling App::Create() or Renderer::Create() replaces the default loader (see Setting Up the Platform).

Loading Fonts in CSS

To load a bundled font without writing a custom loader, define a @font-face rule in CSS:

CSS
@font-face {
  font-family: "Rocket Sans";
  src: url("file:///fonts/rocket-sans.ttf");
}

body { font-family: "Rocket Sans", sans-serif; }

The font loads through the file system like any other asset, while the default OS font loader continues to resolve all other fonts. For details on serving files from memory or disk, see Custom File System.

A custom font loader is only needed when you must supply every family, including fallbacks, from a bundled asset source.

The FontLoader Interface

To provide a custom font loader, subclass FontLoader and pass an instance to Platform::instance().set_font_loader().

The interface requires three virtual methods: Load(), fallback_font(), and fallback_font_for_characters().

đźš§ Thread Safety

The library calls font loader methods from the Renderer's thread and from threads running Web Workers. Because these calls can overlap, the implementation must be safe to call from multiple threads at once.

Loading Font Data

The library calls Load() with a requested family, weight, and style. Return a matching FontFile, or return nullptr— the library then uses a fallback font.

Generic CSS families (serif, sans-serif, and monospace) reach Load() as the family names configured in ViewConfig (by default "Times New Roman", "Arial", and "Courier New"). A loader that ships only bundled fonts sends all of these generic requests to fallback_font() unless you change those ViewConfig names (see Text Rendering and Fonts).

Loading from Memory or Disk

To load font data from memory, call FontFile::Create() with a Buffer containing raw font bytes.

To load a font from disk, pass a file path to FontFile::Create(). The OS opens the file directly— the file must already exist on disk, and loading does not pass through FileSystem.

Handling Font Collections

Font collection files (such as .ttc files) contain several font faces and can include multiple families.

Both FontFile::Create() overloads accept an optional face index as the second argument. Pass the matching face index for collections— without it, the library matches by weight and style alone and cannot distinguish between families that share a file (such as regional CJK faces).

Matching Weights and Styles

The weight parameter is a CSS numeric weight from 100 (thin) to 900 (black), rounded to the nearest 100 (where 400 is normal and 700 is bold).

When no exact match exists, return the closest available face.

If the returned face is not bold for a weight of 600 or higher, or is not slanted for an italic request, the library synthesizes the bold or slant automatically. A page can disable this behavior with the CSS font-synthesis property.

Providing Fallback Fonts

When a requested font family cannot be found or lacks specific glyphs, the library calls fallback methods to locate an alternate family.

The Last-Resort Family

The library calls fallback_font() to retrieve a family name when all other font lookups fail.

Load() must always succeed when passed this fallback family name. If loading this family fails, the library logs a LogLevel::Fatal message and halts because no further fallback exists.

Fallback for Missing Characters

The library calls fallback_font_for_characters() to find a font family capable of drawing specific characters that the active font lacks.

The characters parameter contains one or more UTF-16 characters (almost always a single character). The library calls this method primarily for CJK text missing from the active font, so you should return the name of a bundled family that provides wide character coverage.

Example Implementation

To bundle fonts from both memory and disk, implement FontLoader and install it before creating the Renderer:

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

class MyFontLoader : public FontLoader {
 public:
  ///
  /// The family the library uses when nothing else loads. Load() must
  /// always succeed for it.
  ///
  String fallback_font() const override { return "Rocket Sans"; }

  ///
  /// Pick a family that can draw these characters. We ship one
  /// wide-coverage face for that job.
  ///
  String fallback_font_for_characters(const String& characters, int weight,
                                      bool italic) const override {
    return "Rocket CJK";
  }

  RefPtr<FontFile> Load(const String& family, int weight,
                        bool italic) override {
    ///
    /// Our main family is packed in the asset store, so hand the library
    /// the raw TTF bytes (700 and up is bold, and we ship only upright faces).
    ///
    if (family == "Rocket Sans") {
      const char* file =
          weight >= 700 ? "rocket-sans-bold.ttf" : "rocket-sans.ttf";
      void* data = nullptr;
      size_t size = 0;
      // Pseudo-code, fetch the font bytes from your asset store.
      GetFontBytes(file, &data, &size);
      return FontFile::Create(Buffer::CreateFromCopy(data, size));
    }

    ///
    /// The CJK face is big, so it stays on disk and we pass its OS path
    /// (this one doesn't go through our FileSystem).
    ///
    if (family == "Rocket CJK")
      return FontFile::Create("fonts/rocket-cjk.ttf");

    ///
    /// Anything else, and the library falls back on its own.
    ///
    return nullptr;
  }
};

MyFontLoader font_loader;

void InitPlatform() {
  ///
  /// Install our font loader before creating the Renderer. (Ownership
  /// stays with us, so 'font_loader' must outlive the Renderer.)
  ///
  Platform::instance().set_font_loader(&font_loader);
}