Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

fonts-rs is a Rust workspace for integrating font families into GTK4 projects. It provides a modular framework for exporting glyphs as SVG icons, generating GResource bundles, resolving icon names to Unicode codepoints, and rendering text with software rasterization.

Architecture Overview

The workspace is organized into three layers:

  1. Generic framework - fonts-rs-model (shared types) and fonts-rs-generator (build-time pipeline)
  2. Font family crates - one crate per font family (e.g. fonts-rs-doto, fonts-rs-bravura, fonts-rs-seven-segment)
  3. Nerd Fonts integration - nerd-fonts-model, nerd-fonts-generator, and nerd-fonts-rs for the legacy Nerd Fonts-specific API
graph TD
    subgraph "Generic Framework"
        Model["fonts-rs-model<br/>Shared types"]
        Gen["fonts-rs-generator<br/>Build pipeline"]
    end

    subgraph "Font Family Crates"
        Doto["fonts-rs-doto"]
        Bravura["fonts-rs-bravura"]
        SevenSeg["fonts-rs-seven-segment"]
        NotoEmoji["fonts-rs-noto-emoji"]
        Other["... 10 more"]
    end

    subgraph "Nerd Fonts"
        NFModel["nerd-fonts-model"]
        NFGen["nerd-fonts-generator"]
        NFRs["nerd-fonts-rs"]
    end

    Model --> Gen
    Model --> Doto
    Model --> Bravura
    Model --> SevenSeg
    Model --> NotoEmoji
    Model --> Other
    Gen --> Doto
    Gen --> Bravura
    Gen --> SevenSeg
    Gen --> NotoEmoji
    Gen --> Other
    Model --> NFModel
    Gen --> NFGen
    NFModel --> NFGen
    NFGen --> NFRs
    Model --> NFRs

Features

  • Modular font family crates - each font family is a separate crate with its own build script, GResource bundle, and runtime API
  • Type-safe glyph names - GlyphName<F> phantom typing prevents mix-ups between font families at compile time
  • Variable font support - variants via Cargo features with axis-based rendering (e.g. weight, roundness)
  • GTK4 integration - GResource registration, icon name resolution, CSS providers
  • Software rendering - font loading via ab_glyph for headless rendering (see pixel-drawing)
  • Build-time code generation - phf::Map codepoint maps, Rust constants, and GResource XML generated from font files
  • Metadata generation - keywords, categories, and aliases for search functionality (e.g. Noto Emoji CLDR annotations)

Available Font Families

CrateFontLicenseVariants
fonts-rs-dotoDotoOFL-1.1Variable (wght, ROND)
fonts-rs-seven-segmentDSEG7OFL-1.1Multiple files
fonts-rs-fourteen-segmentDSEG14OFL-1.1Multiple files
fonts-rs-barcode-code39Lib Barcode 39OFL-1.1-
fonts-rs-barcode-code128Lib Barcode 128OFL-1.1-
fonts-rs-barcode-ean13Lib Barcode EAN13OFL-1.1-
fonts-rs-bravuraBravuraOFL-1.1-
fonts-rs-redactedRedactedOFL-1.1-
fonts-rs-dicefontDiceFontOFL-1.1-
fonts-rs-cuernavacaCuernavacaOFL-1.1-
fonts-rs-noto-emojiNoto EmojiOFL-1.1-
nerd-fonts-rsNerd FontsMIT-

License

MIT for framework code. Font files retain their original licenses (OFL-1.1 for most fonts, MIT for Nerd Fonts). See LICENSE.

Getting Started

Prerequisites

Rust

A working Rust toolchain is required. The project uses Rust Edition 2024.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Minimum Supported Rust Version (MSRV)

The crate declares rust-version = "1.92" in its Cargo.toml. The MSRV is verified by CI on every push and pull request.

Linux System Dependencies

The gtk feature requires GTK 4 development libraries.

Ubuntu/Debian:

sudo apt-get install -y pkg-config libgtk-4-dev libglib2.0-dev

Fedora:

sudo dnf install -y pkg-config gtk4-devel glib2-devel

Arch Linux:

sudo pacman -S pkgconf gtk4 glib2

No System Dependencies (font loading or web only)

If you only need font loading (render feature) or web CSS (web feature), no system libraries are required beyond a Rust toolchain.

Installation

From crates.io

Each font family is a separate crate. Add the ones you need:

[dependencies]
# Nerd Fonts (icon name resolution, GTK4 integration)
nerd-fonts-rs = "0.1"

# Specific font families
fonts-rs-doto = "0.1"
fonts-rs-bravura = "0.1"
fonts-rs-seven-segment = "0.1"
fonts-rs-noto-emoji = "0.1"

From Source

git clone https://github.com/smearor/fonts-rs.git
cd fonts-rs
cargo build --release

Feature Selection

Each font family crate provides the same feature gates:

# GTK4 integration (default)
fonts-rs-doto = "0.1"

# GTK4 + software rendering + embedded fonts
fonts-rs-doto = { version = "0.1", features = ["gtk", "render", "embed-fonts"] }

# Software rendering only (no GTK)
fonts-rs-doto = { version = "0.1", default-features = false, features = ["render"] }

# Variable font variant selection (Doto example)
fonts-rs-doto = { version = "0.1", features = ["bold-dot"] }

For Nerd Fonts specifically:

# GTK4 with 4.12+ APIs
nerd-fonts-rs = { version = "0.1", features = ["gtk", "v4_12"] }

# Font loading only (no GTK)
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["render"] }

# Web CSS only
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["web"] }

# Icon metadata (keywords & categories)
nerd-fonts-rs = { version = "0.1", features = ["metadata"] }

Quick Start

Font Family Crate (e.g. Doto)

use fonts_rs_doto::register_glyphs;
use fonts_rs_doto::GlyphNameExt;
use fonts_rs_doto::DotoName;

fn main() {
    // Register GResource (gtk feature)
    register_glyphs().unwrap();

    // Look up a glyph by name
    let name = DotoName::new("doto-a".to_string());
    if let Some(codepoint) = name.codepoint() {
        println!("Glyph codepoint: U+{:04X}", codepoint as u32);
    }
}

Nerd Fonts (nerd-fonts-rs)

use nerd_fonts_rs::{init, resolve_icon_codepoint};

fn main() {
    // Call once at startup
    init(None);

    // Resolve an icon name to its Unicode codepoint
    if let Some(c) = resolve_icon_codepoint("nf-fa-gamepad") {
        println!("Gamepad icon: U+{:04X}", c as u32);
    }
}

Running Tests

cargo test

Building the Documentation

cd book
mdbook build

The rendered HTML is placed in book/book/. Open book/book/index.html in a browser, or run mdbook serve for live preview during development.

Architecture

Workspace Structure

The workspace is organized into 19 crates across three layers:

LayerCratesRole
Generic frameworkfonts-rs-model, fonts-rs-generatorShared types and build pipeline
Font familiesfonts-rs-doto, fonts-rs-bravura, … (10 crates)One crate per font family
Nerd Fontsnerd-fonts-model, nerd-fonts-generator, nerd-fonts-rsNerd Fonts-specific integration
Applicationsnerd-fonts-cheat-sheet, fonts-rs-noto-emoji-cheat-sheetDemo / cheat sheet apps

Crate Dependencies

graph TD
    Model["fonts-rs-model"] --> Gen["fonts-rs-generator"]
    Model --> NFModel["nerd-fonts-model"]
    Gen --> NFGen["nerd-fonts-generator"]
    NFModel --> NFGen
    Gen --> NotoGen["fonts-rs-noto-emoji-generator"]

    Model --> Doto["fonts-rs-doto"]
    Gen --> Doto
    Model --> Bravura["fonts-rs-bravura"]
    Gen --> Bravura
    Model --> Seven["fonts-rs-seven-segment"]
    Gen --> Seven

    NotoGen --> NotoEmoji["fonts-rs-noto-emoji"]
    NFGen --> NFRs["nerd-fonts-rs"]
    Model --> NFRs

    NotoEmoji --> NotoCS["fonts-rs-noto-emoji-cheat-sheet"]
    NFRs --> NFCS["nerd-fonts-cheat-sheet"]

