Load Fonts as Glyph in .NET

Load Fonts as Glyph in .NET

Raymond Tang Raymond Tang 0 1313 1.10 index 3/26/2022

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 onlyTypeface 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;}svg.AppendLine("</svg>");Console.WriteLine(svg.ToString());

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' /></svg>

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

2022032695535-image.png

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);                        }                        newFigure.Segments.Add(seg);                    }                    // 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);                        }                        newFigure.Segments.Add(seg);                    }                    // Type L                    if (segment is LineSegment)                    {                        var seg = segment as LineSegment;                        if (seg != null)                        {                            seg.Point = ShiftPoint(seg.Point, r);                        }                        newFigure.Segments.Add(seg);                    }                }                newFigure.StartPoint = ShiftPoint(figure.StartPoint, r);                newFigure.IsClosed = figure.IsClosed;                newFigure.IsFilled = figure.IsFilled;                newPath.Figures.Add(newFigure);            }            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:

20220326102513-image.png

Example code

dotnetcore-examples/glyph-example

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

.net c#

Join the Discussion

View or add your thoughts below

Comments