以编程方式创建C#位图;裁剪代码开始提供“GDI +中发生一般错误”

时间:2018-02-14 09:21:48

标签: c# bitmap gdi+

我有以下方法将文本呈现为图像。它创建一个大于必要的位图,绘制文本,然后搜索空白空间的位图并将其裁剪掉。在保存图像的位置,它会抛出错误“GDI +中发生一般错误”。这个代码一直在我开发的同一台机器上工作,虽然它没有在很长一段时间内运行,所以自上次工作以来可能会发生合理数量的Windows更新。没有其他任何改变,据我所知,解决方案/ .net框架等 - 我刚刚打开解决方案,在调试中运行它(像往常一样),并产生错误

private void CreateImageFromText(string text, string filename){
  // Set global stage dimensions
  int stageWidth = (int)(text.Length * 3 * _fontSizeNumericUpDown.Value);
  int stageHeight = (int)(3 * _fontSizeNumericUpDown.Value);

  // Create Bitmap placeholder for new image       
  Bitmap createdImage = new Bitmap(stageWidth, stageHeight);
  Color blankPixel = createdImage.GetPixel(0, 0);

  // Draw new blank image
  Graphics imageCanvas = Graphics.FromImage(createdImage);
  imageCanvas.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
  imageCanvas.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
  // Add text
  if (!string.IsNullOrEmpty(text))
  {
    Font font = new Font("Arial", (int)_fontSizeNumericUpDown.Value);
    Font bigFont = new Font("Arial", (int)(_fontSizeNumericUpDown.Value * (decimal)1.25));
    Font veryBigFont = new Font("Arial", (int)(_fontSizeNumericUpDown.Value * (decimal)3));

    if(text.StartsWith("tick:"))
      imageCanvas.DrawString("✔", bigFont, Brushes.Green, 0, 0);
    else if (text.StartsWith("cross:"))
        imageCanvas.DrawString("X", bigFont, Brushes.Red, 0, 0);
    else if (text.StartsWith("highlight:"))
        imageCanvas.DrawString("•", veryBigFont, Brushes.Magenta, 0, 0);
    else
      imageCanvas.DrawString(text, font, Brushes.Black, 0, 0);
  }

  //clip to only part containing text
  Rectangle r = ImageUtils.GetBoundsThatContainData(
      createdImage, 
      blankPixel, 
      searchArea: (text.StartsWith("highlight:") ? new Rectangle?(new Rectangle(10, 20, createdImage.Width - 10, createdImage.Height - 20)) : null)
  );

  // Save cropped
  var img = createdImage.Clone(r, createdImage.PixelFormat);
  img.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
  imageCanvas.Dispose();
  createdImage.Dispose();
}

搜索完整空白像素行的辅助方法是:

public static Rectangle GetBoundsThatContainData(Bitmap createdImage,Color blankPixel,int borderSizePixels = 5,Rectangle?searchArea = null)     {       Rectangle sa = new Rectangle(0,0,createdImage.Width,createdImage.Height);

  if (searchArea.HasValue)
  {
    if (searchArea.Value.X > sa.X)
      sa.X = searchArea.Value.X;

    if (searchArea.Value.Y > sa.Y)
      sa.Y = searchArea.Value.Y;

    if (searchArea.Value.Width < sa.Width)
      sa.Width = searchArea.Value.Width;

    if (searchArea.Value.Height < sa.Height)
      sa.Height = searchArea.Value.Height;
  }

  //look for vertical
  for (int i = (sa.Y + sa.Height) - 1; i >= sa.Y; i--)
  {
    if (!AllPixelsOnHorizontalLineMatch(blankPixel, i, sa, createdImage))
    {
      sa.Height = (i - sa.Y) + 1 + borderSizePixels;
      break;
    }
  }

  if (sa.Y + sa.Height > createdImage.Height)
    sa.Height = createdImage.Height - sa.Y;

  //look for the horizontal
  for (int i = (sa.X + sa.Width) - 1; i >= sa.X; i--)
  {
    if (!AllPixelsOnVerticalLineMatch(blankPixel, i, sa, createdImage))
    {
      sa.Width = (i - sa.X) + 1 + borderSizePixels;
      break;
    }
  }

  if (sa.X + sa.Width > createdImage.Width)
    sa.Width = createdImage.Width - sa.X;

  return sa;
}

帮助器功能正常,给我一个我期待的矩形。

