2011-03-07 216 views
2

我试图生成没有伽玛信息的图像,以便IE8能够正确显示它们。使用下面的代码,但结果是扭曲的图像,看起来没有像原始图像。如何从PNG中删除Gamma信息

///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(); 

这里是original image和从上述代码产生的distorted one

谢谢!

回答

2

我看到同样的问题问几个地方,但似乎没有答案,所以我在这里提供我的。我不知道Java imageio是否保存伽玛值。鉴于伽玛系统依赖的事实,imageio不可能处理它。有一件事是肯定的:imageio在阅读PNG时忽略伽马。

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

下面是从png图像中删除gAMA的原始方式:我们假设输入流为有效的PNG格式。首先读取8个字节,即png标识符0x89504e470d0a1a0aL。然后读取另外25个字节,其中包含图像头。我们总共从文件顶部读取了33个字节。现在将它们保存到另一个带有png扩展名的临时文件。现在它来到一个while循环。我们逐个读取块:如果它不是IEND,它不是gAMA块,我们将它复制到输出tempfile。如果它是一个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被提供给上述测试类,并且伽玛信息(如果有的话)将被删除。下面是结果:

enter image description here

左侧是与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

0

找到您也可以使用(我)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(); 

包含在图书馆samples