I have a changing sized image, and I would like to know which font size I should use to fit that dynamically changing size.
As you know, there is a Graphics.MeasureString
method, which calculates the size of a string. A possible approach is measuring each font size until the best fit is found, but since I need to render a lot frames in a second, the performance impact is too high.
Is there are more efficient way to find a font size, given a specific image width?
First of all, I am pretty sure that there isn't an approach which does not use Graphics.MeasureString()
. At the very least, you would have to analyze a significant part of GDI+'s font rendering code in order to get an estimation of the final font size, since it uses its own renderer.
Fortunately, there are ways to greatly reduce the number of MeasureString()
calls per frame to a small constant (or even zero), depending on the font you are using.
If you use a monospace font, things are easy:
n
different font sizes.Number of calls to MeasureString()
: n
times during startup, 0
times per frame.
If you use a proportional font with variable character sizes, stuff gets more complicated. As said in the preface, it is hard to avoid calling MeasureString()
; however, you can significantly reduce the required number of calls to a small constant, by calculating and refining an estimation.
The general algorithm is:
max
) font size.max
font size.s
can be used to compute an estimated font size: est = s * max
.est
is not yet optimal.An implementation may look as follows:
public Font GetFont(string str, Graphics g, int imgWidth, int imgHeight)
{
// Measure with maximum sized font
var baseSize = g.MeasureString(str, _fontCache[_maxFontSize]);
// Downsample to actual image size
float widthRatio = imgWidth / baseSize.Width;
float heightRatio = imgHeight / baseSize.Height;
float minRatio = Math.Min(widthRatio, heightRatio);
int estimatedFontSize = (int)(_maxFontSize * minRatio);
// Make sure the precomputed font list is always hit
if(estimatedFontSize > _maxFontSize)
estimatedFontSize = _maxFontSize;
else if(estimatedFontSize < _minFontSize)
estimatedFontSize = _minFontSize;
// Make sure the estimated size is not too large
var estimatedSize = g.MeasureString(str, _fontCache[estimatedFontSize]);
bool estimatedSizeWasReduced = false;
while(estimatedSize.Width > imgWidth || estimatedSize.Height > imgHeight)
{
if(estimatedFontSize == _minFontSize)
break;
--estimatedFontSize;
estimatedSizeWasReduced = true;
estimatedSize = g.MeasureString(str, _fontCache[estimatedFontSize]);
++counter;
}
// Can we increase the size a bit?
if(!estimatedSizeWasReduced)
{
while(estimatedSize.Width < imgWidth && estimatedSize.Height < imgHeight)
{
if(estimatedFontSize == _maxFontSize)
break;
++estimatedFontSize;
estimatedSize = g.MeasureString(str, _fontCache[estimatedFontSize]);
}
// We increase the size until it is larger than the image, so we need to go back one step afterwards
if(estimatedFontSize > _minFontSize)
--estimatedFontSize;
}
return _fontCache[estimatedFontSize];
}
Unfortunately the various C# online compilers do not support GDI+, but I uploaded a self-contained sample program as a Gist. The program tested various different image sizes, while logging the number of calls to MeasureString()
.
Number of calls to MeasureString()
: 0
times during startup, 2
or 3
times per frame.
Thus, (in practice) this algorithm has constant complexity and is much more efficient than a linear approach, while it still finds the optimal integer font size.
This approach may be further optimized by adding additional checks in order to save another one or two MeasureString()
calls, if the particular font and character set allow it.
Notes:
MeasureString()
calls small.n
Font
objects and put them into a _fontCache
list, for convenience. From a performance view, this may not be necessary; you may want to measure the overhead of allocating a Font
object.max
. Upscaling is possible as well, but you may lose precision.m
possible strings, it might be helpful to cache their calculated font sizes.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With