import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
/**
* Class PSDReader - Decodes a PhotoShop (.psd) file into one or more frames.
* Supports uncompressed or RLE-compressed RGB files only. Each layer may be
* retrieved as a full frame BufferedImage, or as a smaller image with an offset
* if the layer does not occupy the full frame size. Transparency in the
* original psd file is preserved in the returned BufferedImage"s. Does not
* support additional features in PS versions higher than 3.0. Example: <br>
*
* <pre>
* PSDReader r = new PSDReader();
* r.read("sample.psd");
* int n = r.getFrameCount();
* for (int i = 0; i < n; i++) {
* BufferedImage image = r.getLayer(i);
* Point offset = r.getLayerOffset(i);
* // do something with image
* }
* </pre>
*
* No copyright asserted on the source code of this class. May be used for any
* purpose. Please forward any corrections to kweiner@fmsware.ru.
*
* @author Kevin Weiner, FM Software.
* @version 1.1 January 2004 [bug fix; add RLE support]
*
*/
public class PSDReader {
/**
* File read status: No errors.
*/
public static final int STATUS_OK = 0;
/**
* File read status: Error decoding file (may be partially decoded)
*/
public static final int STATUS_FORMAT_ERROR = 1;
/**
* File read status: Unable to open source.
*/
public static final int STATUS_OPEN_ERROR = 2;
/**
* File read status: Unsupported format
*/
public static final int STATUS_UNSUPPORTED = 3;
public static int ImageType = BufferedImage.TYPE_INT_ARGB;
protected BufferedInputStream input;
protected int frameCount;
protected BufferedImage[] frames;
protected int status = 0;
protected int nChan;
protected int width;
protected int height;
protected int nLayers;
protected int miscLen;
protected boolean hasLayers;
protected LayerInfo[] layers;
protected short[] lineLengths;
protected int lineIndex;
protected boolean rleEncoded;
protected class LayerInfo {
int x, y, w, h;
int nChan;
int[] chanID;
int alpha;
}
/**
* Gets the number of layers read from file.
*
* @return frame count
*/
public int getFrameCount() {
return frameCount;
}
protected void setInput(InputStream stream) {
// open input stream
init();
if (stream == null) {
status = STATUS_OPEN_ERROR;
} else {
if (stream instanceof BufferedInputStream)
input = (BufferedInputStream) stream;
else
input = new BufferedInputStream(stream);
}
}
protected void setInput(String name) {
// open input file
init();
try {
name = name.trim();
if (name.startsWith("file:")) {
name = name.substring(5);
while (name.startsWith("/"))
name = name.substring(1);
}
if (name.indexOf("://") > 0) {
URL url = new URL(name);
input = new BufferedInputStream(url.openStream());
} else {
input = new BufferedInputStream(new FileInputStream(name));
}
} catch (IOException e) {
status = STATUS_OPEN_ERROR;
}
}
/**
* Gets display duration for specified frame. Always returns 0.
*
*/
public int getDelay(int forFrame) {
return 0;
}
/**
* Gets the image contents of frame n. Note that this expands the image to the
* full frame size (if the layer was smaller) and any subsequent use of
* getLayer() will return the full image.
*
* @return BufferedImage representation of frame, or null if n is invalid.
*/
public BufferedImage getFrame(int n) {
BufferedImage im = null;
if ((n >= 0) && (n < nLayers)) {
im = frames[n];
LayerInfo info = layers[n];
if ((info.w != width) || (info.h != height)) {
BufferedImage temp = new BufferedImage(width, height, ImageType);
Graphics2D gc = temp.createGraphics();
gc.drawImage(im, info.x, info.y, null);
gc.dispose();
im = temp;
frames[n] = im;
}
}
return im;
}
/**
* Gets maximum image size. Individual layers may be smaller.
*
* @return maximum image dimensions
*/
public Dimension getFrameSize() {
return new Dimension(width, height);
}
/**
* Gets the first (or only) image read.
*
* @return BufferedImage containing first frame, or null if none.
*/
public BufferedImage getImage() {
return getFrame(0);
}
/**
* Gets the image contents of layer n. May be smaller than full frame size -
* use getFrameOffset() to obtain position of subimage within main image area.
*
* @return BufferedImage representation of layer, or null if n is invalid.
*/
public BufferedImage getLayer(int n) {
BufferedImage im = null;
if ((n >= 0) && (n < nLayers)) {
im = frames[n];
}
return im;
}
/**
* Gets the subimage offset of layer n if it is smaller than the full frame
* size.
*
* @return Point indicating offset from upper left corner of frame.
*/
public Point getLayerOffset(int n) {
Point p = null;
if ((n >= 0) && (n < nLayers)) {
int x = layers[n].x;
int y = layers[n].y;
p = new Point(x, y);
}
if (p == null) {
p = new Point(0, 0);
}
return p;
}
/**
* Reads PhotoShop layers from stream.
*
* @param InputStream
* in PhotoShop format.
* @return read status code (0 = no errors)
*/
public int read(InputStream stream) {
setInput(stream);
process();
return status;
}
/**
* Reads PhotoShop file from specified source (file or URL string)
*
* @param name
* String containing source
* @return read status code (0 = no errors)
*/
public int read(String name) {
setInput(name);
process();
return status;
}
/**
* Closes input stream and discards contents of all frames.
*
*/
public void reset() {
init();
}
protected void close() {
if (input != null) {
try {
input.close();
} catch (Exception e) {
}
input = null;
}
}
protected boolean err() {
return status != STATUS_OK;
}
protected byte[] fillBytes(int size, int value) {
// create byte array filled with given value
byte[] b = new byte[size];
if (value != 0) {
byte v = (byte) value;
for (int i = 0; i < size; i++) {
b[i] = v;
}
}
return b;
}
protected void init() {
close();
frameCount = 0;
frames = null;
layers = null;
hasLayers = true;
status = STATUS_OK;
}
protected void makeDummyLayer() {
// creat dummy layer for non-layered image
rleEncoded = readShort() == 1;
hasLayers = false;
nLayers = 1;
layers = new LayerInfo[1];
LayerInfo layer = new LayerInfo();
layers[0] = layer;
layer.h = height;
layer.w = width;
int nc = Math.min(nChan, 4);
if (rleEncoded) {
// get list of rle encoded line lengths for all channels
readLineLengths(height * nc);
}
layer.nChan = nc;
layer.chanID = new int[nc];
for (int i = 0; i < nc; i++) {
int id = i;
if (i == 3)
id = -1;
layer.chanID[i] = id;
}
}
protected void readLineLengths(int nLines) {
// read list of rle encoded line lengths
lineLengths = new short[nLines];
for (int i = 0; i < nLines; i++) {
lineLengths[i] = readShort();
}
lineIndex = 0;
}
protected BufferedImage makeImage(int w, int h, byte[] r, byte[] g, byte[] b, byte[] a) {
// create image from given plane data
BufferedImage im = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int[] data = ((DataBufferInt) im.getRaster().getDataBuffer()).getData();
int n = w * h;
int j = 0;
while (j < n) {
try {
int ac = a[j] & 0xff;
int rc = r[j] & 0xff;
int gc = g[j] & 0xff;
int bc = b[j] & 0xff;
data[j] = (((((ac << 8) | rc) << 8) | gc) << 8) | bc;
} catch (Exception e) {
}
j++;
}
return im;
}
protected void process() {
// decode PSD file
if (err())
return;
readHeader();
if (err())
return;
readLayerInfo();
if (err())
return;
if (nLayers == 0) {
makeDummyLayer();
if (err())
return;
}
readLayers();
}
protected int readByte() {
// read single byte from input
int curByte = 0;
try {
curByte = input.read();
} catch (IOException e) {
status = STATUS_FORMAT_ERROR;
}
return curByte;
}
protected int readBytes(byte[] bytes, int n) {
// read multiple bytes from input
if (bytes == null)
return 0;
int r = 0;
try {
r = input.read(bytes, 0, n);
} catch (IOException e) {
status = STATUS_FORMAT_ERROR;
}
if (r < n) {
status = STATUS_FORMAT_ERROR;
}
return r;
}
protected void readHeader() {
// read PSD header info
String sig = readString(4);
int ver = readShort();
skipBytes(6);
nChan = readShort();
height = readInt();
width = readInt();
int depth = readShort();
int mode = readShort();
int cmLen = readInt();
skipBytes(cmLen);
int imResLen = readInt();
skipBytes(imResLen);
// require 8-bit RGB data
if ((!sig.equals("8BPS")) || (ver != 1)) {
status = STATUS_FORMAT_ERROR;
} else if ((depth != 8) || (mode != 3)) {
status = STATUS_UNSUPPORTED;
}
}
protected int readInt() {
// read big-endian 32-bit integer
return (((((readByte() << 8) | readByte()) << 8) | readByte()) << 8) | readByte();
}
protected void readLayerInfo() {
// read layer header info
miscLen = readInt();
if (miscLen == 0) {
return; // no layers, only base image
}
int layerInfoLen = readInt();
nLayers = readShort();
if (nLayers > 0) {
layers = new LayerInfo[nLayers];
}
for (int i = 0; i < nLayers; i++) {
LayerInfo info = new LayerInfo();
layers[i] = info;
info.y = readInt();
info.x = readInt();
info.h = readInt() - info.y;
info.w = readInt() - info.x;
info.nChan = readShort();
info.chanID = new int[info.nChan];
for (int j = 0; j < info.nChan; j++) {
int id = readShort();
int size = readInt();
info.chanID[j] = id;
}
String s = readString(4);
if (!s.equals("8BIM")) {
status = STATUS_FORMAT_ERROR;
return;
}
skipBytes(4); // blend mode
info.alpha = readByte();
int clipping = readByte();
int flags = readByte();
readByte(); // filler
int extraSize = readInt();
skipBytes(extraSize);
}
}
protected void readLayers() {
// read and convert each layer to BufferedImage
frameCount = nLayers;
frames = new BufferedImage[nLayers];
for (int i = 0; i < nLayers; i++) {
LayerInfo info = layers[i];
byte[] r = null, g = null, b = null, a = null;
for (int j = 0; j < info.nChan; j++) {
int id = info.chanID[j];
switch (id) {
case 0:
r = readPlane(info.w, info.h);
break;
case 1:
g = readPlane(info.w, info.h);
break;
case 2:
b = readPlane(info.w, info.h);
break;
case -1:
a = readPlane(info.w, info.h);
break;
default:
readPlane(info.w, info.h);
}
if (err())
break;
}
if (err())
break;
int n = info.w * info.h;
if (r == null)
r = fillBytes(n, 0);
if (g == null)
g = fillBytes(n, 0);
if (b == null)
b = fillBytes(n, 0);
if (a == null)
a = fillBytes(n, 255);
BufferedImage im = makeImage(info.w, info.h, r, g, b, a);
frames[i] = im;
}
lineLengths = null;
if ((miscLen > 0) && !err()) {
int n = readInt(); // global layer mask info len
skipBytes(n);
}
}
protected byte[] readPlane(int w, int h) {
// read a single color plane
byte[] b = null;
int size = w * h;
if (hasLayers) {
// get RLE compression info for channel
rleEncoded = readShort() == 1;
if (rleEncoded) {
// list of encoded line lengths
readLineLengths(h);
}
}
if (rleEncoded) {
b = readPlaneCompressed(w, h);
} else {
b = new byte[size];
readBytes(b, size);
}
return b;
}
protected byte[] readPlaneCompressed(int w, int h) {
byte[] b = new byte[w * h];
byte[] s = new byte[w * 2];
int pos = 0;
for (int i = 0; i < h; i++) {
if (lineIndex >= lineLengths.length) {
status = STATUS_FORMAT_ERROR;
return null;
}
int len = lineLengths[lineIndex++];
readBytes(s, len);
decodeRLE(s, 0, len, b, pos);
pos += w;
}
return b;
}
protected void decodeRLE(byte[] src, int sindex, int slen, byte[] dst, int dindex) {
try {
int max = sindex + slen;
while (sindex < max) {
byte b = src[sindex++];
int n = (int) b;
if (n < 0) {
// dup next byte 1-n times
n = 1 - n;
b = src[sindex++];
for (int i = 0; i < n; i++) {
dst[dindex++] = b;
}
} else {
// copy next n+1 bytes
n = n + 1;
System.arraycopy(src, sindex, dst, dindex, n);
dindex += n;
sindex += n;
}
}
} catch (Exception e) {
status = STATUS_FORMAT_ERROR;
}
}
protected short readShort() {
// read big-endian 16-bit integer
return (short) ((readByte() << 8) | readByte());
}
protected String readString(int len) {
// read string of specified length
String s = "";
for (int i = 0; i < len; i++) {
s = s + (char) readByte();
}
return s;
}
protected void skipBytes(int n) {
// skip over n input bytes
for (int i = 0; i < n; i++) {
readByte();
}
}
}