我在多个地方看到了同样的问题,但似乎没有答案,所以我在这里提供我的答案。我不知道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
{
public static final long SIGNATURE = 0x89504E470D0A1A0AL;
private static final int IHDR = 0x49484452;
private static final int IDAT = 0x49444154;
private static final int IEND = 0x49454E44;
private static final int PLTE = 0x504C5445;
private static final int tRNS = 0x74524E53;
private static final int gAMA = 0x67414D41;
private static final int cHRM = 0x6348524D;
private static final int sRGB = 0x73524742;
private static final int iCCP = 0x69434350;
private static final int tEXt = 0x74455874;
private static final int zTXt = 0x7A545874;
private static final int iTXt = 0x69545874;
private static final int bKGD = 0x624B4744;
private static final int pHYs = 0x70485973;
private static final int sBIT = 0x73424954;
private static final int sPLT = 0x73504C54;
private static final int hIST = 0x68495354;
private static final int tIME = 0x74494D45;
public void remove(InputStream is) throws Exception
{
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);
if ((readInt(is)!=13)||(readInt(is) != IHDR))
{
System.out.println("--- NOT A PNG IMAGE ---");
return;
}
ds.writeInt(13);
ds.writeInt(IHDR);
buf = new byte[13+4];
is.read(buf,0,17);
ds.write(buf);
while (true)
{
data_len = readInt(is);
chunk_type = readInt(is);
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:
{
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
{
private static final long SIGNATURE = 0x89504E470D0A1A0AL;
private static final int IHDR = 0x49484452;
private static final int IDAT = 0x49444154;
private static final int IEND = 0x49454E44;
private static final int PLTE = 0x504C5445;
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
{
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);
if ((readInt(is)!=13)||(readInt(is) != IHDR))
{
System.out.println("--- NOT A PNG IMAGE ---");
return;
}
ds.writeInt(13);
ds.writeInt(IHDR);
buf = new byte[13+4];
is.read(buf,0,17);
ds.write(buf);
while (true)
{
data_len = readInt(is);
chunk_type = readInt(is);
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找到。