Measuring Text
Measurement is often the point where text layout stops being abstract and starts affecting a real UI. TextMeasurer lets you run the same shaping and layout engine that rendering uses, which means you can decide widths, line breaks, placements, and bounds before anything is drawn.
The measurement APIs come in three layers:
TextMeasurer: one-shot convenience methods for measuring a string. Best for ad-hoc work.TextBlock: prepares a string once, then measures or renders it repeatedly at different wrapping lengths. See Prepared Text with TextBlock.TextMetrics: the full measurement object returned byTextMeasurer.Measure(...)orTextBlock.Measure(...). Keep this when callers need several measurements, hit testing, carets, or selection geometry from the same laid-out text.
Choose the right measurement
MeasureAdvance(...)returns the logical advance rectangle from layout, including line height and advance.MeasureBounds(...)returns only the tight rendered glyph ink bounds.MeasureRenderableBounds(...)returns the union of the logical advance rectangle and the glyph ink bounds.
The important distinction is that glyph geometry and layout geometry are not the same thing. Glyphs can overshoot the logical advance box, and the logical advance box can also include space that no glyph pixels occupy.
Measure a block of text
using SixLabors.Fonts;
Font font = SystemFonts.CreateFont("Segoe UI", 18);
TextOptions options = new(font)
{
WrappingLength = 320
};
FontRectangle advance = TextMeasurer.MeasureAdvance("Hello world", options);
FontRectangle bounds = TextMeasurer.MeasureBounds("Hello world", options);
FontRectangle renderable = TextMeasurer.MeasureRenderableBounds("Hello world", options);
Replace "Segoe UI" with any installed family that exists on your machine.
Use MeasureAdvance(...) when you care about layout flow, alignment, wrapping, or line-box size.
Use MeasureBounds(...) when you want the pure glyph bounds only.
Use MeasureRenderableBounds(...) when you need the full rendered area that combines layout space and glyph overshoot.
Understand bounds and origin
MeasureBounds(...) returns absolute glyph bounds only, so the returned X and Y can be non-zero, and the width and height reflect only where glyph ink exists.
MeasureRenderableBounds(...) returns a larger conceptual rectangle when needed: it includes the full logical advance rectangle from layout and then expands that rectangle to also include any glyph ink that extends beyond it.
If you need a rectangle that can safely contain both the typographic layout box and any glyph overshoot, prefer MeasureRenderableBounds(...).
Measure per-entry data
TextMeasurer exposes three per-entry collections. Each answers a different layout question and is independent of the others.
using System;
using SixLabors.Fonts;
Font font = SystemFonts.CreateFont("Segoe UI", 18);
TextOptions options = new(font)
{
WrappingLength = 320
};
ReadOnlyMemory<GraphemeMetrics> graphemes = TextMeasurer.GetGraphemeMetrics("Hello world", options);
ReadOnlyMemory<WordMetrics> words = TextMeasurer.GetWordMetrics("Hello world", options);
ReadOnlyMemory<GlyphMetrics> glyphs = TextMeasurer.GetGlyphMetrics("Hello world", options);
GraphemeMetricsis the unit for text interaction: hit testing, caret positioning, range selection, and UI overlays. UseAdvancefor hit targets and selection geometry;Boundsis the rendered ink only and can be empty or overhang.WordMetricsdescribes one Unicode word-boundary segment from UAX #29, including separators.GraphemeStartandStringStartare inclusive;GraphemeEndandStringEndare exclusive.GlyphMetricsexposes laid-out glyph entries for rendering diagnostics or glyph-level visualization. Do not use them as character or caret positions: ligatures, decomposition, fallback, emoji, and combining marks mean one grapheme can map to multiple glyph entries.
These APIs measure laid-out output, not raw UTF-16 code units, so do not assume a one-to-one mapping with the original string in the presence of shaping, ligatures, or complex scripts.
If you need a refresher on the difference between UTF-16 code units, CodePoint values, and graphemes, see Unicode, Code Points, and Graphemes.
Measure lines
When you care about wrapped text, use CountLines(...) and GetLineMetrics(...).
using System;
using SixLabors.Fonts;
Font font = SystemFonts.CreateFont("Segoe UI", 18);
TextOptions options = new(font)
{
WrappingLength = 320
};
int lineCount = TextMeasurer.CountLines("Hello world from Fonts", options);
ReadOnlyMemory<LineMetrics> lines = TextMeasurer.GetLineMetrics("Hello world from Fonts", options);
Each LineMetrics entry includes:
Ascender: the ascender guide position within the line box. This marks where tall glyphs such asHorltypically rise to.Baseline: the baseline position within the line box. This is the line most glyphs sit on.Descender: the descender guide position within the line box. This marks where descending glyph parts such asg,p, orytypically fall to.LineHeight: the total height of the line box after line spacing has been applied.Start: the positioned line-box origin in pixel units.Extent: the positioned line-box size in pixel units.StringIndex,GraphemeIndex,GraphemeCount: the source-text range owned by the line.GraphemeCountis not a glyph count.
Start and Extent are full Vector2 values. Selection and caret APIs use the line box for the cross-axis size, which matches normal text editor and browser behavior: selecting mixed font sizes on the same line paints a consistent line-height rectangle rather than one rectangle per glyph height.
Capture the full measurement with TextMetrics
When a single layout pass needs to feed several questions — overall size, per-line metrics, per-grapheme positions, hit testing, carets, and selection — measure once and keep the returned TextMetrics.
using SixLabors.Fonts;
Font font = SystemFonts.CreateFont("Segoe UI", 18);
TextOptions options = new(font)
{
WrappingLength = 320
};
TextMetrics metrics = TextMeasurer.Measure("Hello world", options);
FontRectangle advance = metrics.Advance;
FontRectangle bounds = metrics.Bounds;
FontRectangle renderable = metrics.RenderableBounds;
int lineCount = metrics.LineCount;
ReadOnlySpan<LineMetrics> lines = metrics.LineMetrics;
ReadOnlySpan<GraphemeMetrics> graphemes = metrics.GraphemeMetrics;
ReadOnlySpan<WordMetrics> words = metrics.WordMetrics;
Line and grapheme collections are in final layout order; for bidi text and reverse line-order layout modes, that can differ from source order. Word collections are in source order because word-boundary navigation is a logical operation.
TextMetrics returns the per-entry collections as ReadOnlySpan<T> because the metrics object owns their lifetime. The TextMeasurer and TextBlock methods return ReadOnlyMemory<T> because those snapshots can be stored alongside other layout state. Use .Span when drawing.
The same object exposes interaction APIs:
TextHit hit = metrics.HitTest(point);
CaretPosition caret = metrics.GetCaretPosition(hit);
ReadOnlyMemory<FontRectangle> selection = metrics.GetSelectionBounds(anchor, focus);
See Hit Testing and Caret Movement and Selection and Bidi Drag for the full editor-style interaction surface.
Keep measurement and rendering aligned
Always measure with the same TextOptions that you intend to render with. Dpi, LineSpacing, WrappingLength, TextDirection, LayoutMode, KerningMode, Tracking, FeatureTags, TextRuns, and fallback fonts all affect the final layout.
For repeated measurement of the same string at different wrapping lengths, prefer TextBlock over calling TextMeasurer multiple times — it shapes the text once and varies wrapping per call.
Practical guidance
- Measure advance when you need layout flow; measure bounds when you need ink or selection geometry.
- Keep the same
TextOptionsfor measuring, rendering, hit testing, and selection. - Use
TextBlockwhen the same shaped text will be inspected or wrapped more than once. - For UI text, test with the longest localized strings and fallback fonts, not only the default language.