From a29960e8ee2724e17116831bd9a440873475dacb Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 30 Jun 2016 14:17:00 +0200 Subject: [PATCH] #248 PSDImageReader now uses correct band indices for grayscale + alpha layers. --- .../imageio/plugins/psd/PSDImageReader.java | 18 +- .../plugins/psd/PSDImageReaderTest.java | 310 +++++++++++------- .../resources/psd/test_grayscale_boxes.psd | Bin 0 -> 171924 bytes 3 files changed, 202 insertions(+), 126 deletions(-) create mode 100644 imageio/imageio-psd/src/test/resources/psd/test_grayscale_boxes.psd diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java index d5f45e16..b30b28d7 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java @@ -267,6 +267,20 @@ public final class PSDImageReader extends ImageReaderBase { List types = new ArrayList<>(); switch (header.mode) { + case PSD.COLOR_MODE_GRAYSCALE: + if (rawType.getNumBands() == 1 && rawType.getBitsPerBand(0) == 8) { + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY)); + } + else if (rawType.getNumBands() >= 2 && rawType.getBitsPerBand(0) == 8) { + types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {1, 0}, DataBuffer.TYPE_BYTE, true, false)); + } + else if (rawType.getNumBands() == 1 && rawType.getBitsPerBand(0) == 16) { + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY)); + } + else if (rawType.getNumBands() >= 2 && rawType.getBitsPerBand(0) == 16) { + types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {1, 0}, DataBuffer.TYPE_USHORT, true, false)); + } + break; case PSD.COLOR_MODE_RGB: // Prefer interleaved versions as they are much faster to display if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) { @@ -283,7 +297,7 @@ public final class PSDImageReader extends ImageReaderBase { // TODO: Integer raster // types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_ARGB)); types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR)); -// + if (!cs.isCS_sRGB()) { // Basically BufferedImage.TYPE_4BYTE_ABGR, with corrected ColorSpace. Possibly slow. types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false)); @@ -1116,7 +1130,7 @@ public final class PSDImageReader extends ImageReaderBase { if (newBandNum > compositeType.getNumBands()) { int[] indices = new int[newBandNum]; for (int i = 0, indicesLength = indices.length; i < indicesLength; i++) { - indices[i] = indicesLength - i; + indices[i] = i; } int[] offs = new int[newBandNum]; diff --git a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java index 73ca0e32..7d93f18f 100755 --- a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java +++ b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderTest.java @@ -138,15 +138,17 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest public void testThumbnailReading() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(0).getInputStream()); + try (ImageInputStream stream = getTestData().get(0).getInputStream()) { + imageReader.setInput(stream); - assertEquals(1, imageReader.getNumThumbnails(0)); + assertEquals(1, imageReader.getNumThumbnails(0)); - BufferedImage thumbnail = imageReader.readThumbnail(0, 0); - assertNotNull(thumbnail); + BufferedImage thumbnail = imageReader.readThumbnail(0, 0); + assertNotNull(thumbnail); - assertEquals(128, thumbnail.getWidth()); - assertEquals(96, thumbnail.getHeight()); + assertEquals(128, thumbnail.getWidth()); + assertEquals(96, thumbnail.getHeight()); + } } @Test @@ -190,41 +192,43 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest public void testThumbnailReadingOutOfBounds() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(0).getInputStream()); + try (ImageInputStream stream = getTestData().get(0).getInputStream()) { + imageReader.setInput(stream); - int numImages = imageReader.getNumImages(true); + int numImages = imageReader.getNumImages(true); - try { - imageReader.getNumThumbnails(numImages + 1); - fail("Expected IndexOutOfBoundsException"); - } - catch (IndexOutOfBoundsException expected) { - assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("index")); - } + try { + imageReader.getNumThumbnails(numImages + 1); + fail("Expected IndexOutOfBoundsException"); + } + catch (IndexOutOfBoundsException expected) { + assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("index")); + } - try { - imageReader.getThumbnailWidth(-1, 0); - fail("Expected IndexOutOfBoundsException"); - } - catch (IndexOutOfBoundsException expected) { - assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("index")); - } + try { + imageReader.getThumbnailWidth(-1, 0); + fail("Expected IndexOutOfBoundsException"); + } + catch (IndexOutOfBoundsException expected) { + assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("index")); + } - try { - imageReader.getThumbnailHeight(0, -2); - fail("Expected IndexOutOfBoundsException"); - } - catch (IndexOutOfBoundsException expected) { - // Sloppy... - assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("-2")); - } + try { + imageReader.getThumbnailHeight(0, -2); + fail("Expected IndexOutOfBoundsException"); + } + catch (IndexOutOfBoundsException expected) { + // Sloppy... + assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("-2")); + } - try { - imageReader.readThumbnail(numImages + 99, 42); - fail("Expected IndexOutOfBoundsException"); - } - catch (IndexOutOfBoundsException expected) { - assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("index")); + try { + imageReader.readThumbnail(numImages + 99, 42); + fail("Expected IndexOutOfBoundsException"); + } + catch (IndexOutOfBoundsException expected) { + assertTrue(expected.getMessage(), expected.getMessage().toLowerCase().contains("index")); + } } } @@ -232,67 +236,73 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest public void testThumbnailDimensions() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(0).getInputStream()); + try (ImageInputStream stream = getTestData().get(0).getInputStream()) { + imageReader.setInput(stream); - assertEquals(1, imageReader.getNumThumbnails(0)); + assertEquals(1, imageReader.getNumThumbnails(0)); - assertEquals(128, imageReader.getThumbnailWidth(0, 0)); - assertEquals(96, imageReader.getThumbnailHeight(0, 0)); + assertEquals(128, imageReader.getThumbnailWidth(0, 0)); + assertEquals(96, imageReader.getThumbnailHeight(0, 0)); + } } @Test public void testThumbnailReadListeners() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(0).getInputStream()); + try (ImageInputStream stream = getTestData().get(0).getInputStream()) { + imageReader.setInput(stream); - final List sequnce = new ArrayList<>(); - imageReader.addIIOReadProgressListener(new ProgressListenerBase() { - private float mLastPercentageDone = 0; + final List seqeunce = new ArrayList<>(); + imageReader.addIIOReadProgressListener(new ProgressListenerBase() { + private float mLastPercentageDone = 0; - @Override - public void thumbnailStarted(final ImageReader pSource, final int pImageIndex, final int pThumbnailIndex) { - sequnce.add("started"); - } + @Override + public void thumbnailStarted(final ImageReader pSource, final int pImageIndex, final int pThumbnailIndex) { + seqeunce.add("started"); + } - @Override - public void thumbnailComplete(final ImageReader pSource) { - sequnce.add("complete"); - } + @Override + public void thumbnailComplete(final ImageReader pSource) { + seqeunce.add("complete"); + } - @Override - public void thumbnailProgress(final ImageReader pSource, final float pPercentageDone) { - // Optional - assertTrue("Listener invoked out of sequence", sequnce.size() == 1); - assertTrue(pPercentageDone >= mLastPercentageDone); - } - }); + @Override + public void thumbnailProgress(final ImageReader pSource, final float pPercentageDone) { + // Optional + assertTrue("Listener invoked out of sequence", seqeunce.size() == 1); + assertTrue(pPercentageDone >= mLastPercentageDone); + } + }); - BufferedImage thumbnail = imageReader.readThumbnail(0, 0); - assertNotNull(thumbnail); + BufferedImage thumbnail = imageReader.readThumbnail(0, 0); + assertNotNull(thumbnail); - assertEquals("Listeners not invoked", 2, sequnce.size()); - assertEquals("started", sequnce.get(0)); - assertEquals("complete", sequnce.get(1)); + assertEquals("Listeners not invoked", 2, seqeunce.size()); + assertEquals("started", seqeunce.get(0)); + assertEquals("complete", seqeunce.get(1)); + } } @Test public void testReadLayers() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(3).getInputStream()); + try (ImageInputStream stream = getTestData().get(3).getInputStream()) { + imageReader.setInput(stream); - int numImages = imageReader.getNumImages(true); + int numImages = imageReader.getNumImages(true); - assertEquals(3, numImages); + assertEquals(3, numImages); - for (int i = 0; i < numImages; i++) { - BufferedImage image = imageReader.read(i); - assertNotNull(image); + 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)); + // Make sure layers are correct size + assertEquals(image.getWidth(), imageReader.getWidth(i)); + assertEquals(image.getHeight(), imageReader.getHeight(i)); + } } } @@ -300,31 +310,31 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest public void testImageTypesLayers() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(3).getInputStream()); + 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); -// System.err.println("rawType: " + rawType); - assertNotNull(rawType); + int numImages = imageReader.getNumImages(true); + for (int i = 0; i < numImages; i++) { + ImageTypeSpecifier rawType = imageReader.getRawImageType(i); + assertNotNull(rawType); - Iterator types = imageReader.getImageTypes(i); + Iterator types = imageReader.getImageTypes(i); - assertNotNull(types); - assertTrue(types.hasNext()); + assertNotNull(types); + assertTrue(types.hasNext()); - boolean found = false; + boolean found = false; - while (types.hasNext()) { - ImageTypeSpecifier type = types.next(); -// System.err.println("type: " + type); + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); - if (!found && (rawType == type || rawType.equals(type))) { - found = true; + if (!found && (rawType == type || rawType.equals(type))) { + found = true; + } } - } - assertTrue("RAW image type not in type iterator", found); + assertTrue("RAW image type not in type iterator", found); + } } } @@ -332,34 +342,36 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest public void testReadLayersExplicitType() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(3).getInputStream()); + try (ImageInputStream stream = getTestData().get(3).getInputStream()) { + imageReader.setInput(stream); - int numImages = imageReader.getNumImages(true); - for (int i = 0; i < numImages; i++) { - Iterator types = imageReader.getImageTypes(i); + int numImages = imageReader.getNumImages(true); + for (int i = 0; i < numImages; i++) { + Iterator types = imageReader.getImageTypes(i); - while (types.hasNext()) { - ImageTypeSpecifier type = types.next(); - ImageReadParam param = imageReader.getDefaultReadParam(); - param.setDestinationType(type); - BufferedImage image = imageReader.read(i, param); + 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()); + 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()); + 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 color space + assertEquals(type.getColorModel().getColorSpace(), image.getColorModel().getColorSpace()); - // Same number of samples - assertEquals(type.getNumBands(), image.getSampleModel().getNumBands()); + // 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)); + // Same number of bits/sample + for (int j = 0; j < type.getNumBands(); j++) { + assertEquals(type.getBitsPerBand(j), image.getSampleModel().getSampleSize(j)); + } } } } @@ -370,23 +382,73 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest public void testReadLayersExplicitDestination() throws IOException { PSDImageReader imageReader = createReader(); - imageReader.setInput(getTestData().get(3).getInputStream()); + try (ImageInputStream stream = getTestData().get(3).getInputStream()) { + imageReader.setInput(stream); - int numImages = imageReader.getNumImages(true); - for (int i = 0; i < numImages; i++) { - Iterator types = imageReader.getImageTypes(i); - int width = imageReader.getWidth(i); - int height = imageReader.getHeight(i); + int numImages = imageReader.getNumImages(true); + for (int i = 0; i < numImages; i++) { + Iterator 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); + 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); + BufferedImage image = imageReader.read(i, param); - assertSame(destination, image); + 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 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(String.format("#%04x != #%04x", colors[i], rgb), colors[i], rgb, 1); + } } } } diff --git a/imageio/imageio-psd/src/test/resources/psd/test_grayscale_boxes.psd b/imageio/imageio-psd/src/test/resources/psd/test_grayscale_boxes.psd new file mode 100644 index 0000000000000000000000000000000000000000..c854942d547755d1062b147e84afbb2ae0bd064c GIT binary patch literal 171924 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 z