Home | History | Annotate | Download | only in media
      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 
     17 package android.media;
     18 
     19 import java.io.IOException;
     20 import java.text.ParsePosition;
     21 import java.text.SimpleDateFormat;
     22 import java.util.Date;
     23 import java.util.HashMap;
     24 import java.util.Map;
     25 import java.util.TimeZone;
     26 
     27 /**
     28  * This is a class for reading and writing Exif tags in a JPEG file.
     29  */
     30 public 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             output[0] = convertRationalLatLonToFloat(latValue, latRef);
    297             output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
    298             return true;
    299         } else {
    300             return false;
    301         }
    302     }
    303 
    304     /**
    305      * Return the altitude in meters. If the exif tag does not exist, return
    306      * <var>defaultValue</var>.
    307      *
    308      * @param defaultValue the value to return if the tag is not available.
    309      */
    310     public double getAltitude(double defaultValue) {
    311         double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
    312         int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
    313 
    314         if (altitude >= 0 && ref >= 0) {
    315             return (double) (altitude * ((ref == 1) ? -1 : 1));
    316         } else {
    317             return defaultValue;
    318         }
    319     }
    320 
    321     /**
    322      * Returns number of milliseconds since Jan. 1, 1970, midnight.
    323      * Returns -1 if the date time information if not available.
    324      * @hide
    325      */
    326     public long getDateTime() {
    327         String dateTimeString = mAttributes.get(TAG_DATETIME);
    328         if (dateTimeString == null) return -1;
    329 
    330         ParsePosition pos = new ParsePosition(0);
    331         try {
    332             Date datetime = sFormatter.parse(dateTimeString, pos);
    333             if (datetime == null) return -1;
    334             return datetime.getTime();
    335         } catch (IllegalArgumentException ex) {
    336             return -1;
    337         }
    338     }
    339 
    340     /**
    341      * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
    342      * Returns -1 if the date time information if not available.
    343      * @hide
    344      */
    345     public long getGpsDateTime() {
    346         String date = mAttributes.get(TAG_GPS_DATESTAMP);
    347         String time = mAttributes.get(TAG_GPS_TIMESTAMP);
    348         if (date == null || time == null) return -1;
    349 
    350         String dateTimeString = date + ' ' + time;
    351         if (dateTimeString == null) return -1;
    352 
    353         ParsePosition pos = new ParsePosition(0);
    354         try {
    355             Date datetime = sFormatter.parse(dateTimeString, pos);
    356             if (datetime == null) return -1;
    357             return datetime.getTime();
    358         } catch (IllegalArgumentException ex) {
    359             return -1;
    360         }
    361     }
    362 
    363     private static float convertRationalLatLonToFloat(
    364             String rationalString, String ref) {
    365         try {
    366             String [] parts = rationalString.split(",");
    367 
    368             String [] pair;
    369             pair = parts[0].split("/");
    370             int degrees = (int) (Float.parseFloat(pair[0].trim())
    371                     / Float.parseFloat(pair[1].trim()));
    372 
    373             pair = parts[1].split("/");
    374             int minutes = (int) ((Float.parseFloat(pair[0].trim())
    375                     / Float.parseFloat(pair[1].trim())));
    376 
    377             pair = parts[2].split("/");
    378             double seconds = Double.parseDouble(pair[0].trim())
    379                     / Double.parseDouble(pair[1].trim());
    380 
    381             double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
    382             if ((ref.equals("S") || ref.equals("W"))) {
    383                 return (float) -result;
    384             }
    385             return (float) result;
    386         } catch (RuntimeException ex) {
    387             // if for whatever reason we can't parse the lat long then return
    388             // null
    389             return 0f;
    390         }
    391     }
    392 
    393     private native boolean appendThumbnailNative(String fileName,
    394             String thumbnailFileName);
    395 
    396     private native void saveAttributesNative(String fileName,
    397             String compressedAttributes);
    398 
    399     private native String getAttributesNative(String fileName);
    400 
    401     private native void commitChangesNative(String fileName);
    402 
    403     private native byte[] getThumbnailNative(String fileName);
    404 }
    405