1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import java.io.IOException;
20 import java.text.ParsePosition;
21 import java.text.SimpleDateFormat;
22 import java.util.Date;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.TimeZone;
26 
27 /**
28  * This is a class for reading and writing Exif tags in a JPEG file.
29  */
30 public class ExifInterface {
31     // The Exif tag names
32     /** Type is int. */
33     public static final String TAG_ORIENTATION = "Orientation";
34     /** Type is String. */
35     public static final String TAG_DATETIME = "DateTime";
36     /** Type is String. */
37     public static final String TAG_MAKE = "Make";
38     /** Type is String. */
39     public static final String TAG_MODEL = "Model";
40     /** Type is int. */
41     public static final String TAG_FLASH = "Flash";
42     /** Type is int. */
43     public static final String TAG_IMAGE_WIDTH = "ImageWidth";
44     /** Type is int. */
45     public static final String TAG_IMAGE_LENGTH = "ImageLength";
46     /** String. Format is "num1/denom1,num2/denom2,num3/denom3". */
47     public static final String TAG_GPS_LATITUDE = "GPSLatitude";
48     /** String. Format is "num1/denom1,num2/denom2,num3/denom3". */
49     public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
50     /** Type is String. */
51     public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
52     /** Type is String. */
53     public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
54     /** Type is String. */
55     public static final String TAG_EXPOSURE_TIME = "ExposureTime";
56     /** Type is String. */
57     public static final String TAG_APERTURE = "FNumber";
58     /** Type is String. */
59     public static final String TAG_ISO = "ISOSpeedRatings";
60     /** Type is String. */
61     public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
62     /** Type is int. */
63     public static final String TAG_SUBSEC_TIME = "SubSecTime";
64     /** Type is int. */
65     public static final String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
66     /** Type is int. */
67     public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
68 
69 
70 
71     /**
72      * @hide
73      */
74     public static final String TAG_SUBSECTIME = "SubSecTime";
75 
76     /**
77      * The altitude (in meters) based on the reference in TAG_GPS_ALTITUDE_REF.
78      * Type is rational.
79      */
80     public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
81 
82     /**
83      * 0 if the altitude is above sea level. 1 if the altitude is below sea
84      * level. Type is int.
85      */
86     public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef";
87 
88     /** Type is String. */
89     public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
90     /** Type is String. */
91     public static final String TAG_GPS_DATESTAMP = "GPSDateStamp";
92     /** Type is int. */
93     public static final String TAG_WHITE_BALANCE = "WhiteBalance";
94     /** Type is rational. */
95     public static final String TAG_FOCAL_LENGTH = "FocalLength";
96     /** Type is String. Name of GPS processing method used for location finding. */
97     public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
98 
99     // Constants used for the Orientation Exif tag.
100     public static final int ORIENTATION_UNDEFINED = 0;
101     public static final int ORIENTATION_NORMAL = 1;
102     public static final int ORIENTATION_FLIP_HORIZONTAL = 2;  // left right reversed mirror
103     public static final int ORIENTATION_ROTATE_180 = 3;
104     public static final int ORIENTATION_FLIP_VERTICAL = 4;  // upside down mirror
105     public static final int ORIENTATION_TRANSPOSE = 5;  // flipped about top-left <--> bottom-right axis
106     public static final int ORIENTATION_ROTATE_90 = 6;  // rotate 90 cw to right it
107     public static final int ORIENTATION_TRANSVERSE = 7;  // flipped about top-right <--> bottom-left axis
108     public static final int ORIENTATION_ROTATE_270 = 8;  // rotate 270 to right it
109 
110     // Constants used for white balance
111     public static final int WHITEBALANCE_AUTO = 0;
112     public static final int WHITEBALANCE_MANUAL = 1;
113     private static SimpleDateFormat sFormatter;
114 
115     static {
116         System.loadLibrary("jhead_jni");
117         sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
118         sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
119     }
120 
121     private String mFilename;
122     private HashMap<String, String> mAttributes;
123     private boolean mHasThumbnail;
124 
125     // Because the underlying implementation (jhead) uses static variables,
126     // there can only be one user at a time for the native functions (and
127     // they cannot keep state in the native code across function calls). We
128     // use sLock to serialize the accesses.
129     private static final Object sLock = new Object();
130 
131     /**
132      * Reads Exif tags from the specified JPEG file.
133      */
ExifInterface(String filename)134     public ExifInterface(String filename) throws IOException {
135         if (filename == null) {
136             throw new IllegalArgumentException("filename cannot be null");
137         }
138         mFilename = filename;
139         loadAttributes();
140     }
141 
142     /**
143      * Returns the value of the specified tag or {@code null} if there
144      * is no such tag in the JPEG file.
145      *
146      * @param tag the name of the tag.
147      */
getAttribute(String tag)148     public String getAttribute(String tag) {
149         return mAttributes.get(tag);
150     }
151 
152     /**
153      * Returns the integer value of the specified tag. If there is no such tag
154      * in the JPEG file or the value cannot be parsed as integer, return
155      * <var>defaultValue</var>.
156      *
157      * @param tag the name of the tag.
158      * @param defaultValue the value to return if the tag is not available.
159      */
getAttributeInt(String tag, int defaultValue)160     public int getAttributeInt(String tag, int defaultValue) {
161         String value = mAttributes.get(tag);
162         if (value == null) return defaultValue;
163         try {
164             return Integer.valueOf(value);
165         } catch (NumberFormatException ex) {
166             return defaultValue;
167         }
168     }
169 
170     /**
171      * Returns the double value of the specified rational tag. If there is no
172      * such tag in the JPEG file or the value cannot be parsed as double, return
173      * <var>defaultValue</var>.
174      *
175      * @param tag the name of the tag.
176      * @param defaultValue the value to return if the tag is not available.
177      */
getAttributeDouble(String tag, double defaultValue)178     public double getAttributeDouble(String tag, double defaultValue) {
179         String value = mAttributes.get(tag);
180         if (value == null) return defaultValue;
181         try {
182             int index = value.indexOf("/");
183             if (index == -1) return defaultValue;
184             double denom = Double.parseDouble(value.substring(index + 1));
185             if (denom == 0) return defaultValue;
186             double num = Double.parseDouble(value.substring(0, index));
187             return num / denom;
188         } catch (NumberFormatException ex) {
189             return defaultValue;
190         }
191     }
192 
193     /**
194      * Set the value of the specified tag.
195      *
196      * @param tag the name of the tag.
197      * @param value the value of the tag.
198      */
setAttribute(String tag, String value)199     public void setAttribute(String tag, String value) {
200         mAttributes.put(tag, value);
201     }
202 
203     /**
204      * Initialize mAttributes with the attributes from the file mFilename.
205      *
206      * mAttributes is a HashMap which stores the Exif attributes of the file.
207      * The key is the standard tag name and the value is the tag's value: e.g.
208      * Model -> Nikon. Numeric values are stored as strings.
209      *
210      * This function also initialize mHasThumbnail to indicate whether the
211      * file has a thumbnail inside.
212      */
loadAttributes()213     private void loadAttributes() throws IOException {
214         // format of string passed from native C code:
215         // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
216         // example:
217         // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
218         mAttributes = new HashMap<String, String>();
219 
220         String attrStr;
221         synchronized (sLock) {
222             attrStr = getAttributesNative(mFilename);
223         }
224 
225         // get count
226         int ptr = attrStr.indexOf(' ');
227         int count = Integer.parseInt(attrStr.substring(0, ptr));
228         // skip past the space between item count and the rest of the attributes
229         ++ptr;
230 
231         for (int i = 0; i < count; i++) {
232             // extract the attribute name
233             int equalPos = attrStr.indexOf('=', ptr);
234             String attrName = attrStr.substring(ptr, equalPos);
235             ptr = equalPos + 1;     // skip past =
236 
237             // extract the attribute value length
238             int lenPos = attrStr.indexOf(' ', ptr);
239             int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
240             ptr = lenPos + 1;       // skip pas the space
241 
242             // extract the attribute value
243             String attrValue = attrStr.substring(ptr, ptr + attrLen);
244             ptr += attrLen;
245 
246             if (attrName.equals("hasThumbnail")) {
247                 mHasThumbnail = attrValue.equalsIgnoreCase("true");
248             } else {
249                 mAttributes.put(attrName, attrValue);
250             }
251         }
252     }
253 
254     /**
255      * Save the tag data into the JPEG file. This is expensive because it involves
256      * copying all the JPG data from one file to another and deleting the old file
257      * and renaming the other. It's best to use {@link #setAttribute(String,String)}
258      * to set all attributes to write and make a single call rather than multiple
259      * calls for each attribute.
260      */
saveAttributes()261     public void saveAttributes() throws IOException {
262         // format of string passed to native C code:
263         // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
264         // example:
265         // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
266         StringBuilder sb = new StringBuilder();
267         int size = mAttributes.size();
268         if (mAttributes.containsKey("hasThumbnail")) {
269             --size;
270         }
271         sb.append(size + " ");
272         for (Map.Entry<String, String> iter : mAttributes.entrySet()) {
273             String key = iter.getKey();
274             if (key.equals("hasThumbnail")) {
275                 // this is a fake attribute not saved as an exif tag
276                 continue;
277             }
278             String val = iter.getValue();
279             sb.append(key + "=");
280             sb.append(val.length() + " ");
281             sb.append(val);
282         }
283         String s = sb.toString();
284         synchronized (sLock) {
285             saveAttributesNative(mFilename, s);
286             commitChangesNative(mFilename);
287         }
288     }
289 
290     /**
291      * Returns true if the JPEG file has a thumbnail.
292      */
hasThumbnail()293     public boolean hasThumbnail() {
294         return mHasThumbnail;
295     }
296 
297     /**
298      * Returns the thumbnail inside the JPEG file, or {@code null} if there is no thumbnail.
299      * The returned data is in JPEG format and can be decoded using
300      * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
301      */
getThumbnail()302     public byte[] getThumbnail() {
303         synchronized (sLock) {
304             return getThumbnailNative(mFilename);
305         }
306     }
307 
308     /**
309      * Returns the offset and length of thumbnail inside the JPEG file, or
310      * {@code null} if there is no thumbnail.
311      *
312      * @return two-element array, the offset in the first value, and length in
313      *         the second, or {@code null} if no thumbnail was found.
314      * @hide
315      */
getThumbnailRange()316     public long[] getThumbnailRange() {
317         synchronized (sLock) {
318             return getThumbnailRangeNative(mFilename);
319         }
320     }
321 
322     /**
323      * Stores the latitude and longitude value in a float array. The first element is
324      * the latitude, and the second element is the longitude. Returns false if the
325      * Exif tags are not available.
326      */
getLatLong(float output[])327     public boolean getLatLong(float output[]) {
328         String latValue = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE);
329         String latRef = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE_REF);
330         String lngValue = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE);
331         String lngRef = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE_REF);
332 
333         if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
334             try {
335                 output[0] = convertRationalLatLonToFloat(latValue, latRef);
336                 output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
337                 return true;
338             } catch (IllegalArgumentException e) {
339                 // if values are not parseable
340             }
341         }
342 
343         return false;
344     }
345 
346     /**
347      * Return the altitude in meters. If the exif tag does not exist, return
348      * <var>defaultValue</var>.
349      *
350      * @param defaultValue the value to return if the tag is not available.
351      */
getAltitude(double defaultValue)352     public double getAltitude(double defaultValue) {
353         double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
354         int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
355 
356         if (altitude >= 0 && ref >= 0) {
357             return (double) (altitude * ((ref == 1) ? -1 : 1));
358         } else {
359             return defaultValue;
360         }
361     }
362 
363     /**
364      * Returns number of milliseconds since Jan. 1, 1970, midnight local time.
365      * Returns -1 if the date time information if not available.
366      * @hide
367      */
getDateTime()368     public long getDateTime() {
369         String dateTimeString = mAttributes.get(TAG_DATETIME);
370         if (dateTimeString == null) return -1;
371 
372         ParsePosition pos = new ParsePosition(0);
373         try {
374             // The exif field is in local time. Parsing it as if it is UTC will yield time
375             // since 1/1/1970 local time
376             Date datetime = sFormatter.parse(dateTimeString, pos);
377             if (datetime == null) return -1;
378             long msecs = datetime.getTime();
379 
380             String subSecs = mAttributes.get(TAG_SUBSECTIME);
381             if (subSecs != null) {
382                 try {
383                     long sub = Long.valueOf(subSecs);
384                     while (sub > 1000) {
385                         sub /= 10;
386                     }
387                     msecs += sub;
388                 } catch (NumberFormatException e) {
389                 }
390             }
391             return msecs;
392         } catch (IllegalArgumentException ex) {
393             return -1;
394         }
395     }
396 
397     /**
398      * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
399      * Returns -1 if the date time information if not available.
400      * @hide
401      */
getGpsDateTime()402     public long getGpsDateTime() {
403         String date = mAttributes.get(TAG_GPS_DATESTAMP);
404         String time = mAttributes.get(TAG_GPS_TIMESTAMP);
405         if (date == null || time == null) return -1;
406 
407         String dateTimeString = date + ' ' + time;
408 
409         ParsePosition pos = new ParsePosition(0);
410         try {
411             Date datetime = sFormatter.parse(dateTimeString, pos);
412             if (datetime == null) return -1;
413             return datetime.getTime();
414         } catch (IllegalArgumentException ex) {
415             return -1;
416         }
417     }
418 
convertRationalLatLonToFloat( String rationalString, String ref)419     private static float convertRationalLatLonToFloat(
420             String rationalString, String ref) {
421         try {
422             String [] parts = rationalString.split(",");
423 
424             String [] pair;
425             pair = parts[0].split("/");
426             double degrees = Double.parseDouble(pair[0].trim())
427                     / Double.parseDouble(pair[1].trim());
428 
429             pair = parts[1].split("/");
430             double minutes = Double.parseDouble(pair[0].trim())
431                     / Double.parseDouble(pair[1].trim());
432 
433             pair = parts[2].split("/");
434             double seconds = Double.parseDouble(pair[0].trim())
435                     / Double.parseDouble(pair[1].trim());
436 
437             double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
438             if ((ref.equals("S") || ref.equals("W"))) {
439                 return (float) -result;
440             }
441             return (float) result;
442         } catch (NumberFormatException e) {
443             // Some of the nubmers are not valid
444             throw new IllegalArgumentException();
445         } catch (ArrayIndexOutOfBoundsException e) {
446             // Some of the rational does not follow the correct format
447             throw new IllegalArgumentException();
448         }
449     }
450 
appendThumbnailNative(String fileName, String thumbnailFileName)451     private native boolean appendThumbnailNative(String fileName,
452             String thumbnailFileName);
453 
saveAttributesNative(String fileName, String compressedAttributes)454     private native void saveAttributesNative(String fileName,
455             String compressedAttributes);
456 
getAttributesNative(String fileName)457     private native String getAttributesNative(String fileName);
458 
commitChangesNative(String fileName)459     private native void commitChangesNative(String fileName);
460 
getThumbnailNative(String fileName)461     private native byte[] getThumbnailNative(String fileName);
462 
getThumbnailRangeNative(String fileName)463     private native long[] getThumbnailRangeNative(String fileName);
464 }
465