Skip to content

Commit f1ec587

Browse files
Unify text measurement API to TextMetrics
1 parent b89a0af commit f1ec587

5 files changed

Lines changed: 68 additions & 347 deletions

File tree

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" />
4141
</ItemGroup>
4242
<ItemGroup>
43-
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.39" />
43+
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.41" />
4444
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.94" />
4545
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.0-alpha.0.54" />
4646
</ItemGroup>

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 2 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -579,141 +579,10 @@ public void DrawGlyphs(
579579
}
580580

581581
/// <inheritdoc />
582-
public RectangleF MeasureTextAdvance(RichTextOptions textOptions, ReadOnlySpan<char> text)
582+
public TextMetrics MeasureText(RichTextOptions textOptions, ReadOnlySpan<char> text)
583583
{
584584
this.EnsureNotDisposed();
585-
586-
if (text.IsEmpty)
587-
{
588-
return RectangleF.Empty;
589-
}
590-
591-
FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions);
592-
return RectangleF.FromLTRB(advance.Left, advance.Top, advance.Right, advance.Bottom);
593-
}
594-
595-
/// <inheritdoc />
596-
public RectangleF MeasureTextBounds(RichTextOptions textOptions, ReadOnlySpan<char> text)
597-
{
598-
this.EnsureNotDisposed();
599-
600-
if (text.IsEmpty)
601-
{
602-
return RectangleF.Empty;
603-
}
604-
605-
FontRectangle bounds = TextMeasurer.MeasureBounds(text, textOptions);
606-
return RectangleF.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
607-
}
608-
609-
/// <inheritdoc />
610-
public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, ReadOnlySpan<char> text)
611-
{
612-
this.EnsureNotDisposed();
613-
614-
if (text.IsEmpty)
615-
{
616-
return RectangleF.Empty;
617-
}
618-
619-
FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, textOptions);
620-
return RectangleF.FromLTRB(renderableBounds.Left, renderableBounds.Top, renderableBounds.Right, renderableBounds.Bottom);
621-
}
622-
623-
/// <inheritdoc />
624-
public RectangleF MeasureTextSize(RichTextOptions textOptions, ReadOnlySpan<char> text)
625-
{
626-
this.EnsureNotDisposed();
627-
628-
if (text.IsEmpty)
629-
{
630-
return RectangleF.Empty;
631-
}
632-
633-
FontRectangle size = TextMeasurer.MeasureSize(text, textOptions);
634-
return RectangleF.FromLTRB(size.Left, size.Top, size.Right, size.Bottom);
635-
}
636-
637-
/// <inheritdoc />
638-
public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> advances)
639-
{
640-
this.EnsureNotDisposed();
641-
642-
if (text.IsEmpty)
643-
{
644-
advances = [];
645-
return false;
646-
}
647-
648-
return TextMeasurer.TryMeasureCharacterAdvances(text, textOptions, out advances);
649-
}
650-
651-
/// <inheritdoc />
652-
public bool TryMeasureCharacterBounds(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> bounds)
653-
{
654-
this.EnsureNotDisposed();
655-
656-
if (text.IsEmpty)
657-
{
658-
bounds = [];
659-
return false;
660-
}
661-
662-
return TextMeasurer.TryMeasureCharacterBounds(text, textOptions, out bounds);
663-
}
664-
665-
/// <inheritdoc />
666-
public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> bounds)
667-
{
668-
this.EnsureNotDisposed();
669-
670-
if (text.IsEmpty)
671-
{
672-
bounds = [];
673-
return false;
674-
}
675-
676-
return TextMeasurer.TryMeasureCharacterRenderableBounds(text, textOptions, out bounds);
677-
}
678-
679-
/// <inheritdoc />
680-
public bool TryMeasureCharacterSizes(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> sizes)
681-
{
682-
this.EnsureNotDisposed();
683-
684-
if (text.IsEmpty)
685-
{
686-
sizes = [];
687-
return false;
688-
}
689-
690-
return TextMeasurer.TryMeasureCharacterSizes(text, textOptions, out sizes);
691-
}
692-
693-
/// <inheritdoc />
694-
public int CountTextLines(RichTextOptions textOptions, ReadOnlySpan<char> text)
695-
{
696-
this.EnsureNotDisposed();
697-
698-
if (text.IsEmpty)
699-
{
700-
return 0;
701-
}
702-
703-
return TextMeasurer.CountLines(text, textOptions);
704-
}
705-
706-
/// <inheritdoc />
707-
public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, ReadOnlySpan<char> text)
708-
{
709-
this.EnsureNotDisposed();
710-
711-
if (text.IsEmpty)
712-
{
713-
return [];
714-
}
715-
716-
return TextMeasurer.GetLineMetrics(text, textOptions);
585+
return TextMeasurer.Measure(text, textOptions);
717586
}
718587

719588
/// <inheritdoc />

src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs

