Microsoft.Maui.Graphics
Microsoft.Maui.Graphics copied to clipboard
Measuring the bounds of text
How can I measure text bounds using this library?
Skia has SKPaint.MeasureText, but I am unable to find an equivalent in this library.
I tried to figure this out a few months ago too. This is what I came-up with using SkiaSharp from the Microsoft.Maui.Graphics.Skia package: https://swharden.com/blog/2021-10-16-maui-graphics-measurestring
SizeF MeasureString(string text, string fontName, float fontSize)
{
var fontService = new SkiaFontService("", "");
using SkiaSharp.SKTypeface typeFace = fontService.GetTypeface(fontName);
using SkiaSharp.SKPaint paint = new() { Typeface = typeFace, TextSize = fontSize };
float width = paint.MeasureText(text);
float height = fontSize;
return new SizeF(width, height);
}
I'll echo your question though: Is there a more idiomatic way to measure strings with Maui.Graphics? Should there exist an ICanvas.MeasureString()?
It seems this is being actively worked on (e.g., https://github.com/dotnet/Microsoft.Maui.Graphics/commit/731cf3adc3daa92f74576c14e17aae70a3438ea2, https://github.com/dotnet/Microsoft.Maui.Graphics/commit/4baa4493336028b5432f53486c9e7354c68a0711) but it looks like canvas.GetStringSize() will measure a string.
The code below runs on the latest source code available in this repository at this time (https://github.com/dotnet/Microsoft.Maui.Graphics/commit/189b4ea0273e7dd6be50ba7446f97b688150dcaf)
Note that canvas.GetStringSize() respects font, but canvas.DrawString() at this time does not 🤷
Font font = new("Impact");
int fontSize = 36;
SizeF textSize = canvas.GetStringSize(message, font, fontSize);
Here's an example .NET 6 console app I got working using the current package on NuGet 6.0.200-preview.12.852
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;
// create a new image with a blue background
using BitmapExportContext context = SkiaGraphicsService.Instance.CreateBitmapExportContext(500, 150);
GraphicsPlatform.RegisterGlobalService(SkiaGraphicsService.Instance);
ICanvas canvas = context.Canvas;
canvas.FillColor = Color.FromArgb("#003366");
canvas.FillRectangle(0, 0, context.Width, context.Height);
// measure the string and draw a rectangle around it
string message = "Hello, Maui.Graphics!";
string fontName = "Impact";
int fontSize = 36;
SizeF textSize = GraphicsPlatform.CurrentService.GetStringSize(message, fontName, fontSize);
PointF textLocation = new(50, 50);
RectangleF textRect = new(textLocation, textSize);
canvas.StrokeColor = Color.FromArgb("#006699");
canvas.StrokeSize = 3;
canvas.DrawRectangle(textRect);
// draw the string
canvas.FontColor = Colors.Yellow;
canvas.FontName = fontName;
canvas.FontSize = fontSize;
canvas.DrawString(message, textLocation.X, textLocation.Y + textSize.Height, HorizontalAlignment.Left);
// save the canvas as an image file
string filePath = Path.GetFullPath("test.png");
using FileStream fs = new(filePath, FileMode.Create);
context.WriteToStream(fs);
Console.WriteLine(filePath);

@mattleibow sorry to @ you directly, but it looks like you've been working on this - I'm not sure where the best place is for feedback.
Will we be able to use PlatformStringSizeService to measure a string on Windows as well? The Windows specific platform implementation doesn't seem to be showing up yet:

Your efforts here are definitely appreciated - ICanvas.GetStringSize is fine when you have access to a canvas during a drawing operation, but in practice it's sometimes necessary to calculate layouts ahead of time before drawing takes place.
Using GetStringSize seems to be incorrect, at least for me - it returns a small width compared with the actual width required for the string. Perhaps it is a retina or multi monitor issue.
It looks like this is working better now, demonstrated by this sample code using the latest main branch c15cc9c
@cmaughan describes a bug where the measured rectangle mismatches the font slightly, demonstrated here
// setup a canvas with a blue background
using BitmapExportContext bmp = new SkiaBitmapExportContext(450, 150, 1.0f);
ICanvas canvas = bmp.Canvas;
canvas.FillColor = Colors.Navy;
canvas.FillRectangle(0, 0, bmp.Width, bmp.Height);
// define and measure a string
PointF stringLocation = new(50, 50);
string stringText = "Hello, Maui.Graphics!";
Font font = new();
float fontSize = 32;
SizeF stringSize = canvas.GetStringSize(stringText, font, fontSize);
Rectangle stringRect = new(stringLocation, stringSize);
// draw the string and its outline
canvas.StrokeColor = Colors.White;
canvas.DrawRectangle(stringRect);
canvas.FontColor = Colors.Yellow;
canvas.Font = font;
canvas.FontSize = fontSize;
canvas.DrawString(
value: stringText,
x: stringLocation.X,
y: stringLocation.Y,
width: stringSize.Width * 1.1f, // NOTE: 10% wider than it should be
height: stringSize.Height * 1.1f, // NOTE: 10% higher than it should be
horizontalAlignment: HorizontalAlignment.Left,
verticalAlignment: VerticalAlignment.Top,
textFlow: TextFlow.OverflowBounds,
lineSpacingAdjustment: 0);
// save the result
string filePath = Path.GetFullPath("Issue279.png");
using FileStream fs = new(filePath, FileMode.Create);
bmp.WriteToStream(fs);
Console.WriteLine(filePath);

I can almost perfectly counteract this bug by intentionally drawing the string using a font size 1 pt smaller than the one used to measure it
canvas.FontSize = fontSize - 1;
canvas.DrawString(
value: stringText,
x: stringLocation.X,
y: stringLocation.Y,
width: stringSize.Width,
height: stringSize.Height,
horizontalAlignment: HorizontalAlignment.Left,
verticalAlignment: VerticalAlignment.Top,
textFlow: TextFlow.OverflowBounds,
lineSpacingAdjustment: 0);

WARNING: In this example GetStringSize() respects font but DrawString() does not
To be clear, my GetStringSize implementation is returning a much smaller representation that it should (I think only in 'X').
The green rect here is 4 x the size of the returned string size for the 'Am' within it.
@swharden Interesting - I think your sample highlights two different things:
- The bounding box bottom is at the baseline of the text, not the bottom - you see the comma and the
pdrop below it. - The measuring seems to start breaking down when certain characters are added into the mix - punctuation seems to be the main culprit:
This looks about right:

Commas and periods are undercalculated:

Exclamation marks are overcalculated:

This method doesn't take multiple lines under consideration. There is:
var width = paint.MeasureText(value);
paint.Dispose();
return new SizeF(width, fontSize);
while it should somehow count text lines and spaces between them.