CosmoFonts.Core
1.3.0
dotnet add package CosmoFonts.Core --version 1.3.0
NuGet\Install-Package CosmoFonts.Core -Version 1.3.0
<PackageReference Include="CosmoFonts.Core" Version="1.3.0" />
<PackageVersion Include="CosmoFonts.Core" Version="1.3.0" />
<PackageReference Include="CosmoFonts.Core" />
paket add CosmoFonts.Core --version 1.3.0
#r "nuget: CosmoFonts.Core, 1.3.0"
#:package CosmoFonts.Core@1.3.0
#addin nuget:?package=CosmoFonts.Core&version=1.3.0
#tool nuget:?package=CosmoFonts.Core&version=1.3.0
CosmoFonts
Pure-C# OpenType font loader and shaping engine. TrueType / CFF / WOFF / WOFF2 in, positioned glyphs out.
License: BSD-3-Clause.
What's here
src/
CosmoFonts.Core/ — pure C#, no native deps
CosmoFonts.Core.HarfBuzz/ — optional HarfBuzz wrapper for complex scripts
tools/
CosmoFonts.SmokeTest/ — diagnostic + HarfBuzz parity comparator
CosmoFonts.CffSmokeTest/ — CFF-specific diagnostic
Core is the pure-managed engine that almost every consumer should pull. Core.HarfBuzz is a thin P/Invoke wrapper around the native HarfBuzzSharp package, useful when you need HarfBuzz-grade shaping for Indic / Khmer / Myanmar / Tibetan and don't mind the native binaries.
Quick start
using CosmoFonts.Loader;
using CosmoFonts.Shaping;
// Load a font (TTF, OTF, TTC, WOFF, WOFF2 — auto-detected from magic).
var face = Face.Load(File.ReadAllBytes("/Library/Fonts/Arial.ttf"));
// Render text into a glyph outline via IGlyphRenderer.
var options = new TextOptions(face, sizePt: 12f) { Origin = new Vector2(50, 100) };
TextRenderer.RenderTextTo(myRenderer, "Hello, world!", options);
// Or measure without rendering.
var size = TextMeasurer.MeasureSize("Hello, world!", options);
The TextRenderer.RenderTextTo pipeline runs:
- NFC normalization (composes "a + combining acute" → "á" when the font has the precomposed glyph)
- BiDi segmentation per UAX #9 (X-rules, weak/neutral resolution, paired brackets, level reordering)
- Per BiDi run:
- Script auto-detection (Latin/Cyrillic/Greek/Arabic/Hebrew/Devanagari/Thai/Han/Hangul/Hiragana/Katakana)
- Arabic positional shaping (base codepoints → Presentation Forms-B based on joining context, with Lam-Alef ligation)
- Per-face fallback segmentation (rune missing in primary face → look it up in fallbacks)
- GSUB substitution (
liga/smcp/calt/init/medi/fina/...) - GPOS positioning (kerning, mark attachment, cursive)
- RTL reversal with mark-offset fix-up
- Concatenate runs in visual order
- Drive the
IGlyphRendererwith positioned outline path commands
TTF / TTC / WOFF / WOFF2
// Single-font file
var face = Face.Load(ttfBytes);
// TTC (TrueType Collection — Helvetica.ttc, Avenir.ttc, Apple Color Emoji.ttc, …)
var helvetica = Face.Load(ttcBytes, fontIndex: 0);
int n = Face.CollectionFontCount(ttcBytes);
// WOFF1 (zlib-compressed) — auto-unwrapped
var face1 = Face.Load(woff1Bytes);
// WOFF2 (Brotli-compressed, with transformed glyf reconstruction) — auto-unwrapped
var face2 = Face.Load(woff2Bytes);
Font fallback
var primary = Face.Load(File.ReadAllBytes("JetBrainsMono-Regular.ttf"));
var fallback = Face.Load(File.ReadAllBytes("Arial.ttf")); // covers Arabic, Cyrillic, …
var options = new TextOptions(primary, 12f)
{
FallbackFaces = new[] { fallback },
};
TextRenderer.RenderTextTo(myRenderer, "Hi مرحبا", options);
// → "Hi " from JetBrains, "مرحبا" from Arial
Fallback faces should ideally have the same UnitsPerEm as the primary; mismatched units make fallback glyphs render at the wrong size.
System fonts
var sys = SystemFontCollection.LoadDefault(); // scans platform default paths
if (sys.TryFind("Helvetica", FontStyle.BoldItalic, out var entry))
using var face = entry.Load();
// Or by exact subfamily name from the font's name table:
sys.TryFind("Avenir", "Heavy Oblique", out _);
// Iterate every variant:
foreach (var e in sys.Entries)
Console.WriteLine($"{e.FamilyName} / {e.SubfamilyName} ← {Path.GetFileName(e.FilePath)} #{e.FontIndex}");
The SystemFontCollection walks each platform's default font directories (macOS / Linux / Windows), indexes every TTF / OTF / TTC, prefers English subfamily names from the font's name table, and key by (family, subfamily) plus a Regular-preferred shortcut by family alone.
Variable fonts
if (face.IsVariable)
{
foreach (var axis in face.Fvar!.Axes)
Console.WriteLine($"{axis.Tag}: {axis.MinValue}..{axis.MaxValue} default={axis.DefaultValue}");
foreach (var inst in face.Fvar.Instances)
Console.WriteLine($"named instance — {string.Join(", ", inst.Coordinates)}");
}
Reading axis metadata + named instances works. Actual glyph interpolation at non-default instances (gvar / HVAR / VVAR / MVAR) is not implemented — you get the default-instance outline.
What's implemented
OpenType layout
| Table | Coverage |
|---|---|
| GSUB | Type 1 (single, fmt 1+2), Type 2 (multiple), Type 3 (alternate), Type 4 (ligature), Type 5 (context, fmts 1+2+3), Type 6 (chained context, fmts 1+2+3), Type 7 (extension), Type 8 (reverse chaining) |
| GPOS | Type 1 (single, fmt 1+2), Type 2 (pair, fmt 1+2), Type 3 (cursive), Type 4 (mark-to-base), Type 5 (mark-to-ligature), Type 6 (mark-to-mark), Type 7 (context, fmts 1+2+3), Type 8 (chained context, fmts 1+2+3), Type 9 (extension) |
| GDEF | glyph class definition + mark attachment class |
| kern | legacy kern table (fallback for fonts without GPOS PairPos) |
Other tables
| Table | Status |
|---|---|
cmap |
formats 4 + 12 |
head, hhea, hmtx, vhea, vmtx, maxp, name, OS/2, post, loca |
yes |
glyf |
TrueType outlines + WOFF2 transformed-glyf reconstruction |
CFF/CFF2 |
CFF Type 2 CharString interpreter (PostScript outlines) |
COLR / CPAL |
parse only — v0 layered color glyphs (renderer integration is downstream) |
fvar |
axes + named instances |
Unicode / shaping
| Subsystem | Status |
|---|---|
| BiDi | UAX #9 — X1–X10 explicit embedding, W1–W7 weak resolution, N0 paired brackets, N1–N2 neutrals, I1–I2 implicit levels, L1 trailing whitespace reset, L2 visual run reordering |
| NFC normalization | via .NET's string.Normalize with font-coverage guard |
| Arabic positional shaping | base codepoints → Presentation Forms-B, including Lam-Alef ligatures |
| Script auto-detection | Latin / Cyrillic / Greek / Armenian / Hebrew / Arabic / Syriac / Devanagari / Bengali / Gurmukhi / Gujarati / Oriya / Tamil / Telugu / Kannada / Malayalam / Sinhala / Thai / Lao / Tibetan / Myanmar / Khmer / Hiragana / Katakana / Han / Hangul |
| Bidi class table | block-range based (~99% of real-world input correctly classified) |
File formats
| Format | Status |
|---|---|
TTF (0x00010000) |
full |
OTF ('OTTO') |
full |
| TTC | every contained font addressable via fontIndex |
| WOFF1 | full (zlib decompression + SFNT reassembly) |
| WOFF2 | full including transformed glyf reconstruction (7-stream + 128-entry triplet decoding) |
What's not implemented
The honest list of gaps relative to a full HarfBuzz:
- Indic complex shapers (Devanagari / Bengali / Gujarati / Gurmukhi / Kannada / Malayalam / Tamil / Telugu / Oriya / Sinhala). We pass them through GSUB, but the multi-pass pre-base / post-base reordering and conjunct cluster shaping HarfBuzz does are not implemented. Output is degraded for these scripts.
- South-East Asian shapers — Khmer / Myanmar / Tibetan / Thai / Lao similarly need stateful cluster transformations.
- Variable font interpolation —
gvar,HVAR,VVAR,MVAR,CFF2deltas. We readfvarmetadata only. - Color emoji rendering — COLR v1 paint graph, sbix bitmap, CBDT/CBLC, SVG-in-OT.
- Apple AAT tables —
morx,kerx(some macOS system fonts use these instead of OT layout). - Full UCD bidi data — we use block-range classification; HarfBuzz uses the full Unicode database.
- UAX #14 line breaking —
TextOptions.LineBreakOracleis a hook the caller can wire; no built-in implementation.
For workloads that hit those gaps, install CosmoFonts.Core.HarfBuzz and use the HarfBuzzShaper directly — it's a thin P/Invoke wrapper around HarfBuzzSharp's native binaries.
Migrating from SixLabors.Fonts
CosmoFonts grew out of replacing SixLabors.Fonts in CosmoImage and tracks SixLabors's feature surface where CosmoImage actually exercised it. The shaping pipeline mirrors SixLabors's TextRenderer / TextOptions / IGlyphRenderer model so the migration is mostly a namespace swap.
At parity (drop-in equivalents)
| Concern | SixLabors.Fonts | CosmoFonts |
|---|---|---|
| Font loading | Font.Family.CreateFont(size) from a FontCollection |
Face.Load(bytes) plus SystemFontCollection.LoadDefault() |
| Text outline emission | TextRenderer.RenderTextTo(IGlyphRenderer, text, RichTextOptions) |
TextRenderer.RenderTextTo(IGlyphRenderer, text, TextOptions) — same shape, slightly different option names |
| Glyph metrics | TextMeasurer.MeasureSize / MeasureBounds / CountLines |
same names, same semantics |
IGlyphRenderer callbacks |
BeginText, BeginGlyph, BeginFigure, MoveTo, LineTo, QuadraticBezierTo, CubicBezierTo, EndFigure, EndGlyph, EndText |
identical method set |
| Wrapping + alignment | WrappingLength, HorizontalAlignment, WordBreaking, TextJustification, LayoutMode |
same property names + semantics |
| OpenType feature tags | FeatureTags on RichTextOptions |
FeatureTags on TextOptions, type-safe Tag records |
| Font fallback | fallback collections | TextOptions.FallbackFaces |
| Variable fonts | FontFamily with axis values |
face.Fvar (axis + named-instance metadata only — see gap below) |
Where CosmoFonts goes further than SixLabors.Fonts
| Feature | Note |
|---|---|
| Full GSUB lookup-type coverage | Including Type 2 (multiple), Type 3 (alternate), Type 5 / 6 / 8 (context, chained context — all three formats), Type 8 (reverse chaining). SixLabors's GSUB historically skips most of these. |
| Full GPOS lookup-type coverage | Including Type 5 (mark-to-ligature), Type 7 (context positioning), Type 8 (chained context positioning) — all three formats each. |
WOFF2 transformed glyf |
Full reconstruction including the 7-substream + 128-entry triplet decoder. SixLabors handled WOFF2 but not always the transformed glyf. |
| UAX #9 N0 paired-bracket BiDi | Brackets inside opposite-direction contexts resolve correctly (Say (مرحبا) to me). |
| TTC with per-font addressing | Helvetica.ttc exposes all 6 contained variants by family + style. |
| English-preferred name selection | Fonts that ship localized name table entries (Catalan, Italian, …) still surface the English subfamily for (family, style) lookup. |
| Per-feature legacy-kern fallback | Fonts with GPOS for marks but no GPOS kern feature still kern via the legacy kern table — matching HarfBuzz, not SixLabors. |
| NFC normalization with coverage guard | "a + combining acute" composes to precomposed "á" only when the font has the precomposed glyph; otherwise the decomposed form is preserved so mark-to-base GPOS can position it. |
Where SixLabors.Fonts goes further than CosmoFonts
| Feature | Status in CosmoFonts |
|---|---|
Decoration callbacks (SetDecoration + EnabledDecorations on IGlyphRenderer) |
Not in IGlyphRenderer. Underline / strikeout / overline are synthesised post-shape from face metrics by the consumer — see CosmoImage's VipsText.cs for one approach. |
| Variable font interpolation | fvar metadata is read; gvar / HVAR / VVAR / MVAR / CFF2 deltas are not. You get the default-instance outline, not interpolated weights/widths. |
| Indic shaping | We pass Devanagari / Bengali / Tamil / etc. through GSUB but don't run the multi-pass pre-base/post-base reordering and conjunct cluster shaping HarfBuzz / SixLabors do. Output is degraded for these scripts. |
| Color emoji rendering | COLR v0 + CPAL parsers exist; the renderer integration to fill paths with per-layer colors does not. |
| TrueType hinting | The prep / fpgm / cvt instruction interpreter is not implemented. Glyph outlines are emitted unhinted. |
| Vertical mixed orientation (CJK tate-chu-yoko) | We have horizontal vertical-layout support but rotated-Latin-in-vertical-CJK isn't implemented; needs renderer-side rotation. |
Migration checklist
- Replace
using SixLabors.Fonts;withusing CosmoFonts.Loader;+using CosmoFonts.Shaping;. - Replace
FontwithFace. Size moves from theFontconstructor to theTextOptions(face, sizePt)constructor. - Replace
RichTextOptions(font) { … }withnew TextOptions(face, sizePt) { … }. Property names mostly carry over (Origin,LineSpacing,HorizontalAlignment,WordBreaking,TextJustification,LayoutMode,WrappingLength,FeatureTags). - Replace
SystemFonts.CreateFont(family, size)withSystemFontCollection.LoadDefault().CreateFont(family)(caches the collection —LoadDefaultdoes file-system I/O, so reuse the result). - Update
IGlyphRendererimplementations:BeginText/BeginGlyphtakeRectFinstead ofFontRectangle,BeginGlyphreturnsvoidinstead ofbool, noinmodifier on parameters. - Replace
Tag.Parse("smcp")— same syntax, lives atCosmoFonts.Font.Opentype.Tag.Parse. - If you used decorations: emit them yourself in your renderer or post-process the output path (CosmoImage's VipsText.cs has a worked example).
Verifying against HarfBuzz
CosmoFonts.SmokeTest is the diagnostic tool used to verify pure-managed output against native HarfBuzz. Throughout the engine's build-out, byte-for-byte parity was confirmed on:
- 8 classical Latin kerning pairs on Arial / OpenSans / Times New Roman / JetBrains Mono / Amiri (Δ = 0.000pt)
- JetBrains Mono code ligatures (
==,=>,->,!=) - Latin combining marks via NFC composition (
á,ê,ö,ñ) - Arabic mark-to-base after BiDi reversal (
بَ→ fatha at offset (307, −106) matching HB exactly)
dotnet run --project tools/CosmoFonts.SmokeTest -- /System/Library/Fonts/Supplemental/Arial.ttf
Build
dotnet build src/CosmoFonts.Core/CosmoFonts.Core.csproj
dotnet build src/CosmoFonts.Core.HarfBuzz/CosmoFonts.Core.HarfBuzz.csproj # optional
dotnet run --project tools/CosmoFonts.SmokeTest -- <path-to-font>
Core targets net10.0. The only external dependency is a sibling Cosmo.Transport project (lightweight transport primitives).
Acknowledgments
Bidi table ranges and Arabic Presentation Forms-B mapping were derived from the Unicode UCD. Font format parsers follow the Microsoft OpenType spec and W3C WOFF2. Algorithm shapes and fixtures cross-checked against HarfBuzz via HarfBuzzSharp.
| Product | Versions 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. |
-
net10.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on CosmoFonts.Core:
| Package | Downloads |
|---|---|
|
CosmoImage
Pure-managed image processing library — loaders, savers, geometric/colour/convolution/effects operations. No native dependencies. |
|
|
CosmoPdf.FastReport
CosmoReport.Core PDF/Image export bridge backed by CosmoPdf — zero native deps, supports PDF/A-3b, embedded XML attachments, Arabic shaping, Roslyn-based ScriptText. ImageExport / EscPos exports use CosmoFonts (pure-C# OpenType + GSUB/GPOS/BiDi) + CosmoImagePdf.Shared raster surface + CosmoImage decode — no SixLabors.ImageSharp. |
|
|
CosmoFonts.Core.HarfBuzz
HarfBuzz-backed shaper for CosmoFonts.Core. First-class Indic / Arabic / Tibetan / Khmer / Myanmar / complex-script shaping via P/Invoke to native HarfBuzz. Pulls in HarfBuzzSharp's native binaries. |
|
|
CosmoPdf
Pure C# PDF generation library — a .NET port of gpdf with a layered architecture (pdf primitives, document model, template builder API). |
GitHub repositories
This package is not used by any popular GitHub repositories.