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