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     /** 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("exif_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      * Stores the latitude and longitude value in a float array. The first element is
    295      * the latitude, and the second element is the longitude. Returns false if the
    296      * Exif tags are not available.
    297      */
    298     public boolean getLatLong(float output[]) {
    299         String latValue = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE);
    300         String latRef = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE_REF);
    301         String lngValue = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE);
    302         String lngRef = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE_REF);
    303 
    304         if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
    305             try {
    306                 output[0] = convertRationalLatLonToFloat(latValue, latRef);
    307                 output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
    308                 return true;
    309             } catch (IllegalArgumentException e) {
    310                 // if values are not parseable
    311             }
    312         }
    313 
    314         return false;
    315     }
    316 
    317     /**
    318      * Return the altitude in meters. If the exif tag does not exist, return
    319      * <var>defaultValue</var>.
    320      *
    321      * @param defaultValue the value to return if the tag is not available.
    322      */
    323     public double getAltitude(double defaultValue) {
    324         double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
    325         int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
    326 
    327         if (altitude >= 0 && ref >= 0) {
    328             return (double) (altitude * ((ref == 1) ? -1 : 1));
    329         } else {
    330             return defaultValue;
    331         }
    332     }
    333 
    334     /**
    335      * Returns number of milliseconds since Jan. 1, 1970, midnight.
    336      * Returns -1 if the date time information if not available.
    337      * @hide
    338      */
    339     public long getDateTime() {
    340         String dateTimeString = mAttributes.get(TAG_DATETIME);
    341         if (dateTimeString == null) return -1;
    342 
    343         ParsePosition pos = new ParsePosition(0);
    344         try {
    345             Date datetime = sFormatter.parse(dateTimeString, pos);
    346             if (datetime == null) return -1;
    347             return datetime.getTime();
    348         } catch (IllegalArgumentException ex) {
    349             return -1;
    350         }
    351     }
    352 
    353     /**
    354      * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
    355      * Returns -1 if the date time information if not available.
    356      * @hide
    357      */
    358     public long getGpsDateTime() {
    359         String date = mAttributes.get(TAG_GPS_DATESTAMP);
    360         String time = mAttributes.get(TAG_GPS_TIMESTAMP);
    361         if (date == null || time == null) return -1;
    362 
    363         String dateTimeString = date + ' ' + time;
    364         if (dateTimeString == null) return -1;
    365 
    366         ParsePosition pos = new ParsePosition(0);
    367         try {
    368             Date datetime = sFormatter.parse(dateTimeString, pos);
    369             if (datetime == null) return -1;
    370             return datetime.getTime();
    371         } catch (IllegalArgumentException ex) {
    372             return -1;
    373         }
    374     }
    375 
    376     private static float convertRationalLatLonToFloat(
    377             String rationalString, String ref) {
    378         try {
    379             String [] parts = rationalString.split(",");
    380 
    381             String [] pair;
    382             pair = parts[0].split("/");
    383             double degrees = Double.parseDouble(pair[0].trim())
    384                     / Double.parseDouble(pair[1].trim());
    385 
    386             pair = parts[1].split("/");
    387             double minutes = Double.parseDouble(pair[0].trim())
    388                     / Double.parseDouble(pair[1].trim());
    389 
    390             pair = parts[2].split("/");
    391             double seconds = Double.parseDouble(pair[0].trim())
    392                     / Double.parseDouble(pair[1].trim());
    393 
    394             double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
    395             if ((ref.equals("S") || ref.equals("W"))) {
    396                 return (float) -result;
    397             }
    398             return (float) result;
    399         } catch (NumberFormatException e) {
    400             // Some of the nubmers are not valid
    401             throw new IllegalArgumentException();
    402         } catch (ArrayIndexOutOfBoundsException e) {
    403             // Some of the rational does not follow the correct format
    404             throw new IllegalArgumentException();
    405         }
    406     }
    407 
    408     private native boolean appendThumbnailNative(String fileName,
    409             String thumbnailFileName);
    410 
    411     private native void saveAttributesNative(String fileName,
    412             String compressedAttributes);
    413 
    414     private native String getAttributesNative(String fileName);
    415 
    416     private native void commitChangesNative(String fileName);
    417 
    418     private native byte[] getThumbnailNative(String fileName);
    419 }
    420