tons of changes
[org.ibex.xt.git] / src / org / ibex / xt / PSDReader.java
diff --git a/src/org/ibex/xt/PSDReader.java b/src/org/ibex/xt/PSDReader.java
new file mode 100644 (file)
index 0000000..c661d25
--- /dev/null
@@ -0,0 +1,553 @@
+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