Chris Roberts

PNG Colour Rendering on the Web

• Posted in Programming

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!