如何从PNG中删除伽马信息?

4
我正在尝试生成没有伽马信息的图像,以便IE8可以正确显示它们。使用了以下代码,但结果是扭曲的图像,看起来与原始图像完全不同。
 ///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();

这里是上述代码生成的原始图片扭曲图片
谢谢!
3个回答

4

我在多个地方看到了同样的问题,但似乎没有答案,所以我在这里提供我的答案。我不知道Java imageio是否保存gamma。考虑到gamma是依赖于系统的,imageio可能处理不了它。有一件事是确定的:imageio在读取png时忽略gamma。

PNG是基于块的图像格式。Gamma是14个辅助块之一,它处理创建图像的计算机系统之间的差异,使它们在不同系统上看起来更或 less相等“明亮”。每个块都以数据长度和块标识符开头,后跟4字节的CRC校验和。数据长度不包括数据长度属性本身和块标识符。gAMA块由十六进制0x67414D41标识。

这是从png图像中删除gAMA的原始方法:我们假设输入流处于有效的PNG格式中。首先读取8个字节,即png标识符0x89504e470d0a1a0aL。然后读取另外25个字节,其中包括图像头。总共我们从文件顶部读取了33个字节。现在将它们保存到具有png扩展名的另一个临时文件中。现在进入while循环。我们逐个读取块:如果它不是IEND,也不是gAMA块,我们将其复制到输出临时文件中。如果它是gAMA块,则跳过它,直到达到IEND,它应该是最后一个块,我们将其复制到临时文件中。完成。这是整个测试代码,以展示如何完成操作(仅用于演示目的,未经优化):

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);       
     }
}

由于输入是Java InputStream,我们可以使用某种编码器将图像编码为PNG格式并写入ByteArrayOutputStream,然后将其作为ByteArrayInputSteam提供给上面的测试类,其中伽马信息(如果有)将被移除。以下是结果:
左边是带有gAMA的原始图像,右边是删除了gAMA的相同图像。
图像来源:http://r6.ca/cs488/kosh.png 编辑:这里是一份修订后的代码,用于删除任何附加块。
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("************************");
             }
          }
        }
     }
}

使用方法: java PNGChunkRemover filename.png all 将移除预定义的14个辅助块。

java PNGChunkRemover filename.png gama time ... 只会删除指定的块,这些块在png文件后面进行指定。

注意: 如果将文件夹名称作为PNGChunkRemover的第一个参数指定,则将处理文件夹中的所有png文件。

上述示例已成为Java图像库的一部分,可在https://github.com/dragon66/icafe找到。


1

你也可以使用(我的)PNGJ库来完成 http://code.google.com/p/pngj/

例如

 PngReader pngr = FileHelper.createPngReader(new File(origFilename));
 PngWriter pngw = FileHelper.createPngWriter(new File(destFilename), pngr.imgInfo, false);
 pngw.copyChunksFirst(pngr, ChunkCopyBehaviour.COPY_ALL); // all chunks are queued
 PngChunkGAMA gama = (PngChunkGAMA) pngw.getChunkList().getQueuedById1(ChunkHelper.gAMA);
 if (gama != null) {
   System.out.println("removing gama chunk gamma=" + gama.getGamma());
   pngw.getChunkList().removeChunk(gama);
 } 
 for (int row = 0; row < pngr.imgInfo.rows; row++) {
   ImageLine l1 = pngr.readRow(row);
   pngw.writeRow(l1, row);
 }
 pngw.copyChunksLast(pngr, ChunkCopyBehaviour.COPY_ALL); // in case some new metadata has been read
 pngw.end();

包含在库示例中。


0
工具 pngcrush 可以删除伽马信息和其他不需要的块:
pngcrush -m 3 -rem gAMA -rem cHRM -rem iCCP -rem sRGB in.png out.png

它同时重新压缩PNG,尝试不同的方法。-m 3选项仅尝试第3种方法,这似乎快速且相当有效。如果您想要最小的PNG,请省略该选项。


那是重新编码。如果你不想重新编码怎么办? - Валерий Заподовников
看起来pngcrush没有“null”编码方法。这可能是一个不错的补丁。 - Sam Watkins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接