Introduction
There are lots of articles around the Internet that show you how to get alpha-transparency to work in Internet Explorer 6 and lower. The problem is that they all "sort of" work. There will be situations when you just can't have alpha-transparent PNGs (like in high-security mode), and your images are drawn with the cream-cyan background color. The good news is that the background color does not have to be cream-cyan, it can be anything you like. The purpose of this article is to show you how to write the information necessary to a PNG file to give a background color to a PNG file, so that in these situations, the visitor gets something that at least tries to match the site.
When writing out PNG files in .NET via Photoshop or the bitmap.Save()
method, there is no support for adding a background color, so you have to do it manually.
I wrote this code for the pay version of StyleSpread. StyleSpread is a CSS compiler but the pay version has image-creation abilities. Because it's a desktop application, I'm able to write PNG files directly to the hard disk, which may not be available to you in ASP.NET. Slight modification to the code may be necessary to get this running on a Web server.
Background
To help you understand, I'm going to do a quick skim over the PNG specification. Start off by creating a 1x1 PNG file in Photoshop (or some other program), and opening up the file in your favorite HEX editor, like UltraEdit.
PNG files are made up of chunks. A chunk is just a block of information. Each chunk has a 4 character identifier, such as IHDR, bKGD, IDAT, and IEND (upper case means it's required, lower case means it's optional). Chunks follow a format that goes like this: 4 bytes that store the length of the chunk (INCLUDING the 4 character identifier), 4 bytes for the identifier, then the chunk data, then 4 bytes for the CRC. CRCs are error-checking mechanisms. They are calculated by combining a bunch of data into a few bytes. If the data doesn't transmit properly, the CRC won't match the data, so you know there's a problem.
The bKGD chunk is the chunk that specifies the background color. We need to insert one into our bitmap.Save()
and Photoshop created PNGs to liberate ourselves from the cream-cyan.
To make things easy, this code always inserts the bKGD chunk directly before the IDAT chunk. The IDAT chunk contains the data for the image, and the bKGD chunk has to be defined before it.
One Last Problem
Ever notice how PNG files look darker in Explorer than they do in other browsers? There is a particularly annoying chunk created by both bitmap.Save()
and Photoshop called gAMA. It defines the gamma level for the image. Explorer understands this chunk while other browsers do not. So if you want the same color in all browsers, this chunk has to go. In StyleSpread, I'm using Ken Silverman's PNGOUT compressor to kill it, because it has a feature to get rid of unwanted chunks. If you want to kill it manually in code, it shouldn't be too hard now that you know a bit about the PNG specification.
About the Code
For starters, you need some code to generate your CRC checks. Code to do this is freely available here, but it's written in C, and anything written in C is ugly code (go ahead, flame me!). I present to you the pretty version in C#. This first method is going to create a lookup table for faster CRC calculation.
private static void CreateCrcTable()
{
uint c;
int k;
int n;
for (n = 0; n < 256; n++)
{
c = (uint)n;
for (k = 0; k < 8; k++)
{
if ((c & 1) == 1)
{
c = 0xedb88320 ^ (c >> 1);
}
else
{
c = c >> 1;
}
}
CrcTable[n] = c;
}
IsTableCreated = true;
}
static uint[] CrcTable = new uint[256];
static bool IsTableCreated = false;
The next function returns the CRC of the bytes in a byte array. This code is fairly complicated. If you're short on time, don't bother reading through it.
public static byte[] GetCrc(byte[] buffer)
{
uint data = 0xFFFFFFFF;
int n;
if (!IsTableCreated)
CreateCrcTable();
for (n = 0; n < buffer.Length; n++)
data = CrcTable[(data ^ buffer[n]) & 0xff] ^ (data >> 8);
data = data ^ 0xFFFFFFFF;
byte b1 = Convert.ToByte(data >> 24);
byte b2 = Convert.ToByte(b1 << 8 ^ data >> 16);
byte b3 = Convert.ToByte(((data >> 16 << 16) ^ (data >> 8 << 8)) >> 8);
byte b4 = Convert.ToByte((data >> 8 << 8) ^ data);
return new byte[] { b1, b2, b3, b4 };
}
Those last 4 byte definitions look complicated, but it's just bitshift trickery to get an int
converted to a byte array. If anyone has a better method, I'd like to hear. Now we just call the GetCrc
method, pass our data, and we'll get the CRC back.
This next function is the one that does the magic. It writes a bKGD chunk to a PNG file on the hard disk.
I'm calling them backup background colors in StyleSpread instead of bKGD chunks. Referring to them as this makes more sense to non-programmers.
public static void WriteBackupBackgroundColor(string fileName, Color color)
{
byte[] lengthData = { 0, 0, 0, 6 };
byte[] bkgdChunk = { 98, 75, 71, 68, 0, color.R, 0, color.G, 0, color.B };
byte[] data;
byte[] crcData = PngUtil.GetCrc(bkgdChunk);
Here are the 4 components that make up a chunk. The length of a bKGD is always 6. And 98, 75, 71, 68 makes bKGD when the conversion to ASCII is done. To be honest, I don't know why the red, green and blue components need to be prepended with 0's, but that's what I saw in UltraEdit when I HEXed the PNG file and it seems to work. :)
using (FileStream fs = new FileStream(fileName, FileMode.Open))
using (BinaryReader binReader = new BinaryReader(fs))
{
data = binReader.ReadBytes((int)binReader.BaseStream.Length);
}
You'll have to modify this to get it to work in ASP.NET. Basically instead of reading the image from the disk, read it from your database or wherever your image data is located.
byte[] newData = new byte[data.Length + 18];
int dataIndex = 0;
bool wroteChunk = false;
for (int i = 0; i < data.Length; i++)
{
if (!wroteChunk && data[i + 4] == 'I' && data[i + 5] == 'D'
&& data[i + 6] == 'A' && data[i + 7] == 'T')
{
Array.Copy(lengthData, 0, newData, dataIndex, 4);
dataIndex += 4;
Array.Copy(bkgdChunk, 0, newData, dataIndex, bkgdChunk.Length);
dataIndex += bkgdChunk.Length;
Array.Copy(crcData, 0, newData, dataIndex, 4);
dataIndex += 4;
wroteChunk = true;
}
newData[dataIndex++] = data[i];
}
Here we're searching for the IDAT chunk. Once we find it, we start writing the bKGD data, and then switch back to writing the rest of the file. Notice I'm checking data[i + 4]
. I'm searching 4 bytes ahead to skip the 4 bytes of chunk length information.
if (File.Exists(fileName))
File.Delete(fileName);
using (FileStream fs = new FileStream(fileName, FileMode.CreateNew))
using (BinaryWriter binWriter = new BinaryWriter(fs))
{
binWriter.Write(newData);
}
}
Delete the file if it exists, and write out a new file. ASP.NET users—instead of writing the PNG to the hard disk, write it to Repsonse.OutputStream
.
Using the Code
Using the code could not be any easier. The included *.zip file contains one source file. It's one static
class, so all you do is include it in your project and call WriteBackupBackgroundColor
.
Points of Interest
The only thing this code does is write out bKGD chunks. I could have made it write out other chunks but that's all I needed. Maybe some brave soul can write a managed replacement library for TweakPNG.
History
- 17th November, 2006 - Initial release