问题
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