8164971: PNG metadata does not handle ImageCreationTime
authorpnarayanan
Fri, 01 Sep 2017 12:32:20 +0530
changeset 47197 a7033867ee20
parent 47196 a3211bb4daff
child 47198 898607275d6e
8164971: PNG metadata does not handle ImageCreationTime Reviewed-by: prr, bpb, jdv Contributed-by: prahalad.kumar.narayanan@oracle.com
jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java
jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java
jdk/test/javax/imageio/plugins/png/PngCreationTimeTest.java
--- a/jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java	Thu Aug 31 15:47:34 2017 -0700
+++ b/jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java	Fri Sep 01 12:32:20 2017 +0530
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2017, 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
@@ -472,6 +472,14 @@
             text = new String(b, "UTF8");
         }
         metadata.iTXt_text.add(text);
+
+        // Check if the text chunk contains image creation time
+        if (keyword.equals(PNGMetadata.tEXt_creationTimeKey)) {
+            // Update Standard/Document/ImageCreationTime from text chunk
+            int index = metadata.iTXt_text.size() - 1;
+            metadata.decodeImageCreationTimeFromTextChunk(
+                    metadata.iTXt_text.listIterator(index));
+        }
     }
 
     private void parse_pHYs_chunk() throws IOException {
@@ -555,6 +563,14 @@
         byte[] b = new byte[chunkLength - keyword.length() - 1];
         stream.readFully(b);
         metadata.tEXt_text.add(new String(b, "ISO-8859-1"));
+
+        // Check if the text chunk contains image creation time
+        if (keyword.equals(PNGMetadata.tEXt_creationTimeKey)) {
+            // Update Standard/Document/ImageCreationTime from text chunk
+            int index = metadata.tEXt_text.size() - 1;
+            metadata.decodeImageCreationTimeFromTextChunk(
+                    metadata.tEXt_text.listIterator(index));
+        }
     }
 
     private void parse_tIME_chunk() throws IOException {
@@ -644,6 +660,14 @@
         byte[] b = new byte[chunkLength - keyword.length() - 2];
         stream.readFully(b);
         metadata.zTXt_text.add(new String(inflate(b), "ISO-8859-1"));
+
+        // Check if the text chunk contains image creation time
+        if (keyword.equals(PNGMetadata.tEXt_creationTimeKey)) {
+            // Update Standard/Document/ImageCreationTime from text chunk
+            int index = metadata.zTXt_text.size() - 1;
+            metadata.decodeImageCreationTimeFromTextChunk(
+                    metadata.zTXt_text.listIterator(index));
+        }
     }
 
     private void readMetadata() throws IIOException {
@@ -713,8 +737,32 @@
                 switch (chunkType) {
                 case IDAT_TYPE:
                     // If chunk type is 'IDAT', we've reached the image data.
-                    stream.skipBytes(-8);
-                    imageStartPosition = stream.getStreamPosition();
+                    if (imageStartPosition == -1L) {
+                        /*
+                         * PNGs may contain multiple IDAT chunks containing
+                         * a portion of image data. We store the position of
+                         * the first IDAT chunk and continue with iteration
+                         * of other chunks that follow image data.
+                         */
+                        imageStartPosition = stream.getStreamPosition() - 8;
+                    }
+                    // Move to the CRC byte location.
+                    stream.skipBytes(chunkLength);
+                    break;
+                case IEND_TYPE:
+                    /*
+                     * If the chunk type is 'IEND', we've reached end of image.
+                     * Seek to the first IDAT chunk for subsequent decoding.
+                     */
+                    stream.seek(imageStartPosition);
+
+                    /*
+                     * flushBefore discards the portion of the stream before
+                     * the indicated position. Hence this should be used after
+                     * we complete iteration over available chunks including
+                     * those that appear after the IDAT.
+                     */
+                    stream.flushBefore(stream.getStreamPosition());
                     break loop;
                 case PLTE_TYPE:
                     parse_PLTE_chunk(chunkLength);
@@ -796,7 +844,6 @@
                     throw new IIOException("Failed to read a chunk of type " +
                             chunkType);
                 }
-                stream.flushBefore(stream.getStreamPosition());
             }
         } catch (IOException e) {
             throw new IIOException("Error reading PNG metadata", e);
--- a/jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java	Thu Aug 31 15:47:34 2017 -0700
+++ b/jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java	Fri Sep 01 12:32:20 2017 +0530
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2017, 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
@@ -30,6 +30,14 @@
 import java.awt.image.SampleModel;
 import java.util.ArrayList;
 import java.util.StringTokenizer;
+import java.util.ListIterator;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.TemporalAccessor;
 import javax.imageio.ImageTypeSpecifier;
 import javax.imageio.metadata.IIOInvalidTreeException;
 import javax.imageio.metadata.IIOMetadata;
@@ -212,7 +220,7 @@
     public ArrayList<String> tEXt_keyword = new ArrayList<String>(); // 1-79 characters
     public ArrayList<String> tEXt_text = new ArrayList<String>();
 
-    // tIME chunk
+    // tIME chunk. Gives the image modification time.
     public boolean tIME_present;
     public int tIME_year;
     public int tIME_month;
@@ -221,6 +229,41 @@
     public int tIME_minute;
     public int tIME_second;
 
+    // Specifies whether metadata contains Standard/Document/ImageCreationTime
+    public boolean creation_time_present;
+
+    // Values that make up Standard/Document/ImageCreationTime
+    public int creation_time_year;
+    public int creation_time_month;
+    public int creation_time_day;
+    public int creation_time_hour;
+    public int creation_time_minute;
+    public int creation_time_second;
+    public ZoneOffset creation_time_offset;
+
+    /*
+     * tEXt_creation_time_present- Specifies whether any text chunk (tEXt, iTXt,
+     * zTXt) exists with image creation time. The data structure corresponding
+     * to the last decoded text chunk with creation time is indicated by the
+     * iterator- tEXt_creation_time_iter.
+     *
+     * Any update to the text chunks with creation time is reflected on
+     * Standard/Document/ImageCreationTime after retrieving time from the text
+     * chunk. If there are multiple text chunks with creation time, the time
+     * retrieved from the last decoded text chunk will be used. A point to note
+     * is that, retrieval of time from text chunks is possible only if the
+     * encoded time in the chunk confirms to either the recommended RFC1123
+     * format or ISO format.
+     *
+     * Similarly, any update to Standard/Document/ImageCreationTime is reflected
+     * on the last decoded text chunk's data structure with time encoded in
+     * RFC1123 format. By updating the text chunk's data structure, we also
+     * ensure that PNGImageWriter will write image creation time on the output.
+     */
+    public boolean tEXt_creation_time_present;
+    private ListIterator<String> tEXt_creation_time_iter = null;
+    public static final String tEXt_creationTimeKey = "Creation Time";
+
     // tRNS chunk
     // If external (non-PNG sourced) data has red = green = blue,
     // always store it as gray and promote when writing
@@ -985,21 +1028,41 @@
     }
 
     public IIOMetadataNode getStandardDocumentNode() {
-        if (!tIME_present) {
-            return null;
+        IIOMetadataNode document_node = null;
+
+        // Check if image modification time exists
+        if (tIME_present) {
+            // Create new document node
+            document_node = new IIOMetadataNode("Document");
+
+            // Node to hold image modification time
+            IIOMetadataNode node = new IIOMetadataNode("ImageModificationTime");
+            node.setAttribute("year", Integer.toString(tIME_year));
+            node.setAttribute("month", Integer.toString(tIME_month));
+            node.setAttribute("day", Integer.toString(tIME_day));
+            node.setAttribute("hour", Integer.toString(tIME_hour));
+            node.setAttribute("minute", Integer.toString(tIME_minute));
+            node.setAttribute("second", Integer.toString(tIME_second));
+            document_node.appendChild(node);
         }
 
-        IIOMetadataNode document_node = new IIOMetadataNode("Document");
-        IIOMetadataNode node = null; // scratch node
+        // Check if image creation time exists
+        if (creation_time_present) {
+            if (document_node == null) {
+                // Create new document node
+                document_node = new IIOMetadataNode("Document");
+            }
 
-        node = new IIOMetadataNode("ImageModificationTime");
-        node.setAttribute("year", Integer.toString(tIME_year));
-        node.setAttribute("month", Integer.toString(tIME_month));
-        node.setAttribute("day", Integer.toString(tIME_day));
-        node.setAttribute("hour", Integer.toString(tIME_hour));
-        node.setAttribute("minute", Integer.toString(tIME_minute));
-        node.setAttribute("second", Integer.toString(tIME_second));
-        document_node.appendChild(node);
+            // Node to hold image creation time
+            IIOMetadataNode node = new IIOMetadataNode("ImageCreationTime");
+            node.setAttribute("year", Integer.toString(creation_time_year));
+            node.setAttribute("month", Integer.toString(creation_time_month));
+            node.setAttribute("day", Integer.toString(creation_time_day));
+            node.setAttribute("hour", Integer.toString(creation_time_hour));
+            node.setAttribute("minute", Integer.toString(creation_time_minute));
+            node.setAttribute("second", Integer.toString(creation_time_second));
+            document_node.appendChild(node);
+        }
 
         return document_node;
     }
@@ -1437,6 +1500,13 @@
                         String text = getAttribute(iTXt_node, "text");
                         iTXt_text.add(text);
 
+                        // Check if the text chunk contains image creation time
+                        if (keyword.equals(PNGMetadata.tEXt_creationTimeKey)) {
+                            // Update Standard/Document/ImageCreationTime
+                            int index = iTXt_text.size()-1;
+                            decodeImageCreationTimeFromTextChunk(
+                                    iTXt_text.listIterator(index));
+                        }
                     }
                     // silently skip invalid text entry
 
@@ -1564,6 +1634,13 @@
                     String text = getAttribute(tEXt_node, "value");
                     tEXt_text.add(text);
 
+                    // Check if the text chunk contains image creation time
+                    if (keyword.equals(PNGMetadata.tEXt_creationTimeKey)) {
+                        // Update Standard/Document/ImageCreationTime
+                        int index = tEXt_text.size()-1;
+                        decodeImageCreationTimeFromTextChunk(
+                                tEXt_text.listIterator(index));
+                    }
                     tEXt_node = tEXt_node.getNextSibling();
                 }
             } else if (name.equals("tIME")) {
@@ -1652,6 +1729,13 @@
                     String text = getAttribute(zTXt_node, "text");
                     zTXt_text.add(text);
 
+                    // Check if the text chunk contains image creation time
+                    if (keyword.equals(PNGMetadata.tEXt_creationTimeKey)) {
+                        // Update Standard/Document/ImageCreationTime
+                        int index = zTXt_text.size()-1;
+                        decodeImageCreationTimeFromTextChunk(
+                                zTXt_text.listIterator(index));
+                    }
                     zTXt_node = zTXt_node.getNextSibling();
                 }
             } else if (name.equals("UnknownChunks")) {
@@ -1952,7 +2036,22 @@
                         tIME_second =
                             getIntAttribute(child, "second", 0, false);
 //                  } else if (childName.equals("SubimageInterpretation")) {
-//                  } else if (childName.equals("ImageCreationTime")) {
+                    } else if (childName.equals("ImageCreationTime")) {
+                        // Extract the creation time values
+                        int year  = getIntAttribute(child, "year");
+                        int month = getIntAttribute(child, "month");
+                        int day   = getIntAttribute(child, "day");
+                        int hour  = getIntAttribute(child, "hour", 0, false);
+                        int mins  = getIntAttribute(child, "minute", 0, false);
+                        int sec   = getIntAttribute(child, "second", 0, false);
+
+                        /*
+                         * Update Standard/Document/ImageCreationTime and encode
+                         * the same in the last decoded text chunk with creation
+                         * time
+                         */
+                        initImageCreationTime(year, month, day, hour, mins, sec);
+                        encodeImageCreationTimeToTextChunk();
                     }
                     child = child.getNextSibling();
                 }
