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