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