OpenFontSharp 1.3.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package OpenFontSharp --version 1.3.0
                    
NuGet\Install-Package OpenFontSharp -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OpenFontSharp" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OpenFontSharp" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="OpenFontSharp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OpenFontSharp --version 1.3.0
                    
#r "nuget: OpenFontSharp, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OpenFontSharp@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OpenFontSharp&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=OpenFontSharp&version=1.3.0
                    
Install as a Cake Tool

OpenFontSharp

OpenFontSharp is a lightweight, pure C# library for reading font metadata, character mappings, glyph outlines, and OpenType tables. It also provides measurement, basic shaping, and font-subsetting utilities for document applications. It is based on Typography.OpenFont.

Website: openfontsharp.smithsoft.co.za

Installation

The current library targets .NET 10 and runs on Windows, Linux, and macOS without requiring installed system fonts.

dotnet add package OpenFontSharp

The NuGet package includes XML API documentation for IntelliSense. See compatibility notes for nullable contracts and upgrading, and release instructions for publishing.

Features and use cases

Feature API Typical use case
Names, style, and language previews OpenFontReader.ReadPreview A font picker or inventory that does not need glyph outlines
Character-to-glyph mapping and outline access Typeface, Glyph, IGlyphTranslator Inspecting font coverage or feeding outlines to your own drawing backend
Consolidated embedding metrics FontLoader, FontInfo Building PDF font descriptors and glyph-width arrays
Unshaped advance measurement TextMeasurer.MeasureWidth(Typeface, ...) Estimating simple text widths in points or pixels
Basic glyph substitution and optional GPOS positioning BasicLatinShaper Simple Latin text in a document pipeline
Glyph subsetting and ID remapping FontSubsetter Reducing embedded font size for glyph-addressed PDF fonts
ToUnicode CMap generation ToUnicodeCMapBuilder Mapping two-byte PDF character IDs to Unicode scalar values, including supplementary planes
Trim and restore TypefaceExtensions.TrimDown / RestoreUp Keeping metrics while temporarily unloading outlines

This is not a PDF writer, a complete text-layout engine, or a ready-made rasterizer. It exposes drawing callbacks and parsed font tables, but your application supplies rendering, line breaking, bidirectional layout, and complex-script shaping.

Read a font and measure simple text

using OpenFontSharp;
using OpenFontSharp.Metrics;

using var stream = File.OpenRead("Roboto-Regular.ttf");
var typeface = new OpenFontReader().Read(stream)
    ?? throw new InvalidOperationException("The font could not be decoded.");

Console.WriteLine($"{typeface.Name ?? "(unnamed)"}: {typeface.GlyphCount} glyphs");
ushort glyphId = typeface.GetGlyphIndex('A'); // Zero means .notdef (missing glyph).
ushort advance = typeface.GetAdvanceWidthFromGlyphIndex(glyphId);
double widthInPoints = TextMeasurer.MeasureWidth(typeface, "Hello", 12);
Console.WriteLine($"A advance: {advance} design units; Hello: {widthInPoints:F2} pt");

FontInfo info = FontLoader.FromTypeface(typeface, File.ReadAllBytes("Roboto-Regular.ttf"));
Console.WriteLine($"Units per em: {info.UnitsPerEm}; ascent: {info.Ascent}");

The reader closes the supplied stream. Use a new readable, seekable stream for each read. Metrics and bounds are in font design units; multiply by fontSize / UnitsPerEm to convert to the units of your chosen font size.

TextMeasurer.MeasureWidth(Typeface, ...) maps Unicode runes but does not apply kerning or shaping. Prefer this overload for real fonts: the legacy FontInfo overload indexes its width array directly by UTF-16 character value, not by a cmap lookup. FontInfo.GlyphWidths produced by FontLoader is indexed by glyph ID, so that overload is not a general-purpose measurement path for loaded fonts.

Preview a font collection and select a member

using OpenFontSharp;

var reader = new OpenFontReader();
var preview = reader.ReadPreview(File.OpenRead("Fonts.ttc"))
    ?? throw new InvalidOperationException("The collection could not be decoded.");
if (!preview.IsFontCollection)
    throw new InvalidOperationException("Expected a font collection.");

for (int i = 0; i < preview.MemberCount; i++)
    Console.WriteLine($"{i}: {preview.GetMember(i).Name}");

