Skip to main content

Fonts and Font Engines

XLibur needs to measure text — how wide is "Total revenue" in 11pt Calibri? — to size columns with AdjustToContents(), calculate row heights, and lay out glyphs. That measurement requires a font library.

Unlike ClosedXML, which bakes SixLabors.Fonts into its core assembly, XLibur's core assembly has no font dependency at all. The font engine ships as a separate, swappable package, so you can pick a font library whose licence suits you — and library authors who never measure text do not inherit a font dependency they did not ask for.

The short version

Install XLibur.Bundle and everything works with no configuration:

dotnet add package XLibur.Bundle
using var workbook = new XLWorkbook(); // font engine resolved automatically
workbook.Worksheets.Add("Data").Columns().AdjustToContents();

XLibur.Bundle = XLibur + XLibur.Fonts.SkiaSharp. The SkiaSharp engine (MIT) is auto-registered by the core the first time you create a workbook — there is no startup call.

warning

If you install the bare XLibur package with no font engine, creating a workbook throws an InvalidOperationException telling you which package to add. This is deliberate: it is how the core stays font-library-agnostic.

Available engines

PackageFont libraryLicenceNotes
XLibur.Fonts.SkiaSharpSkiaSharpMITDefault. Auto-registers. Ships native binaries.
XLibur.Fonts.SixLabors.V1SixLabors.Fonts 1.xApache 2.0Pure-managed. Matches ClosedXML 0.105's engine exactly.
XLibur.Fonts.SixLaborsSixLabors.Fonts 2.xSix Labors Split LicenseCommercial restrictions above $1M revenue.

Measurement parity between SkiaSharp and SixLabors was verified at 0% metric drift across width, descent, height, and max-digit-width — switching engines does not change column widths.

Choosing between them

  • SkiaSharp (default) — MIT, no revenue restrictions, resolves system fonts, and includes an embedded metric-only Calibri-compatible fallback so measurement works in headless and serverless environments with no fonts installed. Trade-off: it wraps native Skia and ships per-platform native binaries.
  • SixLabors.Fonts 1.x — pure managed, no native dependency. Choose this if you are migrating from ClosedXML and want byte-identical behaviour, or if native binaries are a problem in your deployment.
  • SixLabors.Fonts 2.x — only if you specifically need 2.x features and the Split License is acceptable for your organisation.

Registering an engine

Resolution happens in three layers, checked cheapest-first:

  1. Per workbookLoadOptions.FontEngine
  2. Global explicitLoadOptions.DefaultFontEngine, usually set by a package bootstrap
  3. Auto-registered default — the core reflectively locates XLibur.Fonts.SkiaSharp

Anything set explicitly always beats the auto-registered default.

Global registration at startup

Call the bootstrap once, before any workbook is created:

using XLibur.Fonts.SixLabors.V1;

// In Program.cs
SixLaborsV1FontBootstrap.Register();

// Every workbook from here on uses SixLabors.Fonts 1.x
using var workbook = new XLWorkbook();
using XLibur.Fonts.SkiaSharp;

// Force the default engine at a specific point in startup
SkiaSharpFontBootstrap.Register();

Both bootstraps use ??=, so the first registration wins and repeat calls are no-ops. If you need to override a registration that already happened, assign directly:

using XLibur.Excel;
using XLibur.Fonts.SixLabors;

LoadOptions.DefaultFontEngine = new SixLaborsFontEngine("Arial");

Per-workbook registration

Pass an engine through LoadOptions. This overrides whatever global default is in place, for this workbook only:

using XLibur.Excel;
using XLibur.Fonts.SkiaSharp;

var options = new LoadOptions
{
FontEngine = new SkiaSharpFontEngine("Arial"),
};

using var workbook = new XLWorkbook(options);

The same options object works when loading an existing file:

using var workbook = new XLWorkbook("Report.xlsx", options);

The fallback font

Every engine constructor takes a fallback font name — the font used when a workbook asks for a typeface that is not installed:

new SkiaSharpFontEngine("Arial");
new SkiaSharpFontEngine("Microsoft Sans Serif"); // the default engine's choice
new SixLaborsFontEngine("Segoe UI");
new DefaultFontEngine("Microsoft Sans Serif"); // SixLabors 1.x

