首页 > 解决方案 > 计算字体大小以使文本适合特定宽度

问题描述

我有一个改变大小的图像,我想知道我应该使用哪种字体大小来适应动态变化的大小。

如您所知,有一种Graphics.MeasureString方法可以计算字符串的大小。一种可能的方法是测量每种字体大小,直到找到最合适的字体,但是由于我需要在一秒钟内渲染很多帧,因此对性能的影响太大了。

给定特定的图像宽度,是否有更有效的方法来查找字体大小?

标签: c#gdi+

解决方案


首先,我很确定没有一种方法不使用Graphics.MeasureString(). 至少,您必须分析 GDI+ 字体渲染代码的重要部分,才能估计最终字体大小,因为它使用自己的渲染器

幸运的是,有一些方法可以MeasureString()将每帧的调用次数大大减少到一个小的常数(甚至为零),具体取决于您使用的字体。


等宽字体

如果你使用等宽字体,事情很简单:

  1. n测量不同字体大小的任意字符的边界。
  2. 将图像宽度除以文本长度,得到字符的最大宽度。
  3. 使用预先计算的边界列表来确定字体大小。

调用次数MeasureString()n启动期间的0次数,每帧的次数。


比例(可变间距)字体

如果您使用具有可变字符大小的比例字体,事情会变得更加复杂。正如前言所说,难免调用MeasureString();但是,您可以通过计算和改进估计值,将所需的调用次数显着减少到一个小的常量。

一般算法是:

  1. 选择最大 ( max) 字体大小。
  2. max使用字体大小测量您的字符串。
  3. 缩放测量的字符串边界以适应图像边界。由于字体大小大致是线性的,因此s可以使用比例因子来计算估计的字体大小:est = s * max.
  4. est如果估计还不是最佳,请检查极端情况并稍微调整字体大小。

一个实现可能如下所示:

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];
}

不幸的是,各种 C# 在线编译器不支持 GDI+,但我上传了一个自包含的示例程序作为 Gist。该程序测试了各种不同的图像大小,同时记录了对MeasureString().

调用次数MeasureString()0启动期间的次数,23每帧的次数。

因此,(在实践中)该算法具有恒定的复杂性,并且比线性方法更有效,同时它仍然可以找到最佳整数字体大小。

MeasureString()如果特定字体和字符集允许的话,可以通过添加额外的检查来进一步优化这种方法,以便保存另外一两个调用。

备注

  • 示例实现适用于宽度和高度;由于您的问题表明您只对宽度感兴趣,因此您可以安全地删除高度部分。
  • 我只为整数字体大小实现了这个。如果渲染的文本真的应该尽可能大,那么在估计的大小和一个小的(可能是恒定的)偏移之间进行额外的二进制搜索应该会产生一个很好的近似值,同时保持MeasureString()调用的数量很小。
  • 在示例实现中,为了方便起见,我预先分配了所有n Font对象并将它们放入一个_fontCache列表中。从性能的角度来看,这可能没有必要;您可能想要测量分配Font对象的开销。
  • 您不必将字体大小限制在max. 升级也是可能的,但您可能会失去精度。
  • 您可以利用有关呈现文本内容的其他信息。例如,如果只有m可能的字符串,缓存它们计算的字体大小可能会有所帮助。

推荐阅读