Search code examples
c#imagecolorsbitmappalette

How to read 8-bit PNG image as 8-bit PNG image only?


I have a 8-bit PNG image (See the attachment). But when I read it using Image.FromFile method, the pixel format comes as 32-bit. Due to this I am unable to modify the palette.

Please help me.

See below for the code I am using to read the file and update the palette.

    public static Image GetPreviewImage()
    {
        Bitmap updatedImage = null;
        try
        {
            // Reads the colors as a byte array
            byte[] paletteBytes = FetchColorPallette();

            updatedImage = Image.FromFile(@"C:\Screen-SaverBouncing.png");

            ColorPalette colorPalette = updatedImage.Palette;

            int j = 0;
            if (colorPalette.Entries.Length > 0)
            {
                for (int i = 0; i < paletteBytes.Length / 4; i++)
                {
                    Byte AValue = Convert.ToByte(paletteBytes[j]);
                    Byte RValue = Convert.ToByte(paletteBytes[j + 1]);
                    Byte GValue = Convert.ToByte(paletteBytes[j + 2]);
                    Byte BValue = Convert.ToByte(paletteBytes[j + 3]);
                    j += 4;

                    colorPalette.Entries[i] = Color.FromArgb(AValue, RValue, GValue, BValue);
                }
                updatedImage.Palette = colorPalette; ;
            }

            return updatedImage;
        }
        catch
        {
            throw;
        }
    }