Pick something metrically close to what your workbooks actually use. A wildly different fallback produces wildly different column widths.

Headless and containerised environments

Docker images, Azure Functions, and AWS Lambda typically have no system fonts. Two options:

Rely on the embedded fallback

XLibur.Fonts.SkiaSharp and XLibur.Fonts.SixLabors.V1 both embed CarlitoBare — a metric-only, Calibri-compatible font. Because it matches Calibri's metrics, column widths come out correct for the default Excel font even with nothing installed on the box. This is the zero-config path and needs no code.

// Works in an empty container with XLibur.Bundle installed
using var workbook = new XLWorkbook();
workbook.Worksheets.Add("Data").Columns().AdjustToContents();

The SkiaSharp package also references SkiaSharp.NativeAssets.Linux.NoDependencies, so it needs no system fontconfig or freetype.

Supply fonts from streams

When the workbook uses a specific corporate typeface, load the font files yourself. Both engines expose two factories:

using XLibur.Excel;
using XLibur.Fonts.SkiaSharp;

await using var fallback = File.OpenRead("fonts/Inter-Regular.ttf");
await using var bold = File.OpenRead("fonts/Inter-Bold.ttf");

// Only these fonts — the system font collection is ignored entirely
var engine = SkiaSharpFontEngine.CreateOnlyWithFonts(fallback, bold);

var options = new LoadOptions { FontEngine = engine };
using var workbook = new XLWorkbook(options);
// These fonts first, then fall through to whatever the system has
var engine = SkiaSharpFontEngine.CreateWithFontsAndSystemFonts(fallback, bold);

CreateOnlyWithFonts is the deterministic choice for servers: the same input produces the same column widths on every machine, regardless of what is installed.

Embedding the fonts as assembly resources avoids shipping loose files:

var assembly = typeof(Program).Assembly;

using var fallback = assembly.GetManifestResourceStream("MyApp.Fonts.Inter-Regular.ttf")!;
using var bold = assembly.GetManifestResourceStream("MyApp.Fonts.Inter-Bold.ttf")!;

var engine = SkiaSharpFontEngine.CreateOnlyWithFonts(fallback, bold);

The same API exists on SixLaborsFontEngine (2.x) and DefaultFontEngine (1.x):

using XLibur.Fonts.SixLabors.V1;

var engine = DefaultFontEngine.CreateOnlyWithFonts(fallback, bold);

Trimming and AOT

The zero-config path finds the default engine by reflection (Assembly.Load), which is invisible to the trimmer. If you publish trimmed or AOT-compiled, register the engine explicitly so the assembly is rooted:

using XLibur.Fonts.SkiaSharp;

SkiaSharpFontBootstrap.Register(); // a direct reference the trimmer can see

When fonts matter — and when they don't

Text measurement is only used for layout that depends on glyph size:

OperationNeeds a font engine
Columns().AdjustToContents()Yes
Rows().AdjustToContents()Yes
Automatic row height for wrapped textYes
Reading and writing cell valuesNo
Formulas and evaluationNo
Styles, tables, pivot tablesNo
Explicit Column.Width / Row.HeightNo

If your generated files always set explicit widths, the font engine never runs — but the package must still be present, because a workbook cannot be constructed without one.

Font properties on cells

Choosing a typeface for a cell is a styling concern, separate from the engine that measures it — see Styling:

ws.Cell("A1").Style.Font.FontName = "Segoe UI";
ws.Cell("A1").Style.Font.FontSize = 14;
ws.Cell("A1").Style.Font.Bold = true;

// Non-Latin scripts
ws.Cell("A2").Style
.Font.SetFontName("Arabic Typesetting")
.Font.SetFontCharSet(XLFontCharSet.Arabic);

// Follow the workbook theme's heading/body fonts
ws.Cell("A3").Style.Font.FontScheme = XLFontScheme.Major;
note

Setting FontName = "Inter" writes that name into the file. Excel renders it if the machine opening the file has Inter installed; XLibur measures it if the generating machine's font engine can resolve it. These are two independent concerns.

Reference

The full design — interface separation, package structure, and why registration works the way it does — is documented in docs/font-architecture.md in the repository.

Where to next