1 /* 2 * Copyright 2019 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 androidx.camera.core.impl.utils; 18 19 import android.location.Location; 20 21 import androidx.annotation.VisibleForTesting; 22 import androidx.camera.core.ImageProxy; 23 import androidx.camera.core.Logger; 24 import androidx.exifinterface.media.ExifInterface; 25 26 import org.jspecify.annotations.NonNull; 27 import org.jspecify.annotations.Nullable; 28 29 import java.io.ByteArrayInputStream; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.nio.ByteBuffer; 34 import java.text.ParseException; 35 import java.text.SimpleDateFormat; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Date; 39 import java.util.List; 40 import java.util.Locale; 41 import java.util.Objects; 42 43 /** 44 * Utility class for modifying metadata on JPEG files. 45 * 46 * <p>Call {@link #save()} to persist changes to disc. 47 */ 48 public final class Exif { 49 50 /** Timestamp value indicating a timestamp value that is either not set or not valid */ 51 public static final long INVALID_TIMESTAMP = -1; 52 // Forked from ExifInterface.TAG_THUMBNAIL_ORIENTATION. The value is library-internal so we 53 // can't depend on it directly. 54 public static final String TAG_THUMBNAIL_ORIENTATION = "ThumbnailOrientation"; 55 56 private static final String TAG = Exif.class.getSimpleName(); 57 58 private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 59 new ThreadLocal<SimpleDateFormat>() { 60 @Override 61 public SimpleDateFormat initialValue() { 62 return new SimpleDateFormat("yyyy:MM:dd", Locale.US); 63 } 64 }; 65 private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT = 66 new ThreadLocal<SimpleDateFormat>() { 67 @Override 68 public SimpleDateFormat initialValue() { 69 return new SimpleDateFormat("HH:mm:ss", Locale.US); 70 } 71 }; 72 private static final ThreadLocal<SimpleDateFormat> DATETIME_FORMAT = 73 new ThreadLocal<SimpleDateFormat>() { 74 @Override 75 public SimpleDateFormat initialValue() { 76 return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US); 77 } 78 }; 79 80 private static final String KILOMETERS_PER_HOUR = "K"; 81 private static final String MILES_PER_HOUR = "M"; 82 private static final String KNOTS = "N"; 83 84 /** All public tags in {@link ExifInterface}. */ 85 private static final List<String> ALL_EXIF_TAGS = getAllExifTags(); 86 // Exif tags that should not be copied to the cropped image. 87 private static final List<String> DO_NOT_COPY_EXIF_TAGS = Arrays.asList( 88 // Dimension-related tags, which might change after cropping. 89 ExifInterface.TAG_IMAGE_WIDTH, 90 ExifInterface.TAG_IMAGE_LENGTH, 91 ExifInterface.TAG_PIXEL_X_DIMENSION, 92 ExifInterface.TAG_PIXEL_Y_DIMENSION, 93 // Thumbnail-related tags. Currently we do not create thumbnail for cropped images. 94 ExifInterface.TAG_COMPRESSION, // Our primary image is always Jpeg. 95 ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 96 ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 97 ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, 98 ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, 99 TAG_THUMBNAIL_ORIENTATION); 100 101 private final ExifInterface mExifInterface; 102 103 // When true, avoid saving any time. This is a privacy issue. 104 private boolean mRemoveTimestamp = false; 105 Exif(ExifInterface exifInterface)106 private Exif(ExifInterface exifInterface) { 107 mExifInterface = exifInterface; 108 } 109 110 /** 111 * Returns an Exif from the exif data contained in the file. 112 * 113 * @param file the file to read exif data from 114 */ createFromFile(@onNull File file)115 public static @NonNull Exif createFromFile(@NonNull File file) throws IOException { 116 return createFromFileString(file.toString()); 117 } 118 119 /** 120 * Returns an Exif extracted from the given {@link ImageProxy}. 121 * 122 * <p> This method rewinds and reads the given buffer. 123 */ createFromImageProxy(@onNull ImageProxy imageProxy)124 public static @NonNull Exif createFromImageProxy(@NonNull ImageProxy imageProxy) 125 throws IOException { 126 ByteBuffer buffer = imageProxy.getPlanes()[0].getBuffer(); 127 // Rewind to make sure it is at the beginning of the buffer 128 buffer.rewind(); 129 130 byte[] data = new byte[buffer.capacity()]; 131 buffer.get(data); 132 InputStream inputStream = new ByteArrayInputStream(data); 133 return Exif.createFromInputStream(inputStream); 134 } 135 136 /** 137 * Returns an Exif from the exif data contained in the file at the filePath 138 * 139 * @param filePath the path to the file to read exif data from 140 */ createFromFileString(@onNull String filePath)141 public static @NonNull Exif createFromFileString(@NonNull String filePath) throws IOException { 142 return new Exif(new ExifInterface(filePath)); 143 } 144 145 /** 146 * Returns an Exif from the exif data contain in the input stream. 147 * 148 * @param is the input stream to read exif data from 149 */ createFromInputStream(@onNull InputStream is)150 public static @NonNull Exif createFromInputStream(@NonNull InputStream is) throws IOException { 151 return new Exif(new ExifInterface(is)); 152 } 153 convertToExifDateTime(long timestamp)154 private static String convertToExifDateTime(long timestamp) { 155 return DATETIME_FORMAT.get().format(new Date(timestamp)); 156 } 157 convertFromExifDateTime(String dateTime)158 private static Date convertFromExifDateTime(String dateTime) throws ParseException { 159 return DATETIME_FORMAT.get().parse(dateTime); 160 } 161 convertFromExifDate(String date)162 private static Date convertFromExifDate(String date) throws ParseException { 163 return DATE_FORMAT.get().parse(date); 164 } 165 convertFromExifTime(String time)166 private static Date convertFromExifTime(String time) throws ParseException { 167 return TIME_FORMAT.get().parse(time); 168 } 169 170 /** Persists changes to disc. */ save()171 public void save() throws IOException { 172 if (!mRemoveTimestamp) { 173 attachLastModifiedTimestamp(); 174 } 175 mExifInterface.saveAttributes(); 176 } 177 178 /** 179 * Copies Exif values to the given instance. 180 * 181 * <p> This methods is for copying exif data from the original image to the cropped image. Tags 182 * affected by cropping are not copied. 183 */ copyToCroppedImage(@onNull Exif croppedExif)184 public void copyToCroppedImage(@NonNull Exif croppedExif) { 185 List<String> exifTags = new ArrayList<>(ALL_EXIF_TAGS); 186 exifTags.removeAll(DO_NOT_COPY_EXIF_TAGS); 187 for (String tag : exifTags) { 188 String originalValue = mExifInterface.getAttribute(tag); 189 String croppedExifValue = croppedExif.mExifInterface.getAttribute(tag); 190 if (originalValue != null && !Objects.equals(originalValue, croppedExifValue)) { 191 croppedExif.mExifInterface.setAttribute(tag, originalValue); 192 } 193 } 194 } 195 196 @Override toString()197 public String toString() { 198 return String.format( 199 Locale.ENGLISH, 200 "Exif{width=%s, height=%s, rotation=%d, " 201 + "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, " 202 + "timestamp=%s, description=%s}", 203 getWidth(), 204 getHeight(), 205 getRotation(), 206 isFlippedVertically(), 207 isFlippedHorizontally(), 208 getLocation(), 209 getTimestamp(), 210 getDescription()); 211 } 212 getOrientation()213 public int getOrientation() { 214 return mExifInterface.getAttributeInt( 215 ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 216 } 217 218 /** Sets the orientation for the exif. */ setOrientation(int orientation)219 public void setOrientation(int orientation) { 220 mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); 221 } 222 223 /** Returns the width of the photo in pixels. */ getWidth()224 public int getWidth() { 225 return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); 226 } 227 228 /** Returns the height of the photo in pixels. */ getHeight()229 public int getHeight() { 230 return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); 231 } 232 getDescription()233 public @Nullable String getDescription() { 234 return mExifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION); 235 } 236 237 /** Sets the description for the exif. */ setDescription(@ullable String description)238 public void setDescription(@Nullable String description) { 239 mExifInterface.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, description); 240 } 241 242 /** @return The degree of rotation (eg. 0, 90, 180, 270). */ getRotation()243 public int getRotation() { 244 switch (getOrientation()) { 245 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 246 return 0; 247 case ExifInterface.ORIENTATION_ROTATE_180: 248 return 180; 249 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 250 return 180; 251 case ExifInterface.ORIENTATION_TRANSPOSE: 252 return 270; 253 case ExifInterface.ORIENTATION_ROTATE_90: 254 return 90; 255 case ExifInterface.ORIENTATION_TRANSVERSE: 256 return 90; 257 case ExifInterface.ORIENTATION_ROTATE_270: 258 return 270; 259 case ExifInterface.ORIENTATION_NORMAL: 260 // Fall-through 261 case ExifInterface.ORIENTATION_UNDEFINED: 262 // Fall-through 263 default: 264 return 0; 265 } 266 } 267 268 /** @return True if the image is flipped vertically after rotation. */ isFlippedVertically()269 public boolean isFlippedVertically() { 270 switch (getOrientation()) { 271 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 272 return false; 273 case ExifInterface.ORIENTATION_ROTATE_180: 274 return false; 275 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 276 return true; 277 case ExifInterface.ORIENTATION_TRANSPOSE: 278 return true; 279 case ExifInterface.ORIENTATION_ROTATE_90: 280 return false; 281 case ExifInterface.ORIENTATION_TRANSVERSE: 282 return true; 283 case ExifInterface.ORIENTATION_ROTATE_270: 284 return false; 285 case ExifInterface.ORIENTATION_NORMAL: 286 // Fall-through 287 case ExifInterface.ORIENTATION_UNDEFINED: 288 // Fall-through 289 default: 290 return false; 291 } 292 } 293 294 /** @return True if the image is flipped horizontally after rotation. */ isFlippedHorizontally()295 public boolean isFlippedHorizontally() { 296 switch (getOrientation()) { 297 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 298 return true; 299 case ExifInterface.ORIENTATION_ROTATE_180: 300 return false; 301 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 302 return false; 303 case ExifInterface.ORIENTATION_TRANSPOSE: 304 return false; 305 case ExifInterface.ORIENTATION_ROTATE_90: 306 return false; 307 case ExifInterface.ORIENTATION_TRANSVERSE: 308 return false; 309 case ExifInterface.ORIENTATION_ROTATE_270: 310 return false; 311 case ExifInterface.ORIENTATION_NORMAL: 312 // Fall-through 313 case ExifInterface.ORIENTATION_UNDEFINED: 314 // Fall-through 315 default: 316 return false; 317 } 318 } 319 attachLastModifiedTimestamp()320 private void attachLastModifiedTimestamp() { 321 long now = System.currentTimeMillis(); 322 String datetime = convertToExifDateTime(now); 323 324 mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime); 325 326 try { 327 String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime()); 328 mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, subsec); 329 } catch (ParseException e) { 330 } 331 } 332 333 /** 334 * @return The timestamp (in millis) that this picture was modified, or {@link 335 * #INVALID_TIMESTAMP} if no time is available. 336 */ getLastModifiedTimestamp()337 public long getLastModifiedTimestamp() { 338 long timestamp = parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME)); 339 if (timestamp == INVALID_TIMESTAMP) { 340 return INVALID_TIMESTAMP; 341 } 342 343 String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME); 344 if (subSecs != null) { 345 try { 346 long sub = Long.parseLong(subSecs); 347 while (sub > 1000) { 348 sub /= 10; 349 } 350 timestamp += sub; 351 } catch (NumberFormatException e) { 352 // Ignored 353 } 354 } 355 356 return timestamp; 357 } 358 359 /** 360 * @return The timestamp (in millis) that this picture was taken, or {@link #INVALID_TIMESTAMP} 361 * if no time is available. 362 */ getTimestamp()363 public long getTimestamp() { 364 long timestamp = 365 parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); 366 if (timestamp == INVALID_TIMESTAMP) { 367 return INVALID_TIMESTAMP; 368 } 369 370 String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL); 371 if (subSecs != null) { 372 try { 373 long sub = Long.parseLong(subSecs); 374 while (sub > 1000) { 375 sub /= 10; 376 } 377 timestamp += sub; 378 } catch (NumberFormatException e) { 379 // Ignored 380 } 381 } 382 383 return timestamp; 384 } 385 386 /** @return The location this picture was taken, or null if no location is available. */ getLocation()387 public @Nullable Location getLocation() { 388 String provider = mExifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD); 389 double[] latlng = mExifInterface.getLatLong(); 390 double altitude = mExifInterface.getAltitude(0); 391 double speed = mExifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0); 392 String speedRef = mExifInterface.getAttribute(ExifInterface.TAG_GPS_SPEED_REF); 393 speedRef = speedRef == null ? KILOMETERS_PER_HOUR : speedRef; // Ensure speedRef is not null 394 long timestamp = 395 parseTimestamp( 396 mExifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP), 397 mExifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP)); 398 if (latlng == null) { 399 return null; 400 } 401 if (provider == null) { 402 provider = TAG; 403 } 404 405 Location location = new Location(provider); 406 location.setLatitude(latlng[0]); 407 location.setLongitude(latlng[1]); 408 if (altitude != 0) { 409 location.setAltitude(altitude); 410 } 411 if (speed != 0) { 412 switch (speedRef) { 413 case MILES_PER_HOUR: 414 speed = Speed.fromMilesPerHour(speed).toMetersPerSecond(); 415 break; 416 case KNOTS: 417 speed = Speed.fromKnots(speed).toMetersPerSecond(); 418 break; 419 case KILOMETERS_PER_HOUR: 420 // fall through 421 default: 422 speed = Speed.fromKilometersPerHour(speed).toMetersPerSecond(); 423 break; 424 } 425 426 location.setSpeed((float) speed); 427 } 428 if (timestamp != INVALID_TIMESTAMP) { 429 location.setTime(timestamp); 430 } 431 return location; 432 } 433 434 /** 435 * Rotates the image by the given degrees. Can only rotate by right angles (eg. 90, 180, -90). 436 * Other increments will set the orientation to undefined. 437 */ rotate(int degrees)438 public void rotate(int degrees) { 439 if (degrees % 90 != 0) { 440 Logger.w( 441 TAG, 442 String.format(Locale.US, 443 "Can only rotate in right angles (eg. 0, 90, 180, 270). %d is " 444 + "unsupported.", 445 degrees)); 446 mExifInterface.setAttribute( 447 ExifInterface.TAG_ORIENTATION, 448 String.valueOf(ExifInterface.ORIENTATION_UNDEFINED)); 449 return; 450 } 451 452 degrees %= 360; 453 454 int orientation = getOrientation(); 455 while (degrees < 0) { 456 degrees += 90; 457 458 switch (orientation) { 459 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 460 orientation = ExifInterface.ORIENTATION_TRANSPOSE; 461 break; 462 case ExifInterface.ORIENTATION_ROTATE_180: 463 orientation = ExifInterface.ORIENTATION_ROTATE_90; 464 break; 465 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 466 orientation = ExifInterface.ORIENTATION_TRANSVERSE; 467 break; 468 case ExifInterface.ORIENTATION_TRANSPOSE: 469 orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; 470 break; 471 case ExifInterface.ORIENTATION_ROTATE_90: 472 orientation = ExifInterface.ORIENTATION_NORMAL; 473 break; 474 case ExifInterface.ORIENTATION_TRANSVERSE: 475 orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; 476 break; 477 case ExifInterface.ORIENTATION_ROTATE_270: 478 orientation = ExifInterface.ORIENTATION_ROTATE_90; 479 break; 480 case ExifInterface.ORIENTATION_NORMAL: 481 // Fall-through 482 case ExifInterface.ORIENTATION_UNDEFINED: 483 // Fall-through 484 default: 485 orientation = ExifInterface.ORIENTATION_ROTATE_270; 486 break; 487 } 488 } 489 while (degrees > 0) { 490 degrees -= 90; 491 492 switch (orientation) { 493 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 494 orientation = ExifInterface.ORIENTATION_TRANSVERSE; 495 break; 496 case ExifInterface.ORIENTATION_ROTATE_180: 497 orientation = ExifInterface.ORIENTATION_ROTATE_270; 498 break; 499 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 500 orientation = ExifInterface.ORIENTATION_TRANSPOSE; 501 break; 502 case ExifInterface.ORIENTATION_TRANSPOSE: 503 orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; 504 break; 505 case ExifInterface.ORIENTATION_ROTATE_90: 506 orientation = ExifInterface.ORIENTATION_ROTATE_180; 507 break; 508 case ExifInterface.ORIENTATION_TRANSVERSE: 509 orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; 510 break; 511 case ExifInterface.ORIENTATION_ROTATE_270: 512 orientation = ExifInterface.ORIENTATION_NORMAL; 513 break; 514 case ExifInterface.ORIENTATION_NORMAL: 515 // Fall-through 516 case ExifInterface.ORIENTATION_UNDEFINED: 517 // Fall-through 518 default: 519 orientation = ExifInterface.ORIENTATION_ROTATE_90; 520 break; 521 } 522 } 523 mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); 524 } 525 526 /** 527 * Sets attributes to represent a flip of the image over the horizon so that the top and bottom 528 * are reversed. 529 */ flipVertically()530 public void flipVertically() { 531 int orientation; 532 switch (getOrientation()) { 533 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 534 orientation = ExifInterface.ORIENTATION_ROTATE_180; 535 break; 536 case ExifInterface.ORIENTATION_ROTATE_180: 537 orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; 538 break; 539 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 540 orientation = ExifInterface.ORIENTATION_NORMAL; 541 break; 542 case ExifInterface.ORIENTATION_TRANSPOSE: 543 orientation = ExifInterface.ORIENTATION_ROTATE_270; 544 break; 545 case ExifInterface.ORIENTATION_ROTATE_90: 546 orientation = ExifInterface.ORIENTATION_TRANSVERSE; 547 break; 548 case ExifInterface.ORIENTATION_TRANSVERSE: 549 orientation = ExifInterface.ORIENTATION_ROTATE_90; 550 break; 551 case ExifInterface.ORIENTATION_ROTATE_270: 552 orientation = ExifInterface.ORIENTATION_TRANSPOSE; 553 break; 554 case ExifInterface.ORIENTATION_NORMAL: 555 // Fall-through 556 case ExifInterface.ORIENTATION_UNDEFINED: 557 // Fall-through 558 default: 559 orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; 560 break; 561 } 562 mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); 563 } 564 565 /** 566 * Sets attributes to represent a flip of the image over the vertical so that the left and right 567 * are reversed. 568 */ flipHorizontally()569 public void flipHorizontally() { 570 int orientation; 571 switch (getOrientation()) { 572 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 573 orientation = ExifInterface.ORIENTATION_NORMAL; 574 break; 575 case ExifInterface.ORIENTATION_ROTATE_180: 576 orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; 577 break; 578 case ExifInterface.ORIENTATION_FLIP_VERTICAL: 579 orientation = ExifInterface.ORIENTATION_ROTATE_180; 580 break; 581 case ExifInterface.ORIENTATION_TRANSPOSE: 582 orientation = ExifInterface.ORIENTATION_ROTATE_90; 583 break; 584 case ExifInterface.ORIENTATION_ROTATE_90: 585 orientation = ExifInterface.ORIENTATION_TRANSPOSE; 586 break; 587 case ExifInterface.ORIENTATION_TRANSVERSE: 588 orientation = ExifInterface.ORIENTATION_ROTATE_270; 589 break; 590 case ExifInterface.ORIENTATION_ROTATE_270: 591 orientation = ExifInterface.ORIENTATION_TRANSVERSE; 592 break; 593 case ExifInterface.ORIENTATION_NORMAL: 594 // Fall-through 595 case ExifInterface.ORIENTATION_UNDEFINED: 596 // Fall-through 597 default: 598 orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; 599 break; 600 } 601 mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); 602 } 603 604 @VisibleForTesting getMetadata()605 public @Nullable String getMetadata() { 606 return mExifInterface.getAttribute(ExifInterface.TAG_XMP); 607 } 608 609 @VisibleForTesting getExifInterface()610 public @NonNull ExifInterface getExifInterface() { 611 return mExifInterface; 612 } 613 614 /** Attaches the current timestamp to the file. */ attachTimestamp()615 public void attachTimestamp() { 616 long now = System.currentTimeMillis(); 617 String datetime = convertToExifDateTime(now); 618 619 mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime); 620 mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime); 621 622 try { 623 String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime()); 624 mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec); 625 mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec); 626 } catch (ParseException e) { 627 } 628 629 mRemoveTimestamp = false; 630 } 631 632 /** Removes the timestamp from the file. */ removeTimestamp()633 public void removeTimestamp() { 634 mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, null); 635 mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null); 636 mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null); 637 mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null); 638 mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null); 639 mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null); 640 mRemoveTimestamp = true; 641 } 642 643 /** Attaches the given location to the file. */ attachLocation(@onNull Location location)644 public void attachLocation(@NonNull Location location) { 645 mExifInterface.setGpsInfo(location); 646 } 647 648 /** Removes the location from the file. */ removeLocation()649 public void removeLocation() { 650 mExifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null); 651 mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null); 652 mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null); 653 mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null); 654 mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null); 655 mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null); 656 mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null); 657 mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, null); 658 mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null); 659 mExifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null); 660 mExifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null); 661 } 662 663 /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */ parseTimestamp(@ullable String date, @Nullable String time)664 private long parseTimestamp(@Nullable String date, @Nullable String time) { 665 if (date == null && time == null) { 666 return INVALID_TIMESTAMP; 667 } 668 if (time == null) { 669 try { 670 return convertFromExifDate(date).getTime(); 671 } catch (ParseException e) { 672 return INVALID_TIMESTAMP; 673 } 674 } 675 if (date == null) { 676 try { 677 return convertFromExifTime(time).getTime(); 678 } catch (ParseException e) { 679 return INVALID_TIMESTAMP; 680 } 681 } 682 return parseTimestamp(date + " " + time); 683 } 684 685 /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */ parseTimestamp(@ullable String datetime)686 private long parseTimestamp(@Nullable String datetime) { 687 if (datetime == null) { 688 return INVALID_TIMESTAMP; 689 } 690 try { 691 return convertFromExifDateTime(datetime).getTime(); 692 } catch (ParseException e) { 693 return INVALID_TIMESTAMP; 694 } 695 } 696 697 private static final class Speed { fromKilometersPerHour(double kph)698 static Converter fromKilometersPerHour(double kph) { 699 return new Converter(kph * 0.621371); 700 } fromMilesPerHour(double mph)701 static Converter fromMilesPerHour(double mph) { 702 return new Converter(mph); 703 } 704 fromKnots(double knots)705 static Converter fromKnots(double knots) { 706 return new Converter(knots * 1.15078); 707 } 708 709 static final class Converter { 710 final double mMph; 711 Converter(double mph)712 Converter(double mph) { 713 mMph = mph; 714 } 715 toMetersPerSecond()716 double toMetersPerSecond() { 717 return mMph / 2.23694; 718 } 719 } 720 } 721 722 /** 723 * Creates a list that contains all public tags defined in {@link ExifInterface}. 724 * 725 * <p> Deprecated tags are not included. 726 */ getAllExifTags()727 public static @NonNull List<String> getAllExifTags() { 728 return Arrays.asList( 729 ExifInterface.TAG_IMAGE_WIDTH, 730 ExifInterface.TAG_IMAGE_LENGTH, 731 ExifInterface.TAG_BITS_PER_SAMPLE, 732 ExifInterface.TAG_COMPRESSION, 733 ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, 734 ExifInterface.TAG_ORIENTATION, 735 ExifInterface.TAG_SAMPLES_PER_PIXEL, 736 ExifInterface.TAG_PLANAR_CONFIGURATION, 737 ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, 738 ExifInterface.TAG_Y_CB_CR_POSITIONING, 739 ExifInterface.TAG_X_RESOLUTION, 740 ExifInterface.TAG_Y_RESOLUTION, 741 ExifInterface.TAG_RESOLUTION_UNIT, 742 ExifInterface.TAG_STRIP_OFFSETS, 743 ExifInterface.TAG_ROWS_PER_STRIP, 744 ExifInterface.TAG_STRIP_BYTE_COUNTS, 745 ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 746 ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 747 ExifInterface.TAG_TRANSFER_FUNCTION, 748 ExifInterface.TAG_WHITE_POINT, 749 ExifInterface.TAG_PRIMARY_CHROMATICITIES, 750 ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, 751 ExifInterface.TAG_REFERENCE_BLACK_WHITE, 752 ExifInterface.TAG_DATETIME, 753 ExifInterface.TAG_IMAGE_DESCRIPTION, 754 ExifInterface.TAG_MAKE, 755 ExifInterface.TAG_MODEL, 756 ExifInterface.TAG_SOFTWARE, 757 ExifInterface.TAG_ARTIST, 758 ExifInterface.TAG_COPYRIGHT, 759 ExifInterface.TAG_EXIF_VERSION, 760 ExifInterface.TAG_FLASHPIX_VERSION, 761 ExifInterface.TAG_COLOR_SPACE, 762 ExifInterface.TAG_GAMMA, 763 ExifInterface.TAG_PIXEL_X_DIMENSION, 764 ExifInterface.TAG_PIXEL_Y_DIMENSION, 765 ExifInterface.TAG_COMPONENTS_CONFIGURATION, 766 ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, 767 ExifInterface.TAG_MAKER_NOTE, 768 ExifInterface.TAG_USER_COMMENT, 769 ExifInterface.TAG_RELATED_SOUND_FILE, 770 ExifInterface.TAG_DATETIME_ORIGINAL, 771 ExifInterface.TAG_DATETIME_DIGITIZED, 772 ExifInterface.TAG_OFFSET_TIME, 773 ExifInterface.TAG_OFFSET_TIME_ORIGINAL, 774 ExifInterface.TAG_OFFSET_TIME_DIGITIZED, 775 ExifInterface.TAG_SUBSEC_TIME, 776 ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, 777 ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, 778 ExifInterface.TAG_EXPOSURE_TIME, 779 ExifInterface.TAG_F_NUMBER, 780 ExifInterface.TAG_EXPOSURE_PROGRAM, 781 ExifInterface.TAG_SPECTRAL_SENSITIVITY, 782 ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, 783 ExifInterface.TAG_OECF, 784 ExifInterface.TAG_SENSITIVITY_TYPE, 785 ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, 786 ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, 787 ExifInterface.TAG_ISO_SPEED, 788 ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, 789 ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, 790 ExifInterface.TAG_SHUTTER_SPEED_VALUE, 791 ExifInterface.TAG_APERTURE_VALUE, 792 ExifInterface.TAG_BRIGHTNESS_VALUE, 793 ExifInterface.TAG_EXPOSURE_BIAS_VALUE, 794 ExifInterface.TAG_MAX_APERTURE_VALUE, 795 ExifInterface.TAG_SUBJECT_DISTANCE, 796 ExifInterface.TAG_METERING_MODE, 797 ExifInterface.TAG_LIGHT_SOURCE, 798 ExifInterface.TAG_FLASH, 799 ExifInterface.TAG_SUBJECT_AREA, 800 ExifInterface.TAG_FOCAL_LENGTH, 801 ExifInterface.TAG_FLASH_ENERGY, 802 ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, 803 ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, 804 ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, 805 ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, 806 ExifInterface.TAG_SUBJECT_LOCATION, 807 ExifInterface.TAG_EXPOSURE_INDEX, 808 ExifInterface.TAG_SENSING_METHOD, 809 ExifInterface.TAG_FILE_SOURCE, 810 ExifInterface.TAG_SCENE_TYPE, 811 ExifInterface.TAG_CFA_PATTERN, 812 ExifInterface.TAG_CUSTOM_RENDERED, 813 ExifInterface.TAG_EXPOSURE_MODE, 814 ExifInterface.TAG_WHITE_BALANCE, 815 ExifInterface.TAG_DIGITAL_ZOOM_RATIO, 816 ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, 817 ExifInterface.TAG_SCENE_CAPTURE_TYPE, 818 ExifInterface.TAG_GAIN_CONTROL, 819 ExifInterface.TAG_CONTRAST, 820 ExifInterface.TAG_SATURATION, 821 ExifInterface.TAG_SHARPNESS, 822 ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, 823 ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, 824 ExifInterface.TAG_IMAGE_UNIQUE_ID, 825 ExifInterface.TAG_CAMERA_OWNER_NAME, 826 ExifInterface.TAG_BODY_SERIAL_NUMBER, 827 ExifInterface.TAG_LENS_SPECIFICATION, 828 ExifInterface.TAG_LENS_MAKE, 829 ExifInterface.TAG_LENS_MODEL, 830 ExifInterface.TAG_LENS_SERIAL_NUMBER, 831 ExifInterface.TAG_GPS_VERSION_ID, 832 ExifInterface.TAG_GPS_LATITUDE_REF, 833 ExifInterface.TAG_GPS_LATITUDE, 834 ExifInterface.TAG_GPS_LONGITUDE_REF, 835 ExifInterface.TAG_GPS_LONGITUDE, 836 ExifInterface.TAG_GPS_ALTITUDE_REF, 837 ExifInterface.TAG_GPS_ALTITUDE, 838 ExifInterface.TAG_GPS_TIMESTAMP, 839 ExifInterface.TAG_GPS_SATELLITES, 840 ExifInterface.TAG_GPS_STATUS, 841 ExifInterface.TAG_GPS_MEASURE_MODE, 842 ExifInterface.TAG_GPS_DOP, 843 ExifInterface.TAG_GPS_SPEED_REF, 844 ExifInterface.TAG_GPS_SPEED, 845 ExifInterface.TAG_GPS_TRACK_REF, 846 ExifInterface.TAG_GPS_TRACK, 847 ExifInterface.TAG_GPS_IMG_DIRECTION_REF, 848 ExifInterface.TAG_GPS_IMG_DIRECTION, 849 ExifInterface.TAG_GPS_MAP_DATUM, 850 ExifInterface.TAG_GPS_DEST_LATITUDE_REF, 851 ExifInterface.TAG_GPS_DEST_LATITUDE, 852 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, 853 ExifInterface.TAG_GPS_DEST_LONGITUDE, 854 ExifInterface.TAG_GPS_DEST_BEARING_REF, 855 ExifInterface.TAG_GPS_DEST_BEARING, 856 ExifInterface.TAG_GPS_DEST_DISTANCE_REF, 857 ExifInterface.TAG_GPS_DEST_DISTANCE, 858 ExifInterface.TAG_GPS_PROCESSING_METHOD, 859 ExifInterface.TAG_GPS_AREA_INFORMATION, 860 ExifInterface.TAG_GPS_DATESTAMP, 861 ExifInterface.TAG_GPS_DIFFERENTIAL, 862 ExifInterface.TAG_GPS_H_POSITIONING_ERROR, 863 ExifInterface.TAG_INTEROPERABILITY_INDEX, 864 ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, 865 ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, 866 TAG_THUMBNAIL_ORIENTATION, 867 ExifInterface.TAG_DNG_VERSION, 868 ExifInterface.TAG_DEFAULT_CROP_SIZE, 869 ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, 870 ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, 871 ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, 872 ExifInterface.TAG_ORF_ASPECT_FRAME, 873 ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, 874 ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, 875 ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, 876 ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, 877 ExifInterface.TAG_RW2_ISO, 878 ExifInterface.TAG_RW2_JPG_FROM_RAW, 879 ExifInterface.TAG_XMP, 880 ExifInterface.TAG_NEW_SUBFILE_TYPE, 881 ExifInterface.TAG_SUBFILE_TYPE); 882 } 883 } 884 885