- Font format support -- TTF, OTF, and WOFF input
- BMFont output -- text, XML, and binary
.fntformats with.pngatlas pages - GPOS kerning -- extracts kerning pairs directly from OpenType GPOS tables
- Atlas packing -- MaxRects (default) and Skyline algorithms with autofit, power-of-two, and non-square texture support
- Outline rendering -- configurable width and color
- Gradient fill -- per-glyph vertical/angled gradients with midpoint control
- Drop shadow -- offset, blur radius, color, and opacity
- SDF rendering -- signed distance field output for resolution-independent text
- Super sampling -- 2x-4x rasterization with box-filter downscale for smoother edges
- Variable fonts -- set variation axes (weight, width, slant, etc.)
- Color fonts -- COLR/CPAL emoji and color glyph rendering with palette selection
- Channel packing -- pack multiple glyphs into RGBA channels for compact atlases
- Per-channel compositing -- independent control of what each RGBA channel contains (glyph, outline, both, zero, one)
- Font subsetting -- only parses tables for requested codepoints
- Custom glyphs -- replace or add glyphs with user-supplied images
- Texture formats -- PNG (default), TGA, and DDS atlas output
- Reading BMFont files -- load and parse existing
.fntfiles (auto-detects text/XML/binary) - Fluent builder API -- chainable configuration as an alternative to options objects
- System font loading -- generate from installed fonts by family name, with a font registry for platforms without system font access
- Fully in-memory -- entire pipeline runs without touching disk unless you call
ToFile() - Batch generation -- parallel multi-font generation with font caching
- Pipeline metrics -- stage-level timing breakdown for profiling
- Cross-platform -- Windows, Linux, macOS via .NET 10.0
- Pluggable rasterizers -- swap rendering backends to match your platform and feature needs
- Hardened font parsing -- bounds validation and resource limits for untrusted font files
KernSmith supports pluggable rasterizer backends. Install at least one backend package to generate fonts.
| Backend | Package | Platform | Notes |
|---|---|---|---|
| FreeType | KernSmith.Rasterizers.FreeType |
Windows, Linux, macOS | Cross-platform, full feature support. |
| GDI | KernSmith.Rasterizers.Gdi |
Windows only | Matches BMFont reference output for pixel-perfect parity. |
| DirectWrite | KernSmith.Rasterizers.DirectWrite.TerraFX |
Windows only | Color font (COLR/CPAL) and variable font rendering via DirectWrite. |
| StbTrueType | KernSmith.Rasterizers.StbTrueType |
Cross-platform | Pure C#, no native dependencies. Ideal for WASM, AOT, serverless. |
Install a backend:
dotnet add package KernSmith.Rasterizers.FreeType
dotnet add package KernSmith.Rasterizers.Gdi
dotnet add package KernSmith.Rasterizers.DirectWrite.TerraFX
dotnet add package KernSmith.Rasterizers.StbTrueType
Select a backend when generating:
var result = BmFont.Builder()
.WithFont("font.ttf")
.WithSize(32)
.WithRasterizer("gdi")
.Build();KernSmith runs entirely client-side in Blazor WebAssembly using the StbTrueType backend. See the Blazor WASM sample for a working example.
// Generate in-browser — StbTrueType is auto-discovered
var result = BmFont.Generate(fontBytes, new FontGeneratorOptions
{
Size = 32,
Characters = CharacterSet.Ascii,
Backend = RasterizerBackend.StbTrueType
});
string fntText = result.FntText;
byte[] pngData = result.GetPngData(0);AOT compilation (RunAOTCompilation=true) is recommended for production performance.
dotnet add package KernSmith
Or via the NuGet Package Manager:
Install-Package KernSmith
Generate a bitmap font from a .bmfc config file in one call:
using KernSmith;
var result = BmFont.FromConfig("myfont.bmfc");
result.ToFile("output/myfont");Generate from a TTF file using FontGeneratorOptions:
var result = BmFont.Generate("path/to/font.ttf", new FontGeneratorOptions
{
Size = 32,
Characters = CharacterSet.Ascii
});
// Write .fnt + .png + .bmfc files to disk
result.ToFile("output/myfont");For the simplest case, just pass a size:
var result = BmFont.Generate("path/to/font.ttf", 48);You can also pass raw font bytes:
byte[] fontData = File.ReadAllBytes("path/to/font.ttf");
var result = BmFont.Generate(fontData, new FontGeneratorOptions
{
Size = 24,
Characters = CharacterSet.ExtendedAscii,
Kerning = true
});The BmFont.Builder() API provides a chainable alternative:
var result = BmFont.Builder()
.WithFont("path/to/font.ttf")
.WithSize(32)
.WithCharacters(CharacterSet.Ascii)
.WithPadding(1)
.WithSpacing(1, 1)
.WithKerning()
.Build();
result.ToFile("output/myfont");var result = BmFont.Builder()
.WithSystemFont("Arial")
.WithSize(32)
.WithBold() // Uses native bold face, falls back to synthetic
.WithItalic() // Uses native italic face, falls back to synthetic
.Build();
// Force synthetic styling (skip native face lookup)
var result2 = BmFont.Builder()
.WithSystemFont("Arial")
.WithSize(32)
.WithForceSyntheticBold() // Always applies synthetic bold
.WithForceSyntheticItalic() // Always applies synthetic italic
.Build();WithBold()/WithItalic()use the native bold/italic face when available (system fonts), falling back to syntheticWithForceSyntheticBold()/WithForceSyntheticItalic()always apply synthetic styling, skipping native face lookup- When using a file path (not a system font), bold/italic is always synthetic --
WithBold()andWithForceSyntheticBold()produce identical results - For native vs synthetic distinction, use
WithSystemFont()so the font family can be searched for a matching face
For backends without native bold/italic support, use bitmap-level post-processors:
var result = BmFont.Builder()
.WithFont("font.ttf")
.WithSize(32)
.WithPostProcessor(new BoldPostProcessor(strength: 2))
.WithPostProcessor(new ItalicPostProcessor(shear: 0.2f))
.Build();You can also start the builder from a .bmfc config and override individual settings:
var result = BmFont.Builder()
.FromConfig("base.bmfc")
.WithSize(48)
.Build();Generate from a system-installed font by family name:
var result = BmFont.GenerateFromSystem("Arial", new FontGeneratorOptions
{
Size = 36,
Characters = CharacterSet.Ascii
});Or with the builder:
var result = BmFont.Builder()
.WithSystemFont("Arial")
.WithSize(36)
.WithCharacters(CharacterSet.Latin)
.Build();On platforms without system font access (Blazor WASM, mobile, containers), GenerateFromSystem() cannot find installed fonts. Register raw font data to make it available by family name:
byte[] arialData = File.ReadAllBytes("fonts/Arial.ttf");
BmFont.RegisterFont("Arial", arialData);
// Now GenerateFromSystem works with the registered font
var result = BmFont.GenerateFromSystem("Arial", new FontGeneratorOptions
{
Size = 32,
Characters = CharacterSet.Ascii
});Register style variants separately:
BmFont.RegisterFont("Arial", arialRegularData);
BmFont.RegisterFont("Arial", arialBoldData, style: "Bold");
BmFont.RegisterFont("Arial", arialItalicData, style: "Italic");
BmFont.RegisterFont("Arial", arialBoldItalicData, style: "Bold Italic");Registered fonts take priority over system fonts. If a registered font is not found, GenerateFromSystem() falls back to system font lookup automatically. The Bold/Italic/Bold Italic cascade is also applied -- requesting bold will check for a registered "Bold" style before falling back.
Remove registrations when no longer needed:
BmFont.UnregisterFont("Arial"); // Remove default style
BmFont.UnregisterFont("Arial", style: "Bold"); // Remove specific style
BmFont.ClearRegisteredFonts(); // Remove all registrationsRegistering fonts explicitly also ensures cross-platform consistency -- the same fonts render identically everywhere regardless of what the OS has installed.
Generate multiple fonts in parallel with shared font caching:
// Batch generate multiple fonts with parallel execution
var jobs = new List<BatchJob>
{
new BatchJob { SystemFont = "Arial", Options = new FontGeneratorOptions { Size = 32 } },
new BatchJob { SystemFont = "Arial", Options = new FontGeneratorOptions { Size = 48, Bold = true } },
new BatchJob { FontPath = "custom.ttf", Options = new FontGeneratorOptions { Size = 24 } },
};
var result = BmFont.GenerateBatch(jobs, new BatchOptions { MaxParallelism = 4 });
foreach (var job in result.Results)
{
if (job.Success)
job.Result!.ToFile($"output/font-{job.Index}");
}Pre-load fonts for reuse across multiple generations:
// Pre-load fonts for reuse across multiple generations
var cache = new FontCache();
cache.LoadSystemFont("Arial");
cache.LoadFile("custom.ttf");
var result = BmFont.GenerateBatch(jobs, new BatchOptions
{
FontCache = cache,
MaxParallelism = 4
});Profile pipeline stages to identify bottlenecks:
// Profile pipeline stages
var result = BmFont.Generate(fontData, new FontGeneratorOptions
{
Size = 32,
CollectMetrics = true
});
Console.WriteLine(result.Metrics); // Prints stage-level timing breakdownSeveral presets are available, and you can define custom sets:
// Built-in presets
CharacterSet.Ascii // U+0020..U+007E (95 printable ASCII characters)
CharacterSet.ExtendedAscii // U+0020..U+00FF (includes accented Latin characters)
CharacterSet.Latin // ASCII + Latin Extended-A + Latin Extended-B
// From a string of characters
var custom = CharacterSet.FromChars("ABCDabcd0123!@#$");
// From Unicode ranges
var cyrillic = CharacterSet.FromRanges((0x0400, 0x04FF));
// Combine multiple sets
var combined = CharacterSet.Union(CharacterSet.Ascii, cyrillic);var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 48,
Characters = CharacterSet.Ascii,
Outline = 2 // 2-pixel black outline
});With a colored outline:
var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 48,
Characters = CharacterSet.Ascii,
Outline = 3,
OutlineR = 255, // Red outline
OutlineG = 0,
OutlineB = 0
});var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 48,
Characters = CharacterSet.Ascii,
GradientStartR = 255, GradientStartG = 255, GradientStartB = 0, // Yellow top
GradientEndR = 255, GradientEndG = 0, GradientEndB = 0, // Red bottom
GradientAngle = 90f, // Top-to-bottom (default)
GradientMidpoint = 0.5f
});var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 48,
Characters = CharacterSet.Ascii,
ShadowOffsetX = 2,
ShadowOffsetY = 2,
ShadowBlur = 3,
ShadowR = 0, ShadowG = 0, ShadowB = 0, // Black shadow
ShadowOpacity = 0.8f
});For a crisp, uniform silhouette instead of soft antialiased edges, enable hard shadow:
var result = BmFont.Builder()
.WithFont("font.ttf")
.WithSize(48)
.WithCharacters(CharacterSet.Ascii)
.WithShadow(2, 2, 0)
.WithHardShadow()
.Build();All effects can be combined. They are composited in fixed order: shadow (back), outline (middle), gradient (front).
var result = BmFont.Builder()
.WithFont("font.ttf")
.WithSize(64)
.WithCharacters(CharacterSet.Ascii)
.WithOutline(2, 0, 0, 0)
.WithGradient((255, 200, 0), (255, 50, 0), angleDegrees: 90f)
.WithShadow(offsetX: 3, offsetY: 3, blur: 4, color: (0, 0, 0), opacity: 0.6f)
.Build();// Text format (default)
result.ToFile("output/myfont");
// XML format
result.ToFile("output/myfont", OutputFormat.Xml);
// Binary format
result.ToFile("output/myfont", OutputFormat.Binary);ToFile writes the .fnt descriptor, all .png atlas pages, and a .bmfc config file (when source options are available).
Convenience properties give you the .fnt content without calling a formatter directly:
// BMFont text format
string fntText = result.FntText;
// BMFont XML format
string fntXml = result.FntXml;
// BMFont binary format
byte[] fntBinary = result.FntBinary;
// Encode atlas pages to PNG/TGA/DDS byte arrays
byte[][] pngFiles = result.GetPngData();
byte[] firstPng = result.GetPngData(0);
byte[][] tgaFiles = result.GetTgaData();
byte[][] ddsFiles = result.GetDdsData();
// Round-trip: export the config that produced this result
string bmfcText = result.ToBmfc();The older ToString(), ToXml(), and ToBinary() methods still work and return the same data.
Generate a bitmap font and load it straight into your engine without writing temp files:
var result = BmFont.FromConfig("ui-font.bmfc");
// Feed these to your engine's SpriteFont or BMFont loader
string fntText = result.FntText;
byte[] pngBytes = result.GetPngData(0);You can also access the raw RGBA pixel data on each atlas page:
foreach (var page in result.Pages)
{
byte[] rgba = page.PixelData;
int width = page.Width;
int height = page.Height;
// Encode individual pages to other formats
byte[] tga = page.ToTga();
byte[] dds = page.ToDds();
}Use BmfcConfigReader and BmfcConfigWriter to work with .bmfc configuration files programmatically:
using KernSmith;
// Read a .bmfc config file
BmfcConfig config = BmfcConfigReader.Read("path/to/font.bmfc");
// Or parse from a string
BmfcConfig config2 = BmfcConfigReader.Parse(bmfcContent);
// Modify and write back
BmfcConfigWriter.WriteToFile(config, "path/to/output.bmfc");
// Or serialize to a string
string bmfcText = BmfcConfigWriter.Write(config);Load an existing .fnt file (auto-detects text, XML, or binary format):
// Load .fnt and associated .png atlas pages from disk
BmFontResult loaded = BmFont.Load("path/to/myfont.fnt");
// Access the model
var charCount = loaded.Model.Characters.Count;
var kerningCount = loaded.Model.KerningPairs.Count;
// Load just the model without atlas images
var model = BmFont.LoadModel(File.ReadAllBytes("myfont.fnt"));
// Or from a text-format string
var model2 = BmFont.LoadModel(fntTextContent);A reference command-line tool is included in tools/KernSmith.Cli/. See the CLI README for usage.
Available commands: generate, init, batch, benchmark, inspect, convert, list-fonts, list-rasterizers, info.
The init command generates a .bmfc config file from CLI flags without rendering a font, so you can scaffold a config and tweak it by hand.
Use --time to display elapsed time or --profile to show a full pipeline stage breakdown.
var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 48,
Characters = CharacterSet.Ascii,
Sdf = true
});Note: SDF cannot be combined with super sampling (SuperSampleLevel > 1).
var result = BmFont.Builder()
.WithFont("variable-font.ttf")
.WithSize(32)
.WithVariationAxis("wght", 700) // Bold weight
.WithVariationAxis("wdth", 75) // Condensed width
.Build();var result = BmFont.Generate("color-emoji.ttf", new FontGeneratorOptions
{
Size = 64,
ColorFont = true,
ColorPaletteIndex = 0 // CPAL palette index
});Pack glyphs into individual RGBA channels for 4x atlas density:
var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 16,
Characters = CharacterSet.Ascii,
ChannelPacking = true
});Channel packing cannot be combined with color font rendering.
By default, only the TTF tables needed for your requested codepoints are fully parsed, keeping memory usage low for large CJK or Unicode fonts.
Automatically find the smallest power-of-two texture size that fits all glyphs on a single page:
var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 24,
Characters = CharacterSet.Ascii,
AutofitTexture = true
});Rasterize at 2x-4x resolution and downscale for smoother edges:
var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 32,
Characters = CharacterSet.Ascii,
SuperSampleLevel = 2
});var result = BmFont.Generate("font.ttf", new FontGeneratorOptions
{
Size = 32,
TextureFormat = TextureFormat.Tga // Also: TextureFormat.Png, TextureFormat.Dds
});See COMPARISON.md for a detailed feature comparison with BMFont, Hiero, msdf-atlas-gen, and other bitmap font generators.
MIT. See LICENSE for details.
