Dynamic Image Generation
The .Net Framework has, like many frameworks and class libraries before it, some natty little features for drawing images and for saving or streaming them in different formats.
As part of getting to grips with C# I decided to put together some image generation routines for web based buttons, but discovered that it was not all as simple as it should be.
The main problem lies here: There are two types of image format relevant to this problem - 8 bit per pixel (8bpp) images, these use a palette of colors and each pixels byte of data is an index into that palette, hence the 256 color limit; the other type is a true color image at either 24bpp (RGB) or 32bpp (ARGB).


The limitation is this - you can get a drawing context and, therefore, draw onto a true color image but you cannot draw onto an indexed image; you can save an indexed image as a gif with a custom palette, but a true color image is always dithered to the web palette for you.
As I want to use large blocks of solid colors that are not in the web palette I'm left with two choices... Jpeg, and have the jpeg artefacts visible on the image or gif and dither to the web palette, neither of which is acceptable.


This could be solved, you might think, by streaming them out as PNG format images. There's a little bug in doing that though, you end up with an undefined GDI+ exception. After a little digging I found that the GDI libraries for PNG don't like the network stream, so you have to use a memory stream instead.
using System.Drawing.Imaging;
using System.IO;
public void StreamImage(System.Web.HttpResponse Response)
{
Response.ContentType = "image/png";
MemoryStream memoryStream = new MemoryStream();
yourBitmap.Save(memoryStream, ImageFormat.Png);
memoryStream.WriteTo(Response.OutputStream);
}
But I was disappointed with this, as often the background colors on the images, which should match the HTML background color of a CSS style were being rendered differently by the browser despite them being the same RGB value when checked with Photoshop.
So, GIF it has to be.
The end result is to use two bitmaps - one at 24 or 32 bpp to draw your image onto and then a second 8bpp bitmap the same size, with your chosen palette. I call these the canvas and the output respectively. You have to copy one to the other before streaming out the final image.
The algorithm I started with simply used GetPixel and SetPixel to iterate through the two bitmaps, but it became very obvious that the interop code between .net and GDI+ was not going to cope with that. A simple, 200x200 square image was taking several seconds to map.
A fiddle with unsafe code and pointers was needed and resulted, after much optimisation, in this.
private void CopyCanvasToOutput(Bitmap canvasBitmap,
Bitmap outputBitmap)
{
BitmapData canvasData = LockBitmap(canvasBitmap);
try
{
BitmapData outputData = LockBitmap(outputBitmap);
try
{
// Get the height and width of the image in pixels
GraphicsUnit unit = GraphicsUnit.Pixel;
RectangleF bounds = canvasBitmap.GetBounds(ref unit);
int sizeX = (int) bounds.Width;
int sizeY = (int) bounds.Height;
// Get some pointers to the data in memory
Byte* pOutputBase = (Byte*) outputData.Scan0.ToPointer();
Byte* pCanvasBase = (Byte*) canvasData.Scan0.ToPointer();
Byte* pOutputRow = pOutputBase;
Byte* pCanvasRow = pCanvasBase;
PixelDataFor8bppIndexed* pOutputPixel;
PixelDataFor32bppARGB* pCanvasPixel =
(PixelDataFor32bppARGB*) pCanvasBase;
PixelDataFor32bppARGB* pPreviousCanvasPixel =
(PixelDataFor32bppARGB*) pCanvasBase;
// Get the closest colour of the first pixel.
int paletteIndex = ClosestMatch(pCanvasPixel);
// Loop through each row
for (int y = 0; y < sizeY; y++)
{
// Set the current pixels to the start of the row
pOutputPixel = (PixelDataFor8bppIndexed*) pOutputRow;
pCanvasPixel = (PixelDataFor32bppARGB*) pCanvasRow;
// Loop through each column
for (int x = 0; x < sizeX; x++, pOutputPixel++, pCanvasPixel++)
{
if (*((int*) pPreviousCanvasPixel) != *((int*) pCanvasPixel))
{
// If the current pixel is different to
// the previous pixel then find the closest
// match and remember where we were.
paletteIndex = ClosestMatch(pCanvasPixel);
pPreviousCanvasPixel = pCanvasPixel;
}
pOutputPixel->Index = (Byte) paletteIndex;
}
// Increment the row pointers by
// the byte length of a row (Stride)
pOutputRow += outputData.Stride;
pCanvasRow += canvasData.Stride;
}
}
finally
{
outputBitmap.UnlockBits(outputData);
}
}
finally
{
canvasBitmap.UnlockBits(canvasData);
}
}
private BitmapData LockBitmap(Bitmap bitmapToLock)
{
GraphicsUnit unit = GraphicsUnit.Pixel;
RectangleF boundsF = canvasBitmap.GetBounds(ref unit);
Rectangle bounds = new Rectangle((int) boundsF.X,
(int) boundsF.Y,
(int) boundsF.Width,
(int) boundsF.Height);
return bitmapToLock.LockBits(bounds,
ImageLockMode.ReadOnly,
bitmapToLock.PixelFormat);
}
[StructLayout(LayoutKind.Sequential)]
private struct PixelDataFor32bppARGB
{
//in memory byte sequence is BGRA, i.e. backwards.
public byte blue;
public byte green;
public byte red;
public byte alpha;
}
[StructLayout(LayoutKind.Sequential)]
private struct PixelDataFor8bppIndexed
{
public byte Index;
}
Color matching was also a challenge as copying from true color to a palette you have to match RGB values to the closest entry in the palette. The algorithm I've produced doesn't do any dithering or anything as the intention was to avoid any pattern or diffusion, and just map to the closest colors.
private static Hashtable _paletteHashtable = new Hashtable();
private int ClosestMatch(PixelDataFor32bppARGB* pCanvasPixel)
{
byte alpha = pCanvasPixel->alpha;
byte red = pCanvasPixel->red;
byte green = pCanvasPixel->green;
byte blue = pCanvasPixel->blue;
int index = 0;
// Check the alpha
if (alpha < 128)
{
index = 255;
}
else
{
// Look for an exact match
int key = alpha << 24 | red << 16 | green << 8 | blue;
Object paletteValue = _paletteHashtable[key];
if (paletteValue != null)
{
index = (int) paletteValue;
}
else
{
// Look for a closest match
int redDistance;
int greenDistance;
int blueDistance;
int totalDistance;
int minDistance = int.MaxValue;
int loopIndex = 0;
foreach (Color color in outputBitmap.Palette.Entries)
{
redDistance = color.R - red;
greenDistance = color.G - green;
blueDistance = color.B - blue;
// The squaring done here allows us to attach
// more importance to values further away.
totalDistance = (redDistance * redDistance) +
(greenDistance * greenDistance) +
(blueDistance * blueDistance);
if (totalDistance < minDistance)
{
minDistance = totalDistance;
index = loopIndex;
}
loopIndex++;
}
// Add it to the _paletteHashtable
// lock it first and test in case anyone else
// added the index to _paletteHashtable while
// we were still computing the closest match.
lock (_paletteHashtable)
{
if (!_paletteHashtable.ContainsKey(key))
{
_paletteHashtable.Add(key, index);
}
}
}
}
return index;
}