最近在做一个java的图片管理器的课程设计,加入了一个读取psd格式的功能,之所以说加入,是因为jdk本身自带的ImageIO_API只持支持jpg、bmp、png、gif这几种格式的图片格式而已。
之前查过像是imagemagick之类的工具,我发现它们功能太多太强大了,而我想要做的只是读入psd格式文件,让其转化成BufferImage,顺带读取出图片的一点信息,仅此而已,所以我认为没有必要动用到这种航母级的工具。
主要要做的,是找出psd格式文件里面需要用到的字节,不需要用到的,例如图层路径等等的信息,则直接跳过。
下面是psd格式说明文档(4.0.1版本)里面的内容,可以看到它分为5个部分:
我用到的主要是第一部分的file header和最后一部分的image data,前者保存了图片的一些基本信息(长宽,通道数目等等),后者就是图片像素信息。
我把读取psd格式这一功能封装在一个叫PsdReader的类里面,首先是打开文件,我显式使用了FileChannel和MappedByteBuffer加快读取速度,起初用RandomAccessFile也是可以达到同样目的,但是效率过低。构造方法如下:
private BufferedImage img = null;//最终获得的目标图片
private int[] pixels;
private RandomAccessFile raf;
private int[] byteArray;
private int[][][] channelColor;//每条通道的颜色,0号位是red,1号位是green,2号位是blue,如果是4通道,alpha直接跳过
private int[][] numOfBytePerLine;//每行的字节数,扫描图片信息的时候是逐行进行的,因为rle压缩的关系,每行的字节数都不一定相同
private short numOfChannel;//通道的数目,如果是带有透明度,则是4通道,否则通常是3通道
private int height;
private int width;
private short isRle;//压缩方式,是0则没有压缩,是1则是rle压缩
private MappedByteBuffer mbbi;
public PsdReader(File file) {
FileChannel fc = null;
try {
this.raf = new RandomAccessFile(file, "r");
fc = raf.getChannel();
long size = fc.size();
this.mbbi = fc.map(FileChannel.MapMode.READ_ONLY, 0, size);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
this.readFile();//关键,读取图片文件中的信息
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
pixels = new int[width*height];
this.initPixels(pixels);
this.setRGB(img, 0, 0, width, height, pixels);
try {
fc.close();
this.raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readFile() {
try {
//-------第一部分:文件头------------------
//通道数量
this.mbbi.position(0x0c);//因为一开始的4个字节,是8bps(一个符号,我理解成这个标志代表此文件是psd格式文件,接着是两个字节的版本信息,接着是6个字节的保留位,从0x0c开始的两个字节,便是通道数目,之后亦是这样读取想要的信息,便不再累述,详细可参考下面给出的psd格式文档里面的信息表
numOfChannel = this.mbbi.getShort();
//图像高度
height = this.mbbi.getInt();
//图像宽度
width = this.mbbi.getInt();
//图像深度(每个通道的颜色位数)
short depth = this.mbbi.getShort();
//是rgb模式则type=3
short type = this.mbbi.getShort();
//--------第二部分:色彩模式信息,这部分的长度通常为0----
int lenOfColorModel = this.mbbi.getInt();
this.mbbi.position(lenOfColorModel+this.mbbi.position());
//--------第三部分:图像资源数据------------------
int lenOfImageResourceBlock = this.mbbi.getInt();
//System.out.println("lenOfImageResourceBlock="+lenOfImageResourceBlock);
this.mbbi.position(lenOfImageResourceBlock+this.mbbi.position());
//--------第四部分:图层与蒙版信息----------------
int lenOfLayerInfo = this.mbbi.getInt();
this.mbbi.position(lenOfLayerInfo+this.mbbi.position());
//--------第五部分:图像数据--------------------
isRle = this.mbbi.getShort();
} catch (Exception e1) {
e1.printStackTrace();
}
拿到位于文件最后的image data之后,便开始主要操作了:解压。
从上图可以看到,image data是以两个字节的compression开始的,如果图片未经过压缩,则这两个字节是00,否则是01,用editplus等工具的16进制查看器里面可以查看出来,起初我就是这样查找它的压缩规律的,后来发现这样做太愚蠢了。
因为psd格式文档里面明确写道:
由此看出,压缩方式是固定的packBits压缩方式,百度一下可以找到不少的关于此种压缩的解压和编码方式,这里就不累述了,下面是我用java实现的解压方法:
private void unpackbits(int lenOfInput, int[] channelColor) {
short n = 0;
int last = 0;
while(lenOfInput>0){
try {
// n = raf.readByte();
n = this.mbbi.get();
lenOfInput--;
} catch (Exception e) {
e.printStackTrace();
}
if(0<=n && n<=127) {
int repeatTime = n;
++repeatTime;
for(int t=0; t<repeatTime; t++) {
try {
// channelColor[last+t] = raf.readUnsignedByte();
int ti = this.mbbi.get();
if(ti<0) { ti += 256; }
channelColor[last+t] = ti;
lenOfInput--;
} catch (Exception e) {
e.printStackTrace();
}
}
last += repeatTime;
}
else if(-1>=n && n>=-127) {
int val = 0;
int repeatTime = -n;
++repeatTime;
try {
// val = raf.readUnsignedByte();
int ti = this.mbbi.get();
if(ti<0) { ti += 256; }
val = ti;
//System.out.println(val);
lenOfInput--;
} catch (Exception e) {
e.printStackTrace();
}
for(int t=0; t<repeatTime; t++) {
channelColor[last+t] = val;
}
last += repeatTime;
}
else if(n==-128) {
//noop
}
}
}
如果图片是不经过压缩的,那就省略解压这一步,直接将像素数据塞进数组里面就行了。
下面是我写的整个PsdReader的完整代码:
package com.zer0w0.entity;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class PsdReader {
private BufferedImage img = null;
private int[] pixels;
private RandomAccessFile raf;
private int[] byteArray;
//用来接住unsignedByte,byte不存作负数(否则抛异常,说越过颜色范围)
private int[][][] channelColor;
private int[][] numOfBytePerLine;
// private final static int RED = 0;
// private final static int GREEN = 1;
// private final static int BLUE = 2;
private short numOfChannel;
private int height;
private int width;
private short isRle;
private MappedByteBuffer mbbi;
public PsdReader(File file) {
FileChannel fc = null;
try {
this.raf = new RandomAccessFile(file, "r");
fc = raf.getChannel();
long size = fc.size();
this.mbbi = fc.map(FileChannel.MapMode.READ_ONLY, 0, size);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
this.readFile();
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
pixels = new int[width*height];
this.initPixels(pixels);
this.setRGB(img, 0, 0, width, height, pixels);
try {
fc.close();
this.raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public BufferedImage getImg() {
return img;
}
private void initPixels(int[] pixels) {
int index = 0;
int a = 255;
for(int h=0; h<this.height; h++) {
for(int w=0; w<this.width; w++) {
int r = this.channelColor[0][h][w];
int g = this.channelColor[1][h][w];
int b = this.channelColor[2][h][w];
if(this.numOfChannel>3) {
a = this.channelColor[3][h][w];
}
pixels[index] = (a<<24) | (r<<16)
| (g<<8) | b;
index++;
}
}
}
private void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) {
int type = image.getType();
if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB )
image.getRaster().setDataElements( x, y, width, height, pixels );
else
image.setRGB( x, y, width, height, pixels, 0, width );
}
private void readFile() {
try {
//-------第一部分:文件头------------------
//通道数量
// this.raf.seek(0x0c);
this.mbbi.position(0x0c);
// numOfChannel = this.raf.readShort();
numOfChannel = this.mbbi.getShort();
//System.out.println("numOfChannel="+numOfChannel);
//图像高度
// height = this.raf.readInt();
height = this.mbbi.getInt();
//System.out.println("height="+height);
//图像宽度
// width = this.raf.readInt();
width = this.mbbi.getInt();
//System.out.println("width="+width);
//图像深度(每个通道的颜色位数)
// short depth = this.raf.readShort();
short depth = this.mbbi.getShort();
//System.out.println("depth="+depth);
//是rgb模式则type=3
// short type = this.raf.readShort();
short type = this.mbbi.getShort();
//System.out.println("type="+type);
//--------第二部分:色彩模式信息,这部分的长度通常为0----
// int lenOfColorModel = raf.readInt();
int lenOfColorModel = this.mbbi.getInt();
//System.out.println("lenOfColorModel="+lenOfColorModel);
// this.raf.seek(lenOfColorModel+this.raf.getFilePointer());//长度信息占4个字节,但是不用加,下同
this.mbbi.position(lenOfColorModel+this.mbbi.position());
//--------第三部分:图像资源数据------------------
// int lenOfImageResourceBlock = raf.readInt();
int lenOfImageResourceBlock = this.mbbi.getInt();
//System.out.println("lenOfImageResourceBlock="+lenOfImageResourceBlock);
// this.raf.seek(lenOfImageResourceBlock+this.raf.getFilePointer());
this.mbbi.position(lenOfImageResourceBlock+this.mbbi.position());
//--------第四部分:图层与蒙版信息----------------
// int lenOfLayerInfo = raf.readInt();
int lenOfLayerInfo = this.mbbi.getInt();
//System.out.println("lenOfLayer="+lenOfLayerInfo);
// this.raf.seek(lenOfLayerInfo+raf.getFilePointer());
this.mbbi.position(lenOfLayerInfo+this.mbbi.position());
//--------第五部分:图像数据--------------------
// isRle = raf.readShort();
isRle = this.mbbi.getShort();
//System.out.println("isRle="+isRle);
// //System.out.println("nowPosition="+this.raf.getFilePointer());
//System.out.println("nowPosition="+this.mbbi.position());
} catch (Exception e1) {
e1.printStackTrace();
}
this.channelColor = new int[numOfChannel][height][width];
if(isRle==1){
this.numOfBytePerLine = new int[numOfChannel][height];
for(int i=0; i<numOfChannel; i++) {
for(int j=0; j<height; j++) {
try {
//TODO
// this.numOfBytePerLine[i][j] = this.raf.readUnsignedShort();
int ti = this.mbbi.getShort();
if(ti<0) { ti += 65536; }
this.numOfBytePerLine[i][j] = ti;
} catch (Exception e) {
e.printStackTrace();
}
}
}
for(int c=0; c<numOfChannel; c++) {
for(int h=0; h<height; h++) {
this.unpackbits(numOfBytePerLine[c][h],channelColor[c][h]);
}
}
}else if(isRle==0) {
for(int c=0; c<numOfChannel; c++) {
for(int h=0; h<height; h++) {
for(int w=0; w<width; w++) {
try {
// this.channelColor[c][h][w] = this.raf.readUnsignedByte();
int ti = this.mbbi.get();
if(ti<0) { ti += 256; }
this.channelColor[c][h][w] = ti;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
private void unpackbits(int lenOfInput, int[] channelColor) {
short n = 0;
int last = 0;
while(lenOfInput>0){
try {
// n = raf.readByte();
n = this.mbbi.get();
lenOfInput--;
} catch (Exception e) {
e.printStackTrace();
}
if(0<=n && n<=127) {
int repeatTime = n;
++repeatTime;
for(int t=0; t<repeatTime; t++) {
try {
// channelColor[last+t] = raf.readUnsignedByte();
int ti = this.mbbi.get();
if(ti<0) { ti += 256; }
channelColor[last+t] = ti;
lenOfInput--;
} catch (Exception e) {
e.printStackTrace();
}
}
last += repeatTime;
}
else if(-1>=n && n>=-127) {
int val = 0;
int repeatTime = -n;
++repeatTime;
try {
// val = raf.readUnsignedByte();
int ti = this.mbbi.get();
if(ti<0) { ti += 256; }
val = ti;
//System.out.println(val);
lenOfInput--;
} catch (Exception e) {
e.printStackTrace();
}
for(int t=0; t<repeatTime; t++) {
channelColor[last+t] = val;
}
last += repeatTime;
}
else if(n==-128) {
//noop
}
}
}
}
使用new PsdReader(new File(psd文件的路径)).getImg()便可以拿到这个psd文件的BufferedImage了。以上,转载请注明出处。