Build Pipeline

Each font family crate follows the same build pipeline via FontBuild:

flowchart TD
    Font["Font file (TTF/OTF)"] --> Hash["FNV-1a hash"]
    Hash --> Check{"Hash changed?"}
    Check -->|Yes| Export["Export glyphs to SVG"]
    Check -->|No| Skip["Skip export"]
    Export --> Meta["metadata.json"]
    Export --> SVGs["scalable/glyphs/*.svg"]
    Export --> XML["icons.gresource.xml"]
    Meta --> Codegen["Code generation"]
    XML --> GResource["GResource compilation"]
    Codegen --> Codemap["$OUT_DIR/codemap.rs (phf::Map)"]
    Codegen --> Icons["$OUT_DIR/icons.rs (constants)"]
    Codegen --> Variant["$OUT_DIR/variant.rs (variant info)"]
    GResource --> Compiled["icons.gresource (binary)"]
    Skip --> Codegen

Hash-Based Change Detection

FontBuild computes an FNV-1a hash of the font file and stores it in resources/.hash. On subsequent builds, the hash is compared to determine whether re-export is needed. For variable font variants, extra_hash() adds the variant name to the hash, forcing re-export when the active variant changes even if the font file is unchanged.

Code Generation

The default code generation produces three files in OUT_DIR:

  • codemap.rs - phf::Map<char, &str> (codepoint → glyph name) and phf::Map<&str, char> (glyph name → codepoint)
  • icons.rs - pub const strings for each glyph name
  • variant.rs - GRESOURCE_PREFIX and GLYPH_PREFIX constants

Custom code generation is available via FontBuild::run_with().

Font Family Crate Anatomy

Each font family crate has the following structure:

crates/fonts-rs-{name}/
├── Cargo.toml          # Features, dependencies, license-file
├── build.rs            # Build pipeline (FontBuild + ExportConfig)
├── resources/
│   ├── {font}.ttf      # Bundled font file
│   ├── {license}.txt   # Font license
│   └── icons.gresource.xml  # Generated by build.rs
└── src/
    ├── lib.rs          # Runtime API (register_glyphs, all_glyphs, GlyphNameExt)
    ├── definition.rs   # FontFamilyConfig implementation
    ├── naming.rs       # FontFamily marker enum + GlyphName<F> type alias
    ├── variant.rs      # include!(OUT_DIR/variant.rs)
    ├── codepoint_map.rs # include!(OUT_DIR/codemap.rs)
    ├── constants.rs    # include!(OUT_DIR/icons.rs)
    └── fonts.rs        # impl_font_loader! (optional, render feature)

Feature Gates

Each font family crate uses feature gates for optional functionality:

FeatureDescription
gtk (default)GResource registration, GTK4 icon name resolution
renderFont loading via ab_glyph for software rendering
embed-fontsEmbed font files via include_bytes! instead of disk
Variant featuresOne Cargo feature per font variant (e.g. bold-dot)

GResource Registration

The build.rs script compiles GResource bundles via glib-build-tools:

  1. icons.gresource from resources/icons.gresource.xml - contains all exported SVG glyph files
  2. font.gresource (optional) from resources/font.gresource.xml - contains the font file itself for GResource-based font loading

At runtime, register_glyphs() registers the GResource bundle via gio::resources_register_include!("icons.gresource").

Initialization Flow

sequenceDiagram
    participant App as Application
    participant Crate as fonts-rs-{name}
    participant GResource as GResource
    participant CSS as CssProvider

    App->>Crate: register_glyphs()
    Crate->>GResource: resources_register_include!("icons.gresource")
    GResource-->>Crate: OK
    Crate-->>App: Ready

    Note over App,CSS: For Nerd Fonts (nerd-fonts-rs only)
    App->>Crate: init(base_dir)
    Crate->>GResource: Register font + icon GResources
    Crate->>CSS: Load font_face_css() into CssProvider
    Crate->>CSS: Add provider to default display
    Crate-->>App: Ready

fonts-rs-model - Generic Model Types

The fonts-rs-model crate provides font-family-agnostic types shared across all fonts-rs-* crates. These types are the foundation of the modular fonts framework, enabling any font family crate to use the same build pipeline without Nerd Fonts dependencies.

FontFamily (Marker Trait)

A sealed marker trait that identifies each font family at compile time. Each font family crate defines a zero-sized enum implementing FontFamily and the private Sealed trait. This prevents external crates from defining their own font family markers.

#![allow(unused)]
fn main() {
pub trait FontFamily: sealed::Sealed {}
}

Used as the phantom type parameter in GlyphName<F>, so that GlyphName<Doto> and GlyphName<SevenSegment> are distinct types at compile time - preventing mix-ups between font families.

Example

#![allow(unused)]
fn main() {
use fonts_rs_model::FontFamily;
use fonts_rs_model::sealed;

pub enum Doto {}

impl FontFamily for Doto {}
impl sealed::Sealed for Doto {}
}

FontFamilyConfig (Trait)

Base configuration for all font families. Provides compile-time constants for GResource prefixes, icon context directories, and codepoint ranges. Used by both FontDefinition (type-safe glyph names) and ExportConfig (runtime variant pipeline).

#![allow(unused)]
fn main() {
pub trait FontFamilyConfig {
    const FONT_FAMILY_NAME: &'static str;
    const FAMILY_DISPLAY_NAME: &'static str;
    const GRESOURCE_PREFIX: &'static str;
    const ICONS_CONTEXT: &'static str = ICONS_CONTEXT_GLYPHS;
    const CODEPOINT_RANGES: &[CodePointRange] = &[BMP_RANGE];

    fn icons_dir(output_dir: &Path) -> PathBuf { ... }
    fn prepare_icons_dir(output_dir: &Path) -> std::io::Result<PathBuf> { ... }
    fn icons_resource_prefix() -> String { ... }
}
}

Constants

  • FONT_FAMILY_NAME: Slug for GResource prefix and glyph names (e.g. "doto", "dseg7")
  • FAMILY_DISPLAY_NAME: Human-readable name for build logs (e.g. "Doto", "DSEG7")
  • GRESOURCE_PREFIX: Full GResource prefix (e.g. /io/smearor/fonts/doto)
  • ICONS_CONTEXT: Subdirectory after scalable/ - default "glyphs", override e.g. "emoji" for Noto Emoji
  • CODEPOINT_RANGES: Unicode ranges for reverse cmap probing - default BMP, override e.g. ASCII_PRINTABLE_RANGE for barcode fonts

Methods

  • prepare_icons_dir: Removes the scalable/{context}/ directory and recreates it (avoids stale SVGs from previous builds)
  • icons_dir: Returns the path to scalable/{context}/ within the output directory

FontDefinition (Trait)

Extends FontFamilyConfig with type-safe glyph name processing and a default export_glyphs pipeline. Used by font families with semantic PostScript glyph names (e.g. Barcode, Seven-Segment).

#![allow(unused)]
fn main() {
pub trait FontDefinition: FontFamilyConfig {
    type Name: AsRef<str> + Clone + Ord + Serialize + for<'a> Deserialize<'a>;
    type Family: FontFamily;

    fn normalize_name(raw_glyph_name: &str) -> Option<Self::Name>;
    fn should_skip(raw_glyph_name: &str) -> bool { ... }
    fn generate_gresource_xml(...) -> std::io::Result<()> { ... }
    fn export_glyphs(font_path: &Path, output_dir: &Path) -> std::io::Result<usize> { ... }
}
}

Associated Types

  • Name: Glyph name type - GlyphName<Self::Family> for simple families, IconName for Nerd Fonts
  • Family: Font family marker (e.g. Doto, SevenSegment)

Methods

  • normalize_name: Raw glyph name → normalized name (e.g. "zero""dseg7-0")
  • should_skip: Filter for glyph names to skip (default: ., uni, u - overridable)
  • export_glyphs: Default pipeline: SVG export, metadata.json, icons.gresource.xml

FontVariant

Identifies a font variant (e.g. "classic-regular", "bold-dot") and describes how it is realized: separate file, variable font axes, or both.

#![allow(unused)]
fn main() {
pub struct FontVariant {
    name: &'static str,
    variant_type: FontVariantType,
}
}

