6788458: PNGImageReader ignores tRNS chunk while reading non-indexed RGB/Gray images
authorjdv
Wed, 18 Apr 2018 12:33:21 +0530
changeset 49995 6f595ec05539
parent 49994 0e9be7add10a
child 49996 39dc39093c5e
6788458: PNGImageReader ignores tRNS chunk while reading non-indexed RGB/Gray images Reviewed-by: prr, pnarayanan, kaddepalli
src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java
src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java
test/jdk/javax/imageio/plugins/png/ReadPngGrayImageWithTRNSChunk.java
test/jdk/javax/imageio/plugins/png/ReadPngRGBImageWithTRNSChunk.java
--- 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");
+        }
+    }
+}
+