blob: 3768e41b562c116430603f305c0693362716e385 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.hierarchyviewer.ui.util;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/**
* Writes PSD file.
*
* Supports only 8 bits, RGB images with 4 channels.
*/
public class PsdFile {
private final Header mHeader;
private final ColorMode mColorMode;
private final ImageResources mImageResources;
private final LayersMasksInfo mLayersMasksInfo;
private final LayersInfo mLayersInfo;
private final BufferedImage mMergedImage;
private final Graphics2D mGraphics;
public PsdFile(int width, int height) {
mHeader = new Header(width, height);
mColorMode = new ColorMode();
mImageResources = new ImageResources();
mLayersMasksInfo = new LayersMasksInfo();
mLayersInfo = new LayersInfo();
mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
mGraphics = mMergedImage.createGraphics();
}
public void addLayer(String name, BufferedImage image, Point offset) {
addLayer(name, image, offset, true);
}
public void addLayer(String name, BufferedImage image, Point offset, boolean visible) {
mLayersInfo.addLayer(name, image, offset, visible);
if (visible) mGraphics.drawImage(image, null, offset.x, offset.y);
}
public void write(OutputStream stream) {
mLayersMasksInfo.setLayersInfo(mLayersInfo);
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream));
try {
mHeader.write(out);
out.flush();
mColorMode.write(out);
mImageResources.write(out);
mLayersMasksInfo.write(out);
mLayersInfo.write(out);
out.flush();
mLayersInfo.writeImageData(out);
out.flush();
writeImage(mMergedImage, out, false);
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void writeImage(BufferedImage image, DataOutputStream out, boolean split)
throws IOException {
if (!split) out.writeShort(0);
int width = image.getWidth();
int height = image.getHeight();
final int length = width * height;
int[] pixels = new int[length];
image.getData().getDataElements(0, 0, width, height, pixels);
byte[] a = new byte[length];
byte[] r = new byte[length];
byte[] g = new byte[length];
byte[] b = new byte[length];
for (int i = 0; i < length; i++) {
final int pixel = pixels[i];
a[i] = (byte) ((pixel >> 24) & 0xFF);
r[i] = (byte) ((pixel >> 16) & 0xFF);
g[i] = (byte) ((pixel >> 8) & 0xFF);
b[i] = (byte) (pixel & 0xFF);
}
if (split) out.writeShort(0);
if (split) out.write(a);
if (split) out.writeShort(0);
out.write(r);
if (split) out.writeShort(0);
out.write(g);
if (split) out.writeShort(0);
out.write(b);
if (!split) out.write(a);
}
@SuppressWarnings({"UnusedDeclaration"})
static class Header {
static final short MODE_BITMAP = 0;
static final short MODE_GRAYSCALE = 1;
static final short MODE_INDEXED = 2;
static final short MODE_RGB = 3;
static final short MODE_CMYK = 4;
static final short MODE_MULTI_CHANNEL = 7;
static final short MODE_DUOTONE = 8;
static final short MODE_LAB = 9;
final byte[] mSignature = "8BPS".getBytes();
final short mVersion = 1;
final byte[] mReserved = new byte[6];
final short mChannelCount = 4;
final int mHeight;
final int mWidth;
final short mDepth = 8;
final short mMode = MODE_RGB;
Header(int width, int height) {
mWidth = width;
mHeight = height;
}
void write(DataOutputStream out) throws IOException {
out.write(mSignature);
out.writeShort(mVersion);
out.write(mReserved);
out.writeShort(mChannelCount);
out.writeInt(mHeight);
out.writeInt(mWidth);
out.writeShort(mDepth);
out.writeShort(mMode);
}
}
// Unused at the moment
@SuppressWarnings({"UnusedDeclaration"})
static class ColorMode {
final int mLength = 0;
void write(DataOutputStream out) throws IOException {
out.writeInt(mLength);
}
}
// Unused at the moment
@SuppressWarnings({"UnusedDeclaration"})
static class ImageResources {
static final short RESOURCE_RESOLUTION_INFO = 0x03ED;
int mLength = 0;
final byte[] mSignature = "8BIM".getBytes();
final short mResourceId = RESOURCE_RESOLUTION_INFO;
final short mPad = 0;
final int mDataLength = 16;
final short mHorizontalDisplayUnit = 0x48; // 72 dpi
final int mHorizontalResolution = 1;
final short mWidthDisplayUnit = 1;
final short mVerticalDisplayUnit = 0x48; // 72 dpi
final int mVerticalResolution = 1;
final short mHeightDisplayUnit = 1;
ImageResources() {
mLength = mSignature.length;
mLength += 2;
mLength += 2;
mLength += 4;
mLength += 8;
mLength += 8;
}
void write(DataOutputStream out) throws IOException {
out.writeInt(mLength);
out.write(mSignature);
out.writeShort(mResourceId);
out.writeShort(mPad);
out.writeInt(mDataLength);
out.writeShort(mHorizontalDisplayUnit);
out.writeInt(mHorizontalResolution);
out.writeShort(mWidthDisplayUnit);
out.writeShort(mVerticalDisplayUnit);
out.writeInt(mVerticalResolution);
out.writeShort(mHeightDisplayUnit);
}
}
@SuppressWarnings({"UnusedDeclaration"})
static class LayersMasksInfo {
int mMiscLength;
int mLayerInfoLength;
void setLayersInfo(LayersInfo layersInfo) {
mLayerInfoLength = layersInfo.getLength();
// Round to the next multiple of 2
if ((mLayerInfoLength & 0x1) == 0x1) mLayerInfoLength++;
mMiscLength = mLayerInfoLength + 8;
}
void write(DataOutputStream out) throws IOException {
out.writeInt(mMiscLength);
out.writeInt(mLayerInfoLength);
}
}
@SuppressWarnings({"UnusedDeclaration"})
static class LayersInfo {
final List<Layer> mLayers = new ArrayList<Layer>();
void addLayer(String name, BufferedImage image, Point offset, boolean visible) {
mLayers.add(new Layer(name, image, offset, visible));
}
int getLength() {
int length = 2;
for (Layer layer : mLayers) {
length += layer.getLength();
}
return length;
}
void write(DataOutputStream out) throws IOException {
out.writeShort((short) -mLayers.size());
for (Layer layer : mLayers) {
layer.write(out);
}
}
void writeImageData(DataOutputStream out) throws IOException {
for (Layer layer : mLayers) {
layer.writeImageData(out);
}
// Global layer mask info length
out.writeInt(0);
}
}
@SuppressWarnings({"UnusedDeclaration"})
static class Layer {
static final byte OPACITY_TRANSPARENT = 0x0;
static final byte OPACITY_OPAQUE = (byte) 0xFF;
static final byte CLIPPING_BASE = 0x0;
static final byte CLIPPING_NON_BASE = 0x1;
static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1;
static final byte FLAG_INVISIBLE = 0x2;
final int mTop;
final int mLeft;
final int mBottom;
final int mRight;
final short mChannelCount = 4;
final Channel[] mChannelInfo = new Channel[mChannelCount];
final byte[] mBlendSignature = "8BIM".getBytes();
final byte[] mBlendMode = "norm".getBytes();
final byte mOpacity = OPACITY_OPAQUE;
final byte mClipping = CLIPPING_BASE;
byte mFlags = 0x0;
final byte mFiller = 0x0;
int mExtraSize = 4 + 4;
final int mMaskDataLength = 0;
final int mBlendRangeDataLength = 0;
final byte[] mName;
final byte[] mLayerExtraSignature = "8BIM".getBytes();
final byte[] mLayerExtraKey = "luni".getBytes();
int mLayerExtraLength;
final String mOriginalName;
private BufferedImage mImage;
Layer(String name, BufferedImage image, Point offset, boolean visible) {
final int height = image.getHeight();
final int width = image.getWidth();
final int length = width * height;
mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length);
mChannelInfo[1] = new Channel(Channel.ID_RED, length);
mChannelInfo[2] = new Channel(Channel.ID_GREEN, length);
mChannelInfo[3] = new Channel(Channel.ID_BLUE, length);
mTop = offset.y;
mLeft = offset.x;
mBottom = offset.y + height;
mRight = offset.x + width;
mOriginalName = name;
byte[] data = name.getBytes();
try {
mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
final byte[] nameData = new byte[data.length + 1];
nameData[0] = (byte) (data.length & 0xFF);
System.arraycopy(data, 0, nameData, 1, data.length);
// This could be done in the same pass as above
if (nameData.length % 4 != 0) {
data = new byte[nameData.length + 4 - (nameData.length % 4)];
System.arraycopy(nameData, 0, data, 0, nameData.length);
mName = data;
} else {
mName = nameData;
}
mExtraSize += mName.length;
mExtraSize += mLayerExtraLength + 4 + mLayerExtraKey.length +
mLayerExtraSignature.length;
mImage = image;
if (!visible) {
mFlags |= FLAG_INVISIBLE;
}
}
int getLength() {
int length = 4 * 4 + 2;
for (Channel channel : mChannelInfo) {
length += channel.getLength();
}
length += mBlendSignature.length;
length += mBlendMode.length;
length += 4;
length += 4;
length += mExtraSize;
return length;
}
void write(DataOutputStream out) throws IOException {
out.writeInt(mTop);
out.writeInt(mLeft);
out.writeInt(mBottom);
out.writeInt(mRight);
out.writeShort(mChannelCount);
for (Channel channel : mChannelInfo) {
channel.write(out);
}
out.write(mBlendSignature);
out.write(mBlendMode);
out.write(mOpacity);
out.write(mClipping);
out.write(mFlags);
out.write(mFiller);
out.writeInt(mExtraSize);
out.writeInt(mMaskDataLength);
out.writeInt(mBlendRangeDataLength);
out.write(mName);
out.write(mLayerExtraSignature);
out.write(mLayerExtraKey);
out.writeInt(mLayerExtraLength);
out.writeInt(mOriginalName.length() + 1);
out.write(mOriginalName.getBytes("UTF-16"));
}
void writeImageData(DataOutputStream out) throws IOException {
writeImage(mImage, out, true);
}
}
@SuppressWarnings({"UnusedDeclaration"})
static class Channel {
static final short ID_RED = 0;
static final short ID_GREEN = 1;
static final short ID_BLUE = 2;
static final short ID_ALPHA = -1;
static final short ID_LAYER_MASK = -2;
final short mId;
final int mDataLength;
Channel(short id, int dataLength) {
mId = id;
mDataLength = dataLength + 2;
}
int getLength() {
return 2 + 4 + mDataLength;
}
void write(DataOutputStream out) throws IOException {
out.writeShort(mId);
out.writeInt(mDataLength);
}
}
}