Constructors

#![allow(unused)]
fn main() {
// Separate file, no axes
FontVariant::file("classic-regular", "DSEG7Classic-Regular");

// Shared file, axes only (variable font)
FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]);

// Separate file with additional axes
FontVariant::file_with_axes("bold-extended", "MyFont-Bold", &[AxisValue::new("wdth", 125.0)]);
}

Methods

  • is_active(): Checks the CARGO_FEATURE_{NAME} environment variable
  • slug(): Variant name with _ instead of - (for GResource paths)
  • axis_values(): Returns the axis configuration
  • font_file(): Returns the font file (if separate)

FontVariantType

Describes how a variant is realized:

#![allow(unused)]
fn main() {
pub struct FontVariantType {
    pub font_file: Option<FontFile>,
    pub axis_values: AxisValues,
}
}
  • file(): Separate file, no axes
  • axes(): Shared file, axes only (variable font)
  • file_with_axes(): Separate file with additional axes

Axes (Axis, AxisValue, AxisValues)

Variable font axis identification and values:

#![allow(unused)]
fn main() {
pub struct Axis(&'static str);  // e.g. "wght", "ROND"

pub struct AxisValue {
    pub axis: Axis,
    pub value: f32,
}

pub struct AxisValues(&'static [AxisValue]);
}
  • AxisValues::EMPTY - default location (no axes)
  • AxisValues is const-compatible for VARIANTS arrays in build.rs
  • Deref to [AxisValue] for iteration

CodePointRange

An inclusive Unicode range for reverse cmap probing:

#![allow(unused)]
fn main() {
pub struct CodePointRange {
    start: CodePoint,
    end: CodePoint,
}
}

Predefined constants in fonts-rs-model:

  • BMP_RANGE - U+0000U+FFFF (Default)
  • ASCII_PRINTABLE_RANGE - U+0020U+007E
  • PUA_RANGE - U+E000U+F8FF
  • SUPPLEMENTARY_PUA_RANGE - U+F0001U+10FFFF

Codepoint Map Types

Newtype wrappers around HashMap for type-safe lookups:

  • CodePointCategoryMap - HashMap<CodePoint, String>: codepoint → category (e.g. from emoji-test.txt)
  • CodePointKeywordMap - HashMap<CodePoint, Vec<String>>: codepoint → keyword list (e.g. from CLDR annotations)
  • CodePointNameMap - HashMap<CodePoint, String>: codepoint → canonical name (e.g. from CLDR tts fields)
  • GlyphNameMap - HashMap<String, CodePoint>: glyph name → codepoint (e.g. from SMuFL glyphnames.json). Implements Deserialize for the SMuFL JSON format { "glyphName": { "codepoint": "U+E050", ... } }.

GlyphEntry<N>

Metadata for a single exported glyph:

#![allow(unused)]
fn main() {
pub struct GlyphEntry<N: AsRef<str>> {
    pub code: Option<CodePoint>,
    pub name: N,
    pub file: PathBuf,
    pub resource_path: ResourcePath,
}
}
  • GlyphEntry::new(code, name, gresource_prefix, icons_context) - constructs file and resource_path automatically from prefix, context, and name
  • Generic over N: GlyphName<F> for type-safe families, String for the ExportConfig pipeline, IconName for Nerd Fonts

GlyphName<F>

Phantom-typed glyph name newtype:

#![allow(unused)]
fn main() {
pub struct GlyphName<F: FontFamily> {
    name: String,
    _marker: PhantomData<F>,
}
}

Implements AsRef<str>, Display, Serialize, Deserialize, Hash, Eq, Ord. Construction via GlyphName::new(String) after normalization.

Each font family crate defines a type alias:

#![allow(unused)]
fn main() {
pub type DotoName = GlyphName<Doto>;
}

FontFile

A font file name without extension, e.g. "DSEG7Classic-Regular", "Doto". Wraps a &'static str for type-safe font file references in build scripts.

ResourcePath

A GResource resource path (e.g. /io/smearor/fonts/doto/scalable/glyphs/doto-a.svg). Used by GlyphEntry to store the GResource URI for each exported glyph.

fonts-rs-generator - Build-Time Pipeline

The fonts-rs-generator crate provides the build-time pipeline for exporting glyphs from TTF/OTF font files, generating codepoint maps, compiling GResource bundles, and producing Rust source constants. It is used as a build-dependency by every font family crate.

ExportConfig<X: FontFamilyConfig>

Runtime configuration for glyph export, decoupled from FontDefinition. Enables variant-specific parameters at build time without a separate FontDefinition impl per variant.

#![allow(unused)]
fn main() {
pub struct ExportConfig<X: FontFamilyConfig> {
    pub gresource_prefix: String,
    pub glyph_name_prefix: String,
    pub axes: AxisValues,
    pub name_filter: Option<fn(&str) -> bool>,
    pub family_display_name: String,
    _marker: PhantomData<X>,
}
}

Constructors

  • ExportConfig::new() - without a variant, uses X::GRESOURCE_PREFIX and X::FONT_FAMILY_NAME
  • ExportConfig::with_variant(variant) - with a variant, constructs gresource_prefix as {X::GRESOURCE_PREFIX}/{variant_slug} and glyph_name_prefix as {X::FONT_FAMILY_NAME}-{variant}

Methods

  • export_glyphs(font_path, output_dir) - standard export: iterates over all glyph IDs, normalizes names to {glyph_name_prefix}-{kebab}, renders SVGs with axis location
  • export_glyphs_by_name_map(font_path, output_dir, name_map) - export via external name→codepoint map (e.g. SMuFL glyphnames.json): for fonts without PostScript glyph names
  • write_variant_info() - writes OUT_DIR/variant.rs with GRESOURCE_PREFIX and GLYPH_PREFIX constants
  • generate_gresource_xml(entries, path) - generates icons.gresource.xml with quick-xml serialization

export_glyphs() (Two Variants)

There are two distinct export_glyphs methods in the framework:

FontDefinition::export_glyphs (trait default method)

Uses Self::normalize_name for type-safe glyph names. Iterates over all glyph IDs, calls should_skip and normalize_name, renders SVGs without axis location (default location).

ExportConfig::export_glyphs (struct method)

Uses normalize_to_kebab + glyph_name_prefix for string-based names. Renders SVGs with axis location (glyph_to_svg_full_height_at). Supports name_filter for custom glyph filtering.

Output

Both produce:

  • <output_dir>/scalable/{context}/*.svg
  • <output_dir>/metadata.json
  • <output_dir>/icons.gresource.xml

write_variant_info()

Writes OUT_DIR/variant.rs with two constants:

#![allow(unused)]
fn main() {
pub const GRESOURCE_PREFIX: &str = "/io/smearor/fonts/doto/regular_medium";
pub const GLYPH_PREFIX: &str = "doto-regular-medium";
}

Included by lib.rs via include!(concat!(env!("OUT_DIR"), "/variant.rs")). Enables the runtime API to construct GResource paths and glyph names with the active variant prefix.

detect_and_get_active_variant()

On VariantList<X: FontFamilyConfig>:

#![allow(unused)]
fn main() {
pub fn detect_and_get_active_variant(&self, default_index: usize) -> miette::Result<&'a FontVariant>
}

Scans CARGO_FEATURE_{NAME} environment variables for each variant. Returns the active variant or default_index if none is active. Returns an error if more than one variant is active. Uses X::FAMILY_DISPLAY_NAME for diagnostic messages.

FontBuild (Builder)

Builder for the common build.rs pipeline. Handles hash-based change detection, conditional glyph export, GResource compilation, and code generator invocation.

#![allow(unused)]
fn main() {
FontBuild::new(&font_path)
    .extra_hash(entry.as_str())     // for variable font variants
    .compile_font_gresource()       // optional: font.gresource
    .rerun_if_changed(path)         // additional cargo:rerun-if-changed
    .additional_gresource(xml, out) // additional GResource bundles
    .run(|font_path, resources_dir| {
        config.export_glyphs(font_path, resources_dir)
    })
    .map_err(|e| miette::miette!("{e}"))?;
}

Builder Methods

  • extra_hash: Additional hash component (e.g. variant name) - forces re-export on variant change even if the font file is unchanged
  • compile_font_gresource: Also compile font.gresource in addition to icons.gresource
  • rerun_if_changed: Add extra cargo:rerun-if-changed paths
  • additional_gresource: Register additional GResource bundles to compile

Run Methods

  • run: Export closure + default code generation (CodemapGenerator + RustConstantsGenerator)
  • run_with: Export closure + custom code generation closure
  • Hash is computed as FNV-1a of the font file and stored in resources/.hash

impl_font_loader! Macro

Generates a font() -> Option<&'static ab_glyph::FontVec> function that loads a TTF font file and caches it in a OnceLock.

Two variants:

#![allow(unused)]
fn main() {
// Literal: relative to resources/
fonts_rs_generator::impl_font_loader!("Doto.ttf");

// Env var: absolute path set by build.rs via cargo:rustc-env
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}

With the embed-fonts feature, include_bytes! is used; otherwise std::fs::read.

MetadataGenerator (Trait)

Trait for generating phf::Map metadata tables (keywords, categories, aliases) at build time.

#![allow(unused)]
fn main() {
pub trait MetadataGenerator {
    type Name: AsRef<str>;

    fn keywords_for(&self, entry: &GlyphEntry<Self::Name>) -> Vec<String>;
    fn categories_for(&self, entry: &GlyphEntry<Self::Name>) -> Vec<String>;
    fn aliases(&self) -> Vec<(String, String)> { Vec::new() }
    fn deduplicate_statics() -> bool { false }

    fn generate_keywords(&self, entries: &[GlyphEntry<Self::Name>]) -> String { ... }
    fn generate_categories(&self, entries: &[GlyphEntry<Self::Name>]) -> String { ... }
    fn generate_aliases(&self) -> String { ... }
    fn run(&self, entries: &[GlyphEntry<Self::Name>]) -> Result<(), std::io::Error> { ... }
}
}
  • deduplicate_statics() = true: Extracts repeated value lists into static constants (e.g. static KW_0: &[GlyphKeyword] = &[...]) - reduces binary size for large maps with many duplicates
  • run(): Writes keywords.rs, categories.rs, aliases.rs to OUT_DIR

Example implementation: NotoEmojiMetadataGenerator in fonts-rs-noto-emoji-generator uses CodePointKeywordMap and CodePointCategoryMap as data sources.

VariantList<X: FontFamilyConfig>

A typed list of font variants, generic over the font family config. Provides methods to detect the active variant from Cargo features.

#![allow(unused)]
fn main() {
const VARIANTS: VariantList<DotoConfig> = VariantList::new(&[
    FontVariant::axes("regular-medium", &[AxisValue::new("wght", 500.0), AxisValue::new("ROND", 50.0)]),
    FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]),
]);

let entry = VARIANTS.detect_and_get_active_variant(0)?;
}

FontVariantExt (Extension Trait)

Extension trait providing a method to get the font file path from a FontVariant, handling errors if no font file is associated.

set_font_path_env() Helper

Sets cargo:rustc-env with the absolute font path for include_bytes! in lib.rs. Constructs the absolute path from CARGO_MANIFEST_DIR and the given relative font path.

#![allow(unused)]
fn main() {
set_font_path_env("DOTO_FONT_PATH", &font_path)?;
}

build_constants Module

Provides common constants for build scripts:

  • RESOURCES_DIR - the resources/ directory path
  • METADATA_PATH - path to resources/metadata.json
  • HASH_PATH - path to resources/.hash
  • ICONS_GRESOURCE_XML - path to resources/icons.gresource.xml
  • ICONS_GRESOURCE - path to compiled icons.gresource
  • FONT_GRESOURCE_XML - path to resources/font.gresource.xml
  • FONT_GRESOURCE - path to compiled font.gresource

Build Script & Runtime API Patterns

Each font family crate follows a consistent structure with a build.rs for build-time glyph export and a lib.rs for runtime API. This page documents the two main build.rs patterns and the standard lib.rs layout.

Crate File Structure

crates/fonts-rs-{name}/
├── Cargo.toml
├── build.rs
├── resources/
│   ├── {font}.ttf or .otf
│   ├── {license}.txt
│   └── icons.gresource.xml      (generated by build.rs)
└── src/
    ├── lib.rs
    ├── definition.rs
    ├── naming.rs
    ├── variant.rs               (generated by build.rs)
    ├── codepoint_map.rs         (generated by build.rs)
    ├── constants.rs             (generated by build.rs)
    └── fonts.rs                 (optional, render feature)

definition.rs - FontFamilyConfig Implementation

Each font family crate defines a definition.rs with a FontFamilyConfig implementation:

#![allow(unused)]
fn main() {
use const_format::concatcp;
use fonts_rs_generator::FontFamilyConfig;
use fonts_rs_model::CodePointRange;
use fonts_rs_model::GRESOURCE_BASE_PREFIX;
use fonts_rs_model::ASCII_PRINTABLE_RANGE;

pub struct DotoConfig;

impl FontFamilyConfig for DotoConfig {
    const FONT_FAMILY_NAME: &'static str = "doto";
    const FAMILY_DISPLAY_NAME: &'static str = "Doto";
    const GRESOURCE_PREFIX: &'static str = concatcp!(GRESOURCE_BASE_PREFIX, "/", DotoConfig::FONT_FAMILY_NAME);
    const CODEPOINT_RANGES: &[CodePointRange] = ASCII_PRINTABLE_RANGE;
}
}

The definition.rs is included by both build.rs (via #[path = ...]) and lib.rs (via mod definition).

build.rs Patterns

There are two main patterns for build.rs, depending on whether the font has variants.

Pattern 1: Variable Font with Variants (e.g. Doto)

Used for variable fonts where the same TTF file is rendered at different axis locations, or for font families with multiple separate font files. Variants are selected via Cargo features at build time.

use std::path::Path;

use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::VariantList;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::AxisValue;
use fonts_rs_model::FontVariant;

#[path = "src/definition.rs"]
mod definition;
use definition::DotoConfig;

const FONT_FILE: &str = "Doto.ttf";

const VARIANTS: VariantList<DotoConfig> = VariantList::new(&[
    FontVariant::axes("regular-medium", &[AxisValue::new("wght", 500.0), AxisValue::new("ROND", 50.0)]),
    FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]),
    // ...
]);

fn main() -> miette::Result<()> {
    let entry = VARIANTS.detect_and_get_active_variant(12)?;
    let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);

    eprintln!("build.rs: active variant: {} -> {}", entry, font_path.display());

    set_font_path_env("DOTO_FONT_PATH", &font_path)?;

    let config = ExportConfig::<DotoConfig>::with_variant(*entry);

    FontBuild::new(&font_path)
        .extra_hash(entry.as_str())
        .run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
        .map_err(|e| miette::miette!("{e}"))?;

    config.write_variant_info()?;
    Ok(())
}

Key points:

  • VariantList::new(&[...]) defines all variants as a const array
  • detect_and_get_active_variant(default_index) scans Cargo features
  • extra_hash(entry.as_str()) forces re-export when the variant changes
  • ExportConfig::with_variant(*entry) creates variant-specific config
  • write_variant_info() generates OUT_DIR/variant.rs

Pattern 2: Static Font with External Name Map (e.g. Bravura)

Used for fonts without PostScript glyph names (e.g. SMuFL fonts with post table version 3.0). Glyph names come from an external metadata file (e.g. glyphnames.json).

use std::path::Path;

use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::GlyphNameMap;

#[path = "src/definition.rs"]
mod definition;
use definition::BravuraConfig;

const FONT_FILE: &str = "Bravura.otf";
const GLYPHNAMES_FILE: &str = "glyphnames.json";

fn parse_glyphnames(json: &str) -> miette::Result<GlyphNameMap> {
    serde_json::from_str(json).map_err(|e| miette::miette!("Failed to parse glyphnames.json: {e}"))
}

fn main() -> miette::Result<()> {
    let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
    let glyphnames_path = Path::new(build_constants::RESOURCES_DIR).join(GLYPHNAMES_FILE);

    eprintln!("build.rs: exporting glyphs from {}", font_path.display());

    set_font_path_env("BRAVURA_FONT_PATH", &font_path)?;

    let glyphnames_json = std::fs::read_to_string(&glyphnames_path)
        .map_err(|e| miette::miette!("Failed to read {}: {e}", glyphnames_path.display()))?;
    let name_map = parse_glyphnames(&glyphnames_json)?;
    eprintln!("build.rs: parsed {} SMuFL glyph names", name_map.len());

    let config = ExportConfig::<BravuraConfig>::new();

    FontBuild::new(&font_path)
        .run(|font_path, resources_dir| {
            config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
        })
        .map_err(|e| miette::miette!("{e}"))?;

    config.write_variant_info()?;
    Ok(())
}

Key points:

  • ExportConfig::new() - no variant, uses family config defaults
  • export_glyphs_by_name_map() - uses external name→codepoint mapping
  • No extra_hash needed (no variant selection)

lib.rs - Runtime API

Each font family crate has a lib.rs with the following structure:

#![allow(unused)]
fn main() {
pub mod codepoint_map;   // include!(concat!(env!("OUT_DIR"), "/codemap.rs"))
pub mod constants;       // include!(concat!(env!("OUT_DIR"), "/icons.rs"))
pub mod naming;          // FontFamily marker + GlyphName<F> type alias
pub mod variant;         // include!(concat!(env!("OUT_DIR"), "/variant.rs"))

#[cfg(feature = "render")]
pub mod fonts;          // impl_font_loader!(env: "FONT_PATH")

use fonts_rs_model::CodePoint;

// Re-export key types
pub use naming::DotoName;

// GResource registration (gtk feature)
#[cfg(feature = "gtk")]
pub fn register_glyphs() -> Result<(), gio::glib::Error> {
    gio::resources_register_include!("icons.gresource")?;
    Ok(())
}

// Codepoint lookup
pub trait GlyphNameExt {
    fn codepoint(&self) -> Option<CodePoint>;
}

impl GlyphNameExt for DotoName {
    fn codepoint(&self) -> Option<CodePoint> {
        let key = self.as_ref();
        if let Some(ch) = codepoint_map::REVERSE_GLYPHS.get(key).copied() {
            return Some(CodePoint::from(ch));
        }
        let full = format!("{}-{}", variant::GLYPH_PREFIX, key);
        codepoint_map::REVERSE_GLYPHS.get(&full).copied().map(CodePoint::from)
    }
}

// Iterator over all glyphs
pub fn all_glyphs() -> impl Iterator<Item = (char, &'static str)> {
    codepoint_map::GLYPHS.entries().map(|(c, name)| (*c, *name))
}
}

Generated Modules

Three modules are generated by build.rs into OUT_DIR:

  • codepoint_map.rs - phf::Map with GLYPHS (char → name) and REVERSE_GLYPHS (name → char) maps
  • icons.rs - pub const strings for each glyph name
  • variant.rs - GRESOURCE_PREFIX and GLYPH_PREFIX constants

naming.rs - Font Family Marker

#![allow(unused)]
fn main() {
use fonts_rs_model::FontFamily;
use fonts_rs_model::GlyphName;
use fonts_rs_model::sealed;

pub enum Doto {}

impl FontFamily for Doto {}
impl sealed::Sealed for Doto {}

pub type DotoName = GlyphName<Doto>;
}

variant.rs - Generated Variant Info

#![allow(unused)]
fn main() {
// @generated by build.rs - do not edit

/// GResource prefix for Doto.
pub const GRESOURCE_PREFIX: &str = "/io/smearor/fonts/doto/regular_medium";

/// Glyph name prefix for Doto.
pub const GLYPH_PREFIX: &str = "doto-regular-medium";
}

fonts.rs - Font Loading (Optional)

#![allow(unused)]
fn main() {
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}

Generates a font() function that loads the TTF file from the path set by build.rs via cargo:rustc-env. Cached in OnceLock.

Adding a New Font Family

This guide walks through creating a new font family crate in the fonts-rs workspace. The process is designed to require minimal boilerplate - most of the build pipeline is handled by the generic framework in fonts-rs-generator.

Overview

A font family crate integrates a TTF/OTF font into the fonts-rs ecosystem, providing:

  • Build-time glyph export to SVG files
  • GResource bundle compilation for GTK
  • Codepoint map generation (phf::Map)
  • Runtime glyph name resolution and GResource registration
  • Optional software rendering via ab_glyph

Step 1: Create the Crate

Create a new directory under crates/:

crates/fonts-rs-{name}/
├── Cargo.toml
├── build.rs
├── resources/
│   └── {font}.ttf
└── src/
    ├── lib.rs
    ├── definition.rs
    ├── naming.rs
    ├── variant.rs
    ├── codepoint_map.rs
    ├── constants.rs
    └── fonts.rs       (optional)

Step 2: Bundle the Font File

Place the TTF/OTF font file and its license into resources/:

resources/
├── MyFont-Regular.ttf
└── OFL.txt            (or appropriate license)

Step 3: Write definition.rs

Implement FontFamilyConfig with the family’s constants:

#![allow(unused)]
fn main() {
//! `FontFamilyConfig` for the MyFont font family.

use const_format::concatcp;
use fonts_rs_generator::FontFamilyConfig;
use fonts_rs_model::CodePointRange;
use fonts_rs_model::GRESOURCE_BASE_PREFIX;
use fonts_rs_model::PUA_RANGE;

pub struct MyFontConfig;

impl FontFamilyConfig for MyFontConfig {
    const FONT_FAMILY_NAME: &'static str = "myfont";
    const FAMILY_DISPLAY_NAME: &'static str = "MyFont";
    const GRESOURCE_PREFIX: &'static str = concatcp!(GRESOURCE_BASE_PREFIX, "/", MyFontConfig::FONT_FAMILY_NAME);
    const CODEPOINT_RANGES: &[CodePointRange] = &[PUA_RANGE];
}
}

Choose CODEPOINT_RANGES based on the font’s Unicode coverage:

  • BMP_RANGE - full BMP (default, most fonts)
  • ASCII_PRINTABLE_RANGE - barcode fonts, seven-segment display fonts
  • PUA_RANGE - SMuFL music fonts, icon fonts in the Private Use Area

Step 4: Write naming.rs

Define the FontFamily marker enum and GlyphName<F> type alias:

#![allow(unused)]
fn main() {
//! Font family marker type for MyFont.

use fonts_rs_model::FontFamily;
use fonts_rs_model::GlyphName;
use fonts_rs_model::sealed;

/// Marker type identifying MyFont in `GlyphName<MyFont>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum MyFont {}

impl FontFamily for MyFont {}
impl sealed::Sealed for MyFont {}

/// Convenience type alias for MyFont glyph names.
pub type MyFontName = GlyphName<MyFont>;
}

Step 5: Write build.rs

Choose the pattern that matches your font:

Variable Font with Variants

If the font has multiple variants (e.g. weight/roundness combinations), use VariantList + ExportConfig::with_variant:

use std::path::Path;

use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::VariantList;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::AxisValue;
use fonts_rs_model::FontVariant;

#[path = "src/definition.rs"]
mod definition;
use definition::MyFontConfig;

const FONT_FILE: &str = "MyFont.ttf";

const VARIANTS: VariantList<MyFontConfig> = VariantList::new(&[
    FontVariant::axes("regular", &[AxisValue::new("wght", 400.0)]),
    FontVariant::axes("bold", &[AxisValue::new("wght", 700.0)]),
]);

fn main() -> miette::Result<()> {
    let entry = VARIANTS.detect_and_get_active_variant(0)?;
    let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);

    set_font_path_env("MYFONT_FONT_PATH", &font_path)?;

    let config = ExportConfig::<MyFontConfig>::with_variant(*entry);

    FontBuild::new(&font_path)
        .extra_hash(entry.as_str())
        .run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
        .map_err(|e| miette::miette!("{e}"))?;

    config.write_variant_info()?;
    Ok(())
}

Static Font (No Variants)

If the font has a single style, use ExportConfig::new():

use std::path::Path;

use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;

#[path = "src/definition.rs"]
mod definition;
use definition::MyFontConfig;

const FONT_FILE: &str = "MyFont-Regular.ttf";

fn main() -> miette::Result<()> {
    let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);

    set_font_path_env("MYFONT_FONT_PATH", &font_path)?;

    let config = ExportConfig::<MyFontConfig>::new();

    FontBuild::new(&font_path)
        .run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
        .map_err(|e| miette::miette!("{e}"))?;

    config.write_variant_info()?;
    Ok(())
}

Font with External Name Map

If the font lacks PostScript glyph names (e.g. SMuFL fonts), use export_glyphs_by_name_map with an external metadata file:

#![allow(unused)]
fn main() {
use fonts_rs_model::GlyphNameMap;

// Parse external metadata (e.g. SMuFL glyphnames.json)
let name_map: GlyphNameMap = serde_json::from_str(&json)?;

let config = ExportConfig::<MyFontConfig>::new();

FontBuild::new(&font_path)
    .run(|font_path, resources_dir| {
        config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
    })
    .map_err(|e| miette::miette!("{e}"))?;
}

Step 6: Write lib.rs

#![allow(unused)]
fn main() {
//! MyFont font integration for GTK 4.

pub mod codepoint_map;
pub mod constants;
pub mod naming;
pub mod variant;

#[cfg(feature = "render")]
pub mod fonts;

use fonts_rs_model::CodePoint;

pub use naming::MyFontName;

/// Type alias for this font family's glyph name type.
pub type FamilyName = MyFontName;

/// Extension trait adding codepoint lookup to `GlyphName<MyFont>`.
pub trait GlyphNameExt {
    fn codepoint(&self) -> Option<CodePoint>;
}

impl GlyphNameExt for MyFontName {
    fn codepoint(&self) -> Option<CodePoint> {
        let key = self.as_ref();
        if let Some(ch) = codepoint_map::REVERSE_GLYPHS.get(key).copied() {
            return Some(CodePoint::from(ch));
        }
        let full = format!("{}-{}", variant::GLYPH_PREFIX, key);
        codepoint_map::REVERSE_GLYPHS.get(&full).copied().map(CodePoint::from)
    }
}

/// Registers embedded glyphs as a GResource.
#[cfg(feature = "gtk")]
pub fn register_glyphs() -> Result<(), gio::glib::Error> {
    gio::resources_register_include!("icons.gresource")?;
    Ok(())
}

/// Returns an iterator over all known glyphs.
pub fn all_glyphs() -> impl Iterator<Item = (char, &'static str)> {
    codepoint_map::GLYPHS.entries().map(|(c, name)| (*c, *name))
}
}

Generated Module Stubs

Create these stub files that include! the generated code:

src/codepoint_map.rs:

#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/codemap.rs"));
}

src/constants.rs:

#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/icons.rs"));
}

src/variant.rs:

#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/variant.rs"));
}

src/fonts.rs (optional, for render feature):

#![allow(unused)]
fn main() {
fonts_rs_generator::impl_font_loader!(env: "MYFONT_FONT_PATH");
}

Step 7: Configure Cargo.toml

[package]
name = "fonts-rs-myfont"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
description = "MyFont font integration for GTK 4 and pixel-drawing"
publish = true

# Font license
license-file = "resources/OFL.txt"

include = [
    "src/**/*.rs",
    "build.rs",
    "resources/*.ttf",
    "resources/OFL.txt",
    "resources/icons.gresource.xml",
    "Cargo.toml",
]

[features]
default = ["gtk"]

# Variant features (if applicable)
regular = []
bold = []

gtk = ["dep:gtk4", "dep:gio"]
render = ["dep:ab_glyph"]
embed-fonts = []

[dependencies]
fonts-rs-generator.workspace = true
fonts-rs-model.workspace = true
gio = { workspace = true, optional = true }
gtk4 = { workspace = true, optional = true }
ab_glyph = { workspace = true, optional = true }
phf.workspace = true
tracing.workspace = true

[build-dependencies]
fonts-rs-generator.workspace = true
fonts-rs-model.workspace = true
const_format.workspace = true
miette.workspace = true

[lints]
workspace = true

Step 8: Update Workspace Cargo.toml

Add the new crate to the workspace members and dependencies:

[workspace]
members = [
    # ...
    "crates/fonts-rs-myfont",
]

[workspace.dependencies]
# ...
fonts-rs-myfont = { version = "0.1.0", path = "crates/fonts-rs-myfont" }

Step 9: Write Tests

Add tests to lib.rs to verify the export:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_glyphs_is_not_empty() {
        let count = all_glyphs().count();
        assert!(count > 0, "MyFont should export at least one glyph");
    }

    #[test]
    fn all_glyphs_names_start_with_family_prefix() {
        for (_, name) in all_glyphs() {
            assert!(
                name.starts_with("myfont-"),
                "glyph name '{name}' should start with 'myfont-'"
            );
        }
    }

    #[test]
    fn all_glyphs_names_are_lowercase() {
        for (_, name) in all_glyphs() {
            assert_eq!(*name, name.to_lowercase(), "glyph name '{name}' should be lowercase");
        }
    }

    #[test]
    fn all_glyphs_names_contain_no_underscores() {
        for (_, name) in all_glyphs() {
            assert!(!name.contains('_'), "glyph name '{name}' should not contain underscores");
        }
    }

    #[test]
    fn all_glyphs_codepoints_are_unique() {
        let codepoints: Vec<char> = all_glyphs().map(|(c, _)| c).collect();
        let unique: std::collections::HashSet<char> = codepoints.iter().copied().collect();
        assert_eq!(codepoints.len(), unique.len(), "all codepoints should be unique");
    }
}
}