是否有其他人能够在他们的机器上重现GDI错误(我在这里没有其他机器作为比较来测试它是否只影响我的机器)?有关如何诊断原因的任何指示?读取很多这类错误与关闭流位图所依赖的流有关,但在这种情况下没有流;位图不是从任何地方加载的 - 它完全是在代码中创建的。

1 个答案:

答案 0 :(得分:1)

Graphics对象存在时,图像对象被认为处于被编辑状态。图像仅被视为&#34;完成&#34;在处理图形对象之后。您尝试在处置Graphics对象之前保存图像,这可能会导致问题。在代码中添加适当的using块应该可以完全解决这个问题。

除此之外,如果真正的问题出现在AllPixelsOnHorizontalLineMatchAllPixelsOnVerticalLineMatch工具中,那么您在问题中并未包含这些问题。如果他们做了可能搞乱GDI +对象的事情,那么这会影响你以后的保存。

无论如何,这里的功能是用适当的using块重写的:

public static void CreateImageFromText(String text, String filename, Int32 fontSize)
{
    // Set global stage dimensions
    Int32 stageWidth = (Int32)(text.Length * 3 * fontSize);
    Int32 stageHeight = (Int32)(3 * fontSize);

    using (Bitmap createdImage = new Bitmap(stageWidth, stageHeight))
    {
        Color blankPixel = createdImage.GetPixel(0, 0);
        // Draw new blank image
        using (Graphics imageCanvas = Graphics.FromImage(createdImage))
        {
            imageCanvas.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            imageCanvas.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
            // Add text
            if (!string.IsNullOrEmpty(text))
            {
                if (text.StartsWith("tick:"))
                    using (Font bigFont = new Font("Arial", (Int32)(fontSize * (decimal)1.25)))
                        imageCanvas.DrawString("✔", bigFont, Brushes.Green, 0, 0);
                else if (text.StartsWith("cross:"))
                    using (Font bigFont = new Font("Arial", (Int32)(fontSize * (decimal)1.25)))
                        imageCanvas.DrawString("X", bigFont, Brushes.Red, 0, 0);
                else if (text.StartsWith("highlight:"))
                    using (Font veryBigFont = new Font("Arial", (Int32)(fontSize * (decimal)3)))
                        imageCanvas.DrawString("•", veryBigFont, Brushes.Magenta, 0, 0);
                else
                    using (Font font = new Font("Arial", (Int32)fontSize))
                        imageCanvas.DrawString(text, font, Brushes.Black, 0, 0);
            }
        }
        // Honestly not sure what the point of this is, especially given the complete inaccuracy of the original image size calculation.
        Rectangle? searchArea = text.StartsWith("highlight:") ? new Rectangle(10, 20, createdImage.Width - 10, createdImage.Height - 20) : (Rectangle?)null;
        Rectangle r = ImageUtils.GetCropBounds(createdImage, blankPixel, searchArea: searchArea);
        // Save cropped
        using (Image img = createdImage.Clone(r, createdImage.PixelFormat))
            img.Save(filename, ImageFormat.Png);
    }
}

我没有想要重写这些缺少的工具函数,因为使用字节一直处理并将这些函数传递给这些工具函数要高效得多,所以我最后编写了我的函数完全拥有自己的作物功能。我不确定完全你做了什么,但是受约束的搜索区域和边框似乎有用,所以这里是参考:

public static Rectangle GetCropBounds(Bitmap image, Color blankPixel, Int32 borderSizePixels = 5, Rectangle? searchArea = null)
{
    // Not too worried about the other boundaries; the "for" loops will exclude those anyway.
    Int32 yStart = searchArea.HasValue ? Math.Max(0, searchArea.Value.Y) : 0;
    Int32 yEnd   = searchArea.HasValue ? Math.Min(image.Height, searchArea.Value.Y + searchArea.Value.Height) : image.Height;
    Int32 xStart = searchArea.HasValue ? Math.Max(0, searchArea.Value.X) : 0;
    Int32 xEnd   = searchArea.HasValue ? Math.Min(image.Width, searchArea.Value.X + searchArea.Value.Width) : image.Width;
    // Values to calculate
    Int32 top;
    Int32 bottom;
    Int32 left;
    Int32 right;
    // Convert to 32bppARGB and get bytes and stride out.
    Byte[] data;
    Int32 stride;
    using (Bitmap bm = new Bitmap(image))
    {
        BitmapData sourceData = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat);
        stride = sourceData.Stride;
        data = new Byte[stride*bm.Height];
        Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
        bm.UnlockBits(sourceData);
    }
    // ============= Y =============
    // Top = first found row which contains data
    for (top = yStart; top < yEnd; top++)
    {
        Int32 index = top * stride;
        if (!RowClear(data, index, 4, xStart, xEnd, blankPixel))
            break;
    }
    // Sanity check: no data on image. Abort.
    if (top == yEnd)
        return new Rectangle(xStart, yStart, 0, 0);
    // Bottom = last found row which contains data
    for (bottom = yEnd - 1; bottom > top; bottom--)
    {
        Int32 index = bottom * stride;
        if (!RowClear(data, index, 4, xStart, xEnd, blankPixel))
            break;
    }
    // Make bottom the first actually clear row.
    bottom++;
    // ============= X =============
    // Left = first found column which contains data
    for (left = xStart; left < xEnd; left++)
    {
        Int32 index = left * 4;
        if (!ColClear(data, index, stride, yStart, yEnd, blankPixel))
            break;
    }
    // Right = last found row which contains data
    for (right = xEnd - 1; right > left; right--)
    {
        Int32 index = right * 4;
        if (!ColClear(data, index, stride, yStart, yEnd, blankPixel))
            break;
    }
    // Make right the first actually clear column
    right++;
    // Calculate final rectangle values, including border.
    Int32 rectX = Math.Max(0, left - borderSizePixels);
    Int32 rectY = Math.Max(0, top - borderSizePixels);
    Int32 rectW = Math.Min(image.Width, right + borderSizePixels) - rectX;
    Int32 rectH = Math.Min(image.Height, bottom + borderSizePixels) - rectY;
    return new Rectangle(rectX, rectY, rectW, rectH);
}

public static Boolean RowClear(Byte[] data, Int32 index, Int32 pixelWidth, Int32 xStart, Int32 xEnd, Color blankPixel)
{
    Boolean rowOk = true;
    Int32 start = index + pixelWidth * xStart;
    Int32 end = index + pixelWidth * xEnd;
    for (Int32 x = start; x < end; x += pixelWidth)
    {
        if      (blankPixel.A != data[x + 3]) rowOk = false;
        else if (blankPixel.R != data[x + 2]) rowOk = false;
        else if (blankPixel.G != data[x + 1]) rowOk = false;
        else if (blankPixel.B != data[x + 0]) rowOk = false;
        if (!rowOk)
            return false;
    }
    return true;
}

public static Boolean ColClear(Byte[] data, Int32 index, Int32 stride, Int32 yStart, Int32 yEnd, Color blankPixel)
{
    Boolean colOk = true;
    Int32 start = index + stride * yStart;
    Int32 end = index + stride * yEnd;
    for (Int32 y = start; y < end; y += stride)
    {
        if      (blankPixel.A != data[y + 3]) colOk = false;
        else if (blankPixel.R != data[y + 2]) colOk = false;
        else if (blankPixel.G != data[y + 1]) colOk = false;
        else if (blankPixel.B != data[y + 0]) colOk = false;
        if (!colOk)
            return false;
    }
    return true;
}

请注意,您可能希望使用更准确的方法来确定图像所需的大小。 .Net框架有内置的方法。另请注意,由于您总是绘制到(0,0),裁剪功能所留下的5像素边框往往不能在顶部工作。鉴于原始图像大小估计的完全不准确,我也不知道为什么&#34;突出显示:&#34;前缀为裁剪函数提供约束矩形(基于所述不准确的图像大小)。

当摆弄所有这些东西时,我搞砸了一下,并想知道StartsWith调用是否实际意味着符号应该作为前缀而不是整个字符串。所以我最终以这种方式实施它。这是最终重写的功能。它会自动对较大字体的垂直居中进行垂直居中。