Lines changed: 11 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -214,146 +214,26 @@ public void DrawGlyphs(
214214
IEnumerable<GlyphPathCollection> glyphs);
215215

216216
/// <summary>
217-
/// Measures the logical advance of the text in pixel units.
217+
/// Measures the full set of layout metrics for the supplied text in a single pass.
218218
/// </summary>
219219
/// <param name="textOptions">The text shaping and layout options.</param>
220220
/// <param name="text">The text to measure.</param>
221-
/// <returns>The logical advance rectangle of the text if it was to be rendered.</returns>
222-
/// <remarks>
223-
/// This measurement reflects line-box height and horizontal or vertical text advance from the layout model.
224-
/// It does not guarantee that all rendered glyph pixels fit within the returned rectangle.
225-
/// Use <see cref="MeasureTextBounds"/> for glyph ink bounds or
226-
/// <see cref="MeasureTextRenderableBounds"/> for the union of logical advance and rendered bounds.
227-
/// </remarks>
228-
public RectangleF MeasureTextAdvance(RichTextOptions textOptions, ReadOnlySpan<char> text);
229-
230-
/// <summary>
231-
/// Measures the rendered glyph bounds of the text in pixel units.
232-
/// </summary>
233-
/// <param name="textOptions">The text shaping and layout options.</param>
234-
/// <param name="text">The text to measure.</param>
235-
/// <returns>The rendered glyph bounds of the text if it was to be rendered.</returns>
236-
/// <remarks>
237-
/// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle
238-
/// may be smaller or larger than the logical advance and may have a non-zero origin.
239-
/// Use <see cref="MeasureTextAdvance"/> for the logical layout box or
240-
/// <see cref="MeasureTextRenderableBounds"/> for the union of both.
241-
/// </remarks>
242-
public RectangleF MeasureTextBounds(RichTextOptions textOptions, ReadOnlySpan<char> text);
243-
244-
/// <summary>
245-
/// Measures the full renderable bounds of the text in pixel units.
246-
/// </summary>
247-
/// <param name="textOptions">The text shaping and layout options.</param>
248-
/// <param name="text">The text to measure.</param>
249-
/// <returns>
250-
/// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered.
251-
/// </returns>
252-
/// <remarks>
253-
/// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance
254-
/// rectangle and the rendered glyph bounds.
255-
/// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle.
256-
/// </remarks>
257-
public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, ReadOnlySpan<char> text);
258-
259-
/// <summary>
260-
/// Measures the normalized rendered size of the text in pixel units.
261-
/// </summary>
262-
/// <param name="textOptions">The text shaping and layout options.</param>
263-
/// <param name="text">The text to measure.</param>
264-
/// <returns>The rendered size of the text with the origin normalized to <c>(0, 0)</c>.</returns>
265-
/// <remarks>
266-
/// This is equivalent to measuring the rendered bounds and returning only the width and height.
267-
/// Use <see cref="MeasureTextBounds"/> when the returned X and Y offset are also required.
268-
/// </remarks>
269-
public RectangleF MeasureTextSize(RichTextOptions textOptions, ReadOnlySpan<char> text);
270-
271-
/// <summary>
272-
/// Measures the logical advance of each laid-out character entry in pixel units.
273-
/// </summary>
274-
/// <param name="textOptions">The text shaping and layout options.</param>
275-
/// <param name="text">The text to measure.</param>
276-
/// <param name="advances">The list of per-entry logical advances of the text if it was to be rendered.</param>
277-
/// <returns>Whether any of the entries had non-empty advances.</returns>
278-
/// <remarks>
279-
/// Each entry reflects the typographic advance width and height for one character.
280-
/// Use <see cref="TryMeasureCharacterBounds"/> for per-character ink bounds or
281-
/// <see cref="TryMeasureCharacterRenderableBounds"/> for the union of both.
282-
/// </remarks>
283-
public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> advances);
284-
285-
/// <summary>
286-
/// Measures the rendered glyph bounds of each laid-out character entry in pixel units.
287-
/// </summary>
288-
/// <param name="textOptions">The text shaping and layout options.</param>
289-
/// <param name="text">The text to measure.</param>
290-
/// <param name="bounds">The list of per-entry rendered glyph bounds of the text if it was to be rendered.</param>
291-
/// <returns>Whether any of the entries had non-empty bounds.</returns>
292-
/// <remarks>
293-
/// Each entry reflects the tight ink bounds of one rendered glyph.
294-
/// Use <see cref="TryMeasureCharacterAdvances"/> for per-character logical advances or
295-
/// <see cref="TryMeasureCharacterRenderableBounds"/> for the union of both.
296-
/// </remarks>
297-
public bool TryMeasureCharacterBounds(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> bounds);
298-
299-
/// <summary>
300-
/// Measures the full renderable bounds of each laid-out character entry in pixel units.
301-
/// </summary>
302-
/// <param name="textOptions">The text shaping and layout options.</param>
303-
/// <param name="text">The text to measure.</param>
304-
/// <param name="bounds">The list of per-entry renderable bounds of the text if it was to be rendered.</param>
305-
/// <returns>Whether any of the entries had non-empty bounds.</returns>
306-
/// <remarks>
307-
/// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance
308-
/// rectangle and the rendered glyph bounds for the corresponding laid-out entry.
309-
/// Use this when both typographic advance and rendered glyph overshoot must fit within the same rectangle.
310-
/// </remarks>
311-
public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> bounds);
312-
313-
/// <summary>
314-
/// Measures the normalized rendered size of each laid-out character entry in pixel units.
315-
/// </summary>
316-
/// <param name="textOptions">The text shaping and layout options.</param>
317-
/// <param name="text">The text to measure.</param>
318-
/// <param name="sizes">The list of per-entry rendered sizes with the origin normalized to <c>(0, 0)</c>.</param>
319-
/// <returns>Whether any of the entries had non-empty dimensions.</returns>
320-
/// <remarks>
321-
/// This is equivalent to measuring per-character bounds and returning only the width and height.
322-
/// Use <see cref="TryMeasureCharacterBounds"/> when the returned X and Y offset are also required.
323-
/// </remarks>
324-
public bool TryMeasureCharacterSizes(RichTextOptions textOptions, ReadOnlySpan<char> text, out ReadOnlySpan<GlyphBounds> sizes);
325-
326-
/// <summary>
327-
/// Gets the number of laid-out lines contained within the text.
328-
/// </summary>
329-
/// <param name="textOptions">The text shaping and layout options.</param>
330-
/// <param name="text">The text to measure.</param>
331-
/// <returns>The laid-out line count.</returns>
332-
public int CountTextLines(RichTextOptions textOptions, ReadOnlySpan<char> text);
333-
334-
/// <summary>
335-
/// Gets per-line layout metrics for the supplied text.
336-
/// </summary>
337-
/// <param name="textOptions">The text shaping and layout options.</param>
338-
/// <param name="text">The text to measure.</param>
339-
/// <returns>
340-
/// An array of <see cref="LineMetrics"/> in pixel units, one entry per laid-out line.
341-
/// </returns>
221+
/// <returns>A <see cref="TextMetrics"/> value containing every measurement for the laid-out text.</returns>
342222
/// <remarks>
343223
/// <para>
344-
/// The returned <see cref="LineMetrics.Start"/> and <see cref="LineMetrics.Extent"/> are expressed
345-
/// in the primary flow direction for the active layout mode.
224+
/// The returned <see cref="TextMetrics"/> exposes logical advance, rendered bounds, normalized size,
225+
/// combined renderable bounds, per-character entries, and per-line metrics. The text is shaped and
226+
/// laid out once, so this is the cheapest option when more than one measurement is required.
346227
/// </para>
347228
/// <para>
348-
/// <see cref="LineMetrics.Ascender"/>, <see cref="LineMetrics.Baseline"/>, and <see cref="LineMetrics.Descender"/>
349-
/// are line-box positions relative to the current line origin and are suitable for drawing guide lines.
229+
/// When only one or two values are required, call the granular overloads on
230+
/// <see cref="TextMeasurer"/> (for example <see cref="TextMeasurer.MeasureAdvance(ReadOnlySpan{char}, TextOptions)"/>,
231+
/// <see cref="TextMeasurer.MeasureBounds(ReadOnlySpan{char}, TextOptions)"/>, or
232+
/// <see cref="TextMeasurer.CountLines(ReadOnlySpan{char}, TextOptions)"/>) directly.
233+
/// Those overloads skip the per-character and per-line array materialization performed here.
350234
/// </para>
351-
/// <list type="bullet">
352-
/// <item><description>Horizontal layouts: Start = X position, Extent = width.</description></item>
353-
/// <item><description>Vertical layouts: Start = Y position, Extent = height.</description></item>
354-
/// </list>
355235
/// </remarks>
356-
public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, ReadOnlySpan<char> text);
236+
public TextMetrics MeasureText(RichTextOptions textOptions, ReadOnlySpan<char> text);
357237

358238
/// <summary>
359239
/// Draws an image source region into a destination rectangle.

tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference<TPixel>(Te
8181
canvas.Fill(Brushes.Solid(Color.LightSteelBlue.WithAlpha(0.25F)), new Rectangle(0, 0, 712, 276));
8282
canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null);
8383

84-
LineMetrics[] lineMetrics = canvas.GetTextLineMetrics(textOptions, text);
84+
IReadOnlyList<LineMetrics> lineMetrics = canvas.MeasureText(textOptions, text).Lines;
8585
float lineOriginY = textOptions.Origin.Y;
86-
for (int i = 0; i < lineMetrics.Length; i++)
86+
for (int i = 0; i < lineMetrics.Count; i++)
8787
{
8888
LineMetrics metrics = lineMetrics[i];
8989
float startX = metrics.Start;

0 commit comments

Comments
 (0)