最近在做一個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了。以上,轉載請註明出處。