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