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