8164971: PNG metadata does not handle ImageCreationTime
Reviewed-by: prr, bpb, jdv
Contributed-by: prahalad.kumar.narayanan@oracle.com
--- 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();
+ }
+}