---
title: "Convert text to SVG paths"
url: https://recursiveai.org/onefont/cookbook/text-to-svg-path/
description: "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…"
version: "1.3.0"
---

# Convert text to SVG paths

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.

```csharp
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);
}
```

```text
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 by `s = size / UnitsPerEm`, flips y so it points down as in SVG, and moves the glyph to the pen position and baseline. Add the glyph’s `XOffset` and subtract its `YOffset`, which place marks.
- **Segments.** TrueType fonts give quadratic segments (`QuadTo`, SVG `Q`), CFF fonts cubic ones (`CubicTo`, SVG `C`). `Close` ends 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, and `ControlBox()` 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](https://recursiveai.org/oneimage/docs/text/).
