I have a Color[,]
array (2d array of Drawing.Color
) in C#. How can I save that locally as a PNG?
Using only .Net official packages provided in the build, no additional library or Nuget packages.
I have a Color[,]
array (2d array of Drawing.Color
) in C#. How can I save that locally as a PNG?
Using only .Net official packages provided in the build, no additional library or Nuget packages.
The simple way
First create a blank Bitmap
instance with the desired dimensions:
Bitmap bmp = new Bitmap(100, 100);
Loop through your colors array and plot pixels:
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
bmp.SetPixel(i, j, colors[i,j]);
}
}
Finally, save your bitmap to file:
bmp.Save("myfile.png", ImageFormat.Png);
The faster way
The Bitmap.SetPixel
method is slow. A much faster way of accessing a bitmap's pixels is by writing directly to an array of 32-bit values (assuming you're shooting for a 32-bit PNG), and have the Bitmap
class use that array as its backing.
One way to do this is by creating said array, and getting a GCHandle
on it to prevent it from being garbage-collected. The Bitmap
class offers a constructor that allows you to create an instance from an array pointer, a pixel format, and a stride (the number of bytes that make up a single row of the pixel data):
public Bitmap (int width, int height, int stride,
System.Drawing.Imaging.PixelFormat format, IntPtr scan0);
This is how you would create a backing array, a handle, and a Bitmap
instance:
Int32[] bits = new Int32[width * height];
GCHandle handle = GCHandle.Alloc(bits, GCHandleType.Pinned);
Bitmap bmp = new Bitmap(width, height, width * 4,
PixelFormat.Format32bppPArgb, handle.AddrOfPinnedObject());
Note that:
Bitmap
stride is width*4
, which is the number of bytes a single row of pixels takes up (4 bytes per pixel)With this, you can now write pixel values directly into the backing array, and they'll be reflected in the Bitmap
. This is much faster than using Bitmap.SetPixel
. Here is a code sample, which assumes that you've wrapped up everything in a class that knows how wide and tall the bitmap is:
public void SetPixelValue(int x, int y, int color)
{
// Out of bounds?
if (x < 0 || x >= Width || y < 0 || y >= Height) return;
int index = x + (y * Width);
Bits[index] = color;
}
Please note that color
is an int
value, not a Color
value. If you have an array of Color
values, you'll have to convert each to an int
first, e.g.
public void SetPixelColor(int x, int y, Color color)
{
SetPixelValue(x, y, color.ToArgb());
}
This conversion will take time, so it's better to work with int
values all the way. You can make this faster still by forgoing the x/y bounds check, if you're sure you're never using out-of-bounds coordinates:
public void SetPixelValueUnchecked(int x, int y, int color)
{
// No out of bounds checking.
int index = x + (y * Width);
Bits[index] = color;
}
A caveat is in order here. If you wrap Bitmap
this way, you'll still be able to use Graphics
to draw things like lines, rectangles, circles etc. by accessing the Bitmap
instance directly, but without the speed gain of going through the pinned array. If you want these primitives to be drawn more quickly as well, you'll have to provide your own line/circle implementations. Note that in my experience, your own Bresenham line routine will hardly outperform GDI's built-in one, so it may not be worth it.
An even faster way
Things could be faster still if you're able to set multiple pixels in one go. This would apply if you have a horizontal sequence of pixels with the same color value. The fastest way I've found of setting sequences in an array is using Buffer.BlockCopy
. (See here for a discussion). Here is an implementation:
/// <summary>
/// Set a sequential stretch of integers in the bitmap to a specified value.
/// This is done using a Buffer.BlockCopy that duplicates its size on each
/// pass for speed.
/// </summary>
/// <param name="value">Fill value</param>
/// <param name="startIndex">Fill start index</param>
/// <param name="count">Number of ints to fill</param>
private void FillUsingBlockCopy(Int32 value, int startIndex, int count)
{
int numBytesInItem = 4;
int block = 32, index = startIndex;
int endIndex = startIndex + Math.Min(block, count);
while (index < endIndex) // Fill the initial block
Bits[index++] = value;
endIndex = startIndex + count;
for (; index < endIndex; index += block, block *= 2)
{
int actualBlockSize = Math.Min(block, endIndex - index);
Buffer.BlockCopy(Bits, startIndex * numBytesInItem, Bits, index * numBytesInItem, actualBlockSize * numBytesInItem);
}
}
This would be particularly useful when you need a fast way to clear the bitmap, fill a rectangle or a triangle using horizontal lines (for example after triangle rasterization).
// Color 2D Array
var imgColors = new Color[128, 128];
// Get Image Width And Height Form Color Array
int imageH = imgColors.GetLength(0);
int imageW = imgColors.GetLength(1);
// Create Image Instance
Bitmap img = new Bitmap(imageW, imageH);
// Fill Colors on Our Image
for (int x = 0; x < img.Width; ++x)
{
for (int y = 0; y < img.Height; ++y)
{
img.SetPixel(x, y, imgColors[x, y]);
}
}
// Just Save it
img.Save("image.png", ImageFormat.Png);