Skip to content

Commit 0908439

Browse files
committed
Exif updates
Features: Add support for descriptions Add support get getting width/height Add support for speed/altitude from location Add support for lastModifiedTimestamp Bugfixes: Save/parse time in local time Support millis in timestamps (except for location) Remove calls to @hide methods on ExifInterface Use TAG_DATETIME for last modified and DATETIME_ORIGINAL for photo taken timestamp Attach TAG_DATETIME_DIGITIZED too, since we're using a digital camera
1 parent 7c6e7c3 commit 0908439

File tree

3 files changed

+274
-23
lines changed

3 files changed

+274
-23
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ buildscript {
66
mavenCentral()
77
}
88
dependencies {
9-
classpath 'com.android.tools.build:gradle:2.3.2'
9+
classpath 'com.android.tools.build:gradle:2.3.3'
1010
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
1111
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
1212

camera-view/src/main/java/com/xlythe/view/camera/Exif.java

Lines changed: 186 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import java.io.File;
99
import java.io.IOException;
1010
import java.io.InputStream;
11+
import java.text.ParseException;
12+
import java.text.ParsePosition;
1113
import java.text.SimpleDateFormat;
1214
import java.util.Date;
1315
import java.util.Locale;
@@ -19,13 +21,15 @@
1921
public class Exif {
2022
private static final String TAG = Exif.class.getSimpleName();
2123

22-
private static final String DEFAULT_TIMEZONE = "UTC";
23-
private static final String DATE_FORMAT = "yyyy:MM:dd";
24-
private static final String TIME_FORMAT = "HH:mm:ss";
25-
private static final String DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT;
24+
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy:MM:dd", Locale.ENGLISH);
25+
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
26+
private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.ENGLISH);
2627

2728
private final ExifInterface mExifInterface;
2829

30+
// When true, avoid saving any time. This is a privacy issue.
31+
private boolean mRemoveTimestamp = false;
32+
2933
public Exif(File file) throws IOException {
3034
this(file.toString());
3135
}
@@ -46,19 +50,47 @@ private Exif(ExifInterface exifInterface) {
4650
* Persists changes to disc.
4751
*/
4852
public void save() throws IOException {
53+
if (!mRemoveTimestamp) {
54+
attachLastModifiedTimestamp();
55+
}
4956
mExifInterface.saveAttributes();
5057
}
5158

5259
@Override
5360
public String toString() {
54-
return String.format(Locale.ENGLISH, "Exif{location=%s, rotation=%d, isFlippedVertically=%s, isFlippedHorizontally=%s, timestamp=%s}",
55-
getLocation(), getRotation(), isFlippedVertically(), isFlippedHorizontally(), getTimestamp());
61+
return String.format(Locale.ENGLISH, "Exif{width=%s, height=%s, rotation=%d, "
62+
+ "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, "
63+
+ "timestamp=%s, description=%s}",
64+
getWidth(), getHeight(), getRotation(), isFlippedVertically(), isFlippedHorizontally(),
65+
getLocation(), getTimestamp(), getDescription());
5666
}
5767

5868
private int getOrientation() {
5969
return mExifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
6070
}
6171

72+
/**
73+
* Returns the width of the photo in pixels.
74+
*/
75+
public int getWidth() {
76+
return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
77+
}
78+
79+
/**
80+
* Returns the height of the photo in pixels.
81+
*/
82+
public int getHeight() {
83+
return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
84+
}
85+
86+
public String getDescription() {
87+
return mExifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION);
88+
}
89+
90+
public void setDescription(@Nullable String description) {
91+
mExifInterface.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, description);
92+
}
93+
6294
/**
6395
* @return The degree of rotation (eg. 0, 90, 180, 270).
6496
*/
@@ -143,11 +175,66 @@ public boolean isFlippedHorizontally() {
143175
}
144176
}
145177

178+
private void attachLastModifiedTimestamp() {
179+
long now = System.currentTimeMillis();
180+
String datetime = convertToExifDateTime(now);
181+
182+
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime);
183+
184+
try {
185+
String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
186+
mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, subsec);
187+
} catch (ParseException e) {}
188+
}
189+
190+
/**
191+
* @return The timestamp (in millis) that this picture was modified, or -1 if no time is available.
192+
*/
193+
public long getLastModifiedTimestamp() {
194+
long timestamp = parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME));
195+
if (timestamp == -1) {
196+
return -1;
197+
}
198+
199+
String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME);
200+
if (subSecs != null) {
201+
try {
202+
long sub = Long.parseLong(subSecs);
203+
while (sub > 1000) {
204+
sub /= 10;
205+
}
206+
timestamp += sub;
207+
} catch (NumberFormatException e) {
208+
// Ignored
209+
}
210+
}
211+
212+
return timestamp;
213+
}
214+
146215
/**
147216
* @return The timestamp (in millis) that this picture was taken, or -1 if no time is available.
148217
*/
149218
public long getTimestamp() {
150-
return mExifInterface.getDateTime();
219+
long timestamp = parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
220+
if (timestamp == -1) {
221+
return -1;
222+
}
223+
224+
String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL);
225+
if (subSecs != null) {
226+
try {
227+
long sub = Long.parseLong(subSecs);
228+
while (sub > 1000) {
229+
sub /= 10;
230+
}
231+
timestamp += sub;
232+
} catch (NumberFormatException e) {
233+
// Ignored
234+
}
235+
}
236+
237+
return timestamp;
151238
}
152239

