ExifInterface.java revision ab60e7e6e0eae5e9ee4181b173bcd4807b6bd2f3
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
17package android.media;
18
19import java.io.IOException;
20import java.text.ParsePosition;
21import java.text.SimpleDateFormat;
22import java.util.Date;
23import java.util.HashMap;
24import java.util.Map;
25import java.util.TimeZone;
26
27/**
28 * This is a class for reading and writing Exif tags in a JPEG file.
29 */
30public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
451    private native boolean appendThumbnailNative(String fileName,
452            String thumbnailFileName);
453
454    private native void saveAttributesNative(String fileName,
455            String compressedAttributes);
456
457    private native String getAttributesNative(String fileName);
458
459    private native void commitChangesNative(String fileName);
460
461    private native byte[] getThumbnailNative(String fileName);
462
463    private native long[] getThumbnailRangeNative(String fileName);
464}
465