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