Merge "Add support for reading/writing secondary format of DateTime tag" into androidx-master-dev
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 9646ab1..5899803 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -93,18 +93,23 @@
private static final String WEBP_WITHOUT_EXIF = "webp_without_exif.webp";
private static final String WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING =
"webp_lossless_without_exif.webp";
- private static final String JPEG_WITH_DATETIME_TAG = "jpeg_with_datetime_tag.jpg";
+ private static final String JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT =
+ "jpeg_with_datetime_tag_primary_format.jpg";
+ private static final String JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT =
+ "jpeg_with_datetime_tag_secondary_format.jpg";
private static final int[] IMAGE_RESOURCES = new int[] {
R.raw.jpeg_with_exif_byte_order_ii, R.raw.jpeg_with_exif_byte_order_mm,
R.raw.dng_with_exif_with_xmp, R.raw.jpeg_with_exif_with_xmp,
R.raw.png_with_exif_byte_order_ii, R.raw.png_without_exif, R.raw.webp_with_exif,
R.raw.webp_with_anim_without_exif, R.raw.webp_without_exif,
- R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag};
+ R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag_primary_format,
+ R.raw.jpeg_with_datetime_tag_secondary_format};
private static final String[] IMAGE_FILENAMES = new String[] {
JPEG_WITH_EXIF_BYTE_ORDER_II, JPEG_WITH_EXIF_BYTE_ORDER_MM, DNG_WITH_EXIF_WITH_XMP,
JPEG_WITH_EXIF_WITH_XMP, PNG_WITH_EXIF_BYTE_ORDER_II, PNG_WITHOUT_EXIF,
WEBP_WITH_EXIF, WEBP_WITHOUT_EXIF_WITH_ANIM_DATA, WEBP_WITHOUT_EXIF,
- WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG};
+ WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT,
+ JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT};
private static final int USER_READ_WRITE = 0600;
private static final String TEST_TEMP_FILE_NAME = "testImage";
@@ -632,7 +637,7 @@
final String dateTimeValue = "2017:02:02 22:22:22";
final String dateTimeOriginalValue = "2017:01:01 11:11:11";
- File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG);
+ File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
assertEquals(expectedDatetimeValue, (long) exif.getDateTime());
assertEquals(expectedDatetimeValue, (long) exif.getDateTimeOriginal());
@@ -670,6 +675,42 @@
assertEquals(currentTimeStamp - expectedDatetimeOffsetLongValue, (long) exif.getDateTime());
}
+ /**
+ * Test whether ExifInterface can correctly get and set datetime value for a secondary format:
+ * Primary format example: 2020:01:01 00:00:00
+ * Secondary format example: 2020-01-01 00:00:00
+ *
+ * Getting a datetime tag value with the secondary format should work for both
+ * {@link ExifInterface#getAttribute(String)} and {@link ExifInterface#getDateTime()}.
+ * Setting a datetime tag value with the secondary format with
+ * {@link ExifInterface#setAttribute(String, String)} should automatically convert it to the
+ * primary format.
+ */
+ @Test
+ @SmallTest
+ public void testGetSetDateTimeForSecondaryFormat() throws Exception {
+ final long dateTimePrimaryFormatLongValue = 1604075491000L;
+ final String dateTimePrimaryFormatStringValue = "2020-10-30 16:31:31";
+ File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT);
+
+ // Check that secondary format value is read correctly.
+ ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(dateTimePrimaryFormatStringValue,
+ exif.getAttribute(ExifInterface.TAG_DATETIME));
+ assertEquals(dateTimePrimaryFormatLongValue, (long) exif.getDateTime());
+
+ final long dateTimeSecondaryFormatLongValue = 1577836800000L;
+ final String dateTimeSecondaryFormatStringValue = "2020-01-01 00:00:00";
+ final String modifiedDateTimeSecondaryFormatStringValue = "2020:01:01 00:00:00";
+
+ // Check that secondary format value is written correctly.
+ exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeSecondaryFormatStringValue);
+ exif.saveAttributes();
+ assertEquals(modifiedDateTimeSecondaryFormatStringValue,
+ exif.getAttribute(ExifInterface.TAG_DATETIME));
+ assertEquals(dateTimeSecondaryFormatLongValue, (long) exif.getDateTime());
+ }
+
@Test
@LargeTest
public void testRotation() throws IOException {
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
similarity index 100%
rename from exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag.jpg
rename to exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
new file mode 100644
index 0000000..53ad4e9
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 9766912..c6ed238 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -454,6 +454,10 @@
* <li>Length = 19</li>
* <li>Default = None</li>
* </ul>
+ *
+ * <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+ * however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+ * format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
*/
public static final String TAG_DATETIME = "DateTime";
/**
@@ -745,6 +749,10 @@
* <li>Length = 19</li>
* <li>Default = None</li>
* </ul>
+ *
+ * <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+ * however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+ * format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
*/
public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
/**
@@ -763,6 +771,10 @@
* <li>Length = 19</li>
* <li>Default = None</li>
* </ul>
+ *
+ * <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+ * however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+ * format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
*/
public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
/**
@@ -2999,7 +3011,8 @@
private static final int WEBP_CHUNK_TYPE_BYTE_LENGTH = 4;
private static final int WEBP_CHUNK_SIZE_BYTE_LENGTH = 4;
- private static SimpleDateFormat sFormatter;
+ private static SimpleDateFormat sFormatterPrimary;
+ private static SimpleDateFormat sFormatterSecondary;
// See Exchangeable image file format for digital still cameras: Exif version 2.2.
// The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
@@ -3840,8 +3853,10 @@
private static final int IMAGE_TYPE_WEBP = 14;
static {
- sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
- sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
+ sFormatterPrimary.setTimeZone(TimeZone.getTimeZone("UTC"));
+ sFormatterSecondary = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
+ sFormatterSecondary.setTimeZone(TimeZone.getTimeZone("UTC"));
// Build up the hash tables to look up Exif tags for reading Exif tags.
for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
@@ -3890,10 +3905,17 @@
private boolean mXmpIsFromSeparateMarker;
// Pattern to check non zero timestamp
- private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
+ private static final Pattern NON_ZERO_TIME_PATTERN = Pattern.compile(".*[1-9].*");
// Pattern to check gps timestamp
- private static final Pattern sGpsTimestampPattern =
- Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$");
+ private static final Pattern GPS_TIMESTAMP_PATTERN =
+ Pattern.compile("^(\\d{2}):(\\d{2}):(\\d{2})$");
+ // Pattern to check date time primary format (e.g. 2020:01:01 00:00:00)
+ private static final Pattern DATETIME_PRIMARY_FORMAT_PATTERN =
+ Pattern.compile("^(\\d{4}):(\\d{2}):(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+ // Pattern to check date time secondary format (e.g. 2020-01-01 00:00:00)
+ private static final Pattern DATETIME_SECONDARY_FORMAT_PATTERN =
+ Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+ private static final int DATETIME_VALUE_STRING_LENGTH = 19;
/**
* Reads Exif tags from the specified image file.
@@ -4192,6 +4214,28 @@
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
+ // Validate and convert if necessary.
+ if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
+ || TAG_DATETIME_DIGITIZED.equals(tag)) {
+ if (value != null) {
+ boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
+ boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(value).find();
+ // Validate
+ if (value.length() != DATETIME_VALUE_STRING_LENGTH
+ || (!isPrimaryFormat && !isSecondaryFormat)) {
+ Log.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ // If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it to
+ // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
+ // official documentation.
+ // See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
+ if (isSecondaryFormat) {
+ // Replace "-" with ":" to match the primary format.
+ value = value.replaceAll("-", ":");
+ }
+ }
+ }
// Maintain compatibility.
if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
if (DEBUG) {
@@ -4203,7 +4247,7 @@
// Convert the given value to rational values for backwards compatibility.
if (value != null && sTagSetForCompatibility.contains(tag)) {
if (tag.equals(TAG_GPS_TIMESTAMP)) {
- Matcher m = sGpsTimestampPattern.matcher(value);
+ Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
if (!m.find()) {
Log.w(TAG, "Invalid value for " + tag + " : " + value);
return;
@@ -5017,7 +5061,8 @@
setAttribute(TAG_GPS_SPEED_REF, "K");
setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
- String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+", -1);
+ String[] dateTime = sFormatterPrimary.format(
+ new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
}
@@ -5080,7 +5125,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void setDateTime(@NonNull Long timeStamp) {
long sub = timeStamp % 1000;
- setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
+ setAttribute(TAG_DATETIME, sFormatterPrimary.format(new Date(timeStamp)));
setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
}
@@ -5131,16 +5176,22 @@
private static Long parseDateTime(@Nullable String dateTimeString, @Nullable String subSecs,
@Nullable String offsetString) {
- if (dateTimeString == null
- || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return null;
+ if (dateTimeString == null || !NON_ZERO_TIME_PATTERN.matcher(dateTimeString).matches()) {
+ return null;
+ }
ParsePosition pos = new ParsePosition(0);
try {
// The exif field is in local time. Parsing it as if it is UTC will yield time
// since 1/1/1970 local time
- Date datetime = sFormatter.parse(dateTimeString, pos);
- if (datetime == null) return null;
- long msecs = datetime.getTime();
+ Date dateTime = sFormatterPrimary.parse(dateTimeString, pos);
+ if (dateTime == null) {
+ dateTime = sFormatterSecondary.parse(dateTimeString, pos);
+ if (dateTime == null) {
+ return null;
+ }
+ }
+ long msecs = dateTime.getTime();
if (offsetString != null) {
String sign = offsetString.substring(0, 1);
int hour = Integer.parseInt(offsetString.substring(1, 3));
@@ -5179,8 +5230,8 @@
String date = getAttribute(TAG_GPS_DATESTAMP);
String time = getAttribute(TAG_GPS_TIMESTAMP);
if (date == null || time == null
- || (!sNonZeroTimePattern.matcher(date).matches()
- && !sNonZeroTimePattern.matcher(time).matches())) {
+ || (!NON_ZERO_TIME_PATTERN.matcher(date).matches()
+ && !NON_ZERO_TIME_PATTERN.matcher(time).matches())) {
return null;
}
@@ -5188,9 +5239,14 @@
ParsePosition pos = new ParsePosition(0);
try {
- Date datetime = sFormatter.parse(dateTimeString, pos);
- if (datetime == null) return null;
- return datetime.getTime();
+ Date dateTime = sFormatterPrimary.parse(dateTimeString, pos);
+ if (dateTime == null) {
+ dateTime = sFormatterSecondary.parse(dateTimeString, pos);
+ if (dateTime == null) {
+ return null;
+ }
+ }
+ return dateTime.getTime();
} catch (IllegalArgumentException e) {
return null;
}