When I started writing web applications using .NET, I found myself in need to dynamically create thumbnails of images that could be uploaded by the user or pulled from a database.
For 24 bits images, I quickly found the necessary code examples in the .NET framework documentation or on the web.
For 8 bits images, in particular transparent ones, this code did not work and I had to do more research.
The Drawing namespace provides us with several classes to work with images, in particular:
The Bitmap class that encapsulates a GDI+ bitmap, which consists of the pixel data for a graphics image and its attributes.
The Graphics class that encapsulates a GDI+ drawing surface.
The System.Drawing.Imaging namespace provides advanced GDI+ imaging functionality, in particular:
The BitmapData class specifies the attributes of a bitmap image and is used by the LockBits and UnlockBits methods of the Bitmap class.
The ImageFormat class specifies the format of the image.
The Encoder class encapsulates a globally unique identifier (GUID) that identifies the category of an image encoder parameter.
The ImageFormat class specifies the format of the image. (gif, jpg, png...)
The way GDI+ loads and manipulates images of different formats and color depth is not well documented, but the VS debugger helps to find what happens in different cases and it is not always what one would expect.
For example:
If you load an 8 bits transparent png image into a GDI+ Image object, it is converted to a 32 bits/pixel format, losing the palette and transparency.
If you load an 8 bits transparent gif image into a GDI+ Image object, it is kept as a 8 bits/pixel format, the palette and transparency are preserved.
If you load a 4 bits transparent gif image into a GDI+ Image object, it is converted to an 8 bits/pixel format, the 16 entries palette is preserved.
If you try to use the GetThumbnailImage method from the Image object to resize an 8 bits transparent gif image, the thumbnail you obtain is a 24 bits/pixel png file with loss of transparency.
So how do we create thumbnails of transparent images?
Original Gif Image
Png Thumbnail
First we have to start from a gif image, 4 or 8 bits/pixel since it will preserve the palette and transparency. (4 bits/pixel images will be converted to 8 bits/pixel)
Then we create a Bitmap object with the same properties that the original, but with the desired size for the thumbnail.
Then, since we have the same palette in both the original image and the thumbnail we just have to find for each pixel in the thumbnail, the color index scaled to the original image. A way to do this is though pointer arithmetic. It forces us to compile the code with the unsafe directive, but it has the advantage of a fast execution. The IndexedRezise method does that job:
void
IndexedRezise(int xSize, int ySize)
{
BitmapData sourceData;
BitmapData targetData;
AdjustSizes(ref xSize, ref ySize);
scaledBitmap = new Bitmap(xSize, ySize, bitmap.PixelFormat);
scaledBitmap.Palette = bitmap.Palette;
sourceData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly, bitmap.PixelFormat);
try
{
targetData = scaledBitmap.LockBits(new Rectangle(0, 0, xSize, ySize),
ImageLockMode.WriteOnly, scaledBitmap.PixelFormat);
try
{
xFactor = (Double) bitmap.Width / (Double) scaledBitmap.Width;
yFactor = (Double) bitmap.Height / (Double) scaledBitmap.Height;
sourceStride = sourceData.Stride;
sourceScan0 = sourceData.Scan0;
int targetStride = targetData.Stride;
System.IntPtr targetScan0 = targetData.Scan0;
unsafe
{
byte * p = (byte *)(void *)targetScan0;
int nOffset = targetStride - scaledBitmap.Width;
int nWidth = scaledBitmap.Width;
for(int y=0;y < scaledBitmap.Height;++y)
{
for(int x=0; x < nWidth; ++x )
{
p[0] = GetSourceByteAt(x, y);
++p;
}
p += nOffset;
}
}
}
finally
{
scaledBitmap.UnlockBits(targetData);
}
}
finally
{
bitmap.UnlockBits(sourceData);
}
}
First we need to get pointers to the color indexes of the thumbnail image and the original image. We get it though the Lockbits method of the Bitmap object. This method returns a BitmapData object which has a Scan0 property of type IntPtr, pointing to the beginning of the color indexes.
The BitmapData object also has a Stride property which represents the width of the image including some padding so that it is word aligned.
Then we go through each pixel of the thumbnail, making sure we skip the padding at the end of each line. (The padding is the Stride minus the actual width of the image)
For each pixel, we determine the color index of the original image using pointer arithmetic through the GetSourceByteAt method:
byte
GetSourceByteAt(int x, int y)
{
unsafe
{
return ((byte*) ((int)sourceScan0 + (int)
(Math.Floor(y * yFactor) * sourceStride) + (int) Math.Floor(x * xFactor)))[0];
}
}
Our class adjusts the size of the thumbnail to keep it proportional to the original image if you pass 0 as one of the dimensions and allows to save the thumbnail as gif or png image format from a File or a Stream. (If I am right the dynamic creation of gif files on a server is not permitted according to the LCZ compression patent, so in that case, you would use the png format.)
It also allows the making thumbnails of jpg 24 bits images, using a bicubic transform with overload methods to vary the jpeg compression.
The little demo program included lets you test the class by measuring the time needed to create 100 thumbnails of each image format, showing that the resizing of an 8 bits/pixel gif is about 3 times faster than the same operation for a jpeg image of the same size.