int memberOffset = preview.GetMember(0).ActualStreamOffset;
var member = reader.Read(File.OpenRead("Fonts.ttc"), memberOffset)
    ?? throw new InvalidOperationException("The member could not be decoded.");

Read cannot select an entire collection at offset zero: use ReadPreview to obtain a member offset. A collection-level preview has no NameEntry or OS2Table; inspect its members instead. ReadFlags values are currently reserved and do not restrict the tables loaded; use ReadPreview when you need a lightweight read.

Basic shaping

using OpenFontSharp.Shaping;

var shaped = BasicLatinShaper.Shape(typeface, "office");
if (shaped.RequiresComplexShaping)
    throw new NotSupportedException("Use a full shaping engine for this text.");

double shapedWidth = shaped.Advances.Sum(a => (double)a) * 12 / typeface.UnitsPerEm;

By default, the basic shaper applies available GSUB lookups through its glyph-list adapter and legacy kern adjustments; this preserves the existing shaping behavior. To opt in to GPOS pair kerning and Latin mark placement, pass new ShapingOptions { EnableGposPositioning = true } to the overload. It applies GPOS PairPos formats 1 and 2 referenced by the kern feature, and MarkBasePos lookups referenced by mark. When a supported GPOS kern lookup is present, it takes precedence over the legacy kern table. ShapingResult.XOffsets and YOffsets contain the per-glyph placement adjustments in design units.

The basic shaper works on UTF-16 characters rather than Unicode runes and is not suitable for supplementary-plane text, bidirectional layout, complex scripts, or Indic reordering. The complex-shaping flag is a warning, not a guarantee that every unflagged string is fully shaped. Shaping does not return source clusters, so ligatures need an application-provided Unicode mapping when embedding them.

Optional outlines and trimming

var glyph = typeface.GetGlyph(typeface.GetGlyphIndex('A'));
if (glyph.GlyphPoints is { } points && glyph.EndPoints is { } ends)
{
    // Implement IGlyphTranslator to draw or collect TrueType contour commands:
    // translator.Read(points, ends, typeface.CalculateScaleToPixel(24));
}
else if (glyph.GetCff1GlyphData() is { } cff)
{
    Console.WriteLine($"CFF glyph: {cff.Name}");
}

var ticket = typeface.TrimDown();
if (ticket is not null)
{
    // Glyph IDs, bounds, and advance metrics remain usable; outlines are unloaded.
    typeface.RestoreUp(ticket, File.OpenRead("Roboto-Regular.ttf"));
    if (typeface.IsTrimmed())
        throw new InvalidOperationException("Restoration did not match the original font.");
}

Do not substitute empty arrays for missing outlines: null distinguishes CFF, bitmap, or trimmed glyphs from loaded TrueType glyphs. Color, SVG, and bitmap data can also be unavailable or unloaded. Check the actual data and trim state before using format-specific operations.

Colour glyph data (no rendering)

GetColorLayers(glyphId) returns read-only COLR v0 layers in back-to-front painting order. Each ColorLayer contains a GlyphId and PaletteIndex; index 0xFFFF means the application's foreground colour. Missing or trimmed layers return an empty list.

PaletteCount and GetPalette(index) expose CPAL v0/v1 palettes as read-only PaletteColor entries (Blue, Green, Red, Alpha, in BGRA order with sRGB colour components). Invalid palette indices, including when palettes are absent or trimmed, throw ArgumentOutOfRangeException. CPAL v1 palette labels and type metadata are not exposed. COLR versions other than 0 throw a version-specific OpenFontNotSupportedException.

Bitmap-only fonts use the first strike through GetGlyph and ReadBitmapContent; outlines still take priority in hybrid fonts. See bitmap compatibility and limits for supported formats and malformed-data handling. This exposes encoded data and metrics, not a rasteriser.

Web-font containers

WOFF 1 reading requires a host-provided zlib decoder. For example, configure this once during application startup:

using System.IO.Compression;
using OpenFontSharp.WebFont;

WoffDefaultZlibDecompressFunc.DecompressHandler = (compressed, output) =>
{
    using var input = new MemoryStream(compressed);
    using var zlib = new ZLibStream(input, CompressionMode.Decompress);
    zlib.ReadExactly(output);
    return true;
};

