Shape the string, add up the cluster advances, and scale from font units to pixels with size / face.UnitsPerEm. The width includes the font’s kerning and ligatures, and it is the same on every platform because it comes from the font file, not from an installed font or a graphics context.
dotnet new console -o MeasureText && cd MeasureText dotnet add package RecursiveAI.OneFont --version 1.3.0 NOTO=https://github.com/notofonts/notofonts.github.io/raw/main/fonts curl -LO $NOTO/NotoSerif/hinted/ttf/NotoSerif-Regular.ttf
Put this in Program.cs and run dotnet run:
using OneFont; FontFace face = FontFace.Load(File.ReadAllBytes("NotoSerif-Regular.ttf")); var noKerning = new ShapingOptions { Kerning = KerningMode.None }; foreach (string text in new[] { "Wave", "office", "Invoice total: $1,240.00" }) { float width = Width(text, 16), unkerned = Width(text, 16, noKerning); Console.WriteLine($"\"{text}\" at 16 px: {width:F2} px wide " + $"(without kerning {unkerned:F2} px)"); } // The same text at other sizes: width scales linearly with the font size. var sizes = new[] { 12f, 24f, 48f }.Select(px => Width("Wave", px).ToString("F2")); Console.WriteLine($"\"Wave\" at 12 / 24 / 48 px: {string.Join(" / ", sizes)}"); // Height of a line, and of the ink above and below the baseline, at 16 px. FontMetrics m = face.Metrics; float s = 16f / face.UnitsPerEm; Console.WriteLine($"line height {m.LineHeight * s:F2} px, " + $"ascender {m.Ascender * s:F2}, descender {m.Descender * s:F2}"); // Shape, add up the advances (font units), scale to pixels. float Width(string text, float size, ShapingOptions? options = null) { int units = TextShaping.ShapeRun(text, face, options: options).Sum(c => c.Advance); return units * size / face.UnitsPerEm; }
"Wave" at 16 px: 42.46 px wide (without kerning 43.58 px) "office" at 16 px: 41.70 px wide (without kerning 41.70 px) "Invoice total: $1,240.00" at 16 px: 174.21 px wide (without kerning 174.21 px) "Wave" at 12 / 24 / 48 px: 31.85 / 63.70 / 127.39 line height 21.79 px, ascender 17.10, descender -4.69
How it works
- Advances.
Advanceon a cluster is the sum of its glyphs’ advances in design units, including pair kerning with the cluster that follows. Kerning pulls “W” and “a” together, so “Wave” is narrower than its letters added up. - Ligatures. “office” is measured with the font’s ffi ligature, one glyph for three letters, which is why measuring character by character from the character map gives the wrong width.
- Scaling. Design units times
size / UnitsPerEmgives pixels when the size is in pixels, or points when it is in points. - Height.
Metrics.LineHeightis ascender minus descender plus line gap. The descender is negative: it is below the baseline.
Several fonts or scripts
For text that needs more than one font, measure through a FontFallbackChain: each cluster has a FaceIndex, and each face has its own UnitsPerEm. Arabic, Hebrew and Indic text is measured the same way, since shaping produces the joined forms and conjuncts first.
Limits
- Advance width, not ink. The width is where the next text would start. A glyph’s ink can reach past it (an italic f, for example). For the exact ink box, read the outlines’
TightBox(), as in text to SVG path. - Unhinted. Advances are the font’s design advances, not snapped to whole pixels as some renderers do at small sizes.
- One line. To fit text into a width, see Wrap text to a width.