8164971: PNG metadata does not handle ImageCreationTime
Reviewed-by: prr, bpb, jdv
This commit is contained in:
parent
44875d5710
commit
6c012b6381
@ -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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -472,6 +472,14 @@ public class PNGImageReader extends ImageReader {
|
|||||||
text = new String(b, "UTF8");
|
text = new String(b, "UTF8");
|
||||||
}
|
}
|
||||||
metadata.iTXt_text.add(text);
|
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 {
|
private void parse_pHYs_chunk() throws IOException {
|
||||||
@ -555,6 +563,14 @@ public class PNGImageReader extends ImageReader {
|
|||||||
byte[] b = new byte[chunkLength - keyword.length() - 1];
|
byte[] b = new byte[chunkLength - keyword.length() - 1];
|
||||||
stream.readFully(b);
|
stream.readFully(b);
|
||||||
metadata.tEXt_text.add(new String(b, "ISO-8859-1"));
|
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 {
|
private void parse_tIME_chunk() throws IOException {
|
||||||
@ -644,6 +660,14 @@ public class PNGImageReader extends ImageReader {
|
|||||||
byte[] b = new byte[chunkLength - keyword.length() - 2];
|
byte[] b = new byte[chunkLength - keyword.length() - 2];
|
||||||
stream.readFully(b);
|
stream.readFully(b);
|
||||||
metadata.zTXt_text.add(new String(inflate(b), "ISO-8859-1"));
|
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 {
|
private void readMetadata() throws IIOException {
|
||||||
@ -713,8 +737,32 @@ public class PNGImageReader extends ImageReader {
|
|||||||
switch (chunkType) {
|
switch (chunkType) {
|
||||||
case IDAT_TYPE:
|
case IDAT_TYPE:
|
||||||
// If chunk type is 'IDAT', we've reached the image data.
|
// If chunk type is 'IDAT', we've reached the image data.
|
||||||
stream.skipBytes(-8);
|
if (imageStartPosition == -1L) {
|
||||||
imageStartPosition = stream.getStreamPosition();
|
/*
|
||||||
|
* 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;
|
break loop;
|
||||||
case PLTE_TYPE:
|
case PLTE_TYPE:
|
||||||
parse_PLTE_chunk(chunkLength);
|
parse_PLTE_chunk(chunkLength);
|
||||||
@ -796,7 +844,6 @@ public class PNGImageReader extends ImageReader {
|
|||||||
throw new IIOException("Failed to read a chunk of type " +
|
throw new IIOException("Failed to read a chunk of type " +
|
||||||
chunkType);
|
chunkType);
|
||||||
}
|
}
|
||||||
stream.flushBefore(stream.getStreamPosition());
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IIOException("Error reading PNG metadata", e);
|
throw new IIOException("Error reading PNG metadata", e);
|
||||||
|
@ -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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -30,6 +30,14 @@ import java.awt.image.IndexColorModel;
|
|||||||
import java.awt.image.SampleModel;
|
import java.awt.image.SampleModel;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.StringTokenizer;
|
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.ImageTypeSpecifier;
|
||||||
import javax.imageio.metadata.IIOInvalidTreeException;
|
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||||
import javax.imageio.metadata.IIOMetadata;
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
@ -212,7 +220,7 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
public ArrayList<String> tEXt_keyword = new ArrayList<String>(); // 1-79 characters
|
public ArrayList<String> tEXt_keyword = new ArrayList<String>(); // 1-79 characters
|
||||||
public ArrayList<String> tEXt_text = new ArrayList<String>();
|
public ArrayList<String> tEXt_text = new ArrayList<String>();
|
||||||
|
|
||||||
// tIME chunk
|
// tIME chunk. Gives the image modification time.
|
||||||
public boolean tIME_present;
|
public boolean tIME_present;
|
||||||
public int tIME_year;
|
public int tIME_year;
|
||||||
public int tIME_month;
|
public int tIME_month;
|
||||||
@ -221,6 +229,41 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
public int tIME_minute;
|
public int tIME_minute;
|
||||||
public int tIME_second;
|
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
|
// tRNS chunk
|
||||||
// If external (non-PNG sourced) data has red = green = blue,
|
// If external (non-PNG sourced) data has red = green = blue,
|
||||||
// always store it as gray and promote when writing
|
// always store it as gray and promote when writing
|
||||||
@ -985,21 +1028,41 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IIOMetadataNode getStandardDocumentNode() {
|
public IIOMetadataNode getStandardDocumentNode() {
|
||||||
if (!tIME_present) {
|
IIOMetadataNode document_node = null;
|
||||||
return 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");
|
// Check if image creation time exists
|
||||||
IIOMetadataNode node = null; // scratch node
|
if (creation_time_present) {
|
||||||
|
if (document_node == null) {
|
||||||
|
// Create new document node
|
||||||
|
document_node = new IIOMetadataNode("Document");
|
||||||
|
}
|
||||||
|
|
||||||
node = new IIOMetadataNode("ImageModificationTime");
|
// Node to hold image creation time
|
||||||
node.setAttribute("year", Integer.toString(tIME_year));
|
IIOMetadataNode node = new IIOMetadataNode("ImageCreationTime");
|
||||||
node.setAttribute("month", Integer.toString(tIME_month));
|
node.setAttribute("year", Integer.toString(creation_time_year));
|
||||||
node.setAttribute("day", Integer.toString(tIME_day));
|
node.setAttribute("month", Integer.toString(creation_time_month));
|
||||||
node.setAttribute("hour", Integer.toString(tIME_hour));
|
node.setAttribute("day", Integer.toString(creation_time_day));
|
||||||
node.setAttribute("minute", Integer.toString(tIME_minute));
|
node.setAttribute("hour", Integer.toString(creation_time_hour));
|
||||||
node.setAttribute("second", Integer.toString(tIME_second));
|
node.setAttribute("minute", Integer.toString(creation_time_minute));
|
||||||
document_node.appendChild(node);
|
node.setAttribute("second", Integer.toString(creation_time_second));
|
||||||
|
document_node.appendChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
return document_node;
|
return document_node;
|
||||||
}
|
}
|
||||||
@ -1437,6 +1500,13 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
String text = getAttribute(iTXt_node, "text");
|
String text = getAttribute(iTXt_node, "text");
|
||||||
iTXt_text.add(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
|
// silently skip invalid text entry
|
||||||
|
|
||||||
@ -1564,6 +1634,13 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
String text = getAttribute(tEXt_node, "value");
|
String text = getAttribute(tEXt_node, "value");
|
||||||
tEXt_text.add(text);
|
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();
|
tEXt_node = tEXt_node.getNextSibling();
|
||||||
}
|
}
|
||||||
} else if (name.equals("tIME")) {
|
} else if (name.equals("tIME")) {
|
||||||
@ -1652,6 +1729,13 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
String text = getAttribute(zTXt_node, "text");
|
String text = getAttribute(zTXt_node, "text");
|
||||||
zTXt_text.add(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();
|
zTXt_node = zTXt_node.getNextSibling();
|
||||||
}
|
}
|
||||||
} else if (name.equals("UnknownChunks")) {
|
} else if (name.equals("UnknownChunks")) {
|
||||||
@ -1952,7 +2036,22 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
tIME_second =
|
tIME_second =
|
||||||
getIntAttribute(child, "second", 0, false);
|
getIntAttribute(child, "second", 0, false);
|
||||||
// } else if (childName.equals("SubimageInterpretation")) {
|
// } 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();
|
child = child.getNextSibling();
|
||||||
}
|
}
|
||||||
@ -2014,6 +2113,152 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Reset all instance variables to their initial state
|
||||||
public void reset() {
|
public void reset() {
|
||||||
IHDR_present = false;
|
IHDR_present = false;
|
||||||
@ -2035,7 +2280,12 @@ public class PNGMetadata extends IIOMetadata implements Cloneable {
|
|||||||
sRGB_present = false;
|
sRGB_present = false;
|
||||||
tEXt_keyword = new ArrayList<String>();
|
tEXt_keyword = new ArrayList<String>();
|
||||||
tEXt_text = new ArrayList<String>();
|
tEXt_text = new ArrayList<String>();
|
||||||
|
// tIME chunk with Image modification time
|
||||||
tIME_present = false;
|
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;
|
tRNS_present = false;
|
||||||
zTXt_keyword = new ArrayList<String>();
|
zTXt_keyword = new ArrayList<String>();
|
||||||
zTXt_compressionMethod = new ArrayList<Integer>();
|
zTXt_compressionMethod = new ArrayList<Integer>();
|
||||||
|
370
jdk/test/javax/imageio/plugins/png/PngCreationTimeTest.java
Normal file
370
jdk/test/javax/imageio/plugins/png/PngCreationTimeTest.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user