【问题标题】:Compute font size to fit text into a specific width计算字体大小以使文本适合特定宽度
【发布时间】:2020-07-12 20:09:37
【问题描述】:

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

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

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

【问题讨论】:

  • 如果您可以使用等宽字体,那应该是最快的方法,因为所有字符的宽度都相同,并且不需要 MeasureString
  • 在开始render 例程之前计算并列出字体大小...可能是Dictionary<Bitmap, FontSize> 或Dictionary<Size, FontSize>,所以您只需要在渲染时创建、使用和处置Font 对象....
  • 最终取决于您的上下文的生存能力的一种解决方案是找到“最佳”近似尺寸(不是测试所有尺寸),并使用 GPU 缩放来完成所需的尺寸。当然它并不完美(可能会添加一些小的调整大小的工件)。不知道如何使用 GDI+(可能使用 Direct2D,您可以混合使用两种技术)。

标签: c# gdi+


【解决方案1】:

首先,我很确定没有不使用Graphics.MeasureString() 的方法。至少,您必须分析 GDI+ 字体渲染代码的重要部分,才能估计最终字体大小,since it uses its own renderer。

幸运的是,有一些方法可以将每帧的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+,不过我上传了self-contained sample program as a Gist。该程序测试了各种不同的图像大小,同时记录了对MeasureString()的调用次数。

对MeasureString() 的调用次数:在启动期间0 次,2 或3 次每帧。

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

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

注意事项:

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

【讨论】:

  • 我正在尝试每秒渲染 30 帧。如果我没记错的话,你的算法是 On ,这在性能方面是不可接受的。
  • 不,不是。对于可变间距字体,您需要每帧调用 2 或 3 次 MeasureString,即 O(1)。 (假设 n = 不同字体大小的数量)
  • 正如while 循环所暗示的那样,实现确实可能在 O(n) 中运行,但它永远不应该,因为估计非常好。我测试了大范围的图像大小,从未超过 3 个MeasureString 调用(实际上大多数情况下只需要 1 个,因为第一次估计已经是最佳的了)。因此,您可以安全地用if 语句替换while 循环。
【解决方案2】:

我认为最简单的方法是使用 ScaleTransform 方法。此方法允许不计算字体大小。您可以使用任何字体大小并在将文本绘制到所需尺寸之前进行缩放

        using (Font font = new Font(new FontFamily("Times New Roman"), 45, 0, GraphicsUnit.Pixel))
        {
            float width = ClientSize.Width;
            float height = ClientSize.Height;
            e.Graphics.FillRectangle(Brushes.White, 0, 0, width, height);
            SizeF textSize = e.Graphics.MeasureString("Hello World", font);
            float scale = Math.Min(width / textSize.Width, height / textSize.Height);
            e.Graphics.ScaleTransform(scale, scale);
            e.Graphics.DrawString("Hello World", font, Brushes.Black, new PointF(0, 0));
        }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-01-30
    • 2010-11-22
    • 1970-01-01
    • 2013-12-26
    • 2020-02-10
    • 2017-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多