PsdFile.java revision 901803a3b2aaeafb4074974ec5bf9b08cc39d1c8
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.hierarchyviewerlib.ui.util;
18
19import java.awt.Graphics2D;
20import java.awt.Point;
21import java.awt.image.BufferedImage;
22import java.io.BufferedOutputStream;
23import java.io.DataOutputStream;
24import java.io.IOException;
25import java.io.OutputStream;
26import java.io.UnsupportedEncodingException;
27import java.util.ArrayList;
28import java.util.List;
29
30/**
31 * Writes PSD file. Supports only 8 bits, RGB images with 4 channels.
32 */
33public class PsdFile {
34    private final Header mHeader;
35
36    private final ColorMode mColorMode;
37
38    private final ImageResources mImageResources;
39
40    private final LayersMasksInfo mLayersMasksInfo;
41
42    private final LayersInfo mLayersInfo;
43
44    private final BufferedImage mMergedImage;
45
46    private final Graphics2D mGraphics;
47
48    public PsdFile(int width, int height) {
49        mHeader = new Header(width, height);
50        mColorMode = new ColorMode();
51        mImageResources = new ImageResources();
52        mLayersMasksInfo = new LayersMasksInfo();
53        mLayersInfo = new LayersInfo();
54
55        mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
56        mGraphics = mMergedImage.createGraphics();
57    }
58
59    public void addLayer(String name, BufferedImage image, Point offset) {
60        addLayer(name, image, offset, true);
61    }
62
63    public void addLayer(String name, BufferedImage image, Point offset, boolean visible) {
64        mLayersInfo.addLayer(name, image, offset, visible);
65        if (visible)
66            mGraphics.drawImage(image, null, offset.x, offset.y);
67    }
68
69    public void write(OutputStream stream) {
70        mLayersMasksInfo.setLayersInfo(mLayersInfo);
71
72        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream));
73        try {
74            mHeader.write(out);
75            out.flush();
76
77            mColorMode.write(out);
78            mImageResources.write(out);
79            mLayersMasksInfo.write(out);
80            mLayersInfo.write(out);
81            out.flush();
82
83            mLayersInfo.writeImageData(out);
84            out.flush();
85
86            writeImage(mMergedImage, out, false);
87            out.flush();
88        } catch (IOException e) {
89            e.printStackTrace();
90        } finally {
91            try {
92                out.close();
93            } catch (IOException e) {
94                e.printStackTrace();
95            }
96        }
97    }
98
99    private static void writeImage(BufferedImage image, DataOutputStream out, boolean split)
100            throws IOException {
101
102        if (!split)
103            out.writeShort(0);
104
105        int width = image.getWidth();
106        int height = image.getHeight();
107
108        final int length = width * height;
109        int[] pixels = new int[length];
110
111        image.getData().getDataElements(0, 0, width, height, pixels);
112
113        byte[] a = new byte[length];
114        byte[] r = new byte[length];
115        byte[] g = new byte[length];
116        byte[] b = new byte[length];
117
118        for (int i = 0; i < length; i++) {
119            final int pixel = pixels[i];
120            a[i] = (byte) ((pixel >> 24) & 0xFF);
121            r[i] = (byte) ((pixel >> 16) & 0xFF);
122            g[i] = (byte) ((pixel >> 8) & 0xFF);
123            b[i] = (byte) (pixel & 0xFF);
124        }
125
126        if (split)
127            out.writeShort(0);
128        if (split)
129            out.write(a);
130        if (split)
131            out.writeShort(0);
132        out.write(r);
133        if (split)
134            out.writeShort(0);
135        out.write(g);
136        if (split)
137            out.writeShort(0);
138        out.write(b);
139        if (!split)
140            out.write(a);
141    }
142
143    @SuppressWarnings( {
144        "UnusedDeclaration"
145    })
146    static class Header {
147        static final short MODE_BITMAP = 0;
148
149        static final short MODE_GRAYSCALE = 1;
150
151        static final short MODE_INDEXED = 2;
152
153        static final short MODE_RGB = 3;
154
155        static final short MODE_CMYK = 4;
156
157        static final short MODE_MULTI_CHANNEL = 7;
158
159        static final short MODE_DUOTONE = 8;
160
161        static final short MODE_LAB = 9;
162
163        final byte[] mSignature = "8BPS".getBytes(); //$NON-NLS-1$
164
165        final short mVersion = 1;
166
167        final byte[] mReserved = new byte[6];
168
169        final short mChannelCount = 4;
170
171        final int mHeight;
172
173        final int mWidth;
174
175        final short mDepth = 8;
176
177        final short mMode = MODE_RGB;
178
179        Header(int width, int height) {
180            mWidth = width;
181            mHeight = height;
182        }
183
184        void write(DataOutputStream out) throws IOException {
185            out.write(mSignature);
186            out.writeShort(mVersion);
187            out.write(mReserved);
188            out.writeShort(mChannelCount);
189            out.writeInt(mHeight);
190            out.writeInt(mWidth);
191            out.writeShort(mDepth);
192            out.writeShort(mMode);
193        }
194    }
195
196    // Unused at the moment
197    @SuppressWarnings( {
198        "UnusedDeclaration"
199    })
200    static class ColorMode {
201        final int mLength = 0;
202
203        void write(DataOutputStream out) throws IOException {
204            out.writeInt(mLength);
205        }
206    }
207
208    // Unused at the moment
209    @SuppressWarnings( {
210        "UnusedDeclaration"
211    })
212    static class ImageResources {
213        static final short RESOURCE_RESOLUTION_INFO = 0x03ED;
214
215        int mLength = 0;
216
217        final byte[] mSignature = "8BIM".getBytes(); //$NON-NLS-1$
218
219        final short mResourceId = RESOURCE_RESOLUTION_INFO;
220
221        final short mPad = 0;
222
223        final int mDataLength = 16;
224
225        final short mHorizontalDisplayUnit = 0x48; // 72 dpi
226
227        final int mHorizontalResolution = 1;
228
229        final short mWidthDisplayUnit = 1;
230
231        final short mVerticalDisplayUnit = 0x48; // 72 dpi
232
233        final int mVerticalResolution = 1;
234
235        final short mHeightDisplayUnit = 1;
236
237        ImageResources() {
238            mLength = mSignature.length;
239            mLength += 2;
240            mLength += 2;
241            mLength += 4;
242            mLength += 8;
243            mLength += 8;
244        }
245
246        void write(DataOutputStream out) throws IOException {
247            out.writeInt(mLength);
248            out.write(mSignature);
249            out.writeShort(mResourceId);
250            out.writeShort(mPad);
251            out.writeInt(mDataLength);
252            out.writeShort(mHorizontalDisplayUnit);
253            out.writeInt(mHorizontalResolution);
254            out.writeShort(mWidthDisplayUnit);
255            out.writeShort(mVerticalDisplayUnit);
256            out.writeInt(mVerticalResolution);
257            out.writeShort(mHeightDisplayUnit);
258        }
259    }
260
261    @SuppressWarnings( {
262        "UnusedDeclaration"
263    })
264    static class LayersMasksInfo {
265        int mMiscLength;
266
267        int mLayerInfoLength;
268
269        void setLayersInfo(LayersInfo layersInfo) {
270            mLayerInfoLength = layersInfo.getLength();
271            // Round to the next multiple of 2
272            if ((mLayerInfoLength & 0x1) == 0x1)
273                mLayerInfoLength++;
274            mMiscLength = mLayerInfoLength + 8;
275        }
276
277        void write(DataOutputStream out) throws IOException {
278            out.writeInt(mMiscLength);
279            out.writeInt(mLayerInfoLength);
280        }
281    }
282
283    @SuppressWarnings( {
284        "UnusedDeclaration"
285    })
286    static class LayersInfo {
287        final List<Layer> mLayers = new ArrayList<Layer>();
288
289        void addLayer(String name, BufferedImage image, Point offset, boolean visible) {
290            mLayers.add(new Layer(name, image, offset, visible));
291        }
292
293        int getLength() {
294            int length = 2;
295            for (Layer layer : mLayers) {
296                length += layer.getLength();
297            }
298            return length;
299        }
300
301        void write(DataOutputStream out) throws IOException {
302            out.writeShort((short) -mLayers.size());
303            for (Layer layer : mLayers) {
304                layer.write(out);
305            }
306        }
307
308        void writeImageData(DataOutputStream out) throws IOException {
309            for (Layer layer : mLayers) {
310                layer.writeImageData(out);
311            }
312            // Global layer mask info length
313            out.writeInt(0);
314        }
315    }
316
317    @SuppressWarnings( {
318        "UnusedDeclaration"
319    })
320    static class Layer {
321        static final byte OPACITY_TRANSPARENT = 0x0;
322
323        static final byte OPACITY_OPAQUE = (byte) 0xFF;
324
325        static final byte CLIPPING_BASE = 0x0;
326
327        static final byte CLIPPING_NON_BASE = 0x1;
328
329        static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1;
330
331        static final byte FLAG_INVISIBLE = 0x2;
332
333        final int mTop;
334
335        final int mLeft;
336
337        final int mBottom;
338
339        final int mRight;
340
341        final short mChannelCount = 4;
342
343        final Channel[] mChannelInfo = new Channel[mChannelCount];
344
345        final byte[] mBlendSignature = "8BIM".getBytes(); //$NON-NLS-1$
346
347        final byte[] mBlendMode = "norm".getBytes(); //$NON-NLS-1$
348
349        final byte mOpacity = OPACITY_OPAQUE;
350
351        final byte mClipping = CLIPPING_BASE;
352
353        byte mFlags = 0x0;
354
355        final byte mFiller = 0x0;
356
357        int mExtraSize = 4 + 4;
358
359        final int mMaskDataLength = 0;
360
361        final int mBlendRangeDataLength = 0;
362
363        final byte[] mName;
364
365        final byte[] mLayerExtraSignature = "8BIM".getBytes(); //$NON-NLS-1$
366
367        final byte[] mLayerExtraKey = "luni".getBytes(); //$NON-NLS-1$
368
369        int mLayerExtraLength;
370
371        final String mOriginalName;
372
373        private BufferedImage mImage;
374
375        Layer(String name, BufferedImage image, Point offset, boolean visible) {
376            final int height = image.getHeight();
377            final int width = image.getWidth();
378            final int length = width * height;
379
380            mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length);
381            mChannelInfo[1] = new Channel(Channel.ID_RED, length);
382            mChannelInfo[2] = new Channel(Channel.ID_GREEN, length);
383            mChannelInfo[3] = new Channel(Channel.ID_BLUE, length);
384
385            mTop = offset.y;
386            mLeft = offset.x;
387            mBottom = offset.y + height;
388            mRight = offset.x + width;
389
390            mOriginalName = name;
391            byte[] data = name.getBytes();
392
393            try {
394                mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length; //$NON-NLS-1$
395            } catch (UnsupportedEncodingException e) {
396                e.printStackTrace();
397            }
398
399            final byte[] nameData = new byte[data.length + 1];
400            nameData[0] = (byte) (data.length & 0xFF);
401            System.arraycopy(data, 0, nameData, 1, data.length);
402
403            // This could be done in the same pass as above
404            if (nameData.length % 4 != 0) {
405                data = new byte[nameData.length + 4 - (nameData.length % 4)];
406                System.arraycopy(nameData, 0, data, 0, nameData.length);
407                mName = data;
408            } else {
409                mName = nameData;
410            }
411            mExtraSize += mName.length;
412            mExtraSize +=
413                    mLayerExtraLength + 4 + mLayerExtraKey.length + mLayerExtraSignature.length;
414
415            mImage = image;
416
417            if (!visible) {
418                mFlags |= FLAG_INVISIBLE;
419            }
420        }
421
422        int getLength() {
423            int length = 4 * 4 + 2;
424
425            for (Channel channel : mChannelInfo) {
426                length += channel.getLength();
427            }
428
429            length += mBlendSignature.length;
430            length += mBlendMode.length;
431            length += 4;
432            length += 4;
433            length += mExtraSize;
434
435            return length;
436        }
437
438        void write(DataOutputStream out) throws IOException {
439            out.writeInt(mTop);
440            out.writeInt(mLeft);
441            out.writeInt(mBottom);
442            out.writeInt(mRight);
443
444            out.writeShort(mChannelCount);
445            for (Channel channel : mChannelInfo) {
446                channel.write(out);
447            }
448
449            out.write(mBlendSignature);
450            out.write(mBlendMode);
451
452            out.write(mOpacity);
453            out.write(mClipping);
454            out.write(mFlags);
455            out.write(mFiller);
456
457            out.writeInt(mExtraSize);
458            out.writeInt(mMaskDataLength);
459
460            out.writeInt(mBlendRangeDataLength);
461
462            out.write(mName);
463
464            out.write(mLayerExtraSignature);
465            out.write(mLayerExtraKey);
466            out.writeInt(mLayerExtraLength);
467            out.writeInt(mOriginalName.length() + 1);
468            out.write(mOriginalName.getBytes("UTF-16")); //$NON-NLS-1$
469        }
470
471        void writeImageData(DataOutputStream out) throws IOException {
472            writeImage(mImage, out, true);
473        }
474    }
475
476    @SuppressWarnings( {
477        "UnusedDeclaration"
478    })
479    static class Channel {
480        static final short ID_RED = 0;
481
482        static final short ID_GREEN = 1;
483
484        static final short ID_BLUE = 2;
485
486        static final short ID_ALPHA = -1;
487
488        static final short ID_LAYER_MASK = -2;
489
490        final short mId;
491
492        final int mDataLength;
493
494        Channel(short id, int dataLength) {
495            mId = id;
496            mDataLength = dataLength + 2;
497        }
498
499        int getLength() {
500            return 2 + 4 + mDataLength;
501        }
502
503        void write(DataOutputStream out) throws IOException {
504            out.writeShort(mId);
505            out.writeInt(mDataLength);
506        }
507    }
508}
509