PNG Colour Rendering on the Web
When is a colour not a colour? When it's in a PNG image file and you want to display it in a web browser!
I have recently been working on a website which includes a number of PNG images which are generated at run-time by a piece of VB.NET code. During testing we noticed a number of discrepancies between the colours being rendered in the image and the colours specified in the site's CSS file.
To make matters worse, the behaviour varied from browser to browser. Google Chrome behaved as desired - a perfect colour match. Internet Explorer appeared to render the colours in the PNG darker than we wanted. When we tried Firefox, the colours in the PNG were rendered with a different shade of the desired colour (a slightly purple tinge to our desired blue, in our case).
Before I continue, let me say that I am no expert when it comes to the treatment of colours in digital imaging - so if you're looking for an in-depth exploration of the subject you're not going to find it here. I will explain how to get colours in PNG files to match the colours specified in your CSS file, though!
Problem 1 - Firefox
Further reading around the treatment of colours in Firefox revealed that the latest version (3.5) now supports ICC colour profiles by default. ICC colour profiles can be embedded in certain digital image formats (including PNG) to provide additional information about how the colours should be rendered.
I believe the theory is that if you take a digital photograph, the camera will record an ICC profile in the image which relates to the way the camera records colour. If you then view the photograph on a computer which has the correct ICC profile for the monitor you're using installed - the colours displayed should be true to the original.
The downside of this in Firefox 3.5 is that CSS files have no concept of colour profiles. So if there is a colour profile in your PNG file, its colours will be 'corrected' - moving them away from the colours specified in your CSS files.
In other words, Firefox's support of colour profile information is great for photographers, but not so great for web designers!
Problem 2 - Internet Explorer
Internet Explorer's problem was very different to Firefox's - and so I assumed it wasn't related to colour profiles. After yet more reading around the subject, it appears that PNG files can also include some gamma correction information, and other pieces of colour related information.
Each of the popular web browsers appears to offer different levels of support for each of these different elements.
The Solution
The theory is simple; removing the various pieces of colour information from the PNG file will give each of the browsers little choice about how to interpret the colours.
This can be achieved quite easily from the command line using a freeware utility called PNGCrush...
pngcrush -rem cHRM -rem gAMA -rem iCCP -rem sRGB infile.png outfile.png
This proves the theory. With the colour information removed, the colours in the PNG file now match the colours specified in your CSS file.
However, this doesn't solve the problem for automatically generated images using the .NET framework. It would appear that the PNG files created by the framework include default values for a number of pieces of colour related information (as can be seen if you look at the PNG file using TweakPNG.
There is no native support in the .NET framework for manipulating the individual 'Chunks' that make up a PNG file, so if we want to do it we have no choice but to get a bit 'low level'.
I wrote the following function to iterate through the 'Chunks' in some source PNG data, copying them to an output PNG unless they're in an 'ignore' list. The PNG file format is well documented, if you want to find out more about how this works.
Private Function PNGRemoveChunks(ByRef SourceData() As Byte, ByVal ChunkIDsToRemove() As String) As Byte() Dim OutputStream As New MemoryStream Dim CurrentOffset As Integer Dim CurrentChunkLenData(3) As Byte Dim CurrentChunkLen As Integer Dim CurrentChunkType As String Dim RetVal() As Byte ' Copy the 8 PNG header bytes directly to the output Call OutputStream.Write(SourceData, 0, 8) CurrentOffset = 8 ' Iterate through the 'Chunks' in the PNG. ' 12 = Chunk Length Bytes + Chunk Type Bytes + CRC Bytes While (CurrentOffset <= SourceData.Length - 12) ' Determine the length and type of this chunk (being careful to get the bytes in the right order) CurrentChunkLenData(0) = SourceData(CurrentOffset + 0) CurrentChunkLenData(1) = SourceData(CurrentOffset + 1) CurrentChunkLenData(2) = SourceData(CurrentOffset + 2) CurrentChunkLenData(3) = SourceData(CurrentOffset + 3) If (System.BitConverter.IsLittleEndian) Then Call System.Array.Reverse(CurrentChunkLenData) End If CurrentChunkLen = System.BitConverter.ToInt32(CurrentChunkLenData, 0) CurrentChunkType = System.Text.Encoding.ASCII.GetString(SourceData, CurrentOffset + 4, 4) ' If this chunk is not in the list of chunk types to remove, add it to the output If Not ChunkIDsToRemove.Contains(CurrentChunkType) Then Call OutputStream.Write(SourceData, CurrentOffset, CurrentChunkLen + 12) End If ' Look at the next chunk in the file CurrentOffset += CurrentChunkLen + 12 End While ' Tidy up and return the resulting PNG data RetVal = OutputStream.ToArray() Call OutputStream.Close() Return RetVal End Function
To remove all colour information from your image, simply convert the PNG image data to a byte array and call the function like this:
OutputData = PNGRemoveChunks(SourceData, New String() {"gAMA", "iCCP", "cHRM", "sRGB"})
This solved all of our PNG woes - I hope it does the same for you!