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 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 indexushort glyphIndex = tf.CharacterToGlyphMap[(int)c];// Get glyph outlinevar geometry = tf.GetGlyphOutline(glyphIndex, unit);// Get advanced widthdouble advanceWidth = tf.AdvanceWidths[glyphIndex] * unit;// Get advanced heightdouble advanceHeight = tf.AdvanceHeights[glyphIndex] * unit;// Get baselinedouble baseline = tf.Baseline * unit;// Convert to path mini-languagestring 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:
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.
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 Cif (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 Qif (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 Lif (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);}
Example code
dotnetcore-examples/glyph-example
If you have any questions or any thoughts, feel free to add a comment.