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