javapnggamma

How Do I Remove Gamma Information From A PNG


I am trying to produce images without gamma information so that IE8 can display them correctly. Used the following code but the result is a distorted image that looks nothing like the original image.

 ///PNG
  PNGEncodeParam params= PNGEncodeParam.getDefaultEncodeParam(outImage);
  params.unsetGamma();
  params.setChromaticity(DEFAULT_CHROMA);
  params.setSRGBIntent(PNGEncodeParam.INTENT_ABSOLUTE);
  ImageEncoder  encoder= ImageCodec.createImageEncoder("PNG", response.getOutputStream(), params);
  encoder.encode(outImage);
  response.getOutputStream().close();

Here is the original image and the distorted one resulting from the code above.

Thanks!


Solution

  • I saw the same question asked several places but there seems to be no answer, so I am offering mine here. I have no idea whether Java imageio saves gamma or not. Given the fact gamma is system dependent, it is unlikely imageio could handle it. One thing is for sure: imageio ignores gamma when reading pngs.

    PNG is a chunk based image format. Gamma is one of the 14 Ancillary chunks which takes care of the differences of the computer systems that create the image to make them looks more or less equally "bright" on different systems. Each trunk starts with a data length and a trunk identifier followed by a 4 bytes CRC checksum. The data length doesn't include the data length property itself and the trunk identifier. The gAMA chunk is identified by hex 0x67414D41.

    Here is the raw way to remove the gAMA from png image: we assume the input stream is in valid PNG format. First read 8 bytes which is the png identifier 0x89504e470d0a1a0aL. Then read another 25 bytes which comprise of the image header. Altogether we have read 33 bytes from the top of the file. Now save them to another temp file with png extension. Now it comes to a while loop. We read chunks one by one: if it's not IEND and it's not a gAMA chunk, we copy it to the output tempfile. If it's a gAMA trunk, we skip it, until we reach IEND which should be the last chunk and we copy it to the tempfile. Done. Here is the whole test code to show how things are done (it is just for demo purpose, not optimized):

    import java.io.*;
    
    public class RemoveGamma
    {  
         /** PNG signature constant */
         public static final long SIGNATURE = 0x89504E470D0A1A0AL;
         /** PNG Chunk type constants, 4 Critical chunks */
         /** Image header */
         private static final int IHDR = 0x49484452;   // "IHDR"
         /** Image data */
         private static final int IDAT = 0x49444154;   // "IDAT"
        /** Image trailer */
         private static final int IEND = 0x49454E44;   // "IEND"
         /** Palette */
         private static final int PLTE = 0x504C5445;   // "PLTE"
         /** 14 Ancillary chunks */
         /** Transparency */
         private static final int tRNS = 0x74524E53;   // "tRNs"
        /** Image gamma */
         private static final int gAMA = 0x67414D41;   // "gAMA"
         /** Primary chromaticities */
         private static final int cHRM = 0x6348524D;   // "cHRM"
         /** Standard RGB color space */
         private static final int sRGB = 0x73524742;   // "sRGB"
         /** Embedded ICC profile */
         private static final int iCCP = 0x69434350;   // "iCCP"
         /** Textual data */
         private static final int tEXt = 0x74455874;   // "tEXt"
         /** Compressed textual data */
         private static final int zTXt = 0x7A545874;   // "zTXt"
         /** International textual data */
         private static final int iTXt = 0x69545874;   // "iTXt"
         /** Background color */
         private static final int bKGD = 0x624B4744;   // "bKGD"
         /** Physical pixel dimensions */
         private static final int pHYs = 0x70485973;   // "pHYs"
         /** Significant bits */
         private static final int sBIT = 0x73424954;   // "sBIT"
         /** Suggested palette */
         private static final int sPLT = 0x73504C54;   // "sPLT"
         /** Palette histogram */
         private static final int hIST = 0x68495354;   // "hIST"
         /** Image last-modification time */
         private static final int tIME = 0x74494D45;   // "tIME"
    
         public void remove(InputStream is) throws Exception
         {
             //Local variables for reading chunks
              int data_len = 0;
              int chunk_type = 0;
              long CRC = 0;
              byte[] buf=null;
    
              DataOutputStream ds = new DataOutputStream(new FileOutputStream("temp.png")); 
    
              long signature = readLong(is);
    
              if (signature != SIGNATURE)
              {
                  System.out.println("--- NOT A PNG IMAGE ---");
                  return;
              }
    
              ds.writeLong(SIGNATURE);
    
              //*******************************
              //Chuncks follow, start with IHDR
              //*******************************
              /** Chunk layout
                  Each chunk consists of four parts:
    
                  Length
                     A 4-byte unsigned integer giving the number of bytes in the chunk's data field.
                     The length counts only the data field, not itself, the chunk type code, or the CRC.
                     Zero is a valid length. Although encoders and decoders should treat the length as unsigned, 
                     its value must not exceed 2^31-1 bytes.
    
                  Chunk Type
                     A 4-byte chunk type code. For convenience in description and in examining PNG files, 
                     type codes are restricted to consist of uppercase and lowercase ASCII letters 
                     (A-Z and a-z, or 65-90 and 97-122 decimal). However, encoders and decoders must treat 
                     the codes as fixed binary values, not character strings. For example, it would not be
                     correct to represent the type code IDAT by the EBCDIC equivalents of those letters. 
                     Additional naming conventions for chunk types are discussed in the next section.
    
                  Chunk Data
                     The data bytes appropriate to the chunk type, if any. This field can be of zero length.
    
                  CRC
                     A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk,
                     including the chunk type code and chunk data fields, but not including the length field. 
                     The CRC is always present, even for chunks containing no data. See CRC algorithm. 
                */
    
                /** Read header */
                /** We are expecting IHDR */
                if ((readInt(is)!=13)||(readInt(is) != IHDR))
                {
                    System.out.println("--- NOT A PNG IMAGE ---");
                    return;
                }
    
                ds.writeInt(13);//We expect length to be 13 bytes
                ds.writeInt(IHDR);
    
                buf = new byte[13+4];//13 plus 4 bytes CRC
                is.read(buf,0,17);
                ds.write(buf);
    
                while (true)
                {
                    data_len = readInt(is);
                    chunk_type = readInt(is);
                    //System.out.println("chunk type: 0x"+Integer.toHexString(chunk_type));
    
                    if (chunk_type == IEND)
                    {
                       System.out.println("IEND found");
                       ds.writeInt(data_len);
                       ds.writeInt(IEND);
                       int crc = readInt(is);
                       ds.writeInt(crc);
                       break;
                    }
    
                    switch (chunk_type)
                    {
                       case gAMA://or any non-significant chunk you want to remove
                       {
                           System.out.println("gamma found");
                           is.skip(data_len+4);
                           break;
                       }
                       default:
                       {
                           buf = new byte[data_len+4];
                           is.read(buf,0, data_len+4);
                           ds.writeInt(data_len);
                           ds.writeInt(chunk_type);
                           ds.write(buf);
                           break;
                       }
                    }
                }
                is.close();
                ds.close();
         }
    
         private int readInt(InputStream is) throws Exception
         {
             byte[] buf = new byte[4];
             is.read(buf,0,4);
             return (((buf[0]&0xff)<<24)|((buf[1]&0xff)<<16)|
                                    ((buf[2]&0xff)<<8)|(buf[3]&0xff));
         }
    
         private long readLong(InputStream is) throws Exception
         {
             byte[] buf = new byte[8];
             is.read(buf,0,8);
             return (((buf[0]&0xffL)<<56)|((buf[1]&0xffL)<<48)|
                                    ((buf[2]&0xffL)<<40)|((buf[3]&0xffL)<<32)|((buf[4]&0xffL)<<24)|
                                      ((buf[5]&0xffL)<<16)|((buf[6]&0xffL)<<8)|(buf[7]&0xffL));
         }
    
         public static void main(String args[]) throws Exception
         {
            FileInputStream fs = new FileInputStream(args[0]);
            RemoveGamma rg = new RemoveGamma();
            rg.remove(fs);       
         }
    }
    

    Since the input is a Java InputStream, we could use some kind of encoder to encode image as a PNG and write it to a ByteArrayOutputStream which later will be fed to the above test class as a ByteArrayInputSteam and the the gamma information (if any) will be removed. Here is the result:

    enter image description here

    The left side is the original image with gAMA, the right side is the same image with gAMA removed.

    Image source: http://r6.ca/cs488/kosh.png

    Edit: here is a revised version of the code to remove any ancillary chunks.

    import java.io.*;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Set;
    
    public class PNGChunkRemover
    {  
         /** PNG signature constant */
         private static final long SIGNATURE = 0x89504E470D0A1A0AL;
         /** PNG Chunk type constants, 4 Critical chunks */
         /** Image header */
         private static final int IHDR = 0x49484452;   // "IHDR"
         /** Image data */
         private static final int IDAT = 0x49444154;   // "IDAT"
        /** Image trailer */
         private static final int IEND = 0x49454E44;   // "IEND"
         /** Palette */
         private static final int PLTE = 0x504C5445;   // "PLTE"
    
         //Ancillary chunks keys
         private static String[] KEYS = { "TRNS", "GAMA","CHRM","SRGB","ICCP","TEXT","ZTXT",
                                          "ITXT","BKGD","PHYS","SBIT","SPLT","HIST","TIME"};
    
         private static int[]  VALUES = {0x74524E53,0x67414D41,0x6348524D,0x73524742,0x69434350,0x74455874,0x7A545874,
                                         0x69545874,0x624B4744,0x70485973,0x73424954,0x73504C54,0x68495354,0x74494D45};
    
         private static HashMap<String, Integer> TRUNK_TYPES = new HashMap<String, Integer>()
         {{ 
             for(int i=0;i<KEYS.length;i++)
               put(KEYS[i],VALUES[i]);
         }};
    
         private static HashMap<Integer, String> REVERSE_TRUNK_TYPES = new HashMap<Integer,String>()
         {{ 
             for(int i=0;i<KEYS.length;i++)
               put(VALUES[i],KEYS[i]);
         }};
    
         private static Set<Integer> REMOVABLE = new HashSet<Integer>();
    
         private static void remove(InputStream is, File dir, String fileName) throws Exception
         {
             //Local variables for reading chunks
              int data_len = 0;
              int chunk_type = 0;
              byte[] buf=null;
    
              DataOutputStream ds = new DataOutputStream(new FileOutputStream(new File(dir,fileName))); 
    
              long signature = readLong(is);
    
              if (signature != SIGNATURE)
              {
                  System.out.println("--- NOT A PNG IMAGE ---");
                  return;
              }
    
              ds.writeLong(SIGNATURE);
    
              /** Read header */
              /** We are expecting IHDR */
              if ((readInt(is)!=13)||(readInt(is) != IHDR))
              {
                  System.out.println("--- NOT A PNG IMAGE ---");
                  return;
              }
    
              ds.writeInt(13);//We expect length to be 13 bytes
              ds.writeInt(IHDR);
    
              buf = new byte[13+4];//13 plus 4 bytes CRC
              is.read(buf,0,17);
              ds.write(buf);
    
              while (true)
              {
                    data_len = readInt(is);
                    chunk_type = readInt(is);
                    //System.out.println("chunk type: 0x"+Integer.toHexString(chunk_type));
    
                    if (chunk_type == IEND)
                    {
                       System.out.println("IEND found");
                       ds.writeInt(data_len);
                       ds.writeInt(IEND);
                       int crc = readInt(is);
                       ds.writeInt(crc);
                       break;
                    }
                    if(REMOVABLE.contains(chunk_type))
                    {
                        System.out.println(REVERSE_TRUNK_TYPES.get(chunk_type)+"Chunk removed!");
                        is.skip(data_len+4);
                    }
                    else
                    {
                        buf = new byte[data_len+4];
                        is.read(buf,0, data_len+4);
                        ds.writeInt(data_len);
                        ds.writeInt(chunk_type);
                        ds.write(buf);
                    }
              }
              is.close();
              ds.close();
         }
    
         private static int readInt(InputStream is) throws Exception
         {
             byte[] buf = new byte[4];
             int bytes_read = is.read(buf,0,4);
             if(bytes_read<0) return IEND; 
             return (((buf[0]&0xff)<<24)|((buf[1]&0xff)<<16)|
                                    ((buf[2]&0xff)<<8)|(buf[3]&0xff));
         }
    
         private static long readLong(InputStream is) throws Exception
         {
             byte[] buf = new byte[8];
             int bytes_read = is.read(buf,0,8);
             if(bytes_read<0) return IEND; 
             return (((buf[0]&0xffL)<<56)|((buf[1]&0xffL)<<48)|
                                    ((buf[2]&0xffL)<<40)|((buf[3]&0xffL)<<32)|((buf[4]&0xffL)<<24)|
                                      ((buf[5]&0xffL)<<16)|((buf[6]&0xffL)<<8)|(buf[7]&0xffL));
         }
    
         public static void main(String args[]) throws Exception
         {
            if(args.length>0)
            {
              File[] files = {new File(args[0])};
              File dir = new File(".");
    
              if(files[0].isDirectory())
              {
                 dir = files[0];
    
                 files = files[0].listFiles(new FileFilter(){
                    public boolean accept(File file)
                    {
                       if(file.getName().toLowerCase().endsWith("png")){
                          return true;
                       }
                       return false;
                    }
                 }
                );
              }     
    
              if(args.length>1)
              { 
                 FileInputStream fs = null;
    
                 if(args[1].equalsIgnoreCase("all")){
                    REMOVABLE = REVERSE_TRUNK_TYPES.keySet();
                 }
                 else
                 {
                    String key = "";
                    for (int i=1;i<args.length;i++)
                    {
                        key = args[i].toUpperCase();
                        if(TRUNK_TYPES.containsKey(key))
                          REMOVABLE.add(TRUNK_TYPES.get(key));
                    }
                 }
                 for(int i= files.length-1;i>=0;i--)
                 {
                    String outFileName = files[i].getName();
                    outFileName = outFileName.substring(0,outFileName.lastIndexOf('.'))
                        +"_slim.png";
                    System.out.println("<<"+files[i].getName());
                    fs = new FileInputStream(files[i]);
                    remove(fs, dir, outFileName);
                    System.out.println(">>"+outFileName);   
                    System.out.println("************************");
                 }
              }
            }
         }
    }
    

    Usage: java PNGChunkRemover filename.png all will remove any of the predefined 14 ancillary chunks.

    java PNGChunkRemover filename.png gama time ... will only remove the chunks specified after the png file.

    Note: If a folder name is specified as the first argument to the PNGChunkRemover, all png file in the folder will be processed.

    The above example has become part of a Java image library which can be found at https://github.com/dragon66/icafe