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