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