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