Click here to Skip to main content
Click here to Skip to main content

Strip gAMA Chunk from PNG for ASP.NET Applications

By , 10 Jun 2009
 

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

License

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

About the Author

Leonscape
Web Developer Economic Solutions
United Kingdom United Kingdom
Member
Leon has been a professional programmer since 1993, and started specialising in web development around 2004.
 
Developing software in a variety of languages( Assembly, C/C++, Delphi, Basic (All kinds), C#, PHP, etc.) on Windows and Linux systems.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralIt saved mememberChengbin Wang8 Feb '10 - 12:53 
I like png, but I don't like its gama ray that keeps pulling my hair off.
I really don't know how to solve png's dismatch of the CSS colors until I read this excellent article. Thank you so much, Leonscape!
Following is the code example in which Leonscape's approach has been used:
<code>
 
            private static void SliceImage(Bitmap image, int x, int y, int width, int height, string imageDirectory, string fileName)
            {
                  Bitmap slice = new Bitmap(width, height);
                  Graphics g = Graphics.FromImage(slice);
                  //g.Clear(Color.White);
                  g.DrawImage(image, new Rectangle(0, 0, width, height), x, y, width, height, GraphicsUnit.Pixel);
                  //g.Save();
 
                  //slice.Save(Path.Combine(HttpContext.Current.Server.MapPath(imageDirectory), fileName));
                  using (FileStream fs = File.OpenWrite(Path.Combine(HttpContext.Current.Server.MapPath(imageDirectory), fileName)))
                  {
                        using (MemoryStream ms = StripGAMA(slice))
                        {
                              fs.Write(ms.GetBuffer(), 0, (int)ms.Length);
                        }
                  }
 
                  slice.Dispose();
                  g.Dispose();
            }
 
            private static MemoryStream StripGAMA(Bitmap input)
            {
                  //http://www.codeproject.com/KB/web-image/pnggammastrip.aspx?display=Print
                  //http://morris-photographics.com/photoshop/articles/png-gamma.html
                  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;
                  while (offset &lt; 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);
                        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;
                  }
                  return ms;
            }
 
</code>
 
John
GeneralRe: It saved mememberLeonscape12 Feb '10 - 8:09 
I'm glad you found it useful!
Using the wrong tool for the job is half the fun.

QuestionRe: It saved mememberShailesh H18 Feb '12 - 18:41 
Hi,
I found this article usefull too.I had this requirement of deleting the PLTE chunk in the png stream i generated so I used this code.
 
Thanks LeonScape!   Smile | :)
 
I have another requirement , and that is I want to generate a png file without any "ancillary chunks" (like cHRM , sRGB etc) using C#.Net. How do i do it? any suggestions?
 
Thanks & Regards
AnswerRe: It saved mememberLeonscape8 Mar '12 - 23:20 
Sorry I didn't reply earlier, but I didn't see your message til now.
 
I'll give the answer now anyway in case you need it still, or someone else does.
 
All optional chunks have a lower case letter at the start of their name, so when above I'm looking for "gAMA" I change it to check the first letter.
 
This is easy to do with the bytes as its ascii encoding just check the raw value, 97 to 122 are the lower case letters.
 
public MemoryStream StripOptional(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;
    byte firstLetter = 0;
 

    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);
    
        firstLetter = data[offset+4];
        if(firstLetter < 97 || firstLetter > 122)
            ms.Write(data, offset, 12 + chkLength);
 
        offset += 12 + chkLength;
    }
 
    return ms;
}
 

Hope that helps.
Using the wrong tool for the job is half the fun.

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 10 Jun 2009
Article Copyright 2009 by Leonscape
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid