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.
Monospaced fonts
If you use a monospace font, things are easy:
- Measure the bounds of an arbitrary character, for
n different font sizes.
- Divide the image width by the text length, which gives you the maximum width of a character.
- Use the precomputed bounds list to determine the font size.
Number of calls to MeasureString(): n times during startup, 0 times per frame.
Proportional (variable spaced) fonts
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:
- Pick a maximum (
max) font size.
- Measure your string using the
max font size.
- Scale the measured string bounds to fit the image bounds. Since font sizes are roughly linear, the scale factor
s can be used to compute an estimated font size: est = s * max.
- Check for corner cases and slightly adjust the font size if the estimation
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:
- The sample implementation works for both width and height; since your question indicates that you are only interested into the width, you can safely remove the height part.
- I only implemented this for integer font sizes. If the rendered text should really be as large as possible, an additional binary search between the estimated size and a small (maybe constant) offset should yield a good approximation, while keeping the number of
MeasureString() calls small.
- In the sample implementation I preallocated all
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.
- You don't necessarily have to bound the font size at
max. Upscaling is possible as well, but you may lose precision.
- You could exploit additional information about the content of the rendered text. For example, if there were only
m possible strings, it might be helpful to cache their calculated font sizes.