Graphics.MeasureCharacterRanges提供错误的大小计算

时间:2010-04-21 16:53:23

标签: gdi+ system.drawing drawstring

我正在尝试将一些文本渲染到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的{​​{1}}方法的摘录)

其他方法是:

IHttpHandler

现在我有两个问题。

首先,文本有时通过在单词中插入换行符来坚持换行,当它应该不适合并导致while循环再次递减时。我不明白为什么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); } 认为当它不应该在一个单词中包装时,这适合于框内。无论使用何种字符集,我都会表现出这种行为(我用拉丁字母词,以及Unicode范围的其他部分,如西里尔语,希腊语,格鲁吉亚语和亚美尼亚语)。是否有一些设置我应该使用强制Graphics.MeasureCharacterRanges只是在空格字符(或连字符)上换行?第一个问题与post 2499067相同。

其次,在扩展到新的图像和字体大小时,Graphics.MeasureCharacterRanges给了我极高的高度。我正在绘制的Graphics.MeasureCharacterRanges对应于图像的视觉上明显的区域,因此我可以很容易地看到文本的减少时间超过了必要的范围。然而,当我传递一些文字时,RectangleF电话给我的高度几乎是它实际拍摄的两倍。

使用试验和错误设置GetBounds强制退出while循环,我可以看到24pt文本适合边界框,但__minimumSize报告该文本的高度,一旦渲染到图像,是122px(当边界框高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
  以18pt在DIN-Black中编写HESSELINK的VENNEGOOR,尺寸为178×61,限制为212×64

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

5 个答案:

答案 0 :(得分:1)

我有类似的问题。我想知道我正在绘制的文本有多大,以及它将在何处出现,确切地说。我没有断线问题,所以我认为我不能帮助你。我遇到了与所有可用的各种测量技术相同的问题,包括最终使用MeasureCharacterRanges,这对于左侧和右侧都可以正常工作,但对于高度和顶部都没有。 (使用基线可以很好地适用于一些罕见的应用程序。)

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

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

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

答案 1 :(得分:0)

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

fmt.FormatFlags = StringFormatFlags.NoClip;
  

突出部分字形,和   打开文本到达外面   允许格式化矩形   节目。默认情况下所有文字和字形   零件到达格式之外   矩形被剪裁。

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

答案 2 :(得分:0)

我也遇到了MeasureCharacterRanges方法的一些问题。它给了我不同大小的相同字符串甚至相同的Graphics对象。然后我发现它取决于layoutRect参数的值 - 我看不出原因,在我看来这是.NET代码中的一个错误。

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

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

所以我认为使用非整数X坐标的布局矩形时会出现错误。

答案 3 :(得分:0)

好的,所以晚了4年,但这个问题与我的症状完全吻合,我实际上已经找到了原因。

MeasureString和MeasureCharacterRanges中肯定存在一个错误。

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

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方法中触发)为inch-&gt;点(* 72/100) )。当您传入ACTUAL宽度限制时,它正在调整此值,因此MEASURED宽度限制实际上比它应该更短。然后您的文本会比预期更早地包裹,因此您获得的结果比预期的更长。不幸的是,转换也适用于实际的高度结果,因此最好“转换”该值。

答案 4 :(得分:0)

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

红色Nightengale真的很近,因为图形。对于屏幕分辨率,DpiY通常为96。