【问题标题】:Graphics.MeasureCharacterRanges giving wrong size calculationsGraphics.MeasureCharacterRanges 给出错误的尺寸计算
【发布时间】:2018-11-11 06:38:09
【问题描述】:

我正在尝试将一些文本呈现到 Web 窗体应用程序中图像的特定部分。文本将由用户输入,所以我想改变字体大小以确保它适合边界框。

我的代码在我的概念验证实现中表现良好,但我现在正在尝试针对设计器的资产进行测试,这些资产更大,我得到了一些奇怪的结果。

我正在按如下方式运行尺寸计算:

StringFormat fmt = new StringFormat();
fmt.Alignment = StringAlignment.Center;
fmt.LineAlignment = StringAlignment.Near;
fmt.FormatFlags = StringFormatFlags.NoClip;
fmt.Trimming = StringTrimming.None;

int size = __startingSize;
Font font = __fonts.GetFontBySize(size);

while (GetStringBounds(text, font, fmt).IsLargerThan(__textBoundingBox))
{
    context.Trace.Write("MyHandler.ProcessRequest",
        "Decrementing font size to " + size + ", as size is "
        + GetStringBounds(text, font, fmt).Size()
        + " and limit is " + __textBoundingBox.Size());

    size--;

    if (size < __minimumSize)
    {
        break;
    }

    font = __fonts.GetFontBySize(size);
}

context.Trace.Write("MyHandler.ProcessRequest", "Writing " + text + " in "
    + font.FontFamily.Name + " at " + font.SizeInPoints + "pt, size is "
    + GetStringBounds(text, font, fmt).Size()
    + " and limit is " + __textBoundingBox.Size());

然后我使用以下行将文本渲染到我从文件系统中提取的图像上:

g.DrawString(text, font, __brush, __textBoundingBox, fmt);

