Load Fonts as Glyph in .NET

visibility 37 access_time 2 months ago languageEnglish timeline Stats
Load Fonts as Glyph in .NET

In one of my previous articles, I demonstrated about using .NET GDI+ to draw images: Draw Images in ASP.NET Core 5. This approach is good if you only need to run your applications in Windows machines. To implement a cross platform solution, we can convert fonts to glyph and then display it as SVG images which are supported by all modern browsers. This article shows you how to use an open source library GlyphLoader to implement it.

GlyphLoader is a .NET Standard library (written in C#) for TrueType and OpenType. It is designed to be small, efficient and portable while capable of producing high-quality glyph images.

Source code

You can find the source code of the project on GitHub: watertrans/GlyphLoader: GlyphLoader is a .NET Standerd library for TrueType, OpenType.

Download a font

Google Fonts provides many open source fonts (under Apache License 2.0) which can be downloaded.

I will use Roboto to create the sample code.

Convert fonts to glyphs

Create a folder and initialize a Console type .NET project using the following CLI command:

dotnet new console

Add reference to GlyphLoader:

dotnet add package WaterTrans.GlyphLoader

Create a subfolder named fonts in project folder. Download Roboto font from Google Fonts website.

Now open Program.cs and add the following code script:

using WaterTrans.GlyphLoader;

string fontPath = System.IO.Path.Combine(Environment.CurrentDirectory, "fonts/Roboto-Bold.ttf");
using var fontStream = System.IO.File.OpenRead(fontPath);
// Initialize stream only
Typeface tf = new Typeface(fontStream);

var svg = new System.Text.StringBuilder();
double unit = 100;
double x = 20;
double y = 20;
string text = "ABCD";
svg.AppendLine("<svg width='440' height='140' viewBox='0 0 440 140' xmlns='http://www.w3.org/2000/svg' version='1.1'>");

foreach (char c in text)
    // Get glyph index
    ushort glyphIndex = tf.CharacterToGlyphMap[(int)c];

    // Get glyph outline
    var geometry = tf.GetGlyphOutline(glyphIndex, unit);

    // Get advanced width
    double advanceWidth = tf.AdvanceWidths[glyphIndex] * unit;

    // Get advanced height
    double advanceHeight = tf.AdvanceHeights[glyphIndex] * unit;

    // Get baseline
    double baseline = tf.Baseline * unit;

    // Convert to path mini-language
    string miniLanguage = geometry.Figures.ToString(x, y + baseline);

    svg.AppendLine($"<path d='{miniLanguage}' fill='#46DBC4' stroke='#46DBC4' stroke-width='0' />");
    x += advanceWidth;


Run the following command to start the program:

dotnet run

The console application prints out the following SVG code:

<svg width='440' height='140' viewBox='0 0 440 140' xmlns='http://www.w3.org/2000/svg' version='1.1'>
<path d='M71.42,95L66.48,80.35L40.8,80.35L35.92,95L20.34,95L46.81,23.91L60.38,23.91L86.99,95L71.42,95z M53.59,41.88L44.76,68.49L62.53,68.49L53.59,41.88z ' fill='#46DBC4' stroke='#46DBC4' stroke-width='0' />
<path d='M121.27,95L93.63,95L93.63,23.91L118.54,23.91Q131.47,23.91 138.16,28.86Q144.85,33.82 144.85,43.39L144.85,43.39Q144.85,48.61 142.17,52.59Q139.48,56.57 134.7,58.43L134.7,58.43Q140.17,59.79 143.32,63.95Q146.46,68.1 146.46,74.1L146.46,74.1Q146.46,84.36 139.92,89.63Q133.38,94.9 121.27,95L121.27,95z M121.71,64.04L108.28,64.04L108.28,83.23L120.83,83.23Q126.01,83.23 128.91,80.77Q131.82,78.3 131.82,73.96L131.82,73.96Q131.82,64.19 121.71,64.04L121.71,64.04z M108.28,35.77L108.28,53.69L119.12,53.69Q130.21,53.5 130.21,44.85L130.21,44.85Q130.21,40.02 127.4,37.9Q124.59,35.77 118.54,35.77L118.54,35.77L108.28,35.77z ' fill='#46DBC4' stroke='#46DBC4' stroke-width='0' />
<path d='M198.61,71.32L213.26,71.32Q212.43,82.79 204.79,89.38Q197.15,95.98 184.65,95.98L184.65,95.98Q170.98,95.98 163.14,86.77Q155.3,77.57 155.3,61.5L155.3,61.5L155.3,57.16Q155.3,46.9 158.92,39.09Q162.53,31.28 169.24,27.1Q175.96,22.93 184.84,22.93L184.84,22.93Q197.15,22.93 204.67,29.52Q212.19,36.11 213.36,48.03L213.36,48.03L198.71,48.03Q198.17,41.14 194.88,38.04Q191.58,34.94 184.84,34.94L184.84,34.94Q177.52,34.94 173.88,40.19Q170.24,45.44 170.15,56.47L170.15,56.47L170.15,61.85Q170.15,73.37 173.64,78.69Q177.13,84.01 184.65,84.01L184.65,84.01Q191.44,84.01 194.78,80.91Q198.12,77.81 198.61,71.32L198.61,71.32z ' fill='#46DBC4' stroke='#46DBC4' stroke-width='0' />
<path d='M244.9,95L222.88,95L222.88,23.91L244.76,23.91Q254.13,23.91 261.53,28.13Q268.93,32.35 273.08,40.14Q277.23,47.93 277.23,57.84L277.23,57.84L277.23,61.11Q277.23,71.03 273.15,78.74Q269.07,86.46 261.65,90.7Q254.23,94.95 244.9,95L244.9,95z M244.76,35.77L237.53,35.77L237.53,83.23L244.61,83.23Q253.2,83.23 257.74,77.62Q262.29,72 262.38,61.55L262.38,61.55L262.38,57.79Q262.38,46.95 257.89,41.36Q253.4,35.77 244.76,35.77L244.76,35.77z ' fill='#46DBC4' stroke='#46DBC4' stroke-width='0' />

Save the content as SVG file and it looks like the following screenshot:

Now the characters are converted to SVG!

Example use case - SVG Captcha

One possible use case is to implement SVG captcha code in ASP.NET Core so that it will be cross-platform. There is one JavaScript package svg-captcha that has implemented similar function: produck/svg-captcha: generate svg captcha in node

warning Glyph based SVG captcha might not be as secured as other captcha services as the path data is exposed directly. Please evaluate the risks before adopting in your projects. 

To improve security, we can randomize the path geometries by changing SVG path data randomly. The following code provides one algorithm that is similar as svg-captcha package:

private PathGeometry RandomizePath(PathGeometry path)
            var newPath = new PathGeometry();
            Random rnd = new Random();
            var r = rnd.NextDouble() * 0.4 - 0.2;
            foreach (var figure in path.Figures)
                var newFigure = new PathFigure();
                foreach (var segment in figure.Segments)
                    // Type C
                    if (segment is BezierSegment)
                        var seg = segment as BezierSegment;
                        if (seg != null)
                            seg.Point1 = ShiftPoint(seg.Point1, r);
                            seg.Point2 = ShiftPoint(seg.Point2, r);
                            seg.Point3 = ShiftPoint(seg.Point3, r);
                    // Type Q
                    if (segment is QuadraticBezierSegment)
                        var seg = segment as QuadraticBezierSegment;
                        if (seg != null)
                            seg.Point1 = ShiftPoint(seg.Point1, r);
                            seg.Point2 = ShiftPoint(seg.Point2, r);
                    // Type L
                    if (segment is LineSegment)
                        var seg = segment as LineSegment;
                        if (seg != null)
                            seg.Point = ShiftPoint(seg.Point, r);

                newFigure.StartPoint = ShiftPoint(figure.StartPoint, r);
                newFigure.IsClosed = figure.IsClosed;
                newFigure.IsFilled = figure.IsFilled;
            return newPath;

        private Point ShiftPoint(Point p, double r)
            return new Point(p.X + r, p.Y + r);

The following screenshot shows one example from Kontext login page:

Example code


If you have any questions or any thoughts, feel free to add a comment. 

info Last modified by Raymond 2 months ago copyright This page is subject to Site terms.

Please log in or register to comment.

account_circle Log in person_add Register

Log in with external accounts

More from Kontext
Get Started on Reunified .NET 5
visibility 143
thumb_up 0
access_time 2 years ago
Get Started on Reunified .NET 5
Read and parse JSON in C# / .NET Framework
visibility 2,250
thumb_up 0
access_time 2 years ago