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