2011-03-07 242 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