地点:

  • __fontsPrivateFontCollection
  • PrivateFontCollection.GetFontBySize 是一个扩展方法,它返回一个 FontFamily
  • RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
  • int __minimumSize = 8;
  • int __startingSize = 48;
  • Brush __brush = Brushes.White;
  • int size 从 48 开始并在该循环内递减
  • Graphics gSmoothingMode.AntiAliasTextRenderingHint.AntiAlias 设置
  • contextSystem.Web.HttpContext(这是 ProcessRequest 方法的摘录 IHttpHandler

其他方法有:

private static RectangleF GetStringBounds(string text, Font font,
    StringFormat fmt)  
{  
    CharacterRange[] range = { new CharacterRange(0, text.Length) };  
    StringFormat myFormat = fmt.Clone() as StringFormat;  
    myFormat.SetMeasurableCharacterRanges(range);  

    using (Graphics g = Graphics.FromImage(new Bitmap(
       (int) __textBoundingBox.Width - 1,
       (int) __textBoundingBox.Height - 1)))
    {
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

        Region[] regions = g.MeasureCharacterRanges(text, font,
            __textBoundingBox, myFormat);
        return regions[0].GetBounds(g);
    }  
}

public static string Size(this RectangleF rect)
{
    return rect.Width + "×" + rect.Height;
}

public static bool IsLargerThan(this RectangleF a, RectangleF b)
{
    return (a.Width > b.Width) || (a.Height > b.Height);
}

现在我有两个问题。

首先,文本 sometimes 坚持通过在单词中插入换行符来换行,而此时它不适合并导致 while 循环再次递减。我不明白为什么Graphics.MeasureCharacterRanges 认为它不应该在一个单词中自动换行时它适合这个框。无论使用何种字符集(我用拉丁字母单词以及 Unicode 范围的其他部分,如西里尔文、希腊文、格鲁吉亚文和亚美尼亚文),都会表现出这种行为。我应该使用某些设置来强制 Graphics.MeasureCharacterRanges 仅在空白字符(或连字符)处自动换行吗?这第一个问题和post 2499067一样。

其次,在放大到新的图像和字体大小时,Graphics.MeasureCharacterRanges 给了我很大的高度。我在其中绘制的RectangleF 对应于图像的视觉明显区域,因此我可以很容易地看到文本何时减少到超出必要的程度。然而,当我向它传递一些文本时,GetBounds 调用给我的高度几乎是它实际占用的两倍。

使用反复试验设置 __minimumSize 以强制退出 while 循环,我可以看到 24pt 文本适合边界框,但 Graphics.MeasureCharacterRanges 报告该文本的高度,一旦呈现到图像是 122 像素(当边界框高 64 像素并且它适合该框时)。事实上,在不强求的情况下,while 循环迭代到 18pt,此时 Graphics.MeasureCharacterRanges 返回一个合适的值。

跟踪日志摘录如下:

将字体大小减小到 24,因为大小为 193×122,限制为 212×64
将字体大小减小到 23,因为大小为 191×117,限制为 212×64
将字体大小减小到 22,因为大小为 200×75,限制为 212×64
将字体大小减小到 21,因为大小为 192×71,限制为 212×64
将字体大小减小到 20,因为大小为 198×68,限制为 212×64
将字体大小减小到 19,因为大小为 185×65,限制为 212×64
HESSELINK 的 VENNEGOOR 用 DIN-Black 在 18pt 处书写,尺寸为 178×61,限制为 212×64

那么为什么Graphics.MeasureCharacterRanges 给我一个错误的结果?我可以理解它是,比如说,如果循环在 21pt 附近停止时字体的行高(如果我截取结果并在 Paint.Net 中测量它,这在视觉上很合适),但它比它应该做的要远得多因为,坦率地说,它返回了错误的结果。

【问题讨论】:

  • +1 是我在一段时间内看到的记录最多的问题之一!

标签: gdi+ system.drawing drawstring


【解决方案1】:

好吧,晚了 4 年,但这个问题完全符合我的症状,而且我实际上已经找出了原因。

MeasureString 和 MeasureCharacterRanges 中肯定存在错误。

简单的答案是: 确保将宽度限制(MeasureString 中的 int 宽度或 MeasureCharacterRanges 中 boundingRect 的 Size.Width 属性)除以 0.72。当您返回结果时,将每个维度乘以 0.72 以获得真实结果

int measureWidth = Convert.ToInt32((float)width/0.72);
SizeF measureSize = gfx.MeasureString(text, font, measureWidth, format);
float actualHeight = measureSize.Height * (float)0.72;

float measureWidth = width/0.72;
Region[] regions = gfx.MeasureCharacterRanges(text, font, new RectangleF(0,0,measureWidth, format);
float actualHeight = 0;
if(regions.Length>0)
{
    actualHeight = regions[0].GetBounds(gfx).Size.Height * (float)0.72;
}

解释(我可以弄清楚)是与上下文有关的事情是触发 Measure 方法中的转换(不会在 DrawString 方法中触发)为英寸-> 点 (*72/100) .当您传入 ACTUAL 宽度限制时,它正在调整此值,因此 MEASURED 宽度限制实际上比它应该的短。然后,您的文本会比预期的更早换行,因此您得到的高度结果比预期的要长。不幸的是,转换也适用于实际高度结果,因此最好也“取消转换”该值。

【讨论】:

  • 在一个非常烦人的旁注中,我完全不知道为什么 MeasureString 需要一个 int 作为宽度。如果您将图形上下文测量单位设置为英寸,那么(由于 int)您只能将宽度限制设置为 1 英寸或 2 英寸等。完全荒谬!
【解决方案2】:

我也有类似的问题。我想知道我要绘制的文本有多大,以及它会出现在哪里,确切地说。我没有遇到换行问题,所以我想我不能帮助你。我在使用所有可用的各种测量技术时遇到了同样的问题,包括最终使用 MeasureCharacterRanges,它对左右两侧都有效,但对于高度和顶部则完全不适用。 (不过,对于一些罕见的应用程序来说,使用基线可以很好地工作。)

我最终得到了一个非常不优雅、低效但有效的解决方案,至少对于我的用例而言。我在位图上绘制文本,检查位以查看它们最终的位置,这就是我的范围。因为我主要绘制小字体和短字符串,所以对我来说已经足够快了(尤其是我添加的记忆)。也许这并不完全是您所需要的,但也许它可以引导您走上正确的道路。

请注意,此时它需要编译项目以允许不安全的代码,因为我正试图从中挤出每一点效率,但如果您愿意,可以删除该约束。此外,它不像现在那样线程安全,如果需要,您可以轻松添加它。

Dictionary<Tuple<string, Font, Brush>, Rectangle> cachedTextBounds = new Dictionary<Tuple<string, Font, Brush>, Rectangle>();
/// <summary>
/// Determines bounds of some text by actually drawing the text to a bitmap and
/// reading the bits to see where it ended up.  Bounds assume you draw at 0, 0.  If
/// drawing elsewhere, you can easily offset the resulting rectangle appropriately.
/// </summary>
/// <param name="text">The text to be drawn</param>
/// <param name="font">The font to use when drawing the text</param>
/// <param name="brush">The brush to be used when drawing the text</param>
/// <returns>The bounding rectangle of the rendered text</returns>
private unsafe Rectangle RenderedTextBounds(string text, Font font, Brush brush) {

  // First check memoization
  Tuple<string, Font, Brush> t = new Tuple<string, Font, Brush>(text, font, brush);
  try {
    return cachedTextBounds[t];
  }
  catch(KeyNotFoundException) {
    // not cached
  }

  // Draw the string on a bitmap
  Rectangle bounds = new Rectangle();
  Size approxSize = TextRenderer.MeasureText(text, font);
  using(Bitmap bitmap = new Bitmap((int)(approxSize.Width*1.5), (int)(approxSize.Height*1.5))) {
    using(Graphics g = Graphics.FromImage(bitmap))
      g.DrawString(text, font, brush, 0, 0);
    // Unsafe LockBits code takes a bit over 10% of time compared to safe GetPixel code
    BitmapData bd = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    byte* row = (byte*)bd.Scan0;
    // Find left, looking for first bit that has a non-zero alpha channel, so it's not clear
    for(int x = 0; x < bitmap.Width; x++)
      for(int y = 0; y < bitmap.Height; y++)
        if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
          bounds.X = x;
          goto foundX;
        }
  foundX:
    // Right
    for(int x = bitmap.Width - 1; x >= 0; x--)
      for(int y = 0; y < bitmap.Height; y++)
        if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
          bounds.Width = x - bounds.X + 1;
          goto foundWidth;
        }
  foundWidth:
    // Top
    for(int y = 0; y < bitmap.Height; y++)
      for(int x = 0; x < bitmap.Width; x++)
        if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
          bounds.Y = y;
          goto foundY;
        }
  foundY:
    // Bottom
    for(int y = bitmap.Height - 1; y >= 0; y--)
      for(int x = 0; x < bitmap.Width; x++)
        if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
          bounds.Height = y - bounds.Y + 1;
          goto foundHeight;
        }
  foundHeight:
    bitmap.UnlockBits(bd);
  }
  cachedTextBounds[t] = bounds;
  return bounds;
}

