C# Bitmap created programmatically; crop code started giving “A generic error occurred in GDI+”

十年热恋 提交于 2019-12-08 09:10:16

问题


I have the following method that renders text into an image. It makes a larger than necessary bitmap, draws the text, then hunts the bitmap for blank space and crops it off. At the point where the image is saved, it throws the error "A generic error occurred in GDI+". This code has always worked on this same machine, that I develop on, though it hasn't been run in a long time so a reasonable amount of windows updates are likely to have occurred since the last time it worked. Nothing else has changed, to my knowledge re the solution/.net framework etc - I just opened the solution, ran it in debug (like always), and it produced the error

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

The helper method that searches for completely blank rows of pixels is:

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

The helper functions OK, returns me a rect I'm expecting.

Is anyone else able to repro the GDI error on their machine (I don't have another machine here to test as a compare to see if it's affecting just my machine)? Any pointers as to how to diagnose the cause? A read that a lot of these kinds of errors relate to closing the stream the bitmap is resting on, but in this case there is no stream; the bitmap isn't loaded from anywhere - it's created entirely in the code..


回答1:


While the Graphics object exists, the image object is considered to be in a state of being edited. The image is only considered "done" after the graphics object is disposed. You attempt to save the image before disposing that Graphics object, and that can cause problem. Adding proper using blocks to your code should solve this problem completely.

Except, that is, if the real problem is in the AllPixelsOnHorizontalLineMatch or AllPixelsOnVerticalLineMatch tools, which you didn't include in your question. If they do something that might mess up the GDI+ object, then that can affect the saving you do later.

Anyway, here's your function rewritten with proper using blocks:

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

I didn't feel like rewriting these missing tool functions, since it's much more efficient to work with bytes all the way through and pass those on to these tools function, so I just ended up writing my own crop function altogether. I'm not sure it does exactly what yours does, but the constrained search area and the border thing seemed to work, so here it is, for reference:

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

Note that you may want to use a more accurate way to determine the size needed for the image. The .Net framework has inbuilt methods for that. Also note that since you always paint to (0,0), the 5-pixel border that the crop function leaves tends to not work at the top. Given the complete inaccuracy of the original image size estimation, I also have no idea why the "highlight:" prefix gives that constraining rectangle (based on said inaccurate image size) to the crop function.

I messed around a little when fiddling with all that stuff, and wondered if the StartsWith calls actually meant that the symbols were supposed to act as prefix rather than the whole string... so I ended up implementing it that way. Here's the final rewritten function. It automatically does vertical centering of the smaller font on the larger one.

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


来源:https://stackoverflow.com/questions/48783526/c-sharp-bitmap-created-programmatically-crop-code-started-giving-a-generic-err

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!