I found this answer showing how to create a bitmap using PixelFormat.Format8bppIndexed
and wanted to adapt it to cover the other indexed formats (Format1bppIndexed
and Format4bppIndexed
) and to use a palette supplied by me rather than a greyscale palette. This is the resulting code (I've omitted a bunch of validation code in the interests of brevity):
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
/// <summary>
/// Static class to help with creating a Bitmap using one of the indexed pixel formats.
/// </summary>
[SupportedOSPlatform("windows")]
public static class IndexedBitmapHelper
{
/// <summary>
/// Sets the palette and pixels of an indexed Bitmap.
/// </summary>
/// <param name="destinationBitmap">The Bitmap to populate.</param>
/// <param name="colourTable">
/// An array of the possible colours that the Bitmap's pixels can be set to.
/// </param>
/// <param name="colourIndexes">
/// Each entry in this array represents one pixel and is an index into the <paramref name="colourTable"/>.
/// </param>
public static void Populate(
Bitmap destinationBitmap,
Color[] colourTable,
byte[] colourIndexes)
{
SetPalette(destinationBitmap, colourTable);
SetPixels(destinationBitmap, colourIndexes);
}
private static void SetPalette(Bitmap destinationBitmap, Color[] colourTable)
{
var numberOfColours = colourTable.Length;
// The Palette property is of type ColorPalette, which doesn't have a public constructor
// because that would allow people to change the number of colours in a Bitmap's palette.
// So instead of creating a new ColorPalette, we need to take a copy of the one created
// by the Bitmap constructor.
var copyOfPalette = destinationBitmap.Palette;
for (var i = 0; i < numberOfColours; i++)
{
copyOfPalette.Entries[i] = colourTable[i];
}
destinationBitmap.Palette = copyOfPalette;
}
private static void SetPixels(Bitmap destinationBitmap, byte[] colourIndexes)
{
var width = destinationBitmap.Width;
var height = destinationBitmap.Height;
var data = destinationBitmap.LockBits(
new Rectangle(0, 0, width, height),
ImageLockMode.WriteOnly,
PixelFormat.Format8bppIndexed); // <--- why???
var dataOffset = 0;
var scanPtr = data.Scan0.ToInt64();
for (var y = 0; y < height; ++y)
{
// Copy one row of pixels from the colour indexes to the destination bitmap
Marshal.Copy(colourIndexes, dataOffset, new IntPtr(scanPtr), width);
dataOffset += width;
scanPtr += data.Stride;
}
destinationBitmap.UnlockBits(data);
}
}
Example of usage, to create a 1bpp (two colours) image and display it in a PictureBox
on a Windows form:
var width = 10;
var height = 12;
var colourTable = new Color[] { Color.Red, Color.Green };
var colourIndexes = new byte[]
{
0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
1, 0, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 1, 0,
1, 0, 0, 0, 0, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
};
using var destinationBitmap = new Bitmap(width, height, PixelFormat.Format1bppIndexed);
IndexedBitmapHelper.Populate(destinationBitmap, colourTable, colourIndexes);
pictureBox1.Image = destinationBitmap;
This works just fine, however I'm confused by this line in the SetPixels
method:
var data = destinationBitmap.LockBits(
new Rectangle(0, 0, width, height),
ImageLockMode.WriteOnly,
PixelFormat.Format8bppIndexed); // <--- why???
The documentation for the LockBits
method tells me that the third parameter is
A
PixelFormat
enumeration that specifies the data format of this Bitmap
So I should use destinationBitmap.PixelFormat
, i.e. the PixelFormat
of the Bitmap
I'm creating, as the value of this parameter, right? Apparently not, because when I do that and create a 1bpp or 4bpp bitmap, the pixels all seem to be in the wrong place. For the 1bpp example above, this results in a bitmap which looks like this (R = red pixel, G = green pixel)
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
Rather than the expected bitmap
RRRRRGGGGG
RGGGGRRRRG
RGGGGRRRRG
RGGGGRRRRG
RGGGGRRRRG
RGGGGRRRRG
GRGRRGGRGR
GRGRRGGRGR
GRGRRGGRGR
GRGGGRRRGR
GRRRRGGGGR
GGGGGRRRRR
Yet when I use a hard-coded value of PixelFormat.Format8bppIndexed
, the bitmap is created correctly for all three formats.
I'm guessing this is something to do with how the data which makes up a Bitmap
object is laid out in memory, but can anyone explain to me why this only works correctly when I pass PixelFormat.Format8bppIndexed
rather than the actual PixelFormat
of the Bitmap
I'm creating?
The short answer is that the format
parameter of the LockBits
method has nothing to do with the PixelFormat
property of the Bitmap
. Instead, it indicates the number of bits used to represent a single pixel in the byte array from which the bitmap is populated.
So for the example 1bpp image in the question, it's correct to use PixelFormat.Format8bppIndexed
as the value of this parameter, because the source array uses a whole byte to represent each pixel, even though we only need one bit to hold the possible values of each pixel. Most of the time it's fine to do this, and (in my opinion anyway), this is easier and makes for more readable code than the alternative.
However, this is not a very efficient use of memory, as only one bit in every eight (or four bits in every eight if creating a 4bpp bitmap) contains useful information. If working with large 1bpp or 4bpp images, then in theory we can instead use an array which uses the same number of bits per pixel as the bitmap does. For example, to create a 8x2 pixel 1bpp image with the colour indexes...
0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0
... we could use an array like this and pass PixelFormat.Format1bbpIndexed
as the format
parameter of the LockBits
method.
var colourIndexes = new byte[]
{
0b01010101,
0b10101010,
}
How to implement this is a different matter. Bitmaps where width * bits per pixel
is not a multiple of 8 can be tricky to handle correctly. We also mustn't forget that Marshal.Copy
is effectively writing directly to the area of memory occupied by the Bitmap
object, and getting that sort of operation wrong can lead to unexpected and downright confusing bugs, such as heap corruption exception 0xC0000374. In fact, the implementation I'm currently working on appears to have just such a bug. If I manage to fix it then I'll come back and update this answer / post a new answer.
In the mean time, using PixelFormat.Format8bppIndexed
regardless of the actual PixelFormat
of the Bitmap
is fine, as long as memory consumption isn't a concern.