Как удалить информацию о гамме из PNG - PullRequest
3 голосов
/ 07 марта 2011

Я пытаюсь создать изображения без гамма-информации, чтобы 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();

Вот исходное изображение и искаженное , полученное в результате кода выше.

Спасибо!

Ответы [ 2 ]

3 голосов
/ 23 марта 2012

Я видел один и тот же вопрос, заданный в нескольких местах, но, похоже, ответа нет, поэтому я предлагаю свой здесь. Я понятия не имею, сохраняет ли Java imageio гамму или нет. Учитывая тот факт, что гамма зависит от системы, маловероятно, что imageio справится с этим. Одно можно сказать наверняка: imageio игнорирует гамму при чтении PNG.

PNG - это формат изображения на основе чанка. Гамма - это один из 14 вспомогательных блоков, который учитывает различия компьютерных систем, которые создают изображение, чтобы они выглядели более или менее одинаково «ярко» в разных системах. Каждый транк начинается с длины данных и идентификатора транка, за которым следует 4-байтовая контрольная сумма CRC. Длина данных не включает само свойство длины данных и идентификатор соединительной линии. Блок gAMA обозначается гексами 0x67414D41.

Вот необработанный способ удаления gAMA из изображения png: мы предполагаем, что входной поток имеет допустимый формат PNG. Сначала прочитайте 8 байтов, которые являются идентификатором png 0x89504e470d0a1a0aL. Затем прочитайте еще 25 байтов, которые составляют заголовок изображения. Всего мы прочитали 33 байта из верхней части файла. Теперь сохраните их в другой временный файл с расширением png. Теперь дело доходит до цикла пока. Мы читаем куски один за другим: если это не 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 и информации о гамме (если любой) будет удален. Вот результат:

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

1 голос
/ 14 апреля 2012

Вы также можете сделать это с (моей) библиотекой 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();

Входит в библиотеку семплов .

...