Introduction
With Internet Explorer finally having support for PNG transparency with version 7 and above, this useful format can come into its own on websites. Unfortunately, there is another problem, Gamma Correction.
IE Gamma corrects the PNG image when the information is available. That's great. Although it does it incorrectly, at least it is trying. The problem comes from the fact IE doesn't bother gamma correcting anything else. So, if you've got some funky rounded PNG corners, maybe in a nice corn flower blue, then you place them around a div
in which you specify the background color #6495ED, and the image
and the div
are not going to match.
The answer for static images is to remove the gamma information from the PNG. This can be done with quite a few readymade tools. The problem that this little bit of code solves is it remove that information when an image is dynamically generated by your code and saved with GDI+/.NET.
Background
The PNG format is handily split up into chunks. Each chunk provides some information about the image. Some chunks (IHDR, PLTE, IDAT, and IEND) are critical, and must exist to display the image (PLTE is actually critical only for paletted images). The one we're interested in is gAMA. The lower case letter specifies that this is an ancillary chunk and is completely optional. It's this we have to get rid of.
The layout of these chunks within the file is quite simple. First is a 4 byte section specifying the length of the data part of the chunk. Then, there is the header that specifies the chunk type, which is also 4 bytes in length. Next is the data part, which is the length specified in the first section, and finally, we have a CRC part which is used to detect corruption within the data.
The only other thing we need to know is that PNG files start with an eight byte signature, and that our gAMA chunk must appear in the file before the IDAT chunk, and if it exists, the PLTE chunk.
Using the Code
The first thing we have to do then is get our PNG image as bytes so we can mess with them. I've created my dynamic image with a bitmap, so I have to save it as a PNG and then retrieve the bytes from that. The easiest way is to use a MemoryStream
. Also, I'm outputting directly to the Response.OutputStream
so I also return a MemoryStream
.
public MemoryStream StripGAMA(Bitmap input)
{
MemoryStream ms = new MemoryStream();
input.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
byte[] data = ms.ToArray();
ms = new MemoryStream();
ms.Write(data, 0, 8);
int offset = 8;
byte[] chkLenBytes = new byte[4];
int chkLength = 0;
string chkType = string.Empty;
Above, you can see that I've taken the bitmap and saved it to a MemoryStream
and then converted it to a byte array. Then, I've created a new stream and written out the first 8 bytes to it. This is the PNG signature which we can safely ignore. Then, I've declared some variables we are going to need. The only thing to note at this point is that offset
is already at eight because we've already dealt with the signature.
Now, we come to the main part of the code. There's a lot here to deal with the length of the chunks. This is because PNG stores values in network byte order, or Big Endian. .NET usually uses Little Endian, so we have to flip the bytes to get the correct value when we convert to integer. All chunk names (and most text within the PNG format) are encoded with ASCII, so we can use a simple static
function for converting the bytes to text.
while (offset < data.Length-12)
{
chkLenBytes[0] = data[offset];
chkLenBytes[1] = data[offset + 1];
chkLenBytes[2] = data[offset + 2];
chkLenBytes[3] = data[offset + 3];
if(System.BitConverter.IsLittleEndian)
System.Array.Reverse(chkLenBytes);
chkLength = System.BitConverter.ToInt32(chkLenBytes, 0);
chkType = System.Text.Encoding.ASCII.GetString(data, offset + 4, 4);
Now, it is a simple matter of checking the chunk type and writing the chunks we are not interested in out. Or, if we find a gAMA, jumping it and writing out the rest of the bytes. If we find an IDAT or PLTE chunk first, then there is no gAMA chunk, so we can also finish up.
if (chkType != "gAMA")
{
if (chkType == "IDAT" || chkType == "PLTE")
{
ms.Write(data, offset, data.Length - offset);
break;
}
else
{
ms.Write(data, offset, 12 + chkLength);
offset += 12 + chkLength;
}
}
else
{
offset += 12 + chkLength;
ms.Write(data, offset, data.Length - offset);
break;
}
return ms;
The value of 12 comes from the other three parts of the chunk (length, name, and CRC check) at 4 bytes each.
And, that's it. With a little tweaking, you could also use this on your static files if you wish, but it might be easier using Ken Silverman's PNGOUT compressor or any number of other available utilities.
History
- 10th June, 2009: Initial post