Turn a string into SVG path data in C#: shape it with the font’s ligatures and kerning, then write each glyph’s outline through a sink that emits SVG commands.
A glyph outline is a list of figures made of line, quadratic and cubic segments in design units, with y pointing up. GlyphOutline.Write replays them into any IOutlineSink, through a transform. A sink that appends SVG commands gives path data a browser can draw.
using System.Globalization; using System.Numerics; using System.Text; using OneFont; FontFace face = FontFace.Load(File.ReadAllBytes("NotoSerif-Regular.ttf")); string text = "Shaped office"; float size = 48, s = size / face.UnitsPerEm; float baseline = face.Metrics.Ascender * s; // Shape first, so kerning and the ffi ligature are in the paths, then walk the pen along the glyphs. var sink = new SvgPathSink(); float pen = 0; foreach (ShapedCluster cluster in TextShaping.ShapeRun(text, face)) { foreach (ShapedGlyph g in cluster.Glyphs) { // Font units have y up; SVG has y down. Flip, scale and move to the pen position. var transform = new Matrix3x2(s, 0, 0, -s, pen + g.XOffset * s, baseline - g.YOffset * s); face.GetOutline(g.GlyphId).Write(sink, transform); pen += g.XAdvance * s; } } float height = face.Metrics.LineHeight * s; string svg = $""" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {F(pen)} {F(height)}" width="{F(pen)}" height="{F(height)}"> <path d="{sink}"/> </svg> """; File.WriteAllText("shaped.svg", svg); Console.WriteLine($"shaped.svg: {F(pen)} x {F(height)} px, path data {sink.ToString().Length} characters"); Console.WriteLine(sink.ToString()[..72] + "..."); static string F(float v) => SvgPathSink.F(v); // TrueType outlines arrive as quadratic segments (Q), CFF outlines as cubic ones (C). sealed class SvgPathSink : IOutlineSink { private readonly StringBuilder _d = new(); public void MoveTo(float x, float y) => _d.Append($"M{F(x)} {F(y)}"); public void LineTo(float x, float y) => _d.Append($"L{F(x)} {F(y)}"); public void QuadTo(float cx, float cy, float x, float y) => _d.Append($"Q{F(cx)} {F(cy)} {F(x)} {F(y)}"); public void CubicTo(float c1x, float c1y, float c2x, float c2y, float x, float y) => _d.Append($"C{F(c1x)} {F(c1y)} {F(c2x)} {F(c2y)} {F(x)} {F(y)}"); public void Close() => _d.Append('Z'); public override string ToString() => _d.ToString(); public static string F(float v) => v.ToString("0.##", CultureInfo.InvariantCulture); }
shaped.svg: 305.81 x 65.38 px, path data 6971 characters M11.86 51.79Q7.15 51.79 4.73 49.92Q2.3 48.05 2.3 45.02Q2.3 43.58 3.22 42...
The SVG shows “Shaped office” in Noto Serif at 48 px, with “ffi” drawn as the font’s ligature.
- Shape first. Mapping characters straight to glyphs would miss the ligature, kerning, Arabic joining forms and Indic reordering. Walk the shaped glyphs and advance the pen by each
XAdvance. - The transform.
Matrix3x2(s, 0, 0, -s, x, baseline)scales design units bys = size / UnitsPerEm, flips y so it points down as in SVG, and moves the glyph to the pen position and baseline. Add the glyph’sXOffsetand subtract itsYOffset, which place marks. - Segments. TrueType fonts give quadratic segments (
QuadTo, SVGQ), CFF fonts cubic ones (CubicTo, SVGC).Closeends a figure. - Empty glyphs. A space has an empty outline (
IsEmpty), and writing it emits nothing, but its advance still moves the pen. - Bounds.
TightBox()gives the exact extents of an outline’s curves in design units, andControlBox()the box around all its points, control points included. Use them to crop or align a glyph. - Right-to-left runs. A right-to-left run’s clusters come in logical order. Walk them last to first to place them from left to right.
Outlines are unhinted, and a variable font gives its default instance. Color glyphs (COLR) aren’t interpreted. Outlines are cached by the face and safe to read from several threads. A glyph with damaged data throws FontFormatException when its outline is read.
To fill and stroke text on images rather than export paths, use OneImage.Text.