Microsoft.Maui.Graphics icon indicating copy to clipboard operation
Microsoft.Maui.Graphics copied to clipboard

Measuring the bounds of text

Open rick-palmsens opened this issue 3 years ago • 8 comments
trafficstars

How can I measure text bounds using this library? Skia has SKPaint.MeasureText, but I am unable to find an equivalent in this library.

rick-palmsens avatar Jan 14 '22 15:01 rick-palmsens

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()?

swharden avatar Jan 14 '22 15:01 swharden

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);

image

swharden avatar Jan 27 '22 03:01 swharden

@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:

image

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.

mikegoatly avatar Feb 17 '22 21:02 mikegoatly

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.

cmaughan avatar Feb 18 '22 12:02 cmaughan

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);

image

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);

image

WARNING: In this example GetStringSize() respects font but DrawString() does not

swharden avatar Feb 19 '22 20:02 swharden

image 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.

cmaughan avatar Feb 22 '22 14:02 cmaughan

@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 p drop 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: image

Commas and periods are undercalculated: image image

Exclamation marks are overcalculated: image

mikegoatly avatar Feb 22 '22 21:02 mikegoatly

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.

AdamJachocki avatar May 16 '23 12:05 AdamJachocki