6788458: PNGImageReader ignores tRNS chunk while reading non-indexed RGB/Gray images
Reviewed-by: prr, pnarayanan, kaddepalli
--- a/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java Wed Apr 18 10:43:43 2018 +0530
+++ b/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java Wed Apr 18 12:33:21 2018 +0530
@@ -698,10 +698,12 @@
readHeader();
/*
- * Optimization: We can skip the remaining metadata if the
- * ignoreMetadata flag is set, and only if this is not a palette
- * image (in that case, we need to read the metadata to get the
- * tRNS chunk, which is needed for the getImageTypes() method).
+ * Optimization: We can skip reading metadata if ignoreMetadata
+ * flag is set and colorType is not PNG_COLOR_PALETTE. However,
+ * we parse tRNS chunk to retrieve the transparent color from the
+ * metadata. Doing so, helps PNGImageReader to appropriately
+ * identify and set transparent pixels in the decoded image for
+ * colorType PNG_COLOR_RGB and PNG_COLOR_GRAY.
*/
int colorType = metadata.IHDR_colorType;
if (ignoreMetadata && colorType != PNG_COLOR_PALETTE) {
@@ -717,10 +719,19 @@
int chunkType = stream.readInt();
if (chunkType == IDAT_TYPE) {
- // We've reached the image data
+ // We've reached the first IDAT chunk position
stream.skipBytes(-8);
imageStartPosition = stream.getStreamPosition();
+ /*
+ * According to PNG specification tRNS chunk must
+ * precede the first IDAT chunk. So we can stop
+ * reading metadata.
+ */
break;
+ } else if (chunkType == tRNS_TYPE) {
+ parse_tRNS_chunk(chunkLength);
+ // After parsing tRNS chunk we will skip 4 CRC bytes
+ stream.skipBytes(4);
} else {
// Skip the chunk plus the 4 CRC bytes that follow
stream.skipBytes(chunkLength + 4);
@@ -1266,11 +1277,27 @@
break;
}
- if (useSetRect) {
+ /*
+ * For PNG images of color type PNG_COLOR_RGB or PNG_COLOR_GRAY
+ * that contain a specific transparent color (given by tRNS
+ * chunk), we compare the decoded pixel color with the color
+ * given by tRNS chunk to set the alpha on the destination.
+ */
+ boolean tRNSTransparentPixelPresent =
+ theImage.getSampleModel().getNumBands() == inputBands + 1 &&
+ metadata.hasTransparentColor();
+ if (useSetRect &&
+ !tRNSTransparentPixelPresent) {
imRas.setRect(updateMinX, dstY, passRow);
} else {
int newSrcX = srcX;
+ /*
+ * Create intermediate array to fill the extra alpha
+ * channel when tRNSTransparentPixelPresent is true.
+ */
+ final int[] temp = new int[inputBands + 1];
+ final int opaque = (bitDepth < 16) ? 255 : 65535;
for (int dstX = updateMinX;
dstX < updateMinX + updateWidth;
dstX += updateXStep) {
@@ -1281,7 +1308,31 @@
ps[b] = scale[b][ps[b]];
}
}
- imRas.setPixel(dstX, dstY, ps);
+ if (tRNSTransparentPixelPresent) {
+ if (metadata.tRNS_colorType == PNG_COLOR_RGB) {
+ temp[0] = ps[0];
+ temp[1] = ps[1];
+ temp[2] = ps[2];
+ if (ps[0] == metadata.tRNS_red &&
+ ps[1] == metadata.tRNS_green &&
+ ps[2] == metadata.tRNS_blue) {
+ temp[3] = 0;
+ } else {
+ temp[3] = opaque;
+ }
+ } else {
+ // when tRNS_colorType is PNG_COLOR_GRAY
+ temp[0] = ps[0];
+ if (ps[0] == metadata.tRNS_gray) {
+ temp[1] = 0;
+ } else {
+ temp[1] = opaque;
+ }
+ }
+ imRas.setPixel(dstX, dstY, temp);
+ } else {
+ imRas.setPixel(dstX, dstY, ps);
+ }
newSrcX += srcXStep;
}
}
@@ -1422,9 +1473,17 @@
// how many bands are in the image, so perform checking
// of the read param.
int colorType = metadata.IHDR_colorType;
- checkReadParamBandSettings(param,
- inputBandsForColorType[colorType],
- theImage.getSampleModel().getNumBands());
+ if (theImage.getSampleModel().getNumBands()
+ == inputBandsForColorType[colorType] + 1
+ && metadata.hasTransparentColor()) {
+ checkReadParamBandSettings(param,
+ inputBandsForColorType[colorType] + 1,
+ theImage.getSampleModel().getNumBands());
+ } else {
+ checkReadParamBandSettings(param,
+ inputBandsForColorType[colorType],
+ theImage.getSampleModel().getNumBands());
+ }
clearAbortRequest();
processImageStarted(0);
@@ -1506,7 +1565,26 @@
}
switch (colorType) {
+ /*
+ * For PNG images of color type PNG_COLOR_RGB or PNG_COLOR_GRAY that
+ * contain a specific transparent color (given by tRNS chunk), we add
+ * ImageTypeSpecifier(s) that support transparency to the list of
+ * supported image types.
+ */
case PNG_COLOR_GRAY:
+ readMetadata(); // Need tRNS chunk
+
+ if (metadata.hasTransparentColor()) {
+ gray = ColorSpace.getInstance(ColorSpace.CS_GRAY);
+ bandOffsets = new int[2];
+ bandOffsets[0] = 0;
+ bandOffsets[1] = 1;
+ l.add(ImageTypeSpecifier.createInterleaved(gray,
+ bandOffsets,
+ dataType,
+ true,
+ false));
+ }
// Packed grayscale
l.add(ImageTypeSpecifier.createGrayscale(bitDepth,
dataType,
@@ -1514,7 +1592,13 @@
break;
case PNG_COLOR_RGB:
+ readMetadata(); // Need tRNS chunk
+
if (bitDepth == 8) {
+ if (metadata.hasTransparentColor()) {
+ l.add(ImageTypeSpecifier.createFromBufferedImageType(
+ BufferedImage.TYPE_4BYTE_ABGR));
+ }
// some standard types of buffered images
// which can be used as destination
l.add(ImageTypeSpecifier.createFromBufferedImageType(
@@ -1527,6 +1611,19 @@
BufferedImage.TYPE_INT_BGR));
}
+
+ if (metadata.hasTransparentColor()) {
+ rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB);
+ bandOffsets = new int[4];
+ bandOffsets[0] = 0;
+ bandOffsets[1] = 1;
+ bandOffsets[2] = 2;
+ bandOffsets[3] = 3;
+
+ l.add(ImageTypeSpecifier.
+ createInterleaved(rgb, bandOffsets,
+ dataType, true, false));
+ }
// Component R, G, B
rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB);
bandOffsets = new int[3];
--- a/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java Wed Apr 18 10:43:43 2018 +0530
+++ b/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java Wed Apr 18 12:33:21 2018 +0530
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -2259,6 +2259,12 @@
return retVal;
}
+ boolean hasTransparentColor() {
+ return tRNS_present &&
+ (tRNS_colorType == PNGImageReader.PNG_COLOR_RGB ||
+ tRNS_colorType == PNGImageReader.PNG_COLOR_GRAY);
+ }
+
// Reset all instance variables to their initial state
public void reset() {
IHDR_present = false;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/javax/imageio/plugins/png/ReadPngGrayImageWithTRNSChunk.java Wed Apr 18 12:33:21 2018 +0530
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 6788458
+ * @summary Test verifies that PNGImageReader takes tRNS chunk values
+ * into consideration while reading non-indexed Gray PNG images.
+ * @run main ReadPngGrayImageWithTRNSChunk
+ */
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.Color;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriter;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.metadata.IIOInvalidTreeException;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.IIOImage;
+
+public class ReadPngGrayImageWithTRNSChunk {
+
+ private static BufferedImage img;
+ private static ImageWriter writer;
+ private static ImageWriteParam param;
+ private static IIOMetadata metadata;
+ private static byte[] imageByteArray;
+
+ private static void initialize(int type) {
+ int width = 2;
+ int height = 1;
+ img = new BufferedImage(width, height, type);
+ Graphics2D g2D = img.createGraphics();
+
+ // transparent first pixel
+ g2D.setColor(new Color(255, 255, 255));
+ g2D.fillRect(0, 0, 1, 1);
+ // non-transparent second pixel
+ g2D.setColor(new Color(128, 128,128));
+ g2D.fillRect(1, 0, 1, 1);
+ g2D.dispose();
+
+ Iterator<ImageWriter> iterWriter =
+ ImageIO.getImageWritersBySuffix("png");
+ writer = iterWriter.next();
+
+ param = writer.getDefaultWriteParam();
+ ImageTypeSpecifier specifier =
+ ImageTypeSpecifier.
+ createFromBufferedImageType(type);
+ metadata = writer.getDefaultImageMetadata(specifier, param);
+ }
+
+ private static void createTRNSNode(String tRNS_value)
+ throws IIOInvalidTreeException {
+ IIOMetadataNode tRNS_gray = new IIOMetadataNode("tRNS_Grayscale");
+ tRNS_gray.setAttribute("gray", tRNS_value);
+
+ IIOMetadataNode tRNS = new IIOMetadataNode("tRNS");
+ tRNS.appendChild(tRNS_gray);
+ IIOMetadataNode root = new IIOMetadataNode("javax_imageio_png_1.0");
+ root.appendChild(tRNS);
+ metadata.mergeTree("javax_imageio_png_1.0", root);
+ }
+
+ private static void writeImage() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
+ writer.setOutput(ios);
+ writer.write(metadata, new IIOImage(img, null, metadata), param);
+ writer.dispose();
+
+ baos.flush();
+ imageByteArray = baos.toByteArray();
+ baos.close();
+ }
+
+ private static boolean verifyAlphaValue(BufferedImage img) {
+ Color firstPixel = new Color(img.getRGB(0, 0), true);
+ Color secondPixel = new Color(img.getRGB(1, 0), true);
+
+ return firstPixel.getAlpha() != 0 ||
+ secondPixel.getAlpha() != 255;
+ }
+
+ private static boolean read8BitGrayPNGWithTRNSChunk() throws IOException {
+ initialize(BufferedImage.TYPE_BYTE_GRAY);
+ // Create tRNS node and merge it with default metadata
+ createTRNSNode("255");
+
+ writeImage();
+
+ InputStream input= new ByteArrayInputStream(imageByteArray);
+ // Read 8 bit PNG Gray image with tRNS chunk
+ BufferedImage verify_img = ImageIO.read(input);
+ input.close();
+ // Verify alpha values present in first & second pixel
+ return verifyAlphaValue(verify_img);
+ }
+
+ private static boolean read16BitGrayPNGWithTRNSChunk() throws IOException {
+ initialize(BufferedImage.TYPE_USHORT_GRAY);
+ // Create tRNS node and merge it with default metadata
+ createTRNSNode("65535");
+
+ writeImage();
+
+ InputStream input= new ByteArrayInputStream(imageByteArray);
+ // Read 16 bit PNG Gray image with tRNS chunk
+ BufferedImage verify_img = ImageIO.read(input);
+ input.close();
+ // Verify alpha values present in first & second pixel
+ return verifyAlphaValue(verify_img);
+ }
+
+ public static void main(String[] args) throws IOException {
+ boolean read8BitFail, read16BitFail;
+ // read 8 bit PNG Gray image with tRNS chunk
+ read8BitFail = read8BitGrayPNGWithTRNSChunk();
+
+ // read 16 bit PNG Gray image with tRNS chunk
+ read16BitFail = read16BitGrayPNGWithTRNSChunk();
+
+ if (read8BitFail || read16BitFail) {
+ throw new RuntimeException("PNGImageReader is not using" +
+ " transparent pixel information from tRNS chunk properly");
+ }
+ }
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/javax/imageio/plugins/png/ReadPngRGBImageWithTRNSChunk.java Wed Apr 18 12:33:21 2018 +0530
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 6788458
+ * @summary Test verifies that PNGImageReader takes tRNS chunk values
+ * into consideration while reading non-indexed RGB PNG images.
+ * @run main ReadPngRGBImageWithTRNSChunk
+ */
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.Color;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriter;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.metadata.IIOInvalidTreeException;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.IIOImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferUShort;
+import java.awt.image.WritableRaster;
+import java.awt.image.Raster;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.ComponentColorModel;
+import java.awt.Transparency;
+
+public class ReadPngRGBImageWithTRNSChunk {
+
+ private static BufferedImage img;
+ private static IIOMetadata metadata;
+ private static ImageWriteParam param;
+ private static ImageWriter writer;
+ private static byte[] imageByteArray;
+
+ private static void createTRNSNode(String tRNS_value)
+ throws IIOInvalidTreeException {
+ IIOMetadataNode tRNS_rgb = new IIOMetadataNode("tRNS_RGB");
+ tRNS_rgb.setAttribute("red", tRNS_value);
+ tRNS_rgb.setAttribute("green", tRNS_value);
+ tRNS_rgb.setAttribute("blue", tRNS_value);
+
+ IIOMetadataNode tRNS = new IIOMetadataNode("tRNS");
+ tRNS.appendChild(tRNS_rgb);
+ IIOMetadataNode root = new IIOMetadataNode("javax_imageio_png_1.0");
+ root.appendChild(tRNS);
+ metadata.mergeTree("javax_imageio_png_1.0", root);
+ }
+
+ private static void writeImage() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
+ writer.setOutput(ios);
+ writer.write(metadata, new IIOImage(img, null, metadata), param);
+ writer.dispose();
+
+ baos.flush();
+ imageByteArray = baos.toByteArray();
+ baos.close();
+ }
+
+ private static boolean verifyAlphaValue(BufferedImage img) {
+ Color firstPixel = new Color(img.getRGB(0, 0), true);
+ Color secondPixel = new Color(img.getRGB(1, 0), true);
+
+ return firstPixel.getAlpha() != 0 ||
+ secondPixel.getAlpha() != 255;
+ }
+
+ private static boolean read8BitRGBPNGWithTRNSChunk() throws IOException {
+ int width = 2;
+ int height = 1;
+ // Create 8 bit PNG image
+ img = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+ Graphics2D g2D = img.createGraphics();
+
+ // transparent first pixel
+ g2D.setColor(Color.WHITE);
+ g2D.fillRect(0, 0, 1, 1);
+ // non-transparent second pixel
+ g2D.setColor(Color.RED);
+ g2D.fillRect(1, 0, 1, 1);
+ g2D.dispose();
+
+ Iterator<ImageWriter> iterWriter =
+ ImageIO.getImageWritersBySuffix("png");
+ writer = iterWriter.next();
+
+ param = writer.getDefaultWriteParam();
+ ImageTypeSpecifier specifier =
+ ImageTypeSpecifier.
+ createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
+ metadata = writer.getDefaultImageMetadata(specifier, param);
+
+ // Create tRNS node and merge it with default metadata
+ createTRNSNode("255");
+
+ writeImage();
+
+ InputStream input= new ByteArrayInputStream(imageByteArray);
+ // Read 8 bit PNG RGB image with tRNS chunk
+ BufferedImage verify_img = ImageIO.read(input);
+ input.close();
+ // Verify alpha values present in first & second pixel
+ return verifyAlphaValue(verify_img);
+ }
+
+ private static boolean read16BitRGBPNGWithTRNSChunk() throws IOException {
+ // Create 16 bit PNG image
+ int height = 1;
+ int width = 2;
+ int numBands = 3;
+ int shortArrayLength = width * height * numBands;
+ short[] pixelData = new short[shortArrayLength];
+ // transparent first pixel
+ pixelData[0] = (short)0xffff;
+ pixelData[1] = (short)0xffff;
+ pixelData[2] = (short)0xffff;
+ // non-transparent second pixel
+ pixelData[3] = (short)0xffff;
+ pixelData[4] = (short)0xffff;
+ pixelData[5] = (short)0xfffe;
+
+ DataBuffer buffer = new DataBufferUShort(pixelData, shortArrayLength);
+
+ int[] bandOffset = {0, 1 ,2};
+ WritableRaster ras =
+ Raster.createInterleavedRaster(buffer, width, height,
+ width * numBands, numBands, bandOffset, null);
+
+ int nBits[] = {16, 16 ,16};
+ ColorModel colorModel = new
+ ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ nBits, false, false, Transparency.OPAQUE,
+ DataBuffer.TYPE_USHORT);
+ img = new BufferedImage(colorModel, ras, false, null);
+
+ Iterator<ImageWriter> iterWriter =
+ ImageIO.getImageWritersBySuffix("png");
+ writer = iterWriter.next();
+
+ param = writer.getDefaultWriteParam();
+ ImageTypeSpecifier specifier = new ImageTypeSpecifier(img);
+ metadata = writer.getDefaultImageMetadata(specifier, param);
+
+ // Create tRNS node and merge it with default metadata
+ createTRNSNode("65535");
+
+ writeImage();
+
+ InputStream input= new ByteArrayInputStream(imageByteArray);
+ // Read 16 bit PNG RGB image with tRNS chunk
+ BufferedImage verify_img = ImageIO.read(input);
+ input.close();
+ // Verify alpha values present in first & second pixel
+ return verifyAlphaValue(verify_img);
+ }
+
+ public static void main(String[] args) throws IOException {
+ boolean read8BitFail, read16BitFail;
+ // read 8 bit PNG RGB image with tRNS chunk
+ read8BitFail = read8BitRGBPNGWithTRNSChunk();
+
+ // read 16 bit PNG RGB image with tRNS chunk
+ read16BitFail = read16BitRGBPNGWithTRNSChunk();
+
+ if (read8BitFail || read16BitFail) {
+ throw new RuntimeException("PNGImageReader is not using" +
+ " transparent pixel information from tRNS chunk properly");
+ }
+ }
+}
+