Click here to Skip to main content
15,884,838 members
Articles / Programming Languages / C#
Article

Writing a Background Color (bKGD Chunk) to a PNG File

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
17 Nov 2006CPOL5 min read 34.1K   289   8   3
An article explaining a bit about the PNG file format, how write a bKGD chunk to a PNG file to escape Internet Explorer's cream-cyan background color.

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.

C#
/// <summary>
/// Creates the CRC table for calculating a 32-bit CRC.
/// </summary>
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.

C#
/// <summary>
/// Calculates an array of 4 bytes containing the calculated CRC.
/// </summary>
/// <param name="buf">The raw data on which to calculate the CRC.</param>
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.

C#
/// <summary>
/// Writes a backup background color to the specified PNG file.
/// </summary>
/// <param name="fileName">The path and name of the PNG file to write to.</param>
/// <param name="color">The <see cref="Color"/> to set as the backup background color.
/// </param>
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. :)

C#
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.

C#
// 18 bytes is the size of a bKGD chunk
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.

C#
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Canada Canada
I have been working with computers my whole life. Our first family computer, an IBM 8086, was purchased when I was -3. I am a programmer, designer, entrepreneur and active track athlete.

I am the creator of StyleSpread, which is a CSS compiler designed to stop text editing of style sheets.


Comments and Discussions

 
Generaldoesnt appear to resolve bgcolor issue in IE Pin
jwiner9-Jul-09 6:32
jwiner9-Jul-09 6:32 
GeneralByte array from int Pin
hayles27-Sep-07 8:30
hayles27-Sep-07 8:30 
GeneralRe: Byte array from int Pin
stano27-Sep-07 9:17
stano27-Sep-07 9:17 
public static unsafe byte[] GetBytes(int value)
{
byte[] buffer = new byte[4];
fixed (byte* numRef = buffer)
{
*((int*) numRef) = value;
}
return buffer;
}

Hmm ... not what I expected. Scary!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.