153240
/**
@@ -157,7 +244,12 @@ public long getTimestamp() {
157244
public Location getLocation() {
158245
String provider = mExifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
159246
double[] latlng = mExifInterface.getLatLong();
160-
long timestamp = mExifInterface.getGpsDateTime();
247+
double altitude = mExifInterface.getAltitude(0);
248+
double speed = mExifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0)
249+
* mExifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED_REF, 1);
250+
long timestamp = parseTimestamp(
251+
mExifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP),
252+
mExifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
161253
if (latlng == null) {
162254
return null;
163255
}
@@ -168,6 +260,12 @@ public Location getLocation() {
168260
Location location = new Location(provider);
169261
location.setLatitude(latlng[0]);
170262
location.setLongitude(latlng[1]);
263+
if (altitude != 0) {
264+
location.setAltitude(altitude);
265+
}
266+
if (speed != 0) {
267+
location.setSpeed((float) speed);
268+
}
171269
if (timestamp != -1) {
172270
location.setTime(timestamp);
173271
}
@@ -339,17 +437,32 @@ public void flipHorizontally() {
339437
* Attaches the current timestamp to the file.
340438
*/
341439
public void attachTimestamp() {
342-
String timestamp = convertToExifDateTime(System.currentTimeMillis());
343-
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, timestamp);
344-
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, timestamp);
440+
long now = System.currentTimeMillis();
441+
String datetime = convertToExifDateTime(now);
442+
443+
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime);
444+
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime);
445+
446+
try {
447+
String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
448+
mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec);
449+
mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec);
450+
} catch (ParseException e) {}
451+
452+
mRemoveTimestamp = false;
345453
}
346454

347455
/**
348456
* Removes the timestamp from the file.
349457
*/
350458
public void removeTimestamp() {
351-
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
352459
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, null);
460+
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
461+
mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
462+
mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null);
463+
mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null);
464+
mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null);
465+
mRemoveTimestamp = true;
353466
}
354467

355468
/**
@@ -358,6 +471,14 @@ public void removeTimestamp() {
358471
public void attachLocation(Location location) {
359472
mExifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
360473
mExifInterface.setLatLong(location.getLatitude(), location.getLongitude());
474+
if (location.hasAltitude()) {
475+
mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, Double.toString(location.getAltitude()));
476+
mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, Integer.toString(1));
477+
}
478+
if (location.hasSpeed()) {
479+
mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, Float.toString(location.getSpeed()));
480+
mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, Integer.toString(1));
481+
}
361482
mExifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, convertToExifDate(location.getTime()));
362483
mExifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, convertToExifTime(location.getTime()));
363484
}
@@ -375,21 +496,65 @@ public void removeLocation() {
375496
mExifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null);
376497
}
377498

499+
/**
500+
* @return The timestamp (in millis), or -1 if no time is available.
501+
*/
502+
private long parseTimestamp(@Nullable String date, @Nullable String time) {
503+
if (date == null && time == null) {
504+
return -1;
505+
}
506+
if (time == null) {
507+
try {
508+
return convertFromExifDate(date).getTime();
509+
} catch (ParseException e) {
510+
return -1;
511+
}
512+
}
513+
if (date == null) {
514+
try {
515+
return convertFromExifTime(time).getTime();
516+
} catch (ParseException e) {
517+
return -1;
518+
}
519+
}
520+
return parseTimestamp(date + " " + time);
521+
}
522+
523+
/**
524+
* @return The timestamp (in millis), or -1 if no time is available.
525+
*/
526+
private long parseTimestamp(@Nullable String datetime) {
527+
if (datetime == null) {
528+
return -1;
529+
}
530+
try {
531+
return convertFromExifDateTime(datetime).getTime();
532+
} catch (ParseException e) {
533+
return -1;
534+
}
535+
}
536+
378537
private static String convertToExifDateTime(long timestamp) {
379-
SimpleDateFormat format = new SimpleDateFormat(DATETIME_FORMAT, Locale.ENGLISH);
380-
format.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIMEZONE));
381-
return format.format(new Date(timestamp));
538+
return DATETIME_FORMAT.format(new Date(timestamp));
539+
}
540+
541+
private static Date convertFromExifDateTime(String dateTime) throws ParseException {
542+
return DATETIME_FORMAT.parse(dateTime);
382543
}
383544

384545
private static String convertToExifDate(long timestamp) {
385-
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH);
386-
format.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIMEZONE));
387-
return format.format(new Date(timestamp));
546+
return DATE_FORMAT.format(new Date(timestamp));
547+
}
548+
549+
private static Date convertFromExifDate(String date) throws ParseException {
550+
return DATE_FORMAT.parse(date);
388551
}
389552

390553
private static String convertToExifTime(long timestamp) {
391-
SimpleDateFormat format = new SimpleDateFormat(TIME_FORMAT, Locale.ENGLISH);
392-
format.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIMEZONE));
393-
return format.format(new Date(timestamp));
554+
return TIME_FORMAT.format(new Date(timestamp));
555+
}
556+
557+
private static Date convertFromExifTime(String time) throws ParseException {
558+
return TIME_FORMAT.parse(time);
394559
}
395560
}

0 commit comments

Comments
 (0)