3

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.

Alexander van Oostenrijk
  • 4,644
  • 3
  • 23
  • 37
Danial
  • 542
  • 2
  • 9
  • 24
  • You must work with a Bitmap, then call [`Bitmap.Save`](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.image.save?view=netframework-4.8). A Bitmap object itself is a 3 dimensional array (X, Y, RGB), therefore it should be possible for you to completely bypass having a 2d Color array simply by using the Bitmap instead of the Color array. The Bitmap object contains functions to set/get pixel colors at specific coordinates. – Prime Dec 23 '19 at 11:21
  • To draw something you need Color and Shape. You can't draw with colors alone. – Krivitskiy Grigoriy Dec 23 '19 at 11:21
  • 1
    OP and up-voter should perhaps take a refresher of [ask] –  Dec 23 '19 at 11:23
  • Is this WinForms? WPF? –  Dec 23 '19 at 11:23

2 Answers2

4

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:

  • The backing array has 32-bit entries, since we are working with a 32-bit pixel format
  • The 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).

Alexander van Oostenrijk
  • 4,644
  • 3
  • 23
  • 37
3
// 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);
CorrM
  • 498
  • 6
  • 18