Fonts 3.1.0 is out. You can download it today.
Fonts 3.1.0 opens the shaping engine as a public API and adds a full hinting mode that grid-fits both TrueType and CFF outlines. It synthesizes bold and italic styles when a face lacks them, and matches fallback fonts through the operating system. It also shapes complex scripts with more coverage, more correctness, and more speed.
What's new since 3.0.0 #
The main changes since 3.0.0 are:
- A public text shaping API:
TextShaper,TextShapingBuffer, andShapedGlyph, plus glyph-id measurement and rendering. HintingMode.Full, with complete bidirectional TrueType instruction execution and declared-hint grid fitting for CFF outlines.- A
FontWeightAPI with variable-font instancing, synthetic bold, and synthetic oblique fallbacks. - Native OS font matching through DirectWrite, CoreText, and Fontconfig via
SystemFonts.TryMatchCharacter(...). - Resolver-based fallback: shaping consults
TextOptions.FontFallbackResolverfor code points no configured font can shape. - Skip-ink text decorations: underlines and overlines break around glyph ink.
- Color font palette selection with per-entry overrides, and correct COLR v1 composite group rendering.
- Shaping upgrades: a Khmer shaper, kern format 2, Arabic fallback shaping, correct automatic fractions, and refined line layout.
- Visible-bounds culling, baseline anchoring, x-height and cap-height metrics, and memory-backed font loading.
- Zero-allocation steady-state shaping, benchmarked head to head against HarfBuzz.
A public text shaper #
Fonts has always shaped text internally. 3.1 makes the shaper a first-class API for callers that build their own line layout, cache shaped runs, or feed glyphs to an external renderer.
You fill a reusable, caller-owned TextShapingBuffer, shape it against a font, and read back glyph IDs, advances, offsets, and source indexes. Shape(...) handles logical lines with bidirectional resolution. ShapeRun(...) shapes one directional run you have already selected.
using SixLabors.Fonts;
Font font = SystemFonts.CreateFont("Segoe UI", 16);
TextShapingBuffer buffer = new()
{
TextDirection = TextDirection.Auto
};
buffer.Add("Hello world");
TextShaper.Shape(font, buffer);
float penX = 0;
foreach (ShapedGlyph glyph in buffer.Glyphs)
{
// Draw the glyph at penX + glyph.Offset.X, then advance the pen.
penX += glyph.AdvanceWidth;
}
The other half of the contract is glyph-id measurement and rendering. TextMeasurer and TextRenderer accept a shaped buffer, or raw glyph IDs with explicit positions, through the new GlyphOptions type. ImageSharp.Drawing 3.1 accepts the same positioned glyph spans, so shaped output draws directly into images.
Shaping into a caller-owned buffer also anchors a performance push. The steady-state shaping path allocates zero managed bytes in every benchmarked script, enforced by allocation tests. Feature registrations are collected once per plan, and Unicode normalization lookups were rebuilt.
Managed code races a native engine here. The benchmark shapes the same run and font bytes through SixLabors.Fonts and HarfBuzzSharp, reusing one buffer per side. The ratio is SixLabors.Fonts time divided by HarfBuzz time, so lower is better. On .NET 10, Fonts wins three of the ten scripts outright and runs within 12% in another four:
| Scenario | SixLabors.Fonts | HarfBuzz | Ratio |
|---|---|---|---|
| Latin | 2.6744 μs | 2.2843 μs | 1.17x |
| Arabic | 4.9651 μs | 3.9115 μs | 1.27x |
| Hebrew | 4.5843 μs | 4.6725 μs | 0.98x |
| Thai | 4.2530 μs | 3.9104 μs | 1.09x |
| Hangul | 0.9258 μs | 0.5645 μs | 1.64x |
| Devanagari | 7.7347 μs | 9.3177 μs | 0.83x |
| Khmer | 4.1790 μs | 3.7335 μs | 1.12x |
| Myanmar | 25.2861 μs | 22.9402 μs | 1.10x |
| Myanmar Zawgyi | 0.7797 μs | 0.8248 μs | 0.95x |
| Balinese | 2.6941 μs | 3.0572 μs | 0.88x |
Every scenario measured zero bytes allocated per shape, on both sides. HarfBuzz reaches that through pooled buffers. Fonts reaches it the same way, with a reusable caller-owned buffer and pooled scratch state. The .NET 8 results and the measurement configuration are in the pull request.
Full hinting, now including CFF #
3.0 shipped FreeType v40-compatible TrueType hinting: vertical instruction processing with smooth horizontal placement. 3.1 adds HintingMode.Full, which reproduces the classic bi-level rasterizer pipeline that font hints were originally authored for.
Under full hinting, Fonts executes the complete horizontal and vertical TrueType instruction set and rounds the em size to whole pixels. It aligns glyph origins to the pixel grid on both axes. Flow-axis advances become whole-pixel values, taken from the font's hdmx device records where they exist. Measurement uses the same advances as rendering, so layout and raster output agree exactly.
CFF outlines are hinted for the first time. Fonts collects the stem hints, hint masks, and counter masks declared in each charstring, plus the alignment zones and stem widths from the Private DICT. It then fits the outline through a hint map. Standard hinting fits the vertical axis only. Full hinting fits both axes.
using SixLabors.Fonts;
Font font = SystemFonts.CreateFont("Segoe UI", 11);
TextOptions options = new(font)
{
Dpi = 96,
HintingMode = HintingMode.Full
};
Full hinting is the sharpest choice for small UI text. Standard remains the right default for smooth, fractional spacing.
Weight and style synthesis #
TextOptions.FontWeight and TextRun.FontWeight request a weight on the OpenType and CSS scale, from Thin (100) to Black (900). A variable font with a wght axis is instanced at the requested weight, preserving every other configured axis. A static face that cannot satisfy a bold request is thickened with a synthetic bold operation, using the CSS weight boundary of 600. When a run requests italic and the resolved face has none, glyph outlines are sheared to a synthetic oblique.
Color glyphs are excluded from synthesis, because thickening independently colored layers distorts them. The synthetic bold and oblique work came from community contributions, and we are grateful for them.
Native font matching #
SystemFonts.TryMatchCharacter(...) asks the operating system which installed font covers a code point, through DirectWrite on Windows, CoreText on macOS, and Fontconfig on Linux. The result is a FontMatch carrying the family and style, ready to use as a fallback for characters your bundled fonts cannot map.
Shaping can use this matching automatically. Set TextOptions.FontFallbackResolver to SystemFonts.FallbackResolver, and shaping consults the operating system for code points no configured font can shape. The resolver runs after the primary font and every explicit fallback family, at most once per distinct unresolved code point. Implement IFontFallbackResolver for a custom policy.
Alongside it, stream-loaded fonts now copy their data into owned memory, so a FontCollection stays valid after the source stream closes. FontMetrics exposes TryGetGlyphMetrics(...), TryGetTableData(...), and OpenStream() for tooling that needs raw font data.
Color font palettes and composite groups #
COLR glyphs take their colors from the font's CPAL palette table, and many color fonts ship more than one palette. TextOptions.FontPalette selects the palette that resolves the colors. TextRun.FontPalette swaps it over a range of text, and each FontPaletteOverride replaces one palette entry with your own color.
using SixLabors.Fonts;
TextOptions options = new(font)
{
// Resolve COLR glyph colors with the font's second palette.
// Palette indexes are zero-based; null keeps the font's default.
FontPalette = new FontPalette(1)
};
COLR v1 rendering also gained fidelity. Composite groups now render as isolated units that blend onto the content below with their declared composite mode, matching the specification. The IGlyphRenderer contract changed to carry this: BeginGroup(...) and EndGroup() wrap grouped layers, BeginLayer(...) lost its clip parameter, and clips arrive through GlyphRendererParameters.ClipBounds. If you implement a custom renderer, add the two new members when you upgrade.
Shaping coverage and correctness #
Differential tests compare glyph IDs, advances, and offsets against HarfBuzz across a font corpus on Windows and Linux. All 4,851 available cases pass:
- Line breaking, run segmentation, and metrics resolution follow the same rules as web engines, so text lays out consistently across your stack.
- A dedicated Khmer shaper, and kern table format 2 support.
- Arabic fallback shaping and positioning for fonts without OpenType layout tables, and a fix for Arabic in vertical layout.
- Joiner transparency during sequence matching, and nested lookups applied at the positions their context matched.
- Fractions now form automatically around the Unicode fraction slash and no longer trigger on the ordinary solidus, so "1⁄2" forms a fraction and "1/2" does not.
- Characters map through the single
cmapsubtable a font intends, instead of merging candidates across subtables.
Layout and rendering additions #
TextDecorationSkipInkbreaks underlines and overlines around glyph descenders and ascenders, measured from the real outline geometry.TextOptions.VisibleBoundsculls whole lines outside a visible region while keeping the remaining lines positioned as if everything had rendered.- Baseline anchoring reads the OpenType
BASEtable, andFontMetricsexposesXHeightandCapHeight. - Recursion in composite glyphs and CFF subroutines is guarded, the TrueType interpreter pool resets state correctly, and SVG tables with zero entries load cleanly.
Closing #
Fonts 3.1 opens the whole text pipeline to callers. You can shape, measure, hint, synthesize, and render through public APIs. Thousands of corpus cases verify the output, and it runs at native-engine speed.
Please read the release notes for detailed information.
Thank you to everyone who reported issues and contributed code to this release.
- Next: Announcing ImageSharp 4.1.0
- Previous: Announcing PolygonClipper 1.0.0