PSDImageReaderTest.java
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.w3c.dom.NodeList;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.containsStringIgnoringCase;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
/**
* PSDImageReaderTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDImageReaderTest.java,v 1.0 Apr 1, 2008 10:39:17 PM haraldk Exp$
*/
public class PSDImageReaderTest extends ImageReaderAbstractTest<PSDImageReader> {
@Override
protected ImageReaderSpi createProvider() {
return new PSDImageReaderSpi();
}
@Test
public void readTinyInputDeclaringHugeDimensionsIsRejectedBeforeAllocation() throws IOException {
// GHSA-7c3w-m9qh-j2vj: a 40-byte PSD header declaring huge dimensions allocated ~2.75 GB before this guard.
byte[] poc = Base64.getDecoder().decode("OEJQUwABAAAAAAAAAC8AADpBAAA92QAgAAMAAAAAAAAAAAAAAAAAAA==");
ImageReader reader = createReader();
try (ImageInputStream input = new ByteArrayImageInputStream(poc)) {
reader.setInput(input);
IIOException exception = assertThrows(IIOException.class, () -> reader.read(0));
// "exceeding" is unique to the size-guard message, confirming rejection in the guard (before
// allocation), not a later EOF/parse error after a large buffer was committed.
assertTrue(exception.getMessage().contains("exceeding"), exception.getMessage());
}
finally {
reader.dispose();
}
}
@Override
protected List<TestData> getTestData() {
return Arrays.asList(
// 5 channel, RGB
new TestData(getClassLoaderResource("/psd/photoshopping.psd"), new Dimension(300, 225)),
// 1 channel, gray, 8 bit samples
new TestData(getClassLoaderResource("/psd/buttons.psd"), new Dimension(20, 20)),
// 5 channel, CMYK
new TestData(getClassLoaderResource("/psd/escenic-liquid-logo.psd"), new Dimension(595, 420)),
// 3 channel RGB, "no composite layer"
new TestData(getClassLoaderResource("/psd/jugware-icon.psd"), new Dimension(128, 128)),
// 3 channel RGB, old data, no layer info/mask
new TestData(getClassLoaderResource("/psd/MARBLES.PSD"), new Dimension(1419, 1001)),
// 1 channel, indexed color
new TestData(getClassLoaderResource("/psd/coral_fish.psd"), new Dimension(800, 800)),
// 1 channel, bitmap, 1 bit samples
new TestData(getClassLoaderResource("/psd/test_bitmap.psd"), new Dimension(710, 512)),
// 1 channel, gray, 16 bit samples
new TestData(getClassLoaderResource("/psd/test_gray16.psd"), new Dimension(710, 512)),
// 4 channel, CMYK, 16 bit samples
new TestData(getClassLoaderResource("/psd/cmyk_16bits.psd"), new Dimension(1000, 275)),
// 3 channel, RGB, 8 bit samples ("Large Document Format" aka PSB)
new TestData(getClassLoaderResource("/psb/test_original.psb"), new Dimension(710, 512)),
// From http://telegraphics.com.au/svn/psdparse/trunk/psd/
new TestData(getClassLoaderResource("/psd/adobehq.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq_ind.psd"), new Dimension(341, 512)),
// Contains a shorter than normal PrintFlags chunk
new TestData(getClassLoaderResource("/psd/adobehq-2.5.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-3.0.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-5.5.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-7.0.psd"), new Dimension(341, 512)),
// From https://github.com/kmike/psd-tools/tree/master/tests/psd_files
new TestData(getClassLoaderResource("/psd/masks2.psd"), new Dimension(640, 1136)),
// RGB, multiple alpha channels, no transparency
new TestData(getClassLoaderResource("/psd/rgb-multichannel-no-transparency.psd"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/psb/rgb-multichannel-no-transparency.psb"), new Dimension(100, 100)),
// CMYK, uncompressed + contains some uncommon MeSa (instead of 8BIM) resource blocks
new TestData(getClassLoaderResource("/psd/fruit-cmyk-MeSa-resource.psd"), new Dimension(400, 191)),
// 3 channel, RGB, 32 bit samples
new TestData(getClassLoaderResource("/psd/32bit5x5.psd"), new Dimension(5, 5)),
// 3 channel, RGB, written by GIMP, compressed with PackBits runs longer than the row length
new TestData(getClassLoaderResource("/psd/gimp-32x32-packbits-overflow.psd"), new Dimension(32, 32))
// TODO: Need more recent ZIP compressed PSD files from CS2/CS3+
);
}
@Override
protected List<TestData> getTestDataForAffineTransformOpCompatibility() {
return Arrays.asList(
// 5 channel, RGB
new TestData(getClassLoaderResource("/psd/photoshopping.psd"), new Dimension(300, 225)),
// 1 channel, gray, 8 bit samples
new TestData(getClassLoaderResource("/psd/buttons.psd"), new Dimension(20, 20)),
// 3 channel RGB, "no composite layer"
new TestData(getClassLoaderResource("/psd/jugware-icon.psd"), new Dimension(128, 128)),
// 3 channel RGB, old data, no layer info/mask
new TestData(getClassLoaderResource("/psd/MARBLES.PSD"), new Dimension(1419, 1001)),
// 1 channel, indexed color
new TestData(getClassLoaderResource("/psd/coral_fish.psd"), new Dimension(800, 800)),
// 1 channel, bitmap, 1 bit samples
new TestData(getClassLoaderResource("/psd/test_bitmap.psd"), new Dimension(710, 512)),
// 1 channel, gray, 16 bit samples
new TestData(getClassLoaderResource("/psd/test_gray16.psd"), new Dimension(710, 512)),
// 3 channel, RGB, 8 bit samples ("Large Document Format" aka PSB)
new TestData(getClassLoaderResource("/psb/test_original.psb"), new Dimension(710, 512)),
// From http://telegraphics.com.au/svn/psdparse/trunk/psd/
new TestData(getClassLoaderResource("/psd/adobehq.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq_ind.psd"), new Dimension(341, 512)),
// Contains a shorter than normal PrintFlags chunk
new TestData(getClassLoaderResource("/psd/adobehq-2.5.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-3.0.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-5.5.psd"), new Dimension(341, 512)),
new TestData(getClassLoaderResource("/psd/adobehq-7.0.psd"), new Dimension(341, 512)),
// From https://github.com/kmike/psd-tools/tree/master/tests/psd_files
new TestData(getClassLoaderResource("/psd/masks2.psd"), new Dimension(640, 1136)),
// RGB, multiple alpha channels, no transparency
new TestData(getClassLoaderResource("/psd/rgb-multichannel-no-transparency.psd"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/psb/rgb-multichannel-no-transparency.psb"), new Dimension(100, 100))
);
}
@Override
protected List<String> getFormatNames() {
return Collections.singletonList("psd");
}
@Override
protected List<String> getSuffixes() {
return Collections.singletonList("psd");
}
@Override
protected List<String> getMIMETypes() {
return Arrays.asList(
"image/vnd.adobe.photoshop",
"application/vnd.adobe.photoshop",
"image/x-psd"
);
}
@Test
public void testSupportsThumbnail() throws IOException {
PSDImageReader imageReader = createReader();
assertTrue(imageReader.readerSupportsThumbnails());
}
@Test
public void testThumbnailReading() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(0).getInputStream()) {
imageReader.setInput(stream);
assertEquals(1, imageReader.getNumThumbnails(0));
BufferedImage thumbnail = imageReader.readThumbnail(0, 0);
assertNotNull(thumbnail);
assertEquals(128, thumbnail.getWidth());
assertEquals(96, thumbnail.getHeight());
}
}
@Test
public void testThumbnailReadingNoInput() throws IOException {
PSDImageReader imageReader = createReader();
try {
imageReader.getNumThumbnails(0);
fail("Expected IllegalStateException");
}
catch (IllegalStateException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("input"));
}
try {
imageReader.getThumbnailWidth(0, 0);
fail("Expected IllegalStateException");
}
catch (IllegalStateException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("input"));
}
try {
imageReader.getThumbnailHeight(0, 0);
fail("Expected IllegalStateException");
}
catch (IllegalStateException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("input"));
}
try {
imageReader.readThumbnail(0, 0);
fail("Expected IllegalStateException");
}
catch (IllegalStateException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("input"));
}
}
@Test
public void testThumbnailReadingOutOfBounds() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(0).getInputStream()) {
imageReader.setInput(stream);
int numImages = imageReader.getNumImages(true);
try {
imageReader.getNumThumbnails(numImages + 1);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("index"), expected.getMessage());
}
try {
imageReader.getThumbnailWidth(-1, 0);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("index"), expected.getMessage());
}
try {
imageReader.getThumbnailHeight(0, -2);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
// Sloppy...
assertTrue(expected.getMessage().toLowerCase().contains("-2"), expected.getMessage());
}
try {
imageReader.readThumbnail(numImages + 99, 42);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
assertTrue(expected.getMessage().toLowerCase().contains("index"), expected.getMessage());
}
}
}
@Test
public void testThumbnailDimensions() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(0).getInputStream()) {
imageReader.setInput(stream);
assertEquals(1, imageReader.getNumThumbnails(0));
assertEquals(128, imageReader.getThumbnailWidth(0, 0));
assertEquals(96, imageReader.getThumbnailHeight(0, 0));
}
}
@Test
public void testThumbnailReadListeners() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(0).getInputStream()) {
imageReader.setInput(stream);
final List<Object> seqeunce = new ArrayList<>();
imageReader.addIIOReadProgressListener(new ProgressListenerBase() {
private float lastPercentageDone = 0;
@Override
public void thumbnailStarted(final ImageReader pSource, final int pImageIndex, final int pThumbnailIndex) {
seqeunce.add("started");
}
@Override
public void thumbnailComplete(final ImageReader pSource) {
seqeunce.add("complete");
}
@Override
public void thumbnailProgress(final ImageReader pSource, final float pPercentageDone) {
// Optional
assertEquals(1, seqeunce.size(), "Listener invoked out of sequence");
assertTrue(pPercentageDone >= lastPercentageDone);
lastPercentageDone = pPercentageDone;
}
});
BufferedImage thumbnail = imageReader.readThumbnail(0, 0);
assertNotNull(thumbnail);
assertEquals(2, seqeunce.size(), "Listeners not invoked");
assertEquals("started", seqeunce.get(0));
assertEquals("complete", seqeunce.get(1));
}
}
@Test
public void testReadLayers() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(3).getInputStream()) {
imageReader.setInput(stream);
int numImages = imageReader.getNumImages(true);
assertEquals(3, numImages);
for (int i = 0; i < numImages; i++) {
BufferedImage image = imageReader.read(i);
assertNotNull(image);
// Make sure layers are correct size
assertEquals(image.getWidth(), imageReader.getWidth(i));
assertEquals(image.getHeight(), imageReader.getHeight(i));
}
}
}
@Test
public void testImageTypesLayers() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(3).getInputStream()) {
imageReader.setInput(stream);
int numImages = imageReader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
ImageTypeSpecifier rawType = imageReader.getRawImageType(i);
assertNotNull(rawType);
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
assertNotNull(types);
assertTrue(types.hasNext());
boolean found = false;
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
if (!found && (rawType == type || rawType.equals(type))) {
found = true;
}
}
assertTrue(found, "RAW image type not in type iterator");
}
}
}
@Test
public void testReadLayersExplicitType() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(3).getInputStream()) {
imageReader.setInput(stream);
int numImages = imageReader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
ImageReadParam param = imageReader.getDefaultReadParam();
param.setDestinationType(type);
BufferedImage image = imageReader.read(i, param);
assertEquals(type.getBufferedImageType(), image.getType());
if (type.getBufferedImageType() == 0) {
// TODO: If type.getBIT == 0, test more
// Compatible color model
assertEquals(type.getNumComponents(), image.getColorModel().getNumComponents());
// Same color space
assertEquals(type.getColorModel().getColorSpace(), image.getColorModel().getColorSpace());
// Same number of samples
assertEquals(type.getNumBands(), image.getSampleModel().getNumBands());
// Same number of bits/sample
for (int j = 0; j < type.getNumBands(); j++) {
assertEquals(type.getBitsPerBand(j), image.getSampleModel().getSampleSize(j));
}
}
}
}
}
}
@Test
public void testReadLayersExplicitDestination() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = getTestData().get(3).getInputStream()) {
imageReader.setInput(stream);
int numImages = imageReader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
int width = imageReader.getWidth(i);
int height = imageReader.getHeight(i);
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
ImageReadParam param = imageReader.getDefaultReadParam();
BufferedImage destination = type.createBufferedImage(width, height);
param.setDestination(destination);
BufferedImage image = imageReader.read(i, param);
assertSame(destination, image);
}
}
}
}
@Test
public void testGrayAlphaLayers() throws IOException {
PSDImageReader imageReader = createReader();
// The expected colors for each layer
int[] colors = new int[] {
-1, // Don't care
0xff000000,
0xffffffff,
0xff737373,
0xff3c3c3c,
0xff656565,
0xffc9c9c9,
0xff979797,
0xff5a5a5a
};
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psd/test_grayscale_boxes.psd"))) {
imageReader.setInput(stream);
int numImages = imageReader.getNumImages(true);
assertEquals(colors.length, numImages);
// Skip reading the merged composite image
for (int i = 1; i < numImages; i++) {
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
int width = imageReader.getWidth(i);
int height = imageReader.getHeight(i);
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
ImageReadParam param = imageReader.getDefaultReadParam();
BufferedImage destination = type.createBufferedImage(width, height);
param.setDestination(destination);
BufferedImage image = imageReader.read(i, param);
assertSame(destination, image);
// NOTE: Allow some slack, as Java 1.7 and 1.8 color management differs slightly
int rgb = image.getRGB(0, 0);
assertRGBEquals("Colors differ", colors[i], rgb, 1);
}
}
}
}
@Test
public void testMultiChannelNoTransparencyPSD() throws IOException {
PSDImageReader imageReader = createReader();
// The following PSD is RGB, has 4 channels (1 alpha/auxillary channel), but should be treated as opaque
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psd/rgb-multichannel-no-transparency.psd"))) {
imageReader.setInput(stream);
BufferedImage image = imageReader.read(0);
assertEquals(Transparency.OPAQUE, image.getTransparency());
}
}
@Test
public void testMultiChannelNoTransparencyPSB() throws IOException {
PSDImageReader imageReader = createReader();
// The following PSB is RGB, has 4 channels (1 alpha/auxiliary channel), but should be treated as opaque
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psb/rgb-multichannel-no-transparency.psb"))) {
imageReader.setInput(stream);
BufferedImage image = imageReader.read(0);
assertEquals(Transparency.OPAQUE, image.getTransparency());
}
}
@Test
public void testReadUnicodeLayerName() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psd/long-layer-names.psd"))) {
imageReader.setInput(stream);
IIOMetadata metadata = imageReader.getImageMetadata(0);
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(PSDMetadata.NATIVE_METADATA_FORMAT_NAME);
NodeList layerInfo = root.getElementsByTagName("LayerInfo");
assertEquals(1, layerInfo.getLength()); // Sanity
assertEquals("If_The_Layer_Name_Is_Really_Long_Oh_No_What_Do_I_Do", ((IIOMetadataNode) layerInfo.item(0)).getAttribute("name"));
}
}
@Test
public void testGroupLayerRead() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psd/layer_group_32bit5x5.psd"))) {
imageReader.setInput(stream);
IIOMetadata metadata = imageReader.getImageMetadata(0);
List<PSDLayerInfo> layerInfos = ((PSDMetadata) metadata).layerInfo;
assertEquals(8, layerInfos.size());
// Normal layer, top level
PSDLayerInfo layer5 = layerInfos.get(0);
assertNotNull(layer5);
assertEquals("Layer 5", layer5.getLayerName());
assertEquals(2, layer5.getLayerId());
assertEquals(-1, layer5.groupId);
assertFalse(layer5.isGroup);
assertFalse(layer5.isDivider);
// Divider, invisible in UI, in "group 1" (group id 6)
PSDLayerInfo sectionDivider1 = layerInfos.get(1);
assertNotNull(sectionDivider1);
assertEquals("</Layer group>", sectionDivider1.getLayerName());
assertEquals(7, sectionDivider1.getLayerId());
assertEquals(6, sectionDivider1.groupId); // ...or -1?
assertFalse(sectionDivider1.isGroup);
assertTrue(sectionDivider1.isDivider);
// Normal layer, in "group 1" (group id 6)
PSDLayerInfo layer2 = layerInfos.get(2);
assertNotNull(layer2);
assertEquals("Layer 2", layer2.getLayerName());
assertEquals(5, layer2.getLayerId());
assertEquals(6, layer2.groupId);
assertFalse(layer2.isGroup);
assertFalse(layer2.isDivider);
// Divider, invisible in UI, in "group 1" (group id 9)
PSDLayerInfo sectionDivider2 = layerInfos.get(3);
assertNotNull(sectionDivider2);
assertEquals("</Layer group>", sectionDivider2.getLayerName());
assertEquals(10, sectionDivider2.getLayerId());
assertEquals(9, sectionDivider2.groupId); // ...or 6?
assertFalse(sectionDivider2.isGroup);
assertTrue(sectionDivider2.isDivider);
// Normal layer, in "nested group 1" (group id 9)
PSDLayerInfo groupedLayer = layerInfos.get(4);
assertNotNull(groupedLayer);
assertEquals("Nested Group Layer 1", groupedLayer.getLayerName());
assertEquals(8, groupedLayer.getLayerId());
assertEquals(9, groupedLayer.groupId);
assertFalse(groupedLayer.isGroup);
assertFalse(groupedLayer.isDivider);
// Group layer, in "group 1" (group id 6)
PSDLayerInfo nestedGroupLayer = layerInfos.get(5);
assertNotNull(nestedGroupLayer);
assertEquals("nested group 1", nestedGroupLayer.getLayerName());
assertEquals(9, nestedGroupLayer.getLayerId());
assertEquals(6, nestedGroupLayer.groupId);
assertTrue(nestedGroupLayer.isGroup);
assertFalse(nestedGroupLayer.isDivider);
// Group layer, top level
PSDLayerInfo groupLayer = layerInfos.get(6);
assertNotNull(groupLayer);
assertEquals("group 1", groupLayer.getLayerName());
assertEquals(6, groupLayer.getLayerId());
assertEquals(-1, groupLayer.groupId);
assertTrue(groupLayer.isGroup);
assertFalse(groupLayer.isDivider);
// Normal layer, top level
PSDLayerInfo layer1 = layerInfos.get(7);
assertNotNull(layer1);
assertEquals("Layer 1", layer1.getLayerName());
assertEquals(4, layer1.getLayerId());
assertEquals(-1, layer1.groupId);
assertFalse(layer1.isGroup);
assertFalse(layer1.isDivider);
}
}
@Test
public void test16bitLr16AndZIPPredictor() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psd/fruit-cmyk-MeSa-resource.psd"))) {
imageReader.setInput(stream);
assertEquals(5, imageReader.getNumImages(true));
assertEquals(400, imageReader.getWidth(2));
assertEquals(191, imageReader.getHeight(2));
BufferedImage layer2 = imageReader.read(2);// Read the 16 bit ZIP Predictor based layer
assertNotNull(layer2);
assertEquals(400, layer2.getWidth());
assertEquals(191, layer2.getHeight());
assertEquals(ColorSpace.TYPE_CMYK, layer2.getColorModel().getColorSpace().getType());
assertEquals(5, layer2.getColorModel().getNumComponents());
// For cross-platform testing: as the PSD does not have embedded CMYK profile, we'll use built-in RGB conversion
ColorModel cmykAlpha = new ComponentColorModel(new FakeCMYKColorSpace(), true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_USHORT);
layer2 = new BufferedImage(cmykAlpha, layer2.getRaster(), cmykAlpha.isAlphaPremultiplied(), null);
assertRGBEquals("RGB differ at (0,0)", 0xff060808, layer2.getRGB(0, 0), 4);
assertRGBEquals("RGB differ at (399,0)", 0xff060808, layer2.getRGB(399, 0), 4);
assertRGBEquals("RGB differ at (200,95)", 0x00ffffff, layer2.getRGB(200, 95), 4); // Transparent
assertRGBEquals("RGB differ at (0,191)", 0xff060808, layer2.getRGB(0, 190), 4);
assertRGBEquals("RGB differ at (399,191)", 0xff060808, layer2.getRGB(399, 190), 4);
assertEquals(400, imageReader.getWidth(3));
assertEquals(191, imageReader.getHeight(3));
BufferedImage layer3 = imageReader.read(3);// Read the 16 bit ZIP Predictor based layer
assertNotNull(layer3);
assertEquals(400, layer3.getWidth());
assertEquals(191, layer3.getHeight());
assertEquals(ColorSpace.TYPE_CMYK, layer3.getColorModel().getColorSpace().getType());
assertEquals(5, layer3.getColorModel().getNumComponents());
// For cross-platform testing: as the PSD does not have embedded CMYK profile, we'll use built-in RGB conversion
layer3 = new BufferedImage(cmykAlpha, layer3.getRaster(), cmykAlpha.isAlphaPremultiplied(), null);
assertRGBEquals("RGB differ at (0,0)", 0xfff5cb0c, layer3.getRGB(0, 0), 4);
assertRGBEquals("RGB differ at (399,0)", 0xfff5cb0c, layer3.getRGB(399, 0), 4);
assertRGBEquals("RGB differ at (200,95)", 0xffff152a, layer3.getRGB(200, 95), 4); // Red
assertRGBEquals("RGB differ at (0,191)", 0xfff5cb0c, layer3.getRGB(0, 190), 4);
assertRGBEquals("RGB differ at (399,191)", 0xfff5cb0c, layer3.getRGB(399, 190), 4);
}
}
@Test
public void test32bitLr32AndZIPPredictor() throws IOException {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/psd/32bit5x5.psd"))) {
imageReader.setInput(stream);
assertEquals(4, imageReader.getNumImages(true));
assertEquals(5, imageReader.getWidth(1));
assertEquals(5, imageReader.getHeight(1));
BufferedImage image = imageReader.read(1);// Read the 32 bit ZIP Predictor based layer
assertNotNull(image);
assertEquals(5, image.getWidth());
assertEquals(5, image.getHeight());
assertRGBEquals("RGB differ at (0,0)", 0xff888888, image.getRGB(0, 0), 4);
assertRGBEquals("RGB differ at (4,4)", 0xff888888, image.getRGB(4, 4), 4);
}
}
@Test
public void testBrokenPackBitsThrowsEOFException() {
assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
PSDImageReader imageReader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/broken-psd/short-packbits.psd"))) {
imageReader.setInput(stream);
assertEquals(1, imageReader.getNumImages(true));
assertEquals(427, imageReader.getWidth(0));
assertEquals(107, imageReader.getHeight(0));
try {
imageReader.read(0);
fail("Expected EOFException, is the test broken?");
} catch (EOFException expected) {
assertTrue(expected.getMessage().contains("PackBits"));
}
}
});
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void negativeLayerMaskInfiniteLoop() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSB, 3, 10, 10, PSD.COLOR_MODE_RGB);
dataStream.writeInt(0); // color mode data length
dataStream.writeInt(0); // image resources length
dataStream.writeLong(100); // layer+mask info length (PSB: 8 bytes)
dataStream.writeLong(0); // layer info length (PSB: 8 bytes)
dataStream.writeInt(0); // global layer mask length
dataStream.writeBytes("8BIM");
dataStream.writeBytes("LMsk");
dataStream.writeLong(-19); // 64-bit record length, negative
dataStream.write(new byte[64]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.read(0));
assertTrue(exception.getMessage().contains("Length field exceeds Long.MAX_VALUE"));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void negativeAdditionalLayerInfoLengthInfiniteLoop() throws IOException {
ByteArrayOutputStream additional = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(additional)) {
dataStream.writeBytes("8B64");
dataStream.writeBytes("luni");
dataStream.writeLong(-16); // 64-bit record length, negative (seeks back to the signature)
dataStream.write(new byte[16]);
}
byte[] data = createLayeredPSD(createLayer(12 + additional.size(), 0, additional.toByteArray()));
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(data)) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getNumImages(true));
assertThat(exception.getMessage(), containsString("Length field exceeds Long.MAX_VALUE"));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void layerExtraDataSizeTooSmall() throws IOException {
// Extra data size 0 would make the reader seek backwards, and re-read the same data for the next layer
byte[] data = createLayeredPSD(createLayer(0, 0, new byte[0]));
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(data)) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getNumImages(true));
assertThat(exception.getMessage(), containsString("Layer extra data size"));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void negativeLayerBlendingDataSize() throws IOException {
byte[] data = createLayeredPSD(createLayer(12, -8, new byte[0]));
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(data)) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getNumImages(true));
assertThat(exception.getMessage(), containsString("Layer Blending Data size"));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void negativeColorDataLength() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSD, 1, 10, 10, PSD.COLOR_MODE_INDEXED);
dataStream.writeInt(-3); // color mode data length, negative multiple of 3
dataStream.write(new byte[64]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getWidth(0));
assertThat(exception.getMessage(), containsString("corrupt palette information"));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void directoryResourceSizeExceedsInput() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSD, 1, 10, 10, PSD.COLOR_MODE_GRAYSCALE);
dataStream.writeInt(0); // color mode data length
dataStream.writeInt(4 + 2 + 2 + 4 + 64); // image resources length
dataStream.writeBytes("8BIM");
dataStream.writeShort(PSD.RES_XMP_DATA);
dataStream.writeShort(0); // empty pascal name + pad
dataStream.writeInt(Integer.MAX_VALUE); // resource size, way larger than the input
dataStream.write(new byte[64]);
dataStream.writeInt(0); // layer+mask info length
dataStream.writeShort(PSD.COMPRESSION_NONE);
dataStream.write(new byte[100]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getImageMetadata(0));
assertThat(exception.getMessage(), containsString("Resource size exceeds limit"));
}
finally {
reader.dispose();
}
}
@Test
void unsupportedDimensionsPSD() throws IOException {
Dimension[] cases = {
new Dimension(30_001, 10),
new Dimension(10, 0),
new Dimension(-1, 10)
};
for (Dimension dimension : cases) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSD, 1, dimension.height, dimension.width, PSD.COLOR_MODE_GRAYSCALE);
dataStream.write(new byte[64]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getWidth(0), dimension.toString());
assertThat(exception.getMessage(), containsString("Unsupported dimensions"));
}
finally {
reader.dispose();
}
}
}
@Test
void unsupportedDimensionsPSB() throws IOException {
Dimension[] cases = {
new Dimension(10, 300_001)
};
for (Dimension dimension : cases) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSB, 1, dimension.height, dimension.width, PSD.COLOR_MODE_GRAYSCALE);
dataStream.write(new byte[64]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.getWidth(0), dimension.toString());
assertThat(exception.getMessage(), containsString("Unsupported dimensions"));
}
finally {
reader.dispose();
}
}
}
@Test
void validDimensionsPSBExceedingPSDLimit() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSB, 1, 10, 30_001, PSD.COLOR_MODE_GRAYSCALE);
dataStream.writeInt(0); // color mode data length
dataStream.writeInt(0); // image resources length
dataStream.writeLong(0); // layer+mask info length (PSB: 8 bytes)
dataStream.write(new byte[64]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
assertEquals(30_001, reader.getWidth(0));
assertEquals(10, reader.getHeight(0));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void rleByteCountsExceedsInput() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
// 1 x 300000 pixels is within the max expansion ratio for the input size,
// but the RLE byte counts table (4 bytes per row for PSB) can't possibly fit in the input
writeHeader(dataStream, PSD.VERSION_PSB, 1, 300_000, 1, PSD.COLOR_MODE_GRAYSCALE);
dataStream.writeInt(0); // color mode data length
dataStream.writeInt(0); // image resources length
dataStream.writeLong(0); // layer+mask info length (PSB: 8 bytes)
dataStream.writeShort(PSD.COMPRESSION_RLE);
dataStream.write(new byte[1024]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.read(0));
assertThat(exception.getMessage(), containsStringIgnoringCase("image dimensions"));
}
finally {
reader.dispose();
}
}
@Test
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
void negativeRLEByteCount() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSB, 1, 10, 10, PSD.COLOR_MODE_GRAYSCALE); // PSB, 32-bit byte counts
dataStream.writeInt(0); // color mode data length
dataStream.writeInt(0); // image resources length
dataStream.writeLong(0); // layer+mask info length (PSB: 8 bytes)
dataStream.writeShort(PSD.COMPRESSION_RLE);
for (int y = 0; y < 10; y++) {
dataStream.writeInt(-1); // byte count, negative
}
dataStream.write(new byte[256]);
}
PSDImageReader reader = createReader();
try (ImageInputStream stream = new ByteArrayImageInputStream(bytes.toByteArray())) {
reader.setInput(stream);
IIOException exception = assertThrows(IIOException.class, () -> reader.read(0));
assertThat(exception.getMessage(), containsString("RLE byte count"));
}
finally {
reader.dispose();
}
}
private static void writeHeader(final DataOutputStream dataStream, int version, int channels, int height, int width, int mode) throws IOException {
dataStream.writeBytes("8BPS");
dataStream.writeShort(version);
dataStream.write(new byte[6]); // reserved
dataStream.writeShort(channels);
dataStream.writeInt(height);
dataStream.writeInt(width);
dataStream.writeShort(8); // bits per sample
dataStream.writeShort(mode);
}
private static byte[] createLayer(long extraDataSize, int layerBlendingDataSize, byte[] additionalLayerInfo) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
dataStream.writeInt(0); // top
dataStream.writeInt(0); // left
dataStream.writeInt(1); // bottom
dataStream.writeInt(1); // right
dataStream.writeShort(1); // channels
dataStream.writeShort(0); // channel id
dataStream.writeInt(3); // channel data length
dataStream.writeBytes("8BIM"); // blend mode signature
dataStream.writeBytes("norm"); // blend mode
dataStream.writeByte(255); // opacity
dataStream.writeByte(0); // clipping
dataStream.writeByte(0); // flags
dataStream.writeByte(0); // pad
dataStream.writeInt((int) extraDataSize);
dataStream.writeInt(0); // layer mask data size
dataStream.writeInt(layerBlendingDataSize);
dataStream.writeInt(0); // empty pascal name + 3 pad bytes
dataStream.write(additionalLayerInfo);
}
return bytes.toByteArray();
}
private static byte[] createLayeredPSD(byte[] layer) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream dataStream = new DataOutputStream(bytes)) {
writeHeader(dataStream, PSD.VERSION_PSD, 1, 1, 1, PSD.COLOR_MODE_GRAYSCALE);
dataStream.writeInt(0); // color mode data length
dataStream.writeInt(0); // image resources length
dataStream.writeInt(4 + 2 + layer.length + 64 + 4); // layer+mask info length
dataStream.writeInt(2 + layer.length + 64); // layer info length
dataStream.writeShort(1); // layer count
dataStream.write(layer);
dataStream.write(new byte[64]); // channel image data (and some slack)
dataStream.writeInt(0); // global layer mask length
dataStream.write(new byte[64]);
}
return bytes.toByteArray();
}
final static class FakeCMYKColorSpace extends ColorSpace {
FakeCMYKColorSpace() {
super(ColorSpace.TYPE_CMYK, 4);
}
public float[] toRGB(float[] cmyk) {
return new float[] {
(1 - cmyk[0]) * (1 - cmyk[3]),
(1 - cmyk[1]) * (1 - cmyk[3]),
(1 - cmyk[2]) * (1 - cmyk[3])
};
}
public float[] fromRGB(float[] rgb) {
throw new UnsupportedOperationException();
}
public float[] toCIEXYZ(float[] cmyk) {
throw new UnsupportedOperationException();
}
public float[] fromCIEXYZ(float[] cieXYZ) {
throw new UnsupportedOperationException();
}
}
}