Verification

Build the crate and run tests:

cargo build -p fonts-rs-myfont
cargo test -p fonts-rs-myfont
cargo clippy -p fonts-rs-myfont -- -D warnings
cargo fmt -p fonts-rs-myfont -- --check

Check the generated output:

ls resources/scalable/glyphs/*.svg | wc -l    # glyph count
cat resources/metadata.json | head -20         # metadata preview

Glyph Resolution

Each font family crate provides glyph name resolution - mapping human-readable glyph names to Unicode codepoints and vice versa.

Font Family Crates

Font family crates use GlyphName<F> (phantom-typed) for type-safe glyph name resolution. Each crate defines a GlyphNameExt trait with a codepoint() method:

#![allow(unused)]
fn main() {
use fonts_rs_doto::GlyphNameExt;
use fonts_rs_doto::DotoName;

// Look up a glyph by name
let name = DotoName::new("doto-a".to_string());
if let Some(codepoint) = name.codepoint() {
    println!("Glyph codepoint: U+{:04X}", codepoint as u32);
}
}

The codepoint() method looks up the glyph name in the generated phf::Map (REVERSE_GLYPHS), trying both the bare name and the prefixed name ({GLYPH_PREFIX}-{name}).

Iterating All Glyphs

#![allow(unused)]
fn main() {
use fonts_rs_doto::all_glyphs;

for (codepoint, name) in all_glyphs() {
    println!("U+{:04X} → {}", codepoint as u32, name);
}
}

GResource Path Resolution

Each glyph has a GResource path that can be used with GTK:

#![allow(unused)]
fn main() {
// GResource path format: {GRESOURCE_PREFIX}/scalable/{ICONS_CONTEXT}/{glyph_name}.svg
// e.g. /io/smearor/fonts/doto/scalable/glyphs/doto-a.svg
}

Nerd Fonts (nerd-fonts-rs)

Nerd Fonts use a different naming convention. Icon names follow the pattern nf-{prefix}-{name}, for example:

  • nf-fa-gamepad - Font Awesome gamepad icon
  • nf-md-cube - Material Design cube icon
  • nf-linux-tux - Linux Tux icon

The resolve_icon_codepoint function:

  1. Normalizes the input to kebab-case, lower-case
  2. Appends -symbolic suffix if not already present (GTK symbolic icon convention)
  3. Looks up the normalized name in the vendored codepoint map
  4. Returns the Unicode character if found

Usage

#![allow(unused)]
fn main() {
use nerd_fonts_rs::resolve_icon_codepoint;

// Basic resolution
let codepoint = resolve_icon_codepoint("nf-fa-gamepad");
assert_eq!(codepoint, Some('\u{F11B}'));

// Underscores are normalized to hyphens
let codepoint = resolve_icon_codepoint("nf_linux_tux");
assert_eq!(codepoint, Some('\u{F31A}'));

// Case-insensitive
let codepoint = resolve_icon_codepoint("NF-FA-GAMEPAD");
assert_eq!(codepoint, Some('\u{F11B}'));

// Already has -symbolic suffix
let codepoint = resolve_icon_codepoint("nf-fa-gamepad-symbolic");
assert_eq!(codepoint, Some('\u{F11B}'));

// Unknown icon returns None
let codepoint = resolve_icon_codepoint("nf-nonexistent-icon-xyz");
assert_eq!(codepoint, None);
}

GTK Icon Name Resolution

The gtk module provides resolve_gtk_nerd_icon which converts CSS class names (like nf-fa-gamepad or fa-gamepad) into GTK icon names that gtk4::Image::from_icon_name understands.

The vendored icon GResource registers SVG icons under the path /io/smearor/nerd_fonts/icons/. Each icon follows the naming pattern nf-{prefix}-{name}-symbolic (kebab-case, lower-case).

#![allow(unused)]
fn main() {
use nerd_fonts_rs::gtk::resolve_gtk_nerd_icon;

let icon_name = resolve_gtk_nerd_icon("nf-fa-gamepad");
// Returns "nf-fa-gamepad-symbolic"
}

Glyph Export

Glyphs are extracted as GTK4 symbolic SVG icons at build time using skrifa for outline parsing. The export logic lives in the fonts-rs-generator crate and is called automatically by each font family crate’s build.rs.

Automatic Generation

build.rs generates the icon resources on demand via FontBuild. The export is triggered when the font file hash has changed since the last export:

flowchart TD
    A["build.rs runs"] --> B{"resources/.hash exists?"}
    B -->|No| D["Export glyphs from font"]
    B -->|Yes| C{"Font hash matches?"}
    C -->|No| D
    C -->|Yes| E["Skip export"]
    D --> F["Write SVGs, metadata.json, icons.gresource.xml"]
    F --> G["Write .hash"]
    E --> H["Compile GResource bundles"]
    G --> H
    H --> I["Generate phf::Map + icon constants"]
resources/{font}.ttf
  ↓  build.rs (skrifa outline extraction, via ExportConfig)
resources/scalable/glyphs/*.svg   (SVG files)
resources/metadata.json           (name, codepoint, file path)
resources/icons.gresource.xml     (GResource manifest)
resources/.hash                   (FNV-1a hash of font file)
  ↓  build.rs (code generation via FontBuild)
$OUT_DIR/codemap.rs               (phf::Map<char, &str> + phf::Map<&str, char>)
$OUT_DIR/icons.rs                 (icon name constants)
$OUT_DIR/variant.rs               (GRESOURCE_PREFIX + GLYPH_PREFIX)
  ↓  build.rs (glib-build-tools)
icons.gresource                   (icon GResource bundle)

The generated files (scalable/, metadata.json, icons.gresource.xml, .hash) are in .gitignore and not checked into the repository. They are regenerated from the font file as needed.

Caching

FontBuild uses two mechanisms for caching:

Cargo rebuild triggers (cargo:rerun-if-changed):

  • The font file - triggers build.rs re-execution
  • resources/metadata.json - triggers phf::Map regeneration
  • resources/icons.gresource.xml - triggers GResource recompilation
  • build.rs - triggers full rebuild

Font hash comparison (FNV-1a): When build.rs runs, it computes an FNV-1a hash of the font file and compares it against the stored hash in resources/.hash. If the hashes differ (or the file is missing), the full glyph export is triggered. Otherwise the export is skipped.

For variable font variants, extra_hash() adds the variant name to the hash, forcing re-export when the active variant changes even if the font file is unchanged.

This means:

  • Fresh clone: hash missing → export runs
  • Font updated: hash differs → export runs automatically
  • No changes: hash matches → export skipped (fast incremental build)
  • Variant changed: extra hash differs → export runs

No manual rm -rf or cargo clean is needed when updating the font file.

Two Export Methods

ExportConfig::export_glyphs (struct method)

Used by most font family crates. Normalizes glyph names to {glyph_name_prefix}-{kebab} and renders SVGs with the configured axis location. Supports name_filter for custom glyph filtering.

#![allow(unused)]
fn main() {
let config = ExportConfig::<DotoConfig>::with_variant(*entry);

FontBuild::new(&font_path)
    .extra_hash(entry.as_str())
    .run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
    .map_err(|e| miette::miette!("{e}"))?;
}

ExportConfig::export_glyphs_by_name_map (struct method)

Used by fonts without PostScript glyph names (e.g. SMuFL fonts with post table version 3.0). Glyph names come from an external metadata file parsed into a GlyphNameMap.

#![allow(unused)]
fn main() {
let name_map: GlyphNameMap = serde_json::from_str(&glyphnames_json)?;
let config = ExportConfig::<BravuraConfig>::new();

FontBuild::new(&font_path)
    .run(|font_path, resources_dir| {
        config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
    })
    .map_err(|e| miette::miette!("{e}"))?;
}

FontDefinition::export_glyphs (trait default method)

Used by font families with semantic PostScript glyph names (e.g. Barcode, Seven-Segment). Uses Self::normalize_name for type-safe glyph names and renders SVGs at the default axis location.

How It Works

Glyph Outline Extraction

The export uses skrifa to parse font outlines and convert Bezier curve commands directly into SVG path data:

OutlineBuilder methodSVG path command
move_to(x, y)M x y
line_to(x, y)L x y
quad_to(x1, y1, x, y)Q x1 y1 x y
curve_to(x1, y1, x2, y2, x, y)C x1 y1 x2 y2 x y
close()Z

Both TrueType (glyf) and OpenType/CFF outlines are supported transparently by skrifa.

Codepoint Resolution

The export builds a reverse character map (GlyphId → char) by probing codepoints in the ranges defined by FontFamilyConfig::CODEPOINT_RANGES:

  • BMP_RANGE - U+0000U+FFFF (default, most fonts)
  • ASCII_PRINTABLE_RANGE - U+0020U+007E (barcode, segment fonts)
  • PUA_RANGE - U+E000U+F8FF (icon fonts, SMuFL)
  • SUPPLEMENTARY_PUA_RANGE - U+F0001U+10FFFF (Nerd Fonts)

Name Normalization

Glyph names from the font’s post/CFF tables are normalized to GTK-friendly icon names:

  1. Lowercase
  2. Replace _ with -
  3. Replace non-alphanumeric characters (except -) with -
  4. Collapse consecutive -
  5. Strip leading/trailing -
  6. Prefix with {glyph_name_prefix}- (e.g. doto-, dseg7-)

For Nerd Fonts, the prefix is nf- and the suffix -symbolic is added.

Empty Glyphs

Some glyphs (e.g. nonmarkingreturn, blank) have no outline data. These are exported as minimal empty SVGs so their icon names remain registered in metadata.json.

Crate Structure

The export logic lives in the fonts-rs-generator crate:

# Font family crate's Cargo.toml (build-dependency)
[build-dependencies]
fonts-rs-generator.workspace = true
fonts-rs-model.workspace = true
miette.workspace = true

See Build Patterns for complete build.rs examples.

Font Loading

The fonts module (enabled with the render feature) handles loading font files for use with pixel-drawing or other software rendering via ab_glyph.

impl_font_loader! Macro

Each font family crate uses the impl_font_loader! macro from fonts-rs-generator to generate a cached font loading function:

#![allow(unused)]
fn main() {
// Literal: relative to resources/
fonts_rs_generator::impl_font_loader!("Doto.ttf");

// Env var: absolute path set by build.rs via cargo:rustc-env
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}

The macro generates a font() -> Option<&'static ab_glyph::FontVec> function that loads the TTF font file and caches it in a OnceLock. Subsequent calls return a reference to the cached FontVec.

Loading Modes

Embedded Fonts (embed-fonts feature)

Font files are compiled into the binary via include_bytes!:

#![allow(unused)]
fn main() {
// No runtime file access needed
fonts_rs_doto::fonts::font(); // Returns Option<&'static FontVec>
}

From Disk (default)

Fonts are loaded from a path set by build.rs via cargo:rustc-env. The impl_font_loader!(env: ...) variant reads the path from the environment variable at runtime.

Caching

Fonts are loaded once and cached in a OnceLock. Subsequent calls return a reference to the cached FontVec:

#![allow(unused)]
fn main() {
use fonts_rs_doto::fonts::font;

let font1 = font().unwrap();
let font2 = font().unwrap();
assert!(core::ptr::eq(font1 as *const _, font2 as *const _));
}

Nerd Fonts (nerd-fonts-rs)

The nerd-fonts-rs crate provides additional font loading for Nerd Fonts specifically:

  1. Nerd Font (SymbolsNerdFont-Regular.ttf) - TTF file for rendering Nerd Font icons
  2. Label Font (JetBrainsMonoNLNerdFont-Regular.woff2) - WOFF2 file decompressed to TTF at load time for text labels

The label font is stored as WOFF2. The convert_woff2 function decompresses it to TTF using the woff2-patched crate. If decompression fails, it falls back to parsing the data directly as TTF.

Initialization

For nerd-fonts-rs, call init() once at application startup:

#![allow(unused)]
fn main() {
// Use default base directory (relative to executable)
nerd_fonts_rs::init(None);

// Or specify a custom base directory
nerd_fonts_rs::init(Some("/usr/share/fonts/nerd-fonts"));
}

CSS Generation

The css module provides CSS string constants for GTK and web usage.

GTK Font-Face CSS

The font_face_css() function generates a version-adapted @font-face CSS string at runtime. It detects the GTK4 library version and adjusts the src descriptor syntax for compatibility:

  • GTK >= 4.10: emits src: url("...") format("truetype") for better spec compliance with the new GtkCssParser.
  • GTK < 4.10: emits src: url("...") without a format() hint to avoid parse errors with the legacy parser.

It is loaded automatically by init() when the gtk feature is enabled.

#![allow(unused)]
fn main() {
use nerd_fonts_rs::css::font_face_css;

let css = font_face_css();
// The CSS references GResource URLs:
// resource:///io/smearor/nerd_fonts/SymbolsNerdFont-Regular.ttf
// resource:///io/smearor/nerd_fonts/SymbolsNerdFontMono-Regular.ttf
}

The CSS defines:

  • @font-face for NerdFontsSymbolsOnly (proportional)
  • @font-face for NerdFontsSymbolsOnlyMono (monospace)
  • .nerd-icon class for proportional icons
  • .nerd-icon-mono class for monospace icons

The static FONT_FACE_CSS constant retains the baseline CSS (without format() hints) for backward compatibility and testing.

Runtime Version Detection

The GtkVersion struct wraps the runtime GTK4 version and provides comparison helpers:

#![allow(unused)]
fn main() {
use nerd_fonts_rs::css::GtkVersion;

#[cfg(feature = "gtk")]
let version = GtkVersion::runtime();

let legacy = GtkVersion::new(4, 8, 0);
assert!(!legacy.at_least(4, 10));

let modern = GtkVersion::new(4, 14, 2);
assert!(modern.at_least(4, 10));
}

GResource Prefix

The GRESOURCE_PREFIX constant defines where font files are registered in the GResource system:

#![allow(unused)]
fn main() {
use nerd_fonts_rs::css::GRESOURCE_PREFIX;

assert_eq!(GRESOURCE_PREFIX, "/io/smearor/nerd_fonts");
}

Manual CSS Loading

If you need to load the CSS manually (e.g., in a custom initialization flow):

#![allow(unused)]
fn main() {
use gtk4::CssProvider;

let provider = gtk4::CssProvider::new();
let css = nerd_fonts_rs::css::font_face_css();

#[cfg(feature = "v4_12")]
provider.load_from_string(&css);
#[cfg(not(feature = "v4_12"))]
{
    #[allow(deprecated)]
    provider.load_from_data(&css);
}

if let Some(display) = gtk4::gdk::Display::default() {
    gtk4::style_context_add_provider_for_display(
        &display,
        &provider,
        gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
    );
}
}

GTK Integration

Font Family Crates

Each font family crate provides a register_glyphs() function (enabled with the gtk feature) that registers the compiled GResource bundle:

use fonts_rs_doto::register_glyphs;

fn main() {
    // Register GResource (call once at startup)
    register_glyphs().unwrap();

    // Now GTK can resolve icon names from the GResource bundle
    let image = gtk4::Image::from_icon_name("doto-a");
}

register_glyphs() calls gio::resources_register_include!("icons.gresource") to register the compiled GResource binary containing all SVG glyph files.

Nerd Fonts (nerd-fonts-rs)

The nerd-fonts-rs crate provides additional GTK4 functionality:

Initialization

Call init() once at application startup:

use nerd_fonts_rs::init;

fn main() {
    init(None);
    // ...
}

init() performs the following:

  1. Font initialization (with render feature) - Sets the font search base directory
  2. GResource registration - Registers the compiled GResource binary
  3. Icon registration - Registers vendored Nerd Font SVG icons
  4. CSS loading - Loads version-adapted @font-face CSS via font_face_css() into a CssProvider and adds it to the default display

GTK Icon Name Resolution

#![allow(unused)]
fn main() {
use nerd_fonts_rs::gtk::resolve_gtk_nerd_icon;

let icon_name = resolve_gtk_nerd_icon("nf-fa-gamepad");
// Returns "nf-fa-gamepad-symbolic"

let image = gtk4::Image::from_icon_name(&icon_name.unwrap());
}

The function normalizes CSS class names (like nf-fa-gamepad or fa-gamepad) into GTK icon names that gtk4::Image::from_icon_name understands.

Web CSS

The web module (enabled with the web feature) provides a CSS constant for web-based widget rendering.

WEB_NERDFONT_CSS

The WEB_NERDFONT_CSS constant contains a complete CSS stylesheet with:

  • @font-face rules for loading Nerd Font symbol fonts
  • Per-icon content: "\XXXX" mappings for use in web widgets
#![allow(unused)]
fn main() {
use nerd_fonts_rs::web::WEB_NERDFONT_CSS;

// Include in your web HTML
let html = format!(
    "<style>{}</style><span class='nf-fa-gamepad'></span>",
    WEB_NERDFONT_CSS
);
}

Usage with Web Instances

The web CSS is designed for web-based widget rendering systems that display Nerd Font icons in HTML pages. Each icon class maps to its Unicode codepoint via the CSS content property.

Example HTML

<link rel="stylesheet" href="nerdfont.css">
<span class="nerd-icon nf-fa-gamepad"></span>

The CSS is generated from the SymbolsNerdFont-Regular.ttf glyph table and includes mappings for all Nerd Font symbols.

Platform Notes

Linux

Linux is the primary target for the gtk feature.

System Dependencies

# Ubuntu/Debian
sudo apt-get install -y pkg-config libgtk-4-dev libglib2.0-dev

# Fedora
sudo dnf install -y pkg-config gtk4-devel glib2-devel

# Arch Linux
sudo pacman -S pkgconf gtk4 glib2

GResource Compilation

The build.rs script uses glib_build_tools::compile_resources to compile the GResource XML into a binary. This requires glib-build-tools as a build dependency and the GTK 4 development headers to be installed.

macOS

The gtk feature can be used on macOS with GTK 4 installed via Homebrew:

brew install gtk4

No additional configuration is needed beyond the standard Rust build process.

Windows

The gtk feature can be used on Windows with GTK 4 installed via MSYS2 or gvsbuild. See the GTK4 Windows installation guide for details.

Font Loading Mode

The render feature enables font loading (TTF/WOFF2) via ab_glyph. It has no platform-specific dependencies and works on any platform supported by Rust.

[dependencies]
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["render"] }

Web-Only Mode

The web feature has no platform-specific dependencies. It simply provides a CSS string constant.

[dependencies]
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["web"] }

Embedded Fonts

The embed-fonts feature compiles font files directly into the binary. This increases binary size by approximately 200-500 KB but eliminates the need for runtime file access. This is useful for:

  • Standalone binaries
  • Environments without a filesystem
  • Simplified deployment