Solution

  • I had this problem too, and it seems that any paletted png image that contains transparency can't be loaded as being paletted by the .Net framework, despite the fact the .Net functions can perfectly write such a file. In contrast, it has no problems with this if the file is in gif format.

    Transparency in png works by adding an optional "tRNS" chunk in the header, to specify the alpha of each palette entry. The .Net classes read and apply this correctly, so I don't really understand why they insist on converting the image to 32 bit afterwards. What's more, the bug always happens when the transparency chunk is present, even if it marks all colours as fully opaque.

    The structure of the png format is fairly simple; after the identifying bytes, each chunk is 4 bytes of the content size, then 4 ASCII characters for the chunk id, then the chunk content itself, and finally a 4-byte chunk CRC value.

    Given this structure, the solution is fairly simple:

    • Read the file into a byte array.
    • Ensure it is a paletted png file by analysing the header.
    • Find the "tRNS" chunk by jumping from chunk header to chunk header.
    • Read the alpha values from the chunk.
    • Make a new byte array containing the image data, but with the "tRNS" chunk cut out.
    • Create the Bitmap object using a MemoryStream created from the adjusted byte data, resulting in the correct 8-bit image.
    • Fix the color palette using the extracted alpha data.

    If you do the checks and fallbacks right you can just load any image with this function, and if it happens to identify as paletted png with transparency info it'll perform the fix.

    My code:

    /// <summary>
    /// Image loading toolset class which corrects the bug that prevents paletted PNG images with transparency from being loaded as paletted.
    /// </summary>
    public class BitmapLoader
    {
        private static Byte[] PNG_IDENTIFIER = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
    
        /// <summary>
        /// Loads an image, checks if it is a PNG containing palette transparency, and if so, ensures it loads correctly.
        /// The theory can be found at http://www.libpng.org/pub/png/book/chapter08.html
        /// </summary>
        /// <param name="filename">Filename to load</param>
        /// <returns>The loaded image</returns>
        public static Bitmap LoadBitmap(String filename)
        {
            Byte[] data = File.ReadAllBytes(filename);
            return LoadBitmap(data);
        }
    
        /// <summary>
        /// Loads an image, checks if it is a PNG containing palette transparency, and if so, ensures it loads correctly.
        /// The theory can be found at http://www.libpng.org/pub/png/book/chapter08.html
        /// </summary>
        /// <param name="data">File data to load</param>
        /// <returns>The loaded image</returns>
        public static Bitmap LoadBitmap(Byte[] data)
        {
            Byte[] transparencyData = null;
            if (data.Length > PNG_IDENTIFIER.Length)
            {
                // Check if the image is a PNG.
                Byte[] compareData = new Byte[PNG_IDENTIFIER.Length];
                Array.Copy(data, compareData, PNG_IDENTIFIER.Length);
                if (PNG_IDENTIFIER.SequenceEqual(compareData))
                {
                    // Check if it contains a palette.
                    // I'm sure it can be looked up in the header somehow, but meh.
                    Int32 plteOffset = FindChunk(data, "PLTE");
                    if (plteOffset != -1)
                    {
                        // Check if it contains a palette transparency chunk.
                        Int32 trnsOffset = FindChunk(data, "tRNS");
                        if (trnsOffset != -1)
                        {
                            // Get chunk
                            Int32 trnsLength = GetChunkDataLength(data, trnsOffset);
                            transparencyData = new Byte[trnsLength];
                            Array.Copy(data, trnsOffset + 8, transparencyData, 0, trnsLength);
                            // filter out the palette alpha chunk, make new data array
                            Byte[] data2 = new Byte[data.Length - (trnsLength + 12)];
                            Array.Copy(data, 0, data2, 0, trnsOffset);
                            Int32 trnsEnd = trnsOffset + trnsLength + 12;
                            Array.Copy(data, trnsEnd, data2, trnsOffset, data.Length - trnsEnd);
                            data = data2;
                        }
                    }
                }
            }
            Bitmap loadedImage;
            using (MemoryStream ms = new MemoryStream(data))
            using (Bitmap tmp = new Bitmap(ms))
                loadedImage = ImageUtils.CloneImage(tmp);
            ColorPalette pal = loadedImage.Palette;
            if (pal.Entries.Length == 0 || transparencyData == null)
                return loadedImage;
            for (Int32 i = 0; i < pal.Entries.Length; i++)
            {
                if (i >= transparencyData.Length)
                    break;
                Color col = pal.Entries[i];
                pal.Entries[i] = Color.FromArgb(transparencyData[i], col.R, col.G, col.B);
            }
            loadedImage.Palette = pal;
            return loadedImage;
        }
    
        /// <summary>
        /// Finds the start of a png chunk. This assumes the image is already identified as PNG.
        /// It does not go over the first 8 bytes, but starts at the start of the header chunk.
        /// </summary>
        /// <param name="data">The bytes of the png image</param>
        /// <param name="chunkName">The name of the chunk to find.</param>
        /// <returns>The index of the start of the png chunk, or -1 if the chunk was not found.</returns>
        private static Int32 FindChunk(Byte[] data, String chunkName)
        {
            if (chunkName.Length != 4 )
                throw new ArgumentException("Chunk must be 4 characters!", "chunkName");
            Byte[] chunkNamebytes = Encoding.ASCII.GetBytes(chunkName);
            if (chunkNamebytes.Length != 4)
                throw new ArgumentException("Chunk must be 4 characters!", "chunkName");
            Int32 offset = PNG_IDENTIFIER.Length;
            Int32 end = data.Length;
            Byte[] testBytes = new Byte[4];
            // continue until either the end is reached, or there is not enough space behind it for reading a new chunk
            while (offset + 12 <= end)
            {
                Array.Copy(data, offset + 4, testBytes, 0, 4);
                // Alternative for more visual debugging:
                //String currentChunk = Encoding.ASCII.GetString(testBytes);
                //if (chunkName.Equals(currentChunk))
                //    return offset;
                if (chunkNamebytes.SequenceEqual(testBytes))
                    return offset;
                Int32 chunkLength = GetChunkDataLength(data, offset);
                // chunk size + chunk header + chunk checksum = 12 bytes.
                offset += 12 + chunkLength;
            }
            return -1;
        }
    
        private static Int32 GetChunkDataLength(Byte[] data, Int32 offset)
        {
            if (offset + 4 > data.Length)
                throw new IndexOutOfRangeException("Bad chunk size in png image.");
            // Don't want to use BitConverter; then you have to check platform endianness and all that mess.
            Int32 length = data[offset + 3] + (data[offset + 2] << 8) + (data[offset + 1] << 16) + (data[offset] << 24);
            if (length < 0)
                throw new IndexOutOfRangeException("Bad chunk size in png image.");
            return length;
        }
    }
    

    The mentioned ImageUtils.CloneImage is, as far as I know, the only 100% safe way of loading a bitmap and unlinking it from any backing resources like a file or stream. It can be found here.

    Alternatively, you can just create the image from MemoryStream and leave the MemoryStream open. Apparently, for a stream that refers to a simple array rather than to an external resource, this gives no problems for garbage collection despite the IDisposable stream being left open. It's a bit less neat and clean, but a lot simpler. The code for the creation of loadedImage then simply becomes:

    MemoryStream ms = new MemoryStream(data)
    Bitmap loadedImage = new Bitmap(ms);