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