1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.psd;
18
19 import java.awt.Dimension;
20 import java.awt.image.BufferedImage;
21 import java.io.ByteArrayInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.PrintWriter;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.List;
28
29 import org.apache.commons.imaging.AbstractImageParser;
30 import org.apache.commons.imaging.ImageFormat;
31 import org.apache.commons.imaging.ImageFormats;
32 import org.apache.commons.imaging.ImageInfo;
33 import org.apache.commons.imaging.ImagingException;
34 import org.apache.commons.imaging.bytesource.ByteSource;
35 import org.apache.commons.imaging.common.BinaryFunctions;
36 import org.apache.commons.imaging.common.ImageMetadata;
37 import org.apache.commons.imaging.common.XmpEmbeddable;
38 import org.apache.commons.imaging.common.XmpImagingParameters;
39 import org.apache.commons.imaging.formats.psd.dataparsers.AbstractDataParser;
40 import org.apache.commons.imaging.formats.psd.dataparsers.DataParserBitmap;
41 import org.apache.commons.imaging.formats.psd.dataparsers.DataParserCmyk;
42 import org.apache.commons.imaging.formats.psd.dataparsers.DataParserGrayscale;
43 import org.apache.commons.imaging.formats.psd.dataparsers.DataParserIndexed;
44 import org.apache.commons.imaging.formats.psd.dataparsers.DataParserLab;
45 import org.apache.commons.imaging.formats.psd.dataparsers.DataParserRgb;
46 import org.apache.commons.imaging.formats.psd.datareaders.CompressedDataReader;
47 import org.apache.commons.imaging.formats.psd.datareaders.DataReader;
48 import org.apache.commons.imaging.formats.psd.datareaders.UncompressedDataReader;
49 import org.apache.commons.io.IOUtils;
50 import org.apache.commons.lang3.ArrayUtils;
51
52 public class PsdImageParser extends AbstractImageParser<PsdImagingParameters> implements XmpEmbeddable {
53
54 private static final String DEFAULT_EXTENSION = ImageFormats.PSD.getDefaultExtension();
55 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PSD.getExtensions();
56 private static final int PSD_SECTION_HEADER = 0;
57 private static final int PSD_SECTION_COLOR_MODE = 1;
58 private static final int PSD_SECTION_IMAGE_RESOURCES = 2;
59 private static final int PSD_SECTION_LAYER_AND_MASK_DATA = 3;
60 private static final int PSD_SECTION_IMAGE_DATA = 4;
61 private static final int PSD_HEADER_LENGTH = 26;
62 private static final int COLOR_MODE_INDEXED = 2;
63 public static final int IMAGE_RESOURCE_ID_ICC_PROFILE = 0x040F;
64 public static final int IMAGE_RESOURCE_ID_XMP = 0x0424;
65 public static final String BLOCK_NAME_XMP = "XMP";
66
67
68
69
70 public PsdImageParser() {
71
72 }
73
74 @Override
75 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
76 pw.println("gif.dumpImageFile");
77
78 final ImageInfo fImageData = getImageInfo(byteSource);
79 if (fImageData == null) {
80 return false;
81 }
82
83 fImageData.toString(pw, "");
84 final PsdImageContents imageContents = readImageContents(byteSource);
85
86 imageContents.dump(pw);
87 imageContents.header.dump(pw);
88
89 final List<ImageResourceBlock> blocks = readImageResourceBlocks(byteSource,
90
91 null, -1);
92
93 pw.println("blocks.size(): " + blocks.size());
94
95
96 for (int i = 0; i < blocks.size(); i++) {
97 final ImageResourceBlock block = blocks.get(i);
98 pw.println("\t" + i + " (" + Integer.toHexString(block.id) + ", " + "'" + new String(block.nameData, StandardCharsets.ISO_8859_1) + "' ("
99 + block.nameData.length + "), "
100
101
102 + " data: " + block.data.length + " type: '" + ImageResourceType.getDescription(block.id) + "' " + ")");
103 }
104
105 pw.println("");
106
107 return true;
108 }
109
110 @Override
111 protected String[] getAcceptedExtensions() {
112 return ACCEPTED_EXTENSIONS.clone();
113 }
114
115 @Override
116 protected ImageFormat[] getAcceptedTypes() {
117 return new ImageFormat[] { ImageFormats.PSD,
118 };
119 }
120
121 @Override
122 public BufferedImage getBufferedImage(final ByteSource byteSource, final PsdImagingParameters params) throws ImagingException, IOException {
123 final PsdImageContents imageContents = readImageContents(byteSource);
124
125
126 final PsdHeaderInfo header = imageContents.header;
127 if (header == null) {
128 throw new ImagingException("PSD: Couldn't read Header");
129 }
130
131
132
133
134
135
136
137
138
139 readImageResourceBlocks(byteSource,
140
141 null, -1);
142
143 final int width = header.columns;
144 final int height = header.rows;
145
146
147
148
149
150
151 final boolean hasAlpha = false;
152 final BufferedImage result = getBufferedImageFactory(params).getColorBufferedImage(width, height, hasAlpha);
153
154 final AbstractDataParser dataParser;
155 switch (imageContents.header.mode) {
156 case 0:
157 dataParser = new DataParserBitmap();
158 break;
159 case 1:
160 case 8:
161 dataParser = new DataParserGrayscale();
162 break;
163 case 3:
164 dataParser = new DataParserRgb();
165 break;
166 case 4:
167 dataParser = new DataParserCmyk();
168 break;
169 case 9:
170 dataParser = new DataParserLab();
171 break;
172 case COLOR_MODE_INDEXED: {
173
174 final byte[] ColorModeData = getData(byteSource, PSD_SECTION_COLOR_MODE);
175
176
177
178
179
180
181
182 dataParser = new DataParserIndexed(ColorModeData);
183 break;
184 }
185 case 7:
186
187
188
189
190
191
192 default:
193 throw new ImagingException("Unknown Mode: " + imageContents.header.mode);
194 }
195 final DataReader fDataReader;
196 switch (imageContents.compression) {
197 case 0:
198 fDataReader = new UncompressedDataReader(dataParser);
199 break;
200 case 1:
201 fDataReader = new CompressedDataReader(dataParser);
202 break;
203 default:
204 throw new ImagingException("Unknown Compression: " + imageContents.compression);
205 }
206
207 try (InputStream is = getInputStream(byteSource, PSD_SECTION_IMAGE_DATA)) {
208 fDataReader.readData(is, result, imageContents, this);
209
210
211
212
213 }
214
215 return result;
216
217 }
218
219 private int getChannelsPerMode(final int mode) {
220 switch (mode) {
221 case 0:
222 return 1;
223 case 1:
224 return 1;
225 case 2:
226 return -1;
227 case 3:
228 return 3;
229 case 4:
230 return 4;
231 case 7:
232 return -1;
233 case 8:
234 return -1;
235 case 9:
236 return 4;
237 default:
238 return -1;
239
240 }
241 }
242
243 private byte[] getData(final ByteSource byteSource, final int section) throws ImagingException, IOException {
244 try (InputStream is = byteSource.getInputStream()) {
245
246 if (section == PSD_SECTION_HEADER) {
247 return BinaryFunctions.readBytes("Header", is, PSD_HEADER_LENGTH, "Not a Valid PSD File");
248 }
249 BinaryFunctions.skipBytes(is, PSD_HEADER_LENGTH);
250
251 final int colorModeDataLength = BinaryFunctions.read4Bytes("ColorModeDataLength", is, "Not a Valid PSD File", getByteOrder());
252
253 if (section == PSD_SECTION_COLOR_MODE) {
254 return BinaryFunctions.readBytes("ColorModeData", is, colorModeDataLength, "Not a Valid PSD File");
255 }
256
257 BinaryFunctions.skipBytes(is, colorModeDataLength);
258
259
260
261 final int imageResourcesLength = BinaryFunctions.read4Bytes("ImageResourcesLength", is, "Not a Valid PSD File", getByteOrder());
262
263 if (section == PSD_SECTION_IMAGE_RESOURCES) {
264 return BinaryFunctions.readBytes("ImageResources", is, imageResourcesLength, "Not a Valid PSD File");
265 }
266
267 BinaryFunctions.skipBytes(is, imageResourcesLength);
268
269
270
271 final int layerAndMaskDataLength = BinaryFunctions.read4Bytes("LayerAndMaskDataLength", is, "Not a Valid PSD File", getByteOrder());
272
273 if (section == PSD_SECTION_LAYER_AND_MASK_DATA) {
274 return BinaryFunctions.readBytes("LayerAndMaskData", is, layerAndMaskDataLength, "Not a Valid PSD File");
275 }
276
277 BinaryFunctions.skipBytes(is, layerAndMaskDataLength);
278
279
280
281 BinaryFunctions.read2Bytes("Compression", is, "Not a Valid PSD File", getByteOrder());
282
283
284
285
286
287
288
289
290 }
291 throw new ImagingException("getInputStream: Unknown Section: " + section);
292 }
293
294 @Override
295 public String getDefaultExtension() {
296 return DEFAULT_EXTENSION;
297 }
298
299 @Override
300 public PsdImagingParameters getDefaultParameters() {
301 return new PsdImagingParameters();
302 }
303
304 @Override
305 public byte[] getIccProfileBytes(final ByteSource byteSource, final PsdImagingParameters params) throws ImagingException, IOException {
306 final List<ImageResourceBlock> blocks = readImageResourceBlocks(byteSource, new int[] { IMAGE_RESOURCE_ID_ICC_PROFILE, }, 1);
307
308 if (blocks.isEmpty()) {
309 return null;
310 }
311
312 final ImageResourceBlock irb = blocks.get(0);
313 final byte[] bytes = irb.data;
314 if (bytes == null || bytes.length < 1) {
315 return null;
316 }
317 return bytes.clone();
318 }
319
320 @Override
321 public ImageInfo getImageInfo(final ByteSource byteSource, final PsdImagingParameters params) throws ImagingException, IOException {
322 final PsdImageContents imageContents = readImageContents(byteSource);
323
324
325 final PsdHeaderInfo header = imageContents.header;
326 if (header == null) {
327 throw new ImagingException("PSD: Couldn't read Header");
328 }
329
330 final int width = header.columns;
331 final int height = header.rows;
332
333 final List<String> comments = new ArrayList<>();
334
335
336 int bitsPerPixel = header.depth * getChannelsPerMode(header.mode);
337
338
339
340
341 if (bitsPerPixel < 0) {
342 bitsPerPixel = 0;
343 }
344 final ImageFormat format = ImageFormats.PSD;
345 final String formatName = "Photoshop";
346 final String mimeType = "image/x-photoshop";
347
348 final int numberOfImages = -1;
349
350 final boolean progressive = false;
351
352 final int physicalWidthDpi = 72;
353 final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi);
354 final int physicalHeightDpi = 72;
355 final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi);
356
357 final String formatDetails = "Psd";
358
359 final boolean transparent = false;
360 final boolean usesPalette = header.mode == COLOR_MODE_INDEXED;
361 final ImageInfo.ColorType colorType = ImageInfo.ColorType.UNKNOWN;
362
363 final ImageInfo.CompressionAlgorithm compressionAlgorithm;
364 switch (imageContents.compression) {
365 case 0:
366 compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE;
367 break;
368 case 1:
369 compressionAlgorithm = ImageInfo.CompressionAlgorithm.PSD;
370 break;
371 default:
372 compressionAlgorithm = ImageInfo.CompressionAlgorithm.UNKNOWN;
373 }
374
375 return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch,
376 physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm);
377 }
378
379 @Override
380 public Dimension getImageSize(final ByteSource byteSource, final PsdImagingParameters params) throws ImagingException, IOException {
381 final PsdHeaderInfo bhi = readHeader(byteSource);
382
383 return new Dimension(bhi.columns, bhi.rows);
384
385 }
386
387 private InputStream getInputStream(final ByteSource byteSource, final int section) throws ImagingException, IOException {
388 InputStream is = null;
389 boolean notFound = false;
390 try {
391 is = byteSource.getInputStream();
392
393 if (section == PSD_SECTION_HEADER) {
394 return is;
395 }
396
397 BinaryFunctions.skipBytes(is, PSD_HEADER_LENGTH);
398
399
400 final int colorModeDataLength = BinaryFunctions.read4Bytes("ColorModeDataLength", is, "Not a Valid PSD File", getByteOrder());
401
402 if (section == PSD_SECTION_COLOR_MODE) {
403 return is;
404 }
405
406 BinaryFunctions.skipBytes(is, colorModeDataLength);
407
408
409
410 final int imageResourcesLength = BinaryFunctions.read4Bytes("ImageResourcesLength", is, "Not a Valid PSD File", getByteOrder());
411
412 if (section == PSD_SECTION_IMAGE_RESOURCES) {
413 return is;
414 }
415
416 BinaryFunctions.skipBytes(is, imageResourcesLength);
417
418
419
420 final int layerAndMaskDataLength = BinaryFunctions.read4Bytes("LayerAndMaskDataLength", is, "Not a Valid PSD File", getByteOrder());
421
422 if (section == PSD_SECTION_LAYER_AND_MASK_DATA) {
423 return is;
424 }
425
426 BinaryFunctions.skipBytes(is, layerAndMaskDataLength);
427
428
429
430 BinaryFunctions.read2Bytes("Compression", is, "Not a Valid PSD File", getByteOrder());
431
432
433
434
435 if (section == PSD_SECTION_IMAGE_DATA) {
436 return is;
437 }
438 notFound = true;
439 } finally {
440 if (notFound) {
441 IOUtils.close(is);
442 }
443 }
444 throw new ImagingException("getInputStream: Unknown Section: " + section);
445 }
446
447 @Override
448 public ImageMetadata getMetadata(final ByteSource byteSource, final PsdImagingParameters params) throws ImagingException, IOException {
449 return null;
450 }
451
452 @Override
453 public String getName() {
454 return "PSD-Custom";
455 }
456
457
458
459
460
461
462
463
464 @Override
465 public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters params) throws ImagingException, IOException {
466
467 final PsdImageContents imageContents = readImageContents(byteSource);
468
469 final PsdHeaderInfo header = imageContents.header;
470 if (header == null) {
471 throw new ImagingException("PSD: Couldn't read Header");
472 }
473
474 final List<ImageResourceBlock> blocks = readImageResourceBlocks(byteSource, new int[] { IMAGE_RESOURCE_ID_XMP, }, -1);
475
476 if (blocks.isEmpty()) {
477 return null;
478 }
479
480 final List<ImageResourceBlock> xmpBlocks = new ArrayList<>(blocks);
481 if (xmpBlocks.isEmpty()) {
482 return null;
483 }
484 if (xmpBlocks.size() > 1) {
485 throw new ImagingException("PSD contains more than one XMP block.");
486 }
487
488 final ImageResourceBlock block = xmpBlocks.get(0);
489
490
491 return new String(block.data, 0, block.data.length, StandardCharsets.UTF_8);
492 }
493
494 private boolean keepImageResourceBlock(final int id, final int[] imageResourceIDs) {
495 return ArrayUtils.contains(imageResourceIDs, id);
496 }
497
498 private PsdHeaderInfo readHeader(final ByteSource byteSource) throws ImagingException, IOException {
499 try (InputStream is = byteSource.getInputStream()) {
500 return readHeader(is);
501 }
502 }
503
504 private PsdHeaderInfo readHeader(final InputStream is) throws ImagingException, IOException {
505 BinaryFunctions.readAndVerifyBytes(is, new byte[] { 56, 66, 80, 83 }, "Not a Valid PSD File");
506
507 final int version = BinaryFunctions.read2Bytes("Version", is, "Not a Valid PSD File", getByteOrder());
508 final byte[] reserved = BinaryFunctions.readBytes("Reserved", is, 6, "Not a Valid PSD File");
509 final int channels = BinaryFunctions.read2Bytes("Channels", is, "Not a Valid PSD File", getByteOrder());
510 final int rows = BinaryFunctions.read4Bytes("Rows", is, "Not a Valid PSD File", getByteOrder());
511 final int columns = BinaryFunctions.read4Bytes("Columns", is, "Not a Valid PSD File", getByteOrder());
512 final int depth = BinaryFunctions.read2Bytes("Depth", is, "Not a Valid PSD File", getByteOrder());
513 final int mode = BinaryFunctions.read2Bytes("Mode", is, "Not a Valid PSD File", getByteOrder());
514
515 return new PsdHeaderInfo(version, reserved, channels, rows, columns, depth, mode);
516 }
517
518 private PsdImageContents readImageContents(final ByteSource byteSource) throws ImagingException, IOException {
519 try (InputStream is = byteSource.getInputStream()) {
520 return readImageContents(is);
521 }
522 }
523
524 private PsdImageContents readImageContents(final InputStream is) throws ImagingException, IOException {
525 final PsdHeaderInfo header = readHeader(is);
526
527 final int colorModeDataLength = BinaryFunctions.read4Bytes("ColorModeDataLength", is, "Not a Valid PSD File", getByteOrder());
528 BinaryFunctions.skipBytes(is, colorModeDataLength);
529
530
531
532
533 final int imageResourcesLength = BinaryFunctions.read4Bytes("ImageResourcesLength", is, "Not a Valid PSD File", getByteOrder());
534 BinaryFunctions.skipBytes(is, imageResourcesLength);
535
536
537
538
539 final int layerAndMaskDataLength = BinaryFunctions.read4Bytes("LayerAndMaskDataLength", is, "Not a Valid PSD File", getByteOrder());
540 BinaryFunctions.skipBytes(is, layerAndMaskDataLength);
541
542
543
544
545 final int compression = BinaryFunctions.read2Bytes("Compression", is, "Not a Valid PSD File", getByteOrder());
546
547
548
549
550
551
552
553 return new PsdImageContents(header, colorModeDataLength,
554
555 imageResourcesLength,
556
557 layerAndMaskDataLength,
558
559 compression);
560 }
561
562 private List<ImageResourceBlock> readImageResourceBlocks(final byte[] bytes, final int[] imageResourceIDs, final int maxBlocksToRead)
563 throws ImagingException, IOException {
564 return readImageResourceBlocks(new ByteArrayInputStream(bytes), imageResourceIDs, maxBlocksToRead, bytes.length);
565 }
566
567 private List<ImageResourceBlock> readImageResourceBlocks(final ByteSource byteSource, final int[] imageResourceIDs, final int maxBlocksToRead)
568 throws ImagingException, IOException {
569 try (InputStream imageStream = byteSource.getInputStream();
570 InputStream resourceStream = getInputStream(byteSource, PSD_SECTION_IMAGE_RESOURCES)) {
571 final PsdImageContents imageContents = readImageContents(imageStream);
572 final byte[] ImageResources = BinaryFunctions.readBytes("ImageResources", resourceStream, imageContents.imageResourcesLength,
573 "Not a Valid PSD File");
574 return readImageResourceBlocks(ImageResources, imageResourceIDs, maxBlocksToRead);
575 }
576 }
577
578 private List<ImageResourceBlock> readImageResourceBlocks(final InputStream is, final int[] imageResourceIDs, final int maxBlocksToRead, int available)
579 throws ImagingException, IOException {
580 final List<ImageResourceBlock> result = new ArrayList<>();
581
582 while (available > 0) {
583 BinaryFunctions.readAndVerifyBytes(is, new byte[] { 56, 66, 73, 77 }, "Not a Valid PSD File");
584 available -= 4;
585
586 final int id = BinaryFunctions.read2Bytes("ID", is, "Not a Valid PSD File", getByteOrder());
587 available -= 2;
588
589 final int nameLength = BinaryFunctions.readByte("NameLength", is, "Not a Valid PSD File");
590
591 available -= 1;
592 final byte[] nameBytes = BinaryFunctions.readBytes("NameData", is, nameLength, "Not a Valid PSD File");
593 available -= nameLength;
594 if ((nameLength + 1) % 2 != 0) {
595
596 BinaryFunctions.readByte("NameDiscard", is, "Not a Valid PSD File");
597 available -= 1;
598 }
599
600 final int dataSize = BinaryFunctions.read4Bytes("Size", is, "Not a Valid PSD File", getByteOrder());
601 available -= 4;
602
603
604
605
606 final byte[] data = BinaryFunctions.readBytes("Data", is, dataSize, "Not a Valid PSD File");
607 available -= dataSize;
608
609 if (dataSize % 2 != 0) {
610
611 BinaryFunctions.readByte("DataDiscard", is, "Not a Valid PSD File");
612 available -= 1;
613 }
614
615 if (keepImageResourceBlock(id, imageResourceIDs)) {
616 result.add(new ImageResourceBlock(id, nameBytes, data));
617
618 if (maxBlocksToRead >= 0 && result.size() >= maxBlocksToRead) {
619 return result;
620 }
621 }
622
623
624 }
625
626 return result;
627 }
628
629 }