If you check the pixelformat in advance and determine that it is PixelFormat.Format8bppIndexed
, you can just use bmp.PixelFormat
in the LockBits
call, and get the real pixel values directly. This also avoids getting the wrong index in case the palette contains duplicate colours.
You do have to be careful with the stride, though; the byte length of each line in an image in .Net is rounded up to the next multiple of 4 bytes. So on an image with, for example, a width of 386 (like your penguin), the actual data will be 388 bytes per line. This is what they call the 'stride'. To avoid getting problems with this, and instead get completely compact data out, copy the input data per line, in chunks that are the length of the actually-used line data, while moving the read pointer forward by the full stride as given by the BitmapData
object.
The minimum stride is normally calculated as (bpp * width + 7) / 8
. For 8-bit images, of course, this will simply equal the width, since it's 1 byte per pixel, but the function I wrote for it supports any bit depth, even below 8bpp, hence why it makes that calculation.
The full function:
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <param name="collapseStride">Collapse the stride to the minimum required for the image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride, Boolean collapseStride)
{
if (sourceImage == null)
throw new ArgumentNullException("sourceImage", "Source image is null!");
Int32 width = sourceImage.Width;
Int32 height = sourceImage.Height;
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data;
if (collapseStride)
{
Int32 actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * width) + 7) / 8;
Int64 sourcePos = sourceData.Scan0.ToInt64();
Int32 destPos = 0;
data = new Byte[actualDataWidth * height];
for (Int32 y = 0; y < height; ++y)
{
Marshal.Copy(new IntPtr(sourcePos), data, destPos, actualDataWidth);
sourcePos += stride;
destPos += actualDataWidth;
}
stride = actualDataWidth;
}
else
{
data = new Byte[stride * height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
}
sourceImage.UnlockBits(sourceData);
return data;
}
In your case, this can be called simply like this:
if (image.PixelFormat == PixelFormat.Format8bppIndexed)
{
Int32 stride;
Byte[] rawData = GetImageData(image, out stride, true);
// ... do whatever you want with that image data.
}
Do note, as I said in the comment to the other answer, the .Net framework has a bug that prevents 8-bit PNG images that contain palette transparency information from being loaded correctly as 8-bit. To resolve that issue, look at this answer:
A: How to read 8-bit PNG image as 8-bit PNG image only?