001    /*
002     * PSDCodec
003     *
004     * Copyright (c) 2000, 2001, 2002, 2003 Marco Schmidt.
005     * All rights reserved.
006     */
007    
008    package net.sourceforge.jiu.codecs;
009    
010    import java.io.DataInput;
011    import java.io.IOException;
012    import net.sourceforge.jiu.codecs.ImageCodec;
013    import net.sourceforge.jiu.codecs.InvalidFileStructureException;
014    import net.sourceforge.jiu.codecs.UnsupportedTypeException;
015    import net.sourceforge.jiu.codecs.WrongFileFormatException;
016    import net.sourceforge.jiu.data.MemoryGray8Image;
017    import net.sourceforge.jiu.data.MemoryPaletted8Image;
018    import net.sourceforge.jiu.data.MemoryRGB24Image;
019    import net.sourceforge.jiu.data.Gray8Image;
020    import net.sourceforge.jiu.data.Palette;
021    import net.sourceforge.jiu.data.Paletted8Image;
022    import net.sourceforge.jiu.data.RGB24Image;
023    import net.sourceforge.jiu.ops.MissingParameterException;
024    import net.sourceforge.jiu.ops.OperationFailedException;
025    
026    /**
027     * A codec to read images from Photoshop PSD files.
028     * PSD was created by Adobe for their
029     * <a href="http://www.adobe.com/store/products/photoshop.html">Photoshop</a>
030     * image editing software.
031     * Note that only a small subset of valid PSD files is supported by this codec.
032     * Typical file extension is <code>.psd</code>.
033     * @author Marco Schmidt
034     */
035    public class PSDCodec extends ImageCodec
036    {
037            private final static int MAGIC_8BIM = 0x3842494d;
038            private final static int MAGIC_8BPS = 0x38425053;
039            private final static int COLOR_MODE_GRAYSCALE = 1;
040            private final static int COLOR_MODE_INDEXED = 2;
041            private final static int COLOR_MODE_RGB_TRUECOLOR = 3;
042            private final static short COMPRESSION_NONE = 0;
043            private final static short COMPRESSION_PACKBITS = 1;
044            private int magic;
045            private int version;
046            private int channels;
047            private int height;
048            private int width;
049            private int depth;
050            private int colorMode;
051            private short compression;
052            private DataInput in;
053            private Gray8Image gray8Image;
054            private Palette palette;
055            private Paletted8Image paletted8Image;
056            private RGB24Image rgb24Image;
057    
058            private void allocate()
059            {
060                    gray8Image = null;
061                    paletted8Image = null;
062                    rgb24Image = null;
063                    if (depth == 8 && colorMode == COLOR_MODE_RGB_TRUECOLOR)
064                    {
065                            rgb24Image = new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight());
066                            setImage(rgb24Image);
067                    }
068                    else
069                    if (channels == 1 && depth == 8 && colorMode == 2)
070                    {
071                            paletted8Image = new MemoryPaletted8Image(width, height, palette);
072                            setImage(paletted8Image);
073                    }
074                    else
075                    if (channels == 1 && depth == 8 && colorMode == COLOR_MODE_GRAYSCALE)
076                    {
077                            gray8Image = new MemoryGray8Image(width, height);
078                            setImage(gray8Image);
079                    }
080                    else
081                    {
082                            throw new IllegalArgumentException("Unknown image type in PSD file.");
083                    }
084            }
085    
086            private static String getColorTypeName(int colorMode)
087            {
088                    switch(colorMode)
089                    {
090                            case(0): return "Black & white";
091                            case(1): return "Grayscale";
092                            case(2): return "Indexed";
093                            case(3): return "RGB truecolor";
094                            case(4): return "CMYK truecolor";
095                            case(7): return "Multichannel";
096                            case(8): return "Duotone";
097                            case(9): return "Lab";
098                            default: return "Unknown (" + colorMode + ")";
099                    }
100            }
101    
102            public String getFormatName()
103            {
104                    return "Photoshop (PSD)";
105            }
106    
107            public String[] getMimeTypes()
108            {
109                    return new String[] {"image/psd", "image/x-psd"};
110            }
111    
112            public boolean isLoadingSupported()
113            {
114                    return true;
115            }
116    
117            public boolean isSavingSupported()
118            {
119                    return false;
120            }
121    
122            /**
123             * Attempts to load an Image from argument stream <code>in</code> (which
124             * could, as an example, be a <code>RandomAccessFile</code> instance, it 
125             * implements the <code>DataInput</code> interface).
126             * Checks a magic byte sequence and then reads all chunks as they appear
127             * in the IFF file.
128             * Will return the resulting image or null if no image body chunk was
129             * encountered before end-of-stream.
130             * Will throw an exception if the file is corrupt, information is missing
131             * or there were reading errors.
132             */
133            private void load() throws
134                    InvalidFileStructureException,
135                    IOException, 
136                    UnsupportedTypeException,
137                    WrongFileFormatException
138            {
139                    loadHeader();
140                    //System.out.println(width + " x " + height + ", color=" + colorMode + ", channels=" + channels + ", depth=" + depth);
141                    // check values
142                    if (width < 1 || height < 1)
143                    {
144                            throw new InvalidFileStructureException("Cannot load image. " +
145                                    "Invalid pixel resolution in PSD file header (" + width +
146                                    " x " + height + ").");
147                    }
148                    if (colorMode != COLOR_MODE_RGB_TRUECOLOR &&
149                        colorMode != COLOR_MODE_GRAYSCALE &&
150                        colorMode != COLOR_MODE_INDEXED)
151                    {
152                            throw new UnsupportedTypeException("Cannot load image. Only RGB" +
153                                    " truecolor and indexed color are supported for PSD files. " +
154                                    "Found: " +getColorTypeName(colorMode));
155                    }
156                    if (depth != 8)
157                    {
158                            throw new UnsupportedTypeException("Cannot load image. Only a depth of 8 bits " +
159                                    "per channel is supported (found " + depth + 
160                                    " bits).");
161                    }
162    
163                    // COLOR MODE DATA
164                    int colorModeSize = in.readInt();
165                    //System.out.println("colorModeSize=" + colorModeSize);
166                    byte[] colorMap = null;
167                    if (colorMode == COLOR_MODE_INDEXED)
168                    {
169                            if (colorModeSize != 768)
170                            {
171                                    throw new InvalidFileStructureException("Cannot load image." +
172                                            " Color map length was expected to be 768 (found " + 
173                                            colorModeSize + ").");
174                            }
175                            colorMap = new byte[colorModeSize];
176                            in.readFully(colorMap);
177                            palette = new Palette(256, 255);
178                            int offset = 0;
179                            for (int index = 0; index < 256; index++)
180                            {
181                                    palette.putSample(Palette.INDEX_RED, index, colorMap[index] & 0xff);
182                                    palette.putSample(Palette.INDEX_GREEN, index, colorMap[256 + index] & 0xff);
183                                    palette.putSample(Palette.INDEX_BLUE, index, colorMap[512 + index] & 0xff);
184                            }
185                    }
186                    else
187                    {
188                            in.skipBytes(colorModeSize);
189                    }
190                    // IMAGE RESOURCES
191                    int resourceLength = in.readInt();
192                    in.skipBytes(resourceLength);
193                    //System.out.println("resourceLength=" + resourceLength);
194                    // LAYER AND MASK INFORMATION
195                    int miscLength = in.readInt();
196                    in.skipBytes(miscLength);
197                    //System.out.println("miscLength=" + miscLength);
198                    // IMAGE DATA
199                    compression = in.readShort();
200                    if (compression != COMPRESSION_NONE && compression != COMPRESSION_PACKBITS)
201                    {
202                            throw new UnsupportedTypeException("Cannot load image. Unsupported PSD " +
203                                    "compression type (" + compression + ")");
204                    }
205                    //System.out.println("compression=" + compression);
206                    loadImageData();
207            }
208    
209            /**
210             * Reads the PSD header to private members of this class instance.
211             * @throws IOException if there were reading errors
212             */
213            private void loadHeader() throws
214                    IOException,
215                    WrongFileFormatException
216            {
217                    magic = in.readInt();
218                    if (magic != MAGIC_8BPS)
219                    {
220                            throw new WrongFileFormatException("Not a valid PSD file " +
221                                    "(wrong magic byte sequence).");
222                    }
223                    version = in.readShort();
224                    in.skipBytes(6);
225                    channels = in.readShort();
226                    height = in.readInt();
227                    width = in.readInt();
228                    depth = in.readShort();
229                    colorMode = in.readShort();
230            }
231    
232            private void loadPackbitsCompressedData(byte[] data, int offset, int num) throws
233                    InvalidFileStructureException,
234                    IOException
235            {
236                    int x = offset;
237                    int max = offset + num;
238                    while (x < max)
239                    {
240                            byte n = in.readByte();
241                            boolean compressed = false;
242                            int count = -1;
243                            try
244                            {
245                                    if (n >= 0)
246                                    {
247                                            // copy next n + 1 bytes literally
248                                            in.readFully(data, x, n + 1);
249                                            x += (n + 1);
250                                    }
251                                    else
252                                    {
253                                            // if n == -128, nothing happens (stupid design decision)
254                                            if (n != -128)
255                                            {
256                                                    compressed = true;
257                                                    // otherwise, compute counter
258                                                    count = -((int)n) + 1;
259                                                    // read another byte
260                                                    byte value = in.readByte();
261                                                    // write this byte counter times to output
262                                                    while (count-- > 0)
263                                                    {
264                                                            data[x++] = value;
265                                                    }
266                                            }
267                                    }
268                            }
269                            catch (ArrayIndexOutOfBoundsException ioobe)
270                            {
271                                    /* if the encoder did anything wrong, the above code
272                                       could potentially write beyond array boundaries
273                                       (e.g. if runs of data exceed line boundaries);
274                                       this would result in an IndexOutOfBoundsException
275                                       thrown by the virtual machine;
276                                       to give a more understandable error message to the 
277                                       user, this exception is caught here and a
278                                       corresponding IOException is thrown */
279                                    throw new InvalidFileStructureException("Error: RLE-compressed image " +
280                                            "file seems to be corrupt (x=" + x +
281                                            ", count=" + (compressed ? (-((int)n) + 1) : n) + 
282                                            ", compressed=" + (compressed ? "y" : "n") + ", array length=" + data.length + ").");
283                            }
284                    }
285            }
286    
287            private void loadImageData() throws 
288                    InvalidFileStructureException, 
289                    IOException
290            {
291                    setBoundsIfNecessary(width, height);
292                    allocate();
293                    if (compression == COMPRESSION_PACKBITS)
294                    {
295                            // skip counters
296                            in.skipBytes(2 * channels * height);
297                    }
298                    byte[] data = new byte[width];
299                    int totalScanLines = channels * height;
300                    int currentScanLine = 0;
301                    for (int c = 0; c < channels; c++)
302                    {
303                            int offset = 0;
304                            for (int y = 0, destY = - getBoundsY1(); y < height; y++, destY++)
305                            {
306                                    //System.out.println("channel=" + c + ", y=" + y + ", destY=" + destY);
307                                    if (compression == COMPRESSION_PACKBITS)
308                                    {
309                                            loadPackbitsCompressedData(data, 0, width);
310                                    }
311                                    else
312                                    {
313                                            if (compression == COMPRESSION_PACKBITS)
314                                            {
315                                                    in.readFully(data, 0, width);
316                                            }
317                                    }
318                                    setProgress(currentScanLine++, totalScanLines);
319                                    if (!isRowRequired(y))
320                                    {
321                                            continue;
322                                    }
323                                    if (rgb24Image != null)
324                                    {
325                                            int channelIndex = RGB24Image.INDEX_RED;
326                                            if (c == 1)
327                                            {
328                                                    channelIndex = RGB24Image.INDEX_GREEN;
329                                            }
330                                            if (c == 2)
331                                            {
332                                                    channelIndex = RGB24Image.INDEX_BLUE;
333                                            }
334                                            rgb24Image.putByteSamples(channelIndex, 0, destY, getBoundsWidth(), 1, data, getBoundsX1());
335                                    }
336                                    if (gray8Image != null)
337                                    {
338                                            gray8Image.putByteSamples(0, 0, destY, getBoundsWidth(), 1, data, getBoundsX1());
339                                    }
340                                    if (paletted8Image != null)
341                                    {
342                                            paletted8Image.putByteSamples(0, 0, destY, getBoundsWidth(), 1, data, getBoundsX1());
343                                    }
344                            }
345                    }
346            }
347    
348            public void process() throws
349                    OperationFailedException
350            {
351                    initModeFromIOObjects();
352                    try
353                    {
354                            if (getMode() == CodecMode.LOAD)
355                            {
356                                    in = getInputAsDataInput();
357                                    if (in == null)
358                                    {
359                                            throw new MissingParameterException("Input stream / file missing.");
360                                    }
361                                    load();
362                            }
363                            else
364                            {
365                                    throw new OperationFailedException("Only loading is supported in PSD codec.");
366                            }
367                    }
368                    catch (IOException ioe)
369                    {
370                            throw new OperationFailedException("I/O error: " + ioe.toString());
371                    }
372            }
373    }