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