Then use OpenFontReader as with a standalone font. For WOFF2 reading, conversion, and subsetting, install the host Brotli hook once during startup:

Woff2DefaultBrotliDecompressFunc.DecompressHandler = (compressed, output) =>
{
    using var input = new MemoryStream(compressed);
    using var brotli = new BrotliStream(input, CompressionMode.Decompress);
    brotli.CopyTo(output);
    return true;
};

Woff2Converter.ToSfnt(woff2Bytes) returns standalone TTF/OTF bytes, reconstructing transformed glyf, loca, and hmtx tables per the W3C WOFF2 Recommendation. An optional second argument supplies a per-call Brotli decoder instead of the default hook. No Brotli package is required by the library; without a hook it throws NotSupportedException. WOFF2 collections are not supported. The byte-based WOFF1 subsetter uses built-in .NET zlib decompression and does not need the reader hook.

Font subsetting

OpenFontSharp.Subsetting.FontSubsetter keeps only the glyphs you use (plus .notdef and any composite components), renumbers them contiguously and returns the subset font with an old-to-new glyph id map. After container decoding, glyph outlines are copied verbatim, never re-encoded by the subsetter.

var result = FontSubsetter.Subset(fontBytes, usedGlyphIds);        // or Subset(typeface, usedGlyphIds)
var member = FontSubsetter.Subset(ttcBytes, fontIndex, usedGlyphIds); // one font of a collection

The output is always a plain TrueType (0x00010000) or CFF (OTTO) font. When every glyph is requested the font is returned whole and GlyphIdMap is empty. An empty map means glyph IDs are unchanged, not that the output has no glyphs. Certain recognized sfnt inputs without usable outline tables are also returned whole; check the result rather than assuming a size reduction. A format that cannot be subset throws NotSupportedException; the subsetter never invents missing outlines.

Input Test font Result
TrueType (.ttf) Roboto, Shantell Sans (static) Supported
TrueType variable font Shantell Sans (variable) Supported: subset to the default instance. fvar, gvar, HVAR and the other variation tables are dropped
OpenType with CFF outlines (.otf, CFF ) Great Vibes Supported for name-keyed CFF. Subroutines are kept whole, not subset
CID-keyed CFF hand-built CFF table Supported: CIDs, used Font DICTs and FDSelect formats 0/3 are preserved; subroutines are kept whole
CFF glyphs using the deprecated seac accent operator hand-built CFF table Throws NotSupportedException
OpenType with CFF2 outlines (variable .otf) Great Vibes with the table relabelled CFF2 Throws NotSupportedException
TrueType/OpenType collection (.ttc), as bytes Roboto Regular + Bold Throws NotSupportedException unless you pass a font index; supported with Subset(bytes, fontIndex, glyphs)
Collection member read with OpenFontReader.Read(stream, offset) Roboto Regular + Bold Supported
WOFF 1.0 (.woff) Roboto, Great Vibes Supported: decoded, then subset as TTF/OTF
WOFF 2.0 (.woff2) Synthetic simple/composite glyphs and metrics; Roboto; in-test Great Vibes wrapper Supported: host Brotli hook required, decoded then subset as TTF/OTF; collections excluded
Colour / bitmap tables Synthetic reader tests Reading only; colour/bitmap-aware subsetting is not implemented. Outline subsets drop these tables; bitmap-only inputs are returned whole
Unrecognised data n/a Throws NotSupportedException

What a subset keeps: head, hhea, maxp, hmtx, OS/2, name, hinting (cvt , fpgm, prep, gasp) and the outline tables (glyf/loca or CFF ). post becomes version 3 (no glyph names, though CFF glyph names survive in the charset), and cmap is reduced to an empty table, since PDF Identity-H embedding does not use it. Layout and kerning tables (GSUB, GPOS, GDEF, kern) are dropped.

These subsets are for glyph-addressed embedding, not drop-in fonts for ordinary Unicode text rendering. Map and shape text against the original font first, then remap glyph IDs for the subset. FontSubsetter itself resolves composite dependencies from the original bytes; GlyphCollector only collects the initial IDs from the parsed font.

Subset simple BMP text and create a PDF ToUnicode map

using OpenFontSharp;
using OpenFontSharp.Metrics;
using OpenFontSharp.Subsetting;

