1

I have A0 format (600dpi) png (19860px x 28080px) that contains only black and white pixels (one bit per pixel file is about only 3MB). All I want is to save this file as png where white color pixels will be replaced by transparent color.

bitmap.MakeTransparent(color) doesn't work because file is too big. Same issue with using ColorMap

Any ideas how to replace all those white pixels in reasonable time?

Bartek Chyży
  • 521
  • 9
  • 25
  • Have you tried using `SetPixel(...)` to achieve this? – Martin Nov 19 '18 at 12:54
  • Not yet. But SetPixel() is a bit slow. For 19860x28080 it will be last about 15min – Bartek Chyży Nov 19 '18 at 12:56
  • 2
    I'm sorry, you didn't say that it needed to be speedy in your question... – Martin Nov 19 '18 at 12:56
  • Right, my bad. I will have to process about 3000 images like that. :/ – Bartek Chyży Nov 19 '18 at 12:58
  • You need to play with the colormap instead. ColorMap you technically change the table saying color ID "x" is now the RGB "y". On such large file it should be 4-5 seconds tops to change the whole thing. You may want to check my answer [here](https://stackoverflow.com/questions/40284721/coloring-on-bitmap-not-accurate-i-already-looked-at-how-to-change-pixel-color/40286647#40286647) – Franck Nov 19 '18 at 13:54
  • There is only one bit per pixel. If I make it RGB file will be about 2GB instead of 3MB. – Bartek Chyży Nov 19 '18 at 14:09
  • @Franck: Your link leads to an answer that starts with _i doubt this line compile but you get the idea_ The rest doesn't address the size issues one runs into with GDI+. – TaW Nov 19 '18 at 14:12
  • @Bar: What format does the original image have? Can you even load the image into a Bitmap? If yes: For speed use LockBits. If no: I guess you'll have to look for a library to do the job. We can't help with the search, though. – TaW Nov 19 '18 at 14:16
  • @TaW My answer show both options. Either by `Graphics` or using `Bitmap`, second is with bitmap. I does work with extremely large image in excess of 45,000 pixel wide. I used that method to recolor all counties of the USA for Rep territory marking – Franck Nov 19 '18 at 14:58
  • In section 4.4 do you know what type file you have? See : https://www.w3.org/TR/PNG/#11IDAT – jdweng Nov 19 '18 at 17:30
  • Could you post an example of such an image? After the discussion with jdweng I'm really curious about the internals. – Nyerguds Nov 22 '18 at 08:53

2 Answers2

1

You don't need System.Drawing for this operation at all.

See, one-bit-per-pixel black-and-white images in PNG format will be either in grayscale format, or paletted.

PNG is made up of chunks which have the following format:

  • Four bytes of internal chunk length (big-endian)
  • Four ASCII characters of chunk identifier
  • The chunk contents (with the length specified in the first part)
  • A four-byte CRC hash as consistency check.

Now here's the interesting part: PNG has the rather obscure feature that it supports a tRNS chunk to be added that sets the alpha for grayscale and paletted format. For grayscale this chunk contains a two-byte value that indicates which grayscale value should be made transparent (which I assume for one bit per pixel should be '1', since that's white), and for paletted format this contains the alpha for each of its palette colours (though it doesn't have to be as long as the palette; any indices not included default to opaque). And since PNG has no overall indexing of its chunks, you can literally just add that in and be done with it.

The code for reading and writing PNG chunks was posted in earlier answers here:

Reading chunks

Writing chunks

You will need to read the IHDR chunk and check the colour type in the header to see which type of transparency chunk you need to add. An overview of the header format and all colour type possibilities can be found here. Basically, colour type 0 is grayscale, and colour type 3 is paletted, so your image should be one of these two.

For palette transparency, the tRNS chunk should be added right behind the PLTE chunk. For grayscale transparency, I believe it should be just before the first IDAT chunk.

If it's paletted, you do need to check if white is the first or second colour in the palette, so you can set the correct one to transparent.

So once you got that, make a new byte array that's as large as your image plus the added chunk (12 bytes of chunk header and footer and probably 2 bytes of data inside it), and then copy the first part of the file up to the point where the segment should be added into it, then your new segment, and then the rest of the file. Save the bytes array to a file, and you're done.

Nyerguds
  • 5,360
  • 1
  • 31
  • 63
  • the link you posted is working from bitmaps and not a png file. Also you have to be careful of the chunk order. The posted code is randomly checking for a chunk type and not sequentially going through the chunks. – jdweng Nov 20 '18 at 21:13
  • No, the link I posted _returns_ a .Net `Bitmap` _object_ (since its purpose is to load the palette transparency correctly in .Net), but it takes _bytes_ as input. As for the order of the chunks, it does in fact _not_ matter at all for the _search_ procedure. It matters in that the tRNS chunk needs to be right behind the PLTE one, but I specifically mentioned that. – Nyerguds Nov 21 '18 at 09:08
0

I'm not an expert on PNG files, but I read the documentation. I believe if you have a GRAEY SCALE file with only one byte data then all you have to do is add a transparency header before the first IDATA chunk. The transparency header contains two bytes which is a color scale between 0 and (2^bitdepth - 1)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace PNG_Tool
{
    class Program
    {
        const string READ_FILENAME = @"c:\temp\untitled.png";
        const string WRITE_FILENAME = @"c:\temp\untitled1.png";
        static void Main(string[] args)
        {
            PNG png = new PNG(READ_FILENAME, WRITE_FILENAME);

        }
    }
    class PNG
    {
        byte[] header;
        byte[] ident = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
        byte[] TNS = { 0x74, 0x52, 0x4E, 0x53 }; //"tRNS"

        public PNG(string inFilename, string outFilename)
        {
            Stream inStream = File.OpenRead(inFilename);
            BinaryReader reader = new BinaryReader(inStream);
            Stream outStream = File.Open(outFilename, FileMode.Create);
            BinaryWriter writer = new BinaryWriter(outStream);

            Boolean foundIDAT = false;


            header = reader.ReadBytes(8);

            if ((header.Length != ident.Length) || !(header.Select((x,i) => (x == ident[i])).All(x => x)))
            {
                Console.WriteLine("File is not PNG");
                return;
            }
            writer.Write(header);


            while (inStream.Position < inStream.Length)
            {
                byte[] byteLength = reader.ReadBytes(4);
                if (byteLength.Length < 4)
                {
                    Console.WriteLine("Unexpected End Of File");
                    return;
                }
                UInt32 length = (UInt32)((byteLength[0] << 24) | (byteLength[1] << 16) | (byteLength[2] << 8) | byteLength[3]);


                byte[] chunkType = reader.ReadBytes(4);
                if (chunkType.Length < 4)
                {
                    Console.WriteLine("Unexpected End Of File");
                    return;
                }
                string chunkName = Encoding.ASCII.GetString(chunkType);
                byte[] data = reader.ReadBytes((int)length);

                if (data.Length < length)
                {
                    Console.WriteLine("Unexpected End Of File");
                    return;
                }

                byte[] CRC = reader.ReadBytes(4);

                if (CRC.Length < 4)
                {
                    Console.WriteLine("Unexpected End Of File");
                    return;
                }
                uint crc = GetCRC(chunkType, data);

                UInt32 ExpectedCRC = (UInt32)((CRC[0] << 24) | (CRC[1] << 16) | (CRC[2] << 8) | CRC[3]);
                if (crc != ExpectedCRC)
                {
                    Console.WriteLine("Bad CRC");
                }

                switch (chunkName)
                {
                    case "IHDR" :
                        writer.Write(byteLength);
                        writer.Write(chunkType);
                        writer.Write(data);


                        Header chunkHeader = new Header(data);
                        chunkHeader.PrintImageHeader();
                        break;

                    case "IDAT" :
                        if (!foundIDAT)
                        {
                            //add transparency header before first IDAT header
                            byte[] tnsHeader = CreateTransparencyHeader();
                            writer.Write(tnsHeader);
                            foundIDAT = true;

                        }
                        writer.Write(byteLength);
                        writer.Write(chunkType);
                        writer.Write(data);

                        break;

                    default :
                        writer.Write(byteLength);
                        writer.Write(chunkType);
                        writer.Write(data);

                        break;
                }

                writer.Write(CRC);

            }
            reader.Close();
            writer.Flush();
            writer.Close();

        }
        public byte[] CreateTransparencyHeader()
        {
            byte[] white = { 0, 0 };

            List<byte> header = new List<byte>();
            byte[] length = { 0, 0, 0, 2 };  //length is just two bytes
            header.AddRange(length);
            header.AddRange(TNS);
            header.AddRange(white);

            UInt32 crc = GetCRC(TNS, white);
            byte[] crcBytes = { (byte)((crc >> 24) & 0xFF), (byte)((crc >> 16) & 0xFF), (byte)((crc >> 8) & 0xFF), (byte)(crc & 0xFF) };
            header.AddRange(crcBytes);

            return header.ToArray();
        }

        public uint GetCRC(byte[] type, byte[] bytes)
        {
            uint crc = 0xffffffff; /* CRC value is 32bit */
            //crc = CRC32(byteLength, crc);
            crc = CRC32(type, crc);
            crc = CRC32(bytes, crc);

            crc = Reflect(crc, 32);
            crc ^= 0xFFFFFFFF;

            return crc;

        }
        public uint CRC32(byte[] bytes, uint crc)
        {
            const uint polynomial = 0x04C11DB7; /* divisor is 32bit */

            foreach (byte b in bytes)
            {
                crc ^= (uint)(Reflect(b, 8) << 24); /* move byte into MSB of 32bit CRC */

                for (int i = 0; i < 8; i++)
                {
                    if ((crc & 0x80000000) != 0) /* test for MSB = bit 31 */
                    {
                        crc = (uint)((crc << 1) ^ polynomial);
                    }
                    else
                    {
                        crc <<= 1;
                    }
                }
            }
            return crc;
        }
        static public UInt32 Reflect(UInt32 data, int size)
        {
            UInt32 output = 0;
            for (int i = 0; i < size; i++)
            {
                UInt32 lsb = data & 0x01;
                output = (UInt32)((output << 1) | lsb);
                data >>= 1;
            }
            return output;
        }
    }
    public class Header
    {
        public UInt32 width { get; set; }
        public UInt32 height { get; set; }

        byte[] widthBytes { get; set; }
        byte[] heightBytes { get; set; }

        public byte depth { get; set; }
        public byte colourType { get; set; }
        public byte compressionMethod { get; set; }
        public byte filterMethod { get; set; }
        public byte interlaceMethod { get; set; }

        public Header(byte[] bytes)
        {

            UInt32 width = (UInt32)((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
            UInt32 height = (UInt32)((bytes[4] << 24) | (bytes[5] << 16) | (bytes[6] << 8) | bytes[7]);

            widthBytes = new byte[4];
            Array.Copy(bytes, widthBytes, 4);

            heightBytes = new byte[4];
            Array.Copy(bytes, 4, heightBytes, 0, 4);

            depth = bytes[8];
            colourType = bytes[9];
            compressionMethod = bytes[10];
            filterMethod = bytes[11];
            interlaceMethod = bytes[12];
        }
        public void PrintImageHeader()
        {

           Console.WriteLine("Width = '{0}', Height = '{1}', Bit Depth = '{2}', Colour Type = '{3}', Compression Method = '{4}', Filter Medthod = '{5}', Interlace Method = '{6}'",
               width.ToString(),
               height.ToString(),
               depth.ToString(),
               ((COLOUR_TYPE)colourType).ToString(),
               compressionMethod.ToString(),
               filterMethod.ToString(),
               interlaceMethod.ToString()
                   );

         }
        public byte[] GetHeader()
        {
            List<byte> header = new List<byte>();
            header.AddRange(widthBytes);
            header.AddRange(heightBytes);

            header.Add(depth);
            header.Add(colourType);
            header.Add(compressionMethod);
            header.Add(filterMethod);
            header.Add(interlaceMethod);


            return header.ToArray();
        }
    }
    public enum COLOUR_TYPE
    {
        GRAY_SCALE = 0,
        TRUE_COLOUR = 2,
        INDEXED_COLOUR = 3,
        GREY_SCALE_ALPHA = 4,
        TRUE_COLOUR_ALPHA = 6
    }

}
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • Sounds like a good question. Doesn't sound like an answer, though. – TaW Nov 19 '18 at 19:04
  • I wasn't sure what the colour mode was so I couldn't complete the code initially. Reading the comments it appears the OP says it file is one byte Grey Scale so I assumed it was colour mode 0. Reading the PNG documentation is looks like adding a transparency chunk before 1st data chunk will solve issue. – jdweng Nov 20 '18 at 12:50
  • Valid answer, but overcomplicated. You can just extract the palette and surgically insert the tRNS chunk, and be done with it. No need for full parsing of the png file at all. – Nyerguds Nov 20 '18 at 13:41
  • Note that technically the tRNS chunk doesn't have to be the full palette size; any unspecified alpha values default to opaque. – Nyerguds Nov 20 '18 at 14:03
  • Does a GreyScale image contain a palette chunk? I only have full color images so I do not know the chunks in a GreyScale image. I only made the tRNS palette two bytes and made the color value 0. Nothing says in the documentation says the Transparent chunk should be the same size as the palette. There is nothing complicated with my code. It is the simplest I could make it and do everything necessary to add the transparency chunk. The only thing extra I added was to display the Header so we can tell the colorour mode. – jdweng Nov 20 '18 at 14:10
  • Hmm. I wasn't aware of the grayscale format. In theory though, that format can probably be converted to paletted simply by generating a black to white palette for it and changing the type in the header from 0 to 3. The actual image data shouldn't care if it refers to a gray gradient or a palette index. – Nyerguds Nov 20 '18 at 14:25
  • Both Palette (suggested and Main), and Transparency chunks are optional. I think it is easier to add the transparency chunk than the palette chunk. You can only have one Palette chunk. So without knowing the existing chunks any answer may or may not work. Or handling every case would make the code more complicated. – jdweng Nov 20 '18 at 14:53
  • Palette chunk starts with a capital letter. According to standard chunk naming rules, that means it's _not_ optional (for indexed formats). I see now there's indeed a specific tRNS chunk for pure grayscale, that just contains "the gray value to use as transparent colour" though, yes. Your code never checks the colour type when writing the transparency chunk, though, and white would be value 1, not 0... – Nyerguds Nov 21 '18 at 09:13
  • See figure 5.2 (With Plate) and 5.3 (Without Plate) : https://www.w3.org/TR/PNG/#11IDAT. I wasn't sure of the color for white and though it would be 0 since full opaque is 2^8-1 – jdweng Nov 21 '18 at 09:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/184021/discussion-between-nyerguds-and-jdweng). – Nyerguds Nov 21 '18 at 10:23