ExifInterface.java revision a7bdedabf447a559e0b914e4e5623f3af5ac8ef1
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 android.util.Log;
20
21import java.text.ParsePosition;
22import java.text.SimpleDateFormat;
23import java.util.Date;
24import java.util.HashMap;
25import java.util.Map;
26
27/**
28 * Wrapper for native Exif library
29 * {@hide}
30 */
31public class ExifInterface {
32    private static final String TAG = "ExifInterface";
33    private String mFilename;
34
35    // Constants used for the Orientation Exif tag.
36    public static final int ORIENTATION_UNDEFINED = 0;
37    public static final int ORIENTATION_NORMAL = 1;
38
39    // Constants used for white balance
40    public static final int WHITEBALANCE_AUTO = 0;
41    public static final int WHITEBALANCE_MANUAL = 1;
42
43    // left right reversed mirror
44    public static final int ORIENTATION_FLIP_HORIZONTAL = 2;
45    public static final int ORIENTATION_ROTATE_180 = 3;
46
47    // upside down mirror
48    public static final int ORIENTATION_FLIP_VERTICAL = 4;
49
50    // flipped about top-left <--> bottom-right axis
51    public static final int ORIENTATION_TRANSPOSE = 5;
52
53    // rotate 90 cw to right it
54    public static final int ORIENTATION_ROTATE_90 = 6;
55
56    // flipped about top-right <--> bottom-left axis
57    public static final int ORIENTATION_TRANSVERSE = 7;
58
59    // rotate 270 to right it
60    public static final int ORIENTATION_ROTATE_270 = 8;
61
62    // The Exif tag names
63    public static final String TAG_ORIENTATION = "Orientation";
64
65    public static final String TAG_DATETIME = "DateTime";
66    public static final String TAG_MAKE = "Make";
67    public static final String TAG_MODEL = "Model";
68    public static final String TAG_FLASH = "Flash";
69    public static final String TAG_IMAGE_WIDTH = "ImageWidth";
70    public static final String TAG_IMAGE_LENGTH = "ImageLength";
71
72    public static final String TAG_GPS_LATITUDE = "GPSLatitude";
73    public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
74
75    public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
76    public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
77    public static final String TAG_WHITE_BALANCE = "WhiteBalance";
78
79    private boolean mSavedAttributes = false;
80    private boolean mHasThumbnail = false;
81    private HashMap<String, String> mCachedAttributes = null;
82
83    static {
84        System.loadLibrary("exif");
85    }
86
87    private static ExifInterface sExifObj = null;
88    /**
89     * Since the underlying jhead native code is not thread-safe,
90     * ExifInterface should use singleton interface instead of public
91     * constructor.
92     */
93    private static synchronized ExifInterface instance() {
94        if (sExifObj == null) {
95            sExifObj = new ExifInterface();
96        }
97
98        return sExifObj;
99    }
100
101    /**
102     * The following 3 static methods are handy routines for atomic operation
103     * of underlying jhead library. It retrieves EXIF data and then release
104     * ExifInterface immediately.
105     */
106    public static synchronized HashMap<String, String> loadExifData(String filename) {
107        ExifInterface exif = instance();
108        HashMap<String, String> exifData = null;
109        if (exif != null) {
110            exif.setFilename(filename);
111            exifData = exif.getAttributes();
112        }
113        return exifData;
114    }
115
116    public static synchronized void saveExifData(String filename, HashMap<String, String> exifData) {
117        ExifInterface exif = instance();
118        if (exif != null) {
119            exif.setFilename(filename);
120            exif.saveAttributes(exifData);
121        }
122    }
123
124    public static synchronized byte[] getExifThumbnail(String filename) {
125        ExifInterface exif = instance();
126        if (exif != null) {
127            exif.setFilename(filename);
128            return exif.getThumbnail();
129        }
130        return null;
131    }
132
133    public void setFilename(String filename) {
134        if (mFilename == null || !mFilename.equals(filename)) {
135            mFilename = filename;
136            mCachedAttributes = null;
137        }
138    }
139
140    /**
141     * Given a HashMap of Exif tags and associated values, an Exif section in
142     * the JPG file is created and loaded with the tag data. saveAttributes()
143     * is expensive because it involves copying all the JPG data from one file
144     * to another and deleting the old file and renaming the other. It's best
145     * to collect all the attributes to write and make a single call rather
146     * than multiple calls for each attribute. You must call "commitChanges()"
147     * at some point to commit the changes.
148     */
149    public void saveAttributes(HashMap<String, String> attributes) {
150        // format of string passed to native C code:
151        // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
152        // example:
153        // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
154        StringBuilder sb = new StringBuilder();
155        int size = attributes.size();
156        if (attributes.containsKey("hasThumbnail")) {
157            --size;
158        }
159        sb.append(size + " ");
160        for (Map.Entry<String, String> iter : attributes.entrySet()) {
161            String key = iter.getKey();
162            if (key.equals("hasThumbnail")) {
163                // this is a fake attribute not saved as an exif tag
164                continue;
165            }
166            String val = iter.getValue();
167            sb.append(key + "=");
168            sb.append(val.length() + " ");
169            sb.append(val);
170        }
171        String s = sb.toString();
172        saveAttributesNative(mFilename, s);
173        commitChangesNative(mFilename);
174        mSavedAttributes = true;
175    }
176
177    /**
178     * Returns a HashMap loaded with the Exif attributes of the file. The key
179     * is the standard tag name and the value is the tag's value: e.g.
180     * Model -> Nikon. Numeric values are returned as strings.
181     */
182    public HashMap<String, String> getAttributes() {
183        if (mCachedAttributes != null) {
184            return mCachedAttributes;
185        }
186        // format of string passed from native C code:
187        // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
188        // example:
189        // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
190        mCachedAttributes = new HashMap<String, String>();
191
192        String attrStr = getAttributesNative(mFilename);
193
194        // get count
195        int ptr = attrStr.indexOf(' ');
196        int count = Integer.parseInt(attrStr.substring(0, ptr));
197        // skip past the space between item count and the rest of the attributes
198        ++ptr;
199
200        for (int i = 0; i < count; i++) {
201            // extract the attribute name
202            int equalPos = attrStr.indexOf('=', ptr);
203            String attrName = attrStr.substring(ptr, equalPos);
204            ptr = equalPos + 1;     // skip past =
205
206            // extract the attribute value length
207            int lenPos = attrStr.indexOf(' ', ptr);
208            int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
209            ptr = lenPos + 1;       // skip pas the space
210
211            // extract the attribute value
212            String attrValue = attrStr.substring(ptr, ptr + attrLen);
213            ptr += attrLen;
214
215            if (attrName.equals("hasThumbnail")) {
216                mHasThumbnail = attrValue.equalsIgnoreCase("true");
217            } else {
218                mCachedAttributes.put(attrName, attrValue);
219            }
220        }
221        return mCachedAttributes;
222    }
223
224    /**
225     * Given a numerical white balance value, return a
226     * human-readable string describing it.
227     */
228    public static String whiteBalanceToString(int whitebalance) {
229        switch (whitebalance) {
230            case WHITEBALANCE_AUTO:
231                return "Auto";
232            case WHITEBALANCE_MANUAL:
233                return "Manual";
234            default:
235                return "";
236        }
237    }
238
239    /**
240     * Given a numerical orientation, return a human-readable string describing
241     * the orientation.
242     */
243    public static String orientationToString(int orientation) {
244        // TODO: this function needs to be localized and use string resource ids
245        // rather than strings
246        String orientationString;
247        switch (orientation) {
248            case ORIENTATION_NORMAL:
249                orientationString = "Normal";
250                break;
251            case ORIENTATION_FLIP_HORIZONTAL:
252                orientationString = "Flipped horizontal";
253                break;
254            case ORIENTATION_ROTATE_180:
255                orientationString = "Rotated 180 degrees";
256                break;
257            case ORIENTATION_FLIP_VERTICAL:
258                orientationString = "Upside down mirror";
259                break;
260            case ORIENTATION_TRANSPOSE:
261                orientationString = "Transposed";
262                break;
263            case ORIENTATION_ROTATE_90:
264                orientationString = "Rotated 90 degrees";
265                break;
266            case ORIENTATION_TRANSVERSE:
267                orientationString = "Transversed";
268                break;
269            case ORIENTATION_ROTATE_270:
270                orientationString = "Rotated 270 degrees";
271                break;
272            default:
273                orientationString = "Undefined";
274                break;
275        }
276        return orientationString;
277    }
278
279    /**
280     * Copies the thumbnail data out of the filename and puts it in the Exif
281     * data associated with the file used to create this object. You must call
282     * "commitChanges()" at some point to commit the changes.
283     */
284    public boolean appendThumbnail(String thumbnailFileName) {
285        if (!mSavedAttributes) {
286            throw new RuntimeException("Must call saveAttributes "
287                    + "before calling appendThumbnail");
288        }
289        mHasThumbnail = appendThumbnailNative(mFilename, thumbnailFileName);
290        return mHasThumbnail;
291    }
292
293    public boolean hasThumbnail() {
294        if (!mSavedAttributes) {
295            getAttributes();
296        }
297        return mHasThumbnail;
298    }
299
300    public byte[] getThumbnail() {
301        return getThumbnailNative(mFilename);
302    }
303
304    public static float[] getLatLng(HashMap<String, String> exifData) {
305        if (exifData == null) {
306            return null;
307        }
308
309        String latValue = exifData.get(ExifInterface.TAG_GPS_LATITUDE);
310        String latRef = exifData.get(ExifInterface.TAG_GPS_LATITUDE_REF);
311        String lngValue = exifData.get(ExifInterface.TAG_GPS_LONGITUDE);
312        String lngRef = exifData.get(ExifInterface.TAG_GPS_LONGITUDE_REF);
313        float[] latlng = null;
314
315        if (latValue != null && latRef != null
316                && lngValue != null && lngRef != null) {
317            latlng = new float[2];
318            latlng[0] = ExifInterface.convertRationalLatLonToFloat(
319                    latValue, latRef);
320            latlng[1] = ExifInterface.convertRationalLatLonToFloat(
321                    lngValue, lngRef);
322        }
323
324        return latlng;
325    }
326
327    private static SimpleDateFormat sFormatter =
328            new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
329
330    // Returns number of milliseconds since Jan. 1, 1970, midnight GMT.
331    // Returns -1 if the date time information if not available.
332    public static long getDateTime(HashMap<String, String> exifData) {
333        if (exifData == null) {
334            return -1;
335        }
336
337        String dateTimeString = exifData.get(ExifInterface.TAG_DATETIME);
338        if (dateTimeString == null) return -1;
339
340        ParsePosition pos = new ParsePosition(0);
341        try {
342            Date date = sFormatter.parse(dateTimeString, pos);
343            if (date == null) return -1;
344            return date.getTime();
345        } catch (IllegalArgumentException ex) {
346            return -1;
347        }
348    }
349
350    public static float convertRationalLatLonToFloat(
351            String rationalString, String ref) {
352        try {
353            String [] parts = rationalString.split(",");
354
355            String [] pair;
356            pair = parts[0].split("/");
357            int degrees = (int) (Float.parseFloat(pair[0].trim())
358                    / Float.parseFloat(pair[1].trim()));
359
360            pair = parts[1].split("/");
361            int minutes = (int) ((Float.parseFloat(pair[0].trim())
362                    / Float.parseFloat(pair[1].trim())));
363
364            pair = parts[2].split("/");
365            float seconds = Float.parseFloat(pair[0].trim())
366                    / Float.parseFloat(pair[1].trim());
367
368            float result = degrees + (minutes / 60F) + (seconds / (60F * 60F));
369            if ((ref.equals("S") || ref.equals("W"))) {
370                return -result;
371            }
372            return result;
373        } catch (RuntimeException ex) {
374            // if for whatever reason we can't parse the lat long then return
375            // null
376            return 0f;
377        }
378    }
379
380    public static String convertRationalLatLonToDecimalString(
381            String rationalString, String ref, boolean usePositiveNegative) {
382            float result = convertRationalLatLonToFloat(rationalString, ref);
383
384            String preliminaryResult = String.valueOf(result);
385            if (usePositiveNegative) {
386                String neg = (ref.equals("S") || ref.equals("E")) ? "-" : "";
387                return neg + preliminaryResult;
388            } else {
389                return preliminaryResult + String.valueOf((char) 186) + " "
390                        + ref;
391            }
392    }
393
394    public static String makeLatLongString(double d) {
395        d = Math.abs(d);
396
397        int degrees = (int) d;
398
399        double remainder = d - degrees;
400        int minutes = (int) (remainder * 60D);
401        // really seconds * 1000
402        int seconds = (int) (((remainder * 60D) - minutes) * 60D * 1000D);
403
404        String retVal = degrees + "/1," + minutes + "/1," + seconds + "/1000";
405        return retVal;
406    }
407
408    public static String makeLatStringRef(double lat) {
409        return lat >= 0D ? "N" : "S";
410    }
411
412    public static String makeLonStringRef(double lon) {
413        return lon >= 0D ? "W" : "E";
414    }
415
416    private native boolean appendThumbnailNative(String fileName,
417            String thumbnailFileName);
418
419    private native void saveAttributesNative(String fileName,
420            String compressedAttributes);
421
422    private native String getAttributesNative(String fileName);
423
424    private native void commitChangesNative(String fileName);
425
426    private native byte[] getThumbnailNative(String fileName);
427}
428