public static void CreateImageFromText(String text, String filename, Int32 fontSize, Int32 padding)
{
    if (text == null)
        text = String.Empty;
    Boolean prefixTick = text.StartsWith("tick:");
    Boolean prefixCross = !prefixTick && text.StartsWith("cross:");
    Boolean highlight = !prefixTick && !prefixCross && text.StartsWith("highlight:");
    const String symbTick = "✔";
    const String symbCross = "X";
    const String symbBullet = "•";
    // Cut off the prefix part
    if (prefixTick || prefixCross || highlight)
        text = text.Substring(text.IndexOf(":", StringComparison.Ordinal) + 1).TrimStart();
    using (Font font = new Font("Arial", fontSize))
    using (Font prefixFont = new Font("Arial", fontSize * (highlight ? 3f : 1.25f), highlight ? FontStyle.Bold : FontStyle.Regular))
    {
        // Calculate accurate dimensions of required image.
        Single textWidth;
        Single prefixWidth = 0;
        Single requiredHeight = 0;
        Single textHeight;
        Single prefixHeight = 0;
        // Dummy image will have the same dpi as the final one.
        using (Bitmap dummy = new Bitmap(1, 1))
        using (Graphics g = Graphics.FromImage(dummy))
        {
            if (prefixTick)
            {
                SizeF tickSize = g.MeasureString(symbTick, prefixFont);
                requiredHeight = Math.Max(tickSize.Height, requiredHeight);
                prefixWidth = tickSize.Width;
            }
            else if (prefixCross)
            {
                SizeF crossSize = g.MeasureString(symbCross, prefixFont);
                requiredHeight = Math.Max(crossSize.Height, requiredHeight);
                prefixWidth = crossSize.Width;
            }
            else if (highlight)
            {
                SizeF bulletSize = g.MeasureString(symbBullet, prefixFont);
                requiredHeight = Math.Max(bulletSize.Height, requiredHeight);
                prefixWidth = bulletSize.Width;
            }
            prefixHeight = requiredHeight;
            SizeF textSize = g.MeasureString(text.Length == 0 ? " " : text, font);
            textWidth = text.Length == 0 ? 0 : textSize.Width;
            textHeight= textSize.Height;
            requiredHeight = Math.Max(textSize.Height, requiredHeight);
        }
        if (!prefixTick && !prefixCross && !highlight && text.Length == 0)
        {
            Int32 width = padding*2;
            Int32 height = (Int32)Math.Round(textHeight + padding*2, MidpointRounding.AwayFromZero);

            if (width == 0)
                width = 1;
            // Creates an image of the expected height for the font, and a width consisting of only the padding, or 1 for no padding.
            using (Image img = new Bitmap(width, height))
                img.Save(filename, ImageFormat.Png);
            return;
        }
        Single prefixX = 5;
        Single prefixY = 5 + padding + prefixWidth > 0 && requiredHeight > prefixHeight ? (requiredHeight - prefixHeight) / 2 : 0;
        Single textX = 5 + prefixWidth;
        Single textY = 5 + padding + requiredHeight > textHeight ? (requiredHeight - textHeight) / 2 : 0;
        // Set global stage dimensions. Add 10 Pixels to each to allow for 5-pixel border.
        Int32 stageWidth = (Int32)Math.Round(prefixWidth + textWidth, MidpointRounding.AwayFromZero) + 10 + padding * 2;
        Int32 stageHeight = (Int32)Math.Round(requiredHeight, MidpointRounding.AwayFromZero) + 10 + padding * 2;
        // Create Bitmap placeholder for new image       
        using (Bitmap createdImage = new Bitmap(stageWidth, stageHeight))
        {
            Color blankPixel = createdImage.GetPixel(0, 0);
            // Draw new blank image
            using (Graphics imageCanvas = Graphics.FromImage(createdImage))
            {
                imageCanvas.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
                imageCanvas.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
                // Add text
                if (prefixTick)
                    imageCanvas.DrawString(symbTick, prefixFont, Brushes.Green, prefixX, prefixY);
                else if (prefixCross)
                    imageCanvas.DrawString(symbCross, prefixFont, Brushes.Red, prefixX, prefixY);
                else if (highlight)
                    imageCanvas.DrawString(symbBullet, prefixFont, Brushes.Magenta, prefixX, prefixY);
                if (text.Length > 0) 
                    imageCanvas.DrawString(text, font, Brushes.Black, textX, textY);
            }
            //clip to only part containing text. 
            Rectangle r = ImageUtils.GetCropBounds(createdImage, blankPixel, padding);
            if (r.Width <= 0 || r.Height <= 0)
                return; // Possibly throw exception; image formats can't handle 0x0.
            // Save cropped
            createdImage.Save(Path.Combine(Path.GetDirectoryName(filename), Path.GetFileNameWithoutExtension(filename)) + "_orig" + Path.GetExtension(filename), ImageFormat.Png);
            using (Image img = createdImage.Clone(r, createdImage.PixelFormat))
                img.Save(filename, ImageFormat.Png);
        }
    }
}