@@ -2014,6 +2113,152 @@
         }
     }
 
+    void initImageCreationTime(OffsetDateTime offsetDateTime) {
+        // Check for incoming arguments
+        if (offsetDateTime != null) {
+            // set values that make up Standard/Document/ImageCreationTime
+            creation_time_present = true;
+            creation_time_year    = offsetDateTime.getYear();
+            creation_time_month   = offsetDateTime.getMonthValue();
+            creation_time_day     = offsetDateTime.getDayOfMonth();
+            creation_time_hour    = offsetDateTime.getHour();
+            creation_time_minute  = offsetDateTime.getMinute();
+            creation_time_second  = offsetDateTime.getSecond();
+            creation_time_offset  = offsetDateTime.getOffset();
+        }
+    }
+
+    void initImageCreationTime(int year, int month, int day,
+            int hour, int min,int second) {
+        /*
+         * Though LocalDateTime suffices the need to store Standard/Document/
+         * ImageCreationTime, we require the zone offset to encode the same
+         * in the text chunk based on RFC1123 format.
+         */
+        LocalDateTime locDT = LocalDateTime.of(year, month, day, hour, min, second);
+        ZoneOffset offset = ZoneId.systemDefault()
+                                  .getRules()
+                                  .getOffset(locDT);
+        OffsetDateTime offDateTime = OffsetDateTime.of(locDT,offset);
+        initImageCreationTime(offDateTime);
+    }
+
+    void decodeImageCreationTimeFromTextChunk(ListIterator<String> iterChunk) {
+        // Check for incoming arguments
+        if (iterChunk != null && iterChunk.hasNext()) {
+            /*
+             * Save the iterator to mark the last decoded text chunk with
+             * creation time. The contents of this chunk will be updated when
+             * user provides creation time by merging a standard tree with
+             * Standard/Document/ImageCreationTime.
+             */
+            setCreationTimeChunk(iterChunk);
+
+            // Parse encoded time and set Standard/Document/ImageCreationTime.
+            String encodedTime = getEncodedTime();
+            initImageCreationTime(parseEncodedTime(encodedTime));
+        }
+    }
+
+    void encodeImageCreationTimeToTextChunk() {
+        // Check if Standard/Document/ImageCreationTime exists.
+        if (creation_time_present) {
+            // Check if a text chunk with creation time exists.
+            if (tEXt_creation_time_present == false) {
+                // No text chunk exists with image creation time. Add an entry.
+                this.tEXt_keyword.add(tEXt_creationTimeKey);
+                this.tEXt_text.add("Creation Time Place Holder");
+
+                // Update the iterator
+                int index = tEXt_text.size() - 1;
+                setCreationTimeChunk(tEXt_text.listIterator(index));
+            }
+
+            // Encode image creation time with RFC1123 formatter
+            OffsetDateTime offDateTime = OffsetDateTime.of(creation_time_year,
+                    creation_time_month, creation_time_day,
+                    creation_time_hour, creation_time_minute,
+                    creation_time_second, 0, creation_time_offset);
+            DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME;
+            String encodedTime = offDateTime.format(formatter);
+            setEncodedTime(encodedTime);
+        }
+    }
+
+    private void setCreationTimeChunk(ListIterator<String> iter) {
+        // Check for iterator's valid state
+        if (iter != null && iter.hasNext()) {
+            tEXt_creation_time_iter = iter;
+            tEXt_creation_time_present = true;
+        }
+    }
+
+    private void setEncodedTime(String encodedTime) {
+        if (tEXt_creation_time_iter != null
+                && tEXt_creation_time_iter.hasNext()
+                && encodedTime != null) {
+            // Set the value at the iterator and reset its state
+            tEXt_creation_time_iter.next();
+            tEXt_creation_time_iter.set(encodedTime);
+            tEXt_creation_time_iter.previous();
+        }
+    }
+
+    private String getEncodedTime() {
+        String encodedTime = null;
+        if (tEXt_creation_time_iter != null
+                && tEXt_creation_time_iter.hasNext()) {
+            // Get the value at iterator and reset its state
+            encodedTime = tEXt_creation_time_iter.next();
+            tEXt_creation_time_iter.previous();
+        }
+        return encodedTime;
+    }
+
+    private OffsetDateTime parseEncodedTime(String encodedTime) {
+        OffsetDateTime retVal = null;
+        boolean timeDecoded = false;
+
+        /*
+         * PNG specification recommends that image encoders use RFC1123 format
+         * to represent time in String but doesn't mandate. Encoders could
+         * use any convenient format. Hence, we extract time provided the
+         * encoded time complies with either RFC1123 or ISO standards.
+         */
+        try {
+            // Check if the encoded time complies with RFC1123
+            retVal = OffsetDateTime.parse(encodedTime,
+                                          DateTimeFormatter.RFC_1123_DATE_TIME);
+            timeDecoded = true;
+        } catch (DateTimeParseException exception) {
+            // No Op. Encoded time did not comply with RFC1123 standard.
+        }
+
+        if (timeDecoded == false) {
+            try {
+                // Check if the encoded time complies with ISO standard.
+                DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
+                TemporalAccessor dt = formatter.parseBest(encodedTime,
+                        OffsetDateTime::from, LocalDateTime::from);
+
+                if (dt instanceof OffsetDateTime) {
+                    // Encoded time contains date time and zone offset
+                    retVal = (OffsetDateTime) dt;
+                } else if (dt instanceof LocalDateTime) {
+                    /*
+                     * Encoded time contains only date and time. Since zone
+                     * offset information isn't available, we set to the default
+                     */
+                    LocalDateTime locDT = (LocalDateTime) dt;
+                    retVal = OffsetDateTime.of(locDT, ZoneOffset.UTC);
+                }
+            }  catch (DateTimeParseException exception) {
+                // No Op. Encoded time did not comply with ISO standard.
+            }
+        }
+        return retVal;
+    }
+
     // Reset all instance variables to their initial state
     public void reset() {
         IHDR_present = false;
@@ -2035,7 +2280,12 @@
         sRGB_present = false;
         tEXt_keyword = new ArrayList<String>();
         tEXt_text = new ArrayList<String>();
+        // tIME chunk with Image modification time
         tIME_present = false;
+        // Text chunk with Image creation time
+        tEXt_creation_time_present = false;
+        tEXt_creation_time_iter = null;
+        creation_time_present = false;
         tRNS_present = false;
         zTXt_keyword = new ArrayList<String>();
         zTXt_compressionMethod = new ArrayList<Integer>();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/imageio/plugins/png/PngCreationTimeTest.java	Fri Sep 01 12:32:20 2017 +0530
@@ -0,0 +1,370 @@
+/*
+ * Copyright (c) 2017, 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 8164971
+ * @summary The test decodes a png file and checks if the metadata contains
+ *          image creation time. In addition, the test also merges the custom
+ *          metadata tree (both standard and native) and succeeds when the
+ *          metadata contains expected image creation time.
+ * @run main PngCreationTimeTest
+ */
+import java.io.IOException;
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Iterator;
+import java.awt.Graphics2D;
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import javax.imageio.ImageIO;
+import javax.imageio.IIOImage;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import org.w3c.dom.Node;
+import org.w3c.dom.NamedNodeMap;
+
+public class PngCreationTimeTest {
+    // Members
+    private static IIOMetadata pngMetadata = null;
+
+    public static void initializeTest() throws IOException {
+        ImageReader pngImageReader = null;
+        BufferedImage decImage = null;
+        ImageInputStream imageStream = null;
+        String fileName = "duke.png";
+        String separator = System.getProperty("file.separator");
+        String dirPath = System.getProperty("test.src", ".");
+        String filePath = dirPath + separator + fileName;
+        File file = new File(filePath);
+
+        try {
+            Iterator<ImageReader> iterR = ImageIO.getImageReadersBySuffix("PNG");
+            if (iterR.hasNext()) {
+                pngImageReader = iterR.next();
+                ImageReadParam param = pngImageReader.getDefaultReadParam();
+                imageStream = ImageIO.createImageInputStream(file);
+
+                /*
+                 * Last argument- false, informs reader not to ignore the
+                 * metadata from the image file
+                 */
+                pngImageReader.setInput(imageStream, false, false);
+                decImage = pngImageReader.read(0, param);
+                pngMetadata = pngImageReader.getImageMetadata(0);
+
+                // Check if the metadata contains creation time
+                testImageMetadata(pngMetadata);
+            }
+        } finally {
+            // Release ther resources
+            if (imageStream != null) {
+                imageStream.close();
+            }
+            if (pngImageReader != null) {
+                pngImageReader.dispose();
+            }
+        }
+    }
+
+    public static void testImageMetadata(IIOMetadata metadata) {
+        /*
+         * The source file contains Creation Time in its text chunk. Upon
+         * successful decoding, the Standard/Document/ImageCreationTime
+         * should exist in the metadata.
+         */
+        if (metadata != null) {
+            Node keyNode = findNode(metadata.getAsTree("javax_imageio_1.0"),
+                    "ImageCreationTime");
+            if (keyNode == null) {
+                throw new RuntimeException("Test Failed: Reader could not"
+                        + " find creation time in the metadata");
+            }
+        }
+    }
+
+    public static void testSaveCreationTime() throws IOException {
+        File file = null;
+        ImageWriter pngImageWriter = null;
+        ImageReader pngImageReader = null;
+        ImageInputStream inputStream = null;
+        ImageOutputStream outputStream = null;
+        try {
+            // Create a simple image and fill with a color
+            int imageSize = 200;
+            BufferedImage buffImage = new BufferedImage(imageSize, imageSize,
+                    BufferedImage.TYPE_INT_ARGB);
+            Graphics2D g2d = buffImage.createGraphics();
+            g2d.setColor(Color.red);
+            g2d.fillRect(0, 0, imageSize, imageSize);
+
+            // Create a temporary file for the output png image
+            String fileName = "RoundTripTest";
+            file = File.createTempFile(fileName, ".png");
+
+            // Create a PNG writer and write test image with metadata
+            Iterator<ImageWriter> iterW = ImageIO.getImageWritersBySuffix("PNG");
+            if (iterW.hasNext()) {
+                pngImageWriter = iterW.next();
+                outputStream = ImageIO.createImageOutputStream(file);
+                pngImageWriter.setOutput(outputStream);
+
+                // Get the default metadata & add image creation time to it.
+                IIOMetadata metadata = pngImageWriter.getDefaultImageMetadata(
+                        ImageTypeSpecifier.createFromRenderedImage(buffImage),
+                        null);
+                IIOMetadataNode root = createStandardMetadataNodeTree();
+                metadata.mergeTree("javax_imageio_1.0", root);
+
+                // Write a png image using buffImage & metadata
+                IIOImage iioImage = new IIOImage(buffImage, null, metadata);
+                pngImageWriter.write(iioImage);
+            }
+
+            // Create a PNG reader and check if metadata was written
+            Iterator<ImageReader> iterR = ImageIO.getImageReadersBySuffix("PNG");
+            if (iterR.hasNext()) {
+                pngImageReader = iterR.next();
+                inputStream = ImageIO.createImageInputStream(file);
+
+                // Read the image and get the metadata
+                pngImageReader.setInput(inputStream, false, false);
+                pngImageReader.read(0);
+                IIOMetadata imgMetadata = pngImageReader.getImageMetadata(0);
+
+                // Test if the metadata contains creation time.
+                testImageMetadata(imgMetadata);
+            }
+        } finally {
+            // Release the resources held
+            if (inputStream != null) {
+                inputStream.close();
+            }
+            if (outputStream != null) {
+                outputStream.close();
+            }
+            if (pngImageWriter != null) {
+                pngImageWriter.dispose();
+            }
+            if (pngImageReader != null) {
+                pngImageReader.dispose();
+            }
+            // Delete the temp file as well
+            if (file != null) {
+                Files.delete(file.toPath());
+            }
+        }
+    }
+
+    public static void testMergeNativeTree() {
+        // Merge a custom native metadata tree and inspect creation time
+        if (pngMetadata != null) {
+            try {
+                IIOMetadataNode root = createNativeMetadataNodeTree();
+
+                /*
+                 * Merge the native metadata tree created. The data should
+                 * reflect in Standard/Document/ImageCreationTime Node
+                 */
+                pngMetadata.mergeTree("javax_imageio_png_1.0", root);
+                Node keyNode = findNode(pngMetadata.getAsTree("javax_imageio_1.0"),
+                        "ImageCreationTime");
+
+                if (keyNode != null) {
+                    // Query the attributes of the node and check for the value
+                    NamedNodeMap attrMap = keyNode.getAttributes();
+                    String attrValue = attrMap.getNamedItem("year")
+                                              .getNodeValue();
+                    int decYear = Integer.parseInt(attrValue);
+                    if (decYear != 2014) {
+                        // Throw exception. Incorrect year value observed
+                        throw new RuntimeException("Test Failed: Incorrect"
+                                + " creation time value observed");
+                    }
+                } else {
+                    // Throw exception.
+                    throw new RuntimeException("Test Failed: Image creation"
+                            + "time doesn't exist in metadata");
+                }
+            } catch (IOException ex) {
+                // Throw exception.
+                throw new RuntimeException("Test Failed: While executing"
+                        + " mergeTree on metadata.");
+            }
+        }
+    }
+
+    public static void testMergeStandardTree() {
+        // Merge a standard metadata tree and inspect creation time
+        if (pngMetadata != null) {
+            try {
+                IIOMetadataNode root = createStandardMetadataNodeTree();
+
+                /*
+                 * Merge the standard metadata tree created. The data should
+                 * correctly reflect in the native tree
+                 */
+                pngMetadata.mergeTree("javax_imageio_1.0", root);
+                Node keyNode = findNode(pngMetadata.getAsTree("javax_imageio_png_1.0"),
+                        "tEXtEntry");
+                // Last text entry would contain the merged information
+                while (keyNode != null && keyNode.getNextSibling() != null) {
+                    keyNode = keyNode.getNextSibling();
+                }
+
+                if (keyNode != null) {
+                    // Query the attributes of the node and check for the value
+                    NamedNodeMap attrMap = keyNode.getAttributes();
+                    String attrValue = attrMap.getNamedItem("value")
+                                              .getNodeValue();
+                    if (!attrValue.contains("2016")) {
+                        // Throw exception. Incorrect year value observed
+                        throw new RuntimeException("Test Failed: Incorrect"
+                                + " creation time value observed");
+                    }
+                } else {
+                    // Throw exception.
+                    throw new RuntimeException("Test Failed: Image creation"
+                            + "time doesn't exist in metadata");
+                }
+            } catch (IOException ex) {
+                // Throw exception.
+                throw new RuntimeException("Test Failed: While executing"
+                        + " mergeTree on metadata.");
+            }
+        }
+    }
+
+    public static IIOMetadataNode createNativeMetadataNodeTree() {
+        // Create a text node to hold tEXtEntries
+        IIOMetadataNode tEXtNode = new IIOMetadataNode("tEXt");
+
+        // Create tEXt entry to hold random date time
+        IIOMetadataNode randomTimeEntry = new IIOMetadataNode("tEXtEntry");
+        randomTimeEntry.setAttribute("keyword", "Creation Time");
+        randomTimeEntry.setAttribute("value", "21 Dec 2015,Monday");
+        tEXtNode.appendChild(randomTimeEntry);
+
+        // Create a tEXt entry to hold time in RFC1123 format
+        IIOMetadataNode rfcTextEntry = new IIOMetadataNode("tEXtEntry");
+        rfcTextEntry.setAttribute("keyword", "Creation Time");
+        rfcTextEntry.setAttribute("value", "Mon, 21 Dec 2015 09:04:30 +0530");
+        tEXtNode.appendChild(rfcTextEntry);
+
+        // Create a tEXt entry to hold time in ISO format
+        IIOMetadataNode isoTextEntry = new IIOMetadataNode("tEXtEntry");
+        isoTextEntry.setAttribute("keyword", "Creation Time");
+        isoTextEntry.setAttribute("value", "2014-12-21T09:04:30+05:30");
+        tEXtNode.appendChild(isoTextEntry);
+
+        // Create a root node append the text node
+        IIOMetadataNode root = new IIOMetadataNode("javax_imageio_png_1.0");
+        root.appendChild(tEXtNode);
+
+        return root;
+    }
+
+    public static IIOMetadataNode createStandardMetadataNodeTree() {
+        /*
+         * Create standard metadata tree with creation time in
+         * Standard(Root)/Document/ImageCreationTime node
+         */
+        IIOMetadataNode createTimeNode = new IIOMetadataNode("ImageCreationTime");
+        createTimeNode.setAttribute("year", "2016");
+        createTimeNode.setAttribute("month", "12");
+        createTimeNode.setAttribute("day", "21");
+        createTimeNode.setAttribute("hour", "18");
+        createTimeNode.setAttribute("minute", "30");
+        createTimeNode.setAttribute("second", "00");
+
+        // Create the Document node
+        IIOMetadataNode documentNode = new IIOMetadataNode("Document");
+        documentNode.appendChild(createTimeNode);
+
+        // Create a root node append the Document node
+        IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
+        root.appendChild(documentNode);
+
+        return root;
+    }
+
+    public static Node findNode(Node root, String nodeName) {
+        // Return value
+        Node retVal = null;
+
+        if (root != null ) {
+            // Check if the name of root node matches the key
+            String name = root.getNodeName();
+            if (name.equalsIgnoreCase(nodeName)) {
+                return root;
+            }
+
+            // Process all children
+            Node node = root.getFirstChild();
+            while (node != null) {
+                retVal = findNode(node, nodeName);
+                if (retVal != null ) {
+                    // We found the node. Stop the search
+                    break;
+                }
+                node = node.getNextSibling();
+            }
+       }
+
+        return retVal;
+    }
+
+    public static void main(String[] args) throws IOException {
+        /*
+         * Initialize the test by decoding a PNG image that has creation
+         * time in one of its text chunks and check if the metadata returned
+         * contains image creation time.
+         */
+        initializeTest();
+
+        /*
+         * Test the round trip usecase. Write a PNG file with "Creation Time"
+         * in text chunk and decode the same to check if the creation time
+         * was indeed written to the PNG file.
+         */
+        testSaveCreationTime();
+
+        /*
+         * Modify the metadata by merging a standard metadata tree and inspect
+         * the value in the native tree
+         */
+        testMergeNativeTree();
+
+        /*
+         * Modify the metadata by merging a native metadata tree and inspect
+         * the value in the standard tree.
+         */
+        testMergeStandardTree();
+    }
+}