byte[] fontBytes = File.ReadAllBytes("Roboto-Regular.ttf");
var font = new OpenFontReader().Read(new MemoryStream(fontBytes))
    ?? throw new InvalidOperationException("The font could not be decoded.");

// This example deliberately uses simple BMP text without shaping or ligatures.
const string text = "Hello";
var usedGlyphIds = text.Select(c => font.GetGlyphIndex(c)).ToHashSet();
if (usedGlyphIds.Contains(0))
    throw new InvalidOperationException("The font does not cover the sample text.");

var subset = FontSubsetter.Subset(fontBytes, usedGlyphIds);
ushort Remap(ushort oldId) =>
    subset.GlyphIdMap.Count == 0 ? oldId : subset.GlyphIdMap[oldId];

var cidToUnicode = new Dictionary<int, int[]>();
foreach (char c in text)
    cidToUnicode.TryAdd(Remap(font.GetGlyphIndex(c)), new[] { (int)c });

File.WriteAllBytes("Hello-subset.ttf", subset.FontData);
File.WriteAllBytes("Hello-ToUnicode.txt", ToUnicodeCMapBuilder.Build(cidToUnicode));

Your PDF writer must use the same remapped IDs as CIDs (or supply a matching CID-to-GID mapping), create the font dictionaries and width entries, and embed both streams. Multiple characters can map to one glyph; choose the intended extraction mapping in your application. The CMap builder accepts source IDs from 0 through 65535 and either one Unicode scalar or a non-empty sequence of Unicode scalars per source ID. For example, a ligature CID can map to new[] { 0x66, 0x66, 0x69 } (ffi). Destinations use UTF-16BE bytes without a BOM: U+1F600 becomes <D83DDE00>, not <1F600>, and sequences can mix BMP and supplementary scalars. Surrogate code points and out-of-range values are rejected. Consecutive mappings use bfrange only when the UTF-16BE destination bytes can be incremented without overflow or crossing a surrogate boundary; all CMap blocks contain at most 100 entries. The BMP example above does not demonstrate supplementary-text shaping; the basic shaper's separate limitations still apply.

Respect the font's embedding permissions and license. Subsetting retains OS/2 licensing bits; it does not authorize embedding.

Development and quality checks

dotnet build src/OpenFontSharp/OpenFontSharp.csproj -c Release --no-incremental
dotnet test SmithSoftStudios.OpenFontSharp.sln -c Release
dotnet pack src/OpenFontSharp/OpenFontSharp.csproj -c Release --no-build

LibraryQualityTests performs a fresh Release library build with XML documentation enabled and treats CS8618 and CS1591 as errors. Nullable contract tests cover absent tables, CFF versus TrueType outlines, collection previews, and trim/restore behavior. The existing subsetting tests verify output structures and composite dependencies across supported formats.

License

OpenFontSharp contains SmithSoft original code (MIT) and code imported from other open-source projects, each under the licence in its file header (Apache-2.0, MIT, BSD, FreeType). See LICENSE.md for how the code is divided, THIRD-PARTY-NOTICES.md for the credits, required notices and statement of changes, and the licenses folder for the texts. If you copy source code from this project, check each file's header for its licence.

Imported code is based on Typography.OpenFont (WinterDev and contributors), with NRasterizer (Samuel Carlsson), SharpFont (Michael Popoloski), FreeType, Apache PDFBox and the Adobe Glyph List.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on OpenFontSharp:

Package Downloads
ProGPU.Text

ProGPU runtime package for GPU-accelerated .NET composition, rendering, vector graphics, text, and WebGPU platform integration.

ProGPU.Avalonia.Rendering

ProGPU runtime package for GPU-accelerated .NET composition, rendering, vector graphics, text, and WebGPU platform integration.

LeXtudio.DevFlow.Agent.LibreWpf

Package Description

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on OpenFontSharp:

Repository Stars
wieslawsoltes/ProGPU
Cross-platform GPU rendering, composition, vector, text, compute, and DirectX/LibreWPF/WinUI/Avalonia interop on WebGPU and Silk.NET.
Version Downloads Last Updated
1.4.0 0 11/5/2026
1.3.0 0 11/5/2026
1.2.3 6 11/5/2026
1.2.2 70 11/4/2026
1.2.1 93 11/3/2026
1.2.0 104 10/31/2026
1.1.1 86 10/31/2026
1.1.0 79 10/31/2026
1.0.0 107,301 1/17/2024