Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compute font size to fit text into a specific width

Tags:

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?

like image 746
AFgone Avatar asked Jun 27 '20 10:06

AFgone


1 Answers

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:

  1. Measure the bounds of an arbitrary character, for n different font sizes.
  2. Divide the image width by the text length, which gives you the maximum width of a character.
  3. 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:

  1. Pick a maximum (max) font size.
  2. Measure your string using the max font size.
  3. 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.
  4. 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.
like image 164
janw Avatar answered Sep 29 '22 07:09

janw