【讨论】:

  • 干得好!我将不得不尝试一下,看看是否能解决我的问题。虽然我有点讨厌不得不依赖unsafe 代码。您找到该代码的速度有多快?我在使用它的情况下检查文本是否对于固定大小的边界框来说太大并减小while 循环内的字体大小(检查while too big or size gt hard-floor-limit),所以我不想执行昂贵的在那个while 循环中操作……
  • 速度很大程度上取决于字体大小。它分配位图,渲染它,然后搜索边界。较小的字体要快得多,而我认为使用较大的字体会输掉 O(size^2) 之类的东西。可能想猜测小尺寸并工作到太大的尺寸,而不是相反。您可以使用 MeasureCharacterRanges 作为下限的第一个猜测,因为它似乎总是以太小的猜测结束。有很多聪明的空间来寻找合适的大小,用聪明的猜测进行二分搜索等等。我不需要任何这些来达到我的目的,所以我没有玩过。
  • 至于不安全,您可以不这样做,但速度会受到影响。正如所评论的,这比使用 GetPixel 快了近 90%,但您仍然可以使用 LockBits 进行中间方法,而无需不安全的代码。我没有尝试过,因为我对不安全的代码没意见,并且想挤出一些容易获得的性能。有很多方法可以根据您使用这些东西的准确程度来尝试挤出性能,但它们都很复杂。我完全意识到这整件事有点恶心。我认为会有更好的方法,但我的在线搜索和你的一样没有效率。
  • 这真的很有帮助,user12861;谢谢你。希望我很快有机会尝试一下。
【解决方案3】:

您可以尝试删除以下行吗?

fmt.FormatFlags = StringFormatFlags.NoClip;

字形的悬垂部分,以及 展开的文本到达外部 允许格式化矩形 节目。默认所有文本和字形 超出格式的部分 矩形被剪裁了。

这是我能想到的最好的:(

【讨论】:

  • 感谢您发布答案。我确实看过这个,认为这可能是问题所在,但报告的高度几乎是实际高度的两倍,所以我不认为可能是这样。似乎 StringFormatFlags.NoClip 的不同之处在于,如果字母 P 的碗(例如)只是戳到边界框之外,那么它被允许渲染,而不是被剪裁。这似乎不是我遇到的问题。但是谢谢 :o)
【解决方案4】:

MeasureCharacterRanges 方法也有一些问题。它给我相同的字符串甚至相同的Graphics 对象的大小不一致。然后我发现它取决于 layoutRect 参数的值 - 我不明白为什么,在我看来这是 .NET 代码中的错误。

例如,如果 layoutRect 完全为空(所有值都设置为零),我得到了字符串“a”的正确值 - 大小为 {Width=8.898438, Height=18.10938},使用 12pt Ms Sans Serif 字体。

但是,当我将矩形的“X”属性的值设置为非整数(如 1.2)时,它给了我{Width=9, Height=19}

所以我真的认为当您使用具有非整数 X 坐标的布局矩形时存在错误。

【讨论】:

  • 有趣 — 看起来它总是在向上取整您的尺寸。不幸的是,我的 layoutRect 始终是不可分割的——它被定义为 private static readonly RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);并且它的值永远不会改变(显然,因为它被标记为只读)。这绝对是 .Net 代码中的错误,但看起来你的错误和我的错误并不相同。
【解决方案5】:

要将点数转换为屏幕分辨率的 dpi,您需要除以 72 并乘以 DPI,例如: graphics.DpiY * text.Width / 72

Red Nightengale 非常接近,因为对于屏幕分辨率,graphics.DpiY 通常为 96。

【讨论】:

  • 不过,这并不是整个问题的答案。您能否将其扩展为一个答案——包括您需要复制的任何红夜莺的答案?或者,将此作为对 Red Nightingale 答案的评论发布?谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-03
  • 2017-06-15
  • 2010-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多