Browse Source

Version 5.2.4

Threema 1 year ago
parent
commit
43be0a655a
100 changed files with 10027 additions and 7527 deletions
  1. 1 7
      .editorconfig
  2. 17 14
      app/build.gradle
  3. 1 1
      app/jni/Android.mk
  4. 1 1
      app/jni/Application.mk
  5. 3 1
      app/src/libre/play/release-notes/de/default.txt
  6. 3 1
      app/src/libre/play/release-notes/en-US/default.txt
  7. 2 32
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  8. 2 2
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  9. 68 28
      app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java
  10. 18 1
      app/src/main/java/ch/threema/app/activities/MediaViewerActivity.java
  11. 1 1
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  12. 14 2
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  13. 7 0
      app/src/main/java/ch/threema/app/activities/SimpleWebViewActivity.java
  14. 20 6
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  15. 10 0
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  16. 42 45
      app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.kt
  17. 12 0
      app/src/main/java/ch/threema/app/adapters/MessageListAdapter.java
  18. 7 5
      app/src/main/java/ch/threema/app/adapters/SendMediaAdapter.kt
  19. 7 3
      app/src/main/java/ch/threema/app/adapters/SendMediaPreviewAdapter.kt
  20. 9 7
      app/src/main/java/ch/threema/app/adapters/decorators/AnimGifChatAdapterDecorator.java
  21. 272 0
      app/src/main/java/ch/threema/app/adapters/decorators/AnimatedImageDrawableDecorator.java
  22. 11 1
      app/src/main/java/ch/threema/app/adapters/decorators/ChatAdapterDecorator.java
  23. 0 2
      app/src/main/java/ch/threema/app/adapters/decorators/FileChatAdapterDecorator.java
  24. 4 1
      app/src/main/java/ch/threema/app/adapters/decorators/ImageChatAdapterDecorator.java
  25. 3 1
      app/src/main/java/ch/threema/app/adapters/decorators/VideoChatAdapterDecorator.java
  26. 10 10
      app/src/main/java/ch/threema/app/backuprestore/csv/BackupService.java
  27. 3 1
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java
  28. 10 8
      app/src/main/java/ch/threema/app/camera/VideoEditView.java
  29. 2 0
      app/src/main/java/ch/threema/app/emojis/EmojiPicker.java
  30. 34 34
      app/src/main/java/ch/threema/app/fragments/BigMediaFragment.kt
  31. 9 2
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  32. 33 15
      app/src/main/java/ch/threema/app/fragments/mediaviews/ImageViewFragment.java
  33. 4 1
      app/src/main/java/ch/threema/app/glide/AvatarGlideModule.java
  34. 28 2
      app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.java
  35. 22 4
      app/src/main/java/ch/threema/app/mediaattacher/ImagePreviewFragment.java
  36. 3 1
      app/src/main/java/ch/threema/app/mediaattacher/ImagePreviewPagerAdapter.java
  37. 13 4
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachAdapter.java
  38. 17 7
      app/src/main/java/ch/threema/app/mediaattacher/MediaRepository.java
  39. 9 0
      app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java
  40. 3 1
      app/src/main/java/ch/threema/app/mediaattacher/VideoPreviewFragment.java
  41. 3 0
      app/src/main/java/ch/threema/app/messagereceiver/GroupMessageReceiver.java
  42. 4 0
      app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt
  43. 20 6
      app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java
  44. 6 1
      app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java
  45. 8 3
      app/src/main/java/ch/threema/app/services/GroupServiceImpl.java
  46. 18 2
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  47. 15 4
      app/src/main/java/ch/threema/app/services/PreferenceService.java
  48. 31 1
      app/src/main/java/ch/threema/app/services/PreferenceServiceImpl.java
  49. 206 0
      app/src/main/java/ch/threema/app/services/messageplayer/AnimatedImageDrawableMessagePlayer.java
  50. 4 4
      app/src/main/java/ch/threema/app/services/messageplayer/GifMessagePlayer.java
  51. 13 1
      app/src/main/java/ch/threema/app/services/messageplayer/MessagePlayerServiceImpl.java
  52. 44 12
      app/src/main/java/ch/threema/app/ui/AvatarEditView.java
  53. 6 7
      app/src/main/java/ch/threema/app/ui/MediaItem.java
  54. 9 3
      app/src/main/java/ch/threema/app/ui/PaintView.java
  55. 28 2
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  56. 8090 7016
      app/src/main/java/ch/threema/app/utils/ExifInterface.java
  57. 204 0
      app/src/main/java/ch/threema/app/utils/ExifInterfaceUtils.java
  58. 73 39
      app/src/main/java/ch/threema/app/utils/FileUtil.java
  59. 34 9
      app/src/main/java/ch/threema/app/utils/ImageViewUtil.java
  60. 44 19
      app/src/main/java/ch/threema/app/utils/MimeUtil.java
  61. 1 1
      app/src/main/java/ch/threema/app/utils/ThumbnailUtil.java
  62. 1 0
      app/src/main/java/ch/threema/app/voip/Config.java
  63. 48 10
      app/src/main/java/ch/threema/app/voip/VoipBluetoothManager.java
  64. 7 4
      app/src/main/java/ch/threema/app/voip/groupcall/GroupCallManagerImpl.kt
  65. 4 13
      app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallService.kt
  66. 39 0
      app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallServiceBinder.kt
  67. 2 2
      app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallServiceConnection.kt
  68. 2 2
      app/src/main/java/ch/threema/app/webclient/converter/Message.java
  69. 1 0
      app/src/main/java/ch/threema/storage/models/data/media/FileDataModel.java
  70. 10 0
      app/src/main/res/drawable/ic_ink_highlighter_outline.xml
  71. 22 0
      app/src/main/res/drawable/ic_webp.xml
  72. 2 1
      app/src/main/res/layout/activity_global_search.xml
  73. 4 3
      app/src/main/res/layout/activity_media_viewer.xml
  74. 1 2
      app/src/main/res/layout/conversation_bubble_footer_groupack_recv.xml
  75. 2 4
      app/src/main/res/layout/conversation_bubble_footer_groupack_send.xml
  76. 1 1
      app/src/main/res/layout/conversation_list_item_media.xml
  77. 0 1
      app/src/main/res/layout/conversation_list_item_media_recv.xml
  78. 0 1
      app/src/main/res/layout/conversation_list_item_media_send.xml
  79. 1 1
      app/src/main/res/layout/conversation_list_item_video_send.xml
  80. 2 2
      app/src/main/res/layout/fragment_backup_data.xml
  81. 1 1
      app/src/main/res/layout/fragment_backup_threema_safe.xml
  82. 9 1
      app/src/main/res/layout/fragment_image_preview.xml
  83. 2 2
      app/src/main/res/layout/item_media_attach_gallery.xml
  84. 2 2
      app/src/main/res/layout/item_media_gallery.xml
  85. 14 21
      app/src/main/res/layout/item_message_list.xml
  86. 11 2
      app/src/main/res/menu/activity_image_paint.xml
  87. 38 6
      app/src/main/res/values-be-rBY/strings.xml
  88. 20 3
      app/src/main/res/values-cs/strings.xml
  89. 6 3
      app/src/main/res/values-de/strings.xml
  90. 7 2
      app/src/main/res/values-es/strings.xml
  91. 26 21
      app/src/main/res/values-fr/strings.xml
  92. 8 3
      app/src/main/res/values-it/strings.xml
  93. 7 2
      app/src/main/res/values-nl-rNL/strings.xml
  94. 12 3
      app/src/main/res/values-no/strings.xml
  95. 7 2
      app/src/main/res/values-pl/strings.xml
  96. 7 2
      app/src/main/res/values-pt-rBR/strings.xml
  97. 63 5
      app/src/main/res/values-rm/strings.xml
  98. 10 5
      app/src/main/res/values-ru/strings.xml
  99. 15 4
      app/src/main/res/values-sk/strings.xml
  100. 17 8
      app/src/main/res/values-tr/strings.xml

+ 1 - 7
.editorconfig

@@ -6,15 +6,9 @@ charset = utf-8
 end_of_line = lf
 insert_final_newline = true
 trim_trailing_whitespace = true
-
-[*.java]
-indent_style = tab
-indent_size = 4
-
-[*.gradle]
 indent_style = space
 indent_size = 4
 
-[*.php]
+[*.{java,php}]
 indent_style = tab
 indent_size = 4

+ 17 - 14
app/build.gradle

@@ -18,9 +18,9 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "5.2.3"
+def app_version = "5.2.4"
 def beta_suffix = "" // with leading dash
-def defaultVersionCode = 936
+def defaultVersionCode = 943
 
 /**
  * Return the git hash, if git is installed.
@@ -629,6 +629,7 @@ android {
     }
 
     compileOptions {
+        coreLibraryDesugaringEnabled true
         sourceCompatibility JavaVersion.VERSION_11
         targetCompatibility JavaVersion.VERSION_11
     }
@@ -719,6 +720,8 @@ dependencies {
         //resolutionStrategy.failOnVersionConflict()
     }
 
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
+
     implementation project(':domain')
     implementation project(path: ':task-manager')
 
@@ -753,14 +756,14 @@ dependencies {
     implementation 'androidx.activity:activity-ktx:1.8.2'
     implementation 'androidx.sqlite:sqlite:2.2.2'
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
-    implementation "androidx.camera:camera-camera2:1.3.0-beta01"
-    implementation "androidx.camera:camera-lifecycle:1.3.0-beta01"
-    implementation "androidx.camera:camera-view:1.3.0-beta01"
-    implementation 'androidx.camera:camera-video:1.3.0-beta01'
+    implementation "androidx.camera:camera-camera2:1.3.1"
+    implementation "androidx.camera:camera-lifecycle:1.3.1"
+    implementation "androidx.camera:camera-view:1.3.1"
+    implementation 'androidx.camera:camera-video:1.3.1'
     implementation "androidx.media:media:1.7.0"
-    implementation 'androidx.media3:media3-exoplayer:1.2.0'
-    implementation 'androidx.media3:media3-ui:1.2.0'
-    implementation "androidx.media3:media3-session:1.2.0"
+    implementation 'androidx.media3:media3-exoplayer:1.2.1'
+    implementation 'androidx.media3:media3-ui:1.2.1'
+    implementation "androidx.media3:media3-session:1.2.1"
     implementation 'androidx.multidex:multidex:2.0.1'
     implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
     implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
@@ -775,7 +778,7 @@ dependencies {
     implementation 'androidx.room:room-runtime:2.5.2'
     kapt 'androidx.room:room-compiler:2.5.2'
 
-    implementation 'com.google.android.material:material:1.9.0'
+    implementation 'com.google.android.material:material:1.10.0' // last version before switch to tonal system: https://github.com/material-components/material-components-android/releases/tag/1.11.0
     implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on API < 24
     implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.23' // make sure to update this in domain's build.gradle as well
 
@@ -809,7 +812,7 @@ dependencies {
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
 
     // use leak canary in debug builds
-//  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
+//    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
 
     // test dependencies
     testImplementation "junit:junit:$junit_version"
@@ -858,10 +861,10 @@ dependencies {
     // Google Play Services and related libraries
     def googleDependencies = [
         // Play services
-        'com.google.android.gms:play-services-base:18.1.0': [],
+        'com.google.android.gms:play-services-base:18.0.1': [],
 
         // Firebase push
-        'com.google.firebase:firebase-messaging:23.2.1': [
+        'com.google.firebase:firebase-messaging:23.1.2': [
             [group: 'com.google.firebase', module: 'firebase-core'],
             [group: 'com.google.firebase', module: 'firebase-analytics'],
             [group: 'com.google.firebase', module: 'firebase-measurement-connector'],
@@ -891,7 +894,7 @@ dependencies {
     redImplementation(name: 'libgsaverification-client', ext: 'aar')
 
     // Maplibre (may have transitive dependencies on Google location services)
-    def maplibreDependency = 'org.maplibre.gl:android-sdk:10.2.0'
+    def maplibreDependency = 'org.maplibre.gl:android-sdk:10.3.0'
     noneImplementation maplibreDependency
     store_googleImplementation maplibreDependency
     store_google_workImplementation maplibreDependency

+ 1 - 1
app/jni/Android.mk

@@ -9,7 +9,7 @@
 
 LOCAL_PATH       := $(call my-dir)
 
-TARGET_PLATFORM  := android-26
+TARGET_PLATFORM  := android-33
 
 # libscrypt
 

+ 1 - 1
app/jni/Application.mk

@@ -1,4 +1,4 @@
 APP_ABI := armeabi-v7a x86 arm64-v8a x86_64
-APP_PLATFORM := android-19
+APP_PLATFORM := android-21
 APP_DEBUG := false
 APP_OPTIM := release

+ 3 - 1
app/src/libre/play/release-notes/de/default.txt

@@ -1 +1,3 @@
-* Ein Fehler wurde behoben, durch den unbekannte Gruppenmitglieder versehentlich zur Kontaktliste hinzugefügt wurden.
+* Textmarker-Funktion in der Bildbearbeitung
+* Unterstützung von animierten WebP-Bildern
+* Zahlreiche Optimierungen und Fehlerbehebung

+ 3 - 1
app/src/libre/play/release-notes/en-US/default.txt

@@ -1 +1,3 @@
-* Fixed a bug where unknown group members were accidentally added to the contact list
+* Added a highlighter function to image editor
+* Support for animated WebP images
+* Numerous optimizations and bug fixes

+ 2 - 32
app/src/main/java/ch/threema/app/ThreemaApplication.java

@@ -243,11 +243,9 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final int SERVER_MESSAGE_NOTIFICATION_ID = 730;
 	public static final int NOT_ENOUGH_DISK_SPACE_NOTIFICATION_ID = 731;
 	public static final int UNSENT_MESSAGE_NOTIFICATION_ID = 732;
-	public static final int NETWORK_BLOCKED_NOTIFICATION_ID = 733;
 	public static final int WORK_SYNC_NOTIFICATION_ID = 735;
 	public static final int NEW_SYNCED_CONTACTS_NOTIFICATION_ID = 736;
 	public static final int WEB_RESUME_FAILED_NOTIFICATION_ID = 737;
-	public static final int IDENTITY_SYNC_NOTIFICATION_ID = 748;
 	public static final int VOICE_MSG_PLAYER_NOTIFICATION_ID = 749;
 	public static final int INCOMING_CALL_NOTIFICATION_ID = 800;
 	public static final int GROUP_RESPONSE_NOTIFICATION_ID = 801;
@@ -281,7 +279,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final String WORKER_CONNECTIVITY_CHANGE = "ConnectivityChange";
 	public static final String WORKER_AUTO_DELETE = "AutoDelete";
 	public static final String WORKER_AUTOSTART = "Autostart";
-	public static final String WORKER_RESTRICT_BACKGROUND_CHANGED = "RestrictBackgroundChanged";
 
 	public static final Lock onAndroidContactChangeLock = new ReentrantLock();
 
@@ -1306,36 +1303,9 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 							null,
 							null
 						);
-
-						if ((!myIdentity.equals(group.getCreatorIdentity())) || previousMemberCount > 1) {
-							//send all open ballots to the new group member
-							BallotService ballotService = serviceManager.getBallotService();
-							List<BallotModel> openBallots = ballotService.getBallots(new BallotService.BallotFilter() {
-								@Override
-								public MessageReceiver getReceiver() {
-									return receiver;
-								}
-
-								@Override
-								public BallotModel.State[] getStates() {
-									return new BallotModel.State[]{BallotModel.State.OPEN};
-								}
-
-								@Override
-								public boolean filter(BallotModel ballotModel) {
-									//only my ballots please
-									return ballotModel.getCreatorIdentity().equals(myIdentity);
-								}
-							});
-
-							for (BallotModel ballotModel : openBallots) {
-								ballotService.publish(receiver, ballotModel, null, newIdentity);
-							}
-						}
 					}
-
 				} catch (ThreemaException x) {
-					logger.error("Exception", x);
+					logger.error("Could not create group state after new member was added", x);
 				}
 
 				//reset avatar to recreate it!
@@ -1343,7 +1313,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 					serviceManager.getAvatarCacheService()
 							.reset(group);
 				} catch (FileSystemNotPresentException e) {
-					logger.error("Exception", e);
+					logger.error("Could not reset avatar cache", e);
 				}
 			}
 

+ 2 - 2
app/src/main/java/ch/threema/app/activities/HomeActivity.java

@@ -739,14 +739,14 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 					if (skipWhatsNew) {
 						isWhatsNewShown = false; // make sure isWhatsNewShown is set to false here if whatsnew is skipped - otherwise pin unlock will not be shown once
 					} else {
-						isWhatsNewShown = true;
-
 						int previous = preferenceService.getLatestVersion() % 1000;
 
 						// To not show the same dialog twice, it is only shown if the previous version
 						// is prior to the first version that used this dialog.
 						// Use the version code of the first version where this dialog should be shown.
 						if (previous < 925) { // do not show to users of previous release candidate
+							isWhatsNewShown = true;
+
 							Intent intent = new Intent(this, WhatsNewActivity.class);
 							startActivityForResult(intent, REQUEST_CODE_WHATSNEW);
 							overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);

+ 68 - 28
app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java

@@ -57,6 +57,8 @@ import android.widget.Toast;
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.ColorInt;
+import androidx.annotation.DimenRes;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -83,6 +85,8 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Deque;
 import java.util.HashSet;
@@ -178,6 +182,12 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 
 	private static final int STROKE_MODE_BRUSH = 0;
 	private static final int STROKE_MODE_PENCIL = 1;
+
+	private static final int STROKE_MODE_HIGHLIGHTER = 2;
+	@Retention(RetentionPolicy.SOURCE)
+	@IntDef({STROKE_MODE_BRUSH, STROKE_MODE_PENCIL, STROKE_MODE_HIGHLIGHTER})
+	private @interface StrokeMode {}
+
 	private static final int MAX_FACES = 16;
 
 	private static final int ANIMATION_DURATION_MS = 200;
@@ -206,14 +216,14 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 
 	@ColorInt private int penColor, backgroundColor;
 
-	private MenuItem undoItem, drawParentItem, paintItem, pencilItem, blurFacesItem, cropItem;
-	private Drawable brushIcon, pencilIcon;
+	private MenuItem undoItem, drawParentItem, paintItem, pencilItem, highlighterItem, blurFacesItem, cropItem;
+	private Drawable brushIcon, pencilIcon, highlighterIcon;
 	private PaintSelectionPopup paintSelectionPopup;
 	private final Deque<ActionEntity> undoHistory = new LinkedList<>();
 	private long lastAnimationStart = 0;
 	private final MediaItem.Orientation currentOrientation = new MediaItem.Orientation();
 	private boolean saveSemaphore = false;
-	private int strokeMode = STROKE_MODE_BRUSH;
+	private @StrokeMode int strokeMode = STROKE_MODE_BRUSH;
 	private ActivityMode activityMode = ActivityMode.EDIT_IMAGE;
 	private int groupId = -1;
 	private final ExecutorService threadPoolExecutor = Executors.newSingleThreadExecutor();
@@ -401,13 +411,10 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 			@Override
 			protected void onPostExecute(final Bitmap bitmap) {
 				if (bitmap != null) {
-					motionView.post(new Runnable() {
-						@Override
-						public void run() {
-							Layer layer = new Layer();
-							ImageEntity entity = new ImageEntity(layer, bitmap, motionView.getWidth(), motionView.getHeight());
-							motionView.addEntityAndPosition(entity);
-						}
+					motionView.post(() -> {
+						Layer layer = new Layer();
+						ImageEntity entity = new ImageEntity(layer, bitmap, motionView.getWidth(), motionView.getHeight());
+						motionView.addEntityAndPosition(entity);
 					});
 				}
 			}
@@ -488,6 +495,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 
 		this.brushIcon = AppCompatResources.getDrawable(this, R.drawable.ic_brush);
 		this.pencilIcon = AppCompatResources.getDrawable(this, R.drawable.ic_pencil_outline);
+		this.highlighterIcon = AppCompatResources.getDrawable(this, R.drawable.ic_ink_highlighter_outline);
 
 		this.penColor = getResources().getColor(R.color.material_red);
 		this.backgroundColor = Color.WHITE;
@@ -754,12 +762,12 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 	}
 
 	private void selectSticker() {
-		startActivityForResult(new Intent(ImagePaintActivity.this, StickerSelectorActivity.class), REQUEST_CODE_STICKER_SELECTOR);
+		startActivityForResult(new Intent(this, StickerSelectorActivity.class), REQUEST_CODE_STICKER_SELECTOR);
 		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
 	}
 
 	private void enterText() {
-		Intent intent = new Intent(ImagePaintActivity.this, ImagePaintKeyboardActivity.class);
+		Intent intent = new Intent(this, ImagePaintKeyboardActivity.class);
 		intent.putExtra(ImagePaintKeyboardActivity.INTENT_EXTRA_COLOR, penColor);
 		startActivityForResult(intent, REQUEST_CODE_ENTER_TEXT);
 		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
@@ -921,28 +929,38 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 	public boolean onPrepareOptionsMenu(Menu menu) {
 		super.onPrepareOptionsMenu(menu);
 
-		if (this.strokeMode == STROKE_MODE_PENCIL) {
-			drawParentItem.setIcon(pencilIcon);
-		} else {
-			drawParentItem.setIcon(brushIcon);
+		switch (this.strokeMode) {
+			case STROKE_MODE_PENCIL:
+				drawParentItem.setIcon(pencilIcon);
+				break;
+			case STROKE_MODE_HIGHLIGHTER:
+				drawParentItem.setIcon(highlighterIcon);
+				break;
+			default:
+				drawParentItem.setIcon(brushIcon);
+				break;
 		}
 
 		ConfigUtils.tintMenuItem(this, drawParentItem, R.attr.colorOnSurface);
 		ConfigUtils.tintMenuItem(this, paintItem, R.attr.colorOnSurface);
 		ConfigUtils.tintMenuItem(this, pencilItem, R.attr.colorOnSurface);
+		ConfigUtils.tintMenuItem(this, highlighterItem, R.attr.colorOnSurface);
 
 		if (motionView.getSelectedEntity() == null) {
 			// no selected entities => draw mode or neutral mode
 			if (paintView.getActive()) {
-				if (this.strokeMode == STROKE_MODE_PENCIL) {
-					ConfigUtils.tintMenuItem(pencilItem, this.penColor);
-					drawParentItem.setIcon(pencilIcon);
-					ConfigUtils.tintMenuItem(drawParentItem, this.penColor);
-				} else {
-					ConfigUtils.tintMenuItem(paintItem, this.penColor);
-					drawParentItem.setIcon(brushIcon);
-					ConfigUtils.tintMenuItem(drawParentItem, this.penColor);
+				switch (this.strokeMode) {
+					case STROKE_MODE_PENCIL:
+						ConfigUtils.tintMenuItem(pencilItem, this.penColor);
+						break;
+					case STROKE_MODE_HIGHLIGHTER:
+						ConfigUtils.tintMenuItem(highlighterItem, this.penColor);
+						break;
+					default:
+						ConfigUtils.tintMenuItem(paintItem, this.penColor);
+						break;
 				}
+				ConfigUtils.tintMenuItem(drawParentItem, this.penColor);
 			}
 		}
 		undoItem.setVisible(hasChanges());
@@ -972,6 +990,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 		drawParentItem = menu.findItem(R.id.item_draw_parent);
 		paintItem = menu.findItem(R.id.item_draw);
 		pencilItem = menu.findItem(R.id.item_pencil);
+		highlighterItem = menu.findItem(R.id.item_highlighter);
 		blurFacesItem = menu.findItem(R.id.item_face);
 
 		if (activityMode == ActivityMode.DRAWING) {
@@ -1025,6 +1044,14 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 				setStrokeMode(STROKE_MODE_PENCIL);
 				setDrawMode(true);
 			}
+		} else if (id == R.id.item_highlighter) {
+			if (strokeMode == STROKE_MODE_HIGHLIGHTER && this.paintView.getActive()) {
+				// switch to selection mode
+				setDrawMode(false);
+			} else {
+				setStrokeMode(STROKE_MODE_HIGHLIGHTER);
+				setDrawMode(true);
+			}
 		} else if (id == R.id.item_face_blur) {
 			blurFaces(false);
 		} else if (id == R.id.item_face_emoji) {
@@ -1084,10 +1111,23 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 
 	private void setStrokeMode(int strokeMode) {
 		this.strokeMode = strokeMode;
-		this.paintView.setStrokeWidth(
-			getResources().getDimensionPixelSize(strokeMode == STROKE_MODE_PENCIL ?
-				R.dimen.imagepaint_pencil_stroke_width :
-				R.dimen.imagepaint_brush_stroke_width));
+		@DimenRes int strokeWidthDimension;
+		switch (strokeMode) {
+			case STROKE_MODE_HIGHLIGHTER:
+				paintView.setTransparent(true);
+				strokeWidthDimension = R.dimen.imagepaint_highlighter_stroke_width;
+				break;
+			case STROKE_MODE_PENCIL:
+				paintView.setTransparent(false);
+				strokeWidthDimension = R.dimen.imagepaint_pencil_stroke_width;
+				break;
+			default:
+				paintView.setTransparent(false);
+				strokeWidthDimension = R.dimen.imagepaint_brush_stroke_width;
+				break;
+
+		}
+		this.paintView.setStrokeWidth(getResources().getDimensionPixelSize(strokeWidthDimension));
 	}
 
 	private void deleteEntity() {

+ 18 - 1
app/src/main/java/ch/threema/app/activities/MediaViewerActivity.java

@@ -34,6 +34,7 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcelable;
+import android.text.TextUtils;
 import android.util.SparseArray;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -187,6 +188,22 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 		getToolbar().setSubtitleTextAppearance(this, R.style.Threema_TextAppearance_MediaViewer_SubTitle);
 
 		this.caption = findViewById(R.id.caption);
+		ViewCompat.setOnApplyWindowInsetsListener(this.caption, (v, insets) -> {
+			Insets systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+
+			// limit height so that caption doesn't overlap UI elements such as the play button
+			final int lineHeight = ((TextView) v).getLineHeight();
+			final int halfWindowHeight = ConfigUtils.getRealWindowHeight(getWindowManager()) / 2;
+			final int maxTextViewHeight = halfWindowHeight
+				- systemInsets.bottom
+				- getResources().getDimensionPixelSize(R.dimen.mediaviewer_play_button_radius)
+				- getResources().getDimensionPixelSize(R.dimen.mediaviewer_caption_border_bottom)
+				- (getResources().getDimensionPixelSize(R.dimen.mediaviewer_caption_container_padding_vertical) * 2);
+			((TextView) v).setMaxLines(maxTextViewHeight / lineHeight);
+			((TextView) v).setEllipsize(TextUtils.TruncateAt.END);
+
+			return insets;
+		});
 
 		this.captionContainer = findViewById(R.id.caption_container);
 		ViewCompat.setOnApplyWindowInsetsListener(this.captionContainer, (v, insets) -> {
@@ -704,7 +721,7 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 						String mimeType = messageModel.getFileData().getMimeType();
 						if (MimeUtil.isGifFile(mimeType)) {
 							f = new GifViewFragment();
-						} else if (MimeUtil.isImageFile(mimeType)) {
+						} else if (MimeUtil.isSupportedImageFile(mimeType)) {
 							f = new ImageViewFragment();
 						} else if (MimeUtil.isVideoFile(mimeType)) {
 							f = new VideoViewFragment();

+ 1 - 1
app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java

@@ -962,7 +962,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 							int renderingType = messageModel.getFileData().getRenderingType();
 
 							if (messageModel.getFileData().getRenderingType() != FileData.RENDERING_DEFAULT) {
-								mediaType = MimeUtil.getMediaTypeFromMimeType(mimeType);
+								mediaType = MimeUtil.getMediaTypeFromMimeType(mimeType, uri);
 							}
 							sendForwardedMedia(messageReceivers, uri, caption, mediaType, mimeType, renderingType, messageModel.getFileData().getFileName(), messageModel.getFileData().getDurationMs());
 							break;

+ 14 - 2
app/src/main/java/ch/threema/app/activities/SendMediaActivity.java

@@ -698,8 +698,10 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 
 		int newPosition = mediaAdapterManager.getCurrentPosition();
 		if (viewPager.getCurrentItem() != newPosition) {
-			viewPager.postDelayed(() -> viewPager.setCurrentItem(newPosition, true), 50);
-			mediaAdapterManager.update(newPosition, NOTIFY_ADAPTER);
+			viewPager.postDelayed(() -> {
+				viewPager.setCurrentItem(newPosition, true);
+				mediaAdapterManager.update(newPosition, NOTIFY_ADAPTER);
+			}, 50);
 		}
 	}
 
@@ -1153,6 +1155,16 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 	protected void onDestroy() {
 		new Thread(() -> VideoTimelineCache.getInstance().flush()).start();
 
+		if (this.viewPager != null) {
+			this.viewPager.setAdapter(null);
+		}
+		this.sendMediaAdapter = null;
+
+		if (this.recyclerView != null) {
+			this.recyclerView.setAdapter(null);
+		}
+		this.sendMediaPreviewAdapter = null;
+
 		if (preferenceService.getEmojiStyle() != PreferenceService.EmojiStyle_ANDROID) {
 			removeAllListeners();
 		}

+ 7 - 0
app/src/main/java/ch/threema/app/activities/SimpleWebViewActivity.java

@@ -30,7 +30,9 @@ import android.webkit.WebView;
 
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AppCompatDelegate;
+import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
 
 import com.google.android.material.appbar.MaterialToolbar;
 import com.google.android.material.progressindicator.LinearProgressIndicator;
@@ -82,6 +84,11 @@ public abstract class SimpleWebViewActivity extends ThreemaToolbarActivity imple
 		webView = findViewById(R.id.simple_webview);
 		webView.getSettings().setJavaScriptEnabled(requiresJavaScript());
 
+		ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.webview_scroller), (v, insets) -> {
+			v.setPadding(0, 0, 0, insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom);
+			return WindowInsetsCompat.CONSUMED;
+		});
+
 		if (requiresConnection()) {
 			webView.setWebChromeClient(new WebChromeClient() {
 				@Override

+ 20 - 6
app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java

@@ -25,6 +25,7 @@ import static ch.threema.domain.protocol.csp.messages.file.FileData.RENDERING_DE
 
 import android.animation.LayoutTransition;
 import android.content.Context;
+import android.os.Build;
 import android.text.TextUtils;
 import android.util.SparseIntArray;
 import android.view.LayoutInflater;
@@ -72,6 +73,7 @@ import ch.threema.app.adapters.decorators.StatusChatAdapterDecorator;
 import ch.threema.app.adapters.decorators.TextChatAdapterDecorator;
 import ch.threema.app.adapters.decorators.VideoChatAdapterDecorator;
 import ch.threema.app.adapters.decorators.VoipStatusDataChatAdapterDecorator;
+import ch.threema.app.adapters.decorators.AnimatedImageDrawableDecorator;
 import ch.threema.app.cache.ThumbnailCache;
 import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
@@ -153,7 +155,9 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 		TYPE_FILE_MEDIA_RECV,
 		TYPE_FILE_VIDEO_SEND,
 		TYPE_GROUP_CALL_STATUS,
-		TYPE_FORWARD_SECURITY_STATUS
+		TYPE_FORWARD_SECURITY_STATUS,
+		TYPE_IMAGE_ANIMATED_SEND,
+		TYPE_IMAGE_ANIMATED_RECV
 	})
 	public @interface ItemType {}
 
@@ -183,9 +187,11 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 	public static final int TYPE_FILE_VIDEO_SEND = 23;
 	public static final int TYPE_GROUP_CALL_STATUS = 24;
 	public static final int TYPE_FORWARD_SECURITY_STATUS = 25;
+	public static final int TYPE_IMAGE_ANIMATED_SEND = 26;
+	public static final int TYPE_IMAGE_ANIMATED_RECV = 27;
 
 	// don't forget to update this after adding new types:
-	private static final int TYPE_MAX_COUNT = TYPE_FORWARD_SECURITY_STATUS + 1;
+	private static final int TYPE_MAX_COUNT = TYPE_IMAGE_ANIMATED_RECV + 1;
 
 	private OnClickListener onClickListener;
 	private Map<String, Integer> identityColors = null;
@@ -387,8 +393,12 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 						} else if (MimeUtil.isAudioFile(mimeType) && renderingType == FileData.RENDERING_MEDIA) {
 							return o ? TYPE_AUDIO_SEND : TYPE_AUDIO_RECV;
 						} else if (renderingType == FileData.RENDERING_MEDIA || renderingType == FileData.RENDERING_STICKER) {
-							if (MimeUtil.isImageFile(mimeType)) {
-								return o ? TYPE_FILE_MEDIA_SEND : TYPE_FILE_MEDIA_RECV;
+							if (MimeUtil.isSupportedImageFile(mimeType)) {
+								if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ConfigUtils.isSupportedAnimatedImageFormat(mimeType)) {
+									return o ? TYPE_IMAGE_ANIMATED_SEND : TYPE_IMAGE_ANIMATED_RECV;
+								} else {
+									return o ? TYPE_FILE_MEDIA_SEND : TYPE_FILE_MEDIA_RECV;
+								}
 							} else if (MimeUtil.isVideoFile(mimeType)) {
 								return o ? TYPE_FILE_VIDEO_SEND : TYPE_FILE_MEDIA_RECV;
 							}
@@ -426,9 +436,11 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 				return R.layout.conversation_list_item_unread;
 			case TYPE_MEDIA_SEND:
 			case TYPE_FILE_MEDIA_SEND:
+			case TYPE_IMAGE_ANIMATED_SEND:
 				return R.layout.conversation_list_item_media_send;
 			case TYPE_MEDIA_RECV:
 			case TYPE_FILE_MEDIA_RECV:
+			case TYPE_IMAGE_ANIMATED_RECV:
 				return R.layout.conversation_list_item_media_recv;
 			case TYPE_FILE_VIDEO_SEND:
 				return R.layout.conversation_list_item_video_send;
@@ -528,8 +540,6 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 					holder.groupAckThumbsDownImage = itemView.findViewById(R.id.groupack_thumbsdown);
 					holder.tapToResend = itemView.findViewById(R.id.tap_to_resend);
 					holder.starredIcon = itemView.findViewById(R.id.star_icon);
-
-					((ViewGroup) holder.groupAckContainer).getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING|LayoutTransition.APPEARING);
 				}
 				itemView.setTag(holder);
 			}
@@ -598,6 +608,10 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 					} else if (MimeUtil.isAudioFile(messageModel.getFileData().getMimeType()) &&
 						messageModel.getFileData().getRenderingType() == FileData.RENDERING_MEDIA) {
 						decorator = new AudioChatAdapterDecorator(this.context, messageModel, this.decoratorHelper);
+					} else if (ConfigUtils.isSupportedAnimatedImageFormat(messageModel.getFileData().getMimeType()) &&
+						(messageModel.getFileData().getRenderingType() == FileData.RENDERING_MEDIA ||
+							messageModel.getFileData().getRenderingType() == FileData.RENDERING_STICKER)) {
+						decorator = new AnimatedImageDrawableDecorator(this.context, messageModel, this.decoratorHelper);
 					} else {
 						decorator = new FileChatAdapterDecorator(this.context, messageModel, this.decoratorHelper);
 					}

+ 10 - 0
app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java

@@ -40,6 +40,7 @@ import com.google.android.material.imageview.ShapeableImageView;
 import com.google.android.material.shape.ShapeAppearanceModel;
 
 import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
 
 import java.text.Collator;
 import java.text.Normalizer;
@@ -68,9 +69,11 @@ import ch.threema.app.utils.ContactUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.ViewUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 
 public class ContactListAdapter extends FilterableListAdapter implements SectionIndexer {
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ContactListAdapter");
 
 	private static final int MAX_RECENTLY_ADDED_CONTACTS = 1;
 
@@ -342,6 +345,13 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 			}
 		} else {
 			holder = (ContactListHolder) itemView.getTag();
+			if (holder.avatarView != null) {
+				try {
+					requestManager.clear(holder.avatarView);
+				} catch (IllegalArgumentException e) {
+					logger.debug("Invalid destination view");
+				}
+			}
 		}
 
 		final ContactModel contactModel = values.get(position);

+ 42 - 45
app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.kt

@@ -23,9 +23,9 @@ package ch.threema.app.adapters
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.Bitmap
 import android.graphics.Outline
 import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
 import android.util.SparseBooleanArray
 import android.view.LayoutInflater
 import android.view.View
@@ -48,11 +48,9 @@ import ch.threema.storage.models.AbstractMessageModel
 import ch.threema.storage.models.MessageType
 import ch.threema.storage.models.data.MessageContentsType
 import com.bumptech.glide.Glide
-import com.bumptech.glide.load.DataSource
-import com.bumptech.glide.load.engine.GlideException
-import com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions
-import com.bumptech.glide.request.RequestListener
-import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
+import com.bumptech.glide.request.target.CustomViewTarget
+import com.bumptech.glide.request.transition.Transition
 import com.google.android.material.imageview.ShapeableImageView
 
 class MediaGalleryAdapter(
@@ -78,8 +76,8 @@ class MediaGalleryAdapter(
     init {
         this.clickListener = clickListener
         this.columnCount = columnCount
-        this.messageReceiver = messageReceiver;
-        this.foregroundColor = ConfigUtils.getColorFromAttribute(context, R.attr.colorOnBackground);
+        this.messageReceiver = messageReceiver
+        this.foregroundColor = ConfigUtils.getColorFromAttribute(context, R.attr.colorOnBackground)
         this.fileService = ThreemaApplication.getServiceManager()?.fileService
 
         val cornerRadius: Int = context.resources.getDimensionPixelSize(R.dimen.media_gallery_container_radius)
@@ -93,7 +91,8 @@ class MediaGalleryAdapter(
     class MediaGalleryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
         var imageView: ShapeableImageView? = null
         var videoContainerView: View? = null
-        var gifContainerView: View? = null
+        var animatedFormatLabelContainer: View? = null
+        var animatedFormatLabelIconView: ImageView? = null
         var videoDuration: TextView? = null
         var vmContainerView: View? = null
         var vmDuration: TextView? = null
@@ -103,7 +102,8 @@ class MediaGalleryAdapter(
 
         init {
             imageView = itemView.findViewById(R.id.thumbnail_view)
-            gifContainerView = itemView.findViewById(R.id.gif_marker_container)
+            animatedFormatLabelContainer = itemView.findViewById(R.id.animated_format_label_container)
+            animatedFormatLabelIconView = itemView.findViewById(R.id.animated_format_label_icon)
             videoContainerView = itemView.findViewById(R.id.video_marker_container)
             videoDuration = itemView.findViewById(R.id.video_duration_text)
             vmContainerView = itemView.findViewById(R.id.voicemessage_marker_container)
@@ -118,11 +118,11 @@ class MediaGalleryAdapter(
         val holder = MediaGalleryHolder(itemView)
         holder.vmContainerView?.outlineProvider = viewOutlineProvider
         holder.videoContainerView?.outlineProvider = viewOutlineProvider
-        holder.gifContainerView?.outlineProvider = viewOutlineProvider
+        holder.animatedFormatLabelContainer?.outlineProvider = viewOutlineProvider
         holder.textContainerView?.outlineProvider = viewOutlineProvider
         holder.vmContainerView?.clipToOutline = true
         holder.videoContainerView?.clipToOutline = true
-        holder.gifContainerView?.clipToOutline = true
+        holder.animatedFormatLabelContainer?.clipToOutline = true
         holder.textContainerView?.clipToOutline = true
 
         return holder
@@ -134,7 +134,7 @@ class MediaGalleryAdapter(
 
             if (holder.messageId != messageModel.id) {
                 val placeholderIcon : Int = if (messageModel.messageContentsType == MessageContentsType.VOICE_MESSAGE) {
-                    R.drawable.ic_keyboard_voice_outline;
+                    R.drawable.ic_keyboard_voice_outline
                 } else if (messageModel.type == MessageType.FILE) {
                     IconUtil.getMimeIcon(messageModel.fileData.mimeType)
                 } else {
@@ -143,47 +143,48 @@ class MediaGalleryAdapter(
 
                 // do not load contents again if it's unchanged
                 Glide.with(context)
-                    .asBitmap()
                     .load(messageModel)
-                    .transition(BitmapTransitionOptions.withCrossFade())
-                    .centerCrop()
+                    .transition(withCrossFade())
+                    .optionalCenterCrop()
                     .error(placeholderIcon)
-                    .addListener(object : RequestListener<Bitmap?> {
-                        override fun onLoadFailed(
-                            e: GlideException?,
-                            model: Any?,
-                            target: Target<Bitmap?>,
-                            isFirstResource: Boolean
-                        ): Boolean {
+                    .into(object : CustomViewTarget<ShapeableImageView?, Drawable?>(holder.imageView!!) {
+                        override fun onResourceCleared(placeholder: Drawable?) {}
+                        override fun onLoadFailed(errorDrawable: Drawable?) {
                             decorateItem(holder, messageModel)
-                            return false
+                            holder.imageView?.setImageDrawable(errorDrawable)
                         }
-
                         override fun onResourceReady(
-                            resource: Bitmap,
-                            model: Any,
-                            target: Target<Bitmap?>?,
-                            dataSource: DataSource,
-                            isFirstResource: Boolean
-                        ): Boolean {
+                            resource: Drawable,
+                            transition: Transition<in Drawable?>?
+                        ) {
                             holder.textContainerView?.visibility = View.GONE
                             holder.vmContainerView?.visibility = View.GONE
                             holder.imageView?.clearColorFilter()
                             holder.imageView?.scaleType = ImageView.ScaleType.CENTER_CROP
-
+                            holder.imageView?.setImageDrawable(resource)
                             if (messageModel.messageContentsType == MessageContentsType.GIF) {
-                                holder.gifContainerView?.visibility = View.VISIBLE
+                                holder.animatedFormatLabelContainer?.visibility = View.VISIBLE
+                                holder.animatedFormatLabelIconView?.setImageResource(R.drawable.ic_gif_24dp)
+                                holder.animatedFormatLabelIconView?.contentDescription = context.getString(R.string.attach_gif)
+                            } else if (messageModel.messageContentsType == MessageContentsType.IMAGE && ConfigUtils.isSupportedAnimatedImageFormat(messageModel.fileData.mimeType)) {
+                                holder.animatedFormatLabelContainer?.visibility = View.VISIBLE
+                                holder.animatedFormatLabelIconView?.setImageResource(R.drawable.ic_webp)
+                                holder.animatedFormatLabelIconView?.contentDescription = "WebP"
                             } else {
-                                holder.gifContainerView?.visibility = View.GONE
+                                holder.animatedFormatLabelContainer?.visibility = View.GONE
                             }
 
                             if (messageModel.messageContentsType == MessageContentsType.VIDEO) {
-                                val duration: Long = if (messageModel.type == MessageType.VIDEO) {
-                                    messageModel.videoData.duration.toLong()
-                                } else if (messageModel.type == MessageType.FILE) {
-                                    messageModel.fileData.durationSeconds
-                                } else {
-                                    0
+                                val duration: Long = when (messageModel.type) {
+                                    MessageType.VIDEO -> {
+                                        messageModel.videoData.duration.toLong()
+                                    }
+                                    MessageType.FILE -> {
+                                        messageModel.fileData.durationSeconds
+                                    }
+                                    else -> {
+                                        0
+                                    }
                                 }
 
                                 if (duration > 0) {
@@ -196,12 +197,8 @@ class MediaGalleryAdapter(
                             } else {
                                 holder.videoContainerView?.visibility = View.GONE
                             }
-
-                            return false
                         }
-
                     })
-                    .into(holder.imageView!!)
             }
             holder.messageId = messageModel.id
             (holder.itemView as CheckableFrameLayout).isChecked = checkedItems.get(position)
@@ -215,7 +212,7 @@ class MediaGalleryAdapter(
         holder.imageView?.scaleType = ImageView.ScaleType.CENTER
         holder.imageView?.setColorFilter(foregroundColor, PorterDuff.Mode.SRC_IN)
         holder.videoContainerView?.visibility = View.GONE
-        holder.gifContainerView?.visibility = View.GONE
+        holder.animatedFormatLabelContainer?.visibility = View.GONE
 
         if (messageModel.messageContentsType == MessageContentsType.VOICE_MESSAGE) {
             val duration: Long = if (messageModel.type == MessageType.FILE) {

+ 12 - 0
app/src/main/java/ch/threema/app/adapters/MessageListAdapter.java

@@ -34,6 +34,8 @@ import androidx.recyclerview.widget.RecyclerView;
 import com.bumptech.glide.RequestManager;
 import com.google.android.material.button.MaterialButton;
 
+import org.slf4j.Logger;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -51,11 +53,14 @@ import ch.threema.app.ui.EmptyRecyclerView;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.StateBitmapUtil;
 import ch.threema.app.voip.groupcall.GroupCallManager;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ConversationModel;
 import ch.threema.storage.models.GroupModel;
 
 public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationModel, RecyclerView.ViewHolder> {
 
+	private static final Logger logger = LoggingUtil.getThreemaLogger("MessageListAdapter");
+
 	private static final int MAX_SELECTED_ITEMS = 0;
 
 	public static final int TYPE_ITEM = 0;
@@ -202,6 +207,13 @@ public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationMode
 			if (item != null) {
 				conversationModel = item.getConversationModel();
 			}
+			if (((MessageListViewHolder) holder).avatarView != null) {
+				try {
+					requestManager.clear(((MessageListViewHolder) holder).avatarView);
+				} catch (IllegalArgumentException e) {
+					logger.debug("Invalid destination view");
+				}
+			}
 		}
 		if (conversationModel != null && conversationModel.isGroupConversation()) {
 			MessageListViewHolder messageListViewHolder = (MessageListViewHolder) holder;

+ 7 - 5
app/src/main/java/ch/threema/app/adapters/SendMediaAdapter.kt

@@ -21,9 +21,11 @@
 
 package ch.threema.app.adapters
 
+import androidx.annotation.OptIn
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentManager
 import androidx.lifecycle.Lifecycle
+import androidx.media3.common.util.UnstableApi
 import androidx.viewpager2.adapter.FragmentStateAdapter
 import androidx.viewpager2.adapter.FragmentViewHolder
 import androidx.viewpager2.widget.ViewPager2
@@ -54,14 +56,14 @@ class SendMediaAdapter(
 
     override fun getItemCount() = mm.size()
 
-    override fun createFragment(position: Int): Fragment {
+    @OptIn(UnstableApi::class) override fun createFragment(position: Int): Fragment {
         return BigMediaFragment.newInstance(mm.get(position), bottomElemHeight).also {
             fragments[position] = it
             it.setViewPager(viewPager)
         }
     }
 
-    override fun onBindViewHolder(
+    @OptIn(UnstableApi::class) override fun onBindViewHolder(
         holder: FragmentViewHolder,
         position: Int,
         payloads: MutableList<Any>
@@ -80,7 +82,7 @@ class SendMediaAdapter(
         return mm.getItems().map { it.uri.hashCode().toLong() }.contains(itemId)
     }
 
-    override fun filenameUpdated(position: Int) {
+    @OptIn(UnstableApi::class) override fun filenameUpdated(position: Int) {
         if (position < 0 || position >= itemCount) {
             logger.error("Could not update filename at position {} of {} items", position, itemCount)
             return
@@ -88,7 +90,7 @@ class SendMediaAdapter(
         fragments[position]?.updateFilename()
     }
 
-    override fun videoMuteStateUpdated(position: Int) {
+    @OptIn(UnstableApi::class) override fun videoMuteStateUpdated(position: Int) {
         if (position < 0 || position >= itemCount) {
             logger.error("Could not update video at position {} of {} items", position, itemCount)
             return
@@ -100,7 +102,7 @@ class SendMediaAdapter(
         // Nothing to do as the view pager position is updated via the layout
     }
 
-    override fun sendAsFileStateUpdated(position: Int) {
+    @OptIn(UnstableApi::class) override fun sendAsFileStateUpdated(position: Int) {
         fragments[position]?.updateSendAsFileState()
     }
 }

+ 7 - 3
app/src/main/java/ch/threema/app/adapters/SendMediaPreviewAdapter.kt

@@ -56,7 +56,7 @@ class SendMediaPreviewAdapter(
         mm.setMediaPreviewAdapter(this)
     }
 
-    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
+    @Retention(AnnotationRetention.SOURCE)
     @IntDef(VIEW_TYPE_NORMAL, VIEW_TYPE_ADD)
     annotation class ViewType
 
@@ -253,9 +253,10 @@ class SendMediaPreviewAdapter(
      * Initialize the qualifier view
      */
     private fun setQualifierView(item: MediaItem, holder: SendMediaItemHolder) {
+        val imageView: AppCompatImageView = holder.qualifierView.findViewById(R.id.video_icon)
+
         if (item.type == TYPE_VIDEO_CAM || item.type == TYPE_VIDEO) {
             holder.qualifierView.visibility = View.VISIBLE
-            val imageView: AppCompatImageView = holder.qualifierView.findViewById(R.id.video_icon)
             imageView.setImageResource(R.drawable.ic_videocam_black_24dp)
             val durationView = holder.qualifierView.findViewById<TextView>(R.id.video_duration_text)
             if (item.durationMs > 0) {
@@ -266,9 +267,12 @@ class SendMediaPreviewAdapter(
             }
         } else if (item.type == TYPE_GIF) {
             holder.qualifierView.visibility = View.VISIBLE
-            val imageView: AppCompatImageView = holder.qualifierView.findViewById(R.id.video_icon)
             imageView.setImageResource(R.drawable.ic_gif_24dp)
             holder.qualifierView.findViewById<View>(R.id.video_duration_text).visibility = View.GONE
+        } else if (item.type == TYPE_IMAGE_ANIMATED && MimeUtil.isWebPFile(item.mimeType)) {
+            holder.qualifierView.visibility = View.VISIBLE
+            imageView.setImageResource(R.drawable.ic_webp)
+            holder.qualifierView.findViewById<View>(R.id.video_duration_text).visibility = View.GONE
         } else {
             holder.qualifierView.visibility = View.GONE
         }

+ 9 - 7
app/src/main/java/ch/threema/app/adapters/decorators/AnimGifChatAdapterDecorator.java

@@ -58,6 +58,8 @@ public class AnimGifChatAdapterDecorator extends ChatAdapterDecorator {
 
 	@Override
 	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
+		super.configureChatMessage(holder, position);
+
 		final long fileSize;
 
 		logger.debug("configureChatMessage - position " + position);
@@ -97,12 +99,12 @@ public class AnimGifChatAdapterDecorator extends ChatAdapterDecorator {
 		}
 		setOnClickListener(v -> {
 			if (!isInChoiceMode()) {
-				if ((!getPreferenceService().isGifAutoplay() ||
+				if ((!getPreferenceService().isAnimationAutoplay() ||
 					holder.controller.getStatus() == ControllerView.STATUS_READY_TO_DOWNLOAD)) {
 					gifMessagePlayer.open();
 				}
-				if (getPreferenceService().isGifAutoplay() && holder.controller.getStatus() == ControllerView.STATUS_NONE) {
-					gifMessagePlayer.openInExternalPlayer(null);
+				if (getPreferenceService().isAnimationAutoplay() && holder.controller.getStatus() == ControllerView.STATUS_NONE) {
+					gifMessagePlayer.openInExternalPlayer();
 				}
 			}
 		}, holder.messageBlockView);
@@ -153,7 +155,7 @@ public class AnimGifChatAdapterDecorator extends ChatAdapterDecorator {
 					@Override
 					public void onStart(AbstractMessageModel messageModel) {
 						RuntimeUtil.runOnUiThread(() -> {
-							if (!helper.getPreferenceService().isGifAutoplay()) {
+							if (!helper.getPreferenceService().isAnimationAutoplay()) {
 								holder.controller.setProgressing();
 							}
 						});
@@ -164,7 +166,7 @@ public class AnimGifChatAdapterDecorator extends ChatAdapterDecorator {
 						RuntimeUtil.runOnUiThread(() -> {
 							holder.controller.setNeutral();
 							if (success) {
-								if (helper.getPreferenceService().isGifAutoplay()) {
+								if (helper.getPreferenceService().isAnimationAutoplay()) {
 									holder.controller.setVisibility(View.INVISIBLE);
 								} else {
 									setControllerState(holder, messageModel.getFileData(), messageModel.getFileData().getFileSize());
@@ -245,14 +247,14 @@ public class AnimGifChatAdapterDecorator extends ChatAdapterDecorator {
 		logger.debug("setAutoPlay holder position " + holder.position);
 
 		if (fileData.isDownloaded()) {
-			if (helper.getPreferenceService().isGifAutoplay() && gifMessagePlayer != null) {
+			if (helper.getPreferenceService().isAnimationAutoplay() && gifMessagePlayer != null) {
 				gifMessagePlayer.autoPlay();
 				holder.controller.setVisibility(View.INVISIBLE);
 			} else {
 				holder.controller.setPlay();
 			}
 		} else {
-			if (helper.getPreferenceService().isGifAutoplay() && gifMessagePlayer != null && fileSize < MessageServiceImpl.FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO) {
+			if (helper.getPreferenceService().isAnimationAutoplay() && gifMessagePlayer != null && fileSize < MessageServiceImpl.FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO) {
 				gifMessagePlayer.autoPlay();
 				holder.controller.setVisibility(View.INVISIBLE);
 			} else {

+ 272 - 0
app/src/main/java/ch/threema/app/adapters/decorators/AnimatedImageDrawableDecorator.java

@@ -0,0 +1,272 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2014-2024 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.adapters.decorators;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.view.View;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+
+import java.io.File;
+
+import ch.threema.app.services.MessageServiceImpl;
+import ch.threema.app.services.messageplayer.MessagePlayer;
+import ch.threema.app.services.messageplayer.AnimatedImageDrawableMessagePlayer;
+import ch.threema.app.ui.ControllerView;
+import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
+import ch.threema.app.utils.FileUtil;
+import ch.threema.app.utils.ImageViewUtil;
+import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
+import ch.threema.storage.models.AbstractMessageModel;
+import ch.threema.storage.models.MessageState;
+import ch.threema.storage.models.data.media.FileDataModel;
+
+/**
+ * A decorator for animated image formats natively supported by AnimatedImageDrawable
+ * Currently, this is limited to WebP
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class AnimatedImageDrawableDecorator extends ChatAdapterDecorator {
+	private static final Logger logger = LoggingUtil.getThreemaLogger("AnimatedImageDrawableDecorator");
+
+	private static final String LISTENER_TAG = "decorator";
+	private AnimatedImageDrawableMessagePlayer animatedImageDrawableMessagePlayer;
+
+	public AnimatedImageDrawableDecorator(Context context, AbstractMessageModel messageModel, Helper decoratorHelper) {
+		super(context, messageModel, decoratorHelper);
+	}
+
+	@Override
+	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
+		final long fileSize;
+
+		super.configureChatMessage(holder, position);
+
+		logger.debug("configureChatMessage - position " + position);
+
+		animatedImageDrawableMessagePlayer = (AnimatedImageDrawableMessagePlayer) getMessagePlayerService().createPlayer(getMessageModel(), (Activity) getContext(), helper.getMessageReceiver(), null);
+		holder.messagePlayer = animatedImageDrawableMessagePlayer;
+
+		/*
+		 * setup click listeners
+		 */
+		if (holder.controller != null) {
+			holder.controller.setOnClickListener(v -> {
+				int status = holder.controller.getStatus();
+
+				switch (status) {
+					case ControllerView.STATUS_READY_TO_RETRY:
+						propagateControllerRetryClickToParent();
+						break;
+					case ControllerView.STATUS_READY_TO_PLAY:
+					case ControllerView.STATUS_READY_TO_DOWNLOAD:
+						animatedImageDrawableMessagePlayer.open();
+						break;
+					case ControllerView.STATUS_PROGRESSING:
+						if (getMessageModel().isOutbox() && (getMessageModel().getState() == MessageState.TRANSCODING ||
+							getMessageModel().getState() == MessageState.PENDING ||
+							getMessageModel().getState() == MessageState.SENDING)) {
+							getMessageService().remove(getMessageModel());
+						} else {
+							animatedImageDrawableMessagePlayer.cancel();
+						}
+						break;
+					default:
+						// no action taken for other statuses
+						break;
+				}
+			});
+		}
+		setOnClickListener(v -> {
+			if (!isInChoiceMode()) {
+				if ((!getPreferenceService().isAnimationAutoplay() ||
+					holder.controller.getStatus() == ControllerView.STATUS_READY_TO_DOWNLOAD)) {
+					animatedImageDrawableMessagePlayer.open();
+				}
+				if (getPreferenceService().isAnimationAutoplay() && holder.controller.getStatus() == ControllerView.STATUS_NONE) {
+					animatedImageDrawableMessagePlayer.openInExternalPlayer();
+				}
+			}
+		}, holder.messageBlockView);
+
+		/*
+		 * get thumbnail
+		 */
+		Bitmap thumbnail;
+		try {
+			thumbnail = getFileService().getMessageThumbnailBitmap(getMessageModel(),
+				getThumbnailCache());
+		} catch (Exception e) {
+			logger.error("Exception", e);
+			thumbnail = null;
+		}
+
+		final FileDataModel fileData = getMessageModel().getFileData();
+		fileSize = fileData.getFileSize();
+
+		ImageViewUtil.showBitmapOrImagePlaceholder(
+			getContext(),
+			holder.contentView,
+			holder.attachmentImage,
+			thumbnail,
+			getThumbnailWidth()
+		);
+		holder.bodyTextView.setWidth(getThumbnailWidth());
+
+		if (holder.attachmentImage != null) {
+			holder.attachmentImage.invalidate();
+		}
+		if (fileData.getRenderingType() == FileData.RENDERING_STICKER) {
+			setStickerBackground(holder);
+		} else {
+			setDefaultBackground(holder);
+		}
+
+		configureBodyText(holder, fileData.getCaption());
+
+		RuntimeUtil.runOnUiThread(() -> setControllerState(holder, fileData, fileSize));
+
+		setDatePrefix(FileUtil.getFileMessageDatePrefix(getContext(), getMessageModel(), "WebP"));
+
+		animatedImageDrawableMessagePlayer
+				.attachContainer(holder.attachmentImage)
+				// decryption
+				.addListener(LISTENER_TAG, new MessagePlayer.DecryptionListener() {
+					@Override
+					public void onStart(AbstractMessageModel messageModel) {
+						RuntimeUtil.runOnUiThread(() -> {
+							if (!helper.getPreferenceService().isAnimationAutoplay()) {
+								holder.controller.setProgressing();
+							}
+						});
+					}
+
+					@Override
+					public void onEnd(final AbstractMessageModel messageModel, final boolean success, final String message, final File decryptedFile) {
+						RuntimeUtil.runOnUiThread(() -> {
+							holder.controller.setNeutral();
+							if (success) {
+								if (helper.getPreferenceService().isAnimationAutoplay()) {
+									holder.controller.setVisibility(View.INVISIBLE);
+								} else {
+									setControllerState(holder, messageModel.getFileData(), messageModel.getFileData().getFileSize());
+								}
+							} else {
+								holder.controller.setVisibility(View.GONE);
+								if (!TestUtil.empty(message)) {
+									Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
+								}
+							}
+						});
+					}
+				})
+				// download listener
+				.addListener(LISTENER_TAG, new MessagePlayer.DownloadListener() {
+					@Override
+					public void onStart(AbstractMessageModel messageModel) {
+						RuntimeUtil.runOnUiThread(() -> holder.controller.setProgressingDeterminate(100));
+					}
+
+					@Override
+					public void onStatusUpdate(AbstractMessageModel messageModel, final int progress) {
+						RuntimeUtil.runOnUiThread(() -> holder.controller.setProgress(progress));
+					}
+
+					@Override
+					public void onEnd(AbstractMessageModel messageModel, final boolean success, final String message) {
+						//hide progressbar
+						RuntimeUtil.runOnUiThread(() -> {
+							// report error
+							if (success) {
+								holder.controller.setPlay();
+							} else {
+								holder.controller.setReadyToDownload();
+								if (!TestUtil.empty(message)) {
+									Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
+								}
+							}
+						});
+					}
+				});
+
+	}
+
+	private void setControllerState(ComposeMessageHolder holder, FileDataModel fileData, long fileSize) {
+		if (getMessageModel().isOutbox()) {
+			// outgoing message
+			switch (getMessageModel().getState()) {
+				case TRANSCODING:
+					holder.controller.setTranscoding();
+					break;
+				case PENDING:
+				case SENDING:
+					holder.controller.setProgressing();
+					break;
+				case SENDFAILED:
+				case FS_KEY_MISMATCH:
+					holder.controller.setRetry();
+					break;
+				default:
+					setAutoplay(fileData, fileSize, holder);
+			}
+		} else {
+			// incoming message
+			if (getMessageModel() != null && getMessageModel().getState() == MessageState.PENDING) {
+				if (fileData.isDownloaded()) {
+					holder.controller.setProgressing();
+				} else {
+					holder.controller.setProgressingDeterminate(100);
+				}
+			} else {
+				setAutoplay(fileData, fileSize, holder);
+			}
+		}
+	}
+
+	private void setAutoplay(FileDataModel fileData, long fileSize, ComposeMessageHolder holder) {
+		logger.debug("setAutoPlay holder position " + holder.position);
+
+		if (fileData.isDownloaded()) {
+			if (helper.getPreferenceService().isAnimationAutoplay() && animatedImageDrawableMessagePlayer != null) {
+				animatedImageDrawableMessagePlayer.autoPlay();
+				holder.controller.setVisibility(View.INVISIBLE);
+			} else {
+				holder.controller.setPlay();
+			}
+		} else {
+			if (helper.getPreferenceService().isAnimationAutoplay() && animatedImageDrawableMessagePlayer != null && fileSize < MessageServiceImpl.FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO) {
+				animatedImageDrawableMessagePlayer.autoPlay();
+				holder.controller.setVisibility(View.INVISIBLE);
+			} else {
+				holder.controller.setReadyToDownload();
+			}
+		}
+	}
+}

+ 11 - 1
app/src/main/java/ch/threema/app/adapters/decorators/ChatAdapterDecorator.java

@@ -35,6 +35,8 @@ import androidx.appcompat.content.res.AppCompatResources;
 import androidx.fragment.app.Fragment;
 import androidx.media3.session.MediaController;
 
+import com.google.android.material.imageview.ShapeableImageView;
+import com.google.android.material.shape.ShapeAppearanceModel;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.slf4j.Logger;
@@ -57,6 +59,7 @@ import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.services.messageplayer.MessagePlayerService;
 import ch.threema.app.ui.listitemholder.AbstractListItemHolder;
 import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
+import ch.threema.app.utils.ImageViewUtil;
 import ch.threema.app.utils.LinkifyUtil;
 import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.NameUtil;
@@ -420,7 +423,14 @@ abstract public class ChatAdapterDecorator extends AdapterDecorator {
 		return highlightMatches(string, filterString);
 	}
 
-	abstract protected void configureChatMessage(final ComposeMessageHolder holder, final int position);
+	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
+		if (holder.attachmentImage instanceof ShapeableImageView) {
+			ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel.Builder()
+				.setAllCornerSizes(ImageViewUtil.getCornerRadius(getContext()))
+				.build();
+			((ShapeableImageView) holder.attachmentImage).setShapeAppearanceModel(shapeAppearanceModel);
+		}
+	}
 
 	protected void setDatePrefix(String prefix) {
 		datePrefix = prefix;

+ 0 - 2
app/src/main/java/ch/threema/app/adapters/decorators/FileChatAdapterDecorator.java

@@ -24,7 +24,6 @@ package ch.threema.app.adapters.decorators;
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.text.format.Formatter;
 import android.view.View;
 import android.widget.Toast;
@@ -42,7 +41,6 @@ import ch.threema.app.services.messageplayer.MessagePlayer;
 import ch.threema.app.ui.ControllerView;
 import ch.threema.app.ui.DebouncedOnClickListener;
 import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
-import ch.threema.app.utils.AvatarConverterUtil;
 import ch.threema.app.utils.FileUtil;
 import ch.threema.app.utils.IconUtil;
 import ch.threema.app.utils.ImageViewUtil;

+ 4 - 1
app/src/main/java/ch/threema/app/adapters/decorators/ImageChatAdapterDecorator.java

@@ -60,8 +60,11 @@ public class ImageChatAdapterDecorator extends ChatAdapterDecorator {
 
 	@Override
 	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
+		super.configureChatMessage(holder, position);
+
 		final MessagePlayer imageMessagePlayer = getMessagePlayerService().createPlayer(getMessageModel(),
 				(Activity) getContext(), helper.getMessageReceiver(), null);
+
 		logger.debug("configureChatMessage Image");
 
 		holder.messagePlayer = imageMessagePlayer;
@@ -162,7 +165,7 @@ public class ImageChatAdapterDecorator extends ChatAdapterDecorator {
 			thumbnail = null;
 		}
 
-		ImageViewUtil.showRoundedBitmapOrImagePlaceholder(
+		ImageViewUtil.showBitmapOrImagePlaceholder(
 			getContext(),
 			holder.contentView,
 			holder.attachmentImage,

+ 3 - 1
app/src/main/java/ch/threema/app/adapters/decorators/VideoChatAdapterDecorator.java

@@ -61,6 +61,8 @@ public class VideoChatAdapterDecorator extends ChatAdapterDecorator {
 
 	@Override
 	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
+		super.configureChatMessage(holder, position);
+
 		final MessagePlayer videoMessagePlayer = getMessagePlayerService().createPlayer(getMessageModel(),
 			(Activity) getContext(), helper.getMessageReceiver(), null);
 
@@ -277,7 +279,7 @@ public class VideoChatAdapterDecorator extends ChatAdapterDecorator {
 			thumbnail = null;
 		}
 
-		ImageViewUtil.showRoundedBitmapOrMoviePlaceholder(
+		ImageViewUtil.showBitmapOrMoviePlaceholder(
 			getContext(),
 			holder.contentView,
 			holder.attachmentImage,

+ 10 - 10
app/src/main/java/ch/threema/app/backuprestore/csv/BackupService.java

@@ -1087,7 +1087,15 @@ public class BackupService extends Service {
 		}
 
 		try (ByteArrayOutputStream outputStreamBuffer = new ByteArrayOutputStream()) {
-			writeNonces(outputStreamBuffer, zipOutputStream);
+			writeNonces(outputStreamBuffer);
+			// Write nonces to zip *after* the CSVWriter has been closed (and therefore flushed)
+			ZipUtil.addZipStream(
+				zipOutputStream,
+				new ByteArrayInputStream(outputStreamBuffer.toByteArray()),
+				Tags.NONCE_FILE_NAME + Tags.CSV_FILE_POSTFIX,
+				true
+			);
+			logger.info("Nonce backup completed");
 		} catch (IOException | ThreemaException e) {
 			logger.error("Error with byte array output stream", e);
 			return false;
@@ -1097,8 +1105,7 @@ public class BackupService extends Service {
 	}
 
 	private void writeNonces(
-		@NonNull ByteArrayOutputStream outputStream,
-		@NonNull ZipOutputStream zipOutputStream
+		@NonNull ByteArrayOutputStream outputStream
 	) throws ThreemaException, IOException {
 		final String[] nonceHeader = new String[]{Tags.TAG_NONCES};
 		try (
@@ -1126,13 +1133,6 @@ public class BackupService extends Service {
 			long end = System.currentTimeMillis();
 			logger.info("Created row for all nonces in {} ms", end - start);
 		}
-		ZipUtil.addZipStream(
-			zipOutputStream,
-			new ByteArrayInputStream(outputStream.toByteArray()),
-			Tags.NONCE_FILE_NAME + Tags.CSV_FILE_POSTFIX,
-			true
-		);
-		logger.info("Nonce backup completed");
 	}
 
 	/**

+ 3 - 1
app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java

@@ -857,8 +857,9 @@ public class RestoreService extends Service {
 									if (thumbnailBytes != null && thumbnailBytes.length < MAX_THUMBNAIL_SIZE_BYTES) {
 										this.fileService.saveThumbnail(model, thumbnailBytes);
 									}
+								} catch (OutOfMemoryError e) {
+									logger.error("Not enough memory for thumbnail", e);
 								}
-								//
 							}
 						}
 					} else {
@@ -868,6 +869,7 @@ public class RestoreService extends Service {
 								imageData = IOUtils.toByteArray(inputStream);
 								this.fileService.writeConversationMedia(model, imageData);
 							} catch (OutOfMemoryError e) {
+								logger.error("Not enough memory for media", e);
 								imageData = null;
 							}
 

+ 10 - 8
app/src/main/java/ch/threema/app/camera/VideoEditView.java

@@ -51,8 +51,6 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.core.view.ViewCompat;
-import androidx.lifecycle.DefaultLifecycleObserver;
-import androidx.lifecycle.LifecycleOwner;
 
 import org.slf4j.Logger;
 
@@ -81,7 +79,7 @@ import ch.threema.app.utils.VideoUtil;
 import ch.threema.app.video.VideoTimelineThumbnailTask;
 import ch.threema.base.utils.LoggingUtil;
 
-@UnstableApi public class VideoEditView extends FrameLayout implements DefaultLifecycleObserver, VideoTimelineThumbnailTask.VideoTimelineListener {
+@UnstableApi public class VideoEditView extends FrameLayout implements VideoTimelineThumbnailTask.VideoTimelineListener {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("VideoEditView");
 
 	private static final int MOVING_NONE = 0;
@@ -154,8 +152,6 @@ import ch.threema.base.utils.LoggingUtil;
 
 		this.touchTargetWidth = context.getResources().getDimensionPixelSize(R.dimen.video_timeline_touch_target_width);
 
-		((LifecycleOwner)context).getLifecycle().addObserver(this);
-
 		LayoutInflater.from(context).inflate(R.layout.view_video_edit, this, true);
 
 		this.mediaSourceFactory = new DefaultMediaSourceFactory(context);
@@ -270,6 +266,10 @@ import ch.threema.base.utils.LoggingUtil;
 	 * @param listener the timeline drag listener
 	 */
 	public void setOnTimelineDragListener(@Nullable OnTimelineDragListener listener) {
+		if (this.timelineDragListener != null) {
+			timelineDragListener.onTimelineDragStop();
+		}
+
 		this.timelineDragListener = listener;
 	}
 
@@ -635,7 +635,7 @@ import ch.threema.base.utils.LoggingUtil;
 	}
 
 	private void updateVideoTimelineVisibility() {
-		int visibility = videoItem.getVideoSize() == PreferenceService.VideoSize_SEND_AS_FILE ? INVISIBLE : VISIBLE;
+		int visibility = videoItem != null && videoItem.getVideoSize() == PreferenceService.VideoSize_SEND_AS_FILE ? INVISIBLE : VISIBLE;
 
 		timelineGridLayout.setVisibility(visibility);
 		startContainer.setVisibility(visibility);
@@ -753,7 +753,9 @@ import ch.threema.base.utils.LoggingUtil;
 	}
 
 	@Override
-	public void onDestroy(@NonNull LifecycleOwner owner) {
+	protected void onDetachedFromWindow() {
+		this.timelineDragListener = null;
+
 		if (thumbnailThread != null && thumbnailThread.isAlive()) {
 			thumbnailThread.interrupt();
 		}
@@ -766,7 +768,7 @@ import ch.threema.base.utils.LoggingUtil;
 
 		releasePlayer();
 
-		this.context = null;
+		super.onDetachedFromWindow();
 	}
 
 	@Override

+ 2 - 0
app/src/main/java/ch/threema/app/emojis/EmojiPicker.java

@@ -45,6 +45,7 @@ import java.util.ArrayList;
 
 import ch.threema.app.R;
 import ch.threema.app.ui.LockableViewPager;
+import ch.threema.app.utils.EditTextUtil;
 import ch.threema.base.utils.LoggingUtil;
 
 public class EmojiPicker extends LinearLayout implements EmojiSearchWidget.EmojiSearchListener {
@@ -197,6 +198,7 @@ public class EmojiPicker extends LinearLayout implements EmojiSearchWidget.Emoji
 		setLayoutParams(searchLayoutParams);
 		emojiSearchWidget.setVisibility(VISIBLE);
 		emojiSearchWidget.searchInput.requestFocus();
+		EditTextUtil.showSoftKeyboard(emojiSearchWidget.searchInput);
 		pickerHeader.setVisibility(GONE);
 		viewPager.setVisibility(GONE);
 	}

+ 34 - 34
app/src/main/java/ch/threema/app/fragments/BigMediaFragment.kt

@@ -29,6 +29,7 @@ import android.widget.ImageView
 import androidx.core.view.doOnLayout
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.Lifecycle
+import androidx.media3.common.util.UnstableApi
 import androidx.viewpager2.widget.ViewPager2
 import ch.threema.app.R
 import ch.threema.app.camera.VideoEditView
@@ -38,25 +39,34 @@ import ch.threema.app.utils.BitmapUtil.FLIP_HORIZONTAL
 import ch.threema.app.utils.BitmapUtil.FLIP_VERTICAL
 import ch.threema.base.utils.LoggingUtil
 import com.bumptech.glide.Glide
-import com.bumptech.glide.load.resource.bitmap.FitCenter
 import com.bumptech.glide.load.resource.bitmap.Rotate
 import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
-import com.bumptech.glide.request.RequestOptions
 import com.google.android.material.progressindicator.CircularProgressIndicator
 import pl.droidsonroids.gif.GifImageView
 
 private val logger = LoggingUtil.getThreemaLogger("BigMediaFragment")
 
+@UnstableApi
 class BigMediaFragment : Fragment() {
     private var mediaItem: MediaItem? = null
     private var viewPager: ViewPager2? = null
     private lateinit var bigFileView: BigFileView
     private lateinit var bigImageView: ImageView
     private lateinit var bigGifImageView: GifImageView
-    private lateinit var videoEditView: VideoEditView
+    private var videoEditView: VideoEditView? = null
     private lateinit var bigProgressBar: CircularProgressIndicator
     private var bottomElemHeight: Int = 0
     private var isVideo = false
+    private val timelineDragListener : VideoEditView.OnTimelineDragListener = object :
+        VideoEditView.OnTimelineDragListener {
+        override fun onTimelineDragStart() {
+            viewPager?.isUserInputEnabled = false
+        }
+
+        override fun onTimelineDragStop() {
+            viewPager?.isUserInputEnabled = true
+        }
+    }
 
     override fun onCreateView(
         inflater: LayoutInflater, container: ViewGroup?,
@@ -88,11 +98,14 @@ class BigMediaFragment : Fragment() {
     }
 
     override fun onPause() {
+        videoEditView?.releasePlayer()
         super.onPause()
+    }
 
-        if (isVideo) {
-            videoEditView.releasePlayer()
-        }
+    override fun onDestroyView() {
+        videoEditView?.setOnTimelineDragListener(null)
+        videoEditView = null
+        super.onDestroyView()
     }
 
     fun setMediaItem(mediaItem: MediaItem) {
@@ -119,7 +132,7 @@ class BigMediaFragment : Fragment() {
 
 
         when (item.type) {
-            MediaItem.TYPE_IMAGE, MediaItem.TYPE_IMAGE_CAM, MediaItem.TYPE_GIF -> {
+            MediaItem.TYPE_IMAGE, MediaItem.TYPE_IMAGE_CAM, MediaItem.TYPE_GIF, MediaItem.TYPE_IMAGE_ANIMATED -> {
                 showBigImage(item)
             }
             MediaItem.TYPE_VIDEO, MediaItem.TYPE_VIDEO_CAM -> {
@@ -141,25 +154,21 @@ class BigMediaFragment : Fragment() {
     }
 
     fun updateVideoPlayerSound() {
-        if (isVideo) {
-            if (mediaItem?.isMuted == true) {
-                videoEditView.mutePlayer()
-            } else {
-                videoEditView.unmutePlayer()
-            }
+        if (mediaItem?.isMuted == true) {
+            videoEditView?.mutePlayer()
+        } else {
+            this.videoEditView?.unmutePlayer()
         }
     }
 
     fun updateSendAsFileState() {
-        if (isVideo) {
-            videoEditView.updateSendAsFileState()
-        }
+        this.videoEditView?.updateSendAsFileState()
     }
 
     private fun showBigFile(item: MediaItem) {
         this.bigGifImageView.visibility = View.GONE
         this.bigImageView.visibility = View.GONE
-        this.videoEditView.visibility = View.GONE
+        this.videoEditView?.visibility = View.GONE
         this.bigFileView.visibility = View.VISIBLE
         this.bigFileView.setPadding(0, 0, 0, bottomElemHeight)
         this.bigFileView.setMediaItem(item)
@@ -169,27 +178,18 @@ class BigMediaFragment : Fragment() {
         this.bigFileView.visibility = View.GONE
         this.bigImageView.visibility = View.GONE
         this.bigGifImageView.visibility = View.GONE
-        this.videoEditView.visibility = View.VISIBLE
-        this.videoEditView.setOnTimelineDragListener(object :
-            VideoEditView.OnTimelineDragListener {
-            override fun onTimelineDragStart() {
-                viewPager?.isUserInputEnabled = false
-            }
-
-            override fun onTimelineDragStop() {
-                viewPager?.isUserInputEnabled = true
-            }
-        })
-        this.videoEditView.doOnLayout {
-            this.videoEditView.setVideo(item)
+        this.videoEditView?.visibility = View.VISIBLE
+        this.videoEditView?.setOnTimelineDragListener(timelineDragListener)
+        this.videoEditView?.doOnLayout {
+            this.videoEditView?.setVideo(item)
         }
-        this.videoEditView.requestLayout()
+        this.videoEditView?.requestLayout()
     }
 
     private fun showBigImage(item: MediaItem) {
         bigImageView.visibility = View.VISIBLE
         bigFileView.visibility = View.GONE
-        videoEditView.visibility = View.GONE
+        videoEditView?.visibility = View.GONE
         if (item.type == MediaItem.TYPE_GIF) {
             bigProgressBar.visibility = View.GONE
             bigImageView.visibility = View.GONE
@@ -212,8 +212,8 @@ class BigMediaFragment : Fragment() {
 
             Glide.with(context ?: return).load(item.uri)
                 .transition(DrawableTransitionOptions.withCrossFade())
-                .apply(RequestOptions.fitCenterTransform())
-                .transform(Rotate(item.rotation), FitCenter())
+                .optionalFitCenter()
+                .optionalTransform(Rotate(item.rotation))
                 .error(R.drawable.ic_baseline_broken_image_200)
                 .into(bigImageView)
         }

+ 9 - 2
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -1371,6 +1371,15 @@ public class ComposeMessageFragment extends Fragment implements
 					DISPATCH_MODE_STOP
 				)
 			);
+			ViewCompat.setWindowInsetsAnimationCallback(
+				emojiPicker,
+				new TranslateDeferringInsetsAnimationCallback(
+					emojiPicker,
+					WindowInsetsCompat.Type.systemBars(),
+					WindowInsetsCompat.Type.ime(),
+					DISPATCH_MODE_STOP
+				)
+			);
 		} catch (NullPointerException e) {
 			logger.error("Exception", e);
 		}
@@ -1829,7 +1838,6 @@ public class ComposeMessageFragment extends Fragment implements
 	 */
 	private void setupMessageTextClickListener() {
 		if (ConfigUtils.isDefaultEmojiStyle() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
-			this.messageText.setShowSoftInputOnFocus(false);
 			this.messageText.setOnClickListener(v -> {
 				if (isEmojiPickerShown()) {
 					if (ConfigUtils.isLandscape(activity) &&
@@ -1844,7 +1852,6 @@ public class ComposeMessageFragment extends Fragment implements
 				activity.openSoftKeyboard(emojiPicker, messageText);
 			});
 		} else {
-			this.messageText.setShowSoftInputOnFocus(true);
 			this.messageText.setOnClickListener(null);
 		}
 	}

+ 33 - 15
app/src/main/java/ch/threema/app/fragments/mediaviews/ImageViewFragment.java

@@ -23,6 +23,7 @@ package ch.threema.app.fragments.mediaviews;
 
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
+import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
@@ -39,8 +40,10 @@ import java.io.File;
 import java.lang.ref.WeakReference;
 
 import androidx.annotation.NonNull;
+
 import ch.threema.app.R;
 import ch.threema.app.utils.BitmapUtil;
+import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.utils.LoggingUtil;
 
@@ -162,25 +165,40 @@ public class ImageViewFragment extends MediaViewFragment {
 	 * @param file the image file
 	 */
 	private void showImage(@NonNull File file) {
-		imageViewReference.get().setVisibility(View.VISIBLE);
-		imageViewReference.get().setImage(ImageSource.uri(file.getPath()));
+		Drawable drawable = null;
+
+		if (ConfigUtils.isSupportedAnimatedImageFormat(getMessageModel().getFileData().getMimeType())) {
+			drawable = Drawable.createFromPath(file.getPath());
+			if (drawable instanceof AnimatedImageDrawable) {
+				previewViewReference.get().setImageDrawable(drawable);
+				((AnimatedImageDrawable) drawable).start();
+				imageViewReference.get().setVisibility(View.GONE);
+			} else {
+				drawable = null;
+			}
+		}
+
+		if (drawable == null) {
+			imageViewReference.get().setImage(ImageSource.uri(file.getPath()));
+			imageViewReference.get().setVisibility(View.VISIBLE);
 
-		BitmapUtil.ExifOrientation exifOrientation = BitmapUtil.getExifOrientation(requireContext(), Uri.fromFile(file));
-		logger.debug("Orientation = " + exifOrientation);
-		int rotation = (int) exifOrientation.getRotation();
+			BitmapUtil.ExifOrientation exifOrientation = BitmapUtil.getExifOrientation(requireContext(), Uri.fromFile(file));
+			logger.debug("Orientation = " + exifOrientation);
+			int rotation = (int) exifOrientation.getRotation();
 
-		if (exifOrientation.getFlip() != BitmapUtil.FLIP_NONE) {
-			if ((exifOrientation.getFlip() & BitmapUtil.FLIP_VERTICAL) == BitmapUtil.FLIP_VERTICAL) {
-				imageViewReference.get().setScaleY(-1f);
+			if (exifOrientation.getFlip() != BitmapUtil.FLIP_NONE) {
+				if ((exifOrientation.getFlip() & BitmapUtil.FLIP_VERTICAL) == BitmapUtil.FLIP_VERTICAL) {
+					imageViewReference.get().setScaleY(-1f);
+				}
+				if ((exifOrientation.getFlip() & BitmapUtil.FLIP_HORIZONTAL) == BitmapUtil.FLIP_HORIZONTAL) {
+					imageViewReference.get().setScaleX(-1f);
+					// invert rotation to compensate for flip
+					rotation = 360 - rotation;
+				}
 			}
-			if ((exifOrientation.getFlip() & BitmapUtil.FLIP_HORIZONTAL) == BitmapUtil.FLIP_HORIZONTAL) {
-				imageViewReference.get().setScaleX(-1f);
-				// invert rotation to compensate for flip
-				rotation = 360 - rotation;
+			if (exifOrientation.getRotation() != 0F) {
+				imageViewReference.get().setOrientation(rotation);
 			}
 		}
-		if (exifOrientation.getRotation() != 0F) {
-			imageViewReference.get().setOrientation(rotation);
-		}
 	}
 }

+ 4 - 1
app/src/main/java/ch/threema/app/glide/AvatarGlideModule.java

@@ -30,8 +30,10 @@ import com.bumptech.glide.Glide;
 import com.bumptech.glide.GlideBuilder;
 import com.bumptech.glide.Registry;
 import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.load.DecodeFormat;
 import com.bumptech.glide.load.engine.executor.GlideExecutor;
 import com.bumptech.glide.module.AppGlideModule;
+import com.bumptech.glide.request.RequestOptions;
 
 import ch.threema.app.services.AvatarCacheServiceImpl;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -41,7 +43,8 @@ public class AvatarGlideModule extends AppGlideModule {
 
 	@Override
 	public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
-		builder.setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build());
+		builder.setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(4).build());
+		builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
 	}
 
 	@Override

+ 28 - 2
app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.java

@@ -50,6 +50,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.chip.Chip;
 import com.google.android.material.progressindicator.CircularProgressIndicator;
 import com.google.android.material.shape.MaterialShapeDrawable;
+import com.google.android.material.shape.ShapeAppearanceModel;
 
 import org.slf4j.Logger;
 
@@ -144,19 +145,27 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 		};
 
 		ConstraintLayout bottomSheetLayout = findViewById(R.id.bottom_sheet);
+		final float cornerSize = getResources().getDimensionPixelSize(R.dimen.bottomsheet_corner_size);
+
 		final BottomSheetBehavior<ConstraintLayout> bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
 		bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
 			@Override
 			public void onStateChanged(@NonNull View bottomSheet, int newState) {
+				Drawable background = bottomSheetLayout.getBackground();
+
 				switch (newState) {
 					case STATE_HIDDEN:
 						finish();
 						break;
 					case STATE_EXPANDED:
 						findViewById(R.id.drag_handle).setVisibility(View.INVISIBLE);
-						Drawable background = bottomSheetLayout.getBackground();
 						if (background instanceof MaterialShapeDrawable) {
-							getWindow().setStatusBarColor(((MaterialShapeDrawable) background).getResolvedTintColor());
+							MaterialShapeDrawable materialShapeDrawable = (MaterialShapeDrawable) background;
+							getWindow().setStatusBarColor(materialShapeDrawable.getResolvedTintColor());
+							ShapeAppearanceModel shapeAppearanceModel = materialShapeDrawable.getShapeAppearanceModel().toBuilder()
+								.setAllCornerSizes(0)
+								.build();
+							materialShapeDrawable.setShapeAppearanceModel(shapeAppearanceModel);
 						} else {
 							getWindow().setStatusBarColor(getResources().getColor(R.color.attach_status_bar_color_expanded));
 						}
@@ -166,6 +175,15 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 						break;
 					case STATE_DRAGGING:
 						getWindow().setStatusBarColor(getResources().getColor(R.color.attach_status_bar_color_collapsed));
+						if (background instanceof MaterialShapeDrawable) {
+							MaterialShapeDrawable materialShapeDrawable = (MaterialShapeDrawable) background;
+							materialShapeDrawable.setShapeAppearanceModel(
+								materialShapeDrawable.getShapeAppearanceModel().toBuilder()
+									.setTopLeftCornerSize(cornerSize)
+									.setTopRightCornerSize(cornerSize)
+									.build()
+							);
+						}
 					default:
 						break;
 				}
@@ -280,4 +298,12 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 			progressBar.setVisibility(View.GONE);
 		}
 	}
+
+	@Override
+	public void finish() {
+		try {
+			super.finish();
+			overridePendingTransition(0, 0);
+		} catch (Exception ignored) {}
+	}
 }

+ 22 - 4
app/src/main/java/ch/threema/app/mediaattacher/ImagePreviewFragment.java

@@ -21,12 +21,15 @@
 
 package ch.threema.app.mediaattacher;
 
+import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.ImageView;
 
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.request.target.CustomViewTarget;
@@ -45,7 +48,8 @@ import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOption
 
 public class ImagePreviewFragment extends PreviewFragment {
 	private GifImageView gifView;
-	private SubsamplingScaleImageView imageView;
+	private SubsamplingScaleImageView scaleImageView;
+	private ImageView imageView;
 
 	ImagePreviewFragment(MediaAttachItem mediaItem, MediaAttachViewModel mediaAttachViewModel){
 		super(mediaItem, mediaAttachViewModel);
@@ -63,10 +67,12 @@ public class ImagePreviewFragment extends PreviewFragment {
 		super.onViewCreated(view, savedInstanceState);
 
 		if (rootView != null) {
-			this.imageView = rootView.findViewById(R.id.thumbnail_view);
+			this.scaleImageView = rootView.findViewById(R.id.scale_image_view);
 			this.gifView = rootView.findViewById(R.id.gif_view);
+			this.imageView = rootView.findViewById(R.id.image_view);
 
 			if (mediaItem.getType() == MediaItem.TYPE_GIF) {
+				scaleImageView.setVisibility(View.GONE);
 				imageView.setVisibility(View.GONE);
 				Glide.with(ThreemaApplication.getAppContext())
 					.load(mediaItem.getUri())
@@ -77,7 +83,9 @@ public class ImagePreviewFragment extends PreviewFragment {
 				Glide.with(this)
 					.load(mediaItem.getUri())
 					.transition(withCrossFade())
-					.into(new CustomViewTarget<SubsamplingScaleImageView, Drawable>(imageView) {
+					.optionalCenterInside()
+					.error(R.drawable.ic_baseline_broken_image_200)
+					.into(new CustomViewTarget<SubsamplingScaleImageView, Drawable>(scaleImageView) {
 						@Override
 						public void onLoadFailed(@Nullable Drawable errorDrawable) {
 						}
@@ -85,7 +93,17 @@ public class ImagePreviewFragment extends PreviewFragment {
 						@Override
 						public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
 							if (resource instanceof BitmapDrawable) {
-								imageView.setImage(ImageSource.bitmap(((BitmapDrawable) resource).getBitmap()));
+								scaleImageView.setImage(ImageSource.bitmap(((BitmapDrawable) resource).getBitmap()));
+
+								scaleImageView.setVisibility(View.VISIBLE);
+								imageView.setVisibility(View.GONE);
+
+							} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && resource instanceof AnimatedImageDrawable) {
+								imageView.setImageDrawable(resource);
+								((AnimatedImageDrawable)resource).start();
+
+								imageView.setVisibility(View.VISIBLE);
+								scaleImageView.setVisibility(View.GONE);
 							}
 						}
 

+ 3 - 1
app/src/main/java/ch/threema/app/mediaattacher/ImagePreviewPagerAdapter.java

@@ -29,9 +29,11 @@ import java.util.List;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 import androidx.lifecycle.ViewModelProvider;
+import androidx.media3.common.util.UnstableApi;
 import androidx.viewpager2.adapter.FragmentStateAdapter;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.MediaViewerActivity;
@@ -57,7 +59,7 @@ public class ImagePreviewPagerAdapter extends FragmentStateAdapter {
 			args.putBoolean(MediaViewerActivity.EXTRA_ID_IMMEDIATE_PLAY, true);
 
 			PreviewFragment fragment = null;
-			if (mimeType == MediaItem.TYPE_IMAGE || mimeType == MediaItem.TYPE_GIF) {
+			if (mimeType == MediaItem.TYPE_IMAGE || mimeType == MediaItem.TYPE_GIF || mimeType == MediaItem.TYPE_IMAGE_ANIMATED) {
 				fragment = new ImagePreviewFragment(mediaAttachItem, mediaAttachViewModel);
 			} else if (mimeType == MediaItem.TYPE_VIDEO) {
 				fragment = new VideoPreviewFragment(mediaAttachItem, mediaAttachViewModel);

+ 13 - 4
app/src/main/java/ch/threema/app/mediaattacher/MediaAttachAdapter.java

@@ -85,7 +85,8 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 		ShapeableImageView imageView;
 		FrameLayout mediaFrame;
 		CheckableFrameLayout contentView;
-		LinearLayout gifIndicator;
+		LinearLayout animatedFormatLabelContainer;
+		ImageView animatedFormatLabelIconView;
 		LinearLayout videoIndicator;
 		ImageView loadErrorIndicator;
 		TextView videoDuration;
@@ -96,7 +97,8 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 			contentView = (CheckableFrameLayout) itemView;
 			mediaFrame = itemView.findViewById(R.id.media_frame);
 			imageView = itemView.findViewById(R.id.thumbnail_view);
-			gifIndicator = itemView.findViewById(R.id.gif_marker_container);
+			animatedFormatLabelContainer = itemView.findViewById(R.id.animated_format_label_container);
+			animatedFormatLabelIconView = itemView.findViewById(R.id.animated_format_label_icon);
 			videoIndicator = itemView.findViewById(R.id.video_marker_container);
 			loadErrorIndicator = itemView.findViewById(R.id.load_error_indicator);
 			videoDuration = itemView.findViewById(R.id.video_duration_text);
@@ -128,7 +130,8 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 			holder.itemId = mediaAttachItem.getId();
 			CheckableFrameLayout contentView = holder.contentView;
 			ShapeableImageView imageView = holder.imageView;
-			LinearLayout gifIndicator = holder.gifIndicator;
+			LinearLayout gifIndicator = holder.animatedFormatLabelContainer;
+			ImageView gifIcon = holder.animatedFormatLabelIconView;
 			LinearLayout videoIndicator = holder.videoIndicator;
 			ImageView loadErrorIndicator = holder.loadErrorIndicator;
 			TextView videoDuration = holder.videoDuration;
@@ -151,6 +154,7 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 				Glide.with(context).load(mediaAttachItem.getUri())
 					.transition(withCrossFade())
 					.centerInside()
+					.optionalCenterInside()
 					.addListener(new RequestListener<>() {
 						@Override
 						public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
@@ -166,7 +170,7 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 						}
 
 						@Override
-						public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+						public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, Target<Drawable> target, @NonNull DataSource dataSource, boolean isFirstResource) {
 							loadErrorIndicator.setVisibility(View.GONE);
 							contentView.setOnClickListener(view -> {
 								toggleItemChecked(mediaAttachItem);
@@ -182,7 +186,12 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 
 							if (mediaAttachItem.getType() == MediaItem.TYPE_GIF) {
 								gifIndicator.setVisibility(View.VISIBLE);
+								gifIcon.setImageResource(R.drawable.ic_gif_24dp);
 								contentView.setContentDescription(context.getString(R.string.attach_gif) +  ": " + mediaAttachItem.getDisplayName());
+							} else if (mediaAttachItem.getType() == MediaItem.TYPE_IMAGE_ANIMATED) {
+								gifIndicator.setVisibility(View.VISIBLE);
+								gifIcon.setImageResource(R.drawable.ic_webp);
+								contentView.setContentDescription("WebP: " + mediaAttachItem.getDisplayName());
 							} else {
 								gifIndicator.setVisibility(View.GONE);
 							}

+ 17 - 7
app/src/main/java/ch/threema/app/mediaattacher/MediaRepository.java

@@ -45,6 +45,8 @@ import java.util.List;
 
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ui.MediaItem;
+import ch.threema.app.utils.ConfigUtils;
+import ch.threema.app.utils.FileUtil;
 import ch.threema.app.utils.MimeUtil;
 import ch.threema.base.utils.LoggingUtil;
 
@@ -71,19 +73,25 @@ public class MediaRepository {
 		final String[] videoProjection = this.getVideoProjection();
 
 		final List<MediaAttachItem> mediaList = new ArrayList<>();
+		final String imageSelection = MediaStore.Images.ImageColumns.MIME_TYPE + " IN ('" + String.join("','", MimeUtil.getSupportedImageMimeTypes()) + "')";
 
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-			Bundle queryBundle = new Bundle();
-			queryBundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{MediaStore.Images.Media.DATE_MODIFIED});
-			queryBundle.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
+			Bundle videoQueryBundle = new Bundle();
+			videoQueryBundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{MediaStore.Images.Media.DATE_MODIFIED});
+			videoQueryBundle.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
 			if (limit != 0) {
-				queryBundle.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
+				videoQueryBundle.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
 			}
+
+			Bundle imageQueryBundle = new Bundle(videoQueryBundle);
+			// exclude unsupported mime types from image query
+			imageQueryBundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, imageSelection);
+
 			// Process images
 			try (Cursor imageCursor = appContext.getContentResolver().query(
 				MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
 				imageProjection,
-				queryBundle,
+				imageQueryBundle,
 				null
 			)) {
 				addToMediaResults(imageCursor, mediaList, false);
@@ -95,7 +103,7 @@ public class MediaRepository {
 			try (Cursor videoCursor = appContext.getContentResolver().query(
 				MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
 				videoProjection,
-				queryBundle,
+				videoQueryBundle,
 				null
 			)) {
 				addToMediaResults(videoCursor, mediaList, true);
@@ -113,7 +121,7 @@ public class MediaRepository {
 			try (Cursor imageCursor = appContext.getContentResolver().query(
 				MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
 				imageProjection,
-				null,
+				imageSelection,
 				null,
 				MediaStore.Images.Media.DATE_MODIFIED + " DESC " + addLimitQuery
 			)) {
@@ -215,6 +223,8 @@ public class MediaRepository {
 					}
 				} else if (MimeUtil.isGifFile(mimeType)) {
 					type = MediaItem.TYPE_GIF;
+				} else if (ConfigUtils.isSupportedAnimatedImageFormat(mimeType) && FileUtil.isAnimatedImageFile(contentUri)) {
+					type = MediaItem.TYPE_IMAGE_ANIMATED;
 				} else {
 					type = MediaItem.TYPE_IMAGE;
 				}

+ 9 - 0
app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java

@@ -468,6 +468,9 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 							case MediaItem.TYPE_GIF:
 								item.setIcon(R.drawable.ic_gif_24dp);
 								break;
+							case MediaItem.TYPE_IMAGE_ANIMATED:
+								item.setIcon(R.drawable.ic_webp);
+								break;
 						}
 						ConfigUtils.tintMenuItem(MediaSelectionBaseActivity.this, item, R.attr.colorOnSurface);
 					}
@@ -742,6 +745,9 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		else if (mimeTypeTitle.equals(ThreemaApplication.getAppContext().getResources().getString(R.string.media_gallery_gifs))) {
 			mimeTypeIndex = MediaItem.TYPE_GIF;
 		}
+		else if (mimeTypeTitle.equals(ThreemaApplication.getAppContext().getResources().getString(R.string.media_gallery_animated_webps))) {
+			mimeTypeIndex = MediaItem.TYPE_IMAGE_ANIMATED;
+		}
 
 		if (mimeTypeIndex != 0) {
 			mediaAttachViewModel.setMediaByType(mimeTypeIndex);
@@ -764,6 +770,8 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 				return getResources().getString(R.string.media_gallery_videos);
 			case (MediaItem.TYPE_GIF):
 				return getResources().getString(R.string.media_gallery_gifs);
+			case (MediaItem.TYPE_IMAGE_ANIMATED):
+				return getResources().getString(R.string.media_gallery_animated_webps);
 			default:
 				return null;
 		}
@@ -966,6 +974,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		}
 	}
 
+	@Override
 	public abstract void onItemChecked(int count);
 
 	protected void showPermissionRationale(int stringResource) {

+ 3 - 1
app/src/main/java/ch/threema/app/mediaattacher/VideoPreviewFragment.java

@@ -29,6 +29,7 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.media3.common.AudioAttributes;
@@ -51,7 +52,6 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.VideoUtil;
 import ch.threema.base.utils.LoggingUtil;
 
-@UnstableApi
 public class VideoPreviewFragment extends PreviewFragment implements DefaultLifecycleObserver, Player.Listener, PreviewFragmentInterface {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("VideoPreviewFragment");
 
@@ -59,6 +59,7 @@ public class VideoPreviewFragment extends PreviewFragment implements DefaultLife
 	private ExoPlayer videoPlayer;
 	private GestureFrameLayout gestureFrameLayout;
 	private final GestureController.OnStateChangeListener onGestureStateChangeListener = new GestureController.OnStateChangeListener() {
+		@OptIn(markerClass = UnstableApi.class)
 		@Override
 		public void onStateChanged(State state) {
 			if (state.getZoom() > 1.05f || state.getZoom() < 0.95f) {
@@ -143,6 +144,7 @@ public class VideoPreviewFragment extends PreviewFragment implements DefaultLife
 		initializePlayer(false);
 	}
 
+	@OptIn(markerClass = UnstableApi.class)
 	public void initializePlayer(boolean playWhenReady) {
 		try {
 			AudioAttributes audioAttributes = new AudioAttributes.Builder()

+ 3 - 0
app/src/main/java/ch/threema/app/messagereceiver/GroupMessageReceiver.java

@@ -239,6 +239,9 @@ public class GroupMessageReceiver implements MessageReceiver<GroupMessageModel>
 
 			return msg;
 		}, null, filteredIdentities);
+
+		// Save the message model as it now contains the message id
+		saveLocalModel(abstractMessageModel);
 	}
 
 	@Override

+ 4 - 0
app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt

@@ -171,6 +171,10 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment(), GenericAlertDialog.
             contactSyncPreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference: Preference, newValue: Any ->
                 val newCheckedValue = newValue == true
                 if ((preference as TwoStatePreference).isChecked != newCheckedValue) {
+                    requirePreferenceService().emailSyncHashCode = 0
+                    requirePreferenceService().phoneNumberSyncHashCode = 0
+                    requirePreferenceService().timeOfLastContactSync = 0L
+
                     if (newCheckedValue) {
                         enableSync()
                     } else {

+ 20 - 6
app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java

@@ -27,6 +27,7 @@ import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.provider.ContactsContract;
+import android.text.format.DateUtils;
 
 import androidx.annotation.RequiresPermission;
 
@@ -176,9 +177,25 @@ public class SynchronizeContactsRoutine implements Runnable {
 
 			//read emails
 			final Map<String, ContactMatchKeyEmail> emails = this.readEmails();
+			final int emailsHash = emails.keySet().hashCode();
 
 			//read phone numbers
 			final Map<String, ContactMatchKeyPhone> phoneNumbers = this.readPhoneNumbers();
+			final int phoneNumbersHash = phoneNumbers.keySet().hashCode();
+
+			if (preferenceService.getEmailSyncHashCode() == emailsHash
+				&& preferenceService.getPhoneNumberSyncHashCode() == phoneNumbersHash
+				&& (preferenceService.getTimeOfLastContactSync() + DateUtils.DAY_IN_MILLIS) > System.currentTimeMillis()) {
+				logger.info("Contacts are unchanged. Not syncing.");
+				success = true;
+				return;
+			}
+
+			preferenceService.setEmailSyncHashCode(emailsHash);
+			preferenceService.setPhoneNumberSyncHashCode(phoneNumbersHash);
+			preferenceService.setTimeOfLastContactSync(System.currentTimeMillis());
+
+			logger.info("Attempting to sync contacts {} - {}", emails.size(), phoneNumbers.size());
 
 			//send hashes to server and get result
 			MatchTokenStore matchTokenStore = new MatchTokenStore(this.preferenceService);
@@ -348,12 +365,9 @@ public class SynchronizeContactsRoutine implements Runnable {
 					List<ContactModel> contactModels = this.contactService.getByIdentities(preSynchronizedIdentities);
 					this.contactService.save(
 						contactModels,
-						new ContactService.ContactProcessor() {
-							@Override
-							public boolean process(ContactModel contactModel) {
-								contactModel.setAndroidContactLookupKey(null);
-								return true;
-							}
+						contactModel -> {
+							contactModel.setAndroidContactLookupKey(null);
+							return true;
 						}
 					);
 				}

+ 6 - 1
app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java

@@ -85,16 +85,18 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 
 	private final DrawableCrossFadeFactory factory = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
 
+	private final int avatarSizeSmall;
+
 	public AvatarCacheServiceImpl(Context context) {
 		this.context = context;
 
 		this.contactPlaceholder = VectorDrawableCompat.create(context.getResources(), R.drawable.ic_contact, null);
 		this.groupPlaceholder = VectorDrawableCompat.create(context.getResources(), R.drawable.ic_group, null);
 		this.distributionListPlaceholder = VectorDrawableCompat.create(context.getResources(), R.drawable.ic_distribution_list, null);
+		this.avatarSizeSmall = context.getResources().getDimensionPixelSize(R.dimen.avatar_size_small);
 
 		// Use dark theme default gray for placeholder
 		int color = ColorUtil.COLOR_GRAY_DARK;
-		int avatarSizeSmall = context.getResources().getDimensionPixelSize(R.dimen.avatar_size_small);
 		if (contactPlaceholder != null) {
 			AvatarConverterUtil.getAvatarBitmap(contactPlaceholder, color, avatarSizeSmall);
 		}
@@ -234,6 +236,9 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 			if (config.options.disableCache) {
 				requestBuilder = requestBuilder.skipMemoryCache(true);
 			}
+			if (!config.options.highRes) {
+				requestBuilder = requestBuilder.override(avatarSizeSmall);
+			}
 			requestBuilder.into(view);
 		} catch (Exception e) {
 			logger.debug("Glide failure", e);

+ 8 - 3
app/src/main/java/ch/threema/app/services/GroupServiceImpl.java

@@ -766,9 +766,6 @@ public class GroupServiceImpl implements GroupService {
 		for (String newMember: newMembers) {
 			logger.debug("Add member {} to group", newMember);
 			this.addMemberToGroup(groupModel, newMember);
-			ListenerManager.groupListeners.handle(listener ->
-				listener.onNewMember(groupModel, newMember, previousMemberCount)
-			);
 		}
 
 		boolean hasNewMembers = !newMembers.isEmpty();
@@ -834,6 +831,14 @@ public class GroupServiceImpl implements GroupService {
 			this.changeGroupDesc(groupModel, groupDesc);
 		}
 
+		// Trigger the listeners for the new members. Note that this sends out open polls to the new
+		// members and must therefore be called *after* the group setup has been sent.
+		for (String newMember : newMembers) {
+			ListenerManager.groupListeners.handle(listener ->
+				listener.onNewMember(groupModel, newMember, previousMemberCount)
+			);
+		}
+
 		sendGroupCallStart(groupModel, newMembers);
 
 		// Fire the group state listener if the group state has changed

+ 18 - 2
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -28,6 +28,7 @@ import static ch.threema.app.ui.MediaItem.TIME_UNDEFINED;
 import static ch.threema.app.ui.MediaItem.TYPE_FILE;
 import static ch.threema.app.ui.MediaItem.TYPE_GIF;
 import static ch.threema.app.ui.MediaItem.TYPE_IMAGE;
+import static ch.threema.app.ui.MediaItem.TYPE_IMAGE_ANIMATED;
 import static ch.threema.app.ui.MediaItem.TYPE_IMAGE_CAM;
 import static ch.threema.app.ui.MediaItem.TYPE_LOCATION;
 import static ch.threema.app.ui.MediaItem.TYPE_TEXT;
@@ -4125,10 +4126,13 @@ public class MessageServiceImpl implements MessageService {
 					logger.error("Exception", e);
 				}
 				break;
-			case TYPE_VOICEMESSAGE:
+			case TYPE_IMAGE_ANIMATED:
+				metaData.put(FileDataModel.METADATA_KEY_ANIMATED, true);
 				// fallthrough
 			case TYPE_GIF:
 				// fallthrough
+			case TYPE_VOICEMESSAGE:
+				// fallthrough
 			case TYPE_FILE:
 				// "regular" file messages
 				return getContentData(mediaItem);
@@ -4182,7 +4186,7 @@ public class MessageServiceImpl implements MessageService {
 		int mediaType = mediaItem.getType();
 
 		// we want thumbnails for images and videos even if they are to be sent as files
-		if (MimeUtil.isImageFile(fileDataModel.getMimeType()))  {
+		if (MimeUtil.isSupportedImageFile(fileDataModel.getMimeType()))  {
 			mediaType = TYPE_IMAGE;
 		} else if (MimeUtil.isVideoFile(fileDataModel.getMimeType())) {
 			mediaType = TYPE_VIDEO;
@@ -4529,6 +4533,17 @@ public class MessageServiceImpl implements MessageService {
 			mimeType = FileUtil.getMimeTypeFromUri(context, mediaItem.getUri());
 		}
 
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+			// non-animated images are being sent as png files
+			// we should fix the mime type before creating a local message model in order not to confuse the chat adapter
+			if (ConfigUtils.isSupportedAnimatedImageFormat(mimeType)
+				&& mediaItem.getType() != TYPE_IMAGE_ANIMATED
+				&& mediaItem.getType() != TYPE_FILE
+				&& mediaItem.getImageScale() != PreferenceService.ImageScale_SEND_AS_FILE) {
+				mimeType = MimeUtil.MIME_TYPE_IMAGE_PNG;
+			}
+		}
+
 		@FileData.RenderingType int renderingType = mediaItem.getRenderingType();
 
 		// rendering type overrides
@@ -4538,6 +4553,7 @@ public class MessageServiceImpl implements MessageService {
 				renderingType = FileData.RENDERING_MEDIA;
 				break;
 			case TYPE_GIF:
+			case TYPE_IMAGE_ANIMATED:
 				if (renderingType == FileData.RENDERING_DEFAULT) {
 					// do not override stickers
 					renderingType = FileData.RENDERING_MEDIA;

+ 15 - 4
app/src/main/java/ch/threema/app/services/PreferenceService.java

@@ -42,9 +42,6 @@ import ch.threema.domain.protocol.api.work.WorkDirectoryCategory;
 import ch.threema.domain.protocol.api.work.WorkOrganization;
 
 public interface PreferenceService {
-
-	void setMediaGalleryContentTypes(boolean[] contentTypes);
-
 	@Retention(RetentionPolicy.SOURCE)
 	@IntDef({ImageScale_DEFAULT, ImageScale_SMALL, ImageScale_MEDIUM, ImageScale_LARGE, ImageScale_XLARGE, ImageScale_ORIGINAL, ImageScale_SEND_AS_FILE})
 	@interface ImageScale {}
@@ -294,7 +291,7 @@ public interface PreferenceService {
 
 	boolean getWizardRunning();
 
-	boolean isGifAutoplay();
+	boolean isAnimationAutoplay();
 
 	boolean isUseProximitySensor();
 
@@ -585,4 +582,18 @@ public interface PreferenceService {
 	void removeLastNotificationRationaleShown();
 
 	void getMediaGalleryContentTypes(boolean[] contentTypes);
+
+	void setMediaGalleryContentTypes(boolean[] contentTypes);
+
+	int getEmailSyncHashCode();
+
+	int getPhoneNumberSyncHashCode();
+
+	void setEmailSyncHashCode(int emailsHash);
+
+	void setPhoneNumberSyncHashCode(int phoneNumbersHash);
+
+	void setTimeOfLastContactSync(long timeMs);
+
+	long getTimeOfLastContactSync();
 }

+ 31 - 1
app/src/main/java/ch/threema/app/services/PreferenceServiceImpl.java

@@ -788,7 +788,7 @@ public class PreferenceServiceImpl implements PreferenceService {
 	}
 
 	@Override
-	public boolean isGifAutoplay() {
+	public boolean isAnimationAutoplay() {
 		return this.preferenceStore.getBoolean(this.getKeyName(R.string.preferences__gif_autoplay));
 	}
 
@@ -1686,4 +1686,34 @@ public class PreferenceServiceImpl implements PreferenceService {
 			logger.error("JSON error", e);
 		}
 	}
+
+    @Override
+    public int getEmailSyncHashCode() {
+		return this.preferenceStore.getInt(this.getKeyName(R.string.preferences__email_sync_hash));
+    }
+
+	@Override
+	public int getPhoneNumberSyncHashCode() {
+		return this.preferenceStore.getInt(this.getKeyName(R.string.preferences__phone_number_sync_hash));
+	}
+
+	@Override
+	public void setEmailSyncHashCode(int emailsHash) {
+		this.preferenceStore.save(this.getKeyName(R.string.preferences__email_sync_hash), emailsHash);
+	}
+
+	@Override
+	public void setPhoneNumberSyncHashCode(int phoneNumbersHash) {
+		this.preferenceStore.save(this.getKeyName(R.string.preferences__phone_number_sync_hash), phoneNumbersHash);
+	}
+
+	@Override
+	public void setTimeOfLastContactSync(long timeMs) {
+		this.preferenceStore.save(this.getKeyName(R.string.preferences__contact_sync_time), timeMs);
+	}
+
+	@Override
+	public long getTimeOfLastContactSync() {
+		return this.preferenceStore.getLong(this.getKeyName(R.string.preferences__contact_sync_time));
+	}
 }

+ 206 - 0
app/src/main/java/ch/threema/app/services/messageplayer/AnimatedImageDrawableMessagePlayer.java

@@ -0,0 +1,206 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2016-2024 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.services.messageplayer;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.AnimatedImageDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.widget.ImageView;
+
+import androidx.annotation.RequiresApi;
+
+import org.slf4j.Logger;
+
+import java.io.File;
+
+import ch.threema.app.activities.MediaViewerActivity;
+import ch.threema.app.activities.ThreemaActivity;
+import ch.threema.app.messagereceiver.MessageReceiver;
+import ch.threema.app.services.FileService;
+import ch.threema.app.services.MessageService;
+import ch.threema.app.services.PreferenceService;
+import ch.threema.app.utils.IntentDataUtil;
+import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
+import ch.threema.storage.models.AbstractMessageModel;
+import ch.threema.storage.models.data.media.FileDataModel;
+import ch.threema.storage.models.data.media.MediaMessageDataInterface;
+
+/**
+ * A message player for animated image formats supported by AnimatedImageDrawable
+ * Currently, this is limited to WebP
+ */
+@RequiresApi(Build.VERSION_CODES.P)
+public class AnimatedImageDrawableMessagePlayer extends MessagePlayer {
+	private static final Logger logger = LoggingUtil.getThreemaLogger("AnimatedImageDrawableMessagePlayer");
+
+	private final PreferenceService preferenceService;
+	private Drawable imageDrawable;
+	private ImageView imageContainer;
+
+	protected AnimatedImageDrawableMessagePlayer(Context context,
+												 MessageService messageService,
+												 FileService fileService,
+												 PreferenceService preferenceService,
+												 MessageReceiver messageReceiver,
+												 AbstractMessageModel messageModel) {
+		super(context, messageService, fileService, messageReceiver, messageModel);
+		this.preferenceService = preferenceService;
+	}
+
+	public AnimatedImageDrawableMessagePlayer attachContainer(ImageView container) {
+		this.imageContainer = container;
+		return this;
+	}
+
+	@Override
+	public MediaMessageDataInterface getData() {
+		return this.getMessageModel().getFileData();
+	}
+
+	@Override
+	protected AbstractMessageModel setData(MediaMessageDataInterface data) {
+		AbstractMessageModel messageModel = this.getMessageModel();
+		messageModel.setFileData((FileDataModel) data);
+		return messageModel;
+	}
+
+	@Override
+	protected void open(final File decryptedFile) {
+		logger.debug("open(decryptedFile)");
+		if (this.currentActivityRef != null && this.currentActivityRef.get() != null && this.isReceiverMatch(this.currentMessageReceiver)) {
+			final String mimeType = getMessageModel().getFileData().getMimeType();
+
+			if (!TestUtil.empty(mimeType) && decryptedFile.exists()) {
+				if (preferenceService.isAnimationAutoplay()) {
+					autoPlay(decryptedFile);
+				} else {
+					openInExternalPlayer();
+				}
+			}
+		}
+	}
+
+	public void autoPlay(final File decryptedFile) {
+		logger.debug("autoPlay(decryptedFile)");
+
+		if (this.imageContainer != null && this.currentActivityRef != null && this.currentActivityRef.get() != null) {
+			if (this.imageDrawable != null && this.imageDrawable instanceof AnimatedImageDrawable) {
+				((AnimatedImageDrawable) this.imageDrawable).stop();
+			}
+
+			final Uri uri = Uri.parse(decryptedFile.getPath());
+			this.imageDrawable = Drawable.createFromPath(uri.getPath());
+
+			RuntimeUtil.runOnUiThread(() -> {
+				if (imageDrawable != null) {
+					imageContainer.setImageDrawable(imageDrawable);
+					if (imageDrawable instanceof AnimatedImageDrawable && preferenceService.isAnimationAutoplay()) {
+						((AnimatedImageDrawable) imageDrawable).start();
+					}
+				}
+			});
+		}
+	}
+
+	@Override
+	public boolean open() {
+		logger.debug("open");
+
+		return super.open();
+	}
+
+	public boolean autoPlay() {
+		logger.debug("autoPlay");
+
+		return super.open(true);
+	}
+
+	public void openInExternalPlayer() {
+		RuntimeUtil.runOnUiThread(() -> {
+			if (currentActivityRef != null && currentActivityRef.get() != null && this.isReceiverMatch(currentMessageReceiver)) {
+				Intent intent = new Intent(getContext(), MediaViewerActivity.class);
+				IntentDataUtil.append(getMessageModel(), intent);
+				intent.putExtra(MediaViewerActivity.EXTRA_ID_REVERSE_ORDER, true);
+				currentActivityRef.get().startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_MEDIA_VIEWER);
+			}
+		});
+	}
+
+	@Override
+	protected void makePause(int source) {
+		logger.debug("makePause");
+		if (this.imageContainer != null) {
+			if (this.imageDrawable != null && imageDrawable instanceof AnimatedImageDrawable) {
+				AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) imageDrawable;
+				if (animatedImageDrawable.isRunning()) {
+					animatedImageDrawable.stop();
+				}
+			}
+		}
+	}
+
+	@Override
+	protected void makeResume(int source) {
+		logger.debug("makeResume: " + getMessageModel().getId());
+		if (this.imageContainer != null) {
+			if (this.imageDrawable != null && imageDrawable instanceof AnimatedImageDrawable) {
+				AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) imageDrawable;
+				if (!animatedImageDrawable.isRunning()) {
+					animatedImageDrawable.start();
+				}
+			}
+		}
+	}
+
+	@Override
+	public void seekTo(int pos) {
+	}
+
+	@Override
+	public int getDuration() {
+		return 0;
+	}
+
+	@Override
+	public int getPosition() {
+		return 0;
+	}
+
+	@Override
+	public void removeListeners() {
+		super.removeListeners();
+		logger.debug("removeListeners");
+
+		// release webp players if item comes out of view
+		if (this.imageDrawable != null) {
+			if (imageDrawable instanceof AnimatedImageDrawable) {
+				((AnimatedImageDrawable) this.imageDrawable).stop();
+			}
+			this.imageDrawable = null;
+		}
+	}
+}

+ 4 - 4
app/src/main/java/ch/threema/app/services/messageplayer/GifMessagePlayer.java

@@ -88,10 +88,10 @@ public class GifMessagePlayer extends MessagePlayer {
 			final String mimeType = getMessageModel().getFileData().getMimeType();
 
 			if (!TestUtil.empty(mimeType) && decryptedFile.exists()) {
-				if (preferenceService.isGifAutoplay()) {
+				if (preferenceService.isAnimationAutoplay()) {
 					autoPlay(decryptedFile);
 				} else {
-					openInExternalPlayer(decryptedFile);
+					openInExternalPlayer();
 				}
 			}
 		}
@@ -117,7 +117,7 @@ public class GifMessagePlayer extends MessagePlayer {
 			RuntimeUtil.runOnUiThread(() -> {
 				if (gifDrawable != null && !gifDrawable.isRecycled()) {
 					imageContainer.setImageDrawable(gifDrawable);
-					if (preferenceService.isGifAutoplay()) {
+					if (preferenceService.isAnimationAutoplay()) {
 						gifDrawable.start();
 					}
 				}
@@ -138,7 +138,7 @@ public class GifMessagePlayer extends MessagePlayer {
 		return super.open(true);
 	}
 
-	public void openInExternalPlayer(File decryptedFile) {
+	public void openInExternalPlayer() {
 		RuntimeUtil.runOnUiThread(() -> {
 			if (currentActivityRef != null && currentActivityRef.get() != null && this.isReceiverMatch(currentMessageReceiver)) {
 				Intent intent = new Intent(getContext(), MediaViewerActivity.class);

+ 13 - 1
app/src/main/java/ch/threema/app/services/messageplayer/MessagePlayerServiceImpl.java

@@ -41,6 +41,7 @@ import ch.threema.app.services.DeadlineListService;
 import ch.threema.app.services.FileService;
 import ch.threema.app.services.MessageService;
 import ch.threema.app.services.PreferenceService;
+import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.MimeUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.csp.messages.file.FileData;
@@ -123,6 +124,17 @@ public class MessagePlayerServiceImpl implements MessagePlayerService {
 							mediaControllerFuture,
 							messageModel
 						);
+					} else if (ConfigUtils.isSupportedAnimatedImageFormat(messageModel.getFileData().getMimeType())
+							&& (messageModel.getFileData().getRenderingType() == FileData.RENDERING_MEDIA
+							|| messageModel.getFileData().getRenderingType() == FileData.RENDERING_STICKER)) {
+						o = new AnimatedImageDrawableMessagePlayer(
+							this.context,
+							this.messageService,
+							this.fileService,
+							this.preferenceService,
+							messageReceiver,
+							messageModel
+						);
 					} else {
 						o = new FileMessagePlayer(
 								this.context,
@@ -144,7 +156,7 @@ public class MessagePlayerServiceImpl implements MessagePlayerService {
 					messageModel.getFileData().getRenderingType() == FileData.RENDERING_MEDIA) {
 					o.setData(messageModel.getFileData());
 				}
-				logger.debug("recycling existing player " + key);
+				logger.debug("recycling existing player {}", key);
 			}
 			if (o != null) {
 				if (activity != null) {

+ 44 - 12
app/src/main/java/ch/threema/app/ui/AvatarEditView.java

@@ -27,6 +27,7 @@ import static ch.threema.app.dialogs.ContactEditDialog.CONTACT_AVATAR_WIDTH_PX;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -83,6 +84,7 @@ import ch.threema.app.activities.CropImageActivity;
 import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.glide.AvatarOptions;
 import ch.threema.app.listeners.ContactListener;
+import ch.threema.app.listeners.ProfileListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.FileService;
@@ -94,6 +96,7 @@ import ch.threema.app.utils.ColorUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ContactUtil;
 import ch.threema.app.utils.FileUtil;
+import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.utils.LoggingUtil;
@@ -198,6 +201,27 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 		}
 	};
 
+	private final ProfileListener profileListener = new ProfileListener() {
+		@Override
+		public void onAvatarChanged() {
+			reloadProfilePicture();
+		}
+
+		@Override
+		public void onAvatarRemoved() {
+			reloadProfilePicture();
+		}
+
+		@Override
+		public void onNicknameChanged(String newNickname) {}
+	};
+
+	private void reloadProfilePicture() {
+		if (avatarData != null && avatarData.getContactModel() != null && avatarData.getContactModel() == contactService.getMe()) {
+			RuntimeUtil.runOnUiThread(() -> loadAvatarForModel(avatarData.getContactModel(), null));
+		}
+	}
+
 	/**
 	 * Load saved avatar for the specified model - do not call this if changes are to be deferred
 	 */
@@ -554,20 +578,26 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 					// return from image picker
 					if (intent != null && intent.getData() != null) {
 						try {
-							avatarData.setCameraFile(fileService.createTempFile(".camera", ".jpg", false));
-							try (InputStream is = getActivity().getContentResolver().openInputStream(intent.getData());
-							    FileOutputStream fos = new FileOutputStream(avatarData.getCameraFile())) {
-								if (is != null) {
-									IOUtils.copy(is, fos);
-								} else {
-									throw new Exception("Unable to open input stream");
+							ContentResolver contentResolver = getContext().getContentResolver();
+							String mimeType = contentResolver.getType(intent.getData());
+							if (MimeUtil.isSupportedImageFile(mimeType)) {
+								avatarData.setCameraFile(fileService.createTempFile(".camera", ".jpg", false));
+								try (InputStream is = contentResolver.openInputStream(intent.getData());
+									FileOutputStream fos = new FileOutputStream(avatarData.getCameraFile())) {
+									if (is != null) {
+										IOUtils.copy(is, fos);
+									} else {
+										throw new Exception("Unable to open input stream");
+									}
+								} catch (SecurityException e) {
+									logger.error("Unable to open file selected in picker", e);
+									startSamsungPermissionFixFlow();
+									break;
 								}
-							} catch (SecurityException e) {
-								logger.error("Unable to open file selected in picker", e);
-								startSamsungPermissionFixFlow();
-								break;
+								doCrop(avatarData.getCameraFile());
+							} else {
+								Toast.makeText(getContext(), getContext().getString(R.string.unsupported_image_type, mimeType), Toast.LENGTH_LONG).show();
 							}
-							doCrop(avatarData.getCameraFile());
 						} catch (Exception e) {
 							logger.error("Exception", e);
 						}
@@ -844,11 +874,13 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 	@Override
 	public void onCreate(@NonNull LifecycleOwner owner) {
 		ListenerManager.contactListeners.add(this.contactListener);
+		ListenerManager.profileListeners.add(this.profileListener);
 	}
 
 	@Override
 	public void onDestroy(@NonNull LifecycleOwner owner) {
 		ListenerManager.contactListeners.remove(this.contactListener);
+		ListenerManager.profileListeners.remove(this.profileListener);
 	}
 
 	public interface AvatarEditListener {

+ 6 - 7
app/src/main/java/ch/threema/app/ui/MediaItem.java

@@ -78,7 +78,7 @@ public class MediaItem implements Parcelable {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("MediaItem");
 
 	@Retention(RetentionPolicy.SOURCE)
-	@IntDef({TYPE_FILE, TYPE_IMAGE, TYPE_VIDEO, TYPE_IMAGE_CAM, TYPE_VIDEO_CAM, TYPE_GIF, TYPE_VOICEMESSAGE, TYPE_TEXT, TYPE_LOCATION})
+	@IntDef({TYPE_FILE, TYPE_IMAGE, TYPE_VIDEO, TYPE_IMAGE_CAM, TYPE_VIDEO_CAM, TYPE_GIF, TYPE_VOICEMESSAGE, TYPE_TEXT, TYPE_LOCATION, TYPE_IMAGE_ANIMATED})
 	public @interface MediaType {}
 	public static final int TYPE_FILE = 0;
 	public static final int TYPE_IMAGE = 1;
@@ -89,6 +89,7 @@ public class MediaItem implements Parcelable {
 	public static final int TYPE_VOICEMESSAGE = 6;
 	public static final int TYPE_TEXT = 7;
 	public static final int TYPE_LOCATION = 8;
+	public static final int TYPE_IMAGE_ANIMATED = 9; // animated images such as animated WebP
 
 	public static final long TIME_UNDEFINED = Long.MIN_VALUE;
 
@@ -233,7 +234,7 @@ public class MediaItem implements Parcelable {
 		mediaItem.setOriginalUri(originalUri);
 		mediaItem.setFilename(FileUtil.getFilenameFromUri(context.getContentResolver(), mediaItem));
 		if (asFile) {
-			if (MimeUtil.isImageFile(mimeType)) {
+			if (MimeUtil.isSupportedImageFile(mimeType)) {
 				mediaItem.setImageScale(PreferenceService.ImageScale_SEND_AS_FILE);
 			} else if (MimeUtil.isVideoFile(mimeType)) {
 				mediaItem.setVideoSize(PreferenceService.VideoSize_SEND_AS_FILE);
@@ -263,7 +264,7 @@ public class MediaItem implements Parcelable {
 	public MediaItem(Uri uri, String mimeType, String caption) {
 		init();
 
-		this.type = MimeUtil.getMediaTypeFromMimeType(mimeType);
+		this.type = MimeUtil.getMediaTypeFromMimeType(mimeType, uri);
 		if (this.type == TYPE_FILE) {
 			this.renderingType = FileData.RENDERING_DEFAULT;
 		}
@@ -469,8 +470,7 @@ public class MediaItem implements Parcelable {
 	}
 
 	/**
-	 * get MimeType override
-	 * @return
+	 * @return the MimeType override
 	 */
 	public String getMimeType() {
 		return mimeType;
@@ -478,7 +478,6 @@ public class MediaItem implements Parcelable {
 
 	/**
 	 * set MimeType override
-	 * @param mimeType
 	 */
 	public void setMimeType(String mimeType) {
 		this.mimeType = mimeType;
@@ -595,7 +594,7 @@ public class MediaItem implements Parcelable {
 		} else if (type == TYPE_IMAGE || type == TYPE_IMAGE_CAM) {
 			return getImageScale() == PreferenceService.ImageScale_SEND_AS_FILE;
 		} else {
-			return type == TYPE_FILE || type == TYPE_GIF;
+			return type == TYPE_FILE || type == TYPE_GIF || type == TYPE_IMAGE_ANIMATED;
 		}
 	}
 

+ 9 - 3
app/src/main/java/ch/threema/app/ui/PaintView.java

@@ -40,6 +40,7 @@ import java.util.List;
 public class PaintView extends View {
 	private float mX, mY;
 	private int currentColor, currentStrokeWidth, currentWidth, currentHeight;
+	private boolean isTransparent = false;
 	private static final float TOUCH_TOLERANCE = 4;
 	private boolean isActive = true, hasMoved;
 	private TouchListener onTouchListener;
@@ -79,19 +80,20 @@ public class PaintView extends View {
 		return path;
 	}
 
-	private Paint createPaint() {
+	private void createPaint() {
 		Paint paint = new Paint();
 		paints.add(paint);
 
 		paint.setAntiAlias(true);
 		paint.setDither(true);
 		paint.setColor(currentColor);
+		if (isTransparent) {
+			paint.setAlpha(128);
+		}
 		paint.setStyle(Paint.Style.STROKE);
 		paint.setStrokeJoin(Paint.Join.ROUND);
 		paint.setStrokeCap(Paint.Cap.ROUND);
 		paint.setStrokeWidth(currentStrokeWidth);
-
-		return paint;
 	}
 
 	private Path getCurrentPath() {
@@ -257,6 +259,10 @@ public class PaintView extends View {
 		currentColor = color;
 	}
 
+	public void setTransparent(boolean isTransparent) {
+		this.isTransparent = isTransparent;
+	}
+
 	public void setStrokeWidth(int width) {
 		currentStrokeWidth = width;
 	}

+ 28 - 2
app/src/main/java/ch/threema/app/utils/ConfigUtils.java

@@ -144,7 +144,7 @@ public class ConfigUtils {
 	private static final int CONTENT_PROVIDER_BATCH_SIZE = 50;
 	private static final String WORKER_RESTART_AFTER_RESTORE = "restartAfterRestore";
 
-	/* app theme settings in shared preferences */
+    /* app theme settings in shared preferences */
 	@StringDef({THEME_LIGHT, THEME_DARK, THEME_FOLLOW_SYSTEM})
 	public @interface AppThemeSetting {}
 	public static final String THEME_LIGHT = "0";
@@ -847,6 +847,22 @@ public class ConfigUtils {
 		return metrics.widthPixels;
 	}
 
+	/**
+	 * Get real height of window including system insets in case of a fullscreen window
+	 * Also works for floating or split screen windows
+	 * @param windowManager WindowManager
+	 * @return Height in pixel
+	 */
+	public static int getRealWindowHeight(@NonNull WindowManager windowManager) {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+			return windowManager.getCurrentWindowMetrics().getBounds().height();
+		} else {
+			DisplayMetrics metrics = new DisplayMetrics();
+			windowManager.getDefaultDisplay().getRealMetrics(metrics);
+			return metrics.heightPixels;
+		}
+	}
+
 	public static boolean checkManifestPermission(Context context, String packageName, final String permission) {
 		if (TextUtils.isEmpty(permission)) {
 			return false;
@@ -1493,7 +1509,7 @@ public class ConfigUtils {
 	 * @param searchBar The search bar
 	 */
 	public static void adjustSearchBarTextViewMargin(@NonNull Context context, @NonNull SearchBar searchBar) {
-		TextView searchBarTextView = searchBar.findViewById(R.id.search_bar_text_view);
+		TextView searchBarTextView = searchBar.findViewById(R.id.open_search_bar_text_view);
 		if (searchBarTextView != null) {
 			try {
 				SearchBar.LayoutParams layoutParams = (SearchBar.LayoutParams) searchBarTextView.getLayoutParams();
@@ -1591,4 +1607,14 @@ public class ConfigUtils {
 	public static boolean isNotificationsDisabled(@NonNull Context context) {
 		return !NotificationManagerCompat.from(context).areNotificationsEnabled();
 	}
+
+	/**
+	 * Check whether the provided mime type hints at an image format capable of animations and natively supported by the operating system
+	 * @param mimeType Mime type to check
+	 * @return true if conditions are met
+	 */
+	public static boolean isSupportedAnimatedImageFormat(@Nullable String mimeType) {
+		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
+			MimeUtil.isWebPFile(mimeType);
+	}
 }

+ 8090 - 7016
app/src/main/java/ch/threema/app/utils/ExifInterface.java

@@ -30,7 +30,17 @@
 
 package ch.threema.app.utils;
 
-import android.annotation.TargetApi;
+import static ch.threema.app.utils.ExifInterfaceUtils.closeFileDescriptor;
+import static ch.threema.app.utils.ExifInterfaceUtils.closeQuietly;
+import static ch.threema.app.utils.ExifInterfaceUtils.convertToLongArray;
+import static ch.threema.app.utils.ExifInterfaceUtils.copy;
+import static ch.threema.app.utils.ExifInterfaceUtils.parseSubSeconds;
+import static ch.threema.app.utils.ExifInterfaceUtils.startsWith;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+import android.annotation.SuppressLint;
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -38,17 +48,28 @@ import android.location.Location;
 import android.media.MediaDataSource;
 import android.media.MediaMetadataRetriever;
 import android.os.Build;
+import android.system.OsConstants;
 import android.util.Pair;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import ch.threema.app.utils.ExifInterfaceUtils.Api21Impl;
+import ch.threema.app.utils.ExifInterfaceUtils.Api23Impl;
+
 import org.slf4j.Logger;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
-import java.io.Closeable;
+import java.io.ByteArrayOutputStream;
 import java.io.DataInput;
 import java.io.DataInputStream;
 import java.io.EOFException;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -68,6 +89,7 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -78,7048 +100,8100 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.CRC32;
 
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import ch.threema.base.utils.LoggingUtil;
 
-import static ch.threema.base.utils.Utils.byteArrayToHexString;
-import static java.nio.charset.StandardCharsets.US_ASCII;
-
 /**
- * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
+ * This is a class for reading and writing Exif tags in various image file formats.
+ * <p>
+ * Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF.
  * <p>
- * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF.
+ * Supported for writing: JPEG, PNG, WebP.
  * <p>
- * Attribute mutation is supported for JPEG image files.
+ * Note: JPEG and HEIF files may contain XMP data either inside the Exif data chunk or outside of
+ * it. This class will search both locations for XMP data, but if XMP data exist both inside and
+ * outside Exif, will favor the XMP data inside Exif over the one outside.
  */
 public class ExifInterface {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("ExifInterface");
 
 	private static final boolean DEBUG = false;
 
-	// The Exif tag names. See JEITA CP-3451C specifications (Exif 2.3) Section 3-8.
-	// A. Tags related to image data structure
-	/**
-	 *  <p>The number of columns of image data, equal to the number of pixels per row. In JPEG
-	 *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 256</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_IMAGE_WIDTH = "ImageWidth";
-	/**
-	 *  <p>The number of rows of image data. In JPEG compressed data, this tag shall not be used
-	 *  because a JPEG marker is used instead of it.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 257</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_IMAGE_LENGTH = "ImageLength";
-	/**
-	 *  <p>The number of bits per image component. In this standard each component of the image is
-	 *  8 bits, so the value for this tag is 8. See also {@link #TAG_SAMPLES_PER_PIXEL}. In JPEG
-	 *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 258</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 3</li>
-	 *      <li>Default = {@link #BITS_PER_SAMPLE_RGB}</li>
-	 *  </ul>
-	 */
-	public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample";
-	/**
-	 *  <p>The compression scheme used for the image data. When a primary image is JPEG compressed,
-	 *  this designation is not necessary. So, this tag shall not be recorded. When thumbnails use
-	 *  JPEG compression, this tag value is set to 6.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 259</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #DATA_UNCOMPRESSED
-	 *  @see #DATA_JPEG
-	 */
-	public static final String TAG_COMPRESSION = "Compression";
-	/**
-	 *  <p>The pixel composition. In JPEG compressed data, this tag shall not be used because a JPEG
-	 *  marker is used instead of it.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 262</li>
-	 *      <li>Type = SHORT</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #PHOTOMETRIC_INTERPRETATION_RGB
-	 *  @see #PHOTOMETRIC_INTERPRETATION_YCBCR
-	 */
-	public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
-	/**
-	 *  <p>The image orientation viewed in terms of rows and columns.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 274</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #ORIENTATION_NORMAL}</li>
-	 *  </ul>
-	 *
-	 *  @see #ORIENTATION_UNDEFINED
-	 *  @see #ORIENTATION_NORMAL
-	 *  @see #ORIENTATION_FLIP_HORIZONTAL
-	 *  @see #ORIENTATION_ROTATE_180
-	 *  @see #ORIENTATION_FLIP_VERTICAL
-	 *  @see #ORIENTATION_TRANSPOSE
-	 *  @see #ORIENTATION_ROTATE_90
-	 *  @see #ORIENTATION_TRANSVERSE
-	 *  @see #ORIENTATION_ROTATE_270
-	 */
-	public static final String TAG_ORIENTATION = "Orientation";
-	/**
-	 *  <p>The number of components per pixel. Since this standard applies to RGB and YCbCr images,
-	 *  the value set for this tag is 3. In JPEG compressed data, this tag shall not be used because
-	 *  a JPEG marker is used instead of it.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 277</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = 3</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel";
-	/**
-	 *  <p>Indicates whether pixel components are recorded in chunky or planar format. In JPEG
-	 *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.
-	 *  If this field does not exist, the TIFF default, {@link #FORMAT_CHUNKY}, is assumed.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 284</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *  </ul>
-	 *
-	 *  @see #FORMAT_CHUNKY
-	 *  @see #FORMAT_PLANAR
-	 */
-	public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration";
-	/**
-	 *  <p>The sampling ratio of chrominance components in relation to the luminance component.
-	 *  In JPEG compressed data a JPEG marker is used instead of this tag. So, this tag shall not
-	 *  be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 530</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 2</li>
-	 *      <ul>
-	 *          <li>[2, 1] = YCbCr4:2:2</li>
-	 *          <li>[2, 2] = YCbCr4:2:0</li>
-	 *          <li>Other = reserved</li>
-	 *      </ul>
-	 *  </ul>
-	 */
-	public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling";
-	/**
-	 *  <p>The position of chrominance components in relation to the luminance component. This field
-	 *  is designated only for JPEG compressed data or uncompressed YCbCr data. The TIFF default is
-	 *  {@link #Y_CB_CR_POSITIONING_CENTERED}; but when Y:Cb:Cr = 4:2:2 it is recommended in this
-	 *  standard that {@link #Y_CB_CR_POSITIONING_CO_SITED} be used to record data, in order to
-	 *  improve the image quality when viewed on TV systems. When this field does not exist,
-	 *  the reader shall assume the TIFF default. In the case of Y:Cb:Cr = 4:2:0, the TIFF default
-	 *  ({@link #Y_CB_CR_POSITIONING_CENTERED}) is recommended. If the Exif/DCF reader does not
-	 *  have the capability of supporting both kinds of positioning, it shall follow the TIFF
-	 *  default regardless of the value in this field. It is preferable that readers can support
-	 *  both centered and co-sited positioning.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 531</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #Y_CB_CR_POSITIONING_CENTERED}</li>
-	 *  </ul>
-	 *
-	 *  @see #Y_CB_CR_POSITIONING_CENTERED
-	 *  @see #Y_CB_CR_POSITIONING_CO_SITED
-	 */
-	public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning";
-	/**
-	 *  <p>The number of pixels per {@link #TAG_RESOLUTION_UNIT} in the {@link #TAG_IMAGE_WIDTH}
-	 *  direction. When the image resolution is unknown, 72 [dpi] shall be designated.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 282</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = 72</li>
-	 *  </ul>
-	 *
-	 *  @see #TAG_Y_RESOLUTION
-	 *  @see #TAG_RESOLUTION_UNIT
-	 */
-	public static final String TAG_X_RESOLUTION = "XResolution";
-	/**
-	 *  <p>The number of pixels per {@link #TAG_RESOLUTION_UNIT} in the {@link #TAG_IMAGE_WIDTH}
-	 *  direction. The same value as {@link #TAG_X_RESOLUTION} shall be designated.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 283</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = 72</li>
-	 *  </ul>
-	 *
-	 *  @see #TAG_X_RESOLUTION
-	 *  @see #TAG_RESOLUTION_UNIT
-	 */
-	public static final String TAG_Y_RESOLUTION = "YResolution";
-	/**
-	 *  <p>The unit for measuring {@link #TAG_X_RESOLUTION} and {@link #TAG_Y_RESOLUTION}. The same
-	 *  unit is used for both {@link #TAG_X_RESOLUTION} and {@link #TAG_Y_RESOLUTION}. If the image
-	 *  resolution is unknown, {@link #RESOLUTION_UNIT_INCHES} shall be designated.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 296</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
-	 *  </ul>
-	 *
-	 *  @see #RESOLUTION_UNIT_INCHES
-	 *  @see #RESOLUTION_UNIT_CENTIMETERS
-	 *  @see #TAG_X_RESOLUTION
-	 *  @see #TAG_Y_RESOLUTION
-	 */
-	public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit";
-
-	// B. Tags related to recording offset
-	/**
-	 *  <p>For each strip, the byte offset of that strip. It is recommended that this be selected
-	 *  so the number of strip bytes does not exceed 64 KBytes.In the case of JPEG compressed data,
-	 *  this designation is not necessary. So, this tag shall not be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 273</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = StripsPerImage (for {@link #FORMAT_CHUNKY})
-	 *               or {@link #TAG_SAMPLES_PER_PIXEL} * StripsPerImage
-	 *               (for {@link #FORMAT_PLANAR})</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
-	 *  / {@link #TAG_ROWS_PER_STRIP})</p>
-	 *
-	 *  @see #TAG_ROWS_PER_STRIP
-	 *  @see #TAG_STRIP_BYTE_COUNTS
-	 */
-	public static final String TAG_STRIP_OFFSETS = "StripOffsets";
-	/**
-	 *  <p>The number of rows per strip. This is the number of rows in the image of one strip when
-	 *  an image is divided into strips. In the case of JPEG compressed data, this designation is
-	 *  not necessary. So, this tag shall not be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 278</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #TAG_STRIP_OFFSETS
-	 *  @see #TAG_STRIP_BYTE_COUNTS
-	 */
-	public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip";
-	/**
-	 *  <p>The total number of bytes in each strip. In the case of JPEG compressed data, this
-	 *  designation is not necessary. So, this tag shall not be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 279</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = StripsPerImage (when using {@link #FORMAT_CHUNKY})
-	 *               or {@link #TAG_SAMPLES_PER_PIXEL} * StripsPerImage
-	 *               (when using {@link #FORMAT_PLANAR})</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
-	 *  / {@link #TAG_ROWS_PER_STRIP})</p>
-	 */
-	public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts";
-	/**
-	 *  <p>The offset to the start byte (SOI) of JPEG compressed thumbnail data. This shall not be
-	 *  used for primary image JPEG data.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 513</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat";
-	/**
-	 *  <p>The number of bytes of JPEG compressed thumbnail data. This is not used for primary image
-	 *  JPEG data. JPEG thumbnails are not divided but are recorded as a continuous JPEG bitstream
-	 *  from SOI to EOI. APPn and COM markers should not be recorded. Compressed thumbnails shall be
-	 *  recorded in no more than 64 KBytes, including all other data to be recorded in APP1.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 514</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength";
+    // The Exif tag names. See JEITA CP-3451C specifications (Exif 2.3) Section 3-8.
+    // A. Tags related to image data structure
+    /**
+     *  <p>The number of columns of image data, equal to the number of pixels per row. In JPEG
+     *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 256</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_IMAGE_WIDTH = "ImageWidth";
+    /**
+     *  <p>The number of rows of image data. In JPEG compressed data, this tag shall not be used
+     *  because a JPEG marker is used instead of it.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 257</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_IMAGE_LENGTH = "ImageLength";
+    /**
+     *  <p>The number of bits per image component. In this standard each component of the image is
+     *  8 bits, so the value for this tag is 8. See also {@link #TAG_SAMPLES_PER_PIXEL}. In JPEG
+     *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 258</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 3</li>
+     *      <li>Default = {@link #BITS_PER_SAMPLE_RGB}</li>
+     *  </ul>
+     */
+    public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample";
+    /**
+     *  <p>The compression scheme used for the image data. When a primary image is JPEG compressed,
+     *  this designation is not necessary. So, this tag shall not be recorded. When thumbnails use
+     *  JPEG compression, this tag value is set to 6.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 259</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #DATA_UNCOMPRESSED
+     *  @see #DATA_JPEG
+     */
+    public static final String TAG_COMPRESSION = "Compression";
+    /**
+     *  <p>The pixel composition. In JPEG compressed data, this tag shall not be used because a JPEG
+     *  marker is used instead of it.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 262</li>
+     *      <li>Type = SHORT</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #PHOTOMETRIC_INTERPRETATION_RGB
+     *  @see #PHOTOMETRIC_INTERPRETATION_YCBCR
+     */
+    public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
+    /**
+     *  <p>The image orientation viewed in terms of rows and columns.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 274</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #ORIENTATION_NORMAL}</li>
+     *  </ul>
+     *
+     *  @see #ORIENTATION_UNDEFINED
+     *  @see #ORIENTATION_NORMAL
+     *  @see #ORIENTATION_FLIP_HORIZONTAL
+     *  @see #ORIENTATION_ROTATE_180
+     *  @see #ORIENTATION_FLIP_VERTICAL
+     *  @see #ORIENTATION_TRANSPOSE
+     *  @see #ORIENTATION_ROTATE_90
+     *  @see #ORIENTATION_TRANSVERSE
+     *  @see #ORIENTATION_ROTATE_270
+     */
+    public static final String TAG_ORIENTATION = "Orientation";
+    /**
+     *  <p>The number of components per pixel. Since this standard applies to RGB and YCbCr images,
+     *  the value set for this tag is 3. In JPEG compressed data, this tag shall not be used because
+     *  a JPEG marker is used instead of it.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 277</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = 3</li>
+     *  </ul>
+     */
+    public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel";
+    /**
+     *  <p>Indicates whether pixel components are recorded in chunky or planar format. In JPEG
+     *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.
+     *  If this field does not exist, the TIFF default, {@link #FORMAT_CHUNKY}, is assumed.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 284</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *  </ul>
+     *
+     *  @see #FORMAT_CHUNKY
+     *  @see #FORMAT_PLANAR
+     */
+    public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration";
+    /**
+     *  <p>The sampling ratio of chrominance components in relation to the luminance component.
+     *  In JPEG compressed data a JPEG marker is used instead of this tag. So, this tag shall not
+     *  be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 530</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 2</li>
+     *      <ul>
+     *          <li>[2, 1] = YCbCr4:2:2</li>
+     *          <li>[2, 2] = YCbCr4:2:0</li>
+     *          <li>Other = reserved</li>
+     *      </ul>
+     *  </ul>
+     */
+    public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling";
+    /**
+     *  <p>The position of chrominance components in relation to the luminance component. This field
+     *  is designated only for JPEG compressed data or uncompressed YCbCr data. The TIFF default is
+     *  {@link #Y_CB_CR_POSITIONING_CENTERED}; but when Y:Cb:Cr = 4:2:2 it is recommended in this
+     *  standard that {@link #Y_CB_CR_POSITIONING_CO_SITED} be used to record data, in order to
+     *  improve the image quality when viewed on TV systems. When this field does not exist,
+     *  the reader shall assume the TIFF default. In the case of Y:Cb:Cr = 4:2:0, the TIFF default
+     *  ({@link #Y_CB_CR_POSITIONING_CENTERED}) is recommended. If the Exif/DCF reader does not
+     *  have the capability of supporting both kinds of positioning, it shall follow the TIFF
+     *  default regardless of the value in this field. It is preferable that readers can support
+     *  both centered and co-sited positioning.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 531</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #Y_CB_CR_POSITIONING_CENTERED}</li>
+     *  </ul>
+     *
+     *  @see #Y_CB_CR_POSITIONING_CENTERED
+     *  @see #Y_CB_CR_POSITIONING_CO_SITED
+     */
+    public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning";
+    /**
+     *  <p>The number of pixels per {@link #TAG_RESOLUTION_UNIT} in the {@link #TAG_IMAGE_WIDTH}
+     *  direction. When the image resolution is unknown, 72 [dpi] shall be designated.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 282</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = 72</li>
+     *  </ul>
+     *
+     *  @see #TAG_Y_RESOLUTION
+     *  @see #TAG_RESOLUTION_UNIT
+     */
+    public static final String TAG_X_RESOLUTION = "XResolution";
+    /**
+     *  <p>The number of pixels per {@link #TAG_RESOLUTION_UNIT} in the {@link #TAG_IMAGE_WIDTH}
+     *  direction. The same value as {@link #TAG_X_RESOLUTION} shall be designated.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 283</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = 72</li>
+     *  </ul>
+     *
+     *  @see #TAG_X_RESOLUTION
+     *  @see #TAG_RESOLUTION_UNIT
+     */
+    public static final String TAG_Y_RESOLUTION = "YResolution";
+    /**
+     *  <p>The unit for measuring {@link #TAG_X_RESOLUTION} and {@link #TAG_Y_RESOLUTION}. The same
+     *  unit is used for both {@link #TAG_X_RESOLUTION} and {@link #TAG_Y_RESOLUTION}. If the image
+     *  resolution is unknown, {@link #RESOLUTION_UNIT_INCHES} shall be designated.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 296</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
+     *  </ul>
+     *
+     *  @see #RESOLUTION_UNIT_INCHES
+     *  @see #RESOLUTION_UNIT_CENTIMETERS
+     *  @see #TAG_X_RESOLUTION
+     *  @see #TAG_Y_RESOLUTION
+     */
+    public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit";
+
+    // B. Tags related to recording offset
+    /**
+     *  <p>For each strip, the byte offset of that strip. It is recommended that this be selected
+     *  so the number of strip bytes does not exceed 64 KBytes.In the case of JPEG compressed data,
+     *  this designation is not necessary. So, this tag shall not be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 273</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = StripsPerImage (for {@link #FORMAT_CHUNKY})
+     *               or {@link #TAG_SAMPLES_PER_PIXEL} * StripsPerImage
+     *               (for {@link #FORMAT_PLANAR})</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
+     *  / {@link #TAG_ROWS_PER_STRIP})</p>
+     *
+     *  @see #TAG_ROWS_PER_STRIP
+     *  @see #TAG_STRIP_BYTE_COUNTS
+     */
+    public static final String TAG_STRIP_OFFSETS = "StripOffsets";
+    /**
+     *  <p>The number of rows per strip. This is the number of rows in the image of one strip when
+     *  an image is divided into strips. In the case of JPEG compressed data, this designation is
+     *  not necessary. So, this tag shall not be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 278</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #TAG_STRIP_OFFSETS
+     *  @see #TAG_STRIP_BYTE_COUNTS
+     */
+    public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip";
+    /**
+     *  <p>The total number of bytes in each strip. In the case of JPEG compressed data, this
+     *  designation is not necessary. So, this tag shall not be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 279</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = StripsPerImage (when using {@link #FORMAT_CHUNKY})
+     *               or {@link #TAG_SAMPLES_PER_PIXEL} * StripsPerImage
+     *               (when using {@link #FORMAT_PLANAR})</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
+     *  / {@link #TAG_ROWS_PER_STRIP})</p>
+     */
+    public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts";
+    /**
+     *  <p>The offset to the start byte (SOI) of JPEG compressed thumbnail data. This shall not be
+     *  used for primary image JPEG data.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 513</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat";
+    /**
+     *  <p>The number of bytes of JPEG compressed thumbnail data. This is not used for primary image
+     *  JPEG data. JPEG thumbnails are not divided but are recorded as a continuous JPEG bitstream
+     *  from SOI to EOI. APPn and COM markers should not be recorded. Compressed thumbnails shall be
+     *  recorded in no more than 64 KBytes, including all other data to be recorded in APP1.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 514</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength";
+
+    // C. Tags related to Image Data Characteristics
+    /**
+     *  <p>A transfer function for the image, described in tabular style. Normally this tag need not
+     *  be used, since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 301</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 3 * 256</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_TRANSFER_FUNCTION = "TransferFunction";
+    /**
+     *  <p>The chromaticity of the white point of the image. Normally this tag need not be used,
+     *  since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 318</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 2</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_WHITE_POINT = "WhitePoint";
+    /**
+     *  <p>The chromaticity of the three primary colors of the image. Normally this tag need not
+     *  be used, since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 319</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 6</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities";
+    /**
+     *  <p>The matrix coefficients for transformation from RGB to YCbCr image data. About
+     *  the default value, please refer to JEITA CP-3451C Spec, Annex D.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 529</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 3</li>
+     *  </ul>
+     */
+    public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients";
+    /**
+     *  <p>The reference black point value and reference white point value. No defaults are given
+     *  in TIFF, but the values below are given as defaults here. The color space is declared in
+     *  a color space information tag, with the default being the value that gives the optimal image
+     *  characteristics Interoperability these conditions</p>
+     *
+     *  <ul>
+     *      <li>Tag = 532</li>
+     *      <li>Type = RATIONAL</li>
+     *      <li>Count = 6</li>
+     *      <li>Default = [0, 255, 0, 255, 0, 255] (when {@link #TAG_PHOTOMETRIC_INTERPRETATION}
+     *                 is {@link #PHOTOMETRIC_INTERPRETATION_RGB})
+     *                 or [0, 255, 0, 128, 0, 128] (when {@link #TAG_PHOTOMETRIC_INTERPRETATION}
+     *                 is {@link #PHOTOMETRIC_INTERPRETATION_YCBCR})</li>
+     *  </ul>
+     */
+    public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite";
+
+    // D. Other tags
+    /**
+     *  <p>The date and time of image creation. In this standard it is the date and time the file
+     *  was changed. The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
+     *  the date and time separated by one blank character ({@code 0x20}). When the date and time
+     *  are unknown, all the character spaces except colons (":") should be filled with blank
+     *  characters, or else the Interoperability field should be filled with blank characters.
+     *  The character string length is 20 Bytes including NULL for termination. When the field is
+     *  left blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 306</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 19</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+     *  however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+     *  format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
+     */
+    public static final String TAG_DATETIME = "DateTime";
+    /**
+     *  <p>An ASCII string giving the title of the image. It is possible to be added a comment
+     *  such as "1988 company picnic" or the like. Two-byte character codes cannot be used. When
+     *  a 2-byte code is necessary, {@link #TAG_USER_COMMENT} is to be used.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 270</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription";
+    /**
+     *  <p>The manufacturer of the recording equipment. This is the manufacturer of the DSC,
+     *  scanner, video digitizer or other equipment that generated the image. When the field is left
+     *  blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 271</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_MAKE = "Make";
+    /**
+     *  <p>The model name or model number of the equipment. This is the model name of number of
+     *  the DSC, scanner, video digitizer or other equipment that generated the image. When
+     *  the field is left blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 272</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_MODEL = "Model";
+    /**
+     *  <p>This tag records the name and version of the software or firmware of the camera or image
+     *  input device used to generate the image. The detailed format is not specified, but it is
+     *  recommended that the example shown below be followed. When the field is left blank, it is
+     *  treated as unknown.</p>
+     *
+     *  <p>Ex.) "Exif Software Version 1.00a".</p>
+     *
+     *  <ul>
+     *      <li>Tag = 305</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SOFTWARE = "Software";
+    /**
+     *  <p>This tag records the name of the camera owner, photographer or image creator.
+     *  The detailed format is not specified, but it is recommended that the information be written
+     *  as in the example below for ease of Interoperability. When the field is left blank, it is
+     *  treated as unknown.</p>
+     *
+     *  <p>Ex.) "Camera owner, John Smith; Photographer, Michael Brown; Image creator,
+     *  Ken James"</p>
+     *
+     *  <ul>
+     *      <li>Tag = 315</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_ARTIST = "Artist";
+    /**
+     *  <p>Copyright information. In this standard the tag is used to indicate both the photographer
+     *  and editor copyrights. It is the copyright notice of the person or organization claiming
+     *  rights to the image. The Interoperability copyright statement including date and rights
+     *  should be written in this field; e.g., "Copyright, John Smith, 19xx. All rights reserved."
+     *  In this standard the field records both the photographer and editor copyrights, with each
+     *  recorded in a separate part of the statement. When there is a clear distinction between
+     *  the photographer and editor copyrights, these are to be written in the order of photographer
+     *  followed by editor copyright, separated by NULL (in this case, since the statement also ends
+     *  with a NULL, there are two NULL codes) (see example 1). When only the photographer copyright
+     *  is given, it is terminated by one NULL code (see example 2). When only the editor copyright
+     *  is given, the photographer copyright part consists of one space followed by a terminating
+     *  NULL code, then the editor copyright is given (see example 3). When the field is left blank,
+     *  it is treated as unknown.</p>
+     *
+     *  <p>Ex. 1) When both the photographer copyright and editor copyright are given.
+     *  <ul><li>Photographer copyright + NULL + editor copyright + NULL</li></ul></p>
+     *  <p>Ex. 2) When only the photographer copyright is given.
+     *  <ul><li>Photographer copyright + NULL</li></ul></p>
+     *  <p>Ex. 3) When only the editor copyright is given.
+     *  <ul><li>Space ({@code 0x20}) + NULL + editor copyright + NULL</li></ul></p>
+     *
+     *  <ul>
+     *      <li>Tag = 315</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_COPYRIGHT = "Copyright";
+
+    // Exif IFD Attribute Information
+    // A. Tags related to version
+    /**
+     *  <p>The version of this standard supported. Nonexistence of this field is taken to mean
+     *  nonconformance to the standard. In according with conformance to this standard, this tag
+     *  shall be recorded like "0230” as 4-byte ASCII.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 36864</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Length = 4</li>
+     *      <li>Default = "0230"</li>
+     *  </ul>
+     */
+    public static final String TAG_EXIF_VERSION = "ExifVersion";
+    /**
+     *  <p>The Flashpix format version supported by a FPXR file. If the FPXR function supports
+     *  Flashpix format Ver. 1.0, this is indicated similarly to {@link #TAG_EXIF_VERSION} by
+     *  recording "0100" as 4-byte ASCII.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 40960</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Length = 4</li>
+     *      <li>Default = "0100"</li>
+     *  </ul>
+     */
+    public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion";
+
+    // B. Tags related to image data characteristics
+    /**
+     *  <p>The color space information tag is always recorded as the color space specifier.
+     *  Normally {@link #COLOR_SPACE_S_RGB} is used to define the color space based on the PC
+     *  monitor conditions and environment. If a color space other than {@link #COLOR_SPACE_S_RGB}
+     *  is used, {@link #COLOR_SPACE_UNCALIBRATED} is set. Image data recorded as
+     *  {@link #COLOR_SPACE_UNCALIBRATED} may be treated as {@link #COLOR_SPACE_S_RGB} when it is
+     *  converted to Flashpix.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 40961</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *  </ul>
+     *
+     *  @see #COLOR_SPACE_S_RGB
+     *  @see #COLOR_SPACE_UNCALIBRATED
+     */
+    public static final String TAG_COLOR_SPACE = "ColorSpace";
+    /**
+     *  <p>Indicates the value of coefficient gamma. The formula of transfer function used for image
+     *  reproduction is expressed as follows.</p>
+     *
+     *  <p>(Reproduced value) = (Input value) ^ gamma</p>
+     *
+     *  <p>Both reproduced value and input value indicate normalized value, whose minimum value is
+     *  0 and maximum value is 1.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42240</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GAMMA = "Gamma";
+
+    // C. Tags related to image configuration
+    /**
+     *  <p>Information specific to compressed data. When a compressed file is recorded, the valid
+     *  width of the meaningful image shall be recorded in this tag, whether or not there is padding
+     *  data or a restart marker. This tag shall not exist in an uncompressed file.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 40962</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension";
+    /**
+     *  <p>Information specific to compressed data. When a compressed file is recorded, the valid
+     *  height of the meaningful image shall be recorded in this tag, whether or not there is
+     *  padding data or a restart marker. This tag shall not exist in an uncompressed file.
+     *  Since data padding is unnecessary in the vertical direction, the number of lines recorded
+     *  in this valid image height tag will in fact be the same as that recorded in the SOF.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 40963</li>
+     *      <li>Type = Unsigned short or Unsigned long</li>
+     *      <li>Count = 1</li>
+     *  </ul>
+     */
+    public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension";
+    /**
+     *  <p>Information specific to compressed data. The channels of each component are arranged
+     *  in order from the 1st component to the 4th. For uncompressed data the data arrangement is
+     *  given in the {@link #TAG_PHOTOMETRIC_INTERPRETATION}. However, since
+     *  {@link #TAG_PHOTOMETRIC_INTERPRETATION} can only express the order of Y, Cb and Cr, this tag
+     *  is provided for cases when compressed data uses components other than Y, Cb, and Cr and to
+     *  enable support of other sequences.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37121</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Length = 4</li>
+     *      <li>Default = 4 5 6 0 (if RGB uncompressed) or 1 2 3 0 (other cases)</li>
+     *      <ul>
+     *          <li>0 = does not exist</li>
+     *          <li>1 = Y</li>
+     *          <li>2 = Cb</li>
+     *          <li>3 = Cr</li>
+     *          <li>4 = R</li>
+     *          <li>5 = G</li>
+     *          <li>6 = B</li>
+     *          <li>other = reserved</li>
+     *      </ul>
+     *  </ul>
+     */
+    public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration";
+    /**
+     *  <p>Information specific to compressed data. The compression mode used for a compressed image
+     *  is indicated in unit bits per pixel.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37122</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel";
+
+    // D. Tags related to user information
+    /**
+     *  <p>A tag for manufacturers of Exif/DCF writers to record any desired information.
+     *  The contents are up to the manufacturer, but this tag shall not be used for any other than
+     *  its intended purpose.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37500</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_MAKER_NOTE = "MakerNote";
+    /**
+     *  <p>A tag for Exif users to write keywords or comments on the image besides those in
+     *  {@link #TAG_IMAGE_DESCRIPTION}, and without the character code limitations of it.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37510</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_USER_COMMENT = "UserComment";
+
+    // E. Tags related to related file information
+    /**
+     *  <p>This tag is used to record the name of an audio file related to the image data. The only
+     *  relational information recorded here is the Exif audio file name and extension (an ASCII
+     *  string consisting of 8 characters + '.' + 3 characters). The path is not recorded.</p>
+     *
+     *  <p>When using this tag, audio files shall be recorded in conformance to the Exif audio
+     *  format. Writers can also store the data such as Audio within APP2 as Flashpix extension
+     *  stream data. Audio files shall be recorded in conformance to the Exif audio format.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 40964</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 12</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile";
+
+    // F. Tags related to date and time
+    /**
+     *  <p>The date and time when the original image data was generated. For a DSC the date and time
+     *  the picture was taken are recorded. The format is "YYYY:MM:DD HH:MM:SS" with time shown in
+     *  24-hour format, and the date and time separated by one blank character ({@code 0x20}).
+     *  When the date and time are unknown, all the character spaces except colons (":") should be
+     *  filled with blank characters, or else the Interoperability field should be filled with blank
+     *  characters. When the field is left blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 36867</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 19</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+     *  however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+     *  format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
+     */
+    public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
+    /**
+     *  <p>The date and time when the image was stored as digital data. If, for example, an image
+     *  was captured by DSC and at the same time the file was recorded, then
+     *  {@link #TAG_DATETIME_ORIGINAL} and this tag will have the same contents. The format is
+     *  "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and the date and time separated by
+     *  one blank character ({@code 0x20}). When the date and time are unknown, all the character
+     *  spaces except colons (":")should be filled with blank characters, or else
+     *  the Interoperability field should be filled with blank characters. When the field is left
+     *  blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 36868</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 19</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+     *  however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+     *  format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
+     */
+    public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+    /**
+     *  <p>A tag used to record the offset from UTC (the time difference from Universal Time
+     *  Coordinated including daylight saving time) of the time of DateTime tag. The format when
+     *  recording the offset is "±HH:MM". The part of "±" shall be recorded as "+" or "-". When
+     *  the offsets are unknown, all the character spaces except colons (":") should be filled
+     *  with blank characters, or else the Interoperability field should be filled with blank
+     *  characters. The character string length is 7 Bytes including NULL for termination. When
+     *  the field is left blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 36880</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 7</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_OFFSET_TIME = "OffsetTime";
+    /**
+     *  <p>A tag used to record the offset from UTC (the time difference from Universal Time
+     *  Coordinated including daylight saving time) of the time of DateTimeOriginal tag. The format
+     *  when recording the offset is "±HH:MM". The part of "±" shall be recorded as "+" or "-". When
+     *  the offsets are unknown, all the character spaces except colons (":") should be filled
+     *  with blank characters, or else the Interoperability field should be filled with blank
+     *  characters. The character string length is 7 Bytes including NULL for termination. When
+     *  the field is left blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 36881</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 7</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_OFFSET_TIME_ORIGINAL = "OffsetTimeOriginal";
+    /**
+     *  <p>A tag used to record the offset from UTC (the time difference from Universal Time
+     *  Coordinated including daylight saving time) of the time of DateTimeDigitized tag. The format
+     *  when recording the offset is "±HH:MM". The part of "±" shall be recorded as "+" or "-". When
+     *  the offsets are unknown, all the character spaces except colons (":") should be filled
+     *  with blank characters, or else the Interoperability field should be filled with blank
+     *  characters. The character string length is 7 Bytes including NULL for termination. When
+     *  the field is left blank, it is treated as unknown.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 36882</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 7</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_OFFSET_TIME_DIGITIZED = "OffsetTimeDigitized";
+    /**
+     *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37520</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SUBSEC_TIME = "SubSecTime";
+    /**
+     *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME_ORIGINAL}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37521</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SUBSEC_TIME_ORIGINAL = "SubSecTimeOriginal";
+    /**
+     *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME_DIGITIZED}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37522</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SUBSEC_TIME_DIGITIZED = "SubSecTimeDigitized";
+
+    // G. Tags related to picture-taking condition
+    /**
+     *  <p>Exposure time, given in seconds.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 33434</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_EXPOSURE_TIME = "ExposureTime";
+    /**
+     *  <p>The F number.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 33437</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_F_NUMBER = "FNumber";
+    /**
+     *  <p>TThe class of the program used by the camera to set exposure when the picture is taken.
+     *  The tag values are as follows.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34850</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #EXPOSURE_PROGRAM_NOT_DEFINED}</li>
+     *  </ul>
+     *
+     *  @see #EXPOSURE_PROGRAM_NOT_DEFINED
+     *  @see #EXPOSURE_PROGRAM_MANUAL
+     *  @see #EXPOSURE_PROGRAM_NORMAL
+     *  @see #EXPOSURE_PROGRAM_APERTURE_PRIORITY
+     *  @see #EXPOSURE_PROGRAM_SHUTTER_PRIORITY
+     *  @see #EXPOSURE_PROGRAM_CREATIVE
+     *  @see #EXPOSURE_PROGRAM_ACTION
+     *  @see #EXPOSURE_PROGRAM_PORTRAIT_MODE
+     *  @see #EXPOSURE_PROGRAM_LANDSCAPE_MODE
+     */
+    public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
+    /**
+     *  <p>Indicates the spectral sensitivity of each channel of the camera used. The tag value is
+     *  an ASCII string compatible with the standard developed by the ASTM Technical committee.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34852</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity";
+    /**
+     *  @deprecated Use {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} instead.
+     *  @see #TAG_PHOTOGRAPHIC_SENSITIVITY
+     */
+    @Deprecated public static final String TAG_ISO_SPEED_RATINGS = "ISOSpeedRatings";
+    /**
+     *  <p>This tag indicates the sensitivity of the camera or input device when the image was shot.
+     *  More specifically, it indicates one of the following values that are parameters defined in
+     *  ISO 12232: standard output sensitivity (SOS), recommended exposure index (REI), or ISO
+     *  speed. Accordingly, if a tag corresponding to a parameter that is designated by
+     *  {@link #TAG_SENSITIVITY_TYPE} is recorded, the values of the tag and of this tag are
+     *  the same. However, if the value is 65535 or higher, the value of this tag shall be 65535.
+     *  When recording this tag, {@link #TAG_SENSITIVITY_TYPE} should also be recorded. In addition,
+     *  while “Count = Any”, only 1 count should be used when recording this tag.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34855</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = Any</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_PHOTOGRAPHIC_SENSITIVITY = "PhotographicSensitivity";
+    /**
+     *  <p>Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. OECF is
+     *  the relationship between the camera optical input and the image values.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34856</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_OECF = "OECF";
+    /**
+     *  <p>This tag indicates which one of the parameters of ISO12232 is
+     *  {@link #TAG_PHOTOGRAPHIC_SENSITIVITY}. Although it is an optional tag, it should be recorded
+     *  when {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} is recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34864</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #SENSITIVITY_TYPE_UNKNOWN
+     *  @see #SENSITIVITY_TYPE_SOS
+     *  @see #SENSITIVITY_TYPE_REI
+     *  @see #SENSITIVITY_TYPE_ISO_SPEED
+     *  @see #SENSITIVITY_TYPE_SOS_AND_REI
+     *  @see #SENSITIVITY_TYPE_SOS_AND_ISO
+     *  @see #SENSITIVITY_TYPE_REI_AND_ISO
+     *  @see #SENSITIVITY_TYPE_SOS_AND_REI_AND_ISO
+     */
+    public static final String TAG_SENSITIVITY_TYPE = "SensitivityType";
+    /**
+     *  <p>This tag indicates the standard output sensitivity value of a camera or input device
+     *  defined in ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
+     *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34865</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_STANDARD_OUTPUT_SENSITIVITY = "StandardOutputSensitivity";
+    /**
+     *  <p>This tag indicates the recommended exposure index value of a camera or input device
+     *  defined in ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
+     *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34866</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_RECOMMENDED_EXPOSURE_INDEX = "RecommendedExposureIndex";
+    /**
+     *  <p>This tag indicates the ISO speed value of a camera or input device that is defined in
+     *  ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
+     *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34867</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_ISO_SPEED = "ISOSpeed";
+    /**
+     *  <p>This tag indicates the ISO speed latitude yyy value of a camera or input device that is
+     *  defined in ISO 12232. However, this tag shall not be recorded without {@link #TAG_ISO_SPEED}
+     *  and {@link #TAG_ISO_SPEED_LATITUDE_ZZZ}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34868</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_ISO_SPEED_LATITUDE_YYY = "ISOSpeedLatitudeyyy";
+    /**
+     *  <p>This tag indicates the ISO speed latitude zzz value of a camera or input device that is
+     *  defined in ISO 12232. However, this tag shall not be recorded without {@link #TAG_ISO_SPEED}
+     *  and {@link #TAG_ISO_SPEED_LATITUDE_YYY}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 34869</li>
+     *      <li>Type = Unsigned long</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_ISO_SPEED_LATITUDE_ZZZ = "ISOSpeedLatitudezzz";
+    /**
+     *  <p>Shutter speed. The unit is the APEX setting.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37377</li>
+     *      <li>Type = Signed rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue";
+    /**
+     *  <p>The lens aperture. The unit is the APEX value.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37378</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_APERTURE_VALUE = "ApertureValue";
+    /**
+     *  <p>The value of brightness. The unit is the APEX value. Ordinarily it is given in the range
+     *  of -99.99 to 99.99. Note that if the numerator of the recorded value is 0xFFFFFFFF,
+     *  Unknown shall be indicated.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37379</li>
+     *      <li>Type = Signed rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue";
+    /**
+     *  <p>The exposure bias. The unit is the APEX value. Ordinarily it is given in the range of
+     *  -99.99 to 99.99.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37380</li>
+     *      <li>Type = Signed rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+    /**
+     *  <p>The smallest F number of the lens. The unit is the APEX value. Ordinarily it is given
+     *  in the range of 00.00 to 99.99, but it is not limited to this range.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37381</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue";
+    /**
+     *  <p>The distance to the subject, given in meters. Note that if the numerator of the recorded
+     *  value is 0xFFFFFFFF, Infinity shall be indicated; and if the numerator is 0, Distance
+     *  unknown shall be indicated.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37382</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
+    /**
+     *  <p>The metering mode.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37383</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #METERING_MODE_UNKNOWN}</li>
+     *  </ul>
+     *
+     *  @see #METERING_MODE_UNKNOWN
+     *  @see #METERING_MODE_AVERAGE
+     *  @see #METERING_MODE_CENTER_WEIGHT_AVERAGE
+     *  @see #METERING_MODE_SPOT
+     *  @see #METERING_MODE_MULTI_SPOT
+     *  @see #METERING_MODE_PATTERN
+     *  @see #METERING_MODE_PARTIAL
+     *  @see #METERING_MODE_OTHER
+     */
+    public static final String TAG_METERING_MODE = "MeteringMode";
+    /**
+     *  <p>The kind of light source.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37384</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #LIGHT_SOURCE_UNKNOWN}</li>
+     *  </ul>
+     *
+     *  @see #LIGHT_SOURCE_UNKNOWN
+     *  @see #LIGHT_SOURCE_DAYLIGHT
+     *  @see #LIGHT_SOURCE_FLUORESCENT
+     *  @see #LIGHT_SOURCE_TUNGSTEN
+     *  @see #LIGHT_SOURCE_FLASH
+     *  @see #LIGHT_SOURCE_FINE_WEATHER
+     *  @see #LIGHT_SOURCE_CLOUDY_WEATHER
+     *  @see #LIGHT_SOURCE_SHADE
+     *  @see #LIGHT_SOURCE_DAYLIGHT_FLUORESCENT
+     *  @see #LIGHT_SOURCE_DAY_WHITE_FLUORESCENT
+     *  @see #LIGHT_SOURCE_COOL_WHITE_FLUORESCENT
+     *  @see #LIGHT_SOURCE_WHITE_FLUORESCENT
+     *  @see #LIGHT_SOURCE_WARM_WHITE_FLUORESCENT
+     *  @see #LIGHT_SOURCE_STANDARD_LIGHT_A
+     *  @see #LIGHT_SOURCE_STANDARD_LIGHT_B
+     *  @see #LIGHT_SOURCE_STANDARD_LIGHT_C
+     *  @see #LIGHT_SOURCE_D55
+     *  @see #LIGHT_SOURCE_D65
+     *  @see #LIGHT_SOURCE_D75
+     *  @see #LIGHT_SOURCE_D50
+     *  @see #LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN
+     *  @see #LIGHT_SOURCE_OTHER
+     */
+    public static final String TAG_LIGHT_SOURCE = "LightSource";
+    /**
+     *  <p>This tag indicates the status of flash when the image was shot. Bit 0 indicates the flash
+     *  firing status, bits 1 and 2 indicate the flash return status, bits 3 and 4 indicate
+     *  the flash mode, bit 5 indicates whether the flash function is present, and bit 6 indicates
+     *  "red eye" mode.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37385</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *  </ul>
+     *
+     *  @see #FLAG_FLASH_FIRED
+     *  @see #FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED
+     *  @see #FLAG_FLASH_RETURN_LIGHT_DETECTED
+     *  @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
+     *  @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
+     *  @see #FLAG_FLASH_MODE_AUTO
+     *  @see #FLAG_FLASH_NO_FLASH_FUNCTION
+     *  @see #FLAG_FLASH_RED_EYE_SUPPORTED
+     */
+    public static final String TAG_FLASH = "Flash";
+    /**
+     *  <p>This tag indicates the location and area of the main subject in the overall scene.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37396</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 2 or 3 or 4</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  <p>The subject location and area are defined by Count values as follows.</p>
+     *
+     *  <ul>
+     *      <li>Count = 2 Indicates the location of the main subject as coordinates. The first value
+     *                    is the X coordinate and the second is the Y coordinate.</li>
+     *      <li>Count = 3 The area of the main subject is given as a circle. The circular area is
+     *                    expressed as center coordinates and diameter. The first value is
+     *                    the center X coordinate, the second is the center Y coordinate, and
+     *                    the third is the diameter.</li>
+     *      <li>Count = 4 The area of the main subject is given as a rectangle. The rectangular
+     *                    area is expressed as center coordinates and area dimensions. The first
+     *                    value is the center X coordinate, the second is the center Y coordinate,
+     *                    the third is the width of the area, and the fourth is the height of
+     *                    the area.</li>
+     *  </ul>
+     *
+     *  <p>Note that the coordinate values, width, and height are expressed in relation to the upper
+     *  left as origin, prior to rotation processing as per {@link #TAG_ORIENTATION}.</p>
+     */
+    public static final String TAG_SUBJECT_AREA = "SubjectArea";
+    /**
+     *  <p>The actual focal length of the lens, in mm. Conversion is not made to the focal length
+     *  of a 35mm film camera.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 37386</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_FOCAL_LENGTH = "FocalLength";
+    /**
+     *  <p>Indicates the strobe energy at the time the image is captured, as measured in Beam Candle
+     *  Power Seconds (BCPS).</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41483</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_FLASH_ENERGY = "FlashEnergy";
+    /**
+     *  <p>This tag records the camera or input device spatial frequency table and SFR values in
+     *  the direction of image width, image height, and diagonal direction, as specified in
+     *  ISO 12233.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41484</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse";
+    /**
+     *  <p>Indicates the number of pixels in the image width (X) direction per
+     *  {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41486</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution";
+    /**
+     *  <p>Indicates the number of pixels in the image height (Y) direction per
+     *  {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41487</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution";
+    /**
+     *  <p>Indicates the unit for measuring {@link #TAG_FOCAL_PLANE_X_RESOLUTION} and
+     *  {@link #TAG_FOCAL_PLANE_Y_RESOLUTION}. This value is the same as
+     *  {@link #TAG_RESOLUTION_UNIT}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41488</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
+     *  </ul>
+     *
+     *  @see #TAG_RESOLUTION_UNIT
+     *  @see #RESOLUTION_UNIT_INCHES
+     *  @see #RESOLUTION_UNIT_CENTIMETERS
+     */
+    public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit";
+    /**
+     *  <p>Indicates the location of the main subject in the scene. The value of this tag represents
+     *  the pixel at the center of the main subject relative to the left edge, prior to rotation
+     *  processing as per {@link #TAG_ORIENTATION}. The first value indicates the X column number
+     *  and second indicates the Y row number. When a camera records the main subject location,
+     *  it is recommended that {@link #TAG_SUBJECT_AREA} be used instead of this tag.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41492</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 2</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_SUBJECT_LOCATION = "SubjectLocation";
+    /**
+     *  <p>Indicates the exposure index selected on the camera or input device at the time the image
+     *  is captured.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41493</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_EXPOSURE_INDEX = "ExposureIndex";
+    /**
+     *  <p>Indicates the image sensor type on the camera or input device.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41495</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #SENSOR_TYPE_NOT_DEFINED
+     *  @see #SENSOR_TYPE_ONE_CHIP
+     *  @see #SENSOR_TYPE_TWO_CHIP
+     *  @see #SENSOR_TYPE_THREE_CHIP
+     *  @see #SENSOR_TYPE_COLOR_SEQUENTIAL
+     *  @see #SENSOR_TYPE_TRILINEAR
+     *  @see #SENSOR_TYPE_COLOR_SEQUENTIAL_LINEAR
+     */
+    public static final String TAG_SENSING_METHOD = "SensingMethod";
+    /**
+     *  <p>Indicates the image source. If a DSC recorded the image, this tag value always shall
+     *  be set to {@link #FILE_SOURCE_DSC}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41728</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = {@link #FILE_SOURCE_DSC}</li>
+     *  </ul>
+     *
+     *  @see #FILE_SOURCE_OTHER
+     *  @see #FILE_SOURCE_TRANSPARENT_SCANNER
+     *  @see #FILE_SOURCE_REFLEX_SCANNER
+     *  @see #FILE_SOURCE_DSC
+     */
+    public static final String TAG_FILE_SOURCE = "FileSource";
+    /**
+     *  <p>Indicates the type of scene. If a DSC recorded the image, this tag value shall always
+     *  be set to {@link #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41729</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = 1</li>
+     *  </ul>
+     *
+     *  @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED
+     */
+    public static final String TAG_SCENE_TYPE = "SceneType";
+    /**
+     *  <p>Indicates the color filter array (CFA) geometric pattern of the image sensor when
+     *  a one-chip color area sensor is used. It does not apply to all sensing methods.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41730</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #TAG_SENSING_METHOD
+     *  @see #SENSOR_TYPE_ONE_CHIP
+     */
+    public static final String TAG_CFA_PATTERN = "CFAPattern";
+    /**
+     *  <p>This tag indicates the use of special processing on image data, such as rendering geared
+     *  to output. When special processing is performed, the Exif/DCF reader is expected to disable
+     *  or minimize any further processing.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41985</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #RENDERED_PROCESS_NORMAL}</li>
+     *  </ul>
+     *
+     *  @see #RENDERED_PROCESS_NORMAL
+     *  @see #RENDERED_PROCESS_CUSTOM
+     */
+    public static final String TAG_CUSTOM_RENDERED = "CustomRendered";
+    /**
+     *  <p>This tag indicates the exposure mode set when the image was shot.
+     *  In {@link #EXPOSURE_MODE_AUTO_BRACKET}, the camera shoots a series of frames of the same
+     *  scene at different exposure settings.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41986</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #EXPOSURE_MODE_AUTO
+     *  @see #EXPOSURE_MODE_MANUAL
+     *  @see #EXPOSURE_MODE_AUTO_BRACKET
+     */
+    public static final String TAG_EXPOSURE_MODE = "ExposureMode";
+    /**
+     *  <p>This tag indicates the white balance mode set when the image was shot.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41987</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #WHITEBALANCE_AUTO
+     *  @see #WHITEBALANCE_MANUAL
+     */
+    public static final String TAG_WHITE_BALANCE = "WhiteBalance";
+    /**
+     *  <p>This tag indicates the digital zoom ratio when the image was shot. If the numerator of
+     *  the recorded value is 0, this indicates that digital zoom was not used.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41988</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+    /**
+     *  <p>This tag indicates the equivalent focal length assuming a 35mm film camera, in mm.
+     *  A value of 0 means the focal length is unknown. Note that this tag differs from
+     *  {@link #TAG_FOCAL_LENGTH}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41989</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm";
+    /**
+     *  <p>This tag indicates the type of scene that was shot. It may also be used to record
+     *  the mode in which the image was shot. Note that this differs from
+     *  {@link #TAG_SCENE_TYPE}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41990</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = 0</li>
+     *  </ul>
+     *
+     *  @see #SCENE_CAPTURE_TYPE_STANDARD
+     *  @see #SCENE_CAPTURE_TYPE_LANDSCAPE
+     *  @see #SCENE_CAPTURE_TYPE_PORTRAIT
+     *  @see #SCENE_CAPTURE_TYPE_NIGHT
+     */
+    public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType";
+    /**
+     *  <p>This tag indicates the degree of overall image gain adjustment.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41991</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #GAIN_CONTROL_NONE
+     *  @see #GAIN_CONTROL_LOW_GAIN_UP
+     *  @see #GAIN_CONTROL_HIGH_GAIN_UP
+     *  @see #GAIN_CONTROL_LOW_GAIN_DOWN
+     *  @see #GAIN_CONTROL_HIGH_GAIN_DOWN
+     */
+    public static final String TAG_GAIN_CONTROL = "GainControl";
+    /**
+     *  <p>This tag indicates the direction of contrast processing applied by the camera when
+     *  the image was shot.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41992</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #CONTRAST_NORMAL}</li>
+     *  </ul>
+     *
+     *  @see #CONTRAST_NORMAL
+     *  @see #CONTRAST_SOFT
+     *  @see #CONTRAST_HARD
+     */
+    public static final String TAG_CONTRAST = "Contrast";
+    /**
+     *  <p>This tag indicates the direction of saturation processing applied by the camera when
+     *  the image was shot.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41993</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #SATURATION_NORMAL}</li>
+     *  </ul>
+     *
+     *  @see #SATURATION_NORMAL
+     *  @see #SATURATION_LOW
+     *  @see #SATURATION_HIGH
+     */
+    public static final String TAG_SATURATION = "Saturation";
+    /**
+     *  <p>This tag indicates the direction of sharpness processing applied by the camera when
+     *  the image was shot.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41994</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = {@link #SHARPNESS_NORMAL}</li>
+     *  </ul>
+     *
+     *  @see #SHARPNESS_NORMAL
+     *  @see #SHARPNESS_SOFT
+     *  @see #SHARPNESS_HARD
+     */
+    public static final String TAG_SHARPNESS = "Sharpness";
+    /**
+     *  <p>This tag indicates information on the picture-taking conditions of a particular camera
+     *  model. The tag is used only to indicate the picture-taking conditions in the Exif/DCF
+     *  reader.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41995</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription";
+    /**
+     *  <p>This tag indicates the distance to the subject.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 41996</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #SUBJECT_DISTANCE_RANGE_UNKNOWN
+     *  @see #SUBJECT_DISTANCE_RANGE_MACRO
+     *  @see #SUBJECT_DISTANCE_RANGE_CLOSE_VIEW
+     *  @see #SUBJECT_DISTANCE_RANGE_DISTANT_VIEW
+     */
+    public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange";
+
+    // H. Other tags
+    /**
+     *  <p>This tag indicates an identifier assigned uniquely to each image. It is recorded as
+     *  an ASCII string equivalent to hexadecimal notation and 128-bit fixed length.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42016</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 32</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID";
+    /**
+     *  <p>This tag records the owner of a camera used in photography as an ASCII string.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42032</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @deprecated Use {@link #TAG_CAMERA_OWNER_NAME} instead.
+     */
+    @Deprecated
+    public static final String TAG_CAMARA_OWNER_NAME = "CameraOwnerName";
+    /**
+     *  <p>This tag records the owner of a camera used in photography as an ASCII string.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42032</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_CAMERA_OWNER_NAME = "CameraOwnerName";
+    /**
+     *  <p>This tag records the serial number of the body of the camera that was used in photography
+     *  as an ASCII string.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42033</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_BODY_SERIAL_NUMBER = "BodySerialNumber";
+    /**
+     *  <p>This tag notes minimum focal length, maximum focal length, minimum F number in the
+     *  minimum focal length, and minimum F number in the maximum focal length, which are
+     *  specification information for the lens that was used in photography. When the minimum
+     *  F number is unknown, the notation is 0/0.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42034</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 4</li>
+     *      <li>Default = None</li>
+     *      <ul>
+     *          <li>Value 1 := Minimum focal length (unit: mm)</li>
+     *          <li>Value 2 : = Maximum focal length (unit: mm)</li>
+     *          <li>Value 3 : = Minimum F number in the minimum focal length</li>
+     *          <li>Value 4 : = Minimum F number in the maximum focal length</li>
+     *      </ul>
+     *  </ul>
+     */
+    public static final String TAG_LENS_SPECIFICATION = "LensSpecification";
+    /**
+     *  <p>This tag records the lens manufacturer as an ASCII string.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42035</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_LENS_MAKE = "LensMake";
+    /**
+     *  <p>This tag records the lens’s model name and model number as an ASCII string.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42036</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_LENS_MODEL = "LensModel";
+    /**
+     *  <p>This tag records the serial number of the interchangeable lens that was used in
+     *  photography as an ASCII string.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 42037</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_LENS_SERIAL_NUMBER = "LensSerialNumber";
+
+    // GPS Attribute Information
+    /**
+     *  <p>Indicates the version of GPS Info IFD. The version is given as 2.3.0.0. This tag is
+     *  mandatory when GPS-related tags are present. Note that this tag is written as a different
+     *  byte than {@link #TAG_EXIF_VERSION}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 0</li>
+     *      <li>Type = Byte</li>
+     *      <li>Count = 4</li>
+     *      <li>Default = 2.3.0.0</li>
+     *      <ul>
+     *          <li>2300 = Version 2.3</li>
+     *          <li>Other = reserved</li>
+     *      </ul>
+     *  </ul>
+     */
+    public static final String TAG_GPS_VERSION_ID = "GPSVersionID";
+    /**
+     *  <p>Indicates whether the latitude is north or south latitude.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 1</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #LATITUDE_NORTH
+     *  @see #LATITUDE_SOUTH
+     */
+    public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
+    /**
+     *  <p>Indicates the latitude. The latitude is expressed as three RATIONAL values giving
+     *  the degrees, minutes, and seconds, respectively. If latitude is expressed as degrees,
+     *  minutes and seconds, a typical format would be dd/1,mm/1,ss/1. When degrees and minutes are
+     *  used and, for example, fractions of minutes are given up to two decimal places, the format
+     *  would be dd/1,mmmm/100,0/1.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 2</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 3</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_LATITUDE = "GPSLatitude";
+    /**
+     *  <p>Indicates whether the longitude is east or west longitude.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 3</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #LONGITUDE_EAST
+     *  @see #LONGITUDE_WEST
+     */
+    public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
+    /**
+     *  <p>Indicates the longitude. The longitude is expressed as three RATIONAL values giving
+     *  the degrees, minutes, and seconds, respectively. If longitude is expressed as degrees,
+     *  minutes and seconds, a typical format would be ddd/1,mm/1,ss/1. When degrees and minutes
+     *  are used and, for example, fractions of minutes are given up to two decimal places,
+     *  the format would be ddd/1,mmmm/100,0/1.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 4</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 3</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
+    /**
+     *  <p>Indicates the altitude used as the reference altitude. If the reference is sea level
+     *  and the altitude is above sea level, 0 is given. If the altitude is below sea level,
+     *  a value of 1 is given and the altitude is indicated as an absolute value in
+     *  {@link #TAG_GPS_ALTITUDE}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 5</li>
+     *      <li>Type = Byte</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = 0</li>
+     *  </ul>
+     *
+     *  @see #ALTITUDE_ABOVE_SEA_LEVEL
+     *  @see #ALTITUDE_BELOW_SEA_LEVEL
+     */
+    public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef";
+    /**
+     *  <p>Indicates the altitude based on the reference in {@link #TAG_GPS_ALTITUDE_REF}.
+     *  The reference unit is meters.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 6</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
+    /**
+     *  <p>Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
+     *  unsigned rational values giving the hour, minute, and second.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 7</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 3</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
+    /**
+     *  <p>Indicates the GPS satellites used for measurements. This tag may be used to describe
+     *  the number of satellites, their ID number, angle of elevation, azimuth, SNR and other
+     *  information in ASCII notation. The format is not specified. If the GPS receiver is incapable
+     *  of taking measurements, value of the tag shall be set to {@code null}.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 8</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_SATELLITES = "GPSSatellites";
+    /**
+     *  <p>Indicates the status of the GPS receiver when the image is recorded. 'A' means
+     *  measurement is in progress, and 'V' means the measurement is interrupted.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 9</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #GPS_MEASUREMENT_IN_PROGRESS
+     *  @see #GPS_MEASUREMENT_INTERRUPTED
+     */
+    public static final String TAG_GPS_STATUS = "GPSStatus";
+    /**
+     *  <p>Indicates the GPS measurement mode. Originally it was defined for GPS, but it may
+     *  be used for recording a measure mode to record the position information provided from
+     *  a mobile base station or wireless LAN as well as GPS.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 10</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #GPS_MEASUREMENT_2D
+     *  @see #GPS_MEASUREMENT_3D
+     */
+    public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode";
+    /**
+     *  <p>Indicates the GPS DOP (data degree of precision). An HDOP value is written during
+     *  two-dimensional measurement, and PDOP during three-dimensional measurement.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 11</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_DOP = "GPSDOP";
+    /**
+     *  <p>Indicates the unit used to express the GPS receiver speed of movement.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 12</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = {@link #GPS_SPEED_KILOMETERS_PER_HOUR}</li>
+     *  </ul>
+     *
+     *  @see #GPS_SPEED_KILOMETERS_PER_HOUR
+     *  @see #GPS_SPEED_MILES_PER_HOUR
+     *  @see #GPS_SPEED_KNOTS
+     */
+    public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
+    /**
+     *  <p>Indicates the speed of GPS receiver movement.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 13</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_SPEED = "GPSSpeed";
+    /**
+     *  <p>Indicates the reference for giving the direction of GPS receiver movement.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 14</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
+     *  </ul>
+     *
+     *  @see #GPS_DIRECTION_TRUE
+     *  @see #GPS_DIRECTION_MAGNETIC
+     */
+    public static final String TAG_GPS_TRACK_REF = "GPSTrackRef";
+    /**
+     *  <p>Indicates the direction of GPS receiver movement.
+     *  The range of values is from 0.00 to 359.99.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 15</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_TRACK = "GPSTrack";
+    /**
+     *  <p>Indicates the reference for giving the direction of the image when it is captured.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 16</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
+     *  </ul>
+     *
+     *  @see #GPS_DIRECTION_TRUE
+     *  @see #GPS_DIRECTION_MAGNETIC
+     */
+    public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef";
+    /**
+     *  <p>ndicates the direction of the image when it was captured.
+     *  The range of values is from 0.00 to 359.99.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 17</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection";
+    /**
+     *  <p>Indicates the geodetic survey data used by the GPS receiver. If the survey data is
+     *  restricted to Japan,the value of this tag is 'TOKYO' or 'WGS-84'. If a GPS Info tag is
+     *  recorded, it is strongly recommended that this tag be recorded.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 18</li>
+     *      <li>Type = String</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum";
+    /**
+     *  <p>Indicates whether the latitude of the destination point is north or south latitude.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 19</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #LATITUDE_NORTH
+     *  @see #LATITUDE_SOUTH
+     */
+    public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef";
+    /**
+     *  <p>Indicates the latitude of the destination point. The latitude is expressed as three
+     *  unsigned rational values giving the degrees, minutes, and seconds, respectively.
+     *  If latitude is expressed as degrees, minutes and seconds, a typical format would be
+     *  dd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes
+     *  are given up to two decimal places, the format would be dd/1, mmmm/100, 0/1.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 20</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 3</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude";
+    /**
+     *  <p>Indicates whether the longitude of the destination point is east or west longitude.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 21</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #LONGITUDE_EAST
+     *  @see #LONGITUDE_WEST
+     */
+    public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef";
+    /**
+     *  <p>Indicates the longitude of the destination point. The longitude is expressed as three
+     *  unsigned rational values giving the degrees, minutes, and seconds, respectively.
+     *  If longitude is expressed as degrees, minutes and seconds, a typical format would be ddd/1,
+     *  mm/1, ss/1. When degrees and minutes are used and, for example, fractions of minutes are
+     *  given up to two decimal places, the format would be ddd/1, mmmm/100, 0/1.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 22</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 3</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude";
+    /**
+     *  <p>Indicates the reference used for giving the bearing to the destination point.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 23</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
+     *  </ul>
+     *
+     *  @see #GPS_DIRECTION_TRUE
+     *  @see #GPS_DIRECTION_MAGNETIC
+     */
+    public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef";
+    /**
+     *  <p>Indicates the bearing to the destination point.
+     *  The range of values is from 0.00 to 359.99.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 24</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing";
+    /**
+     *  <p>Indicates the unit used to express the distance to the destination point.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 25</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 1</li>
+     *      <li>Default = {@link #GPS_DISTANCE_KILOMETERS}</li>
+     *  </ul>
+     *
+     *  @see #GPS_DISTANCE_KILOMETERS
+     *  @see #GPS_DISTANCE_MILES
+     *  @see #GPS_DISTANCE_NAUTICAL_MILES
+     */
+    public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef";
+    /**
+     *  <p>Indicates the distance to the destination point.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 26</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance";
+    /**
+     *  <p>A character string recording the name of the method used for location finding.
+     *  The first byte indicates the character code used, and this is followed by the name of
+     *  the method.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 27</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
+    /**
+     *  <p>A character string recording the name of the GPS area. The first byte indicates
+     *  the character code used, and this is followed by the name of the GPS area.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 28</li>
+     *      <li>Type = Undefined</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation";
+    /**
+     *  <p>A character string recording date and time information relative to UTC (Coordinated
+     *  Universal Time). The format is "YYYY:MM:DD".</p>
+     *
+     *  <ul>
+     *      <li>Tag = 29</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 10</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_DATESTAMP = "GPSDateStamp";
+    /**
+     *  <p>Indicates whether differential correction is applied to the GPS receiver.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 30</li>
+     *      <li>Type = Unsigned short</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     *
+     *  @see #GPS_MEASUREMENT_NO_DIFFERENTIAL
+     *  @see #GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED
+     */
+    public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential";
+    /**
+     *  <p>This tag indicates horizontal positioning errors in meters.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 31</li>
+     *      <li>Type = Unsigned rational</li>
+     *      <li>Count = 1</li>
+     *      <li>Default = None</li>
+     *  </ul>
+     */
+    public static final String TAG_GPS_H_POSITIONING_ERROR = "GPSHPositioningError";
+
+    // Interoperability IFD Attribute Information
+    /**
+     *  <p>Indicates the identification of the Interoperability rule.</p>
+     *
+     *  <ul>
+     *      <li>Tag = 1</li>
+     *      <li>Type = String</li>
+     *      <li>Length = 4</li>
+     *      <li>Default = None</li>
+     *      <ul>
+     *          <li>"R98" = Indicates a file conforming to R98 file specification of Recommended
+     *                      Exif Interoperability Rules (Exif R 98) or to DCF basic file stipulated
+     *                      by Design Rule for Camera File System.</li>
+     *          <li>"THM" = Indicates a file conforming to DCF thumbnail file stipulated by Design
+     *                      rule for Camera File System.</li>
+     *          <li>“R03” = Indicates a file conforming to DCF Option File stipulated by Design rule
+     *                      for Camera File System.</li>
+     *      </ul>
+     *  </ul>
+     */
+    public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex";
+
+    /**
+     * @see #TAG_IMAGE_LENGTH
+     */
+    public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength";
+    /**
+     * @see #TAG_IMAGE_WIDTH
+     */
+    public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth";
+
+    // TODO: Unhide this when it can be public.
+    /**
+     * @see #TAG_ORIENTATION
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String TAG_THUMBNAIL_ORIENTATION = "ThumbnailOrientation";
+    /** Type is int. DNG Specification 1.4.0.0. Section 4 */
+    public static final String TAG_DNG_VERSION = "DNGVersion";
+    /** Type is int. DNG Specification 1.4.0.0. Section 4 */
+    public static final String TAG_DEFAULT_CROP_SIZE = "DefaultCropSize";
+    /** Type is undefined. See Olympus MakerNote tags in http://www.exiv2.org/tags-olympus.html. */
+    public static final String TAG_ORF_THUMBNAIL_IMAGE = "ThumbnailImage";
+    /** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
+    public static final String TAG_ORF_PREVIEW_IMAGE_START = "PreviewImageStart";
+    /** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
+    public static final String TAG_ORF_PREVIEW_IMAGE_LENGTH = "PreviewImageLength";
+    /** Type is int. See Olympus Image Processing tags in http://www.exiv2.org/tags-olympus.html. */
+    public static final String TAG_ORF_ASPECT_FRAME = "AspectFrame";
+    /**
+     * Type is int. See PanasonicRaw tags in
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
+     */
+    public static final String TAG_RW2_SENSOR_BOTTOM_BORDER = "SensorBottomBorder";
+    /**
+     * Type is int. See PanasonicRaw tags in
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
+     */
+    public static final String TAG_RW2_SENSOR_LEFT_BORDER = "SensorLeftBorder";
+    /**
+     * Type is int. See PanasonicRaw tags in
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
+     */
+    public static final String TAG_RW2_SENSOR_RIGHT_BORDER = "SensorRightBorder";
+    /**
+     * Type is int. See PanasonicRaw tags in
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
+     */
+    public static final String TAG_RW2_SENSOR_TOP_BORDER = "SensorTopBorder";
+    /**
+     * Type is int. See PanasonicRaw tags in
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
+     */
+    public static final String TAG_RW2_ISO = "ISO";
+    /**
+     * Type is undefined. See PanasonicRaw tags in
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
+     */
+    public static final String TAG_RW2_JPG_FROM_RAW = "JpgFromRaw";
+    /**
+     * Type is byte[]. See <a href=
+     * "https://en.wikipedia.org/wiki/Extensible_Metadata_Platform">Extensible
+     * Metadata Platform (XMP)</a> for details on contents.
+     */
+    public static final String TAG_XMP = "Xmp";
+    /** Type is int. See JEITA CP-3451C Spec Section 3: Bilevel Images. */
+    public static final String TAG_NEW_SUBFILE_TYPE = "NewSubfileType";
+    /** Type is int. See JEITA CP-3451C Spec Section 3: Bilevel Images. */
+    public static final String TAG_SUBFILE_TYPE = "SubfileType";
+
+    /**
+     * Private tags used for pointing the other IFD offsets.
+     * The types of the following tags are int.
+     * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
+     * For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
+     */
+    private static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
+    private static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
+    private static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
+    private static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
+    // Proprietary pointer tags used for ORF files.
+    // See http://www.exiv2.org/tags-olympus.html
+    private static final String TAG_ORF_CAMERA_SETTINGS_IFD_POINTER = "CameraSettingsIFDPointer";
+    private static final String TAG_ORF_IMAGE_PROCESSING_IFD_POINTER = "ImageProcessingIFDPointer";
+
+    private static final int MAX_THUMBNAIL_SIZE = 512;
+
+    // Constants used for the Orientation Exif tag.
+    public static final int ORIENTATION_UNDEFINED = 0;
+    public static final int ORIENTATION_NORMAL = 1;
+    /**
+     * Indicates the image is left right reversed mirror.
+     */
+    public static final int ORIENTATION_FLIP_HORIZONTAL = 2;
+    /**
+     * Indicates the image is rotated by 180 degree clockwise.
+     */
+    public static final int ORIENTATION_ROTATE_180 = 3;
+    /**
+     * Indicates the image is upside down mirror, it can also be represented by flip
+     * horizontally firstly and rotate 180 degree clockwise.
+     */
+    public static final int ORIENTATION_FLIP_VERTICAL = 4;
+    /**
+     * Indicates the image is flipped about top-left <--> bottom-right axis, it can also be
+     * represented by flip horizontally firstly and rotate 270 degree clockwise.
+     */
+    public static final int ORIENTATION_TRANSPOSE = 5;
+    /**
+     * Indicates the image is rotated by 90 degree clockwise.
+     */
+    public static final int ORIENTATION_ROTATE_90 = 6;
+    /**
+     * Indicates the image is flipped about top-right <--> bottom-left axis, it can also be
+     * represented by flip horizontally firstly and rotate 90 degree clockwise.
+     */
+    public static final int ORIENTATION_TRANSVERSE = 7;
+    /**
+     * Indicates the image is rotated by 270 degree clockwise.
+     */
+    public static final int ORIENTATION_ROTATE_270 = 8;
+    private static final List<Integer> ROTATION_ORDER = Arrays.asList(ORIENTATION_NORMAL,
+            ORIENTATION_ROTATE_90, ORIENTATION_ROTATE_180, ORIENTATION_ROTATE_270);
+    private static final List<Integer> FLIPPED_ROTATION_ORDER = Arrays.asList(
+            ORIENTATION_FLIP_HORIZONTAL, ORIENTATION_TRANSVERSE, ORIENTATION_FLIP_VERTICAL,
+            ORIENTATION_TRANSPOSE);
+
+    /**
+     * The constant used by {@link #TAG_PLANAR_CONFIGURATION} to denote Chunky format.
+     */
+    public static final short FORMAT_CHUNKY = 1;
+    /**
+     * The constant used by {@link #TAG_PLANAR_CONFIGURATION} to denote Planar format.
+     */
+    public static final short FORMAT_PLANAR = 2;
+
+    /**
+     * The constant used by {@link #TAG_Y_CB_CR_POSITIONING} to denote Centered positioning.
+     */
+    public static final short Y_CB_CR_POSITIONING_CENTERED = 1;
+    /**
+     * The constant used by {@link #TAG_Y_CB_CR_POSITIONING} to denote Co-sited positioning.
+     */
+    public static final short Y_CB_CR_POSITIONING_CO_SITED = 2;
+
+    /**
+     * The constant used to denote resolution unit as inches.
+     */
+    public static final short RESOLUTION_UNIT_INCHES = 2;
+    /**
+     * The constant used to denote resolution unit as centimeters.
+     */
+    public static final short RESOLUTION_UNIT_CENTIMETERS = 3;
+
+    /**
+     * The constant used by {@link #TAG_COLOR_SPACE} to denote sRGB color space.
+     */
+    public static final int COLOR_SPACE_S_RGB = 1;
+    /**
+     * The constant used by {@link #TAG_COLOR_SPACE} to denote Uncalibrated.
+     */
+    public static final int COLOR_SPACE_UNCALIBRATED = 65535;
+
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is not defined.
+     */
+    public static final short EXPOSURE_PROGRAM_NOT_DEFINED = 0;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Manual.
+     */
+    public static final short EXPOSURE_PROGRAM_MANUAL = 1;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Normal.
+     */
+    public static final short EXPOSURE_PROGRAM_NORMAL = 2;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is
+     * Aperture priority.
+     */
+    public static final short EXPOSURE_PROGRAM_APERTURE_PRIORITY = 3;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is
+     * Shutter priority.
+     */
+    public static final short EXPOSURE_PROGRAM_SHUTTER_PRIORITY = 4;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Creative
+     * program (biased toward depth of field).
+     */
+    public static final short EXPOSURE_PROGRAM_CREATIVE = 5;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Action
+     * program (biased toward fast shutter speed).
+     */
+    public static final short EXPOSURE_PROGRAM_ACTION = 6;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Portrait
+     * mode (for closeup photos with the background out of focus).
+     */
+    public static final short EXPOSURE_PROGRAM_PORTRAIT_MODE = 7;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Landscape
+     * mode (for landscape photos with the background in focus).
+     */
+    public static final short EXPOSURE_PROGRAM_LANDSCAPE_MODE = 8;
+
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is unknown.
+     */
+    public static final short SENSITIVITY_TYPE_UNKNOWN = 0;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
+     * output sensitivity (SOS).
+     */
+    public static final short SENSITIVITY_TYPE_SOS = 1;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Recommended
+     * exposure index (REI).
+     */
+    public static final short SENSITIVITY_TYPE_REI = 2;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is ISO speed.
+     */
+    public static final short SENSITIVITY_TYPE_ISO_SPEED = 3;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
+     * output sensitivity (SOS) and recommended exposure index (REI).
+     */
+    public static final short SENSITIVITY_TYPE_SOS_AND_REI = 4;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
+     * output sensitivity (SOS) and ISO speed.
+     */
+    public static final short SENSITIVITY_TYPE_SOS_AND_ISO = 5;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Recommended
+     * exposure index (REI) and ISO speed.
+     */
+    public static final short SENSITIVITY_TYPE_REI_AND_ISO = 6;
+    /**
+     * The constant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
+     * output sensitivity (SOS) and recommended exposure index (REI) and ISO speed.
+     */
+    public static final short SENSITIVITY_TYPE_SOS_AND_REI_AND_ISO = 7;
+
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is unknown.
+     */
+    public static final short METERING_MODE_UNKNOWN = 0;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is Average.
+     */
+    public static final short METERING_MODE_AVERAGE = 1;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is
+     * CenterWeightedAverage.
+     */
+    public static final short METERING_MODE_CENTER_WEIGHT_AVERAGE = 2;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is Spot.
+     */
+    public static final short METERING_MODE_SPOT = 3;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is MultiSpot.
+     */
+    public static final short METERING_MODE_MULTI_SPOT = 4;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is Pattern.
+     */
+    public static final short METERING_MODE_PATTERN = 5;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is Partial.
+     */
+    public static final short METERING_MODE_PARTIAL = 6;
+    /**
+     * The constant used by {@link #TAG_METERING_MODE} to denote metering mode is other.
+     */
+    public static final short METERING_MODE_OTHER = 255;
+
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is unknown.
+     */
+    public static final short LIGHT_SOURCE_UNKNOWN = 0;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Daylight.
+     */
+    public static final short LIGHT_SOURCE_DAYLIGHT = 1;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Fluorescent.
+     */
+    public static final short LIGHT_SOURCE_FLUORESCENT = 2;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Tungsten
+     * (incandescent light).
+     */
+    public static final short LIGHT_SOURCE_TUNGSTEN = 3;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Flash.
+     */
+    public static final short LIGHT_SOURCE_FLASH = 4;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Fine weather.
+     */
+    public static final short LIGHT_SOURCE_FINE_WEATHER = 9;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Cloudy weather.
+     */
+    public static final short LIGHT_SOURCE_CLOUDY_WEATHER = 10;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Shade.
+     */
+    public static final short LIGHT_SOURCE_SHADE = 11;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Daylight fluorescent
+     * (D 5700 - 7100K).
+     */
+    public static final short LIGHT_SOURCE_DAYLIGHT_FLUORESCENT = 12;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Day white
+     * fluorescent (N 4600 - 5500K).
+     */
+    public static final short LIGHT_SOURCE_DAY_WHITE_FLUORESCENT = 13;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Cool white
+     * fluorescent (W 3800 - 4500K).
+     */
+    public static final short LIGHT_SOURCE_COOL_WHITE_FLUORESCENT = 14;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is White fluorescent
+     * (WW 3250 - 3800K).
+     */
+    public static final short LIGHT_SOURCE_WHITE_FLUORESCENT = 15;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Warm white
+     * fluorescent (L 2600 - 3250K).
+     */
+    public static final short LIGHT_SOURCE_WARM_WHITE_FLUORESCENT = 16;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light A.
+     */
+    public static final short LIGHT_SOURCE_STANDARD_LIGHT_A = 17;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light B.
+     */
+    public static final short LIGHT_SOURCE_STANDARD_LIGHT_B = 18;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light C.
+     */
+    public static final short LIGHT_SOURCE_STANDARD_LIGHT_C = 19;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D55.
+     */
+    public static final short LIGHT_SOURCE_D55 = 20;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D65.
+     */
+    public static final short LIGHT_SOURCE_D65 = 21;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D75.
+     */
+    public static final short LIGHT_SOURCE_D75 = 22;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D50.
+     */
+    public static final short LIGHT_SOURCE_D50 = 23;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is ISO studio tungsten.
+     */
+    public static final short LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN = 24;
+    /**
+     * The constant used by {@link #TAG_LIGHT_SOURCE} to denote light source is other.
+     */
+    public static final short LIGHT_SOURCE_OTHER = 255;
+
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate whether the flash is fired.
+     */
+    public static final short FLAG_FLASH_FIRED = 0b0000_0001;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate strobe return light is not detected.
+     */
+    public static final short FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED = 0b0000_0100;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate strobe return light is detected.
+     */
+    public static final short FLAG_FLASH_RETURN_LIGHT_DETECTED = 0b0000_0110;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Compulsory flash
+     * firing.
+     *
+     * @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
+     * @see #FLAG_FLASH_MODE_AUTO
+     */
+    public static final short FLAG_FLASH_MODE_COMPULSORY_FIRING = 0b0000_1000;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Compulsory flash
+     * suppression.
+     *
+     * @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
+     * @see #FLAG_FLASH_MODE_AUTO
+     */
+    public static final short FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION = 0b0001_0000;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Auto.
+     *
+     * @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
+     * @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
+     */
+    public static final short FLAG_FLASH_MODE_AUTO = 0b0001_1000;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate no flash function is present.
+     */
+    public static final short FLAG_FLASH_NO_FLASH_FUNCTION = 0b0010_0000;
+    /**
+     * The flag used by {@link #TAG_FLASH} to indicate red-eye reduction is supported.
+     */
+    public static final short FLAG_FLASH_RED_EYE_SUPPORTED = 0b0100_0000;
+
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is not
+     * defined.
+     */
+    public static final short SENSOR_TYPE_NOT_DEFINED = 1;
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is One-chip
+     * color area sensor.
+     */
+    public static final short SENSOR_TYPE_ONE_CHIP = 2;
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Two-chip
+     * color area sensor.
+     */
+    public static final short SENSOR_TYPE_TWO_CHIP = 3;
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is
+     * Three-chip color area sensor.
+     */
+    public static final short SENSOR_TYPE_THREE_CHIP = 4;
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Color
+     * sequential area sensor.
+     */
+    public static final short SENSOR_TYPE_COLOR_SEQUENTIAL = 5;
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Trilinear
+     * sensor.
+     */
+    public static final short SENSOR_TYPE_TRILINEAR = 7;
+    /**
+     * The constant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Color
+     * sequential linear sensor.
+     */
+    public static final short SENSOR_TYPE_COLOR_SEQUENTIAL_LINEAR = 8;
+
+    /**
+     * The constant used by {@link #TAG_FILE_SOURCE} to denote the source is other.
+     */
+    public static final short FILE_SOURCE_OTHER = 0;
+    /**
+     * The constant used by {@link #TAG_FILE_SOURCE} to denote the source is scanner of transparent
+     * type.
+     */
+    public static final short FILE_SOURCE_TRANSPARENT_SCANNER = 1;
+    /**
+     * The constant used by {@link #TAG_FILE_SOURCE} to denote the source is scanner of reflex type.
+     */
+    public static final short FILE_SOURCE_REFLEX_SCANNER = 2;
+    /**
+     * The constant used by {@link #TAG_FILE_SOURCE} to denote the source is DSC.
+     */
+    public static final short FILE_SOURCE_DSC = 3;
+
+    /**
+     * The constant used by {@link #TAG_SCENE_TYPE} to denote the scene is directly photographed.
+     */
+    public static final short SCENE_TYPE_DIRECTLY_PHOTOGRAPHED = 1;
+
+    /**
+     * The constant used by {@link #TAG_CUSTOM_RENDERED} to denote no special processing is used.
+     */
+    public static final short RENDERED_PROCESS_NORMAL = 0;
+    /**
+     * The constant used by {@link #TAG_CUSTOM_RENDERED} to denote special processing is used.
+     */
+    public static final short RENDERED_PROCESS_CUSTOM = 1;
+
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Auto.
+     */
+    public static final short EXPOSURE_MODE_AUTO = 0;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Manual.
+     */
+    public static final short EXPOSURE_MODE_MANUAL = 1;
+    /**
+     * The constant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Auto bracket.
+     */
+    public static final short EXPOSURE_MODE_AUTO_BRACKET = 2;
+
+    /**
+     * The constant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Auto.
+     *
+     * @deprecated Use {@link #WHITE_BALANCE_AUTO} instead.
+     */
+    @Deprecated public static final int WHITEBALANCE_AUTO = 0;
+    /**
+     * The constant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Manual.
+     *
+     * @deprecated Use {@link #WHITE_BALANCE_MANUAL} instead.
+     */
+    @Deprecated public static final int WHITEBALANCE_MANUAL = 1;
+    /**
+     * The constant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Auto.
+     */
+    public static final short WHITE_BALANCE_AUTO = 0;
+    /**
+     * The constant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Manual.
+     */
+    public static final short WHITE_BALANCE_MANUAL = 1;
+
+    /**
+     * The constant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
+     * Standard.
+     */
+    public static final short SCENE_CAPTURE_TYPE_STANDARD = 0;
+    /**
+     * The constant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
+     * Landscape.
+     */
+    public static final short SCENE_CAPTURE_TYPE_LANDSCAPE = 1;
+    /**
+     * The constant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
+     * Portrait.
+     */
+    public static final short SCENE_CAPTURE_TYPE_PORTRAIT = 2;
+    /**
+     * The constant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
+     * Night scene.
+     */
+    public static final short SCENE_CAPTURE_TYPE_NIGHT = 3;
+
+    /**
+     * The constant used by {@link #TAG_GAIN_CONTROL} to denote none gain adjustment.
+     */
+    public static final short GAIN_CONTROL_NONE = 0;
+    /**
+     * The constant used by {@link #TAG_GAIN_CONTROL} to denote low gain up.
+     */
+    public static final short GAIN_CONTROL_LOW_GAIN_UP = 1;
+    /**
+     * The constant used by {@link #TAG_GAIN_CONTROL} to denote high gain up.
+     */
+    public static final short GAIN_CONTROL_HIGH_GAIN_UP = 2;
+    /**
+     * The constant used by {@link #TAG_GAIN_CONTROL} to denote low gain down.
+     */
+    public static final short GAIN_CONTROL_LOW_GAIN_DOWN = 3;
+    /**
+     * The constant used by {@link #TAG_GAIN_CONTROL} to denote high gain down.
+     */
+    public static final short GAIN_CONTROL_HIGH_GAIN_DOWN = 4;
+
+    /**
+     * The constant used by {@link #TAG_CONTRAST} to denote normal contrast.
+     */
+    public static final short CONTRAST_NORMAL = 0;
+    /**
+     * The constant used by {@link #TAG_CONTRAST} to denote soft contrast.
+     */
+    public static final short CONTRAST_SOFT = 1;
+    /**
+     * The constant used by {@link #TAG_CONTRAST} to denote hard contrast.
+     */
+    public static final short CONTRAST_HARD = 2;
+
+    /**
+     * The constant used by {@link #TAG_SATURATION} to denote normal saturation.
+     */
+    public static final short SATURATION_NORMAL = 0;
+    /**
+     * The constant used by {@link #TAG_SATURATION} to denote low saturation.
+     */
+    public static final short SATURATION_LOW = 0;
+    /**
+     * The constant used by {@link #TAG_SHARPNESS} to denote high saturation.
+     */
+    public static final short SATURATION_HIGH = 0;
+
+    /**
+     * The constant used by {@link #TAG_SHARPNESS} to denote normal sharpness.
+     */
+    public static final short SHARPNESS_NORMAL = 0;
+    /**
+     * The constant used by {@link #TAG_SHARPNESS} to denote soft sharpness.
+     */
+    public static final short SHARPNESS_SOFT = 1;
+    /**
+     * The constant used by {@link #TAG_SHARPNESS} to denote hard sharpness.
+     */
+    public static final short SHARPNESS_HARD = 2;
+
+    /**
+     * The constant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
+     * is unknown.
+     */
+    public static final short SUBJECT_DISTANCE_RANGE_UNKNOWN = 0;
+    /**
+     * The constant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
+     * is Macro.
+     */
+    public static final short SUBJECT_DISTANCE_RANGE_MACRO = 1;
+    /**
+     * The constant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
+     * is Close view.
+     */
+    public static final short SUBJECT_DISTANCE_RANGE_CLOSE_VIEW = 2;
+    /**
+     * The constant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
+     * is Distant view.
+     */
+    public static final short SUBJECT_DISTANCE_RANGE_DISTANT_VIEW = 3;
+
+    /**
+     * The constant used by GPS latitude-related tags to denote the latitude is North latitude.
+     *
+     * @see #TAG_GPS_LATITUDE_REF
+     * @see #TAG_GPS_DEST_LATITUDE_REF
+     */
+    public static final String LATITUDE_NORTH = "N";
+    /**
+     * The constant used by GPS latitude-related tags to denote the latitude is South latitude.
+     *
+     * @see #TAG_GPS_LATITUDE_REF
+     * @see #TAG_GPS_DEST_LATITUDE_REF
+     */
+    public static final String LATITUDE_SOUTH = "S";
+
+    /**
+     * The constant used by GPS longitude-related tags to denote the longitude is East longitude.
+     *
+     * @see #TAG_GPS_LONGITUDE_REF
+     * @see #TAG_GPS_DEST_LONGITUDE_REF
+     */
+    public static final String LONGITUDE_EAST = "E";
+    /**
+     * The constant used by GPS longitude-related tags to denote the longitude is West longitude.
+     *
+     * @see #TAG_GPS_LONGITUDE_REF
+     * @see #TAG_GPS_DEST_LONGITUDE_REF
+     */
+    public static final String LONGITUDE_WEST = "W";
+
+    /**
+     * The constant used by {@link #TAG_GPS_ALTITUDE_REF} to denote the altitude is above sea level.
+     */
+    public static final short ALTITUDE_ABOVE_SEA_LEVEL = 0;
+    /**
+     * The constant used by {@link #TAG_GPS_ALTITUDE_REF} to denote the altitude is below sea level.
+     */
+    public static final short ALTITUDE_BELOW_SEA_LEVEL = 1;
+
+    /**
+     * The constant used by {@link #TAG_GPS_STATUS} to denote GPS measurement is in progress.
+     */
+    public static final String GPS_MEASUREMENT_IN_PROGRESS = "A";
+    /**
+     * The constant used by {@link #TAG_GPS_STATUS} to denote GPS measurement is interrupted.
+     */
+    public static final String GPS_MEASUREMENT_INTERRUPTED = "V";
+
+    /**
+     * The constant used by {@link #TAG_GPS_MEASURE_MODE} to denote GPS measurement is
+     * 2-dimensional.
+     */
+    public static final String GPS_MEASUREMENT_2D = "2";
+    /**
+     * The constant used by {@link #TAG_GPS_MEASURE_MODE} to denote GPS measurement is
+     * 3-dimensional.
+     */
+    public static final String GPS_MEASUREMENT_3D = "3";
+
+    /**
+     * The constant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is kilometers per
+     * hour.
+     */
+    public static final String GPS_SPEED_KILOMETERS_PER_HOUR = "K";
+    /**
+     * The constant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is miles per hour.
+     */
+    public static final String GPS_SPEED_MILES_PER_HOUR = "M";
+    /**
+     * The constant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is knots.
+     */
+    public static final String GPS_SPEED_KNOTS = "N";
+
+    /**
+     * The constant used by GPS attributes to denote the direction is true direction.
+     */
+    public static final String GPS_DIRECTION_TRUE = "T";
+    /**
+     * The constant used by GPS attributes to denote the direction is magnetic direction.
+     */
+    public static final String GPS_DIRECTION_MAGNETIC = "M";
+
+    /**
+     * The constant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is
+     * kilometers.
+     */
+    public static final String GPS_DISTANCE_KILOMETERS = "K";
+    /**
+     * The constant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is miles.
+     */
+    public static final String GPS_DISTANCE_MILES = "M";
+    /**
+     * The constant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is
+     * nautical miles.
+     */
+    public static final String GPS_DISTANCE_NAUTICAL_MILES = "N";
+
+    /**
+     * The constant used by {@link #TAG_GPS_DIFFERENTIAL} to denote no differential correction is
+     * applied.
+     */
+    public static final short GPS_MEASUREMENT_NO_DIFFERENTIAL = 0;
+    /**
+     * The constant used by {@link #TAG_GPS_DIFFERENTIAL} to denote differential correction is
+     * applied.
+     */
+    public static final short GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED = 1;
+
+    /**
+     * The constant used by {@link #TAG_COMPRESSION} to denote the image is not compressed.
+     */
+    public static final int DATA_UNCOMPRESSED = 1;
+    /**
+     * The constant used by {@link #TAG_COMPRESSION} to denote the image is huffman compressed.
+     */
+    public static final int DATA_HUFFMAN_COMPRESSED = 2;
+    /**
+     * The constant used by {@link #TAG_COMPRESSION} to denote the image is JPEG.
+     */
+    public static final int DATA_JPEG = 6;
+    /**
+     * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
+     * Section 3, Compression
+     */
+    public static final int DATA_JPEG_COMPRESSED = 7;
+    /**
+     * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
+     * Section 3, Compression
+     */
+    public static final int DATA_DEFLATE_ZIP = 8;
+    /**
+     * The constant used by {@link #TAG_COMPRESSION} to denote the image is pack-bits compressed.
+     */
+    public static final int DATA_PACK_BITS_COMPRESSED = 32773;
+    /**
+     * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
+     * Section 3, Compression
+     */
+    public static final int DATA_LOSSY_JPEG = 34892;
+
+    /**
+     * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
+     * See JEITA CP-3451C Spec Section 6, Differences from Palette Color Images
+     */
+    public static final int[] BITS_PER_SAMPLE_RGB = new int[] { 8, 8, 8 };
+    /**
+     * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
+     * See JEITA CP-3451C Spec Section 4, Differences from Bilevel Images
+     */
+    public static final int[] BITS_PER_SAMPLE_GREYSCALE_1 = new int[] { 4 };
+    /**
+     * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
+     * See JEITA CP-3451C Spec Section 4, Differences from Bilevel Images
+     */
+    public static final int[] BITS_PER_SAMPLE_GREYSCALE_2 = new int[] { 8 };
+
+    /**
+     * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
+     */
+    public static final int PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO = 0;
+    /**
+     * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
+     */
+    public static final int PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1;
+    /**
+     * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
+     */
+    public static final int PHOTOMETRIC_INTERPRETATION_RGB = 2;
+    /**
+     * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
+     */
+    public static final int PHOTOMETRIC_INTERPRETATION_YCBCR = 6;
+
+    /**
+     * The constant used by {@link #TAG_NEW_SUBFILE_TYPE}. See JEITA CP-3451C Spec Section 8.
+     */
+    public static final int ORIGINAL_RESOLUTION_IMAGE = 0;
+    /**
+     * The constant used by {@link #TAG_NEW_SUBFILE_TYPE}. See JEITA CP-3451C Spec Section 8.
+     */
+    public static final int REDUCED_RESOLUTION_IMAGE = 1;
+
+    /**
+     * Constant used to indicate that the input stream contains the full image data.
+     * <p>
+     * The format of the image data should follow one of the image formats supported by this class.
+     */
+    public static final int STREAM_TYPE_FULL_IMAGE_DATA = 0;
+    /**
+     * Constant used to indicate that the input stream contains only Exif data.
+     * <p>
+     * The format of the Exif-only data must follow the below structure:
+     *     Exif Identifier Code ("Exif\0\0") + TIFF header + IFD data
+     * See JEITA CP-3451C Section 4.5.2 and 4.5.4 specifications for more details.
+     */
+    public static final int STREAM_TYPE_EXIF_DATA_ONLY = 1;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({STREAM_TYPE_FULL_IMAGE_DATA, STREAM_TYPE_EXIF_DATA_ONLY})
+    public @interface ExifStreamType {}
+
+    // Maximum size for checking file type signature (see image_type_recognition_lite.cc)
+    private static final int SIGNATURE_CHECK_SIZE = 5000;
+
+    static final byte[] JPEG_SIGNATURE = new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff};
+    private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW";
+    private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84;
+
+    private static final byte[] HEIF_TYPE_FTYP = new byte[] {'f', 't', 'y', 'p'};
+    private static final byte[] HEIF_BRAND_MIF1 = new byte[] {'m', 'i', 'f', '1'};
+    private static final byte[] HEIF_BRAND_HEIC = new byte[] {'h', 'e', 'i', 'c'};
+
+    // See http://fileformats.archiveteam.org/wiki/Olympus_ORF
+    private static final short ORF_SIGNATURE_1 = 0x4f52;
+    private static final short ORF_SIGNATURE_2 = 0x5352;
+    // There are two formats for Olympus Makernote Headers. Each has different identifiers and
+    // offsets to the actual data.
+    // See http://www.exiv2.org/makernote.html#R1
+    private static final byte[] ORF_MAKER_NOTE_HEADER_1 = new byte[] {(byte) 0x4f, (byte) 0x4c,
+            (byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x00}; // "OLYMP\0"
+    private static final byte[] ORF_MAKER_NOTE_HEADER_2 = new byte[] {(byte) 0x4f, (byte) 0x4c,
+            (byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x55, (byte) 0x53, (byte) 0x00,
+            (byte) 0x49, (byte) 0x49}; // "OLYMPUS\0II"
+    private static final int ORF_MAKER_NOTE_HEADER_1_SIZE = 8;
+    private static final int ORF_MAKER_NOTE_HEADER_2_SIZE = 12;
+
+    // See http://fileformats.archiveteam.org/wiki/RW2
+    private static final short RW2_SIGNATURE = 0x0055;
+
+    // See http://fileformats.archiveteam.org/wiki/Pentax_PEF
+    private static final String PEF_SIGNATURE = "PENTAX";
+    // See http://www.exiv2.org/makernote.html#R11
+    private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6;
+
+    // See PNG (Portable Network Graphics) Specification, Version 1.2,
+    // 3.1. PNG file signature
+    private static final byte[] PNG_SIGNATURE = new byte[] {(byte) 0x89, (byte) 0x50, (byte) 0x4e,
+            (byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a};
+    // See "Extensions to the PNG 1.2 Specification, Version 1.5.0",
+    // 3.7. eXIf Exchangeable Image File (Exif) Profile
+    private static final byte[] PNG_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x65, (byte) 0x58,
+            (byte) 0x49, (byte) 0x66};
+    private static final byte[] PNG_CHUNK_TYPE_IHDR = new byte[]{(byte) 0x49, (byte) 0x48,
+            (byte) 0x44, (byte) 0x52};
+    private static final byte[] PNG_CHUNK_TYPE_IEND = new byte[]{(byte) 0x49, (byte) 0x45,
+            (byte) 0x4e, (byte) 0x44};
+    private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
+    private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
+
+    // See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
+    private static final byte[] WEBP_SIGNATURE_1 = new byte[] {'R', 'I', 'F', 'F'};
+    private static final byte[] WEBP_SIGNATURE_2 = new byte[] {'W', 'E', 'B', 'P'};
+    private static final int WEBP_FILE_SIZE_BYTE_LENGTH = 4;
+    private static final byte[] WEBP_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x45, (byte) 0x58,
+            (byte) 0x49, (byte) 0x46};
+    private static final byte[] WEBP_VP8_SIGNATURE = new byte[]{(byte) 0x9d, (byte) 0x01,
+            (byte) 0x2a};
+    private static final byte WEBP_VP8L_SIGNATURE = (byte) 0x2f;
+    private static final byte[] WEBP_CHUNK_TYPE_VP8X = "VP8X".getBytes(Charset.defaultCharset());
+    private static final byte[] WEBP_CHUNK_TYPE_VP8L = "VP8L".getBytes(Charset.defaultCharset());
+    private static final byte[] WEBP_CHUNK_TYPE_VP8 = "VP8 ".getBytes(Charset.defaultCharset());
+    private static final byte[] WEBP_CHUNK_TYPE_ANIM = "ANIM".getBytes(Charset.defaultCharset());
+    private static final byte[] WEBP_CHUNK_TYPE_ANMF = "ANMF".getBytes(Charset.defaultCharset());
+    private static final int WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH = 10;
+    private static final int WEBP_CHUNK_TYPE_BYTE_LENGTH = 4;
+    private static final int WEBP_CHUNK_SIZE_BYTE_LENGTH = 4;
+
+    private static SimpleDateFormat sFormatterPrimary;
+    private static SimpleDateFormat sFormatterSecondary;
+
+    // See Exchangeable image file format for digital still cameras: Exif version 2.2.
+    // The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
+    // They are called "Image File Directory". They have multiple data formats to cover various
+    // image metadata from GPS longitude to camera model name.
+
+    // Types of Exif byte alignments (see JEITA CP-3451C Section 4.5.2)
+    static final short BYTE_ALIGN_II = 0x4949;  // II: Intel order
+    static final short BYTE_ALIGN_MM = 0x4d4d;  // MM: Motorola order
+
+    // TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2)
+    static final byte START_CODE = 0x2a; // 42
+    private static final int IFD_OFFSET = 8;
+
+    // Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
+    private static final int IFD_FORMAT_BYTE = 1;
+    private static final int IFD_FORMAT_STRING = 2;
+    private static final int IFD_FORMAT_USHORT = 3;
+    private static final int IFD_FORMAT_ULONG = 4;
+    private static final int IFD_FORMAT_URATIONAL = 5;
+    private static final int IFD_FORMAT_SBYTE = 6;
+    private static final int IFD_FORMAT_UNDEFINED = 7;
+    private static final int IFD_FORMAT_SSHORT = 8;
+    private static final int IFD_FORMAT_SLONG = 9;
+    private static final int IFD_FORMAT_SRATIONAL = 10;
+    private static final int IFD_FORMAT_SINGLE = 11;
+    private static final int IFD_FORMAT_DOUBLE = 12;
+    // Format indicating a new IFD entry (See Adobe PageMaker® 6.0 TIFF Technical Notes, "New Tag")
+    private static final int IFD_FORMAT_IFD = 13;
+
+    private static final int SKIP_BUFFER_SIZE = 8192;
+
+    // Names for the data formats for debugging purpose.
+    static final String[] IFD_FORMAT_NAMES = new String[] {
+            "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
+            "SLONG", "SRATIONAL", "SINGLE", "DOUBLE", "IFD"
+    };
+    // Sizes of the components of each IFD value format
+    static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
+            0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
+    };
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static final byte[] EXIF_ASCII_PREFIX = new byte[] {
+            0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
+    };
+
+    // A class for indicating EXIF rational type.
+    private static class Rational {
+        public final long numerator;
+        public final long denominator;
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        Rational(double value) {
+            this((long) (value * 10000), 10000);
+        }
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        Rational(long numerator, long denominator) {
+            // Handle erroneous case
+            if (denominator == 0) {
+                this.numerator = 0;
+                this.denominator = 1;
+                return;
+            }
+            this.numerator = numerator;
+            this.denominator = denominator;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return numerator + "/" + denominator;
+        }
+
+        public double calculate() {
+            return (double) numerator / denominator;
+        }
+    }
+
+    // A class for indicating EXIF attribute.
+    private static class ExifAttribute {
+        public static final long BYTES_OFFSET_UNKNOWN = -1;
+
+        public final int format;
+        public final int numberOfComponents;
+        public final long bytesOffset;
+        public final byte[] bytes;
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
+            this(format, numberOfComponents, BYTES_OFFSET_UNKNOWN, bytes);
+        }
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        ExifAttribute(int format, int numberOfComponents, long bytesOffset, byte[] bytes) {
+            this.format = format;
+            this.numberOfComponents = numberOfComponents;
+            this.bytesOffset = bytesOffset;
+            this.bytes = bytes;
+        }
+
+        public static ExifAttribute createUShort(int[] values, ByteOrder byteOrder) {
+            final ByteBuffer buffer = ByteBuffer.wrap(
+                    new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]);
+            buffer.order(byteOrder);
+            for (int value : values) {
+                buffer.putShort((short) value);
+            }
+            return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array());
+        }
+
+        public static ExifAttribute createUShort(int value, ByteOrder byteOrder) {
+            return createUShort(new int[] {value}, byteOrder);
+        }
+
+        public static ExifAttribute createULong(long[] values, ByteOrder byteOrder) {
+            final ByteBuffer buffer = ByteBuffer.wrap(
+                    new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]);
+            buffer.order(byteOrder);
+            for (long value : values) {
+                buffer.putInt((int) value);
+            }
+            return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array());
+        }
+
+        public static ExifAttribute createULong(long value, ByteOrder byteOrder) {
+            return createULong(new long[] {value}, byteOrder);
+        }
+
+        public static ExifAttribute createSLong(int[] values, ByteOrder byteOrder) {
+            final ByteBuffer buffer = ByteBuffer.wrap(
+                    new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]);
+            buffer.order(byteOrder);
+            for (int value : values) {
+                buffer.putInt(value);
+            }
+            return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array());
+        }
+
+        public static ExifAttribute createByte(String value) {
+            // Exception for GPSAltitudeRef tag
+            if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') {
+                final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') };
+                return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes);
+            }
+            final byte[] ascii = value.getBytes(ASCII);
+            return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii);
+        }
+
+        public static ExifAttribute createString(String value) {
+            final byte[] ascii = (value + '\0').getBytes(ASCII);
+            return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii);
+        }
+
+        public static ExifAttribute createURational(Rational[] values, ByteOrder byteOrder) {
+            final ByteBuffer buffer = ByteBuffer.wrap(
+                    new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]);
+            buffer.order(byteOrder);
+            for (Rational value : values) {
+                buffer.putInt((int) value.numerator);
+                buffer.putInt((int) value.denominator);
+            }
+            return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array());
+        }
+
+        public static ExifAttribute createURational(Rational value, ByteOrder byteOrder) {
+            return createURational(new Rational[] {value}, byteOrder);
+        }
+
+        public static ExifAttribute createSRational(Rational[] values, ByteOrder byteOrder) {
+            final ByteBuffer buffer = ByteBuffer.wrap(
+                    new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]);
+            buffer.order(byteOrder);
+            for (Rational value : values) {
+                buffer.putInt((int) value.numerator);
+                buffer.putInt((int) value.denominator);
+            }
+            return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array());
+        }
+
+        public static ExifAttribute createDouble(double[] values, ByteOrder byteOrder) {
+            final ByteBuffer buffer = ByteBuffer.wrap(
+                    new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]);
+            buffer.order(byteOrder);
+            for (double value : values) {
+                buffer.putDouble(value);
+            }
+            return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
+        }
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        Object getValue(ByteOrder byteOrder) {
+            ByteOrderedDataInputStream inputStream = null;
+            try {
+                inputStream = new ByteOrderedDataInputStream(bytes);
+                inputStream.setByteOrder(byteOrder);
+                switch (format) {
+                    case IFD_FORMAT_BYTE:
+                    case IFD_FORMAT_SBYTE: {
+                        // Exception for GPSAltitudeRef tag
+                        if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) {
+                            return new String(new char[] { (char) (bytes[0] + '0') });
+                        }
+                        return new String(bytes, ASCII);
+                    }
+                    case IFD_FORMAT_UNDEFINED:
+                    case IFD_FORMAT_STRING: {
+                        int index = 0;
+                        if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
+                            boolean same = true;
+                            for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
+                                if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
+                                    same = false;
+                                    break;
+                                }
+                            }
+                            if (same) {
+                                index = EXIF_ASCII_PREFIX.length;
+                            }
+                        }
+
+                        StringBuilder stringBuilder = new StringBuilder();
+                        while (index < numberOfComponents) {
+                            int ch = bytes[index];
+                            if (ch == 0) {
+                                break;
+                            }
+                            if (ch >= 32) {
+                                stringBuilder.append((char) ch);
+                            } else {
+                                stringBuilder.append('?');
+                            }
+                            ++index;
+                        }
+                        return stringBuilder.toString();
+                    }
+                    case IFD_FORMAT_USHORT: {
+                        final int[] values = new int[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            values[i] = inputStream.readUnsignedShort();
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_ULONG: {
+                        final long[] values = new long[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            values[i] = inputStream.readUnsignedInt();
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_URATIONAL: {
+                        final Rational[] values = new Rational[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            final long numerator = inputStream.readUnsignedInt();
+                            final long denominator = inputStream.readUnsignedInt();
+                            values[i] = new Rational(numerator, denominator);
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_SSHORT: {
+                        final int[] values = new int[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            values[i] = inputStream.readShort();
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_SLONG: {
+                        final int[] values = new int[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            values[i] = inputStream.readInt();
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_SRATIONAL: {
+                        final Rational[] values = new Rational[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            final long numerator = inputStream.readInt();
+                            final long denominator = inputStream.readInt();
+                            values[i] = new Rational(numerator, denominator);
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_SINGLE: {
+                        final double[] values = new double[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            values[i] = inputStream.readFloat();
+                        }
+                        return values;
+                    }
+                    case IFD_FORMAT_DOUBLE: {
+                        final double[] values = new double[numberOfComponents];
+                        for (int i = 0; i < numberOfComponents; ++i) {
+                            values[i] = inputStream.readDouble();
+                        }
+                        return values;
+                    }
+                    default:
+                        return null;
+                }
+            } catch (IOException e) {
+                logger.warn("IOException occurred during reading a value", e);
+                return null;
+            } finally {
+                if (inputStream != null) {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) {
+                        logger.error("IOException occurred while closing InputStream", e);
+                    }
+                }
+            }
+        }
+
+        public double getDoubleValue(ByteOrder byteOrder) {
+            Object value = getValue(byteOrder);
+            if (value == null) {
+                throw new NumberFormatException("NULL can't be converted to a double value");
+            }
+            if (value instanceof String) {
+                return Double.parseDouble((String) value);
+            }
+            if (value instanceof long[]) {
+                long[] array = (long[]) value;
+                if (array.length == 1) {
+                    return array[0];
+                }
+                throw new NumberFormatException("There are more than one component");
+            }
+            if (value instanceof int[]) {
+                int[] array = (int[]) value;
+                if (array.length == 1) {
+                    return array[0];
+                }
+                throw new NumberFormatException("There are more than one component");
+            }
+            if (value instanceof double[]) {
+                double[] array = (double[]) value;
+                if (array.length == 1) {
+                    return array[0];
+                }
+                throw new NumberFormatException("There are more than one component");
+            }
+            if (value instanceof Rational[]) {
+                Rational[] array = (Rational[]) value;
+                if (array.length == 1) {
+                    return array[0].calculate();
+                }
+                throw new NumberFormatException("There are more than one component");
+            }
+            throw new NumberFormatException("Couldn't find a double value");
+        }
+
+        public int getIntValue(ByteOrder byteOrder) {
+            Object value = getValue(byteOrder);
+            if (value == null) {
+                throw new NumberFormatException("NULL can't be converted to a integer value");
+            }
+            if (value instanceof String) {
+                return Integer.parseInt((String) value);
+            }
+            if (value instanceof long[]) {
+                long[] array = (long[]) value;
+                if (array.length == 1) {
+                    return (int) array[0];
+                }
+                throw new NumberFormatException("There are more than one component");
+            }
+            if (value instanceof int[]) {
+                int[] array = (int[]) value;
+                if (array.length == 1) {
+                    return array[0];
+                }
+                throw new NumberFormatException("There are more than one component");
+            }
+            throw new NumberFormatException("Couldn't find a integer value");
+        }
+
+		// Threema-added
+		public String getUTF8StringValue() {
+			if (bytes != null && bytes.length > 0) {
+				return new String(bytes, StandardCharsets.UTF_8);
+			}
+			return null;
+		}
 
-	// C. Tags related to Image Data Characteristics
-	/**
-	 *  <p>A transfer function for the image, described in tabular style. Normally this tag need not
-	 *  be used, since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 301</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 3 * 256</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_TRANSFER_FUNCTION = "TransferFunction";
-	/**
-	 *  <p>The chromaticity of the white point of the image. Normally this tag need not be used,
-	 *  since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 318</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 2</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_WHITE_POINT = "WhitePoint";
-	/**
-	 *  <p>The chromaticity of the three primary colors of the image. Normally this tag need not
-	 *  be used, since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 319</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 6</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities";
-	/**
-	 *  <p>The matrix coefficients for transformation from RGB to YCbCr image data. About
-	 *  the default value, please refer to JEITA CP-3451C Spec, Annex D.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 529</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 3</li>
-	 *  </ul>
-	 */
-	public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients";
-	/**
-	 *  <p>The reference black point value and reference white point value. No defaults are given
-	 *  in TIFF, but the values below are given as defaults here. The color space is declared in
-	 *  a color space information tag, with the default being the value that gives the optimal image
-	 *  characteristics Interoperability these conditions</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 532</li>
-	 *      <li>Type = RATIONAL</li>
-	 *      <li>Count = 6</li>
-	 *      <li>Default = [0, 255, 0, 255, 0, 255] (when {@link #TAG_PHOTOMETRIC_INTERPRETATION}
-	 *                 is {@link #PHOTOMETRIC_INTERPRETATION_RGB})
-	 *                 or [0, 255, 0, 128, 0, 128] (when {@link #TAG_PHOTOMETRIC_INTERPRETATION}
-	 *                 is {@link #PHOTOMETRIC_INTERPRETATION_YCBCR})</li>
-	 *  </ul>
+        public String getStringValue(ByteOrder byteOrder) {
+            Object value = getValue(byteOrder);
+            if (value == null) {
+                return null;
+            }
+            if (value instanceof String) {
+                return (String) value;
+            }
+
+            final StringBuilder stringBuilder = new StringBuilder();
+            if (value instanceof long[]) {
+                long[] array = (long[]) value;
+                for (int i = 0; i < array.length; ++i) {
+                    stringBuilder.append(array[i]);
+                    if (i + 1 != array.length) {
+                        stringBuilder.append(",");
+                    }
+                }
+                return stringBuilder.toString();
+            }
+            if (value instanceof int[]) {
+                int[] array = (int[]) value;
+                for (int i = 0; i < array.length; ++i) {
+                    stringBuilder.append(array[i]);
+                    if (i + 1 != array.length) {
+                        stringBuilder.append(",");
+                    }
+                }
+                return stringBuilder.toString();
+            }
+            if (value instanceof double[]) {
+                double[] array = (double[]) value;
+                for (int i = 0; i < array.length; ++i) {
+                    stringBuilder.append(array[i]);
+                    if (i + 1 != array.length) {
+                        stringBuilder.append(",");
+                    }
+                }
+                return stringBuilder.toString();
+            }
+            if (value instanceof Rational[]) {
+                Rational[] array = (Rational[]) value;
+                for (int i = 0; i < array.length; ++i) {
+                    stringBuilder.append(array[i].numerator);
+                    stringBuilder.append('/');
+                    stringBuilder.append(array[i].denominator);
+                    if (i + 1 != array.length) {
+                        stringBuilder.append(",");
+                    }
+                }
+                return stringBuilder.toString();
+            }
+            return null;
+        }
+
+        public int size() {
+            return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents;
+        }
+    }
+
+    // A class for indicating EXIF tag.
+    static class ExifTag {
+        public final int number;
+        public final String name;
+        public final int primaryFormat;
+        public final int secondaryFormat;
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        ExifTag(String name, int number, int format) {
+            this.name = name;
+            this.number = number;
+            this.primaryFormat = format;
+            this.secondaryFormat = -1;
+        }
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        ExifTag(String name, int number, int primaryFormat, int secondaryFormat) {
+            this.name = name;
+            this.number = number;
+            this.primaryFormat = primaryFormat;
+            this.secondaryFormat = secondaryFormat;
+        }
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        boolean isFormatCompatible(int format) {
+            if (primaryFormat == IFD_FORMAT_UNDEFINED || format == IFD_FORMAT_UNDEFINED) {
+                return true;
+            } else if (primaryFormat == format || secondaryFormat == format) {
+                return true;
+            } else if ((primaryFormat == IFD_FORMAT_ULONG || secondaryFormat == IFD_FORMAT_ULONG)
+                    && format == IFD_FORMAT_USHORT) {
+                return true;
+            } else if ((primaryFormat == IFD_FORMAT_SLONG || secondaryFormat == IFD_FORMAT_SLONG)
+                    && format == IFD_FORMAT_SSHORT) {
+                return true;
+            } else if ((primaryFormat == IFD_FORMAT_DOUBLE || secondaryFormat == IFD_FORMAT_DOUBLE)
+                    && format == IFD_FORMAT_SINGLE) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    // Primary image IFD TIFF tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] {
+            // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
+            new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
+            new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
+            new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
+            new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
+            new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
+            new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
+            // See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
+            new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
+            new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+            // RW2 file tags
+            // See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html)
+            new ExifTag(TAG_RW2_SENSOR_TOP_BORDER, 4, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_RW2_SENSOR_LEFT_BORDER, 5, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_RW2_SENSOR_BOTTOM_BORDER, 6, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_RW2_SENSOR_RIGHT_BORDER, 7, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_RW2_ISO, 23, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_RW2_JPG_FROM_RAW, 46, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_XMP, 700, IFD_FORMAT_BYTE),
+    };
+
+    // Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] {
+            new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SPECTRAL_SENSITIVITY, 34852, IFD_FORMAT_STRING),
+            new ExifTag(TAG_PHOTOGRAPHIC_SENSITIVITY, 34855, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_OECF, 34856, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_SENSITIVITY_TYPE, 34864, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_STANDARD_OUTPUT_SENSITIVITY, 34865, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_RECOMMENDED_EXPOSURE_INDEX, 34866, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ISO_SPEED, 34867, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ISO_SPEED_LATITUDE_YYY, 34868, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ISO_SPEED_LATITUDE_ZZZ, 34869, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING),
+            new ExifTag(TAG_OFFSET_TIME, 36880, IFD_FORMAT_STRING),
+            new ExifTag(TAG_OFFSET_TIME_ORIGINAL, 36881, IFD_FORMAT_STRING),
+            new ExifTag(TAG_OFFSET_TIME_DIGITIZED, 36882, IFD_FORMAT_STRING),
+            new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_COMPRESSED_BITS_PER_PIXEL, 37122, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_SUBJECT_DISTANCE, 37382, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_SUBJECT_AREA, 37396, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_MAKER_NOTE, 37500, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_USER_COMMENT, 37510, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
+            new ExifTag(TAG_SUBSEC_TIME_ORIGINAL, 37521, IFD_FORMAT_STRING),
+            new ExifTag(TAG_SUBSEC_TIME_DIGITIZED, 37522, IFD_FORMAT_STRING),
+            new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_RELATED_SOUND_FILE, 40964, IFD_FORMAT_STRING),
+            new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_FLASH_ENERGY, 41483, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_SPATIAL_FREQUENCY_RESPONSE, 41484, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_FOCAL_PLANE_X_RESOLUTION, 41486, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_FOCAL_PLANE_Y_RESOLUTION, 41487, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SUBJECT_LOCATION, 41492, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_EXPOSURE_INDEX, 41493, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_CFA_PATTERN, 41730, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_DIGITAL_ZOOM_RATIO, 41988, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_FOCAL_LENGTH_IN_35MM_FILM, 41989, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_GAIN_CONTROL, 41991, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_DEVICE_SETTING_DESCRIPTION, 41995, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_SUBJECT_DISTANCE_RANGE, 41996, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_IMAGE_UNIQUE_ID, 42016, IFD_FORMAT_STRING),
+            new ExifTag(TAG_CAMERA_OWNER_NAME, 42032, IFD_FORMAT_STRING),
+            new ExifTag(TAG_BODY_SERIAL_NUMBER, 42033, IFD_FORMAT_STRING),
+            new ExifTag(TAG_LENS_SPECIFICATION, 42034, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_LENS_MAKE, 42035, IFD_FORMAT_STRING),
+            new ExifTag(TAG_LENS_MODEL, 42036, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GAMMA, 42240, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
+    };
+
+    // Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.6 Tag Support Levels)
+    private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
+            new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
+            // Allow SRATIONAL to be compatible with apps using wrong format and
+            // even if it is negative, it may be valid latitude / longitude.
+            new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_SATELLITES, 8, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_STATUS, 9, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_MEASURE_MODE, 10, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DOP, 11, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_SPEED, 13, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_TRACK, 15, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_IMG_DIRECTION, 17, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_MAP_DATUM, 18, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_LATITUDE_REF, 19, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_LATITUDE, 20, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_DEST_LONGITUDE_REF, 21, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_LONGITUDE, 22, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_BEARING, 24, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_DISTANCE, 26, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_PROCESSING_METHOD, 27, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_GPS_AREA_INFORMATION, 28, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_GPS_DATESTAMP, 29, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DIFFERENTIAL, 30, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_GPS_H_POSITIONING_ERROR, 31, IFD_FORMAT_URATIONAL)
+    };
+    // Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
+            new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
+    };
+    // IFD Thumbnail tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
+            // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
+            new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
+            new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
+            new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
+            new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_THUMBNAIL_ORIENTATION, 274, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
+            new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
+            new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
+            // See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
+            new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
+            new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
+    };
+
+    // RAF file tag (See piex.cc line 372)
+    private static final ExifTag TAG_RAF_IMAGE_SIZE =
+            new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT);
+
+    // ORF file tags (See http://www.exiv2.org/tags-olympus.html)
+    private static final ExifTag[] ORF_MAKER_NOTE_TAGS = new ExifTag[] {
+            new ExifTag(TAG_ORF_THUMBNAIL_IMAGE, 256, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_ULONG)
+    };
+    private static final ExifTag[] ORF_CAMERA_SETTINGS_TAGS = new ExifTag[] {
+            new ExifTag(TAG_ORF_PREVIEW_IMAGE_START, 257, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ORF_PREVIEW_IMAGE_LENGTH, 258, IFD_FORMAT_ULONG)
+    };
+    private static final ExifTag[] ORF_IMAGE_PROCESSING_TAGS = new ExifTag[] {
+            new ExifTag(TAG_ORF_ASPECT_FRAME, 4371, IFD_FORMAT_USHORT)
+    };
+    // PEF file tag (See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Pentax.html)
+    private static final ExifTag[] PEF_TAGS = new ExifTag[] {
+            new ExifTag(TAG_COLOR_SPACE, 55, IFD_FORMAT_USHORT)
+    };
+
+    // See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
+    // The following values are used for indicating pointers to the other Image File Directories.
+
+    // Indices of Exif Ifd tag groups
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
+            IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
+            IFD_TYPE_ORF_CAMERA_SETTINGS, IFD_TYPE_ORF_IMAGE_PROCESSING, IFD_TYPE_PEF})
+    public @interface IfdType {}
+
+    static final int IFD_TYPE_PRIMARY = 0;
+    private static final int IFD_TYPE_EXIF = 1;
+    private static final int IFD_TYPE_GPS = 2;
+    private static final int IFD_TYPE_INTEROPERABILITY = 3;
+    static final int IFD_TYPE_THUMBNAIL = 4;
+    static final int IFD_TYPE_PREVIEW = 5;
+    private static final int IFD_TYPE_ORF_MAKER_NOTE = 6;
+    private static final int IFD_TYPE_ORF_CAMERA_SETTINGS = 7;
+    private static final int IFD_TYPE_ORF_IMAGE_PROCESSING = 8;
+    private static final int IFD_TYPE_PEF = 9;
+
+    // List of Exif tag groups
+    static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
+            IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
+            IFD_THUMBNAIL_TAGS, IFD_TIFF_TAGS, ORF_MAKER_NOTE_TAGS, ORF_CAMERA_SETTINGS_TAGS,
+            ORF_IMAGE_PROCESSING_TAGS, PEF_TAGS
+    };
+    // List of tags for pointing to the other image file directory offset.
+    private static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[] {
+            new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_BYTE)
+    };
+
+    // Mappings from tag number to tag name and each item represents one IFD tag group.
+    @SuppressWarnings("unchecked")
+    private static final HashMap<Integer, ExifTag>[] sExifTagMapsForReading =
+            new HashMap[EXIF_TAGS.length];
+    // Mappings from tag name to tag number and each item represents one IFD tag group.
+    @SuppressWarnings("unchecked")
+    private static final HashMap<String, ExifTag>[] sExifTagMapsForWriting =
+            new HashMap[EXIF_TAGS.length];
+    private static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
+            TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE,
+            TAG_GPS_TIMESTAMP));
+    // Mappings from tag number to IFD type for pointer tags.
+    private static final HashMap<Integer, Integer> sExifPointerTagMap = new HashMap<>();
+
+    // See JPEG File Interchange Format Version 1.02.
+    // The following values are defined for handling JPEG streams. In this implementation, we are
+    // not only getting information from EXIF but also from some JPEG special segments such as
+    // MARKER_COM for user comment and MARKER_SOFx for image width and height.
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static final Charset ASCII = Charset.forName("US-ASCII");
+    // Identifier for EXIF APP1 segment in JPEG
+    static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(ASCII);
+    // Identifier for XMP APP1 segment in JPEG
+    private static final byte[] IDENTIFIER_XMP_APP1 =
+            "http://ns.adobe.com/xap/1.0/\0".getBytes(ASCII);
+    // JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
+    // the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
+    // of frame(baseline DCT) and the image size info exists in its beginning part.
+    static final byte MARKER = (byte) 0xff;
+    private static final byte MARKER_SOI = (byte) 0xd8;
+    private static final byte MARKER_SOF0 = (byte) 0xc0;
+    private static final byte MARKER_SOF1 = (byte) 0xc1;
+    private static final byte MARKER_SOF2 = (byte) 0xc2;
+    private static final byte MARKER_SOF3 = (byte) 0xc3;
+    private static final byte MARKER_SOF5 = (byte) 0xc5;
+    private static final byte MARKER_SOF6 = (byte) 0xc6;
+    private static final byte MARKER_SOF7 = (byte) 0xc7;
+    private static final byte MARKER_SOF9 = (byte) 0xc9;
+    private static final byte MARKER_SOF10 = (byte) 0xca;
+    private static final byte MARKER_SOF11 = (byte) 0xcb;
+    private static final byte MARKER_SOF13 = (byte) 0xcd;
+    private static final byte MARKER_SOF14 = (byte) 0xce;
+    private static final byte MARKER_SOF15 = (byte) 0xcf;
+    private static final byte MARKER_SOS = (byte) 0xda;
+    static final byte MARKER_APP1 = (byte) 0xe1;
+    private static final byte MARKER_COM = (byte) 0xfe;
+    static final byte MARKER_EOI = (byte) 0xd9;
+
+    // Supported Image File Types
+    static final int IMAGE_TYPE_UNKNOWN = 0;
+    static final int IMAGE_TYPE_ARW = 1;
+    static final int IMAGE_TYPE_CR2 = 2;
+    static final int IMAGE_TYPE_DNG = 3;
+    static final int IMAGE_TYPE_JPEG = 4;
+    static final int IMAGE_TYPE_NEF = 5;
+    static final int IMAGE_TYPE_NRW = 6;
+    static final int IMAGE_TYPE_ORF = 7;
+    static final int IMAGE_TYPE_PEF = 8;
+    static final int IMAGE_TYPE_RAF = 9;
+    static final int IMAGE_TYPE_RW2 = 10;
+    static final int IMAGE_TYPE_SRW = 11;
+    static final int IMAGE_TYPE_HEIF = 12;
+    static final int IMAGE_TYPE_PNG = 13;
+    static final int IMAGE_TYPE_WEBP = 14;
+
+    static {
+        sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
+        sFormatterPrimary.setTimeZone(TimeZone.getTimeZone("UTC"));
+        sFormatterSecondary = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
+        sFormatterSecondary.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        // Build up the hash tables to look up Exif tags for reading Exif tags.
+        for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+            sExifTagMapsForReading[ifdType] = new HashMap<>();
+            sExifTagMapsForWriting[ifdType] = new HashMap<>();
+            for (ExifTag tag : EXIF_TAGS[ifdType]) {
+                sExifTagMapsForReading[ifdType].put(tag.number, tag);
+                sExifTagMapsForWriting[ifdType].put(tag.name, tag);
+            }
+        }
+
+        // Build up the hash table to look up Exif pointer tags.
+        sExifPointerTagMap.put(EXIF_POINTER_TAGS[0].number, IFD_TYPE_PREVIEW); // 330
+        sExifPointerTagMap.put(EXIF_POINTER_TAGS[1].number, IFD_TYPE_EXIF); // 34665
+        sExifPointerTagMap.put(EXIF_POINTER_TAGS[2].number, IFD_TYPE_GPS); // 34853
+        sExifPointerTagMap.put(EXIF_POINTER_TAGS[3].number, IFD_TYPE_INTEROPERABILITY); // 40965
+        sExifPointerTagMap.put(EXIF_POINTER_TAGS[4].number, IFD_TYPE_ORF_CAMERA_SETTINGS); // 8224
+        sExifPointerTagMap.put(EXIF_POINTER_TAGS[5].number, IFD_TYPE_ORF_IMAGE_PROCESSING); // 8256
+    }
+
+    private String mFilename;
+    private FileDescriptor mSeekableFileDescriptor;
+    private AssetManager.AssetInputStream mAssetInputStream;
+    private int mMimeType;
+    private boolean mIsExifDataOnly;
+    @SuppressWarnings("unchecked")
+    private final HashMap<String, ExifAttribute>[] mAttributes = new HashMap[EXIF_TAGS.length];
+    private Set<Integer> mAttributesOffsets = new HashSet<>(EXIF_TAGS.length);
+    private ByteOrder mExifByteOrder = BIG_ENDIAN;
+    private boolean mHasThumbnail;
+    private boolean mHasThumbnailStrips;
+    private boolean mAreThumbnailStripsConsecutive;
+    // Used to indicate the position of the thumbnail (doesn't include offset to EXIF data segment).
+    private int mThumbnailOffset;
+    private int mThumbnailLength;
+    private byte[] mThumbnailBytes;
+    private int mThumbnailCompression;
+    // Used to indicate offset from the start of the original input stream to EXIF data
+    private int mOffsetToExifData;
+    private int mOrfMakerNoteOffset;
+    private int mOrfThumbnailOffset;
+    private int mOrfThumbnailLength;
+    private boolean mModified;
+    // XMP data can be contained as either part of the EXIF data (tag number 700), or as a
+    // separate data marker (a separate MARKER_APP1).
+    private boolean mXmpIsFromSeparateMarker;
+
+    // Pattern to check non zero timestamp
+    private static final Pattern NON_ZERO_TIME_PATTERN = Pattern.compile(".*[1-9].*");
+    // Pattern to check gps timestamp
+    private static final Pattern GPS_TIMESTAMP_PATTERN =
+            Pattern.compile("^(\\d{2}):(\\d{2}):(\\d{2})$");
+    // Pattern to check date time primary format (e.g. 2020:01:01 00:00:00)
+    private static final Pattern DATETIME_PRIMARY_FORMAT_PATTERN =
+            Pattern.compile("^(\\d{4}):(\\d{2}):(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+    // Pattern to check date time secondary format (e.g. 2020-01-01 00:00:00)
+    private static final Pattern DATETIME_SECONDARY_FORMAT_PATTERN =
+            Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+    private static final int DATETIME_VALUE_STRING_LENGTH = 19;
+
+    /**
+     * Reads Exif tags from the specified image file.
+     *
+     * @param file the file of the image data
+     * @throws NullPointerException if file is null
+     * @throws IOException if an I/O error occurs while retrieving file descriptor via
+     *         {@link FileInputStream#getFD()}.
+     */
+    public ExifInterface(@NonNull File file) throws IOException {
+        if (file == null) {
+            throw new NullPointerException("file cannot be null");
+        }
+        initForFilename(file.getAbsolutePath());
+    }
+
+    /**
+     * Reads Exif tags from the specified image file.
+     *
+     * @param filename the name of the file of the image data
+     * @throws NullPointerException if file name is null
+     * @throws IOException if an I/O error occurs while retrieving file descriptor via
+     *         {@link FileInputStream#getFD()}.
+     */
+    public ExifInterface(@NonNull String filename) throws IOException {
+        if (filename == null) {
+            throw new NullPointerException("filename cannot be null");
+        }
+        initForFilename(filename);
+    }
+
+    /**
+     * Reads Exif tags from the specified image file descriptor. Attribute mutation is supported
+     * for writable and seekable file descriptors only. This constructor will not rewind the offset
+     * of the given file descriptor. Developers should close the file descriptor after use.
+     *
+     * @param fileDescriptor the file descriptor of the image data
+     * @throws NullPointerException if file descriptor is null
+     * @throws IOException if an error occurs while duplicating the file descriptor.
+     */
+    public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException {
+        if (fileDescriptor == null) {
+            throw new NullPointerException("fileDescriptor cannot be null");
+        }
+        mAssetInputStream = null;
+        mFilename = null;
+
+        boolean isFdDuped = false;
+        if (Build.VERSION.SDK_INT >= 21 && isSeekableFD(fileDescriptor)) {
+            mSeekableFileDescriptor = fileDescriptor;
+            // Keep the original file descriptor in order to save attributes when it's seekable.
+            // Otherwise, just close the given file descriptor after reading it because the save
+            // feature won't be working.
+            try {
+                fileDescriptor = Api21Impl.dup(fileDescriptor);
+                isFdDuped = true;
+            } catch (Exception e) {
+                throw new IOException("Failed to duplicate file descriptor", e);
+            }
+        } else {
+            mSeekableFileDescriptor = null;
+        }
+        FileInputStream in = null;
+        try {
+            in = new FileInputStream(fileDescriptor);
+            loadAttributes(in);
+        } finally {
+            closeQuietly(in);
+            if (isFdDuped) {
+                closeFileDescriptor(fileDescriptor);
+            }
+        }
+    }
+
+    /**
+     * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
+     * for input streams. The given input stream will proceed from its current position. Developers
+     * should close the input stream after use. This constructor is not intended to be used with
+     * an input stream that performs any networking operations.
+     *
+     * @param inputStream the input stream that contains the image data
+     * @throws NullPointerException if the input stream is null
+     */
+    public ExifInterface(@NonNull InputStream inputStream) throws IOException {
+        this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA);
+    }
+
+    /**
+     * Reads Exif tags from the specified image input stream based on the stream type. Attribute
+     * mutation is not supported for input streams. The given input stream will proceed from its
+     * current position. Developers should close the input stream after use. This constructor is not
+     * intended to be used with an input stream that performs any networking operations.
+     *
+     * @param inputStream the input stream that contains the image data
+     * @param streamType the type of input stream
+     * @throws NullPointerException if the input stream is null
+     * @throws IOException if an I/O error occurs while retrieving file descriptor via
+     *         {@link FileInputStream#getFD()}.
+     */
+    public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType)
+            throws IOException {
+        if (inputStream == null) {
+            throw new NullPointerException("inputStream cannot be null");
+        }
+        mFilename = null;
+
+        mIsExifDataOnly = streamType == STREAM_TYPE_EXIF_DATA_ONLY;
+        if (mIsExifDataOnly) {
+            mAssetInputStream = null;
+            mSeekableFileDescriptor = null;
+        } else {
+            if (inputStream instanceof AssetManager.AssetInputStream) {
+                mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
+                mSeekableFileDescriptor = null;
+            } else if (inputStream instanceof FileInputStream
+                    && isSeekableFD(((FileInputStream) inputStream).getFD())) {
+                mAssetInputStream = null;
+                mSeekableFileDescriptor = ((FileInputStream) inputStream).getFD();
+            } else {
+                mAssetInputStream = null;
+                mSeekableFileDescriptor = null;
+            }
+        }
+        loadAttributes(inputStream);
+    }
+
+    /**
+     * Returns whether ExifInterface currently supports reading data from the specified mime type
+     * or not.
+     *
+     * @param mimeType the string value of mime type
+     */
+    public static boolean isSupportedMimeType(@NonNull String mimeType) {
+        if (mimeType == null) {
+            throw new NullPointerException("mimeType shouldn't be null");
+        }
+
+        switch (mimeType.toLowerCase(Locale.ROOT)) {
+            case "image/jpeg":
+            case "image/x-adobe-dng":
+            case "image/x-canon-cr2":
+            case "image/x-nikon-nef":
+            case "image/x-nikon-nrw":
+            case "image/x-sony-arw":
+            case "image/x-panasonic-rw2":
+            case "image/x-olympus-orf":
+            case "image/x-pentax-pef":
+            case "image/x-samsung-srw":
+            case "image/x-fuji-raf":
+            case "image/heic":
+            case "image/heif":
+            case "image/png":
+            case "image/webp":
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag in
+     * the image file.
+     *
+     * @param tag the name of the tag.
+     */
+    @SuppressWarnings("deprecation")
+    @Nullable
+    private ExifAttribute getExifAttribute(@NonNull String tag) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        // Maintain compatibility.
+        if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+            if (DEBUG) {
+                logger.debug("getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+                        + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+            }
+            tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+        }
+        // Retrieves all tag groups. The value from primary image tag group has a higher priority
+        // than the value from the thumbnail tag group if there are more than one candidates.
+        for (int i = 0; i < EXIF_TAGS.length; ++i) {
+            ExifAttribute value = mAttributes[i].get(tag);
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+	/**
+	 * Threema-added
+	 * Returns the value of the specified tag assuming it's a UTF-8 string or {@code null} if there
+	 * is no such tag in the image file.
+	 * @param tag
+	 * @return
 	 */
-	public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite";
+	@Nullable
+	public String getUTF8StringAttribute(@NonNull String tag) {
+		ExifAttribute attribute = getExifAttribute(tag);
+		if (attribute != null) {
+			return attribute.getUTF8StringValue();
+		}
+		return null;
+	}
 
-	// D. Other tags
-	/**
-	 *  <p>The date and time of image creation. In this standard it is the date and time the file
-	 *  was changed. The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
-	 *  the date and time separated by one blank character ({@code 0x20}). When the date and time
-	 *  are unknown, all the character spaces except colons (":") should be filled with blank
-	 *  characters, or else the Interoperability field should be filled with blank characters.
-	 *  The character string length is 20 Bytes including NULL for termination. When the field is
-	 *  left blank, it is treated as unknown.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 306</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 19</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_DATETIME = "DateTime";
-	/**
-	 *  <p>An ASCII string giving the title of the image. It is possible to be added a comment
-	 *  such as "1988 company picnic" or the like. Two-byte character codes cannot be used. When
-	 *  a 2-byte code is necessary, {@link #TAG_USER_COMMENT} is to be used.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 270</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription";
-	/**
-	 *  <p>The manufacturer of the recording equipment. This is the manufacturer of the DSC,
-	 *  scanner, video digitizer or other equipment that generated the image. When the field is left
-	 *  blank, it is treated as unknown.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 271</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_MAKE = "Make";
-	/**
-	 *  <p>The model name or model number of the equipment. This is the model name of number of
-	 *  the DSC, scanner, video digitizer or other equipment that generated the image. When
-	 *  the field is left blank, it is treated as unknown.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 272</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_MODEL = "Model";
-	/**
-	 *  <p>This tag records the name and version of the software or firmware of the camera or image
-	 *  input device used to generate the image. The detailed format is not specified, but it is
-	 *  recommended that the example shown below be followed. When the field is left blank, it is
-	 *  treated as unknown.</p>
-	 *
-	 *  <p>Ex.) "Exif Software Version 1.00a".</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 305</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SOFTWARE = "Software";
-	/**
-	 *  <p>This tag records the name of the camera owner, photographer or image creator.
-	 *  The detailed format is not specified, but it is recommended that the information be written
-	 *  as in the example below for ease of Interoperability. When the field is left blank, it is
-	 *  treated as unknown.</p>
-	 *
-	 *  <p>Ex.) "Camera owner, John Smith; Photographer, Michael Brown; Image creator,
-	 *  Ken James"</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 315</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_ARTIST = "Artist";
-	/**
-	 *  <p>Copyright information. In this standard the tag is used to indicate both the photographer
-	 *  and editor copyrights. It is the copyright notice of the person or organization claiming
-	 *  rights to the image. The Interoperability copyright statement including date and rights
-	 *  should be written in this field; e.g., "Copyright, John Smith, 19xx. All rights reserved."
-	 *  In this standard the field records both the photographer and editor copyrights, with each
-	 *  recorded in a separate part of the statement. When there is a clear distinction between
-	 *  the photographer and editor copyrights, these are to be written in the order of photographer
-	 *  followed by editor copyright, separated by NULL (in this case, since the statement also ends
-	 *  with a NULL, there are two NULL codes) (see example 1). When only the photographer copyright
-	 *  is given, it is terminated by one NULL code (see example 2). When only the editor copyright
-	 *  is given, the photographer copyright part consists of one space followed by a terminating
-	 *  NULL code, then the editor copyright is given (see example 3). When the field is left blank,
-	 *  it is treated as unknown.</p>
-	 *
-	 *  <p>Ex. 1) When both the photographer copyright and editor copyright are given.
-	 *  <ul><li>Photographer copyright + NULL + editor copyright + NULL</li></ul></p>
-	 *  <p>Ex. 2) When only the photographer copyright is given.
-	 *  <ul><li>Photographer copyright + NULL</li></ul></p>
-	 *  <p>Ex. 3) When only the editor copyright is given.
-	 *  <ul><li>Space ({@code 0x20}) + NULL + editor copyright + NULL</li></ul></p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 315</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_COPYRIGHT = "Copyright";
+    /**
+     * Returns the value of the specified tag or {@code null} if there
+     * is no such tag in the image file.
+     *
+     * @param tag the name of the tag.
+     */
+    @Nullable
+    public String getAttribute(@NonNull String tag) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        ExifAttribute attribute = getExifAttribute(tag);
+        if (attribute != null) {
+            if (!sTagSetForCompatibility.contains(tag)) {
+                return attribute.getStringValue(mExifByteOrder);
+            }
+            if (tag.equals(TAG_GPS_TIMESTAMP)) {
+                // Convert the rational values to the custom formats for backwards compatibility.
+                if (attribute.format != IFD_FORMAT_URATIONAL
+                        && attribute.format != IFD_FORMAT_SRATIONAL) {
+                    logger.warn("GPS Timestamp format is not rational. format=" + attribute.format);
+                    return null;
+                }
+                Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
+                if (array == null || array.length != 3) {
+                    logger.warn("Invalid GPS Timestamp array. array=" + Arrays.toString(array));
+                    return null;
+                }
+                return String.format("%02d:%02d:%02d",
+                        (int) ((float) array[0].numerator / array[0].denominator),
+                        (int) ((float) array[1].numerator / array[1].denominator),
+                        (int) ((float) array[2].numerator / array[2].denominator));
+            }
+            try {
+                return Double.toString(attribute.getDoubleValue(mExifByteOrder));
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the integer value of the specified tag. If there is no such tag
+     * in the image file or the value cannot be parsed as integer, return
+     * <var>defaultValue</var>.
+     *
+     * @param tag the name of the tag.
+     * @param defaultValue the value to return if the tag is not available.
+     */
+    public int getAttributeInt(@NonNull String tag, int defaultValue) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        ExifAttribute exifAttribute = getExifAttribute(tag);
+        if (exifAttribute == null) {
+            return defaultValue;
+        }
+
+        try {
+            return exifAttribute.getIntValue(mExifByteOrder);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Returns the double value of the tag that is specified as rational or contains a
+     * double-formatted value. If there is no such tag in the image file or the value cannot be
+     * parsed as double, return <var>defaultValue</var>.
+     *
+     * @param tag the name of the tag.
+     * @param defaultValue the value to return if the tag is not available.
+     */
+    public double getAttributeDouble(@NonNull String tag, double defaultValue) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        ExifAttribute exifAttribute = getExifAttribute(tag);
+        if (exifAttribute == null) {
+            return defaultValue;
+        }
+
+        try {
+            return exifAttribute.getDoubleValue(mExifByteOrder);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Sets the value of the specified tag.
+     *
+     * @param tag the name of the tag.
+     * @param value the value of the tag.
+     */
+    @SuppressWarnings("deprecation")
+    public void setAttribute(@NonNull String tag, @Nullable String value) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        // Validate and convert if necessary.
+        if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
+                || TAG_DATETIME_DIGITIZED.equals(tag)) {
+            if (value != null) {
+                boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
+                boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(value).find();
+                // Validate
+                if (value.length() != DATETIME_VALUE_STRING_LENGTH
+                        || (!isPrimaryFormat && !isSecondaryFormat)) {
+                    logger.warn("Invalid value for " + tag + " : " + value);
+                    return;
+                }
+                // If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it to
+                // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
+                // official documentation.
+                // See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
+                if (isSecondaryFormat) {
+                    // Replace "-" with ":" to match the primary format.
+                    value = value.replaceAll("-", ":");
+                }
+            }
+        }
+        // Maintain compatibility.
+        if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+            if (DEBUG) {
+                logger.debug("setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+                        + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+            }
+            tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+        }
+        // Convert the given value to rational values for backwards compatibility.
+        if (value != null && sTagSetForCompatibility.contains(tag)) {
+            if (tag.equals(TAG_GPS_TIMESTAMP)) {
+                Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
+                if (!m.find()) {
+                    logger.warn("Invalid value for " + tag + " : " + value);
+                    return;
+                }
+                value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1,"
+                        + Integer.parseInt(m.group(3)) + "/1";
+            } else {
+                try {
+                    double doubleValue = Double.parseDouble(value);
+                    value = new Rational(doubleValue).toString();
+                } catch (NumberFormatException e) {
+                    logger.warn("Invalid value for " + tag + " : " + value);
+                    return;
+                }
+            }
+        }
+
+        for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
+            if (i == IFD_TYPE_THUMBNAIL && !mHasThumbnail) {
+                continue;
+            }
+            final ExifTag exifTag = sExifTagMapsForWriting[i].get(tag);
+            if (exifTag != null) {
+                if (value == null) {
+                    mAttributes[i].remove(tag);
+                    continue;
+                }
+                Pair<Integer, Integer> guess = guessDataFormat(value);
+                int dataFormat;
+                if (exifTag.primaryFormat == guess.first || exifTag.primaryFormat == guess.second) {
+                    dataFormat = exifTag.primaryFormat;
+                } else if (exifTag.secondaryFormat != -1 && (exifTag.secondaryFormat == guess.first
+                        || exifTag.secondaryFormat == guess.second)) {
+                    dataFormat = exifTag.secondaryFormat;
+                } else if (exifTag.primaryFormat == IFD_FORMAT_BYTE
+                        || exifTag.primaryFormat == IFD_FORMAT_UNDEFINED
+                        || exifTag.primaryFormat == IFD_FORMAT_STRING) {
+                    dataFormat = exifTag.primaryFormat;
+                } else {
+                    if (DEBUG) {
+                        logger.debug("Given tag (" + tag
+                                + ") value didn't match with one of expected "
+                                + "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat]
+                                + (exifTag.secondaryFormat == -1 ? "" : ", "
+                                + IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: "
+                                + IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? "" : ", "
+                                + IFD_FORMAT_NAMES[guess.second]) + ")");
+                    }
+                    continue;
+                }
+                switch (dataFormat) {
+                    case IFD_FORMAT_BYTE: {
+                        mAttributes[i].put(tag, ExifAttribute.createByte(value));
+                        break;
+                    }
+                    case IFD_FORMAT_UNDEFINED:
+                    case IFD_FORMAT_STRING: {
+                        mAttributes[i].put(tag, ExifAttribute.createString(value));
+                        break;
+                    }
+                    case IFD_FORMAT_USHORT: {
+                        final String[] values = value.split(",", -1);
+                        final int[] intArray = new int[values.length];
+                        for (int j = 0; j < values.length; ++j) {
+                            intArray[j] = Integer.parseInt(values[j]);
+                        }
+                        mAttributes[i].put(tag,
+                                ExifAttribute.createUShort(intArray, mExifByteOrder));
+                        break;
+                    }
+                    case IFD_FORMAT_SLONG: {
+                        final String[] values = value.split(",", -1);
+                        final int[] intArray = new int[values.length];
+                        for (int j = 0; j < values.length; ++j) {
+                            intArray[j] = Integer.parseInt(values[j]);
+                        }
+                        mAttributes[i].put(tag,
+                                ExifAttribute.createSLong(intArray, mExifByteOrder));
+                        break;
+                    }
+                    case IFD_FORMAT_ULONG: {
+                        final String[] values = value.split(",", -1);
+                        final long[] longArray = new long[values.length];
+                        for (int j = 0; j < values.length; ++j) {
+                            longArray[j] = Long.parseLong(values[j]);
+                        }
+                        mAttributes[i].put(tag,
+                                ExifAttribute.createULong(longArray, mExifByteOrder));
+                        break;
+                    }
+                    case IFD_FORMAT_URATIONAL: {
+                        final String[] values = value.split(",", -1);
+                        final Rational[] rationalArray = new Rational[values.length];
+                        for (int j = 0; j < values.length; ++j) {
+                            final String[] numbers = values[j].split("/", -1);
+                            rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
+                                    (long) Double.parseDouble(numbers[1]));
+                        }
+                        mAttributes[i].put(tag,
+                                ExifAttribute.createURational(rationalArray, mExifByteOrder));
+                        break;
+                    }
+                    case IFD_FORMAT_SRATIONAL: {
+                        final String[] values = value.split(",", -1);
+                        final Rational[] rationalArray = new Rational[values.length];
+                        for (int j = 0; j < values.length; ++j) {
+                            final String[] numbers = values[j].split("/", -1);
+                            rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
+                                    (long) Double.parseDouble(numbers[1]));
+                        }
+                        mAttributes[i].put(tag,
+                                ExifAttribute.createSRational(rationalArray, mExifByteOrder));
+                        break;
+                    }
+                    case IFD_FORMAT_DOUBLE: {
+                        final String[] values = value.split(",", -1);
+                        final double[] doubleArray = new double[values.length];
+                        for (int j = 0; j < values.length; ++j) {
+                            doubleArray[j] = Double.parseDouble(values[j]);
+                        }
+                        mAttributes[i].put(tag,
+                                ExifAttribute.createDouble(doubleArray, mExifByteOrder));
+                        break;
+                    }
+                    default:
+                        if (DEBUG) {
+                            logger.debug("Data format isn't one of expected formats: " + dataFormat);
+                        }
+                        continue;
+                }
+            }
+        }
+    }
+
+    /**
+     * Resets the {@link #TAG_ORIENTATION} of the image to be {@link #ORIENTATION_NORMAL}.
+     */
+    public void resetOrientation() {
+        setAttribute(TAG_ORIENTATION, Integer.toString(ORIENTATION_NORMAL));
+    }
+
+    /**
+     * Rotates the image by the given degree clockwise. The degree should be a multiple of
+     * 90 (e.g, 90, 180, -90, etc.).
+     *
+     * @param degree The degree of rotation.
+     */
+    public void rotate(int degree) {
+        if (degree % 90 !=0) {
+            throw new IllegalArgumentException("degree should be a multiple of 90");
+        }
+
+        int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+        int currentIndex, newIndex;
+        int resultOrientation;
+        if (ROTATION_ORDER.contains(currentOrientation)) {
+            currentIndex = ROTATION_ORDER.indexOf(currentOrientation);
+            newIndex = (currentIndex + degree / 90) % 4;
+            newIndex += newIndex < 0 ? 4 : 0;
+            resultOrientation = ROTATION_ORDER.get(newIndex);
+        } else if (FLIPPED_ROTATION_ORDER.contains(currentOrientation)) {
+            currentIndex = FLIPPED_ROTATION_ORDER.indexOf(currentOrientation);
+            newIndex = (currentIndex + degree / 90) % 4;
+            newIndex += newIndex < 0 ? 4 : 0;
+            resultOrientation = FLIPPED_ROTATION_ORDER.get(newIndex);
+        } else {
+            resultOrientation = ORIENTATION_UNDEFINED;
+        }
+
+        setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
+    }
+
+    /**
+     * Flips the image vertically.
+     */
+    public void flipVertically() {
+        int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+        int resultOrientation;
+        switch (currentOrientation) {
+            case ORIENTATION_FLIP_HORIZONTAL:
+                resultOrientation = ORIENTATION_ROTATE_180;
+                break;
+            case ORIENTATION_ROTATE_180:
+                resultOrientation = ORIENTATION_FLIP_HORIZONTAL;
+                break;
+            case ORIENTATION_FLIP_VERTICAL:
+                resultOrientation = ORIENTATION_NORMAL;
+                break;
+            case ORIENTATION_TRANSPOSE:
+                resultOrientation = ORIENTATION_ROTATE_270;
+                break;
+            case ORIENTATION_ROTATE_90:
+                resultOrientation = ORIENTATION_TRANSVERSE;
+                break;
+            case ORIENTATION_TRANSVERSE:
+                resultOrientation = ORIENTATION_ROTATE_90;
+                break;
+            case ORIENTATION_ROTATE_270:
+                resultOrientation = ORIENTATION_TRANSPOSE;
+                break;
+            case ORIENTATION_NORMAL:
+                resultOrientation = ORIENTATION_FLIP_VERTICAL;
+                break;
+            case ORIENTATION_UNDEFINED:
+            default:
+                resultOrientation = ORIENTATION_UNDEFINED;
+                break;
+        }
+        setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
+    }
+
+    /**
+     * Flips the image horizontally.
+     */
+    public void flipHorizontally() {
+        int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+        int resultOrientation;
+        switch (currentOrientation) {
+            case ORIENTATION_FLIP_HORIZONTAL:
+                resultOrientation = ORIENTATION_NORMAL;
+                break;
+            case ORIENTATION_ROTATE_180:
+                resultOrientation = ORIENTATION_FLIP_VERTICAL;
+                break;
+            case ORIENTATION_FLIP_VERTICAL:
+                resultOrientation = ORIENTATION_ROTATE_180;
+                break;
+            case ORIENTATION_TRANSPOSE:
+                resultOrientation = ORIENTATION_ROTATE_90;
+                break;
+            case ORIENTATION_ROTATE_90:
+                resultOrientation = ORIENTATION_TRANSPOSE;
+                break;
+            case ORIENTATION_TRANSVERSE:
+                resultOrientation = ORIENTATION_ROTATE_270;
+                break;
+            case ORIENTATION_ROTATE_270:
+                resultOrientation = ORIENTATION_TRANSVERSE;
+                break;
+            case ORIENTATION_NORMAL:
+                resultOrientation = ORIENTATION_FLIP_HORIZONTAL;
+                break;
+            case ORIENTATION_UNDEFINED:
+            default:
+                resultOrientation = ORIENTATION_UNDEFINED;
+                break;
+        }
+        setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
+    }
+
+    /**
+     * Returns if the current image orientation is flipped.
+     *
+     * @see #getRotationDegrees()
+     */
+    public boolean isFlipped() {
+        int orientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+        switch (orientation) {
+            case ORIENTATION_FLIP_HORIZONTAL:
+            case ORIENTATION_TRANSVERSE:
+            case ORIENTATION_FLIP_VERTICAL:
+            case ORIENTATION_TRANSPOSE:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Returns the rotation degrees for the current image orientation. If the image is flipped,
+     * i.e., {@link #isFlipped()} returns {@code true}, the rotation degrees will be base on
+     * the assumption that the image is first flipped horizontally (along Y-axis), and then do
+     * the rotation. For example, {@link #ORIENTATION_TRANSPOSE} will be interpreted as flipped
+     * horizontally first, and then rotate 270 degrees clockwise.
+     *
+     * @return The rotation degrees of the image after the horizontal flipping is applied, if any.
+     *
+     * @see #isFlipped()
+     */
+    public int getRotationDegrees() {
+        int orientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+        switch (orientation) {
+            case ORIENTATION_ROTATE_90:
+            case ORIENTATION_TRANSVERSE:
+                return 90;
+            case ORIENTATION_ROTATE_180:
+            case ORIENTATION_FLIP_VERTICAL:
+                return 180;
+            case ORIENTATION_ROTATE_270:
+            case ORIENTATION_TRANSPOSE:
+                return 270;
+            case ORIENTATION_UNDEFINED:
+            case ORIENTATION_NORMAL:
+            case ORIENTATION_FLIP_HORIZONTAL:
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Remove any values of the specified tag.
+     *
+     * @param tag the name of the tag.
+     */
+    private void removeAttribute(String tag) {
+        for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
+            mAttributes[i].remove(tag);
+        }
+    }
+
+    /**
+     * This function decides which parser to read the image data according to the given input stream
+     * type and the content of the input stream.
+     */
+    private void loadAttributes(@NonNull InputStream in) {
+        try {
+            // Initialize mAttributes.
+            for (int i = 0; i < EXIF_TAGS.length; ++i) {
+                mAttributes[i] = new HashMap<>();
+            }
+
+            // Check file type
+            if (!mIsExifDataOnly) {
+                in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE);
+                mMimeType = getMimeType((BufferedInputStream) in);
+            }
+
+            if (shouldSupportSeek(mMimeType)) {
+                SeekableByteOrderedDataInputStream inputStream =
+                        new SeekableByteOrderedDataInputStream(in);
+                if (mIsExifDataOnly) {
+                    if (!getStandaloneAttributes(inputStream)) {
+                        return;
+                    }
+                } else {
+                    if (mMimeType == IMAGE_TYPE_HEIF) {
+                        getHeifAttributes(inputStream);
+                    } else if (mMimeType == IMAGE_TYPE_ORF) {
+                        getOrfAttributes(inputStream);
+                    } else if (mMimeType == IMAGE_TYPE_RW2) {
+                        getRw2Attributes(inputStream);
+                    } else {
+                        getRawAttributes(inputStream);
+                    }
+                }
+                // Set thumbnail image offset and length
+                inputStream.seek(mOffsetToExifData);
+                setThumbnailData(inputStream);
+            } else {
+                ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in);
+                if (mMimeType == IMAGE_TYPE_JPEG) {
+                    getJpegAttributes(inputStream, /* offsetToJpeg= */ 0,
+                            IFD_TYPE_PRIMARY);
+                } else if (mMimeType == IMAGE_TYPE_PNG) {
+                    getPngAttributes(inputStream);
+                } else if (mMimeType == IMAGE_TYPE_RAF) {
+                    getRafAttributes(inputStream);
+                } else if (mMimeType == IMAGE_TYPE_WEBP) {
+                    getWebpAttributes(inputStream);
+                }
+            }
+        } catch (IOException | UnsupportedOperationException e) {
+            // Ignore exceptions in order to keep the compatibility with the old versions of
+            // ExifInterface.
+            if (DEBUG) {
+                logger.warn("Invalid image: ExifInterface got an unsupported image format file"
+                        + "(ExifInterface supports JPEG and some RAW image formats only) "
+                        + "or a corrupted JPEG file to ExifInterface.", e);
+            }
+        } finally {
+            addDefaultValuesForCompatibility();
+
+            if (DEBUG) {
+                printAttributes();
+            }
+        }
+    }
+
+    private static boolean isSeekableFD(FileDescriptor fd) {
+        if (Build.VERSION.SDK_INT >= 21) {
+            try {
+                Api21Impl.lseek(fd, 0, OsConstants.SEEK_CUR);
+                return true;
+            } catch (Exception e) {
+                if (DEBUG) {
+                    logger.debug("The file descriptor for the given input is not seekable");
+                }
+                return false;
+            }
+        }
+        return false;
+    }
+
+    // Prints out attributes for debugging.
+    private void printAttributes() {
+        for (int i = 0; i < mAttributes.length; ++i) {
+            logger.debug("The size of tag group[" + i + "]: " + mAttributes[i].size());
+            for (Map.Entry<String, ExifAttribute> entry : mAttributes[i].entrySet()) {
+                final ExifAttribute tagValue = entry.getValue();
+                logger.debug("tagName: " + entry.getKey() + ", tagType: " + tagValue.toString()
+                        + ", tagValue: '" + tagValue.getStringValue(mExifByteOrder) + "'");
+            }
+        }
+    }
+
+    /**
+     * Save the tag data into the original image file. This is expensive because it involves
+     * copying all the data from one file to another and deleting the old file and renaming the
+     * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
+     * and make a single call rather than multiple calls for each attribute.
+     * <p>
+     * This method is supported for JPEG, PNG, and WebP formats.
+     * <p class="note">
+     * Note: after calling this method, any attempts to obtain range information
+     * from {@link #getAttributeRange(String)} or {@link #getThumbnailRange()}
+     * will throw {@link IllegalStateException}, since the offsets may have
+     * changed in the newly written file.
+     * <p>
+     * For WebP format, the Exif data will be stored as an Extended File Format, and it may not be
+     * supported for older readers.
+     * <p>
+     * For PNG format, the Exif data will be stored as an "eXIf" chunk as per
+     * "Extensions to the PNG 1.2 Specification, Version 1.5.0".
+     */
+    public void saveAttributes() throws IOException {
+        if (!isSupportedFormatForSavingAttributes(mMimeType)) {
+            throw new IOException("ExifInterface only supports saving attributes for JPEG, PNG, "
+                    + "and WebP formats.");
+        }
+        if (mSeekableFileDescriptor == null && mFilename == null) {
+            throw new IOException(
+                    "ExifInterface does not support saving attributes for the current input.");
+        }
+        if (mHasThumbnail && mHasThumbnailStrips && !mAreThumbnailStripsConsecutive) {
+            throw new IOException("ExifInterface does not support saving attributes when the image "
+                    + "file has non-consecutive thumbnail strips");
+        }
+
+        // Remember the fact that we've changed the file on disk from what was
+        // originally parsed, meaning we can't answer range questions
+        mModified = true;
+
+        // Keep the thumbnail in memory
+        mThumbnailBytes = getThumbnail();
+
+        FileInputStream in = null;
+        FileOutputStream out = null;
+        File tempFile;
+        try {
+            // Copy the original file to temporary file.
+            tempFile = File.createTempFile("temp", "tmp");
+            if (mFilename != null) {
+                in = new FileInputStream(mFilename);
+            } else {
+                // mSeekableFileDescriptor will be non-null only for SDK_INT >= 21, but this check
+                // is needed to prevent calling Os.lseek at runtime for SDK < 21.
+                if (Build.VERSION.SDK_INT >= 21) {
+                    Api21Impl.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
+                    in = new FileInputStream(mSeekableFileDescriptor);
+                }
+            }
+            out = new FileOutputStream(tempFile);
+            copy(in, out);
+        } catch (Exception e) {
+            throw new IOException("Failed to copy original file to temp file", e);
+        } finally {
+            closeQuietly(in);
+            closeQuietly(out);
+        }
+
+        in = null;
+        out = null;
+        BufferedInputStream bufferedIn = null;
+        BufferedOutputStream bufferedOut = null;
+        boolean shouldKeepTempFile = false;
+        try {
+            // Save the new file.
+            in = new FileInputStream(tempFile);
+            if (mFilename != null) {
+                out = new FileOutputStream(mFilename);
+            } else {
+                // mSeekableFileDescriptor will be non-null only for SDK_INT >= 21, but this check
+                // is needed to prevent calling Os.lseek at runtime for SDK < 21.
+                if (Build.VERSION.SDK_INT >= 21) {
+                    Api21Impl.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
+                    out = new FileOutputStream(mSeekableFileDescriptor);
+                }
+            }
+            bufferedIn = new BufferedInputStream(in);
+            bufferedOut = new BufferedOutputStream(out);
+            if (mMimeType == IMAGE_TYPE_JPEG) {
+				// Threema-modified: set noExif to true
+                saveJpegAttributes(bufferedIn, bufferedOut, true);
+            } else if (mMimeType == IMAGE_TYPE_PNG) {
+                savePngAttributes(bufferedIn, bufferedOut);
+            } else if (mMimeType == IMAGE_TYPE_WEBP) {
+                saveWebpAttributes(bufferedIn, bufferedOut);
+            }
+        } catch (Exception e) {
+            try {
+                // Restore original file
+                in = new FileInputStream(tempFile);
+                if (mFilename != null) {
+                    out = new FileOutputStream(mFilename);
+                } else {
+                    // mSeekableFileDescriptor will be non-null only for SDK_INT >= 21, but this
+                    // check is needed to prevent calling Os.lseek at runtime for SDK < 21.
+                    if (Build.VERSION.SDK_INT >= 21) {
+                        Api21Impl.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
+                        out = new FileOutputStream(mSeekableFileDescriptor);
+                    }
+                }
+                copy(in, out);
+            } catch (Exception exception) {
+                shouldKeepTempFile = true;
+                throw new IOException("Failed to save new file. Original file is stored in "
+                        + tempFile.getAbsolutePath(), exception);
+            } finally {
+                closeQuietly(in);
+                closeQuietly(out);
+            }
+            throw new IOException("Failed to save new file", e);
+        } finally {
+            closeQuietly(bufferedIn);
+            closeQuietly(bufferedOut);
+            if (!shouldKeepTempFile) {
+                tempFile.delete();
+            }
+        }
+
+        // Discard the thumbnail in memory
+        mThumbnailBytes = null;
+    }
 
-	// Exif IFD Attribute Information
-	// A. Tags related to version
 	/**
-	 *  <p>The version of this standard supported. Nonexistence of this field is taken to mean
-	 *  nonconformance to the standard. In according with conformance to this standard, this tag
-	 *  shall be recorded like "0230” as 4-byte ASCII.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 36864</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Length = 4</li>
-	 *      <li>Default = "0230"</li>
-	 *  </ul>
-	 */
-	public static final String TAG_EXIF_VERSION = "ExifVersion";
-	/**
-	 *  <p>The Flashpix format version supported by a FPXR file. If the FPXR function supports
-	 *  Flashpix format Ver. 1.0, this is indicated similarly to {@link #TAG_EXIF_VERSION} by
-	 *  recording "0100" as 4-byte ASCII.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 40960</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Length = 4</li>
-	 *      <li>Default = "0100"</li>
-	 *  </ul>
+	 * Save the tag data into the original image file. This is expensive because it involves
+	 * copying all the data from one file to another and deleting the old file and renaming the
+	 * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
+	 * and make a single call rather than multiple calls for each attribute.
+	 * <p>
+	 * This method is only supported for JPEG files.
+	 * </p>
 	 */
-	public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion";
+	// Threema-added method
+	public void saveAttributes(InputStream inputStream, OutputStream outputStream, boolean noExif) throws IOException {
+		if (!isSupportedFormatForSavingAttributes(mMimeType) || mMimeType != IMAGE_TYPE_JPEG) {
+			throw new IOException("ExifInterface only supports saving attributes on JPEG formats.");
+		}
 
-	// B. Tags related to image data characteristics
-	/**
-	 *  <p>The color space information tag is always recorded as the color space specifier.
-	 *  Normally {@link #COLOR_SPACE_S_RGB} is used to define the color space based on the PC
-	 *  monitor conditions and environment. If a color space other than {@link #COLOR_SPACE_S_RGB}
-	 *  is used, {@link #COLOR_SPACE_UNCALIBRATED} is set. Image data recorded as
-	 *  {@link #COLOR_SPACE_UNCALIBRATED} may be treated as {@link #COLOR_SPACE_S_RGB} when it is
-	 *  converted to Flashpix.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 40961</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *  </ul>
-	 *
-	 *  @see #COLOR_SPACE_S_RGB
-	 *  @see #COLOR_SPACE_UNCALIBRATED
-	 */
-	public static final String TAG_COLOR_SPACE = "ColorSpace";
-	/**
-	 *  <p>Indicates the value of coefficient gamma. The formula of transfer function used for image
-	 *  reproduction is expressed as follows.</p>
-	 *
-	 *  <p>(Reproduced value) = (Input value) ^ gamma</p>
-	 *
-	 *  <p>Both reproduced value and input value indicate normalized value, whose minimum value is
-	 *  0 and maximum value is 1.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42240</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GAMMA = "Gamma";
+		// Keep the thumbnail in memory
+		mThumbnailBytes = getThumbnail();
 
-	// C. Tags related to image configuration
-	/**
-	 *  <p>Information specific to compressed data. When a compressed file is recorded, the valid
-	 *  width of the meaningful image shall be recorded in this tag, whether or not there is padding
-	 *  data or a restart marker. This tag shall not exist in an uncompressed file.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 40962</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension";
-	/**
-	 *  <p>Information specific to compressed data. When a compressed file is recorded, the valid
-	 *  height of the meaningful image shall be recorded in this tag, whether or not there is
-	 *  padding data or a restart marker. This tag shall not exist in an uncompressed file.
-	 *  Since data padding is unnecessary in the vertical direction, the number of lines recorded
-	 *  in this valid image height tag will in fact be the same as that recorded in the SOF.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 40963</li>
-	 *      <li>Type = Unsigned short or Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *  </ul>
-	 */
-	public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension";
-	/**
-	 *  <p>Information specific to compressed data. The channels of each component are arranged
-	 *  in order from the 1st component to the 4th. For uncompressed data the data arrangement is
-	 *  given in the {@link #TAG_PHOTOMETRIC_INTERPRETATION}. However, since
-	 *  {@link #TAG_PHOTOMETRIC_INTERPRETATION} can only express the order of Y, Cb and Cr, this tag
-	 *  is provided for cases when compressed data uses components other than Y, Cb, and Cr and to
-	 *  enable support of other sequences.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37121</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Length = 4</li>
-	 *      <li>Default = 4 5 6 0 (if RGB uncompressed) or 1 2 3 0 (other cases)</li>
-	 *      <ul>
-	 *          <li>0 = does not exist</li>
-	 *          <li>1 = Y</li>
-	 *          <li>2 = Cb</li>
-	 *          <li>3 = Cr</li>
-	 *          <li>4 = R</li>
-	 *          <li>5 = G</li>
-	 *          <li>6 = B</li>
-	 *          <li>other = reserved</li>
-	 *      </ul>
-	 *  </ul>
-	 */
-	public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration";
-	/**
-	 *  <p>Information specific to compressed data. The compression mode used for a compressed image
-	 *  is indicated in unit bits per pixel.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37122</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel";
+		saveJpegAttributes(inputStream, outputStream, noExif);
 
-	// D. Tags related to user information
-	/**
-	 *  <p>A tag for manufacturers of Exif/DCF writers to record any desired information.
-	 *  The contents are up to the manufacturer, but this tag shall not be used for any other than
-	 *  its intended purpose.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37500</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_MAKER_NOTE = "MakerNote";
-	/**
-	 *  <p>A tag for Exif users to write keywords or comments on the image besides those in
-	 *  {@link #TAG_IMAGE_DESCRIPTION}, and without the character code limitations of it.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37510</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_USER_COMMENT = "UserComment";
+		// Discard the thumbnail in memory
+		mThumbnailBytes = null;
+	}
 
-	// E. Tags related to related file information
 	/**
-	 *  <p>This tag is used to record the name of an audio file related to the image data. The only
-	 *  relational information recorded here is the Exif audio file name and extension (an ASCII
-	 *  string consisting of 8 characters + '.' + 3 characters). The path is not recorded.</p>
-	 *
-	 *  <p>When using this tag, audio files shall be recorded in conformance to the Exif audio
-	 *  format. Writers can also store the data such as Audio within APP2 as Flashpix extension
-	 *  stream data. Audio files shall be recorded in conformance to the Exif audio format.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 40964</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 12</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile";
+     * Returns true if the image file has a thumbnail.
+     */
+    public boolean hasThumbnail() {
+        return mHasThumbnail;
+    }
+
+    /**
+     * Returns true if the image file has the given attribute defined.
+     *
+     * @param tag the name of the tag.
+     */
+    public boolean hasAttribute(@NonNull String tag) {
+        return getExifAttribute(tag) != null;
+    }
+
+    /**
+     * Returns the JPEG compressed thumbnail inside the image file, or {@code null} if there is no
+     * JPEG compressed thumbnail.
+     * The returned data can be decoded using
+     * {@link BitmapFactory#decodeByteArray(byte[],int,int)}
+     */
+    @Nullable
+    public byte[] getThumbnail() {
+        if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
+            return getThumbnailBytes();
+        }
+        return null;
+    }
+
+    /**
+     * Returns the thumbnail bytes inside the image file, regardless of the compression type of the
+     * thumbnail image.
+     */
+    @Nullable
+    public byte[] getThumbnailBytes() {
+        if (!mHasThumbnail) {
+            return null;
+        }
+        if (mThumbnailBytes != null) {
+            return mThumbnailBytes;
+        }
+
+        // Read the thumbnail.
+        InputStream in = null;
+        FileDescriptor newFileDescriptor = null;
+        try {
+            if (mAssetInputStream != null) {
+                in = mAssetInputStream;
+                if (in.markSupported()) {
+                    in.reset();
+                } else {
+                    logger.debug("Cannot read thumbnail from inputstream without mark/reset support");
+                    return null;
+                }
+            } else if (mFilename != null) {
+                in = new FileInputStream(mFilename);
+            } else {
+                // mSeekableFileDescriptor will be non-null only for SDK_INT >= 21, but this check
+                // is needed to prevent calling Os.lseek and Os.dup at runtime for SDK < 21.
+                if (Build.VERSION.SDK_INT >= 21) {
+                    newFileDescriptor = Api21Impl.dup(mSeekableFileDescriptor);
+                    Api21Impl.lseek(newFileDescriptor, 0, OsConstants.SEEK_SET);
+                    in = new FileInputStream(newFileDescriptor);
+                }
+            }
+            if (in == null) {
+                // Should not be reached this.
+                throw new FileNotFoundException();
+            }
+
+            ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in);
+            inputStream.skipFully(mThumbnailOffset + mOffsetToExifData);
+            // TODO: Need to handle potential OutOfMemoryError
+            byte[] buffer = new byte[mThumbnailLength];
+            inputStream.readFully(buffer);
+            mThumbnailBytes = buffer;
+            return buffer;
+        } catch (Exception e) {
+            // Couldn't get a thumbnail image.
+            logger.debug("Encountered exception while getting thumbnail", e);
+        } finally {
+            closeQuietly(in);
+            if (newFileDescriptor != null) {
+                closeFileDescriptor(newFileDescriptor);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the
+     * thumbnail compression value, or {@code null} if the compression type is unsupported.
+     */
+    @Nullable
+    public Bitmap getThumbnailBitmap() {
+        if (!mHasThumbnail) {
+            return null;
+        } else if (mThumbnailBytes == null) {
+            mThumbnailBytes = getThumbnailBytes();
+        }
+
+        if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
+            return BitmapFactory.decodeByteArray(mThumbnailBytes, 0, mThumbnailLength);
+        } else if (mThumbnailCompression == DATA_UNCOMPRESSED) {
+            int[] rgbValues = new int[mThumbnailBytes.length / 3];
+            byte alpha = (byte) 0xff000000;
+            for (int i = 0; i < rgbValues.length; i++) {
+                rgbValues[i] = alpha + (mThumbnailBytes[3 * i] << 16)
+                        + (mThumbnailBytes[3 * i + 1] << 8) + mThumbnailBytes[3 * i + 2];
+            }
+
+            ExifAttribute imageLengthAttribute =
+                    mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_THUMBNAIL_IMAGE_LENGTH);
+            ExifAttribute imageWidthAttribute =
+                    mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_THUMBNAIL_IMAGE_WIDTH);
+            if (imageLengthAttribute != null && imageWidthAttribute != null) {
+                int imageLength = imageLengthAttribute.getIntValue(mExifByteOrder);
+                int imageWidth = imageWidthAttribute.getIntValue(mExifByteOrder);
+                return Bitmap.createBitmap(
+                        rgbValues, imageWidth, imageLength, Bitmap.Config.ARGB_8888);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if thumbnail image is JPEG Compressed, or false if either thumbnail image does
+     * not exist or thumbnail image is uncompressed.
+     */
+    public boolean isThumbnailCompressed() {
+        if (!mHasThumbnail) {
+            return false;
+        }
+        if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the offset and length of thumbnail inside the image file, or
+     * {@code null} if either there is no thumbnail or the thumbnail bytes are stored
+     * non-consecutively.
+     *
+     * @return two-element array, the offset in the first value, and length in
+     *         the second, or {@code null} if no thumbnail was found or the thumbnail strips are
+     *         not placed consecutively.
+     * @throws IllegalStateException if {@link #saveAttributes()} has been
+     *             called since the underlying file was initially parsed, since
+     *             that means offsets may have changed.
+     */
+    @Nullable
+    public long[] getThumbnailRange() {
+        if (mModified) {
+            throw new IllegalStateException(
+                    "The underlying file has been modified since being parsed");
+        }
+
+        if (mHasThumbnail) {
+            if (mHasThumbnailStrips && !mAreThumbnailStripsConsecutive) {
+                return null;
+            }
+            return new long[] { mThumbnailOffset + mOffsetToExifData, mThumbnailLength };
+        }
+        return null;
+    }
+
+    /**
+     * Returns the offset and length of the requested tag inside the image file,
+     * or {@code null} if the tag is not contained.
+     *
+     * @return two-element array, the offset in the first value, and length in
+     *         the second, or {@code null} if no tag was found.
+     * @throws IllegalStateException if {@link #saveAttributes()} has been
+     *             called since the underlying file was initially parsed, since
+     *             that means offsets may have changed.
+     */
+    @Nullable
+    public long[] getAttributeRange(@NonNull String tag) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        if (mModified) {
+            throw new IllegalStateException(
+                    "The underlying file has been modified since being parsed");
+        }
+
+        final ExifAttribute attribute = getExifAttribute(tag);
+        if (attribute != null) {
+            return new long[] { attribute.bytesOffset, attribute.bytes.length };
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the raw bytes for the value of the requested tag inside the image
+     * file, or {@code null} if the tag is not contained.
+     *
+     * @return raw bytes for the value of the requested tag, or {@code null} if
+     *         no tag was found.
+     */
+    @Nullable
+    public byte[] getAttributeBytes(@NonNull String tag) {
+        if (tag == null) {
+            throw new NullPointerException("tag shouldn't be null");
+        }
+        final ExifAttribute attribute = getExifAttribute(tag);
+        if (attribute != null) {
+            return attribute.bytes;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Stores the latitude and longitude value in a float array. The first element is the latitude,
+     * and the second element is the longitude. Returns false if the Exif tags are not available.
+     *
+     * @deprecated Use {@link #getLatLong()} instead.
+     */
+    @Deprecated
+    public boolean getLatLong(float output[]) {
+        double[] latLong = getLatLong();
+        if (latLong == null) {
+            return false;
+        }
+
+        output[0] = (float) latLong[0];
+        output[1] = (float) latLong[1];
+        return true;
+    }
+
+    /**
+     * Gets the latitude and longitude values.
+     * <p>
+     * If there are valid latitude and longitude values in the image, this method returns a double
+     * array where the first element is the latitude and the second element is the longitude.
+     * Otherwise, it returns null.
+     */
+    @Nullable
+    public double[] getLatLong() {
+        String latValue = getAttribute(TAG_GPS_LATITUDE);
+        String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
+        String lngValue = getAttribute(TAG_GPS_LONGITUDE);
+        String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF);
+
+        if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
+            try {
+                double latitude = convertRationalLatLonToDouble(latValue, latRef);
+                double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
+                return new double[] {latitude, longitude};
+            } catch (IllegalArgumentException e) {
+                logger.warn("Latitude/longitude values are not parsable. "
+                        + String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
+                        latValue, latRef, lngValue, lngRef));
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Sets the GPS-related information. It will set GPS processing method, latitude and longitude
+     * values, GPS timestamp, and speed information at the same time.
+     *
+     * This method is a No-Op if the location parameter is null.
+     *
+     * @param location the {@link Location} object returned by GPS service.
+     */
+    public void setGpsInfo(@Nullable Location location) {
+        if (location == null) {
+            return;
+        }
+        setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
+        setLatLong(location.getLatitude(), location.getLongitude());
+        setAltitude(location.getAltitude());
+        // Location objects store speeds in m/sec. Translates it to km/hr here.
+        setAttribute(TAG_GPS_SPEED_REF, "K");
+        setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
+                * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
+        String[] dateTime = sFormatterPrimary.format(
+                new Date(location.getTime())).split("\\s+", -1);
+        setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
+        setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
+    }
+
+    /**
+     * Sets the latitude and longitude values.
+     *
+     * @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
+     *                 90.0.
+     * @param longitude the decimal value of longitude. Must be a valid double value between -180.0
+     *                  and 180.0.
+     * @throws IllegalArgumentException If {@code latitude} or {@code longitude} is outside the
+     *                                  specified range.
+     */
+    public void setLatLong(double latitude, double longitude) {
+        if (latitude < -90.0 || latitude > 90.0 || Double.isNaN(latitude)) {
+            throw new IllegalArgumentException("Latitude value " + latitude + " is not valid.");
+        }
+        if (longitude < -180.0 || longitude > 180.0 || Double.isNaN(longitude)) {
+            throw new IllegalArgumentException("Longitude value " + longitude + " is not valid.");
+        }
+        setAttribute(TAG_GPS_LATITUDE_REF, latitude >= 0 ? "N" : "S");
+        setAttribute(TAG_GPS_LATITUDE, convertDecimalDegree(Math.abs(latitude)));
+        setAttribute(TAG_GPS_LONGITUDE_REF, longitude >= 0 ? "E" : "W");
+        setAttribute(TAG_GPS_LONGITUDE, convertDecimalDegree(Math.abs(longitude)));
+    }
+
+    /**
+     * Return the altitude in meters. If the exif tag does not exist, return
+     * <var>defaultValue</var>.
+     *
+     * @param defaultValue the value to return if the tag is not available.
+     */
+    public double getAltitude(double defaultValue) {
+        double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
+        int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
+
+        if (altitude >= 0 && ref >= 0) {
+            return (altitude * ((ref == 1) ? -1 : 1));
+        } else {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Sets the altitude in meters.
+     */
+    public void setAltitude(double altitude) {
+        String ref = altitude >= 0 ? "0" : "1";
+        setAttribute(TAG_GPS_ALTITUDE, new Rational(Math.abs(altitude)).toString());
+        setAttribute(TAG_GPS_ALTITUDE_REF, ref);
+    }
+
+    /**
+     * Set the date time value.
+     *
+     * @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void setDateTime(@NonNull Long timeStamp) {
+        if (timeStamp == null) {
+            throw new NullPointerException("Timestamp should not be null.");
+        }
+
+        if (timeStamp < 0) {
+            throw new IllegalArgumentException("Timestamp should a positive value.");
+        }
+
+        long subsec = timeStamp % 1000;
+        String subsecString = Long.toString(subsec);
+        for (int i = subsecString.length(); i < 3; i++) {
+            subsecString = "0" + subsecString;
+        }
+        setAttribute(TAG_DATETIME, sFormatterPrimary.format(new Date(timeStamp)));
+        setAttribute(TAG_SUBSEC_TIME, subsecString);
+    }
+
+    /**
+     * Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
+     * Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
+     *
+     * @return null if date time information is unavailable or invalid.
+     *
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Nullable
+    public Long getDateTime() {
+        return parseDateTime(getAttribute(TAG_DATETIME),
+                getAttribute(TAG_SUBSEC_TIME),
+                getAttribute(TAG_OFFSET_TIME));
+    }
+
+    /**
+     * Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
+     * milliseconds since Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
+     *
+     * @return null if digitized date time information is unavailable or invalid.
+     *
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Nullable
+    public Long getDateTimeDigitized() {
+        return parseDateTime(getAttribute(TAG_DATETIME_DIGITIZED),
+                getAttribute(TAG_SUBSEC_TIME_DIGITIZED),
+                getAttribute(TAG_OFFSET_TIME_DIGITIZED));
+    }
+
+    /**
+     * Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
+     * milliseconds since Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
+     *
+     * @return null if original date time information is unavailable or invalid.
+     *
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Nullable
+    public Long getDateTimeOriginal() {
+        return parseDateTime(getAttribute(TAG_DATETIME_ORIGINAL),
+                getAttribute(TAG_SUBSEC_TIME_ORIGINAL),
+                getAttribute(TAG_OFFSET_TIME_ORIGINAL));
+    }
+
+    private static Long parseDateTime(@Nullable String dateTimeString, @Nullable String subSecs,
+            @Nullable String offsetString) {
+        if (dateTimeString == null || !NON_ZERO_TIME_PATTERN.matcher(dateTimeString).matches()) {
+            return null;
+        }
+
+        ParsePosition pos = new ParsePosition(0);
+        try {
+            // The exif field is in local time. Parsing it as if it is UTC will yield time
+            // since 1/1/1970 local time
+            Date dateTime = sFormatterPrimary.parse(dateTimeString, pos);
+            if (dateTime == null) {
+                dateTime = sFormatterSecondary.parse(dateTimeString, pos);
+                if (dateTime == null) {
+                    return null;
+                }
+            }
+            long msecs = dateTime.getTime();
+            if (offsetString != null) {
+                String sign = offsetString.substring(0, 1);
+                int hour = Integer.parseInt(offsetString.substring(1, 3));
+                int min = Integer.parseInt(offsetString.substring(4, 6));
+                if (("+".equals(sign) || "-".equals(sign))
+                        && ":".equals(offsetString.substring(3, 4))
+                        && hour <= 14 /* max UTC hour value */) {
+                    msecs += (hour * 60 + min) * 60 * 1000 * ("-".equals(sign) ? 1 : -1);
+                }
+            }
+
+            if (subSecs != null) {
+                msecs += parseSubSeconds(subSecs);
+            }
+            return msecs;
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
+     * @return null if the date time information is not available.
+     */
+    @SuppressLint("AutoBoxing") /* Not a performance-critical call, thus not a big concern. */
+    @Nullable
+    public Long getGpsDateTime() {
+        String date = getAttribute(TAG_GPS_DATESTAMP);
+        String time = getAttribute(TAG_GPS_TIMESTAMP);
+        if (date == null || time == null
+                || (!NON_ZERO_TIME_PATTERN.matcher(date).matches()
+                && !NON_ZERO_TIME_PATTERN.matcher(time).matches())) {
+            return null;
+        }
+
+        String dateTimeString = date + ' ' + time;
+
+        ParsePosition pos = new ParsePosition(0);
+        try {
+            Date dateTime = sFormatterPrimary.parse(dateTimeString, pos);
+            if (dateTime == null) {
+                dateTime = sFormatterSecondary.parse(dateTimeString, pos);
+                if (dateTime == null) {
+                    return null;
+                }
+            }
+            return dateTime.getTime();
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    private void initForFilename(String filename) throws IOException {
+        if (filename == null) {
+            throw new NullPointerException("filename cannot be null");
+        }
+        FileInputStream in = null;
+        mAssetInputStream = null;
+        mFilename = filename;
+        try {
+            in = new FileInputStream(filename);
+            if (isSeekableFD(in.getFD())) {
+                mSeekableFileDescriptor = in.getFD();
+            } else {
+                mSeekableFileDescriptor = null;
+            }
+            loadAttributes(in);
+        } finally {
+            closeQuietly(in);
+        }
+    }
+
+    private static double convertRationalLatLonToDouble(String rationalString, String ref) {
+        try {
+            String [] parts = rationalString.split(",", -1);
+
+            String [] pair;
+            pair = parts[0].split("/", -1);
+            double degrees = Double.parseDouble(pair[0].trim())
+                    / Double.parseDouble(pair[1].trim());
+
+            pair = parts[1].split("/", -1);
+            double minutes = Double.parseDouble(pair[0].trim())
+                    / Double.parseDouble(pair[1].trim());
+
+            pair = parts[2].split("/", -1);
+            double seconds = Double.parseDouble(pair[0].trim())
+                    / Double.parseDouble(pair[1].trim());
+
+            double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
+            if ((ref.equals("S") || ref.equals("W"))) {
+                return -result;
+            } else if (ref.equals("N") || ref.equals("E")) {
+                return result;
+            } else {
+                // Not valid
+                throw new IllegalArgumentException();
+            }
+        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+            // Not valid
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private String convertDecimalDegree(double decimalDegree) {
+        long degrees = (long) decimalDegree;
+        long minutes = (long) ((decimalDegree - degrees) * 60.0);
+        long seconds = Math.round((decimalDegree - degrees - minutes / 60.0) * 3600.0 * 1e7);
+        return degrees + "/1," + minutes + "/1," + seconds + "/10000000";
+    }
+
+    // Checks the type of image file
+    private int getMimeType(BufferedInputStream in) throws IOException {
+        in.mark(SIGNATURE_CHECK_SIZE);
+        byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE];
+        in.read(signatureCheckBytes);
+        in.reset();
+        if (isJpegFormat(signatureCheckBytes)) {
+            return IMAGE_TYPE_JPEG;
+        } else if (isRafFormat(signatureCheckBytes)) {
+            return IMAGE_TYPE_RAF;
+        } else if (isHeifFormat(signatureCheckBytes)) {
+            return IMAGE_TYPE_HEIF;
+        } else if (isOrfFormat(signatureCheckBytes)) {
+            return IMAGE_TYPE_ORF;
+        } else if (isRw2Format(signatureCheckBytes)) {
+            return IMAGE_TYPE_RW2;
+        } else if (isPngFormat(signatureCheckBytes)) {
+            return IMAGE_TYPE_PNG;
+        } else if (isWebpFormat(signatureCheckBytes)) {
+            return IMAGE_TYPE_WEBP;
+        }
+        // Certain file formats (PEF) are identified in readImageFileDirectory()
+        return IMAGE_TYPE_UNKNOWN;
+    }
+
+    /**
+     * This method looks at the first 3 bytes to determine if this file is a JPEG file.
+     * See http://www.media.mit.edu/pia/Research/deepview/exif.html, "JPEG format and Marker"
+     */
+	// Threema-modified: make public
+    public static boolean isJpegFormat(byte[] signatureCheckBytes) throws IOException {
+        for (int i = 0; i < JPEG_SIGNATURE.length; i++) {
+            if (signatureCheckBytes[i] != JPEG_SIGNATURE[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * This method looks at the first 15 bytes to determine if this file is a RAF file.
+     * There is no official specification for RAF files from Fuji, but there is an online archive of
+     * image file specifications:
+     * http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
+     */
+    private boolean isRafFormat(byte[] signatureCheckBytes) throws IOException {
+        byte[] rafSignatureBytes = RAF_SIGNATURE.getBytes(Charset.defaultCharset());
+        for (int i = 0; i < rafSignatureBytes.length; i++) {
+            if (signatureCheckBytes[i] != rafSignatureBytes[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
+        ByteOrderedDataInputStream signatureInputStream = null;
+        try {
+            signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
+
+            long chunkSize = signatureInputStream.readInt();
+            byte[] chunkType = new byte[4];
+            signatureInputStream.readFully(chunkType);
+
+            if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) {
+                return false;
+            }
+
+            long chunkDataOffset = 8;
+            if (chunkSize == 1) {
+                // This indicates that the next 8 bytes represent the chunk size,
+                // and chunk data comes after that.
+                chunkSize = signatureInputStream.readLong();
+                if (chunkSize < 16) {
+                    // The smallest valid chunk is 16 bytes long in this case.
+                    return false;
+                }
+                chunkDataOffset += 8;
+            }
+
+            // only sniff up to signatureCheckBytes.length
+            if (chunkSize > signatureCheckBytes.length) {
+                chunkSize = signatureCheckBytes.length;
+            }
+
+            long chunkDataSize = chunkSize - chunkDataOffset;
+
+            // It should at least have major brand (4-byte) and minor version (4-byte).
+            // The rest of the chunk (if any) is a list of (4-byte) compatible brands.
+            if (chunkDataSize < 8) {
+                return false;
+            }
+
+            byte[] brand = new byte[4];
+            boolean isMif1 = false;
+            boolean isHeic = false;
+            for (long i = 0; i < chunkDataSize / 4;  ++i) {
+                try {
+                    signatureInputStream.readFully(brand);
+                } catch (EOFException e) {
+                    return false;
+                }
+                if (i == 1) {
+                    // Skip this index, it refers to the minorVersion, not a brand.
+                    continue;
+                }
+                if (Arrays.equals(brand, HEIF_BRAND_MIF1)) {
+                    isMif1 = true;
+                } else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) {
+                    isHeic = true;
+                }
+                if (isMif1 && isHeic) {
+                    return true;
+                }
+            }
+        } catch (Exception e) {
+            if (DEBUG) {
+                logger.debug("Exception parsing HEIF file type box.", e);
+            }
+        } finally {
+            if (signatureInputStream != null) {
+                signatureInputStream.close();
+                signatureInputStream = null;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * ORF has a similar structure to TIFF but it contains a different signature at the TIFF Header.
+     * This method looks at the 2 bytes following the Byte Order bytes to determine if this file is
+     * an ORF file.
+     * There is no official specification for ORF files from Olympus, but there is an online archive
+     * of image file specifications:
+     * http://fileformats.archiveteam.org/wiki/Olympus_ORF
+     */
+    private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException {
+        ByteOrderedDataInputStream signatureInputStream = null;
+
+        try {
+            signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
+
+            // Read byte order
+            mExifByteOrder = readByteOrder(signatureInputStream);
+            // Set byte order
+            signatureInputStream.setByteOrder(mExifByteOrder);
+
+            short orfSignature = signatureInputStream.readShort();
+            return orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2;
+        } catch (Exception e) {
+            // Do nothing
+        } finally {
+            if (signatureInputStream != null) {
+                signatureInputStream.close();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * RW2 is TIFF-based, but stores 0x55 signature byte instead of 0x42 at the header
+     * See http://lclevy.free.fr/raw/
+     */
+    private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException {
+        ByteOrderedDataInputStream signatureInputStream = null;
+
+        try {
+            signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
+
+            // Read byte order
+            mExifByteOrder = readByteOrder(signatureInputStream);
+            // Set byte order
+            signatureInputStream.setByteOrder(mExifByteOrder);
+
+            short signatureByte = signatureInputStream.readShort();
+            return signatureByte == RW2_SIGNATURE;
+        } catch (Exception e) {
+            // Do nothing
+        } finally {
+            if (signatureInputStream != null) {
+                signatureInputStream.close();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * PNG's file signature is first 8 bytes.
+     * See PNG (Portable Network Graphics) Specification, Version 1.2, 3.1. PNG file signature
+     */
+    private boolean isPngFormat(byte[] signatureCheckBytes) throws IOException {
+        for (int i = 0; i < PNG_SIGNATURE.length; i++) {
+            if (signatureCheckBytes[i] != PNG_SIGNATURE[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * WebP's file signature is composed of 12 bytes:
+     *   'RIFF' (4 bytes) + file length value (4 bytes) + 'WEBP' (4 bytes)
+     * See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
+     */
+    private boolean isWebpFormat(byte[] signatureCheckBytes) throws IOException {
+        for (int i = 0; i < WEBP_SIGNATURE_1.length; i++) {
+            if (signatureCheckBytes[i] != WEBP_SIGNATURE_1[i]) {
+                return false;
+            }
+        }
+        for (int i = 0; i < WEBP_SIGNATURE_2.length; i++) {
+            if (signatureCheckBytes[i + WEBP_SIGNATURE_1.length + WEBP_FILE_SIZE_BYTE_LENGTH]
+                    != WEBP_SIGNATURE_2[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Loads EXIF attributes from a JPEG input stream.
+     *
+     * @param in The input stream that starts with the JPEG data.
+     * @param offsetToJpeg The offset to JPEG data for the original input stream.
+     * @param imageType The image type from which to retrieve metadata. Use IFD_TYPE_PRIMARY for
+     *                   primary image, IFD_TYPE_PREVIEW for preview image, and
+     *                   IFD_TYPE_THUMBNAIL for thumbnail image.
+     * @throws IOException If the data contains invalid JPEG markers, offsets, or length values.
+     */
+    private void getJpegAttributes(ByteOrderedDataInputStream in, int offsetToJpeg, int imageType)
+            throws IOException {
+        // See JPEG File Interchange Format Specification, "JFIF Specification"
+        if (DEBUG) {
+            logger.debug("getJpegAttributes starting with: " + in);
+        }
+        // JPEG uses Big Endian by default. See https://people.cs.umass.edu/~verts/cs32/endian.html
+        in.setByteOrder(BIG_ENDIAN);
+
+        int bytesRead = 0;
+
+        byte marker;
+        if ((marker = in.readByte()) != MARKER) {
+            throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+        }
+        ++bytesRead;
+        if (in.readByte() != MARKER_SOI) {
+            throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+        }
+        ++bytesRead;
+        while (true) {
+            marker = in.readByte();
+            if (marker != MARKER) {
+                throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
+            }
+            ++bytesRead;
+            marker = in.readByte();
+            if (DEBUG) {
+                logger.debug("Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
+            }
+            ++bytesRead;
+
+            // EOI indicates the end of an image and in case of SOS, JPEG image stream starts and
+            // the image data will terminate right after.
+            if (marker == MARKER_EOI || marker == MARKER_SOS) {
+                break;
+            }
+            int length = in.readUnsignedShort() - 2;
+            bytesRead += 2;
+            if (DEBUG) {
+                logger.debug("JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: "
+                        + (length + 2) + ")");
+            }
+            if (length < 0) {
+                throw new IOException("Invalid length");
+            }
+            switch (marker) {
+                case MARKER_APP1: {
+                    final int start = bytesRead;
+                    final byte[] bytes = new byte[length];
+                    in.readFully(bytes);
+                    bytesRead += length;
+                    length = 0;
+
+                    if (startsWith(bytes, IDENTIFIER_EXIF_APP1)) {
+                        final byte[] value = Arrays.copyOfRange(bytes, IDENTIFIER_EXIF_APP1.length,
+                                bytes.length);
+                        // Save offset to EXIF data for handling thumbnail and attribute offsets.
+                        mOffsetToExifData = offsetToJpeg
+                                + /* offset to EXIF from JPEG start */ start
+                                + IDENTIFIER_EXIF_APP1.length;
+                        readExifSegment(value, imageType);
+
+                        setThumbnailData(new ByteOrderedDataInputStream(value));
+                    } else if (startsWith(bytes, IDENTIFIER_XMP_APP1)) {
+                        // See XMP Specification Part 3: Storage in Files, 1.1.3 JPEG, Table 6
+                        final int offset = start + IDENTIFIER_XMP_APP1.length;
+                        final byte[] value = Arrays.copyOfRange(bytes,
+                                IDENTIFIER_XMP_APP1.length, bytes.length);
+                        // TODO: check if ignoring separate XMP data when tag 700 already exists is
+                        //  valid.
+                        if (getAttribute(TAG_XMP) == null) {
+                            mAttributes[IFD_TYPE_PRIMARY].put(TAG_XMP, new ExifAttribute(
+                                    IFD_FORMAT_BYTE, value.length, offset, value));
+                            mXmpIsFromSeparateMarker = true;
+                        }
+                    }
+                    break;
+                }
+
+                case MARKER_COM: {
+                    byte[] bytes = new byte[length];
+                    in.readFully(bytes);
+                    length = 0;
+                    if (getAttribute(TAG_USER_COMMENT) == null) {
+                        mAttributes[IFD_TYPE_EXIF].put(TAG_USER_COMMENT, ExifAttribute.createString(
+                                new String(bytes, ASCII)));
+                    }
+                    break;
+                }
+
+                case MARKER_SOF0:
+                case MARKER_SOF1:
+                case MARKER_SOF2:
+                case MARKER_SOF3:
+                case MARKER_SOF5:
+                case MARKER_SOF6:
+                case MARKER_SOF7:
+                case MARKER_SOF9:
+                case MARKER_SOF10:
+                case MARKER_SOF11:
+                case MARKER_SOF13:
+                case MARKER_SOF14:
+                case MARKER_SOF15: {
+                    in.skipFully(1);
+                    mAttributes[imageType].put(imageType != IFD_TYPE_THUMBNAIL
+                                    ? TAG_IMAGE_LENGTH : TAG_THUMBNAIL_IMAGE_LENGTH,
+                            ExifAttribute.createULong(in.readUnsignedShort(), mExifByteOrder));
+                    mAttributes[imageType].put(imageType != IFD_TYPE_THUMBNAIL
+                                    ? TAG_IMAGE_WIDTH : TAG_THUMBNAIL_IMAGE_WIDTH,
+                            ExifAttribute.createULong(in.readUnsignedShort(), mExifByteOrder));
+                    length -= 5;
+                    break;
+                }
+
+                default: {
+                    break;
+                }
+            }
+            if (length < 0) {
+                throw new IOException("Invalid length");
+            }
+            in.skipFully(length);
+            bytesRead += length;
+        }
+        // Restore original byte order
+        in.setByteOrder(mExifByteOrder);
+    }
+
+    private void getRawAttributes(SeekableByteOrderedDataInputStream in) throws IOException {
+        // Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
+        parseTiffHeaders(in);
+
+        // Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
+        readImageFileDirectory(in, IFD_TYPE_PRIMARY);
+
+        // Update ImageLength/Width tags for all image data.
+        updateImageSizeValues(in, IFD_TYPE_PRIMARY);
+        updateImageSizeValues(in, IFD_TYPE_PREVIEW);
+        updateImageSizeValues(in, IFD_TYPE_THUMBNAIL);
+
+        // Check if each image data is in valid position.
+        validateImages();
+
+        if (mMimeType == IMAGE_TYPE_PEF) {
+            // PEF files contain a MakerNote data, which contains the data for ColorSpace tag.
+            // See http://lclevy.free.fr/raw/ and piex.cc PefGetPreviewData()
+            ExifAttribute makerNoteAttribute =
+                    mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
+            if (makerNoteAttribute != null) {
+                // Create an ordered DataInputStream for MakerNote
+								// MODIFIED BY THREEMA
+				try (SeekableByteOrderedDataInputStream makerNoteDataInputStream =
+                        new SeekableByteOrderedDataInputStream(makerNoteAttribute.bytes)) {
+					makerNoteDataInputStream.setByteOrder(mExifByteOrder);
 
-	// F. Tags related to date and time
-	/**
-	 *  <p>The date and time when the original image data was generated. For a DSC the date and time
-	 *  the picture was taken are recorded. The format is "YYYY:MM:DD HH:MM:SS" with time shown in
-	 *  24-hour format, and the date and time separated by one blank character ({@code 0x20}).
-	 *  When the date and time are unknown, all the character spaces except colons (":") should be
-	 *  filled with blank characters, or else the Interoperability field should be filled with blank
-	 *  characters. When the field is left blank, it is treated as unknown.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 36867</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 19</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
-	/**
-	 *  <p>The date and time when the image was stored as digital data. If, for example, an image
-	 *  was captured by DSC and at the same time the file was recorded, then
-	 *  {@link #TAG_DATETIME_ORIGINAL} and this tag will have the same contents. The format is
-	 *  "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and the date and time separated by
-	 *  one blank character ({@code 0x20}). When the date and time are unknown, all the character
-	 *  spaces except colons (":")should be filled with blank characters, or else
-	 *  the Interoperability field should be filled with blank characters. When the field is left
-	 *  blank, it is treated as unknown.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 36868</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 19</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
-	/**
-	 *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37520</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SUBSEC_TIME = "SubSecTime";
-	/**
-	 *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME_ORIGINAL}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37521</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SUBSEC_TIME_ORIGINAL = "SubSecTimeOriginal";
-	/**
-	 *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME_DIGITIZED}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37522</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SUBSEC_TIME_DIGITIZED = "SubSecTimeDigitized";
+					// Skip to MakerNote data
+					makerNoteDataInputStream.skipFully(PEF_MAKER_NOTE_SKIP_SIZE);
 
-	// G. Tags related to picture-taking condition
-	/**
-	 *  <p>Exposure time, given in seconds.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 33434</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_EXPOSURE_TIME = "ExposureTime";
-	/**
-	 *  <p>The F number.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 33437</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_F_NUMBER = "FNumber";
-	/**
-	 *  <p>TThe class of the program used by the camera to set exposure when the picture is taken.
-	 *  The tag values are as follows.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34850</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #EXPOSURE_PROGRAM_NOT_DEFINED}</li>
-	 *  </ul>
-	 *
-	 *  @see #EXPOSURE_PROGRAM_NOT_DEFINED
-	 *  @see #EXPOSURE_PROGRAM_MANUAL
-	 *  @see #EXPOSURE_PROGRAM_NORMAL
-	 *  @see #EXPOSURE_PROGRAM_APERTURE_PRIORITY
-	 *  @see #EXPOSURE_PROGRAM_SHUTTER_PRIORITY
-	 *  @see #EXPOSURE_PROGRAM_CREATIVE
-	 *  @see #EXPOSURE_PROGRAM_ACTION
-	 *  @see #EXPOSURE_PROGRAM_PORTRAIT_MODE
-	 *  @see #EXPOSURE_PROGRAM_LANDSCAPE_MODE
-	 */
-	public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
-	/**
-	 *  <p>Indicates the spectral sensitivity of each channel of the camera used. The tag value is
-	 *  an ASCII string compatible with the standard developed by the ASTM Technical committee.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34852</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity";
-	/**
-	 *  @deprecated Use {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} instead.
-	 *  @see #TAG_PHOTOGRAPHIC_SENSITIVITY
-	 */
-	@Deprecated public static final String TAG_ISO_SPEED_RATINGS = "ISOSpeedRatings";
-	/**
-	 *  <p>This tag indicates the sensitivity of the camera or input device when the image was shot.
-	 *  More specifically, it indicates one of the following values that are parameters defined in
-	 *  ISO 12232: standard output sensitivity (SOS), recommended exposure index (REI), or ISO
-	 *  speed. Accordingly, if a tag corresponding to a parameter that is designated by
-	 *  {@link #TAG_SENSITIVITY_TYPE} is recorded, the values of the tag and of this tag are
-	 *  the same. However, if the value is 65535 or higher, the value of this tag shall be 65535.
-	 *  When recording this tag, {@link #TAG_SENSITIVITY_TYPE} should also be recorded. In addition,
-	 *  while “Count = Any”, only 1 count should be used when recording this tag.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34855</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = Any</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_PHOTOGRAPHIC_SENSITIVITY = "PhotographicSensitivity";
-	/**
-	 *  <p>Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. OECF is
-	 *  the relationship between the camera optical input and the image values.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34856</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_OECF = "OECF";
-	/**
-	 *  <p>This tag indicates which one of the parameters of ISO12232 is
-	 *  {@link #TAG_PHOTOGRAPHIC_SENSITIVITY}. Although it is an optional tag, it should be recorded
-	 *  when {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} is recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34864</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #SENSITIVITY_TYPE_UNKNOWN
-	 *  @see #SENSITIVITY_TYPE_SOS
-	 *  @see #SENSITIVITY_TYPE_REI
-	 *  @see #SENSITIVITY_TYPE_ISO_SPEED
-	 *  @see #SENSITIVITY_TYPE_SOS_AND_REI
-	 *  @see #SENSITIVITY_TYPE_SOS_AND_ISO
-	 *  @see #SENSITIVITY_TYPE_REI_AND_ISO
-	 *  @see #SENSITIVITY_TYPE_SOS_AND_REI_AND_ISO
-	 */
-	public static final String TAG_SENSITIVITY_TYPE = "SensitivityType";
-	/**
-	 *  <p>This tag indicates the standard output sensitivity value of a camera or input device
-	 *  defined in ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
-	 *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34865</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_STANDARD_OUTPUT_SENSITIVITY = "StandardOutputSensitivity";
-	/**
-	 *  <p>This tag indicates the recommended exposure index value of a camera or input device
-	 *  defined in ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
-	 *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34866</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_RECOMMENDED_EXPOSURE_INDEX = "RecommendedExposureIndex";
-	/**
-	 *  <p>This tag indicates the ISO speed value of a camera or input device that is defined in
-	 *  ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
-	 *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34867</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_ISO_SPEED = "ISOSpeed";
-	/**
-	 *  <p>This tag indicates the ISO speed latitude yyy value of a camera or input device that is
-	 *  defined in ISO 12232. However, this tag shall not be recorded without {@link #TAG_ISO_SPEED}
-	 *  and {@link #TAG_ISO_SPEED_LATITUDE_ZZZ}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34868</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_ISO_SPEED_LATITUDE_YYY = "ISOSpeedLatitudeyyy";
-	/**
-	 *  <p>This tag indicates the ISO speed latitude zzz value of a camera or input device that is
-	 *  defined in ISO 12232. However, this tag shall not be recorded without {@link #TAG_ISO_SPEED}
-	 *  and {@link #TAG_ISO_SPEED_LATITUDE_YYY}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 34869</li>
-	 *      <li>Type = Unsigned long</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_ISO_SPEED_LATITUDE_ZZZ = "ISOSpeedLatitudezzz";
-	/**
-	 *  <p>Shutter speed. The unit is the APEX setting.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37377</li>
-	 *      <li>Type = Signed rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue";
-	/**
-	 *  <p>The lens aperture. The unit is the APEX value.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37378</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_APERTURE_VALUE = "ApertureValue";
-	/**
-	 *  <p>The value of brightness. The unit is the APEX value. Ordinarily it is given in the range
-	 *  of -99.99 to 99.99. Note that if the numerator of the recorded value is 0xFFFFFFFF,
-	 *  Unknown shall be indicated.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37379</li>
-	 *      <li>Type = Signed rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue";
-	/**
-	 *  <p>The exposure bias. The unit is the APEX value. Ordinarily it is given in the range of
-	 *  -99.99 to 99.99.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37380</li>
-	 *      <li>Type = Signed rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
-	/**
-	 *  <p>The smallest F number of the lens. The unit is the APEX value. Ordinarily it is given
-	 *  in the range of 00.00 to 99.99, but it is not limited to this range.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37381</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue";
-	/**
-	 *  <p>The distance to the subject, given in meters. Note that if the numerator of the recorded
-	 *  value is 0xFFFFFFFF, Infinity shall be indicated; and if the numerator is 0, Distance
-	 *  unknown shall be indicated.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37382</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
-	/**
-	 *  <p>The metering mode.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37383</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #METERING_MODE_UNKNOWN}</li>
-	 *  </ul>
-	 *
-	 *  @see #METERING_MODE_UNKNOWN
-	 *  @see #METERING_MODE_AVERAGE
-	 *  @see #METERING_MODE_CENTER_WEIGHT_AVERAGE
-	 *  @see #METERING_MODE_SPOT
-	 *  @see #METERING_MODE_MULTI_SPOT
-	 *  @see #METERING_MODE_PATTERN
-	 *  @see #METERING_MODE_PARTIAL
-	 *  @see #METERING_MODE_OTHER
-	 */
-	public static final String TAG_METERING_MODE = "MeteringMode";
-	/**
-	 *  <p>The kind of light source.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37384</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #LIGHT_SOURCE_UNKNOWN}</li>
-	 *  </ul>
-	 *
-	 *  @see #LIGHT_SOURCE_UNKNOWN
-	 *  @see #LIGHT_SOURCE_DAYLIGHT
-	 *  @see #LIGHT_SOURCE_FLUORESCENT
-	 *  @see #LIGHT_SOURCE_TUNGSTEN
-	 *  @see #LIGHT_SOURCE_FLASH
-	 *  @see #LIGHT_SOURCE_FINE_WEATHER
-	 *  @see #LIGHT_SOURCE_CLOUDY_WEATHER
-	 *  @see #LIGHT_SOURCE_SHADE
-	 *  @see #LIGHT_SOURCE_DAYLIGHT_FLUORESCENT
-	 *  @see #LIGHT_SOURCE_DAY_WHITE_FLUORESCENT
-	 *  @see #LIGHT_SOURCE_COOL_WHITE_FLUORESCENT
-	 *  @see #LIGHT_SOURCE_WHITE_FLUORESCENT
-	 *  @see #LIGHT_SOURCE_WARM_WHITE_FLUORESCENT
-	 *  @see #LIGHT_SOURCE_STANDARD_LIGHT_A
-	 *  @see #LIGHT_SOURCE_STANDARD_LIGHT_B
-	 *  @see #LIGHT_SOURCE_STANDARD_LIGHT_C
-	 *  @see #LIGHT_SOURCE_D55
-	 *  @see #LIGHT_SOURCE_D65
-	 *  @see #LIGHT_SOURCE_D75
-	 *  @see #LIGHT_SOURCE_D50
-	 *  @see #LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN
-	 *  @see #LIGHT_SOURCE_OTHER
-	 */
-	public static final String TAG_LIGHT_SOURCE = "LightSource";
-	/**
-	 *  <p>This tag indicates the status of flash when the image was shot. Bit 0 indicates the flash
-	 *  firing status, bits 1 and 2 indicate the flash return status, bits 3 and 4 indicate
-	 *  the flash mode, bit 5 indicates whether the flash function is present, and bit 6 indicates
-	 *  "red eye" mode.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37385</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *  </ul>
-	 *
-	 *  @see #FLAG_FLASH_FIRED
-	 *  @see #FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED
-	 *  @see #FLAG_FLASH_RETURN_LIGHT_DETECTED
-	 *  @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
-	 *  @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
-	 *  @see #FLAG_FLASH_MODE_AUTO
-	 *  @see #FLAG_FLASH_NO_FLASH_FUNCTION
-	 *  @see #FLAG_FLASH_RED_EYE_SUPPORTED
-	 */
-	public static final String TAG_FLASH = "Flash";
-	/**
-	 *  <p>This tag indicates the location and area of the main subject in the overall scene.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37396</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 2 or 3 or 4</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  <p>The subject location and area are defined by Count values as follows.</p>
-	 *
-	 *  <ul>
-	 *      <li>Count = 2 Indicates the location of the main subject as coordinates. The first value
-	 *                    is the X coordinate and the second is the Y coordinate.</li>
-	 *      <li>Count = 3 The area of the main subject is given as a circle. The circular area is
-	 *                    expressed as center coordinates and diameter. The first value is
-	 *                    the center X coordinate, the second is the center Y coordinate, and
-	 *                    the third is the diameter.</li>
-	 *      <li>Count = 4 The area of the main subject is given as a rectangle. The rectangular
-	 *                    area is expressed as center coordinates and area dimensions. The first
-	 *                    value is the center X coordinate, the second is the center Y coordinate,
-	 *                    the third is the width of the area, and the fourth is the height of
-	 *                    the area.</li>
-	 *  </ul>
-	 *
-	 *  <p>Note that the coordinate values, width, and height are expressed in relation to the upper
-	 *  left as origin, prior to rotation processing as per {@link #TAG_ORIENTATION}.</p>
-	 */
-	public static final String TAG_SUBJECT_AREA = "SubjectArea";
-	/**
-	 *  <p>The actual focal length of the lens, in mm. Conversion is not made to the focal length
-	 *  of a 35mm film camera.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 37386</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_FOCAL_LENGTH = "FocalLength";
-	/**
-	 *  <p>Indicates the strobe energy at the time the image is captured, as measured in Beam Candle
-	 *  Power Seconds (BCPS).</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41483</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_FLASH_ENERGY = "FlashEnergy";
-	/**
-	 *  <p>This tag records the camera or input device spatial frequency table and SFR values in
-	 *  the direction of image width, image height, and diagonal direction, as specified in
-	 *  ISO 12233.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41484</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse";
-	/**
-	 *  <p>Indicates the number of pixels in the image width (X) direction per
-	 *  {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41486</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution";
-	/**
-	 *  <p>Indicates the number of pixels in the image height (Y) direction per
-	 *  {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41487</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution";
-	/**
-	 *  <p>Indicates the unit for measuring {@link #TAG_FOCAL_PLANE_X_RESOLUTION} and
-	 *  {@link #TAG_FOCAL_PLANE_Y_RESOLUTION}. This value is the same as
-	 *  {@link #TAG_RESOLUTION_UNIT}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41488</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
-	 *  </ul>
-	 *
-	 *  @see #TAG_RESOLUTION_UNIT
-	 *  @see #RESOLUTION_UNIT_INCHES
-	 *  @see #RESOLUTION_UNIT_CENTIMETERS
-	 */
-	public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit";
-	/**
-	 *  <p>Indicates the location of the main subject in the scene. The value of this tag represents
-	 *  the pixel at the center of the main subject relative to the left edge, prior to rotation
-	 *  processing as per {@link #TAG_ORIENTATION}. The first value indicates the X column number
-	 *  and second indicates the Y row number. When a camera records the main subject location,
-	 *  it is recommended that {@link #TAG_SUBJECT_AREA} be used instead of this tag.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41492</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 2</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_SUBJECT_LOCATION = "SubjectLocation";
-	/**
-	 *  <p>Indicates the exposure index selected on the camera or input device at the time the image
-	 *  is captured.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41493</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_EXPOSURE_INDEX = "ExposureIndex";
-	/**
-	 *  <p>Indicates the image sensor type on the camera or input device.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41495</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #SENSOR_TYPE_NOT_DEFINED
-	 *  @see #SENSOR_TYPE_ONE_CHIP
-	 *  @see #SENSOR_TYPE_TWO_CHIP
-	 *  @see #SENSOR_TYPE_THREE_CHIP
-	 *  @see #SENSOR_TYPE_COLOR_SEQUENTIAL
-	 *  @see #SENSOR_TYPE_TRILINEAR
-	 *  @see #SENSOR_TYPE_COLOR_SEQUENTIAL_LINEAR
-	 */
-	public static final String TAG_SENSING_METHOD = "SensingMethod";
-	/**
-	 *  <p>Indicates the image source. If a DSC recorded the image, this tag value always shall
-	 *  be set to {@link #FILE_SOURCE_DSC}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41728</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = {@link #FILE_SOURCE_DSC}</li>
-	 *  </ul>
-	 *
-	 *  @see #FILE_SOURCE_OTHER
-	 *  @see #FILE_SOURCE_TRANSPARENT_SCANNER
-	 *  @see #FILE_SOURCE_REFLEX_SCANNER
-	 *  @see #FILE_SOURCE_DSC
-	 */
-	public static final String TAG_FILE_SOURCE = "FileSource";
-	/**
-	 *  <p>Indicates the type of scene. If a DSC recorded the image, this tag value shall always
-	 *  be set to {@link #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41729</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = 1</li>
-	 *  </ul>
-	 *
-	 *  @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED
-	 */
-	public static final String TAG_SCENE_TYPE = "SceneType";
-	/**
-	 *  <p>Indicates the color filter array (CFA) geometric pattern of the image sensor when
-	 *  a one-chip color area sensor is used. It does not apply to all sensing methods.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41730</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #TAG_SENSING_METHOD
-	 *  @see #SENSOR_TYPE_ONE_CHIP
-	 */
-	public static final String TAG_CFA_PATTERN = "CFAPattern";
-	/**
-	 *  <p>This tag indicates the use of special processing on image data, such as rendering geared
-	 *  to output. When special processing is performed, the Exif/DCF reader is expected to disable
-	 *  or minimize any further processing.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41985</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #RENDERED_PROCESS_NORMAL}</li>
-	 *  </ul>
-	 *
-	 *  @see #RENDERED_PROCESS_NORMAL
-	 *  @see #RENDERED_PROCESS_CUSTOM
-	 */
-	public static final String TAG_CUSTOM_RENDERED = "CustomRendered";
-	/**
-	 *  <p>This tag indicates the exposure mode set when the image was shot.
-	 *  In {@link #EXPOSURE_MODE_AUTO_BRACKET}, the camera shoots a series of frames of the same
-	 *  scene at different exposure settings.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41986</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #EXPOSURE_MODE_AUTO
-	 *  @see #EXPOSURE_MODE_MANUAL
-	 *  @see #EXPOSURE_MODE_AUTO_BRACKET
-	 */
-	public static final String TAG_EXPOSURE_MODE = "ExposureMode";
-	/**
-	 *  <p>This tag indicates the white balance mode set when the image was shot.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41987</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #WHITEBALANCE_AUTO
-	 *  @see #WHITEBALANCE_MANUAL
-	 */
-	public static final String TAG_WHITE_BALANCE = "WhiteBalance";
-	/**
-	 *  <p>This tag indicates the digital zoom ratio when the image was shot. If the numerator of
-	 *  the recorded value is 0, this indicates that digital zoom was not used.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41988</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
-	/**
-	 *  <p>This tag indicates the equivalent focal length assuming a 35mm film camera, in mm.
-	 *  A value of 0 means the focal length is unknown. Note that this tag differs from
-	 *  {@link #TAG_FOCAL_LENGTH}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41989</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm";
-	/**
-	 *  <p>This tag indicates the type of scene that was shot. It may also be used to record
-	 *  the mode in which the image was shot. Note that this differs from
-	 *  {@link #TAG_SCENE_TYPE}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41990</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = 0</li>
-	 *  </ul>
-	 *
-	 *  @see #SCENE_CAPTURE_TYPE_STANDARD
-	 *  @see #SCENE_CAPTURE_TYPE_LANDSCAPE
-	 *  @see #SCENE_CAPTURE_TYPE_PORTRAIT
-	 *  @see #SCENE_CAPTURE_TYPE_NIGHT
-	 */
-	public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType";
-	/**
-	 *  <p>This tag indicates the degree of overall image gain adjustment.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41991</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #GAIN_CONTROL_NONE
-	 *  @see #GAIN_CONTROL_LOW_GAIN_UP
-	 *  @see #GAIN_CONTROL_HIGH_GAIN_UP
-	 *  @see #GAIN_CONTROL_LOW_GAIN_DOWN
-	 *  @see #GAIN_CONTROL_HIGH_GAIN_DOWN
-	 */
-	public static final String TAG_GAIN_CONTROL = "GainControl";
-	/**
-	 *  <p>This tag indicates the direction of contrast processing applied by the camera when
-	 *  the image was shot.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41992</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #CONTRAST_NORMAL}</li>
-	 *  </ul>
-	 *
-	 *  @see #CONTRAST_NORMAL
-	 *  @see #CONTRAST_SOFT
-	 *  @see #CONTRAST_HARD
-	 */
-	public static final String TAG_CONTRAST = "Contrast";
-	/**
-	 *  <p>This tag indicates the direction of saturation processing applied by the camera when
-	 *  the image was shot.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41993</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #SATURATION_NORMAL}</li>
-	 *  </ul>
-	 *
-	 *  @see #SATURATION_NORMAL
-	 *  @see #SATURATION_LOW
-	 *  @see #SATURATION_HIGH
-	 */
-	public static final String TAG_SATURATION = "Saturation";
-	/**
-	 *  <p>This tag indicates the direction of sharpness processing applied by the camera when
-	 *  the image was shot.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41994</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = {@link #SHARPNESS_NORMAL}</li>
-	 *  </ul>
-	 *
-	 *  @see #SHARPNESS_NORMAL
-	 *  @see #SHARPNESS_SOFT
-	 *  @see #SHARPNESS_HARD
-	 */
-	public static final String TAG_SHARPNESS = "Sharpness";
-	/**
-	 *  <p>This tag indicates information on the picture-taking conditions of a particular camera
-	 *  model. The tag is used only to indicate the picture-taking conditions in the Exif/DCF
-	 *  reader.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41995</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription";
-	/**
-	 *  <p>This tag indicates the distance to the subject.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 41996</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #SUBJECT_DISTANCE_RANGE_UNKNOWN
-	 *  @see #SUBJECT_DISTANCE_RANGE_MACRO
-	 *  @see #SUBJECT_DISTANCE_RANGE_CLOSE_VIEW
-	 *  @see #SUBJECT_DISTANCE_RANGE_DISTANT_VIEW
-	 */
-	public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange";
+					// Read IFD data from MakerNote
+					readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_PEF);
 
-	// H. Other tags
-	/**
-	 *  <p>This tag indicates an identifier assigned uniquely to each image. It is recorded as
-	 *  an ASCII string equivalent to hexadecimal notation and 128-bit fixed length.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42016</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 32</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID";
-	/**
-	 *  <p>This tag records the owner of a camera used in photography as an ASCII string.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42032</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_CAMARA_OWNER_NAME = "CameraOwnerName";
-	/**
-	 *  <p>This tag records the serial number of the body of the camera that was used in photography
-	 *  as an ASCII string.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42033</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_BODY_SERIAL_NUMBER = "BodySerialNumber";
-	/**
-	 *  <p>This tag notes minimum focal length, maximum focal length, minimum F number in the
-	 *  minimum focal length, and minimum F number in the maximum focal length, which are
-	 *  specification information for the lens that was used in photography. When the minimum
-	 *  F number is unknown, the notation is 0/0.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42034</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 4</li>
-	 *      <li>Default = None</li>
-	 *      <ul>
-	 *          <li>Value 1 := Minimum focal length (unit: mm)</li>
-	 *          <li>Value 2 : = Maximum focal length (unit: mm)</li>
-	 *          <li>Value 3 : = Minimum F number in the minimum focal length</li>
-	 *          <li>Value 4 : = Minimum F number in the maximum focal length</li>
-	 *      </ul>
-	 *  </ul>
-	 */
-	public static final String TAG_LENS_SPECIFICATION = "LensSpecification";
-	/**
-	 *  <p>This tag records the lens manufacturer as an ASCII string.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42035</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_LENS_MAKE = "LensMake";
-	/**
-	 *  <p>This tag records the lens’s model name and model number as an ASCII string.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42036</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_LENS_MODEL = "LensModel";
-	/**
-	 *  <p>This tag records the serial number of the interchangeable lens that was used in
-	 *  photography as an ASCII string.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 42037</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_LENS_SERIAL_NUMBER = "LensSerialNumber";
+					// Update ColorSpace tag
+					ExifAttribute colorSpaceAttribute =
+						mAttributes[IFD_TYPE_PEF].get(TAG_COLOR_SPACE);
+					if (colorSpaceAttribute != null) {
+						mAttributes[IFD_TYPE_EXIF].put(TAG_COLOR_SPACE, colorSpaceAttribute);
+					}
+				}
+            }
+        }
+    }
+
+    /**
+     * RAF files contains a JPEG and a CFA data.
+     * The JPEG contains two images, a preview and a thumbnail, while the CFA contains a RAW image.
+     * This method looks at the first 160 bytes of a RAF file to retrieve the offset and length
+     * values for the JPEG and CFA data.
+     * Using that data, it parses the JPEG data to retrieve the preview and thumbnail image data,
+     * then parses the CFA metadata to retrieve the primary image length/width values.
+     * For data format details, see http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
+     */
+    private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException {
+        if (DEBUG) {
+            logger.debug("getRafAttributes starting with: " + in);
+        }
+        // Retrieve offset & length values
+        in.skipFully(RAF_OFFSET_TO_JPEG_IMAGE_OFFSET);
+        byte[] offsetToJpegBytes = new byte[4];
+        byte[] jpegLengthBytes = new byte[4];
+        byte[] cfaHeaderOffsetBytes = new byte[4];
+        in.readFully(offsetToJpegBytes);
+        in.readFully(jpegLengthBytes);
+        in.readFully(cfaHeaderOffsetBytes);
+        int offsetToJpeg = ByteBuffer.wrap(offsetToJpegBytes).getInt();
+        int jpegLength = ByteBuffer.wrap(jpegLengthBytes).getInt();
+        int cfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt();
+
+        byte[] jpegBytes = new byte[jpegLength];
+        in.skipFully(offsetToJpeg - in.position());
+        in.readFully(jpegBytes);
+
+        // Retrieve JPEG image metadata
+        ByteOrderedDataInputStream jpegInputStream = new ByteOrderedDataInputStream(jpegBytes);
+        getJpegAttributes(jpegInputStream, offsetToJpeg, IFD_TYPE_PREVIEW);
+
+        // Skip to CFA header offset.
+        in.skipFully(cfaHeaderOffset - in.position());
+
+        // Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists
+        in.setByteOrder(BIG_ENDIAN);
+        int numberOfDirectoryEntry = in.readInt();
+        if (DEBUG) {
+            logger.debug("numberOfDirectoryEntry: " + numberOfDirectoryEntry);
+        }
+        // CFA stores some metadata about the RAW image. Since CFA uses proprietary tags, can only
+        // find and retrieve image size information tags, while skipping others.
+        // See piex.cc RafGetDimension()
+        for (int i = 0; i < numberOfDirectoryEntry; ++i) {
+            int tagNumber = in.readUnsignedShort();
+            int numberOfBytes = in.readUnsignedShort();
+            if (tagNumber == TAG_RAF_IMAGE_SIZE.number) {
+                int imageLength = in.readShort();
+                int imageWidth = in.readShort();
+                ExifAttribute imageLengthAttribute =
+                        ExifAttribute.createUShort(imageLength, mExifByteOrder);
+                ExifAttribute imageWidthAttribute =
+                        ExifAttribute.createUShort(imageWidth, mExifByteOrder);
+                mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
+                mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
+                if (DEBUG) {
+                    logger.debug("Updated to length: " + imageLength + ", width: " + imageWidth);
+                }
+                return;
+            }
+            in.skipFully(numberOfBytes);
+        }
+    }
+
+    // Support for getting MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET and
+    // MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH was added SDK 28.
+    private void getHeifAttributes(final SeekableByteOrderedDataInputStream in) throws IOException {
+        if (Build.VERSION.SDK_INT >= 28) {
+            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+            try {
+                Api23Impl.setDataSource(retriever, new MediaDataSource() {
+                    long mPosition;
+
+                    @Override
+                    public void close() throws IOException {}
+
+                    @Override
+                    public int readAt(long position, byte[] buffer, int offset, int size)
+                            throws IOException {
+                        if (size == 0) {
+                            return 0;
+                        }
+                        if (position < 0) {
+                            return -1;
+                        }
+                        try {
+                            if (mPosition != position) {
+                                // We don't allow seek to positions after the available bytes,
+                                // the input stream won't be able to seek back then.
+                                // However, if we hit an exception before (mPosition set to -1),
+                                // let it try the seek in hope it might recover.
+                                if (mPosition >= 0 && position >= mPosition + in.available()) {
+                                    return -1;
+                                }
+                                in.seek(position);
+                                mPosition = position;
+                            }
+
+                            // If the read will cause us to go over the available bytes,
+                            // reduce the size so that we stay in the available range.
+                            // Otherwise the input stream may not be able to seek back.
+                            if (size > in.available()) {
+                                size = in.available();
+                            }
+
+                            int bytesRead = in.read(buffer, offset, size);
+                            if (bytesRead >= 0) {
+                                mPosition += bytesRead;
+                                return bytesRead;
+                            }
+                        } catch (IOException e) {
+                            // do nothing
+                        }
+                        mPosition = -1; // need to seek on next read
+                        return -1;
+                    }
+
+                    @Override
+                    public long getSize() throws IOException {
+                        return -1;
+                    }
+                });
+
+                String exifOffsetStr = retriever.extractMetadata(
+                        33); // MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET
+                String exifLengthStr = retriever.extractMetadata(
+                        34); // MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH
+                String hasImage = retriever.extractMetadata(
+                        MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
+                String hasVideo = retriever.extractMetadata(
+                        MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
+
+                String width = null;
+                String height = null;
+                String rotation = null;
+                final String metadataValueYes = "yes";
+                // If the file has both image and video, prefer image info over video info.
+                // App querying ExifInterface is most likely using the bitmap path which
+                // picks the image first.
+                if (metadataValueYes.equals(hasImage)) {
+                    width = retriever.extractMetadata(
+                            MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
+                    height = retriever.extractMetadata(
+                            MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
+                    rotation = retriever.extractMetadata(
+                            MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
+                } else if (metadataValueYes.equals(hasVideo)) {
+                    width = retriever.extractMetadata(
+                            MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
+                    height = retriever.extractMetadata(
+                            MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
+                    rotation = retriever.extractMetadata(
+                            MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+                }
+
+                if (width != null) {
+                    mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
+                            ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder));
+                }
+
+                if (height != null) {
+                    mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
+                            ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder));
+                }
+
+                if (rotation != null) {
+                    int orientation = ExifInterface.ORIENTATION_NORMAL;
+
+                    // all rotation angles in CW
+                    switch (Integer.parseInt(rotation)) {
+                        case 90:
+                            orientation = ExifInterface.ORIENTATION_ROTATE_90;
+                            break;
+                        case 180:
+                            orientation = ExifInterface.ORIENTATION_ROTATE_180;
+                            break;
+                        case 270:
+                            orientation = ExifInterface.ORIENTATION_ROTATE_270;
+                            break;
+                    }
+
+                    mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
+                            ExifAttribute.createUShort(orientation, mExifByteOrder));
+                }
+
+                if (exifOffsetStr != null && exifLengthStr != null) {
+                    int offset = Integer.parseInt(exifOffsetStr);
+                    int length = Integer.parseInt(exifLengthStr);
+                    if (length <= 6) {
+                        throw new IOException("Invalid exif length");
+                    }
+                    in.seek(offset);
+                    byte[] identifier = new byte[6];
+                    in.readFully(identifier);
+                    offset += 6;
+                    length -= 6;
+                    if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
+                        throw new IOException("Invalid identifier");
+                    }
+
+                    // TODO: Need to handle potential OutOfMemoryError
+                    byte[] bytes = new byte[length];
+                    in.readFully(bytes);
+                    // Save offset to EXIF data for handling thumbnail and attribute offsets.
+                    mOffsetToExifData = offset;
+                    readExifSegment(bytes, IFD_TYPE_PRIMARY);
+                }
+
+                String xmpOffsetStr = retriever.extractMetadata(
+                        41); // MediaMetadataRetriever.METADATA_KEY_XMP_OFFSET
+                String xmpLengthStr = retriever.extractMetadata(
+                        42); // MediaMetadataRetriever.METADATA_KEY_XMP_LENGTH
+                if (xmpOffsetStr != null && xmpLengthStr != null) {
+                    int offset = Integer.parseInt(xmpOffsetStr);
+                    int length = Integer.parseInt(xmpLengthStr);
+                    in.seek(offset);
+                    byte[] xmpBytes = new byte[length];
+                    in.readFully(xmpBytes);
+                    if (getAttribute(TAG_XMP) == null) {
+                        mAttributes[IFD_TYPE_PRIMARY].put(TAG_XMP, new ExifAttribute(
+                                IFD_FORMAT_BYTE, xmpBytes.length, offset, xmpBytes));
+                    }
+                }
+
+                if (DEBUG) {
+                    logger.debug("Heif meta: " + width + "x" + height + ", rotation " + rotation);
+                }
+            } catch (RuntimeException e) {
+                throw new UnsupportedOperationException("Failed to read EXIF from HEIF file. "
+                        + "Given stream is either malformed or unsupported.");
+            } finally {
+                try {
+                    retriever.release();
+                } catch (IOException e) {
+                    // Nothing we can  do about it.
+                }
+            }
+        } else {
+            throw new UnsupportedOperationException("Reading EXIF from HEIF files "
+                    + "is supported from SDK 28 and above");
+        }
+    }
+
+    /** Reads standalone EXIF data, returning whether the data was read successfully. */
+    private boolean getStandaloneAttributes(SeekableByteOrderedDataInputStream in)
+            throws IOException {
+        byte[] signatureCheckBytes = new byte[IDENTIFIER_EXIF_APP1.length];
+        in.readFully(signatureCheckBytes);
+        if (!Arrays.equals(signatureCheckBytes, IDENTIFIER_EXIF_APP1)) {
+            logger.warn("Given data is not EXIF-only.");
+            return false;
+        }
+        // TODO: Need to handle potential OutOfMemoryError
+        byte[] data = in.readToEnd();
+        // Save offset to EXIF data for handling thumbnail and attribute offsets.
+        mOffsetToExifData = IDENTIFIER_EXIF_APP1.length;
+        readExifSegment(data, IFD_TYPE_PRIMARY);
+        return true;
+    }
+
+    /**
+     * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail
+     * images. Both data takes the form of IFDs and can therefore be read with the
+     * readImageFileDirectory() method.
+     * This method reads all the necessary data and updates the primary/preview/thumbnail image
+     * information according to the GetOlympusPreviewImage() method in piex.cc.
+     * For data format details, see the following:
+     * http://fileformats.archiveteam.org/wiki/Olympus_ORF
+     * https://libopenraw.freedesktop.org/wiki/Olympus_ORF
+     */
+    private void getOrfAttributes(SeekableByteOrderedDataInputStream in) throws IOException {
+        // Retrieve primary image data
+        // Other Exif data will be located in the Makernote.
+        getRawAttributes(in);
+
+        // Additionally retrieve preview/thumbnail information from MakerNote tag, which contains
+        // proprietary tags and therefore does not have offical documentation
+        // See GetOlympusPreviewImage() in piex.cc & http://www.exiv2.org/tags-olympus.html
+        ExifAttribute makerNoteAttribute =
+                mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
+        if (makerNoteAttribute != null) {
+            // Create an ordered DataInputStream for MakerNote
+					 			// MODIFIED BY THREEMA
+			try (SeekableByteOrderedDataInputStream makerNoteDataInputStream =
+                    new SeekableByteOrderedDataInputStream(makerNoteAttribute.bytes)) {
+				makerNoteDataInputStream.setByteOrder(mExifByteOrder);
 
-	// GPS Attribute Information
-	/**
-	 *  <p>Indicates the version of GPS Info IFD. The version is given as 2.3.0.0. This tag is
-	 *  mandatory when GPS-related tags are present. Note that this tag is written as a different
-	 *  byte than {@link #TAG_EXIF_VERSION}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 0</li>
-	 *      <li>Type = Byte</li>
-	 *      <li>Count = 4</li>
-	 *      <li>Default = 2.3.0.0</li>
-	 *      <ul>
-	 *          <li>2300 = Version 2.3</li>
-	 *          <li>Other = reserved</li>
-	 *      </ul>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_VERSION_ID = "GPSVersionID";
-	/**
-	 *  <p>Indicates whether the latitude is north or south latitude.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 1</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #LATITUDE_NORTH
-	 *  @see #LATITUDE_SOUTH
-	 */
-	public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
-	/**
-	 *  <p>Indicates the latitude. The latitude is expressed as three RATIONAL values giving
-	 *  the degrees, minutes, and seconds, respectively. If latitude is expressed as degrees,
-	 *  minutes and seconds, a typical format would be dd/1,mm/1,ss/1. When degrees and minutes are
-	 *  used and, for example, fractions of minutes are given up to two decimal places, the format
-	 *  would be dd/1,mmmm/100,0/1.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 2</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 3</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_LATITUDE = "GPSLatitude";
-	/**
-	 *  <p>Indicates whether the longitude is east or west longitude.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 3</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #LONGITUDE_EAST
-	 *  @see #LONGITUDE_WEST
-	 */
-	public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
-	/**
-	 *  <p>Indicates the longitude. The longitude is expressed as three RATIONAL values giving
-	 *  the degrees, minutes, and seconds, respectively. If longitude is expressed as degrees,
-	 *  minutes and seconds, a typical format would be ddd/1,mm/1,ss/1. When degrees and minutes
-	 *  are used and, for example, fractions of minutes are given up to two decimal places,
-	 *  the format would be ddd/1,mmmm/100,0/1.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 4</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 3</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
-	/**
-	 *  <p>Indicates the altitude used as the reference altitude. If the reference is sea level
-	 *  and the altitude is above sea level, 0 is given. If the altitude is below sea level,
-	 *  a value of 1 is given and the altitude is indicated as an absolute value in
-	 *  {@link #TAG_GPS_ALTITUDE}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 5</li>
-	 *      <li>Type = Byte</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = 0</li>
-	 *  </ul>
-	 *
-	 *  @see #ALTITUDE_ABOVE_SEA_LEVEL
-	 *  @see #ALTITUDE_BELOW_SEA_LEVEL
-	 */
-	public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef";
-	/**
-	 *  <p>Indicates the altitude based on the reference in {@link #TAG_GPS_ALTITUDE_REF}.
-	 *  The reference unit is meters.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 6</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
-	/**
-	 *  <p>Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
-	 *  unsigned rational values giving the hour, minute, and second.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 7</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 3</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
-	/**
-	 *  <p>Indicates the GPS satellites used for measurements. This tag may be used to describe
-	 *  the number of satellites, their ID number, angle of elevation, azimuth, SNR and other
-	 *  information in ASCII notation. The format is not specified. If the GPS receiver is incapable
-	 *  of taking measurements, value of the tag shall be set to {@code null}.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 8</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_SATELLITES = "GPSSatellites";
-	/**
-	 *  <p>Indicates the status of the GPS receiver when the image is recorded. 'A' means
-	 *  measurement is in progress, and 'V' means the measurement is interrupted.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 9</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_MEASUREMENT_IN_PROGRESS
-	 *  @see #GPS_MEASUREMENT_INTERRUPTED
-	 */
-	public static final String TAG_GPS_STATUS = "GPSStatus";
-	/**
-	 *  <p>Indicates the GPS measurement mode. Originally it was defined for GPS, but it may
-	 *  be used for recording a measure mode to record the position information provided from
-	 *  a mobile base station or wireless LAN as well as GPS.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 10</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_MEASUREMENT_2D
-	 *  @see #GPS_MEASUREMENT_3D
-	 */
-	public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode";
-	/**
-	 *  <p>Indicates the GPS DOP (data degree of precision). An HDOP value is written during
-	 *  two-dimensional measurement, and PDOP during three-dimensional measurement.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 11</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_DOP = "GPSDOP";
-	/**
-	 *  <p>Indicates the unit used to express the GPS receiver speed of movement.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 12</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = {@link #GPS_SPEED_KILOMETERS_PER_HOUR}</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_SPEED_KILOMETERS_PER_HOUR
-	 *  @see #GPS_SPEED_MILES_PER_HOUR
-	 *  @see #GPS_SPEED_KNOTS
-	 */
-	public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
-	/**
-	 *  <p>Indicates the speed of GPS receiver movement.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 13</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_SPEED = "GPSSpeed";
-	/**
-	 *  <p>Indicates the reference for giving the direction of GPS receiver movement.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 14</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_DIRECTION_TRUE
-	 *  @see #GPS_DIRECTION_MAGNETIC
-	 */
-	public static final String TAG_GPS_TRACK_REF = "GPSTrackRef";
-	/**
-	 *  <p>Indicates the direction of GPS receiver movement.
-	 *  The range of values is from 0.00 to 359.99.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 15</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_TRACK = "GPSTrack";
-	/**
-	 *  <p>Indicates the reference for giving the direction of the image when it is captured.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 16</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_DIRECTION_TRUE
-	 *  @see #GPS_DIRECTION_MAGNETIC
-	 */
-	public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef";
-	/**
-	 *  <p>ndicates the direction of the image when it was captured.
-	 *  The range of values is from 0.00 to 359.99.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 17</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection";
-	/**
-	 *  <p>Indicates the geodetic survey data used by the GPS receiver. If the survey data is
-	 *  restricted to Japan,the value of this tag is 'TOKYO' or 'WGS-84'. If a GPS Info tag is
-	 *  recorded, it is strongly recommended that this tag be recorded.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 18</li>
-	 *      <li>Type = String</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum";
-	/**
-	 *  <p>Indicates whether the latitude of the destination point is north or south latitude.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 19</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #LATITUDE_NORTH
-	 *  @see #LATITUDE_SOUTH
-	 */
-	public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef";
-	/**
-	 *  <p>Indicates the latitude of the destination point. The latitude is expressed as three
-	 *  unsigned rational values giving the degrees, minutes, and seconds, respectively.
-	 *  If latitude is expressed as degrees, minutes and seconds, a typical format would be
-	 *  dd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes
-	 *  are given up to two decimal places, the format would be dd/1, mmmm/100, 0/1.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 20</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 3</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude";
-	/**
-	 *  <p>Indicates whether the longitude of the destination point is east or west longitude.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 21</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #LONGITUDE_EAST
-	 *  @see #LONGITUDE_WEST
-	 */
-	public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef";
-	/**
-	 *  <p>Indicates the longitude of the destination point. The longitude is expressed as three
-	 *  unsigned rational values giving the degrees, minutes, and seconds, respectively.
-	 *  If longitude is expressed as degrees, minutes and seconds, a typical format would be ddd/1,
-	 *  mm/1, ss/1. When degrees and minutes are used and, for example, fractions of minutes are
-	 *  given up to two decimal places, the format would be ddd/1, mmmm/100, 0/1.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 22</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 3</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude";
-	/**
-	 *  <p>Indicates the reference used for giving the bearing to the destination point.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 23</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_DIRECTION_TRUE
-	 *  @see #GPS_DIRECTION_MAGNETIC
-	 */
-	public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef";
-	/**
-	 *  <p>Indicates the bearing to the destination point.
-	 *  The range of values is from 0.00 to 359.99.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 24</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing";
-	/**
-	 *  <p>Indicates the unit used to express the distance to the destination point.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 25</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 1</li>
-	 *      <li>Default = {@link #GPS_DISTANCE_KILOMETERS}</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_DISTANCE_KILOMETERS
-	 *  @see #GPS_DISTANCE_MILES
-	 *  @see #GPS_DISTANCE_NAUTICAL_MILES
-	 */
-	public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef";
-	/**
-	 *  <p>Indicates the distance to the destination point.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 26</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance";
-	/**
-	 *  <p>A character string recording the name of the method used for location finding.
-	 *  The first byte indicates the character code used, and this is followed by the name of
-	 *  the method.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 27</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
-	/**
-	 *  <p>A character string recording the name of the GPS area. The first byte indicates
-	 *  the character code used, and this is followed by the name of the GPS area.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 28</li>
-	 *      <li>Type = Undefined</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation";
-	/**
-	 *  <p>A character string recording date and time information relative to UTC (Coordinated
-	 *  Universal Time). The format is "YYYY:MM:DD".</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 29</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 10</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_DATESTAMP = "GPSDateStamp";
-	/**
-	 *  <p>Indicates whether differential correction is applied to the GPS receiver.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 30</li>
-	 *      <li>Type = Unsigned short</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 *
-	 *  @see #GPS_MEASUREMENT_NO_DIFFERENTIAL
-	 *  @see #GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED
-	 */
-	public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential";
-	/**
-	 *  <p>This tag indicates horizontal positioning errors in meters.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 31</li>
-	 *      <li>Type = Unsigned rational</li>
-	 *      <li>Count = 1</li>
-	 *      <li>Default = None</li>
-	 *  </ul>
-	 */
-	public static final String TAG_GPS_H_POSITIONING_ERROR = "GPSHPositioningError";
-
-	// Interoperability IFD Attribute Information
-	/**
-	 *  <p>Indicates the identification of the Interoperability rule.</p>
-	 *
-	 *  <ul>
-	 *      <li>Tag = 1</li>
-	 *      <li>Type = String</li>
-	 *      <li>Length = 4</li>
-	 *      <li>Default = None</li>
-	 *      <ul>
-	 *          <li>"R98" = Indicates a file conforming to R98 file specification of Recommended
-	 *                      Exif Interoperability Rules (Exif R 98) or to DCF basic file stipulated
-	 *                      by Design Rule for Camera File System.</li>
-	 *          <li>"THM" = Indicates a file conforming to DCF thumbnail file stipulated by Design
-	 *                      rule for Camera File System.</li>
-	 *          <li>“R03” = Indicates a file conforming to DCF Option File stipulated by Design rule
-	 *                      for Camera File System.</li>
-	 *      </ul>
-	 *  </ul>
-	 */
-	public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex";
-
-	/**
-	 * @see #TAG_IMAGE_LENGTH
-	 */
-	public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength";
-	/**
-	 * @see #TAG_IMAGE_WIDTH
-	 */
-	public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth";
-	/** Type is int. DNG Specification 1.4.0.0. Section 4 */
-	public static final String TAG_DNG_VERSION = "DNGVersion";
-	/** Type is int. DNG Specification 1.4.0.0. Section 4 */
-	public static final String TAG_DEFAULT_CROP_SIZE = "DefaultCropSize";
-	/** Type is undefined. See Olympus MakerNote tags in http://www.exiv2.org/tags-olympus.html. */
-	public static final String TAG_ORF_THUMBNAIL_IMAGE = "ThumbnailImage";
-	/** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
-	public static final String TAG_ORF_PREVIEW_IMAGE_START = "PreviewImageStart";
-	/** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
-	public static final String TAG_ORF_PREVIEW_IMAGE_LENGTH = "PreviewImageLength";
-	/** Type is int. See Olympus Image Processing tags in http://www.exiv2.org/tags-olympus.html. */
-	public static final String TAG_ORF_ASPECT_FRAME = "AspectFrame";
-	/**
-	 * Type is int. See PanasonicRaw tags in
-	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
-	 */
-	public static final String TAG_RW2_SENSOR_BOTTOM_BORDER = "SensorBottomBorder";
-	/**
-	 * Type is int. See PanasonicRaw tags in
-	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
-	 */
-	public static final String TAG_RW2_SENSOR_LEFT_BORDER = "SensorLeftBorder";
-	/**
-	 * Type is int. See PanasonicRaw tags in
-	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
-	 */
-	public static final String TAG_RW2_SENSOR_RIGHT_BORDER = "SensorRightBorder";
-	/**
-	 * Type is int. See PanasonicRaw tags in
-	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
-	 */
-	public static final String TAG_RW2_SENSOR_TOP_BORDER = "SensorTopBorder";
-	/**
-	 * Type is int. See PanasonicRaw tags in
-	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
-	 */
-	public static final String TAG_RW2_ISO = "ISO";
-	/**
-	 * Type is undefined. See PanasonicRaw tags in
-	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
-	 */
-	public static final String TAG_RW2_JPG_FROM_RAW = "JpgFromRaw";
-	/** Type is int. See JEITA CP-3451C Spec Section 3: Bilevel Images. */
-	public static final String TAG_NEW_SUBFILE_TYPE = "NewSubfileType";
-	/** Type is int. See JEITA CP-3451C Spec Section 3: Bilevel Images. */
-	public static final String TAG_SUBFILE_TYPE = "SubfileType";
-
-	/**
-	 * Private tags used for pointing the other IFD offsets.
-	 * The types of the following tags are int.
-	 * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
-	 * For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
-	 */
-	private static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
-	private static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
-	private static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
-	private static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
-	// Proprietary pointer tags used for ORF files.
-	// See http://www.exiv2.org/tags-olympus.html
-	private static final String TAG_ORF_CAMERA_SETTINGS_IFD_POINTER = "CameraSettingsIFDPointer";
-	private static final String TAG_ORF_IMAGE_PROCESSING_IFD_POINTER = "ImageProcessingIFDPointer";
-
-	// Private tags used for thumbnail information.
-	private static final String TAG_HAS_THUMBNAIL = "HasThumbnail";
-	private static final String TAG_THUMBNAIL_OFFSET = "ThumbnailOffset";
-	private static final String TAG_THUMBNAIL_LENGTH = "ThumbnailLength";
-	private static final String TAG_THUMBNAIL_DATA = "ThumbnailData";
-	private static final int MAX_THUMBNAIL_SIZE = 512;
-
-	// Constants used for the Orientation Exif tag.
-	public static final int ORIENTATION_UNDEFINED = 0;
-	public static final int ORIENTATION_NORMAL = 1;
-	/**
-	 * Indicates the image is left right reversed mirror.
-	 */
-	public static final int ORIENTATION_FLIP_HORIZONTAL = 2;
-	/**
-	 * Indicates the image is rotated by 180 degree clockwise.
-	 */
-	public static final int ORIENTATION_ROTATE_180 = 3;
-	/**
-	 * Indicates the image is upside down mirror, it can also be represented by flip
-	 * horizontally firstly and rotate 180 degree clockwise.
-	 */
-	public static final int ORIENTATION_FLIP_VERTICAL = 4;
-	/**
-	 * Indicates the image is flipped about top-left <--> bottom-right axis, it can also be
-	 * represented by flip horizontally firstly and rotate 270 degree clockwise.
-	 */
-	public static final int ORIENTATION_TRANSPOSE = 5;
-	/**
-	 * Indicates the image is rotated by 90 degree clockwise.
-	 */
-	public static final int ORIENTATION_ROTATE_90 = 6;
-	/**
-	 * Indicates the image is flipped about top-right <--> bottom-left axis, it can also be
-	 * represented by flip horizontally firstly and rotate 90 degree clockwise.
-	 */
-	public static final int ORIENTATION_TRANSVERSE = 7;
-	/**
-	 * Indicates the image is rotated by 270 degree clockwise.
-	 */
-	public static final int ORIENTATION_ROTATE_270 = 8;
-	private static final List<Integer> ROTATION_ORDER = Arrays.asList(ORIENTATION_NORMAL,
-			ORIENTATION_ROTATE_90, ORIENTATION_ROTATE_180, ORIENTATION_ROTATE_270);
-	private static final List<Integer> FLIPPED_ROTATION_ORDER = Arrays.asList(
-			ORIENTATION_FLIP_HORIZONTAL, ORIENTATION_TRANSVERSE, ORIENTATION_FLIP_VERTICAL,
-			ORIENTATION_TRANSPOSE);
-
-	/**
-	 * The contant used by {@link #TAG_PLANAR_CONFIGURATION} to denote Chunky format.
-	 */
-	public static final short FORMAT_CHUNKY = 1;
-	/**
-	 * The contant used by {@link #TAG_PLANAR_CONFIGURATION} to denote Planar format.
-	 */
-	public static final short FORMAT_PLANAR = 2;
-
-	/**
-	 * The contant used by {@link #TAG_Y_CB_CR_POSITIONING} to denote Centered positioning.
-	 */
-	public static final short Y_CB_CR_POSITIONING_CENTERED = 1;
-	/**
-	 * The contant used by {@link #TAG_Y_CB_CR_POSITIONING} to denote Co-sited positioning.
-	 */
-	public static final short Y_CB_CR_POSITIONING_CO_SITED = 2;
-
-	/**
-	 * The contant used to denote resolution unit as inches.
-	 */
-	public static final short RESOLUTION_UNIT_INCHES = 2;
-	/**
-	 * The contant used to denote resolution unit as centimeters.
-	 */
-	public static final short RESOLUTION_UNIT_CENTIMETERS = 3;
-
-	/**
-	 * The contant used by {@link #TAG_COLOR_SPACE} to denote sRGB color space.
-	 */
-	public static final int COLOR_SPACE_S_RGB = 1;
-	/**
-	 * The contant used by {@link #TAG_COLOR_SPACE} to denote Uncalibrated.
-	 */
-	public static final int COLOR_SPACE_UNCALIBRATED = 65535;
-
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is not defined.
-	 */
-	public static final short EXPOSURE_PROGRAM_NOT_DEFINED = 0;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Manual.
-	 */
-	public static final short EXPOSURE_PROGRAM_MANUAL = 1;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Normal.
-	 */
-	public static final short EXPOSURE_PROGRAM_NORMAL = 2;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is
-	 * Aperture priority.
-	 */
-	public static final short EXPOSURE_PROGRAM_APERTURE_PRIORITY = 3;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is
-	 * Shutter priority.
-	 */
-	public static final short EXPOSURE_PROGRAM_SHUTTER_PRIORITY = 4;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Creative
-	 * program (biased toward depth of field).
-	 */
-	public static final short EXPOSURE_PROGRAM_CREATIVE = 5;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Action
-	 * program (biased toward fast shutter speed).
-	 */
-	public static final short EXPOSURE_PROGRAM_ACTION = 6;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Portrait mode
-	 * (for closeup photos with the background out of focus).
-	 */
-	public static final short EXPOSURE_PROGRAM_PORTRAIT_MODE = 7;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Landscape
-	 * mode (for landscape photos with the background in focus).
-	 */
-	public static final short EXPOSURE_PROGRAM_LANDSCAPE_MODE = 8;
-
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is unknown.
-	 */
-	public static final short SENSITIVITY_TYPE_UNKNOWN = 0;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
-	 * output sensitivity (SOS).
-	 */
-	public static final short SENSITIVITY_TYPE_SOS = 1;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Recommended
-	 * exposure index (REI).
-	 */
-	public static final short SENSITIVITY_TYPE_REI = 2;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is ISO speed.
-	 */
-	public static final short SENSITIVITY_TYPE_ISO_SPEED = 3;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
-	 * output sensitivity (SOS) and recommended exposure index (REI).
-	 */
-	public static final short SENSITIVITY_TYPE_SOS_AND_REI = 4;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
-	 * output sensitivity (SOS) and ISO speed.
-	 */
-	public static final short SENSITIVITY_TYPE_SOS_AND_ISO = 5;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Recommended
-	 * exposure index (REI) and ISO speed.
-	 */
-	public static final short SENSITIVITY_TYPE_REI_AND_ISO = 6;
-	/**
-	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
-	 * output sensitivity (SOS) and recommended exposure index (REI) and ISO speed.
-	 */
-	public static final short SENSITIVITY_TYPE_SOS_AND_REI_AND_ISO = 7;
-
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is unknown.
-	 */
-	public static final short METERING_MODE_UNKNOWN = 0;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Average.
-	 */
-	public static final short METERING_MODE_AVERAGE = 1;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is
-	 * CenterWeightedAverage.
-	 */
-	public static final short METERING_MODE_CENTER_WEIGHT_AVERAGE = 2;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Spot.
-	 */
-	public static final short METERING_MODE_SPOT = 3;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is MultiSpot.
-	 */
-	public static final short METERING_MODE_MULTI_SPOT = 4;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Pattern.
-	 */
-	public static final short METERING_MODE_PATTERN = 5;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Partial.
-	 */
-	public static final short METERING_MODE_PARTIAL = 6;
-	/**
-	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is other.
-	 */
-	public static final short METERING_MODE_OTHER = 255;
-
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is unknown.
-	 */
-	public static final short LIGHT_SOURCE_UNKNOWN = 0;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Daylight.
-	 */
-	public static final short LIGHT_SOURCE_DAYLIGHT = 1;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Fluorescent.
-	 */
-	public static final short LIGHT_SOURCE_FLUORESCENT = 2;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Tungsten
-	 * (incandescent light).
-	 */
-	public static final short LIGHT_SOURCE_TUNGSTEN = 3;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Flash.
-	 */
-	public static final short LIGHT_SOURCE_FLASH = 4;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Fine weather.
-	 */
-	public static final short LIGHT_SOURCE_FINE_WEATHER = 9;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Cloudy weather.
-	 */
-	public static final short LIGHT_SOURCE_CLOUDY_WEATHER = 10;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Shade.
-	 */
-	public static final short LIGHT_SOURCE_SHADE = 11;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Daylight fluorescent
-	 * (D 5700 - 7100K).
-	 */
-	public static final short LIGHT_SOURCE_DAYLIGHT_FLUORESCENT = 12;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Day white fluorescent
-	 * (N 4600 - 5500K).
-	 */
-	public static final short LIGHT_SOURCE_DAY_WHITE_FLUORESCENT = 13;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Cool white
-	 * fluorescent (W 3800 - 4500K).
-	 */
-	public static final short LIGHT_SOURCE_COOL_WHITE_FLUORESCENT = 14;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is White fluorescent
-	 * (WW 3250 - 3800K).
-	 */
-	public static final short LIGHT_SOURCE_WHITE_FLUORESCENT = 15;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Warm white
-	 * fluorescent (L 2600 - 3250K).
-	 */
-	public static final short LIGHT_SOURCE_WARM_WHITE_FLUORESCENT = 16;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light A.
-	 */
-	public static final short LIGHT_SOURCE_STANDARD_LIGHT_A = 17;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light B.
-	 */
-	public static final short LIGHT_SOURCE_STANDARD_LIGHT_B = 18;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light C.
-	 */
-	public static final short LIGHT_SOURCE_STANDARD_LIGHT_C = 19;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D55.
-	 */
-	public static final short LIGHT_SOURCE_D55 = 20;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D65.
-	 */
-	public static final short LIGHT_SOURCE_D65 = 21;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D75.
-	 */
-	public static final short LIGHT_SOURCE_D75 = 22;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D50.
-	 */
-	public static final short LIGHT_SOURCE_D50 = 23;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is ISO studio tungsten.
-	 */
-	public static final short LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN = 24;
-	/**
-	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is other.
-	 */
-	public static final short LIGHT_SOURCE_OTHER = 255;
-
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate whether the flash is fired.
-	 */
-	public static final short FLAG_FLASH_FIRED = 0b0000_0001;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate strobe return light is not detected.
-	 */
-	public static final short FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED = 0b0000_0100;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate strobe return light is detected.
-	 */
-	public static final short FLAG_FLASH_RETURN_LIGHT_DETECTED = 0b0000_0110;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Compulsory flash
-	 * firing.
-	 *
-	 * @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
-	 * @see #FLAG_FLASH_MODE_AUTO
-	 */
-	public static final short FLAG_FLASH_MODE_COMPULSORY_FIRING = 0b0000_1000;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Compulsory flash
-	 * suppression.
-	 *
-	 * @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
-	 * @see #FLAG_FLASH_MODE_AUTO
-	 */
-	public static final short FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION = 0b0001_0000;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Auto.
-	 *
-	 * @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
-	 * @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
-	 */
-	public static final short FLAG_FLASH_MODE_AUTO = 0b0001_1000;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate no flash function is present.
-	 */
-	public static final short FLAG_FLASH_NO_FLASH_FUNCTION = 0b0010_0000;
-	/**
-	 * The flag used by {@link #TAG_FLASH} to indicate red-eye reduction is supported.
-	 */
-	public static final short FLAG_FLASH_RED_EYE_SUPPORTED = 0b0100_0000;
-
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is not
-	 * defined.
-	 */
-	public static final short SENSOR_TYPE_NOT_DEFINED = 1;
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is One-chip
-	 * color area sensor.
-	 */
-	public static final short SENSOR_TYPE_ONE_CHIP = 2;
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Two-chip
-	 * color area sensor.
-	 */
-	public static final short SENSOR_TYPE_TWO_CHIP = 3;
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Three-chip
-	 * color area sensor.
-	 */
-	public static final short SENSOR_TYPE_THREE_CHIP = 4;
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Color
-	 * sequential area sensor.
-	 */
-	public static final short SENSOR_TYPE_COLOR_SEQUENTIAL = 5;
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Trilinear
-	 * sensor.
-	 */
-	public static final short SENSOR_TYPE_TRILINEAR = 7;
-	/**
-	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Color
-	 * sequential linear sensor.
-	 */
-	public static final short SENSOR_TYPE_COLOR_SEQUENTIAL_LINEAR = 8;
-
-	/**
-	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is other.
-	 */
-	public static final short FILE_SOURCE_OTHER = 0;
-	/**
-	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is scanner of transparent
-	 * type.
-	 */
-	public static final short FILE_SOURCE_TRANSPARENT_SCANNER = 1;
-	/**
-	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is scanner of reflex type.
-	 */
-	public static final short FILE_SOURCE_REFLEX_SCANNER = 2;
-	/**
-	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is DSC.
-	 */
-	public static final short FILE_SOURCE_DSC = 3;
-
-	/**
-	 * The contant used by {@link #TAG_SCENE_TYPE} to denote the scene is directly photographed.
-	 */
-	public static final short SCENE_TYPE_DIRECTLY_PHOTOGRAPHED = 1;
-
-	/**
-	 * The contant used by {@link #TAG_CUSTOM_RENDERED} to denote no special processing is used.
-	 */
-	public static final short RENDERED_PROCESS_NORMAL = 0;
-	/**
-	 * The contant used by {@link #TAG_CUSTOM_RENDERED} to denote special processing is used.
-	 */
-	public static final short RENDERED_PROCESS_CUSTOM = 1;
-
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Auto.
-	 */
-	public static final short EXPOSURE_MODE_AUTO = 0;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Manual.
-	 */
-	public static final short EXPOSURE_MODE_MANUAL = 1;
-	/**
-	 * The contant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Auto bracket.
-	 */
-	public static final short EXPOSURE_MODE_AUTO_BRACKET = 2;
-
-	/**
-	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Auto.
-	 *
-	 * @deprecated Use {@link #WHITE_BALANCE_AUTO} instead.
-	 */
-	@Deprecated public static final int WHITEBALANCE_AUTO = 0;
-	/**
-	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Manual.
-	 *
-	 * @deprecated Use {@link #WHITE_BALANCE_MANUAL} instead.
-	 */
-	@Deprecated public static final int WHITEBALANCE_MANUAL = 1;
-	/**
-	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Auto.
-	 */
-	public static final short WHITE_BALANCE_AUTO = 0;
-	/**
-	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Manual.
-	 */
-	public static final short WHITE_BALANCE_MANUAL = 1;
-
-	/**
-	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
-	 * Standard.
-	 */
-	public static final short SCENE_CAPTURE_TYPE_STANDARD = 0;
-	/**
-	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
-	 * Landscape.
-	 */
-	public static final short SCENE_CAPTURE_TYPE_LANDSCAPE = 1;
-	/**
-	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
-	 * Portrait.
-	 */
-	public static final short SCENE_CAPTURE_TYPE_PORTRAIT = 2;
-	/**
-	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is Night
-	 * scene.
-	 */
-	public static final short SCENE_CAPTURE_TYPE_NIGHT = 3;
-
-	/**
-	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote none gain adjustment.
-	 */
-	public static final short GAIN_CONTROL_NONE = 0;
-	/**
-	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote low gain up.
-	 */
-	public static final short GAIN_CONTROL_LOW_GAIN_UP = 1;
-	/**
-	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote high gain up.
-	 */
-	public static final short GAIN_CONTROL_HIGH_GAIN_UP = 2;
-	/**
-	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote low gain down.
-	 */
-	public static final short GAIN_CONTROL_LOW_GAIN_DOWN = 3;
-	/**
-	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote high gain down.
-	 */
-	public static final short GAIN_CONTROL_HIGH_GAIN_DOWN = 4;
-
-	/**
-	 * The contant used by {@link #TAG_CONTRAST} to denote normal contrast.
-	 */
-	public static final short CONTRAST_NORMAL = 0;
-	/**
-	 * The contant used by {@link #TAG_CONTRAST} to denote soft contrast.
-	 */
-	public static final short CONTRAST_SOFT = 1;
-	/**
-	 * The contant used by {@link #TAG_CONTRAST} to denote hard contrast.
-	 */
-	public static final short CONTRAST_HARD = 2;
-
-	/**
-	 * The contant used by {@link #TAG_SATURATION} to denote normal saturation.
-	 */
-	public static final short SATURATION_NORMAL = 0;
-	/**
-	 * The contant used by {@link #TAG_SATURATION} to denote low saturation.
-	 */
-	public static final short SATURATION_LOW = 0;
-	/**
-	 * The contant used by {@link #TAG_SHARPNESS} to denote high saturation.
-	 */
-	public static final short SATURATION_HIGH = 0;
-
-	/**
-	 * The contant used by {@link #TAG_SHARPNESS} to denote normal sharpness.
-	 */
-	public static final short SHARPNESS_NORMAL = 0;
-	/**
-	 * The contant used by {@link #TAG_SHARPNESS} to denote soft sharpness.
-	 */
-	public static final short SHARPNESS_SOFT = 1;
-	/**
-	 * The contant used by {@link #TAG_SHARPNESS} to denote hard sharpness.
-	 */
-	public static final short SHARPNESS_HARD = 2;
-
-	/**
-	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
-	 * is unknown.
-	 */
-	public static final short SUBJECT_DISTANCE_RANGE_UNKNOWN = 0;
-	/**
-	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
-	 * is Macro.
-	 */
-	public static final short SUBJECT_DISTANCE_RANGE_MACRO = 1;
-	/**
-	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
-	 * is Close view.
-	 */
-	public static final short SUBJECT_DISTANCE_RANGE_CLOSE_VIEW = 2;
-	/**
-	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
-	 * is Distant view.
-	 */
-	public static final short SUBJECT_DISTANCE_RANGE_DISTANT_VIEW = 3;
-
-	/**
-	 * The contant used by GPS latitude-related tags to denote the latitude is North latitude.
-	 *
-	 * @see #TAG_GPS_LATITUDE_REF
-	 * @see #TAG_GPS_DEST_LATITUDE_REF
-	 */
-	public static final String LATITUDE_NORTH = "N";
-	/**
-	 * The contant used by GPS latitude-related tags to denote the latitude is South latitude.
-	 *
-	 * @see #TAG_GPS_LATITUDE_REF
-	 * @see #TAG_GPS_DEST_LATITUDE_REF
-	 */
-	public static final String LATITUDE_SOUTH = "S";
-
-	/**
-	 * The contant used by GPS longitude-related tags to denote the longitude is East longitude.
-	 *
-	 * @see #TAG_GPS_LONGITUDE_REF
-	 * @see #TAG_GPS_DEST_LONGITUDE_REF
-	 */
-	public static final String LONGITUDE_EAST = "E";
-	/**
-	 * The contant used by GPS longitude-related tags to denote the longitude is West longitude.
-	 *
-	 * @see #TAG_GPS_LONGITUDE_REF
-	 * @see #TAG_GPS_DEST_LONGITUDE_REF
-	 */
-	public static final String LONGITUDE_WEST = "W";
-
-	/**
-	 * The contant used by {@link #TAG_GPS_ALTITUDE_REF} to denote the altitude is above sea level.
-	 */
-	public static final short ALTITUDE_ABOVE_SEA_LEVEL = 0;
-	/**
-	 * The contant used by {@link #TAG_GPS_ALTITUDE_REF} to denote the altitude is below sea level.
-	 */
-	public static final short ALTITUDE_BELOW_SEA_LEVEL = 1;
-
-	/**
-	 * The contant used by {@link #TAG_GPS_STATUS} to denote GPS measurement is in progress.
-	 */
-	public static final String GPS_MEASUREMENT_IN_PROGRESS = "A";
-	/**
-	 * The contant used by {@link #TAG_GPS_STATUS} to denote GPS measurement is interrupted.
-	 */
-	public static final String GPS_MEASUREMENT_INTERRUPTED = "V";
-
-	/**
-	 * The contant used by {@link #TAG_GPS_MEASURE_MODE} to denote GPS measurement is 2-dimensional.
-	 */
-	public static final String GPS_MEASUREMENT_2D = "2";
-	/**
-	 * The contant used by {@link #TAG_GPS_MEASURE_MODE} to denote GPS measurement is 3-dimensional.
-	 */
-	public static final String GPS_MEASUREMENT_3D = "3";
-
-	/**
-	 * The contant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is kilometers per
-	 * hour.
-	 */
-	public static final String GPS_SPEED_KILOMETERS_PER_HOUR = "K";
-	/**
-	 * The contant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is miles per hour.
-	 */
-	public static final String GPS_SPEED_MILES_PER_HOUR = "M";
-	/**
-	 * The contant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is knots.
-	 */
-	public static final String GPS_SPEED_KNOTS = "N";
-
-	/**
-	 * The contant used by GPS attributes to denote the direction is true direction.
-	 */
-	public static final String GPS_DIRECTION_TRUE = "T";
-	/**
-	 * The contant used by GPS attributes to denote the direction is magnetic direction.
-	 */
-	public static final String GPS_DIRECTION_MAGNETIC = "M";
-
-	/**
-	 * The contant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is
-	 * kilometers.
-	 */
-	public static final String GPS_DISTANCE_KILOMETERS = "K";
-	/**
-	 * The contant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is miles.
-	 */
-	public static final String GPS_DISTANCE_MILES = "M";
-	/**
-	 * The contant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is
-	 * nautical miles.
-	 */
-	public static final String GPS_DISTANCE_NAUTICAL_MILES = "N";
-
-	/**
-	 * The contant used by {@link #TAG_GPS_DIFFERENTIAL} to denote no differential correction is
-	 * applied.
-	 */
-	public static final short GPS_MEASUREMENT_NO_DIFFERENTIAL = 0;
-	/**
-	 * The contant used by {@link #TAG_GPS_DIFFERENTIAL} to denote differential correction is
-	 * applied.
-	 */
-	public static final short GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED = 1;
-
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is not compressed.
-	 */
-	public static final int DATA_UNCOMPRESSED = 1;
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is huffman compressed.
-	 */
-	public static final int DATA_HUFFMAN_COMPRESSED = 2;
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is JPEG.
-	 */
-	public static final int DATA_JPEG = 6;
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
-	 * Section 3, Compression
-	 */
-	public static final int DATA_JPEG_COMPRESSED = 7;
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
-	 * Section 3, Compression
-	 */
-	public static final int DATA_DEFLATE_ZIP = 8;
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is pack-bits compressed.
-	 */
-	public static final int DATA_PACK_BITS_COMPRESSED = 32773;
-	/**
-	 * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
-	 * Section 3, Compression
-	 */
-	public static final int DATA_LOSSY_JPEG = 34892;
-
-	/**
-	 * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
-	 * See JEITA CP-3451C Spec Section 6, Differences from Palette Color Images
-	 */
-	protected static final int[] BITS_PER_SAMPLE_RGB = new int[] { 8, 8, 8 };
-	/**
-	 * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
-	 * See JEITA CP-3451C Spec Section 4, Differences from Bilevel Images
-	 */
-	protected static final int[] BITS_PER_SAMPLE_GREYSCALE_1 = new int[] { 4 };
-	/**
-	 * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
-	 * See JEITA CP-3451C Spec Section 4, Differences from Bilevel Images
-	 */
-	protected static final int[] BITS_PER_SAMPLE_GREYSCALE_2 = new int[] { 8 };
-
-	/**
-	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
-	 */
-	public static final int PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO = 0;
-	/**
-	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
-	 */
-	public static final int PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1;
-	/**
-	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
-	 */
-	public static final int PHOTOMETRIC_INTERPRETATION_RGB = 2;
-	/**
-	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
-	 */
-	public static final int PHOTOMETRIC_INTERPRETATION_YCBCR = 6;
-
-	/**
-	 * The constant used by {@link #TAG_NEW_SUBFILE_TYPE}. See JEITA CP-3451C Spec Section 8.
-	 */
-	public static final int ORIGINAL_RESOLUTION_IMAGE = 0;
-	/**
-	 * The constant used by {@link #TAG_NEW_SUBFILE_TYPE}. See JEITA CP-3451C Spec Section 8.
-	 */
-	public static final int REDUCED_RESOLUTION_IMAGE = 1;
-
-	// Maximum size for checking file type signature (see image_type_recognition_lite.cc)
-	private static final int SIGNATURE_CHECK_SIZE = 5000;
-
-	static final byte[] JPEG_SIGNATURE = new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff};
-	private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW";
-	private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84;
-	private static final int RAF_INFO_SIZE = 160;
-	private static final int RAF_JPEG_LENGTH_VALUE_SIZE = 4;
-
-	private static final byte[] HEIF_TYPE_FTYP = new byte[] {'f', 't', 'y', 'p'};
-	private static final byte[] HEIF_BRAND_MIF1 = new byte[] {'m', 'i', 'f', '1'};
-	private static final byte[] HEIF_BRAND_HEIC = new byte[] {'h', 'e', 'i', 'c'};
-
-	// See http://fileformats.archiveteam.org/wiki/Olympus_ORF
-	private static final short ORF_SIGNATURE_1 = 0x4f52;
-	private static final short ORF_SIGNATURE_2 = 0x5352;
-	// There are two formats for Olympus Makernote Headers. Each has different identifiers and
-	// offsets to the actual data.
-	// See http://www.exiv2.org/makernote.html#R1
-	private static final byte[] ORF_MAKER_NOTE_HEADER_1 = new byte[] {(byte) 0x4f, (byte) 0x4c,
-			(byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x00}; // "OLYMP\0"
-	private static final byte[] ORF_MAKER_NOTE_HEADER_2 = new byte[] {(byte) 0x4f, (byte) 0x4c,
-			(byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x55, (byte) 0x53, (byte) 0x00,
-			(byte) 0x49, (byte) 0x49}; // "OLYMPUS\0II"
-	private static final int ORF_MAKER_NOTE_HEADER_1_SIZE = 8;
-	private static final int ORF_MAKER_NOTE_HEADER_2_SIZE = 12;
-
-	// See http://fileformats.archiveteam.org/wiki/RW2
-	private static final short RW2_SIGNATURE = 0x0055;
-
-	// See http://fileformats.archiveteam.org/wiki/Pentax_PEF
-	private static final String PEF_SIGNATURE = "PENTAX";
-	// See http://www.exiv2.org/makernote.html#R11
-	private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6;
-
-	// See PNG (Portable Network Graphics) Specification, Version 1.2,
-	// 3.1. PNG file signature
-	private static final byte[] PNG_SIGNATURE = new byte[] {(byte) 0x89, (byte) 0x50, (byte) 0x4e,
-			(byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a};
-	// See PNG (Portable Network Graphics) Specification, Version 1.2,
-	// 3.7. eXIf Exchangeable Image File (Exif) Profile
-	private static final byte[] PNG_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x65, (byte) 0x58,
-			(byte) 0x49, (byte) 0x66};
-	private static final byte[] PNG_CHUNK_TYPE_IHDR = new byte[]{(byte) 0x49, (byte) 0x48,
-			(byte) 0x44, (byte) 0x52};
-	private static final byte[] PNG_CHUNK_TYPE_IEND = new byte[]{(byte) 0x49, (byte) 0x45,
-			(byte) 0x4e, (byte) 0x44};
-	private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
-	private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
-	private static final int PNG_OFFSET_TO_IHDR_BYTES = 12;
-
-	// See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
-	private static final byte[] WEBP_SIGNATURE_1 = new byte[] {'R', 'I', 'F', 'F'};
-	private static final byte[] WEBP_SIGNATURE_2 = new byte[] {'W', 'E', 'B', 'P'};
-	private static final int WEBP_FILE_SIZE_BYTE_LENGTH = 4;
-	private static final byte[] WEBP_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x45, (byte) 0x58,
-			(byte) 0x49, (byte) 0x46};
-	private static final int WEBP_CHUNK_TYPE_BYTE_LENGTH = 4;
-	private static final int WEBP_CHUNK_SIZE_BYTE_LENGTH = 4;
-
-	private static SimpleDateFormat sFormatter;
-
-	// See Exchangeable image file format for digital still cameras: Exif version 2.2.
-	// The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
-	// They are called "Image File Directory". They have multiple data formats to cover various
-	// image metadata from GPS longitude to camera model name.
-
-	// Types of Exif byte alignments (see JEITA CP-3451C Section 4.5.2)
-	static final short BYTE_ALIGN_II = 0x4949;  // II: Intel order
-	static final short BYTE_ALIGN_MM = 0x4d4d;  // MM: Motorola order
-
-	// TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2)
-	static final byte START_CODE = 0x2a; // 42
-	private static final int IFD_OFFSET = 8;
-
-	// Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
-	private static final int IFD_FORMAT_BYTE = 1;
-	private static final int IFD_FORMAT_STRING = 2;
-	private static final int IFD_FORMAT_USHORT = 3;
-	private static final int IFD_FORMAT_ULONG = 4;
-	private static final int IFD_FORMAT_URATIONAL = 5;
-	private static final int IFD_FORMAT_SBYTE = 6;
-	private static final int IFD_FORMAT_UNDEFINED = 7;
-	private static final int IFD_FORMAT_SSHORT = 8;
-	private static final int IFD_FORMAT_SLONG = 9;
-	private static final int IFD_FORMAT_SRATIONAL = 10;
-	private static final int IFD_FORMAT_SINGLE = 11;
-	private static final int IFD_FORMAT_DOUBLE = 12;
-	// Format indicating a new IFD entry (See Adobe PageMaker® 6.0 TIFF Technical Notes, "New Tag")
-	private static final int IFD_FORMAT_IFD = 13;
-	// Names for the data formats for debugging purpose.
-	static final String[] IFD_FORMAT_NAMES = new String[] {
-			"", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
-			"SLONG", "SRATIONAL", "SINGLE", "DOUBLE"
-	};
-	// Sizes of the components of each IFD value format
-	static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
-			0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
-	};
-
-	@SuppressWarnings("WeakerAccess") /* synthetic access */
-	static final byte[] EXIF_ASCII_PREFIX = new byte[] {
-			0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
-	};
-
-	// A class for indicating EXIF rational type.
-	private static class Rational {
-		public final long numerator;
-		public final long denominator;
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		Rational(double value) {
-			this((long) (value * 10000), 10000);
-		}
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		Rational(long numerator, long denominator) {
-			// Handle erroneous case
-			if (denominator == 0) {
-				this.numerator = 0;
-				this.denominator = 1;
-				return;
-			}
-			this.numerator = numerator;
-			this.denominator = denominator;
-		}
-
-		@Override
-		public String toString() {
-			return numerator + "/" + denominator;
-		}
-
-		public double calculate() {
-			return (double) numerator / denominator;
-		}
-	}
-
-	// A class for indicating EXIF attribute.
-	public static class ExifAttribute {
-		public final int format;
-		public final int numberOfComponents;
-		public final byte[] bytes;
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
-			this.format = format;
-			this.numberOfComponents = numberOfComponents;
-			this.bytes = bytes;
-		}
-
-		public static ExifAttribute createUShort(int[] values, ByteOrder byteOrder) {
-			final ByteBuffer buffer = ByteBuffer.wrap(
-					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]);
-			buffer.order(byteOrder);
-			for (int value : values) {
-				buffer.putShort((short) value);
-			}
-			return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array());
-		}
-
-		public static ExifAttribute createUShort(int value, ByteOrder byteOrder) {
-			return createUShort(new int[] {value}, byteOrder);
-		}
-
-		public static ExifAttribute createULong(long[] values, ByteOrder byteOrder) {
-			final ByteBuffer buffer = ByteBuffer.wrap(
-					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]);
-			buffer.order(byteOrder);
-			for (long value : values) {
-				buffer.putInt((int) value);
-			}
-			return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array());
-		}
-
-		public static ExifAttribute createULong(long value, ByteOrder byteOrder) {
-			return createULong(new long[] {value}, byteOrder);
-		}
-
-		public static ExifAttribute createSLong(int[] values, ByteOrder byteOrder) {
-			final ByteBuffer buffer = ByteBuffer.wrap(
-					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]);
-			buffer.order(byteOrder);
-			for (int value : values) {
-				buffer.putInt(value);
-			}
-			return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array());
-		}
-
-		public static ExifAttribute createSLong(int value, ByteOrder byteOrder) {
-			return createSLong(new int[] {value}, byteOrder);
-		}
-
-		public static ExifAttribute createByte(String value) {
-			// Exception for GPSAltitudeRef tag
-			if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') {
-				final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') };
-				return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes);
-			}
-			final byte[] ascii = value.getBytes(US_ASCII);
-			return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii);
-		}
-
-		public static ExifAttribute createString(String value) {
-			return createString(value, US_ASCII);
-		}
-
-		public static ExifAttribute createString(String value, Charset encoding) {
-			final byte[] ascii = (value + '\0').getBytes(encoding);
-
-			return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii);
-		}
-
-		public static ExifAttribute createURational(Rational[] values, ByteOrder byteOrder) {
-			final ByteBuffer buffer = ByteBuffer.wrap(
-					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]);
-			buffer.order(byteOrder);
-			for (Rational value : values) {
-				buffer.putInt((int) value.numerator);
-				buffer.putInt((int) value.denominator);
-			}
-			return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array());
-		}
-
-		public static ExifAttribute createURational(Rational value, ByteOrder byteOrder) {
-			return createURational(new Rational[] {value}, byteOrder);
-		}
-
-		public static ExifAttribute createSRational(Rational[] values, ByteOrder byteOrder) {
-			final ByteBuffer buffer = ByteBuffer.wrap(
-					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]);
-			buffer.order(byteOrder);
-			for (Rational value : values) {
-				buffer.putInt((int) value.numerator);
-				buffer.putInt((int) value.denominator);
-			}
-			return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array());
-		}
-
-		public static ExifAttribute createSRational(Rational value, ByteOrder byteOrder) {
-			return createSRational(new Rational[] {value}, byteOrder);
-		}
-
-		public static ExifAttribute createDouble(double[] values, ByteOrder byteOrder) {
-			final ByteBuffer buffer = ByteBuffer.wrap(
-					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]);
-			buffer.order(byteOrder);
-			for (double value : values) {
-				buffer.putDouble(value);
-			}
-			return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
-		}
-
-		public static ExifAttribute createDouble(double value, ByteOrder byteOrder) {
-			return createDouble(new double[] {value}, byteOrder);
-		}
-
-		@Override
-		public String toString() {
-			return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
-		}
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		Object getValue(ByteOrder byteOrder) {
-			ByteOrderedDataInputStream inputStream = null;
-			try {
-				inputStream = new ByteOrderedDataInputStream(bytes);
-				inputStream.setByteOrder(byteOrder);
-				switch (format) {
-					case IFD_FORMAT_BYTE:
-					case IFD_FORMAT_SBYTE: {
-						// Exception for GPSAltitudeRef tag
-						if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) {
-							return new String(new char[] { (char) (bytes[0] + '0') });
-						}
-						return new String(bytes, US_ASCII);
-					}
-					case IFD_FORMAT_UNDEFINED:
-					case IFD_FORMAT_STRING: {
-						int index = 0;
-						if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
-							boolean same = true;
-							for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
-								if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
-									same = false;
-									break;
-								}
-							}
-							if (same) {
-								index = EXIF_ASCII_PREFIX.length;
-							}
-						}
-
-						StringBuilder stringBuilder = new StringBuilder();
-						while (index < numberOfComponents) {
-							int ch = bytes[index];
-							if (ch == 0) {
-								break;
-							}
-							if (ch >= 32) {
-								stringBuilder.append((char) ch);
-							} else {
-								stringBuilder.append('?');
-							}
-							++index;
-						}
-						return stringBuilder.toString();
-					}
-					case IFD_FORMAT_USHORT: {
-						final int[] values = new int[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							values[i] = inputStream.readUnsignedShort();
-						}
-						return values;
-					}
-					case IFD_FORMAT_ULONG: {
-						final long[] values = new long[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							values[i] = inputStream.readUnsignedInt();
-						}
-						return values;
-					}
-					case IFD_FORMAT_URATIONAL: {
-						final Rational[] values = new Rational[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							final long numerator = inputStream.readUnsignedInt();
-							final long denominator = inputStream.readUnsignedInt();
-							values[i] = new Rational(numerator, denominator);
-						}
-						return values;
-					}
-					case IFD_FORMAT_SSHORT: {
-						final int[] values = new int[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							values[i] = inputStream.readShort();
-						}
-						return values;
-					}
-					case IFD_FORMAT_SLONG: {
-						final int[] values = new int[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							values[i] = inputStream.readInt();
-						}
-						return values;
-					}
-					case IFD_FORMAT_SRATIONAL: {
-						final Rational[] values = new Rational[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							final long numerator = inputStream.readInt();
-							final long denominator = inputStream.readInt();
-							values[i] = new Rational(numerator, denominator);
-						}
-						return values;
-					}
-					case IFD_FORMAT_SINGLE: {
-						final double[] values = new double[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							values[i] = inputStream.readFloat();
-						}
-						return values;
-					}
-					case IFD_FORMAT_DOUBLE: {
-						final double[] values = new double[numberOfComponents];
-						for (int i = 0; i < numberOfComponents; ++i) {
-							values[i] = inputStream.readDouble();
-						}
-						return values;
-					}
-					default:
-						return null;
-				}
-			} catch (IOException e) {
-				logger.warn("IOException occurred during reading a value", e);
-				return null;
-			} finally {
-				if (inputStream != null) {
-					try {
-						inputStream.close();
-					} catch (IOException e) {
-						logger.error("IOException occurred while closing InputStream", e);
-					}
-				}
-			}
-		}
-
-		public double getDoubleValue(ByteOrder byteOrder) {
-			Object value = getValue(byteOrder);
-			if (value == null) {
-				throw new NumberFormatException("NULL can't be converted to a double value");
-			}
-			if (value instanceof String) {
-				return Double.parseDouble((String) value);
-			}
-			if (value instanceof long[]) {
-				long[] array = (long[]) value;
-				if (array.length == 1) {
-					return array[0];
-				}
-				throw new NumberFormatException("There are more than one component");
-			}
-			if (value instanceof int[]) {
-				int[] array = (int[]) value;
-				if (array.length == 1) {
-					return array[0];
-				}
-				throw new NumberFormatException("There are more than one component");
-			}
-			if (value instanceof double[]) {
-				double[] array = (double[]) value;
-				if (array.length == 1) {
-					return array[0];
-				}
-				throw new NumberFormatException("There are more than one component");
-			}
-			if (value instanceof Rational[]) {
-				Rational[] array = (Rational[]) value;
-				if (array.length == 1) {
-					return array[0].calculate();
-				}
-				throw new NumberFormatException("There are more than one component");
-			}
-			throw new NumberFormatException("Couldn't find a double value");
-		}
-
-		public int getIntValue(ByteOrder byteOrder) {
-			Object value = getValue(byteOrder);
-			if (value == null) {
-				throw new NumberFormatException("NULL can't be converted to a integer value");
-			}
-			if (value instanceof String) {
-				return Integer.parseInt((String) value);
-			}
-			if (value instanceof long[]) {
-				long[] array = (long[]) value;
-				if (array.length == 1) {
-					return (int) array[0];
-				}
-				throw new NumberFormatException("There are more than one component");
-			}
-			if (value instanceof int[]) {
-				int[] array = (int[]) value;
-				if (array.length == 1) {
-					return array[0];
-				}
-				throw new NumberFormatException("There are more than one component");
-			}
-			throw new NumberFormatException("Couldn't find a integer value");
-		}
-
-		public String getUTF8StringValue() {
-			if (bytes != null && bytes.length > 0) {
-				return new String(bytes, StandardCharsets.UTF_8);
-			}
-			return null;
-		}
-
-		public String getStringValue(ByteOrder byteOrder) {
-			Object value = getValue(byteOrder);
-			if (value == null) {
-				return null;
-			}
-			if (value instanceof String) {
-				return (String) value;
-			}
-
-			final StringBuilder stringBuilder = new StringBuilder();
-			if (value instanceof long[]) {
-				long[] array = (long[]) value;
-				for (int i = 0; i < array.length; ++i) {
-					stringBuilder.append(array[i]);
-					if (i + 1 != array.length) {
-						stringBuilder.append(",");
-					}
-				}
-				return stringBuilder.toString();
-			}
-			if (value instanceof int[]) {
-				int[] array = (int[]) value;
-				for (int i = 0; i < array.length; ++i) {
-					stringBuilder.append(array[i]);
-					if (i + 1 != array.length) {
-						stringBuilder.append(",");
-					}
-				}
-				return stringBuilder.toString();
-			}
-			if (value instanceof double[]) {
-				double[] array = (double[]) value;
-				for (int i = 0; i < array.length; ++i) {
-					stringBuilder.append(array[i]);
-					if (i + 1 != array.length) {
-						stringBuilder.append(",");
-					}
-				}
-				return stringBuilder.toString();
-			}
-			if (value instanceof Rational[]) {
-				Rational[] array = (Rational[]) value;
-				for (int i = 0; i < array.length; ++i) {
-					stringBuilder.append(array[i].numerator);
-					stringBuilder.append('/');
-					stringBuilder.append(array[i].denominator);
-					if (i + 1 != array.length) {
-						stringBuilder.append(",");
-					}
-				}
-				return stringBuilder.toString();
-			}
-			return null;
-		}
-
-		public int size() {
-			return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents;
-		}
-	}
-
-	// A class for indicating EXIF tag.
-	static class ExifTag {
-		public final int number;
-		public final String name;
-		public final int primaryFormat;
-		public final int secondaryFormat;
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		ExifTag(String name, int number, int format) {
-			this.name = name;
-			this.number = number;
-			this.primaryFormat = format;
-			this.secondaryFormat = -1;
-		}
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		ExifTag(String name, int number, int primaryFormat, int secondaryFormat) {
-			this.name = name;
-			this.number = number;
-			this.primaryFormat = primaryFormat;
-			this.secondaryFormat = secondaryFormat;
-		}
-
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		boolean isFormatCompatible(int format) {
-			if (primaryFormat == IFD_FORMAT_UNDEFINED || format == IFD_FORMAT_UNDEFINED) {
-				return true;
-			} else if (primaryFormat == format || secondaryFormat == format) {
-				return true;
-			} else if ((primaryFormat == IFD_FORMAT_ULONG || secondaryFormat == IFD_FORMAT_ULONG)
-					&& format == IFD_FORMAT_USHORT) {
-				return true;
-			} else if ((primaryFormat == IFD_FORMAT_SLONG || secondaryFormat == IFD_FORMAT_SLONG)
-					&& format == IFD_FORMAT_SSHORT) {
-				return true;
-			} else if ((primaryFormat == IFD_FORMAT_DOUBLE || secondaryFormat == IFD_FORMAT_DOUBLE)
-					&& format == IFD_FORMAT_SINGLE) {
-				return true;
-			}
-			return false;
-		}
-	}
-
-	// Primary image IFD TIFF tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
-	private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] {
-			// For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
-			new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
-			new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
-			new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
-			new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
-			new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
-			new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
-			new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
-			// See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
-			new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
-			new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
-			// RW2 file tags
-			// See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html)
-			new ExifTag(TAG_RW2_SENSOR_TOP_BORDER, 4, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_RW2_SENSOR_LEFT_BORDER, 5, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_RW2_SENSOR_BOTTOM_BORDER, 6, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_RW2_SENSOR_RIGHT_BORDER, 7, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_RW2_ISO, 23, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_RW2_JPG_FROM_RAW, 46, IFD_FORMAT_UNDEFINED)
-	};
-
-	// Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
-	private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] {
-			new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SPECTRAL_SENSITIVITY, 34852, IFD_FORMAT_STRING),
-			new ExifTag(TAG_PHOTOGRAPHIC_SENSITIVITY, 34855, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_OECF, 34856, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING),
-			new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING),
-			new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING),
-			new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_COMPRESSED_BITS_PER_PIXEL, 37122, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
-			new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
-			new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
-			new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_SUBJECT_DISTANCE, 37382, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_SUBJECT_AREA, 37396, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_MAKER_NOTE, 37500, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_USER_COMMENT, 37510, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
-			new ExifTag(TAG_SUBSEC_TIME_ORIGINAL, 37521, IFD_FORMAT_STRING),
-			new ExifTag(TAG_SUBSEC_TIME_DIGITIZED, 37522, IFD_FORMAT_STRING),
-			new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_RELATED_SOUND_FILE, 40964, IFD_FORMAT_STRING),
-			new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_FLASH_ENERGY, 41483, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_SPATIAL_FREQUENCY_RESPONSE, 41484, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_FOCAL_PLANE_X_RESOLUTION, 41486, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_FOCAL_PLANE_Y_RESOLUTION, 41487, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SUBJECT_LOCATION, 41492, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_EXPOSURE_INDEX, 41493, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_CFA_PATTERN, 41730, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_DIGITAL_ZOOM_RATIO, 41988, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_FOCAL_LENGTH_IN_35MM_FILM, 41989, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_GAIN_CONTROL, 41991, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_DEVICE_SETTING_DESCRIPTION, 41995, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_SUBJECT_DISTANCE_RANGE, 41996, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_IMAGE_UNIQUE_ID, 42016, IFD_FORMAT_STRING),
-			new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
-			new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
-	};
-
-	// Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
-	private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
-			new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
-			new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
-			new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_SATELLITES, 8, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_STATUS, 9, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_MEASURE_MODE, 10, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DOP, 11, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_SPEED, 13, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_TRACK, 15, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_IMG_DIRECTION, 17, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_MAP_DATUM, 18, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DEST_LATITUDE_REF, 19, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DEST_LATITUDE, 20, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_DEST_LONGITUDE_REF, 21, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DEST_LONGITUDE, 22, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DEST_BEARING, 24, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DEST_DISTANCE, 26, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_GPS_PROCESSING_METHOD, 27, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_GPS_AREA_INFORMATION, 28, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_GPS_DATESTAMP, 29, IFD_FORMAT_STRING),
-			new ExifTag(TAG_GPS_DIFFERENTIAL, 30, IFD_FORMAT_USHORT)
-	};
-	// Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
-	private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
-			new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
-	};
-	// IFD Thumbnail tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
-	private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
-			// For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
-			new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
-			new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
-			new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
-			new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
-			new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
-			new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
-			new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
-			// See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
-			new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
-			new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
-			new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
-			new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
-			new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
-	};
-
-	// RAF file tag (See piex.cc line 372)
-	private static final ExifTag TAG_RAF_IMAGE_SIZE =
-			new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT);
-
-	// ORF file tags (See http://www.exiv2.org/tags-olympus.html)
-	private static final ExifTag[] ORF_MAKER_NOTE_TAGS = new ExifTag[] {
-			new ExifTag(TAG_ORF_THUMBNAIL_IMAGE, 256, IFD_FORMAT_UNDEFINED),
-			new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_ULONG)
-	};
-	private static final ExifTag[] ORF_CAMERA_SETTINGS_TAGS = new ExifTag[] {
-			new ExifTag(TAG_ORF_PREVIEW_IMAGE_START, 257, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_ORF_PREVIEW_IMAGE_LENGTH, 258, IFD_FORMAT_ULONG)
-	};
-	private static final ExifTag[] ORF_IMAGE_PROCESSING_TAGS = new ExifTag[] {
-			new ExifTag(TAG_ORF_ASPECT_FRAME, 4371, IFD_FORMAT_USHORT)
-	};
-	// PEF file tag (See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Pentax.html)
-	private static final ExifTag[] PEF_TAGS = new ExifTag[] {
-			new ExifTag(TAG_COLOR_SPACE, 55, IFD_FORMAT_USHORT)
-	};
-
-	// See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
-	// The following values are used for indicating pointers to the other Image File Directories.
-
-	// Indices of Exif Ifd tag groups
-	/** @hide */
-	@RestrictTo(RestrictTo.Scope.LIBRARY)
-	@Retention(RetentionPolicy.SOURCE)
-	@IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
-			IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
-			IFD_TYPE_ORF_CAMERA_SETTINGS, IFD_TYPE_ORF_IMAGE_PROCESSING, IFD_TYPE_PEF})
-	public @interface IfdType {}
-
-	static final int IFD_TYPE_PRIMARY = 0;
-	private static final int IFD_TYPE_EXIF = 1;
-	private static final int IFD_TYPE_GPS = 2;
-	private static final int IFD_TYPE_INTEROPERABILITY = 3;
-	static final int IFD_TYPE_THUMBNAIL = 4;
-	static final int IFD_TYPE_PREVIEW = 5;
-	private static final int IFD_TYPE_ORF_MAKER_NOTE = 6;
-	private static final int IFD_TYPE_ORF_CAMERA_SETTINGS = 7;
-	private static final int IFD_TYPE_ORF_IMAGE_PROCESSING = 8;
-	private static final int IFD_TYPE_PEF = 9;
-
-	// List of Exif tag groups
-	static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
-			IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
-			IFD_THUMBNAIL_TAGS, IFD_TIFF_TAGS, ORF_MAKER_NOTE_TAGS, ORF_CAMERA_SETTINGS_TAGS,
-			ORF_IMAGE_PROCESSING_TAGS, PEF_TAGS
-	};
-	// List of tags for pointing to the other image file directory offset.
-	private static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[] {
-			new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
-			new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_BYTE),
-			new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_BYTE)
-	};
-
-	// Tags for indicating the thumbnail offset and length
-	private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
-			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG);
-	private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
-			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG);
-
-	// Mappings from tag number to tag name and each item represents one IFD tag group.
-	@SuppressWarnings("unchecked")
-	private static final HashMap<Integer, ExifTag>[] sExifTagMapsForReading =
-			new HashMap[EXIF_TAGS.length];
-	// Mappings from tag name to tag number and each item represents one IFD tag group.
-	@SuppressWarnings("unchecked")
-	private static final HashMap<String, ExifTag>[] sExifTagMapsForWriting =
-			new HashMap[EXIF_TAGS.length];
-	private static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
-			TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE,
-			TAG_GPS_TIMESTAMP));
-	// Mappings from tag number to IFD type for pointer tags.
-	@SuppressWarnings("unchecked")
-	private static final HashMap<Integer, Integer> sExifPointerTagMap = new HashMap();
-
-	// See JPEG File Interchange Format Version 1.02.
-	// The following values are defined for handling JPEG streams. In this implementation, we are
-	// not only getting information from EXIF but also from some JPEG special segments such as
-	// MARKER_COM for user comment and MARKER_SOFx for image width and height.
-	//
-	// Identifier for EXIF APP1 segment in JPEG
-	static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(US_ASCII);
-	// JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
-	// the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
-	// of frame(baseline DCT) and the image size info exists in its beginning part.
-	static final byte MARKER = (byte) 0xff;
-	private static final byte MARKER_SOI = (byte) 0xd8;
-	private static final byte MARKER_SOF0 = (byte) 0xc0;
-	private static final byte MARKER_SOF1 = (byte) 0xc1;
-	private static final byte MARKER_SOF2 = (byte) 0xc2;
-	private static final byte MARKER_SOF3 = (byte) 0xc3;
-	private static final byte MARKER_SOF5 = (byte) 0xc5;
-	private static final byte MARKER_SOF6 = (byte) 0xc6;
-	private static final byte MARKER_SOF7 = (byte) 0xc7;
-	private static final byte MARKER_SOF9 = (byte) 0xc9;
-	private static final byte MARKER_SOF10 = (byte) 0xca;
-	private static final byte MARKER_SOF11 = (byte) 0xcb;
-	private static final byte MARKER_SOF13 = (byte) 0xcd;
-	private static final byte MARKER_SOF14 = (byte) 0xce;
-	private static final byte MARKER_SOF15 = (byte) 0xcf;
-	private static final byte MARKER_SOS = (byte) 0xda;
-	static final byte MARKER_APP1 = (byte) 0xe1;
-	private static final byte MARKER_COM = (byte) 0xfe;
-	static final byte MARKER_EOI = (byte) 0xd9;
-
-	// Supported Image File Types
-	private static final int IMAGE_TYPE_UNKNOWN = 0;
-	private static final int IMAGE_TYPE_ARW = 1;
-	private static final int IMAGE_TYPE_CR2 = 2;
-	private static final int IMAGE_TYPE_DNG = 3;
-	private static final int IMAGE_TYPE_JPEG = 4;
-	private static final int IMAGE_TYPE_NEF = 5;
-	private static final int IMAGE_TYPE_NRW = 6;
-	private static final int IMAGE_TYPE_ORF = 7;
-	private static final int IMAGE_TYPE_PEF = 8;
-	private static final int IMAGE_TYPE_RAF = 9;
-	private static final int IMAGE_TYPE_RW2 = 10;
-	private static final int IMAGE_TYPE_SRW = 11;
-	private static final int IMAGE_TYPE_HEIF = 12;
-	private static final int IMAGE_TYPE_PNG = 13;
-	private static final int IMAGE_TYPE_WEBP = 14;
-
-	static {
-		sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
-		sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
-
-		// Build up the hash tables to look up Exif tags for reading Exif tags.
-		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
-			sExifTagMapsForReading[ifdType] = new HashMap<>();
-			sExifTagMapsForWriting[ifdType] = new HashMap<>();
-			for (ExifTag tag : EXIF_TAGS[ifdType]) {
-				sExifTagMapsForReading[ifdType].put(tag.number, tag);
-				sExifTagMapsForWriting[ifdType].put(tag.name, tag);
-			}
-		}
-
-		// Build up the hash table to look up Exif pointer tags.
-		sExifPointerTagMap.put(EXIF_POINTER_TAGS[0].number, IFD_TYPE_PREVIEW); // 330
-		sExifPointerTagMap.put(EXIF_POINTER_TAGS[1].number, IFD_TYPE_EXIF); // 34665
-		sExifPointerTagMap.put(EXIF_POINTER_TAGS[2].number, IFD_TYPE_GPS); // 34853
-		sExifPointerTagMap.put(EXIF_POINTER_TAGS[3].number, IFD_TYPE_INTEROPERABILITY); // 40965
-		sExifPointerTagMap.put(EXIF_POINTER_TAGS[4].number, IFD_TYPE_ORF_CAMERA_SETTINGS); // 8224
-		sExifPointerTagMap.put(EXIF_POINTER_TAGS[5].number, IFD_TYPE_ORF_IMAGE_PROCESSING); // 8256
-	}
-
-	private final String mFilename;
-	private final AssetManager.AssetInputStream mAssetInputStream;
-	private int mMimeType;
-	@SuppressWarnings("unchecked")
-	private final HashMap<String, ExifAttribute>[] mAttributes = new HashMap[EXIF_TAGS.length];
-	private Set<Integer> mAttributesOffsets = new HashSet<>(EXIF_TAGS.length);
-	private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN;
-	private boolean mHasThumbnail;
-	// The following values used for indicating a thumbnail position.
-	private int mThumbnailOffset;
-	private int mThumbnailLength;
-	private byte[] mThumbnailBytes;
-	private int mThumbnailCompression;
-	private int mExifOffset;
-	private int mOrfMakerNoteOffset;
-	private int mOrfThumbnailOffset;
-	private int mOrfThumbnailLength;
-	private int mRw2JpgFromRawOffset;
-	private boolean mIsSupportedFile;
-
-	// Pattern to check non zero timestamp
-	private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
-	// Pattern to check gps timestamp
-	private static final Pattern sGpsTimestampPattern =
-			Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$");
-
-	/**
-	 * Reads Exif tags from the specified image file.
-	 */
-	public ExifInterface(@NonNull String filename) throws IOException {
-		if (filename == null) {
-			throw new IllegalArgumentException("filename cannot be null");
-		}
-		FileInputStream in = null;
-		mAssetInputStream = null;
-		mFilename = filename;
-		try {
-			in = new FileInputStream(filename);
-			loadAttributes(in);
-		} finally {
-			closeQuietly(in);
-		}
-	}
-
-	/**
-	 * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
-	 * for input streams. The given input stream will proceed its current position. Developers
-	 * should close the input stream after use. This constructor is not intended to be used with
-	 * an input stream that performs any networking operations.
-	 */
-	public ExifInterface(@NonNull InputStream inputStream) throws IOException {
-		if (inputStream == null) {
-			throw new IllegalArgumentException("inputStream cannot be null");
-		}
-		mFilename = null;
-		if (inputStream instanceof AssetManager.AssetInputStream) {
-			mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
-		} else {
-			mAssetInputStream = null;
-		}
-		loadAttributes(inputStream);
-	}
-
-	/**
-	 * Returns whether ExifInterface currently supports parsing data from the specified mime type
-	 * or not.
-	 *
-	 * @param mimeType the string value of mime type
-	 */
-	public static boolean isSupportedMimeType(@NonNull String mimeType) {
-		if (mimeType == null) {
-			throw new NullPointerException("mimeType shouldn't be null");
-		}
-
-		switch (mimeType.toLowerCase(Locale.ROOT)) {
-			case "image/jpeg":
-			case "image/x-adobe-dng":
-			case "image/x-canon-cr2":
-			case "image/x-nikon-nef":
-			case "image/x-nikon-nrw":
-			case "image/x-sony-arw":
-			case "image/x-panasonic-rw2":
-			case "image/x-olympus-orf":
-			case "image/x-pentax-pef":
-			case "image/x-samsung-srw":
-			case "image/x-fuji-raf":
-			case "image/heic":
-			case "image/heif":
-				return true;
-			default:
-				return false;
-		}
-	}
-
-	/**
-	 * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag in
-	 * the image file.
-	 *
-	 * @param tag the name of the tag.
-	 */
-	@Nullable
-	public ExifAttribute getExifAttribute(@NonNull String tag) {
-		if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
-			if (DEBUG) {
-				logger.debug("getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
-						+ "TAG_PHOTOGRAPHIC_SENSITIVITY.");
-			}
-			tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
-		}
-		// Retrieves all tag groups. The value from primary image tag group has a higher priority
-		// than the value from the thumbnail tag group if there are more than one candidates.
-		for (int i = 0; i < EXIF_TAGS.length; ++i) {
-			ExifAttribute value = mAttributes[i].get(tag);
-			if (value != null) {
-				return value;
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Returns the value of the specified tag assuming it's a UTF-8 string or {@code null} if there
-	 * is no such tag in the image file.
-	 * @param tag
-	 * @return
-	 */
-	@Nullable
-	public String getUTF8StringAttribute(@NonNull String tag) {
-		ExifAttribute attribute = getExifAttribute(tag);
-		if (attribute != null) {
-			return attribute.getUTF8StringValue();
-		}
-		return null;
-	}
-
-	/**
-	 * Returns the value of the specified tag or {@code null} if there
-	 * is no such tag in the image file.
-	 *
-	 * @param tag the name of the tag.
-	 */
-	@Nullable
-	public String getAttribute(@NonNull String tag) {
-		ExifAttribute attribute = getExifAttribute(tag);
-		if (attribute != null) {
-			if (!sTagSetForCompatibility.contains(tag)) {
-				return attribute.getStringValue(mExifByteOrder);
-			}
-			if (tag.equals(TAG_GPS_TIMESTAMP)) {
-				// Convert the rational values to the custom formats for backwards compatibility.
-				if (attribute.format != IFD_FORMAT_URATIONAL
-						&& attribute.format != IFD_FORMAT_SRATIONAL) {
-					logger.warn("GPS Timestamp format is not rational. format=" + attribute.format);
-					return null;
-				}
-				Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
-				if (array == null || array.length != 3) {
-					logger.warn("Invalid GPS Timestamp array. array=" + Arrays.toString(array));
-					return null;
-				}
-				return String.format("%02d:%02d:%02d",
-						(int) ((float) array[0].numerator / array[0].denominator),
-						(int) ((float) array[1].numerator / array[1].denominator),
-						(int) ((float) array[2].numerator / array[2].denominator));
-			}
-			try {
-				return Double.toString(attribute.getDoubleValue(mExifByteOrder));
-			} catch (NumberFormatException e) {
-				return null;
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Returns the integer value of the specified tag. If there is no such tag
-	 * in the image file or the value cannot be parsed as integer, return
-	 * <var>defaultValue</var>.
-	 *
-	 * @param tag the name of the tag.
-	 * @param defaultValue the value to return if the tag is not available.
-	 */
-	public int getAttributeInt(@NonNull String tag, int defaultValue) {
-		ExifAttribute exifAttribute = getExifAttribute(tag);
-		if (exifAttribute == null) {
-			return defaultValue;
-		}
-
-		try {
-			return exifAttribute.getIntValue(mExifByteOrder);
-		} catch (NumberFormatException e) {
-			return defaultValue;
-		}
-	}
-
-	/**
-	 * Returns the double value of the tag that is specified as rational or contains a
-	 * double-formatted value. If there is no such tag in the image file or the value cannot be
-	 * parsed as double, return <var>defaultValue</var>.
-	 *
-	 * @param tag the name of the tag.
-	 * @param defaultValue the value to return if the tag is not available.
-	 */
-	public double getAttributeDouble(@NonNull String tag, double defaultValue) {
-		ExifAttribute exifAttribute = getExifAttribute(tag);
-		if (exifAttribute == null) {
-			return defaultValue;
-		}
-
-		try {
-			return exifAttribute.getDoubleValue(mExifByteOrder);
-		} catch (NumberFormatException e) {
-			return defaultValue;
-		}
-	}
-
-	/**
-	 * Sets the value of the specified tag.
-	 *
-	 * @param tag the name of the tag.
-	 * @param value the value of the tag.
-	 */
-	public void setAttribute(@NonNull String tag, @Nullable String value) {
-		setAttribute(tag, value, US_ASCII);
-	}
-
-	/**
-	 * Sets the value of the specified tag.
-	 *
-	 * @param tag the name of the tag.
-	 * @param value the value of the tag.
-	 * @param encoding string charset, defaults to ASCII
-	 */
-	public void setAttribute(@NonNull String tag, @Nullable String value, Charset encoding) {
-		if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
-			if (DEBUG) {
-				logger.debug("setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
-						+ "TAG_PHOTOGRAPHIC_SENSITIVITY.");
-			}
-			tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
-		}
-		// Convert the given value to rational values for backwards compatibility.
-		if (value != null && sTagSetForCompatibility.contains(tag)) {
-			if (tag.equals(TAG_GPS_TIMESTAMP)) {
-				Matcher m = sGpsTimestampPattern.matcher(value);
-				if (!m.find()) {
-					logger.warn("Invalid value for " + tag + " : " + value);
-					return;
-				}
-				value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1,"
-						+ Integer.parseInt(m.group(3)) + "/1";
-			} else {
-				try {
-					double doubleValue = Double.parseDouble(value);
-					value = new Rational(doubleValue).toString();
-				} catch (NumberFormatException e) {
-					logger.warn("Invalid value for " + tag + " : " + value);
-					return;
-				}
-			}
-		}
-
-		for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
-			if (i == IFD_TYPE_THUMBNAIL && !mHasThumbnail) {
-				continue;
-			}
-			final ExifTag exifTag = sExifTagMapsForWriting[i].get(tag);
-			if (exifTag != null) {
-				if (value == null) {
-					mAttributes[i].remove(tag);
-					continue;
-				}
-				Pair<Integer, Integer> guess = guessDataFormat(value);
-				int dataFormat;
-				if (exifTag.primaryFormat == guess.first || exifTag.primaryFormat == guess.second) {
-					dataFormat = exifTag.primaryFormat;
-				} else if (exifTag.secondaryFormat != -1 && (exifTag.secondaryFormat == guess.first
-						|| exifTag.secondaryFormat == guess.second)) {
-					dataFormat = exifTag.secondaryFormat;
-				} else if (exifTag.primaryFormat == IFD_FORMAT_BYTE
-						|| exifTag.primaryFormat == IFD_FORMAT_UNDEFINED
-						|| exifTag.primaryFormat == IFD_FORMAT_STRING) {
-					dataFormat = exifTag.primaryFormat;
-				} else {
-					logger.warn("Given tag (" + tag + ") value didn't match with one of expected "
-							+ "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat]
-							+ (exifTag.secondaryFormat == -1 ? "" : ", "
-							+ IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: "
-							+ IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? "" : ", "
-							+ IFD_FORMAT_NAMES[guess.second]) + ")");
-					continue;
-				}
-				switch (dataFormat) {
-					case IFD_FORMAT_BYTE: {
-						mAttributes[i].put(tag, ExifAttribute.createByte(value));
-						break;
-					}
-					case IFD_FORMAT_UNDEFINED:
-					case IFD_FORMAT_STRING: {
-						mAttributes[i].put(tag, ExifAttribute.createString(value, encoding));
-						break;
-					}
-					case IFD_FORMAT_USHORT: {
-						final String[] values = value.split(",", -1);
-						final int[] intArray = new int[values.length];
-						for (int j = 0; j < values.length; ++j) {
-							intArray[j] = Integer.parseInt(values[j]);
-						}
-						mAttributes[i].put(tag,
-								ExifAttribute.createUShort(intArray, mExifByteOrder));
-						break;
-					}
-					case IFD_FORMAT_SLONG: {
-						final String[] values = value.split(",", -1);
-						final int[] intArray = new int[values.length];
-						for (int j = 0; j < values.length; ++j) {
-							intArray[j] = Integer.parseInt(values[j]);
-						}
-						mAttributes[i].put(tag,
-								ExifAttribute.createSLong(intArray, mExifByteOrder));
-						break;
-					}
-					case IFD_FORMAT_ULONG: {
-						final String[] values = value.split(",", -1);
-						final long[] longArray = new long[values.length];
-						for (int j = 0; j < values.length; ++j) {
-							longArray[j] = Long.parseLong(values[j]);
-						}
-						mAttributes[i].put(tag,
-								ExifAttribute.createULong(longArray, mExifByteOrder));
-						break;
-					}
-					case IFD_FORMAT_URATIONAL: {
-						final String[] values = value.split(",", -1);
-						final Rational[] rationalArray = new Rational[values.length];
-						for (int j = 0; j < values.length; ++j) {
-							final String[] numbers = values[j].split("/", -1);
-							rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
-									(long) Double.parseDouble(numbers[1]));
-						}
-						mAttributes[i].put(tag,
-								ExifAttribute.createURational(rationalArray, mExifByteOrder));
-						break;
-					}
-					case IFD_FORMAT_SRATIONAL: {
-						final String[] values = value.split(",", -1);
-						final Rational[] rationalArray = new Rational[values.length];
-						for (int j = 0; j < values.length; ++j) {
-							final String[] numbers = values[j].split("/", -1);
-							rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
-									(long) Double.parseDouble(numbers[1]));
-						}
-						mAttributes[i].put(tag,
-								ExifAttribute.createSRational(rationalArray, mExifByteOrder));
-						break;
-					}
-					case IFD_FORMAT_DOUBLE: {
-						final String[] values = value.split(",", -1);
-						final double[] doubleArray = new double[values.length];
-						for (int j = 0; j < values.length; ++j) {
-							doubleArray[j] = Double.parseDouble(values[j]);
-						}
-						mAttributes[i].put(tag,
-								ExifAttribute.createDouble(doubleArray, mExifByteOrder));
-						break;
-					}
-					default:
-						logger.warn("Data format isn't one of expected formats: " + dataFormat);
-						continue;
-				}
-			}
-		}
-	}
-
-	/**
-	 * Resets the {@link #TAG_ORIENTATION} of the image to be {@link #ORIENTATION_NORMAL}.
-	 */
-	public void resetOrientation() {
-		setAttribute(TAG_ORIENTATION, Integer.toString(ORIENTATION_NORMAL));
-	}
-
-	/**
-	 * Rotates the image by the given degree clockwise. The degree should be a multiple of
-	 * 90 (e.g, 90, 180, -90, etc.).
-	 *
-	 * @param degree The degree of rotation.
-	 */
-	public void rotate(int degree) throws IllegalArgumentException {
-		if (degree % 90 !=0) {
-			throw new IllegalArgumentException("degree should be a multiple of 90");
-		}
-
-		int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
-		int currentIndex, newIndex;
-		int resultOrientation;
-		if (ROTATION_ORDER.contains(currentOrientation)) {
-			currentIndex = ROTATION_ORDER.indexOf(currentOrientation);
-			newIndex = (currentIndex + degree / 90) % 4;
-			newIndex += newIndex < 0 ? 4 : 0;
-			resultOrientation = ROTATION_ORDER.get(newIndex);
-		} else if (FLIPPED_ROTATION_ORDER.contains(currentOrientation)) {
-			currentIndex = FLIPPED_ROTATION_ORDER.indexOf(currentOrientation);
-			newIndex = (currentIndex + degree / 90) % 4;
-			newIndex += newIndex < 0 ? 4 : 0;
-			resultOrientation = FLIPPED_ROTATION_ORDER.get(newIndex);
-		} else {
-			resultOrientation = ORIENTATION_UNDEFINED;
-		}
-
-		setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
-	}
-
-	/**
-	 * Flips the image vertically.
-	 */
-	public void flipVertically() {
-		int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
-		int resultOrientation;
-		switch (currentOrientation) {
-			case ORIENTATION_FLIP_HORIZONTAL:
-				resultOrientation = ORIENTATION_ROTATE_180;
-				break;
-			case ORIENTATION_ROTATE_180:
-				resultOrientation = ORIENTATION_FLIP_HORIZONTAL;
-				break;
-			case ORIENTATION_FLIP_VERTICAL:
-				resultOrientation = ORIENTATION_NORMAL;
-				break;
-			case ORIENTATION_TRANSPOSE:
-				resultOrientation = ORIENTATION_ROTATE_270;
-				break;
-			case ORIENTATION_ROTATE_90:
-				resultOrientation = ORIENTATION_TRANSVERSE;
-				break;
-			case ORIENTATION_TRANSVERSE:
-				resultOrientation = ORIENTATION_ROTATE_90;
-				break;
-			case ORIENTATION_ROTATE_270:
-				resultOrientation = ORIENTATION_TRANSPOSE;
-				break;
-			case ORIENTATION_NORMAL:
-				resultOrientation = ORIENTATION_FLIP_VERTICAL;
-				break;
-			case ORIENTATION_UNDEFINED:
-			default:
-				resultOrientation = ORIENTATION_UNDEFINED;
-				break;
-		}
-		setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
-	}
-
-	/**
-	 * Flips the image horizontally.
-	 */
-	public void flipHorizontally() {
-		int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
-		int resultOrientation;
-		switch (currentOrientation) {
-			case ORIENTATION_FLIP_HORIZONTAL:
-				resultOrientation = ORIENTATION_NORMAL;
-				break;
-			case ORIENTATION_ROTATE_180:
-				resultOrientation = ORIENTATION_FLIP_VERTICAL;
-				break;
-			case ORIENTATION_FLIP_VERTICAL:
-				resultOrientation = ORIENTATION_ROTATE_180;
-				break;
-			case ORIENTATION_TRANSPOSE:
-				resultOrientation = ORIENTATION_ROTATE_90;
-				break;
-			case ORIENTATION_ROTATE_90:
-				resultOrientation = ORIENTATION_TRANSPOSE;
-				break;
-			case ORIENTATION_TRANSVERSE:
-				resultOrientation = ORIENTATION_ROTATE_270;
-				break;
-			case ORIENTATION_ROTATE_270:
-				resultOrientation = ORIENTATION_TRANSVERSE;
-				break;
-			case ORIENTATION_NORMAL:
-				resultOrientation = ORIENTATION_FLIP_HORIZONTAL;
-				break;
-			case ORIENTATION_UNDEFINED:
-			default:
-				resultOrientation = ORIENTATION_UNDEFINED;
-				break;
-		}
-		setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
-	}
-
-	/**
-	 * Returns if the current image orientation is flipped.
-	 *
-	 * @see #getRotationDegrees()
-	 */
-	public boolean isFlipped() {
-		int orientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
-		switch (orientation) {
-			case ORIENTATION_FLIP_HORIZONTAL:
-			case ORIENTATION_TRANSVERSE:
-			case ORIENTATION_FLIP_VERTICAL:
-			case ORIENTATION_TRANSPOSE:
-				return true;
-			default:
-				return false;
-		}
-	}
-
-	/**
-	 * Returns the rotation degrees for the current image orientation. If the image is flipped,
-	 * i.e., {@link #isFlipped()} returns {@code true}, the rotation degrees will be base on
-	 * the assumption that the image is first flipped horizontally (along Y-axis), and then do
-	 * the rotation. For example, {@link #ORIENTATION_TRANSPOSE} will be interpreted as flipped
-	 * horizontally first, and then rotate 270 degrees clockwise.
-	 *
-	 * @return The rotation degrees of the image after the horizontal flipping is applied, if any.
-	 *
-	 * @see #isFlipped()
-	 */
-	public int getRotationDegrees() {
-		int orientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
-		switch (orientation) {
-			case ORIENTATION_ROTATE_90:
-			case ORIENTATION_TRANSVERSE:
-				return 90;
-			case ORIENTATION_ROTATE_180:
-			case ORIENTATION_FLIP_VERTICAL:
-				return 180;
-			case ORIENTATION_ROTATE_270:
-			case ORIENTATION_TRANSPOSE:
-				return 270;
-			case ORIENTATION_UNDEFINED:
-			case ORIENTATION_NORMAL:
-			case ORIENTATION_FLIP_HORIZONTAL:
-			default:
-				return 0;
-		}
-	}
-
-	/**
-	 * Update the values of the tags in the tag groups if any value for the tag already was stored.
-	 *
-	 * @param tag the name of the tag.
-	 * @param value the value of the tag in a form of {@link ExifAttribute}.
-	 * @return Returns {@code true} if updating is placed.
-	 */
-	private boolean updateAttribute(String tag, ExifAttribute value) {
-		boolean updated = false;
-		for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
-			if (mAttributes[i].containsKey(tag)) {
-				mAttributes[i].put(tag, value);
-				updated = true;
-			}
-		}
-		return updated;
-	}
-
-	/**
-	 * Remove any values of the specified tag.
-	 *
-	 * @param tag the name of the tag.
-	 */
-	private void removeAttribute(String tag) {
-		for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
-			mAttributes[i].remove(tag);
-		}
-	}
-
-	/**
-	 * This function decides which parser to read the image data according to the given input stream
-	 * type and the content of the input stream. In each case, it reads the first three bytes to
-	 * determine whether the image data format is JPEG or not.
-	 */
-	private void loadAttributes(@NonNull InputStream in) throws IOException {
-		try {
-			// Initialize mAttributes.
-			for (int i = 0; i < EXIF_TAGS.length; ++i) {
-				mAttributes[i] = new HashMap<>();
-			}
-
-			// Check file type
-			in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE);
-			mMimeType = getMimeType((BufferedInputStream) in);
-
-			// Create byte-ordered input stream
-			ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in);
-
-			switch (mMimeType) {
-				case IMAGE_TYPE_JPEG: {
-					getJpegAttributes(inputStream, 0, IFD_TYPE_PRIMARY); // 0 is offset
-					break;
-				}
-				case IMAGE_TYPE_RAF: {
-					getRafAttributes(inputStream);
-					break;
-				}
-				case IMAGE_TYPE_HEIF: {
-					getHeifAttributes(inputStream);
-					break;
-				}
-				case IMAGE_TYPE_ORF: {
-					getOrfAttributes(inputStream);
-					break;
-				}
-				case IMAGE_TYPE_RW2: {
-					getRw2Attributes(inputStream);
-					break;
-				}
-				case IMAGE_TYPE_PNG: {
-					getPngAttributes(inputStream);
-					break;
-				}
-				case IMAGE_TYPE_WEBP: {
-					getWebpAttributes(inputStream);
-					break;
-				}
-				case IMAGE_TYPE_ARW:
-				case IMAGE_TYPE_CR2:
-				case IMAGE_TYPE_DNG:
-				case IMAGE_TYPE_NEF:
-				case IMAGE_TYPE_NRW:
-				case IMAGE_TYPE_PEF:
-				case IMAGE_TYPE_SRW:
-				case IMAGE_TYPE_UNKNOWN: {
-					getRawAttributes(inputStream);
-					break;
-				}
-				default: {
-					break;
-				}
-			}
-			// Set thumbnail image offset and length
-			setThumbnailData(inputStream);
-			mIsSupportedFile = true;
-		} catch (IOException e) {
-			// Ignore exceptions in order to keep the compatibility with the old versions of
-			// ExifInterface.
-			mIsSupportedFile = false;
-			if (DEBUG) {
-				logger.warn("Invalid image: ExifInterface got an unsupported image format file"
-						+ "(ExifInterface supports JPEG and some RAW image formats only) "
-						+ "or a corrupted JPEG file to ExifInterface.", e);
-			}
-		} finally {
-			addDefaultValuesForCompatibility();
-
-			if (DEBUG) {
-				printAttributes();
-			}
-		}
-	}
-
-	// Prints out attributes for debugging.
-	private void printAttributes() {
-		for (int i = 0; i < mAttributes.length; ++i) {
-			logger.debug("The size of tag group[" + i + "]: " + mAttributes[i].size());
-			for (Map.Entry<String, ExifAttribute> entry : mAttributes[i].entrySet()) {
-				final ExifAttribute tagValue = entry.getValue();
-				logger.debug("tagName: " + entry.getKey() + ", tagType: " + tagValue.toString()
-						+ ", tagValue: '" + tagValue.getStringValue(mExifByteOrder) + "'");
-			}
-		}
-	}
-
-	/**
-	 * Save the tag data into the original image file. This is expensive because it involves
-	 * copying all the data from one file to another and deleting the old file and renaming the
-	 * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
-	 * and make a single call rather than multiple calls for each attribute.
-	 * <p>
-	 * This method is only supported for JPEG files.
-	 * </p>
-	 */
-	public void saveAttributes() throws IOException {
-		if (!mIsSupportedFile || mMimeType != IMAGE_TYPE_JPEG) {
-			throw new IOException("ExifInterface only supports saving attributes on JPEG formats.");
-		}
-		if (mFilename == null) {
-			throw new IOException(
-					"ExifInterface does not support saving attributes for the current input.");
-		}
-
-		// Keep the thumbnail in memory
-		mThumbnailBytes = getThumbnail();
-
-		File tempFile = new File(mFilename + ".tmp");
-		File originalFile = new File(mFilename);
-		if (!originalFile.renameTo(tempFile)) {
-			throw new IOException("Could not rename to " + tempFile.getAbsolutePath());
-		}
-
-		FileInputStream in = null;
-		FileOutputStream out = null;
-		try {
-			// Save the new file.
-			in = new FileInputStream(tempFile);
-			out = new FileOutputStream(mFilename);
-			saveJpegAttributes(in, out, true);
-		} finally {
-			closeQuietly(in);
-			closeQuietly(out);
-			FileUtil.deleteFileOrWarn(tempFile, "saveAttributes", logger);
-		}
-
-		// Discard the thumbnail in memory
-		mThumbnailBytes = null;
-	}
-
-	/**
-	 * Save the tag data into the original image file. This is expensive because it involves
-	 * copying all the data from one file to another and deleting the old file and renaming the
-	 * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
-	 * and make a single call rather than multiple calls for each attribute.
-	 * <p>
-	 * This method is only supported for JPEG files.
-	 * </p>
-	 */
-	public void saveAttributes(InputStream inputStream, OutputStream outputStream, boolean noExif) throws IOException {
-		if (!mIsSupportedFile || mMimeType != IMAGE_TYPE_JPEG) {
-			throw new IOException("ExifInterface only supports saving attributes on JPEG formats.");
-		}
-
-		// Keep the thumbnail in memory
-		mThumbnailBytes = getThumbnail();
-
-		saveJpegAttributes(inputStream, outputStream, noExif);
-
-		// Discard the thumbnail in memory
-		mThumbnailBytes = null;
-	}
-
-	/**
-	 * Returns true if the image file has a thumbnail.
-	 */
-	public boolean hasThumbnail() {
-		return mHasThumbnail;
-	}
-
-	/**
-	 * Returns the JPEG compressed thumbnail inside the image file, or {@code null} if there is no
-	 * JPEG compressed thumbnail.
-	 * The returned data can be decoded using
-	 * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
-	 */
-	@Nullable
-	public byte[] getThumbnail() {
-		if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
-			return getThumbnailBytes();
-		}
-		return null;
-	}
-
-	/**
-	 * Returns the thumbnail bytes inside the image file, regardless of the compression type of the
-	 * thumbnail image.
-	 */
-	@Nullable
-	public byte[] getThumbnailBytes() {
-		if (!mHasThumbnail) {
-			return null;
-		}
-		if (mThumbnailBytes != null) {
-			return mThumbnailBytes;
-		}
-
-		// Read the thumbnail.
-		InputStream in = null;
-		try {
-			if (mAssetInputStream != null) {
-				in = mAssetInputStream;
-				if (in.markSupported()) {
-					in.reset();
-				} else {
-					logger.debug("Cannot read thumbnail from inputstream without mark/reset support");
-					return null;
-				}
-			} else if (mFilename != null) {
-				in = new FileInputStream(mFilename);
-			}
-			if (in == null) {
-				// Should not be reached this.
-				throw new FileNotFoundException();
-			}
-			if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
-				throw new IOException("Corrupted image");
-			}
-			byte[] buffer = new byte[mThumbnailLength];
-			if (in.read(buffer) != mThumbnailLength) {
-				throw new IOException("Corrupted image");
-			}
-			mThumbnailBytes = buffer;
-			return buffer;
-		} catch (IOException e) {
-			// Couldn't get a thumbnail image.
-			logger.debug("Encountered exception while getting thumbnail", e);
-		} finally {
-			closeQuietly(in);
-		}
-		return null;
-	}
-
-	/**
-	 * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the
-	 * thumbnail compression value, or {@code null} if the compression type is unsupported.
-	 */
-	@Nullable
-	public Bitmap getThumbnailBitmap() {
-		if (!mHasThumbnail) {
-			return null;
-		} else if (mThumbnailBytes == null) {
-			mThumbnailBytes = getThumbnailBytes();
-		}
-
-		if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
-			return BitmapFactory.decodeByteArray(mThumbnailBytes, 0, mThumbnailLength);
-		} else if (mThumbnailCompression == DATA_UNCOMPRESSED) {
-			int[] rgbValues = new int[mThumbnailBytes.length / 3];
-			byte alpha = (byte) 0xff000000;
-			for (int i = 0; i < rgbValues.length; i++) {
-				rgbValues[i] = alpha + (mThumbnailBytes[3 * i] << 16)
-						+ (mThumbnailBytes[3 * i + 1] << 8) + mThumbnailBytes[3 * i + 2];
-			}
-
-			ExifAttribute imageLengthAttribute =
-					(ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_IMAGE_LENGTH);
-			ExifAttribute imageWidthAttribute =
-					(ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_IMAGE_WIDTH);
-			if (imageLengthAttribute != null && imageWidthAttribute != null) {
-				int imageLength = imageLengthAttribute.getIntValue(mExifByteOrder);
-				int imageWidth = imageWidthAttribute.getIntValue(mExifByteOrder);
-				return Bitmap.createBitmap(
-						rgbValues, imageWidth, imageLength, Bitmap.Config.ARGB_8888);
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Returns true if thumbnail image is JPEG Compressed, or false if either thumbnail image does
-	 * not exist or thumbnail image is uncompressed.
-	 */
-	public boolean isThumbnailCompressed() {
-		return mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED;
-	}
-
-	/**
-	 * Returns the offset and length of thumbnail inside the image file, or
-	 * {@code null} if there is no thumbnail.
-	 *
-	 * @return two-element array, the offset in the first value, and length in
-	 *         the second, or {@code null} if no thumbnail was found.
-	 */
-	@Nullable
-	public long[] getThumbnailRange() {
-		if (!mHasThumbnail) {
-			return null;
-		}
-
-		long[] range = new long[2];
-		range[0] = mThumbnailOffset;
-		range[1] = mThumbnailLength;
-
-		return range;
-	}
-
-	/**
-	 * Stores the latitude and longitude value in a float array. The first element is the latitude,
-	 * and the second element is the longitude. Returns false if the Exif tags are not available.
-	 *
-	 * @deprecated Use {@link #getLatLong()} instead.
-	 */
-	@Deprecated
-	public boolean getLatLong(float output[]) {
-		double[] latLong = getLatLong();
-		if (latLong == null) {
-			return false;
-		}
-
-		output[0] = (float) latLong[0];
-		output[1] = (float) latLong[1];
-		return true;
-	}
-
-	/**
-	 * Gets the latitude and longitude values.
-	 * <p>
-	 * If there are valid latitude and longitude values in the image, this method returns a double
-	 * array where the first element is the latitude and the second element is the longitude.
-	 * Otherwise, it returns null.
-	 */
-	@Nullable
-	public double[] getLatLong() {
-		String latValue = getAttribute(TAG_GPS_LATITUDE);
-		String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
-		String lngValue = getAttribute(TAG_GPS_LONGITUDE);
-		String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF);
-
-		if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
-			try {
-				double latitude = convertRationalLatLonToDouble(latValue, latRef);
-				double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
-				return new double[] {latitude, longitude};
-			} catch (IllegalArgumentException e) {
-				logger.warn("Latitude/longitude values are not parseable. " +
-						String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
-								latValue, latRef, lngValue, lngRef));
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Sets the GPS-related information. It will set GPS processing method, latitude and longitude
-	 * values, GPS timestamp, and speed information at the same time.
-	 *
-	 * @param location the {@link Location} object returned by GPS service.
-	 */
-	public void setGpsInfo(Location location) {
-		if (location == null) {
-			return;
-		}
-		setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
-		setLatLong(location.getLatitude(), location.getLongitude());
-		setAltitude(location.getAltitude());
-		// Location objects store speeds in m/sec. Translates it to km/hr here.
-		setAttribute(TAG_GPS_SPEED_REF, "K");
-		setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
-				* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
-		String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+", -1);
-		setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
-		setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
-	}
-
-	/**
-	 * Sets the latitude and longitude values.
-	 *
-	 * @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
-	 *                 90.0.
-	 * @param longitude the decimal value of longitude. Must be a valid double value between -180.0
-	 *                  and 180.0.
-	 * @throws IllegalArgumentException If {@code latitude} or {@code longitude} is outside the
-	 *                                  specified range.
-	 */
-	public void setLatLong(double latitude, double longitude) {
-		if (latitude < -90.0 || latitude > 90.0 || Double.isNaN(latitude)) {
-			throw new IllegalArgumentException("Latitude value " + latitude + " is not valid.");
-		}
-		if (longitude < -180.0 || longitude > 180.0 || Double.isNaN(longitude)) {
-			throw new IllegalArgumentException("Longitude value " + longitude + " is not valid.");
-		}
-		setAttribute(TAG_GPS_LATITUDE_REF, latitude >= 0 ? "N" : "S");
-		setAttribute(TAG_GPS_LATITUDE, convertDecimalDegree(Math.abs(latitude)));
-		setAttribute(TAG_GPS_LONGITUDE_REF, longitude >= 0 ? "E" : "W");
-		setAttribute(TAG_GPS_LONGITUDE, convertDecimalDegree(Math.abs(longitude)));
-	}
-
-	/**
-	 * Return the altitude in meters. If the exif tag does not exist, return
-	 * <var>defaultValue</var>.
-	 *
-	 * @param defaultValue the value to return if the tag is not available.
-	 */
-	public double getAltitude(double defaultValue) {
-		double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
-		int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
-
-		if (altitude >= 0 && ref >= 0) {
-			return (altitude * ((ref == 1) ? -1 : 1));
-		} else {
-			return defaultValue;
-		}
-	}
-
-	/**
-	 * Sets the altitude in meters.
-	 */
-	public void setAltitude(double altitude) {
-		String ref = altitude >= 0 ? "0" : "1";
-		setAttribute(TAG_GPS_ALTITUDE, new Rational(Math.abs(altitude)).toString());
-		setAttribute(TAG_GPS_ALTITUDE_REF, ref);
-	}
-
-	/**
-	 * Set the date time value.
-	 *
-	 * @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
-	 * @hide
-	 */
-	@RestrictTo(RestrictTo.Scope.LIBRARY)
-	public void setDateTime(long timeStamp) {
-		long sub = timeStamp % 1000;
-		setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
-		setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
-	}
-
-	/**
-	 * Returns number of milliseconds since Jan. 1, 1970, midnight local time.
-	 * Returns -1 if the date time information if not available.
-	 * @hide
-	 */
-	@RestrictTo(RestrictTo.Scope.LIBRARY)
-	public long getDateTime() {
-		String dateTimeString = getAttribute(TAG_DATETIME);
-		if (dateTimeString == null
-				|| !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1;
-
-		ParsePosition pos = new ParsePosition(0);
-		try {
-			// The exif field is in local time. Parsing it as if it is UTC will yield time
-			// since 1/1/1970 local time
-			Date datetime = sFormatter.parse(dateTimeString, pos);
-			if (datetime == null) return -1;
-			long msecs = datetime.getTime();
-
-			String subSecs = getAttribute(TAG_SUBSEC_TIME);
-			if (subSecs != null) {
-				try {
-					long sub = Long.parseLong(subSecs);
-					while (sub > 1000) {
-						sub /= 10;
-					}
-					msecs += sub;
-				} catch (NumberFormatException e) {
-					// Ignored
-				}
-			}
-			return msecs;
-		} catch (IllegalArgumentException e) {
-			return -1;
-		}
-	}
-
-	/**
-	 * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
-	 * Returns -1 if the date time information if not available.
-	 * @hide
-	 */
-	@RestrictTo(RestrictTo.Scope.LIBRARY)
-	public long getGpsDateTime() {
-		String date = getAttribute(TAG_GPS_DATESTAMP);
-		String time = getAttribute(TAG_GPS_TIMESTAMP);
-		if (date == null || time == null
-				|| (!sNonZeroTimePattern.matcher(date).matches()
-				&& !sNonZeroTimePattern.matcher(time).matches())) {
-			return -1;
-		}
-
-		String dateTimeString = date + ' ' + time;
-
-		ParsePosition pos = new ParsePosition(0);
-		try {
-			Date datetime = sFormatter.parse(dateTimeString, pos);
-			if (datetime == null) return -1;
-			return datetime.getTime();
-		} catch (IllegalArgumentException e) {
-			return -1;
-		}
-	}
-
-	private static double convertRationalLatLonToDouble(String rationalString, String ref) {
-		try {
-			String [] parts = rationalString.split(",", -1);
-
-			String [] pair;
-			pair = parts[0].split("/", -1);
-			double degrees = Double.parseDouble(pair[0].trim())
-					/ Double.parseDouble(pair[1].trim());
-
-			pair = parts[1].split("/", -1);
-			double minutes = Double.parseDouble(pair[0].trim())
-					/ Double.parseDouble(pair[1].trim());
-
-			pair = parts[2].split("/", -1);
-			double seconds = Double.parseDouble(pair[0].trim())
-					/ Double.parseDouble(pair[1].trim());
-
-			double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
-			if ((ref.equals("S") || ref.equals("W"))) {
-				return -result;
-			} else if (ref.equals("N") || ref.equals("E")) {
-				return result;
-			} else {
-				// Not valid
-				throw new IllegalArgumentException();
-			}
-		} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
-			// Not valid
-			throw new IllegalArgumentException();
-		}
-	}
-
-	private String convertDecimalDegree(double decimalDegree) {
-		long degrees = (long) decimalDegree;
-		long minutes = (long) ((decimalDegree - degrees) * 60.0);
-		long seconds = Math.round((decimalDegree - degrees - minutes / 60.0) * 3600.0 * 1e7);
-		return degrees + "/1," + minutes + "/1," + seconds + "/10000000";
-	}
-
-	// Checks the type of image file
-	private int getMimeType(BufferedInputStream in) throws IOException {
-		// TODO (b/142218289): Need to handle case where input stream does not support mark
-		in.mark(SIGNATURE_CHECK_SIZE);
-		byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE];
-		in.read(signatureCheckBytes);
-		in.reset();
-		if (isJpegFormat(signatureCheckBytes)) {
-			return IMAGE_TYPE_JPEG;
-		} else if (isRafFormat(signatureCheckBytes)) {
-			return IMAGE_TYPE_RAF;
-		} else if (isHeifFormat(signatureCheckBytes)) {
-			return IMAGE_TYPE_HEIF;
-		} else if (isOrfFormat(signatureCheckBytes)) {
-			return IMAGE_TYPE_ORF;
-		} else if (isRw2Format(signatureCheckBytes)) {
-			return IMAGE_TYPE_RW2;
-		} else if (isPngFormat(signatureCheckBytes)) {
-			return IMAGE_TYPE_PNG;
-		} else if (isWebpFormat(signatureCheckBytes)) {
-			return IMAGE_TYPE_WEBP;
-		}
-		// Certain file formats (PEF) are identified in readImageFileDirectory()
-		return IMAGE_TYPE_UNKNOWN;
-	}
-
-	/**
-	 * This method looks at the first 3 bytes to determine if this file is a JPEG file.
-	 * See http://www.media.mit.edu/pia/Research/deepview/exif.html, "JPEG format and Marker"
-	 */
-	public static boolean isJpegFormat(byte[] signatureCheckBytes) throws IOException {
-		for (int i = 0; i < JPEG_SIGNATURE.length; i++) {
-			if (signatureCheckBytes[i] != JPEG_SIGNATURE[i]) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	/**
-	 * This method looks at the first 15 bytes to determine if this file is a RAF file.
-	 * There is no official specification for RAF files from Fuji, but there is an online archive of
-	 * image file specifications:
-	 * http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
-	 */
-	private boolean isRafFormat(byte[] signatureCheckBytes) throws IOException {
-		byte[] rafSignatureBytes = RAF_SIGNATURE.getBytes(Charset.defaultCharset());
-		for (int i = 0; i < rafSignatureBytes.length; i++) {
-			if (signatureCheckBytes[i] != rafSignatureBytes[i]) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
-		ByteOrderedDataInputStream signatureInputStream = null;
-		try {
-			signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
-			signatureInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
-
-			long chunkSize = signatureInputStream.readInt();
-			byte[] chunkType = new byte[4];
-			signatureInputStream.read(chunkType);
-
-			if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) {
-				return false;
-			}
-
-			long chunkDataOffset = 8;
-			if (chunkSize == 1) {
-				// This indicates that the next 8 bytes represent the chunk size,
-				// and chunk data comes after that.
-				chunkSize = signatureInputStream.readLong();
-				if (chunkSize < 16) {
-					// The smallest valid chunk is 16 bytes long in this case.
-					return false;
-				}
-				chunkDataOffset += 8;
-			}
-
-			// only sniff up to signatureCheckBytes.length
-			if (chunkSize > signatureCheckBytes.length) {
-				chunkSize = signatureCheckBytes.length;
-			}
-
-			long chunkDataSize = chunkSize - chunkDataOffset;
-
-			// It should at least have major brand (4-byte) and minor version (4-byte).
-			// The rest of the chunk (if any) is a list of (4-byte) compatible brands.
-			if (chunkDataSize < 8) {
-				return false;
-			}
-
-			byte[] brand = new byte[4];
-			boolean isMif1 = false;
-			boolean isHeic = false;
-			for (long i = 0; i < chunkDataSize / 4;  ++i) {
-				if (signatureInputStream.read(brand) != brand.length) {
-					return false;
-				}
-				if (i == 1) {
-					// Skip this index, it refers to the minorVersion, not a brand.
-					continue;
-				}
-				if (Arrays.equals(brand, HEIF_BRAND_MIF1)) {
-					isMif1 = true;
-				} else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) {
-					isHeic = true;
-				}
-				if (isMif1 && isHeic) {
-					return true;
-				}
-			}
-		} catch (Exception e) {
-			if (DEBUG) {
-				logger.debug("Exception parsing HEIF file type box.", e);
-			}
-		} finally {
-			if (signatureInputStream != null) {
-				signatureInputStream.close();
-				signatureInputStream = null;
-			}
-		}
-		return false;
-	}
-
-	/**
-	 * ORF has a similar structure to TIFF but it contains a different signature at the TIFF Header.
-	 * This method looks at the 2 bytes following the Byte Order bytes to determine if this file is
-	 * an ORF file.
-	 * There is no official specification for ORF files from Olympus, but there is an online archive
-	 * of image file specifications:
-	 * http://fileformats.archiveteam.org/wiki/Olympus_ORF
-	 */
-	private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException {
-		ByteOrderedDataInputStream signatureInputStream =
-				new ByteOrderedDataInputStream(signatureCheckBytes);
-		// Read byte order
-		mExifByteOrder = readByteOrder(signatureInputStream);
-		// Set byte order
-		signatureInputStream.setByteOrder(mExifByteOrder);
-
-		short orfSignature = signatureInputStream.readShort();
-		signatureInputStream.close();
-		return orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2;
-	}
-
-	/**
-	 * RW2 is TIFF-based, but stores 0x55 signature byte instead of 0x42 at the header
-	 * See http://lclevy.free.fr/raw/
-	 */
-	private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException {
-		ByteOrderedDataInputStream signatureInputStream =
-				new ByteOrderedDataInputStream(signatureCheckBytes);
-		// Read byte order
-		mExifByteOrder = readByteOrder(signatureInputStream);
-		// Set byte order
-		signatureInputStream.setByteOrder(mExifByteOrder);
-
-		short signatureByte = signatureInputStream.readShort();
-		signatureInputStream.close();
-		return signatureByte == RW2_SIGNATURE;
-	}
-
-
-	/**
-	 * PNG's file signature is first 8 bytes.
-	 * See PNG (Portable Network Graphics) Specification, Version 1.2, 3.1. PNG file signature
-	 */
-	private boolean isPngFormat(byte[] signatureCheckBytes) throws IOException {
-		for (int i = 0; i < PNG_SIGNATURE.length; i++) {
-			if (signatureCheckBytes[i] != PNG_SIGNATURE[i]) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	/**
-	 * WebP's file signature is composed of 12 bytes:
-	 *   'RIFF' (4 bytes) + file length value (4 bytes) + 'WEBP' (4 bytes)
-	 * See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
-	 */
-	private boolean isWebpFormat(byte[] signatureCheckBytes) throws IOException {
-		for (int i = 0; i < WEBP_SIGNATURE_1.length; i++) {
-			if (signatureCheckBytes[i] != WEBP_SIGNATURE_1[i]) {
-				return false;
-			}
-		}
-		for (int i = 0; i < WEBP_SIGNATURE_2.length; i++) {
-			if (signatureCheckBytes[i + WEBP_SIGNATURE_1.length + WEBP_FILE_SIZE_BYTE_LENGTH]
-					!= WEBP_SIGNATURE_2[i]) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	/**
-	 * Loads EXIF attributes from a JPEG input stream.
-	 *
-	 * @param in The input stream that starts with the JPEG data.
-	 * @param jpegOffset The offset value in input stream for JPEG data.
-	 * @param imageType The image type from which to retrieve metadata. Use IFD_TYPE_PRIMARY for
-	 *                   primary image, IFD_TYPE_PREVIEW for preview image, and
-	 *                   IFD_TYPE_THUMBNAIL for thumbnail image.
-	 * @throws IOException If the data contains invalid JPEG markers, offsets, or length values.
-	 */
-	private void getJpegAttributes(ByteOrderedDataInputStream in, int jpegOffset, int imageType)
-			throws IOException {
-		// See JPEG File Interchange Format Specification, "JFIF Specification"
-		if (DEBUG) {
-			logger.debug("getJpegAttributes starting with: " + in);
-		}
-
-		// JPEG uses Big Endian by default. See https://people.cs.umass.edu/~verts/cs32/endian.html
-		in.setByteOrder(ByteOrder.BIG_ENDIAN);
-
-		// Skip to JPEG data
-		in.seek(jpegOffset);
-		int bytesRead = jpegOffset;
-
-		byte marker;
-		if ((marker = in.readByte()) != MARKER) {
-			throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
-		}
-		++bytesRead;
-		if (in.readByte() != MARKER_SOI) {
-			throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
-		}
-		++bytesRead;
-		while (true) {
-			marker = in.readByte();
-			if (marker != MARKER) {
-				throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
-			}
-			++bytesRead;
-			marker = in.readByte();
-			if (DEBUG) {
-				logger.debug("Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
-			}
-			++bytesRead;
-
-			// EOI indicates the end of an image and in case of SOS, JPEG image stream starts and
-			// the image data will terminate right after.
-			if (marker == MARKER_EOI || marker == MARKER_SOS) {
-				break;
-			}
-			int length = in.readUnsignedShort() - 2;
-			bytesRead += 2;
-			if (DEBUG) {
-				logger.debug("JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: "
-						+ (length + 2) + ")");
-			}
-			if (length < 0) {
-				throw new IOException("Invalid length");
-			}
-			switch (marker) {
-				case MARKER_APP1: {
-					if (DEBUG) {
-						logger.debug("MARKER_APP1");
-					}
-					if (length < 6) {
-						// Skip if it's not an EXIF APP1 segment.
-						break;
-					}
-					byte[] identifier = new byte[6];
-					if (in.read(identifier) != 6) {
-						throw new IOException("Invalid exif");
-					}
-					bytesRead += 6;
-					length -= 6;
-					if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
-						// Skip if it's not an EXIF APP1 segment.
-						break;
-					}
-					if (length <= 0) {
-						throw new IOException("Invalid exif");
-					}
-					if (DEBUG) {
-						logger.debug("readExifSegment with a byte array (length: " + length + ")");
-					}
-					// Save offset values for createJpegThumbnailBitmap() function
-					mExifOffset = bytesRead;
-
-					byte[] bytes = new byte[length];
-					if (in.read(bytes) != length) {
-						throw new IOException("Invalid exif");
-					}
-					bytesRead += length;
-					length = 0;
-
-					readExifSegment(bytes, imageType);
-					break;
-				}
-
-				case MARKER_COM: {
-					byte[] bytes = new byte[length];
-					if (in.read(bytes) != length) {
-						throw new IOException("Invalid exif");
-					}
-					length = 0;
-					if (getAttribute(TAG_USER_COMMENT) == null) {
-						mAttributes[IFD_TYPE_EXIF].put(TAG_USER_COMMENT, ExifAttribute.createString(
-								new String(bytes, US_ASCII)));
-					}
-					break;
-				}
-
-				case MARKER_SOF0:
-				case MARKER_SOF1:
-				case MARKER_SOF2:
-				case MARKER_SOF3:
-				case MARKER_SOF5:
-				case MARKER_SOF6:
-				case MARKER_SOF7:
-				case MARKER_SOF9:
-				case MARKER_SOF10:
-				case MARKER_SOF11:
-				case MARKER_SOF13:
-				case MARKER_SOF14:
-				case MARKER_SOF15: {
-					if (in.skipBytes(1) != 1) {
-						throw new IOException("Invalid SOFx");
-					}
-					mAttributes[imageType].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong(
-							in.readUnsignedShort(), mExifByteOrder));
-					mAttributes[imageType].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong(
-							in.readUnsignedShort(), mExifByteOrder));
-					length -= 5;
-					break;
-				}
-
-				default: {
-					break;
-				}
-			}
-			if (length < 0) {
-				throw new IOException("Invalid length");
-			}
-			if (in.skipBytes(length) != length) {
-				throw new IOException("Invalid JPEG segment");
-			}
-			bytesRead += length;
-		}
-		// Restore original byte order
-		in.setByteOrder(mExifByteOrder);
-	}
-
-	private void getRawAttributes(ByteOrderedDataInputStream in) throws IOException {
-		// Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
-		parseTiffHeaders(in, in.available());
-
-		// Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
-		readImageFileDirectory(in, IFD_TYPE_PRIMARY);
-
-		// Update ImageLength/Width tags for all image data.
-		updateImageSizeValues(in, IFD_TYPE_PRIMARY);
-		updateImageSizeValues(in, IFD_TYPE_PREVIEW);
-		updateImageSizeValues(in, IFD_TYPE_THUMBNAIL);
-
-		// Check if each image data is in valid position.
-		validateImages();
-
-		if (mMimeType == IMAGE_TYPE_PEF) {
-			// PEF files contain a MakerNote data, which contains the data for ColorSpace tag.
-			// See http://lclevy.free.fr/raw/ and piex.cc PefGetPreviewData()
-			ExifAttribute makerNoteAttribute =
-					(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
-			if (makerNoteAttribute != null) {
-				// Create an ordered DataInputStream for MakerNote
-
-				// MODIFIED BY THREEMA
-				try (ByteOrderedDataInputStream makerNoteDataInputStream =
-						new ByteOrderedDataInputStream(makerNoteAttribute.bytes)) {
-					makerNoteDataInputStream.setByteOrder(mExifByteOrder);
-
-					// Seek to MakerNote data
-					makerNoteDataInputStream.seek(PEF_MAKER_NOTE_SKIP_SIZE);
-
-					// Read IFD data from MakerNote
-					readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_PEF);
-
-					// Update ColorSpace tag
-					ExifAttribute colorSpaceAttribute =
-						(ExifAttribute) mAttributes[IFD_TYPE_PEF].get(TAG_COLOR_SPACE);
-					if (colorSpaceAttribute != null) {
-						mAttributes[IFD_TYPE_EXIF].put(TAG_COLOR_SPACE, colorSpaceAttribute);
-					}
-				}
-			}
-		}
-	}
-
-	/**
-	 * RAF files contains a JPEG and a CFA data.
-	 * The JPEG contains two images, a preview and a thumbnail, while the CFA contains a RAW image.
-	 * This method looks at the first 160 bytes of a RAF file to retrieve the offset and length
-	 * values for the JPEG and CFA data.
-	 * Using that data, it parses the JPEG data to retrieve the preview and thumbnail image data,
-	 * then parses the CFA metadata to retrieve the primary image length/width values.
-	 * For data format details, see http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
-	 */
-	private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException {
-		// Retrieve offset & length values
-		in.skipBytes(RAF_OFFSET_TO_JPEG_IMAGE_OFFSET);
-		byte[] jpegOffsetBytes = new byte[4];
-		byte[] cfaHeaderOffsetBytes = new byte[4];
-		in.read(jpegOffsetBytes);
-		// Skip JPEG length value since it is not needed
-		in.skipBytes(RAF_JPEG_LENGTH_VALUE_SIZE);
-		in.read(cfaHeaderOffsetBytes);
-		int rafJpegOffset = ByteBuffer.wrap(jpegOffsetBytes).getInt();
-		int rafCfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt();
-
-		// Retrieve JPEG image metadata
-		getJpegAttributes(in, rafJpegOffset, IFD_TYPE_PREVIEW);
-
-		// Skip to CFA header offset.
-		in.seek(rafCfaHeaderOffset);
-
-		// Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists
-		in.setByteOrder(ByteOrder.BIG_ENDIAN);
-		int numberOfDirectoryEntry = in.readInt();
-		if (DEBUG) {
-			logger.debug("numberOfDirectoryEntry: " + numberOfDirectoryEntry);
-		}
-		// CFA stores some metadata about the RAW image. Since CFA uses proprietary tags, can only
-		// find and retrieve image size information tags, while skipping others.
-		// See piex.cc RafGetDimension()
-		for (int i = 0; i < numberOfDirectoryEntry; ++i) {
-			int tagNumber = in.readUnsignedShort();
-			int numberOfBytes = in.readUnsignedShort();
-			if (tagNumber == TAG_RAF_IMAGE_SIZE.number) {
-				int imageLength = in.readShort();
-				int imageWidth = in.readShort();
-				ExifAttribute imageLengthAttribute =
-						ExifAttribute.createUShort(imageLength, mExifByteOrder);
-				ExifAttribute imageWidthAttribute =
-						ExifAttribute.createUShort(imageWidth, mExifByteOrder);
-				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
-				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
-				if (DEBUG) {
-					logger.debug("Updated to length: " + imageLength + ", width: " + imageWidth);
-				}
-				return;
-			}
-			in.skipBytes(numberOfBytes);
-		}
-	}
-
-	@TargetApi(Build.VERSION_CODES.P)
-	private void getHeifAttributes(final ByteOrderedDataInputStream in) throws IOException {
-		// MODIFIED BY THREEMA
-		// do not use automatic resource management on MediaMetadataRetriever
-		final MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-		try {
-			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-				retriever.setDataSource(new MediaDataSource() {
-					long mPosition;
-
-					@Override
-					public void close() throws IOException {}
-
-					@Override
-					public int readAt(long position, byte[] buffer, int offset, int size)
-							throws IOException {
-						if (size == 0) {
-							return 0;
-						}
-						if (position < 0) {
-							return -1;
-						}
-						try {
-							if (mPosition != position) {
-								// We don't allow seek to positions after the available bytes,
-								// the input stream won't be able to seek back then.
-								// However, if we hit an exception before (mPosition set to -1),
-								// let it try the seek in hope it might recover.
-								if (mPosition >= 0 && position >= mPosition + in.available()) {
-									return -1;
-								}
-								in.seek(position);
-								mPosition = position;
-							}
-
-							// If the read will cause us to go over the available bytes,
-							// reduce the size so that we stay in the available range.
-							// Otherwise the input stream may not be able to seek back.
-							if (size > in.available()) {
-								size = in.available();
-							}
-
-							int bytesRead = in.read(buffer, offset, size);
-							if (bytesRead >= 0) {
-								mPosition += bytesRead;
-								return bytesRead;
-							}
-						} catch (IOException e) {
-							// do nothing
-						}
-						mPosition = -1; // need to seek on next read
-						return -1;
-					}
-
-					@Override
-					public long getSize() throws IOException {
-						return -1;
-					}
-				});
-			} else {
-				if (mFilename != null) {
-					retriever.setDataSource(mFilename);
-				} else {
-					return;
-				}
-			}
-
-			String exifOffsetStr = retriever.extractMetadata(
-					33); // MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET
-			String exifLengthStr = retriever.extractMetadata(
-					34); // MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH
-			String hasImage = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
-			String hasVideo = retriever.extractMetadata(
-					MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
-
-			String width = null;
-			String height = null;
-			String rotation = null;
-			final String metadataValueYes = "yes";
-			// If the file has both image and video, prefer image info over video info.
-			// App querying ExifInterface is most likely using the bitmap path which
-			// picks the image first.
-			if (metadataValueYes.equals(hasImage)) {
-				width = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
-				height = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
-				rotation = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
-			} else if (metadataValueYes.equals(hasVideo)) {
-				width = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
-				height = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
-				rotation = retriever.extractMetadata(
-						MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
-			}
-
-			if (width != null) {
-				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
-						ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder));
-			}
-
-			if (height != null) {
-				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
-						ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder));
-			}
-
-			if (rotation != null) {
-				int orientation = ExifInterface.ORIENTATION_NORMAL;
-
-				// all rotation angles in CW
-				switch (Integer.parseInt(rotation)) {
-					case 90:
-						orientation = ORIENTATION_ROTATE_90;
-						break;
-					case 180:
-						orientation = ORIENTATION_ROTATE_180;
-						break;
-					case 270:
-						orientation = ORIENTATION_ROTATE_270;
-						break;
-				}
-
-				mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
-						ExifAttribute.createUShort(orientation, mExifByteOrder));
-			}
-
-			if (exifOffsetStr != null && exifLengthStr != null) {
-				int offset = Integer.parseInt(exifOffsetStr);
-				int length = Integer.parseInt(exifLengthStr);
-				if (length <= 6) {
-					throw new IOException("Invalid exif length");
-				}
-				in.seek(offset);
-				byte[] identifier = new byte[6];
-				if (in.read(identifier) != 6) {
-					throw new IOException("Can't read identifier");
-				}
-				offset += 6;
-				length -= 6;
-				if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
-					throw new IOException("Invalid identifier");
-				}
-
-				// TODO: Need to handle potential OutOfMemoryError
-				byte[] bytes = new byte[length];
-				if (in.read(bytes) != length) {
-					throw new IOException("Can't read exif");
-				}
-				readExifSegment(bytes, IFD_TYPE_PRIMARY);
-			}
-
-			if (DEBUG) {
-				logger.debug("Heif meta: " + width + "x" + height + ", rotation " + rotation);
-			}
-		} finally {
-			retriever.release();
-		}
-	}
-
-	/**
-	 * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail
-	 * images. Both data takes the form of IFDs and can therefore be read with the
-	 * readImageFileDirectory() method.
-	 * This method reads all the necessary data and updates the primary/preview/thumbnail image
-	 * information according to the GetOlympusPreviewImage() method in piex.cc.
-	 * For data format details, see the following:
-	 * http://fileformats.archiveteam.org/wiki/Olympus_ORF
-	 * https://libopenraw.freedesktop.org/wiki/Olympus_ORF
-	 */
-	private void getOrfAttributes(ByteOrderedDataInputStream in) throws IOException {
-		// Retrieve primary image data
-		// Other Exif data will be located in the Makernote.
-		getRawAttributes(in);
-
-		// Additionally retrieve preview/thumbnail information from MakerNote tag, which contains
-		// proprietary tags and therefore does not have offical documentation
-		// See GetOlympusPreviewImage() in piex.cc & http://www.exiv2.org/tags-olympus.html
-		ExifAttribute makerNoteAttribute =
-				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
-		if (makerNoteAttribute != null) {
-			// Create an ordered DataInputStream for MakerNote
-			// MODIFIED BY THREEMA
-			try (ByteOrderedDataInputStream makerNoteDataInputStream =
-					new ByteOrderedDataInputStream(makerNoteAttribute.bytes)) {
-				makerNoteDataInputStream.setByteOrder(mExifByteOrder);
-
-				// There are two types of headers for Olympus MakerNotes
-				// See http://www.exiv2.org/makernote.html#R1
-				byte[] makerNoteHeader1Bytes = new byte[ORF_MAKER_NOTE_HEADER_1.length];
-				makerNoteDataInputStream.readFully(makerNoteHeader1Bytes);
-				makerNoteDataInputStream.seek(0);
-				byte[] makerNoteHeader2Bytes = new byte[ORF_MAKER_NOTE_HEADER_2.length];
-				makerNoteDataInputStream.readFully(makerNoteHeader2Bytes);
-				// Skip the corresponding amount of bytes for each header type
-				if (Arrays.equals(makerNoteHeader1Bytes, ORF_MAKER_NOTE_HEADER_1)) {
-					makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_1_SIZE);
-				} else if (Arrays.equals(makerNoteHeader2Bytes, ORF_MAKER_NOTE_HEADER_2)) {
-					makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_2_SIZE);
-				}
-
-				// Read IFD data from MakerNote
-				readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_ORF_MAKER_NOTE);
-			}
-
-			// Retrieve & update preview image offset & length values
-			ExifAttribute imageStartAttribute = (ExifAttribute)
-					mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_START);
-			ExifAttribute imageLengthAttribute = (ExifAttribute)
-					mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_LENGTH);
-
-			if (imageStartAttribute != null && imageLengthAttribute != null) {
-				mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT,
-						imageStartAttribute);
-				mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
-						imageLengthAttribute);
-			}
-
-			// TODO: Check this behavior in other ORF files
-			// Retrieve primary image length & width values
-			// See piex.cc GetOlympusPreviewImage()
-			ExifAttribute aspectFrameAttribute = (ExifAttribute)
-					mAttributes[IFD_TYPE_ORF_IMAGE_PROCESSING].get(TAG_ORF_ASPECT_FRAME);
-			if (aspectFrameAttribute != null) {
-				int[] aspectFrameValues = (int[]) aspectFrameAttribute.getValue(mExifByteOrder);
-				if (aspectFrameValues == null || aspectFrameValues.length != 4) {
-					logger.warn("Invalid aspect frame values. frame="
-							+ Arrays.toString(aspectFrameValues));
-					return;
-				}
-				if (aspectFrameValues[2] > aspectFrameValues[0] &&
-						aspectFrameValues[3] > aspectFrameValues[1]) {
-					int primaryImageWidth = aspectFrameValues[2] - aspectFrameValues[0] + 1;
-					int primaryImageLength = aspectFrameValues[3] - aspectFrameValues[1] + 1;
-					// Swap width & length values
-					if (primaryImageWidth < primaryImageLength) {
-						primaryImageWidth += primaryImageLength;
-						primaryImageLength = primaryImageWidth - primaryImageLength;
-						primaryImageWidth -= primaryImageLength;
-					}
-					ExifAttribute primaryImageWidthAttribute =
-							ExifAttribute.createUShort(primaryImageWidth, mExifByteOrder);
-					ExifAttribute primaryImageLengthAttribute =
-							ExifAttribute.createUShort(primaryImageLength, mExifByteOrder);
-
-					mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, primaryImageWidthAttribute);
-					mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, primaryImageLengthAttribute);
-				}
-			}
-		}
-	}
-
-	// RW2 contains the primary image data in IFD0 and the preview and/or thumbnail image data in
-	// the JpgFromRaw tag
-	// See https://libopenraw.freedesktop.org/wiki/Panasonic_RAW/ and piex.cc Rw2GetPreviewData()
-	private void getRw2Attributes(ByteOrderedDataInputStream in) throws IOException {
-		// Retrieve primary image data
-		getRawAttributes(in);
-
-		// Retrieve preview and/or thumbnail image data
-		ExifAttribute jpgFromRawAttribute =
-				(ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_JPG_FROM_RAW);
-		if (jpgFromRawAttribute != null) {
-			getJpegAttributes(in, mRw2JpgFromRawOffset, IFD_TYPE_PREVIEW);
-		}
-
-		// Set ISO tag value if necessary
-		ExifAttribute rw2IsoAttribute =
-				(ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_ISO);
-		ExifAttribute exifIsoAttribute =
-				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PHOTOGRAPHIC_SENSITIVITY);
-		if (rw2IsoAttribute != null && exifIsoAttribute == null) {
-			// Place this attribute only if it doesn't exist
-			mAttributes[IFD_TYPE_EXIF].put(TAG_PHOTOGRAPHIC_SENSITIVITY, rw2IsoAttribute);
-		}
-	}
-
-	// PNG contains the EXIF data as a Special-Purpose Chunk
-	private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException {
-		if (DEBUG) {
-			logger.debug("getPngAttributes starting with: " + in);
-		}
-
-		// PNG uses Big Endian by default.
-		// See PNG (Portable Network Graphics) Specification, Version 1.2,
-		// 2.1. Integers and byte order
-		in.setByteOrder(ByteOrder.BIG_ENDIAN);
-
-		int bytesRead = 0;
-
-		// Skip the signature bytes
-		in.skipBytes(PNG_SIGNATURE.length);
-		bytesRead += PNG_SIGNATURE.length;
-
-		// Each chunk is made up of four parts:
-		//   1) Length: 4-byte unsigned integer indicating the number of bytes in the
-		//   Chunk Data field. Excludes Chunk Type and CRC bytes.
-		//   2) Chunk Type: 4-byte chunk type code.
-		//   3) Chunk Data: The data bytes. Can be zero-length.
-		//   4) CRC: 4-byte data calculated on the preceding bytes in the chunk. Always
-		//   present.
-		// --> 4 (length bytes) + 4 (type bytes) + X (data bytes) + 4 (CRC bytes)
-		// See PNG (Portable Network Graphics) Specification, Version 1.2,
-		// 3.2. Chunk layout
-		try {
-			while (true) {
-				int length = in.readInt();
-				bytesRead += 4;
-
-				byte[] type = new byte[PNG_CHUNK_TYPE_BYTE_LENGTH];
-				if (in.read(type) != type.length) {
-					throw new IOException("Encountered invalid length while parsing PNG chunk"
-							+ "type");
-				}
-				bytesRead += PNG_CHUNK_TYPE_BYTE_LENGTH;
-
-				// The first chunk must be the IHDR chunk
-				if (bytesRead == 16 && !Arrays.equals(type, PNG_CHUNK_TYPE_IHDR)) {
-					throw new IOException("Encountered invalid PNG file--IHDR chunk should appear"
-							+ "as the first chunk");
-				}
-
-				if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
-					// IEND marks the end of the image.
-					break;
-				} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
-					// TODO: Need to handle potential OutOfMemoryError
-					byte[] data = new byte[length];
-					if (in.read(data) != length) {
-						throw new IOException("Failed to read given length for given PNG chunk "
-								+ "type: " + byteArrayToHexString(type));
-					}
-
-					// Compare CRC values for potential data corruption.
-					int dataCrcValue = in.readInt();
-					// Cyclic Redundancy Code used to check for corruption of the data
-					CRC32 crc = new CRC32();
-					crc.update(type);
-					crc.update(data);
-					if ((int) crc.getValue() != dataCrcValue) {
-						throw new IOException("Encountered invalid CRC value for PNG-EXIF chunk."
-								+ "\n recorded CRC value: " + dataCrcValue + ", calculated CRC "
-								+ "value: " + crc.getValue());
-					}
-
-					readExifSegment(data, IFD_TYPE_PRIMARY);
-
-					validateImages();
-
-					// Save offset values for handleThumbnailFromJfif() function
-					mExifOffset = bytesRead;
-					break;
-				} else {
-					// Skip to next chunk
-					in.skipBytes(length + PNG_CHUNK_CRC_BYTE_LENGTH);
-					bytesRead += length + PNG_CHUNK_CRC_BYTE_LENGTH;
-				}
-			}
-		} catch (EOFException e) {
-			// Should not reach here. Will only reach here if the file is corrupted or
-			// does not follow the PNG specifications
-			throw new IOException("Encountered corrupt PNG file.");
-		}
-	}
-
-	// WebP contains EXIF data as a RIFF File Format Chunk
-	// All references below can be found in the following link.
-	// https://developers.google.com/speed/webp/docs/riff_container
-	private void getWebpAttributes(ByteOrderedDataInputStream in) throws IOException {
-		if (DEBUG) {
-			logger.debug("getWebpAttributes starting with: " + in);
-		}
-
-		// WebP uses little-endian by default.
-		// See Section "Terminology & Basics"
-		in.setByteOrder(ByteOrder.LITTLE_ENDIAN);
-
-		in.skipBytes(WEBP_SIGNATURE_1.length);
-		// File size corresponds to the size of the entire file from offset 8.
-		// See Section "WebP File Header"
-		int fileSize = in.readInt() + 8;
-		int bytesRead = 8;
-		bytesRead += in.skipBytes(WEBP_SIGNATURE_2.length);
-
-		try {
-			while (true) {
-				// Each chunk is made up of three parts:
-				//   1) Chunk FourCC: 4-byte concatenating four ASCII characters.
-				//   2) Chunk Size: 4-byte unsigned integer indicating the size of the chunk.
-				//                  Excludes Chunk FourCC and Chunk Size bytes.
-				//   3) Chunk Payload: data payload. A single padding byte ('0') is added if
-				//                     Chunk Size is odd.
-				// See Section "RIFF File Format"
-				byte[] code = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
-				if (in.read(code) != code.length) {
-					throw new IOException("Encountered invalid length while parsing WebP chunk"
-							+ "type");
-				}
-				bytesRead += 4;
-
-				int chunkSize = in.readInt();
-				bytesRead += 4;
-
-				if (Arrays.equals(WEBP_CHUNK_TYPE_EXIF, code)) {
-					// TODO: Need to handle potential OutOfMemoryError
-					byte[] payload = new byte[chunkSize];
-					if (in.read(payload) != chunkSize) {
-						throw new IOException("Failed to read given length for given PNG chunk "
-								+ "type: " + byteArrayToHexString(code));
-					}
-					readExifSegment(payload, IFD_TYPE_PRIMARY);
-					break;
-				} else {
-					// Add a single padding byte at end if chunk size is odd
-					chunkSize = (chunkSize % 2 == 1) ? chunkSize + 1 : chunkSize;
-
-					// Check if skipping to next chunk is necessary
-					if (bytesRead + chunkSize == fileSize) {
-						// Reached end of file
-						break;
-					} else if (bytesRead + chunkSize > fileSize) {
-						throw new IOException("Encountered WebP file with invalid chunk size");
-					}
-
-					// Skip to next chunk
-					int skipped = in.skipBytes(chunkSize);
-					if (skipped != chunkSize) {
-						throw new IOException("Encountered WebP file with invalid chunk size");
-					}
-					bytesRead += skipped;
-				}
-			}
-			// Save offset values for handleThumbnailFromJfif() function
-			mExifOffset = bytesRead;
-		} catch (EOFException e) {
-			// Should not reach here. Will only reach here if the file is corrupted or
-			// does not follow the WebP specifications
-			throw new IOException("Encountered corrupt WebP file.");
-		}
-	}
-
-	// Stores a new JPEG image with EXIF attributes into a given output stream.
-	private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream, boolean stripExif)
-			throws IOException {
-		// See JPEG File Interchange Format Specification, "JFIF Specification"
-		if (DEBUG) {
-			logger.debug("saveJpegAttributes starting with (inputStream: " + inputStream
-					+ ", outputStream: " + outputStream + ")");
-		}
-		DataInputStream dataInputStream = new DataInputStream(inputStream);
-		ByteOrderedDataOutputStream dataOutputStream =
-				new ByteOrderedDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN);
-		if (dataInputStream.readByte() != MARKER) {
-			throw new IOException("Invalid marker");
-		}
-		dataOutputStream.writeByte(MARKER);
-		if (dataInputStream.readByte() != MARKER_SOI) {
-			throw new IOException("Invalid marker");
-		}
-		dataOutputStream.writeByte(MARKER_SOI);
-
-		// Write EXIF APP1 segment
-		if (!stripExif) {
-			dataOutputStream.writeByte(MARKER);
-			dataOutputStream.writeByte(MARKER_APP1);
-			writeExifSegment(dataOutputStream, 6);
-		}
-
-		byte[] bytes = new byte[4096];
-
-		while (true) {
-			byte marker = dataInputStream.readByte();
-			if (marker != MARKER) {
-				throw new IOException("Invalid marker");
-			}
-			marker = dataInputStream.readByte();
-			switch (marker) {
-				case MARKER_APP1: {
-					int length = dataInputStream.readUnsignedShort() - 2;
-					if (length < 0) {
-						throw new IOException("Invalid length");
-					}
-					byte[] identifier = new byte[6];
-					if (length >= 6) {
-						if (dataInputStream.read(identifier) != 6) {
-							throw new IOException("Invalid exif");
-						}
-						if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
-							// Skip the original EXIF APP1 segment.
-							if (dataInputStream.skipBytes(length - 6) != length - 6) {
-								throw new IOException("Invalid length");
-							}
-							break;
-						}
-					}
-					// Copy non-EXIF APP1 segment.
-					dataOutputStream.writeByte(MARKER);
-					dataOutputStream.writeByte(marker);
-					dataOutputStream.writeUnsignedShort(length + 2);
-					if (length >= 6) {
-						length -= 6;
-						dataOutputStream.write(identifier);
-					}
-					int read;
-					while (length > 0 && (read = dataInputStream.read(
-							bytes, 0, Math.min(length, bytes.length))) >= 0) {
-						dataOutputStream.write(bytes, 0, read);
-						length -= read;
-					}
-					break;
-				}
-				case MARKER_EOI:
-				case MARKER_SOS: {
-					dataOutputStream.writeByte(MARKER);
-					dataOutputStream.writeByte(marker);
-					// Copy all the remaining data
-					copy(dataInputStream, dataOutputStream);
-					return;
-				}
-				default: {
-					// Copy JPEG segment
-					dataOutputStream.writeByte(MARKER);
-					dataOutputStream.writeByte(marker);
-					int length = dataInputStream.readUnsignedShort();
-					dataOutputStream.writeUnsignedShort(length);
-					length -= 2;
-					if (length < 0) {
-						throw new IOException("Invalid length");
-					}
-					int read;
-					while (length > 0 && (read = dataInputStream.read(
-							bytes, 0, Math.min(length, bytes.length))) >= 0) {
-						dataOutputStream.write(bytes, 0, read);
-						length -= read;
-					}
-					break;
-				}
-			}
-		}
-	}
-
-	// Reads the given EXIF byte area and save its tag data into attributes.
-	private void readExifSegment(byte[] exifBytes, int imageType) throws IOException {
-		ByteOrderedDataInputStream dataInputStream =
-				new ByteOrderedDataInputStream(exifBytes);
-
-		// Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
-		parseTiffHeaders(dataInputStream, exifBytes.length);
-
-		// Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
-		readImageFileDirectory(dataInputStream, imageType);
-	}
-
-	private void addDefaultValuesForCompatibility() {
-		// If DATETIME tag has no value, then set the value to DATETIME_ORIGINAL tag's.
-		String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL);
-		if (valueOfDateTimeOriginal != null && getAttribute(TAG_DATETIME) == null) {
-			mAttributes[IFD_TYPE_PRIMARY].put(TAG_DATETIME,
-					ExifAttribute.createString(valueOfDateTimeOriginal));
-		}
-
-		// Add the default value.
-		if (getAttribute(TAG_IMAGE_WIDTH) == null) {
-			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-		if (getAttribute(TAG_IMAGE_LENGTH) == null) {
-			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-		if (getAttribute(TAG_ORIENTATION) == null) {
-			mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-		if (getAttribute(TAG_LIGHT_SOURCE) == null) {
-			mAttributes[IFD_TYPE_EXIF].put(TAG_LIGHT_SOURCE,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-	}
-
-	private ByteOrder readByteOrder(ByteOrderedDataInputStream dataInputStream)
-			throws IOException {
-		// Read byte order.
-		short byteOrder = dataInputStream.readShort();
-		switch (byteOrder) {
-			case BYTE_ALIGN_II:
-				if (DEBUG) {
-					logger.debug("readExifSegment: Byte Align II");
-				}
-				return ByteOrder.LITTLE_ENDIAN;
-			case BYTE_ALIGN_MM:
-				if (DEBUG) {
-					logger.debug("readExifSegment: Byte Align MM");
-				}
-				return ByteOrder.BIG_ENDIAN;
-			default:
-				throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder));
-		}
-	}
-
-	private void parseTiffHeaders(ByteOrderedDataInputStream dataInputStream,
-	                              int exifBytesLength) throws IOException {
-		// Read byte order
-		mExifByteOrder = readByteOrder(dataInputStream);
-		// Set byte order
-		dataInputStream.setByteOrder(mExifByteOrder);
-
-		// Check start code
-		int startCode = dataInputStream.readUnsignedShort();
-		if (mMimeType != IMAGE_TYPE_ORF && mMimeType != IMAGE_TYPE_RW2 && startCode != START_CODE) {
-			throw new IOException("Invalid start code: " + Integer.toHexString(startCode));
-		}
-
-		// Read and skip to first ifd offset
-		int firstIfdOffset = dataInputStream.readInt();
-		if (firstIfdOffset < 8 || firstIfdOffset >= exifBytesLength) {
-			throw new IOException("Invalid first Ifd offset: " + firstIfdOffset);
-		}
-		firstIfdOffset -= 8;
-		if (firstIfdOffset > 0) {
-			if (dataInputStream.skipBytes(firstIfdOffset) != firstIfdOffset) {
-				throw new IOException("Couldn't jump to first Ifd: " + firstIfdOffset);
-			}
-		}
-	}
-
-	// Reads image file directory, which is a tag group in EXIF.
-	private void readImageFileDirectory(ByteOrderedDataInputStream dataInputStream,
-	                                    @IfdType int ifdType) throws IOException {
-		// Save offset of current IFD to prevent reading an IFD that is already read.
-		mAttributesOffsets.add(dataInputStream.mPosition);
-
-		if (dataInputStream.mPosition + 2 > dataInputStream.mLength) {
-			// Return if there is no data from the offset.
-			return;
-		}
-		// See TIFF 6.0 Section 2: TIFF Structure, Figure 1.
-		short numberOfDirectoryEntry = dataInputStream.readShort();
-		if (DEBUG) {
-			logger.debug("numberOfDirectoryEntry: " + numberOfDirectoryEntry);
-		}
-		if (dataInputStream.mPosition + 12 * numberOfDirectoryEntry > dataInputStream.mLength
-				|| numberOfDirectoryEntry <= 0) {
-			// Return if the size of entries is either too big or negative.
-			return;
-		}
-
-		// See TIFF 6.0 Section 2: TIFF Structure, "Image File Directory".
-		for (short i = 0; i < numberOfDirectoryEntry; ++i) {
-			int tagNumber = dataInputStream.readUnsignedShort();
-			int dataFormat = dataInputStream.readUnsignedShort();
-			int numberOfComponents = dataInputStream.readInt();
-			// Next four bytes is for data offset or value.
-			long nextEntryOffset = dataInputStream.peek() + 4L;
-
-			// Look up a corresponding tag from tag number
-			ExifTag tag = (ExifTag) sExifTagMapsForReading[ifdType].get(tagNumber);
-
-			if (DEBUG) {
-				logger.debug(String.format("ifdType: %d, tagNumber: %d, tagName: %s, dataFormat: %d, "
-								+ "numberOfComponents: %d", ifdType, tagNumber,
-						tag != null ? tag.name : null, dataFormat, numberOfComponents));
-			}
-
-			long byteCount = 0;
-			boolean valid = false;
-			if (tag == null) {
-				logger.warn("Skip the tag entry since tag number is not defined: " + tagNumber);
-			} else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
-				logger.warn("Skip the tag entry since data format is invalid: " + dataFormat);
-			} else if (!tag.isFormatCompatible(dataFormat)) {
-				logger.warn("Skip the tag entry since data format is unexpected for tag: " + tag.name);
-			} else {
-				if (dataFormat == IFD_FORMAT_UNDEFINED) {
-					dataFormat = tag.primaryFormat;
-				}
-				byteCount = (long) numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
-				if (byteCount < 0 || byteCount > Integer.MAX_VALUE) {
-					logger.warn("Skip the tag entry since the number of components is invalid: "
-							+ numberOfComponents);
-				} else {
-					valid = true;
-				}
-			}
-			if (!valid) {
-				dataInputStream.seek(nextEntryOffset);
-				continue;
-			}
-
-			// Read a value from data field or seek to the value offset which is stored in data
-			// field if the size of the entry value is bigger than 4.
-			if (byteCount > 4) {
-				int offset = dataInputStream.readInt();
-				if (DEBUG) {
-					logger.debug("seek to data offset: " + offset);
-				}
-				if (mMimeType == IMAGE_TYPE_ORF) {
-					if (TAG_MAKER_NOTE.equals(tag.name)) {
-						// Save offset value for reading thumbnail
-						mOrfMakerNoteOffset = offset;
-					} else if (ifdType == IFD_TYPE_ORF_MAKER_NOTE
-							&& TAG_ORF_THUMBNAIL_IMAGE.equals(tag.name)) {
-						// Retrieve & update values for thumbnail offset and length values for ORF
-						mOrfThumbnailOffset = offset;
-						mOrfThumbnailLength = numberOfComponents;
-
-						ExifAttribute compressionAttribute =
-								ExifAttribute.createUShort(DATA_JPEG, mExifByteOrder);
-						ExifAttribute jpegInterchangeFormatAttribute =
-								ExifAttribute.createULong(mOrfThumbnailOffset, mExifByteOrder);
-						ExifAttribute jpegInterchangeFormatLengthAttribute =
-								ExifAttribute.createULong(mOrfThumbnailLength, mExifByteOrder);
-
-						mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_COMPRESSION, compressionAttribute);
-						mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
-								jpegInterchangeFormatAttribute);
-						mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
-								jpegInterchangeFormatLengthAttribute);
-					}
-				} else if (mMimeType == IMAGE_TYPE_RW2) {
-					if (TAG_RW2_JPG_FROM_RAW.equals(tag.name)) {
-						mRw2JpgFromRawOffset = offset;
-					}
-				}
-				if (offset + byteCount <= dataInputStream.mLength) {
-					dataInputStream.seek(offset);
-				} else {
-					// Skip if invalid data offset.
-					logger.warn("Skip the tag entry since data offset is invalid: " + offset);
-					dataInputStream.seek(nextEntryOffset);
-					continue;
-				}
-			}
-
-			// Recursively parse IFD when a IFD pointer tag appears.
-			Integer nextIfdType = sExifPointerTagMap.get(tagNumber);
-			if (DEBUG) {
-				logger.debug("nextIfdType: " + nextIfdType + " byteCount: " + byteCount);
-			}
-
-			if (nextIfdType != null) {
-				long offset = -1L;
-				// Get offset from data field
-				switch (dataFormat) {
-					case IFD_FORMAT_USHORT: {
-						offset = dataInputStream.readUnsignedShort();
-						break;
-					}
-					case IFD_FORMAT_SSHORT: {
-						offset = dataInputStream.readShort();
-						break;
-					}
-					case IFD_FORMAT_ULONG: {
-						offset = dataInputStream.readUnsignedInt();
-						break;
-					}
-					case IFD_FORMAT_SLONG:
-					case IFD_FORMAT_IFD: {
-						offset = dataInputStream.readInt();
-						break;
-					}
-					default: {
-						// Nothing to do
-						break;
-					}
-				}
-				if (DEBUG) {
-					logger.debug(String.format("Offset: %d, tagName: %s", offset, tag.name));
-				}
-
-				// Check if the next IFD offset
-				// 1. Exists within the boundaries of the input stream
-				// 2. Does not point to a previously read IFD.
-				if (offset > 0L && offset < dataInputStream.mLength) {
-					if (!mAttributesOffsets.contains((int) offset)) {
-						dataInputStream.seek(offset);
-						readImageFileDirectory(dataInputStream, nextIfdType);
-					} else {
-						logger.warn("Skip jump into the IFD since it has already been read: "
-								+ "IfdType " + nextIfdType + " (at " + offset + ")");
-					}
-				} else {
-					logger.warn("Skip jump into the IFD since its offset is invalid: " + offset);
-				}
-
-				dataInputStream.seek(nextEntryOffset);
-				continue;
-			}
-
-			byte[] bytes = new byte[(int) byteCount];
-			dataInputStream.readFully(bytes);
-			ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, bytes);
-			mAttributes[ifdType].put(tag.name, attribute);
-
-			// DNG files have a DNG Version tag specifying the version of specifications that the
-			// image file is following.
-			// See http://fileformats.archiveteam.org/wiki/DNG
-			if (TAG_DNG_VERSION.equals(tag.name)) {
-				mMimeType = IMAGE_TYPE_DNG;
-			}
-
-			// PEF files have a Make or Model tag that begins with "PENTAX" or a compression tag
-			// that is 65535.
-			// See http://fileformats.archiveteam.org/wiki/Pentax_PEF
-			if (((TAG_MAKE.equals(tag.name) || TAG_MODEL.equals(tag.name))
-					&& attribute.getStringValue(mExifByteOrder).contains(PEF_SIGNATURE))
-					|| (TAG_COMPRESSION.equals(tag.name)
-					&& attribute.getIntValue(mExifByteOrder) == 65535)) {
-				mMimeType = IMAGE_TYPE_PEF;
-			}
-
-			// Seek to next tag offset
-			if (dataInputStream.peek() != nextEntryOffset) {
-				dataInputStream.seek(nextEntryOffset);
-			}
-		}
-
-		if (dataInputStream.peek() + 4 <= dataInputStream.mLength) {
-			int nextIfdOffset = dataInputStream.readInt();
-			if (DEBUG) {
-				logger.debug(String.format("nextIfdOffset: %d", nextIfdOffset));
-			}
-			// Check if the next IFD offset
-			// 1. Exists within the boundaries of the input stream
-			// 2. Does not point to a previously read IFD.
-			if (nextIfdOffset > 0L && nextIfdOffset < dataInputStream.mLength) {
-				if (!mAttributesOffsets.contains(nextIfdOffset)) {
-					dataInputStream.seek(nextIfdOffset);
-					if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
-						// Do not overwrite thumbnail IFD data if it alreay exists.
-						readImageFileDirectory(dataInputStream, IFD_TYPE_THUMBNAIL);
-					} else if (mAttributes[IFD_TYPE_PREVIEW].isEmpty()) {
-						readImageFileDirectory(dataInputStream, IFD_TYPE_PREVIEW);
-					}
-				} else {
-					logger.warn("Stop reading file since re-reading an IFD may cause an "
-							+ "infinite loop: " + nextIfdOffset);
-				}
-			} else {
-				logger.warn("Stop reading file since a wrong offset may cause an infinite loop: "
-						+ nextIfdOffset);
-			}
-		}
-	}
-
-	/**
-	 * JPEG compressed images do not contain IMAGE_LENGTH & IMAGE_WIDTH tags.
-	 * This value uses JpegInterchangeFormat(JPEG data offset) value, and calls getJpegAttributes()
-	 * to locate SOF(Start of Frame) marker and update the image length & width values.
-	 * See JEITA CP-3451C Table 5 and Section 4.8.1. B.
-	 */
-	private void retrieveJpegImageSize(ByteOrderedDataInputStream in, int imageType)
-			throws IOException {
-		// Check if image already has IMAGE_LENGTH & IMAGE_WIDTH values
-		ExifAttribute imageLengthAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_LENGTH);
-		ExifAttribute imageWidthAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_WIDTH);
-
-		if (imageLengthAttribute == null || imageWidthAttribute == null) {
-			// Find if offset for JPEG data exists
-			ExifAttribute jpegInterchangeFormatAttribute =
-					(ExifAttribute) mAttributes[imageType].get(TAG_JPEG_INTERCHANGE_FORMAT);
-			if (jpegInterchangeFormatAttribute != null) {
-				int jpegInterchangeFormat =
-						jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
-
-				// Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags
-				getJpegAttributes(in, jpegInterchangeFormat, imageType);
-			}
-		}
-	}
-
-	// Sets thumbnail offset & length attributes based on JpegInterchangeFormat or StripOffsets tags
-	private void setThumbnailData(ByteOrderedDataInputStream in) throws IOException {
-		HashMap thumbnailData = mAttributes[IFD_TYPE_THUMBNAIL];
-
-		ExifAttribute compressionAttribute =
-				(ExifAttribute) thumbnailData.get(TAG_COMPRESSION);
-		if (compressionAttribute != null) {
-			mThumbnailCompression = compressionAttribute.getIntValue(mExifByteOrder);
-			switch (mThumbnailCompression) {
-				case DATA_JPEG: {
-					handleThumbnailFromJfif(in, thumbnailData);
-					break;
-				}
-				case DATA_UNCOMPRESSED:
-				case DATA_JPEG_COMPRESSED: {
-					if (isSupportedDataType(thumbnailData)) {
-						handleThumbnailFromStrips(in, thumbnailData);
-					}
-					break;
-				}
-			}
-		} else {
-			// Thumbnail data may not contain Compression tag value
-			mThumbnailCompression = DATA_JPEG;
-			handleThumbnailFromJfif(in, thumbnailData);
-		}
-	}
-
-	// Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values
-	// and reads the corresponding bytes if stream does not support seek function
-	private void handleThumbnailFromJfif(ByteOrderedDataInputStream in, HashMap thumbnailData)
-			throws IOException {
-		ExifAttribute jpegInterchangeFormatAttribute =
-				(ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT);
-		ExifAttribute jpegInterchangeFormatLengthAttribute =
-				(ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
-		if (jpegInterchangeFormatAttribute != null
-				&& jpegInterchangeFormatLengthAttribute != null) {
-			int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
-			int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);
-
-			// The following code limits the size of thumbnail size not to overflow EXIF data area.
-			thumbnailLength = Math.min(thumbnailLength, in.available() - thumbnailOffset);
-			if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_RAF
-					|| mMimeType == IMAGE_TYPE_RW2) {
-				thumbnailOffset += mExifOffset;
-			} else if (mMimeType == IMAGE_TYPE_ORF) {
-				// Update offset value since RAF files have IFD data preceding MakerNote data.
-				thumbnailOffset += mOrfMakerNoteOffset;
-			}
-			if (DEBUG) {
-				logger.debug("Setting thumbnail attributes with offset: " + thumbnailOffset
-						+ ", length: " + thumbnailLength);
-			}
-			if (thumbnailOffset > 0 && thumbnailLength > 0) {
-				mHasThumbnail = true;
-				mThumbnailOffset = thumbnailOffset;
-				mThumbnailLength = thumbnailLength;
-				if (mFilename == null && mAssetInputStream == null) {
-					// Save the thumbnail in memory if the input doesn't support reading again.
-					byte[] thumbnailBytes = new byte[thumbnailLength];
-					in.seek(thumbnailOffset);
-					in.readFully(thumbnailBytes);
-					mThumbnailBytes = thumbnailBytes;
-				}
-			}
-		}
-	}
-
-	// Check StripOffsets & StripByteCounts tags to retrieve thumbnail offset & length values
-	private void handleThumbnailFromStrips(ByteOrderedDataInputStream in, HashMap thumbnailData)
-			throws IOException {
-		ExifAttribute stripOffsetsAttribute =
-				(ExifAttribute) thumbnailData.get(TAG_STRIP_OFFSETS);
-		ExifAttribute stripByteCountsAttribute =
-				(ExifAttribute) thumbnailData.get(TAG_STRIP_BYTE_COUNTS);
-
-		if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
-			long[] stripOffsets =
-					convertToLongArray(stripOffsetsAttribute.getValue(mExifByteOrder));
-			long[] stripByteCounts =
-					convertToLongArray(stripByteCountsAttribute.getValue(mExifByteOrder));
-
-			if (stripOffsets == null) {
-				logger.warn("stripOffsets should not be null.");
-				return;
-			}
-			if (stripByteCounts == null) {
-				logger.warn("stripByteCounts should not be null.");
-				return;
-			}
-
-			long totalStripByteCount = 0;
-			for (long byteCount : stripByteCounts) {
-				totalStripByteCount += byteCount;
-			}
-
-			// Set thumbnail byte array data for non-consecutive strip bytes
-			byte[] totalStripBytes = new byte[(int) totalStripByteCount];
-
-			int bytesRead = 0;
-			int bytesAdded = 0;
-			for (int i = 0; i < stripOffsets.length; i++) {
-				int stripOffset = (int) stripOffsets[i];
-				int stripByteCount = (int) stripByteCounts[i];
-
-				// Skip to offset
-				int skipBytes = stripOffset - bytesRead;
-				if (skipBytes < 0) {
-					logger.debug("Invalid strip offset value");
-				}
-				in.seek(skipBytes);
-				bytesRead += skipBytes;
-
-				// Read strip bytes
-				byte[] stripBytes = new byte[stripByteCount];
-				in.read(stripBytes);
-				bytesRead += stripByteCount;
-
-				// Add bytes to array
-				System.arraycopy(stripBytes, 0, totalStripBytes, bytesAdded,
-						stripBytes.length);
-				bytesAdded += stripBytes.length;
-			}
-
-			mHasThumbnail = true;
-			mThumbnailBytes = totalStripBytes;
-			mThumbnailLength = totalStripBytes.length;
-		}
-	}
-
-	// Check if thumbnail data type is currently supported or not
-	private boolean isSupportedDataType(HashMap thumbnailData) throws IOException {
-		ExifAttribute bitsPerSampleAttribute =
-				(ExifAttribute) thumbnailData.get(TAG_BITS_PER_SAMPLE);
-		if (bitsPerSampleAttribute != null) {
-			int[] bitsPerSampleValue = (int[]) bitsPerSampleAttribute.getValue(mExifByteOrder);
-
-			if (Arrays.equals(BITS_PER_SAMPLE_RGB, bitsPerSampleValue)) {
-				return true;
-			}
-
-			// See DNG Specification 1.4.0.0. Section 3, Compression.
-			if (mMimeType == IMAGE_TYPE_DNG) {
-				ExifAttribute photometricInterpretationAttribute =
-						(ExifAttribute) thumbnailData.get(TAG_PHOTOMETRIC_INTERPRETATION);
-				if (photometricInterpretationAttribute != null) {
-					int photometricInterpretationValue
-							= photometricInterpretationAttribute.getIntValue(mExifByteOrder);
-					if ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO
-							&& Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_GREYSCALE_2))
-							|| ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_YCBCR)
-							&& (Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_RGB)))) {
-						return true;
-					} else {
-						// TODO: Add support for lossless Huffman JPEG data
-					}
-				}
-			}
-		}
-		if (DEBUG) {
-			logger.debug("Unsupported data type value");
-		}
-		return false;
-	}
-
-	// Returns true if the image length and width values are <= 512.
-	// See Section 4.8 of http://standardsproposals.bsigroup.com/Home/getPDF/567
-	private boolean isThumbnail(HashMap map) throws IOException {
-		ExifAttribute imageLengthAttribute = (ExifAttribute) map.get(TAG_IMAGE_LENGTH);
-		ExifAttribute imageWidthAttribute = (ExifAttribute) map.get(TAG_IMAGE_WIDTH);
-
-		if (imageLengthAttribute != null && imageWidthAttribute != null) {
-			int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder);
-			int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder);
-			if (imageLengthValue <= MAX_THUMBNAIL_SIZE && imageWidthValue <= MAX_THUMBNAIL_SIZE) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	// Validate primary, preview, thumbnail image data by comparing image size
-	private void validateImages() throws IOException {
-		// Swap images based on size (primary > preview > thumbnail)
-		swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_PREVIEW);
-		swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_THUMBNAIL);
-		swapBasedOnImageSize(IFD_TYPE_PREVIEW, IFD_TYPE_THUMBNAIL);
-
-		// Check if image has PixelXDimension/PixelYDimension tags, which contain valid image
-		// sizes, excluding padding at the right end or bottom end of the image to make sure that
-		// the values are multiples of 64. See JEITA CP-3451C Table 5 and Section 4.8.1. B.
-		ExifAttribute pixelXDimAttribute =
-				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_X_DIMENSION);
-		ExifAttribute pixelYDimAttribute =
-				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_Y_DIMENSION);
-		if (pixelXDimAttribute != null && pixelYDimAttribute != null) {
-			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, pixelXDimAttribute);
-			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, pixelYDimAttribute);
-		}
-
-		// Check whether thumbnail image exists and whether preview image satisfies the thumbnail
-		// image requirements
-		if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
-			if (isThumbnail(mAttributes[IFD_TYPE_PREVIEW])) {
-				mAttributes[IFD_TYPE_THUMBNAIL] = mAttributes[IFD_TYPE_PREVIEW];
-				mAttributes[IFD_TYPE_PREVIEW] = new HashMap<>();
-			}
-		}
-
-		// Check if the thumbnail image satisfies the thumbnail size requirements
-		if (!isThumbnail(mAttributes[IFD_TYPE_THUMBNAIL])) {
-			logger.debug("No image meets the size requirements of a thumbnail image.");
-		}
-	}
-
-	/**
-	 * If image is uncompressed, ImageWidth/Length tags are used to store size info.
-	 * However, uncompressed images often store extra pixels around the edges of the final image,
-	 * which results in larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags.
-	 * This method corrects those tag values by checking first the values of TAG_DEFAULT_CROP_SIZE
-	 * See DNG Specification 1.4.0.0. Section 4. (DefaultCropSize)
-	 *
-	 * If image is a RW2 file, valid image sizes are stored in SensorBorder tags.
-	 * See tiff_parser.cc GetFullDimension32()
-	 * */
-	private void updateImageSizeValues(ByteOrderedDataInputStream in, int imageType)
-			throws IOException {
-		// Uncompressed image valid image size values
-		ExifAttribute defaultCropSizeAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_DEFAULT_CROP_SIZE);
-		// RW2 image valid image size values
-		ExifAttribute topBorderAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_TOP_BORDER);
-		ExifAttribute leftBorderAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_LEFT_BORDER);
-		ExifAttribute bottomBorderAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_BOTTOM_BORDER);
-		ExifAttribute rightBorderAttribute =
-				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_RIGHT_BORDER);
-
-		if (defaultCropSizeAttribute != null) {
-			// Update for uncompressed image
-			ExifAttribute defaultCropSizeXAttribute, defaultCropSizeYAttribute;
-			if (defaultCropSizeAttribute.format == IFD_FORMAT_URATIONAL) {
-				Rational[] defaultCropSizeValue =
-						(Rational[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
-				if (defaultCropSizeValue == null || defaultCropSizeValue.length != 2) {
-					logger.warn("Invalid crop size values. cropSize="
-							+ Arrays.toString(defaultCropSizeValue));
-					return;
-				}
-				defaultCropSizeXAttribute =
-						ExifAttribute.createURational(defaultCropSizeValue[0], mExifByteOrder);
-				defaultCropSizeYAttribute =
-						ExifAttribute.createURational(defaultCropSizeValue[1], mExifByteOrder);
-			} else {
-				int[] defaultCropSizeValue =
-						(int[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
-				if (defaultCropSizeValue == null || defaultCropSizeValue.length != 2) {
-					logger.warn("Invalid crop size values. cropSize="
-							+ Arrays.toString(defaultCropSizeValue));
-					return;
-				}
-				defaultCropSizeXAttribute =
-						ExifAttribute.createUShort(defaultCropSizeValue[0], mExifByteOrder);
-				defaultCropSizeYAttribute =
-						ExifAttribute.createUShort(defaultCropSizeValue[1], mExifByteOrder);
-			}
-			mAttributes[imageType].put(TAG_IMAGE_WIDTH, defaultCropSizeXAttribute);
-			mAttributes[imageType].put(TAG_IMAGE_LENGTH, defaultCropSizeYAttribute);
-		} else if (topBorderAttribute != null && leftBorderAttribute != null &&
-				bottomBorderAttribute != null && rightBorderAttribute != null) {
-			// Update for RW2 image
-			int topBorderValue = topBorderAttribute.getIntValue(mExifByteOrder);
-			int bottomBorderValue = bottomBorderAttribute.getIntValue(mExifByteOrder);
-			int rightBorderValue = rightBorderAttribute.getIntValue(mExifByteOrder);
-			int leftBorderValue = leftBorderAttribute.getIntValue(mExifByteOrder);
-			if (bottomBorderValue > topBorderValue && rightBorderValue > leftBorderValue) {
-				int length = bottomBorderValue - topBorderValue;
-				int width = rightBorderValue - leftBorderValue;
-				ExifAttribute imageLengthAttribute =
-						ExifAttribute.createUShort(length, mExifByteOrder);
-				ExifAttribute imageWidthAttribute =
-						ExifAttribute.createUShort(width, mExifByteOrder);
-				mAttributes[imageType].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
-				mAttributes[imageType].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
-			}
-		} else {
-			retrieveJpegImageSize(in, imageType);
-		}
-	}
-
-	// Writes an Exif segment into the given output stream.
-	private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream,
-	                             int exifOffsetFromBeginning) throws IOException {
-		// The following variables are for calculating each IFD tag group size in bytes.
-		int[] ifdOffsets = new int[EXIF_TAGS.length];
-		int[] ifdDataSizes = new int[EXIF_TAGS.length];
-
-		// Remove IFD pointer tags (we'll re-add it later.)
-		for (ExifTag tag : EXIF_POINTER_TAGS) {
-			removeAttribute(tag.name);
-		}
-		// Remove old thumbnail data
-		removeAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name);
-		removeAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name);
-
-		// Remove null value tags.
-		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
-			for (Object obj : mAttributes[ifdType].entrySet().toArray()) {
-				final Map.Entry entry = (Map.Entry) obj;
-				if (entry.getValue() == null) {
-					mAttributes[ifdType].remove(entry.getKey());
-				}
-			}
-		}
-
-		// Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
-		// offset when there is one or more tags in the thumbnail IFD.
-		if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
-			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-		if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
-			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-		if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
-			mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name,
-					ExifAttribute.createULong(0, mExifByteOrder));
-		}
-		if (mHasThumbnail) {
-			mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name,
-					ExifAttribute.createULong(0, mExifByteOrder));
-			mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name,
-					ExifAttribute.createULong(mThumbnailLength, mExifByteOrder));
-		}
-
-		// Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
-		// value which has a bigger size than 4 bytes.
-		for (int i = 0; i < EXIF_TAGS.length; ++i) {
-			int sum = 0;
-			for (Map.Entry<String, ExifAttribute> entry : mAttributes[i].entrySet()) {
-				final ExifAttribute exifAttribute = entry.getValue();
-				final int size = exifAttribute.size();
-				if (size > 4) {
-					sum += size;
-				}
-			}
-			ifdDataSizes[i] += sum;
-		}
-
-		// Calculate IFD offsets.
-		int position = 8;
-		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
-			if (!mAttributes[ifdType].isEmpty()) {
-				ifdOffsets[ifdType] = position;
-				position += 2 + mAttributes[ifdType].size() * 12 + 4 + ifdDataSizes[ifdType];
-			}
-		}
-		if (mHasThumbnail) {
-			int thumbnailOffset = position;
-			mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name,
-					ExifAttribute.createULong(thumbnailOffset, mExifByteOrder));
-			mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset;
-			position += mThumbnailLength;
-		}
-
-		// Calculate the total size
-		int totalSize = position + 8;  // eight bytes is for header part.
-		if (DEBUG) {
-			logger.debug("totalSize length: " + totalSize);
-			for (int i = 0; i < EXIF_TAGS.length; ++i) {
-				logger.debug(String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
-						i, ifdOffsets[i], mAttributes[i].size(), ifdDataSizes[i]));
-			}
-		}
-
-		// Update IFD pointer tags with the calculated offsets.
-		if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
-			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
-					ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifByteOrder));
-		}
-		if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
-			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
-					ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifByteOrder));
-		}
-		if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
-			mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name, ExifAttribute.createULong(
-					ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifByteOrder));
-		}
-
-		// Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
-		dataOutputStream.writeUnsignedShort(totalSize);
-		dataOutputStream.write(IDENTIFIER_EXIF_APP1);
-		dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN
-				? BYTE_ALIGN_MM : BYTE_ALIGN_II);
-		dataOutputStream.setByteOrder(mExifByteOrder);
-		dataOutputStream.writeUnsignedShort(START_CODE);
-		dataOutputStream.writeUnsignedInt(IFD_OFFSET);
-
-		// Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9.
-		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
-			if (!mAttributes[ifdType].isEmpty()) {
-				// See JEITA CP-3451C Section 4.6.2: IFD structure.
-				// Write entry count
-				dataOutputStream.writeUnsignedShort(mAttributes[ifdType].size());
-
-				// Write entry info
-				int dataOffset = ifdOffsets[ifdType] + 2 + mAttributes[ifdType].size() * 12 + 4;
-				for (Map.Entry<String, ExifAttribute> entry : mAttributes[ifdType].entrySet()) {
-					// Convert tag name to tag number.
-					final ExifTag tag = sExifTagMapsForWriting[ifdType].get(entry.getKey());
-					final int tagNumber = tag.number;
-					final ExifAttribute attribute = entry.getValue();
-					final int size = attribute.size();
-
-					dataOutputStream.writeUnsignedShort(tagNumber);
-					dataOutputStream.writeUnsignedShort(attribute.format);
-					dataOutputStream.writeInt(attribute.numberOfComponents);
-					if (size > 4) {
-						dataOutputStream.writeUnsignedInt(dataOffset);
-						dataOffset += size;
-					} else {
-						dataOutputStream.write(attribute.bytes);
-						// Fill zero up to 4 bytes
-						if (size < 4) {
-							for (int i = size; i < 4; ++i) {
-								dataOutputStream.writeByte(0);
-							}
-						}
-					}
-				}
-
-				// Write the next offset. It writes the offset of thumbnail IFD if there is one or
-				// more tags in the thumbnail IFD when the current IFD is the primary image TIFF
-				// IFD; Otherwise 0.
-				if (ifdType == 0 && !mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
-					dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_TYPE_THUMBNAIL]);
-				} else {
-					dataOutputStream.writeUnsignedInt(0);
-				}
-
-				// Write values of data field exceeding 4 bytes after the next offset.
-				for (Map.Entry<String, ExifAttribute> entry : mAttributes[ifdType].entrySet()) {
-					ExifAttribute attribute = entry.getValue();
-
-					if (attribute.bytes.length > 4) {
-						dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length);
-					}
-				}
-			}
-		}
-
-		// Write thumbnail
-		if (mHasThumbnail) {
-			dataOutputStream.write(getThumbnailBytes());
-		}
-
-		// Reset the byte order to big endian in order to write remaining parts of the JPEG file.
-		dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
-
-		return totalSize;
-	}
-
-	/**
-	 * Determines the data format of EXIF entry value.
-	 *
-	 * @param entryValue The value to be determined.
-	 * @return Returns two data formats gussed as a pair in integer. If there is no two candidate
-	data formats for the given entry value, returns {@code -1} in the second of the pair.
-	 */
-	private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
-		// See TIFF 6.0 Section 2, "Image File Directory".
-		// Take the first component if there are more than one component.
-		if (entryValue.contains(",")) {
-			String[] entryValues = entryValue.split(",", -1);
-			Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
-			if (dataFormat.first == IFD_FORMAT_STRING) {
-				return dataFormat;
-			}
-			for (int i = 1; i < entryValues.length; ++i) {
-				final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
-				int first = -1, second = -1;
-				if (guessDataFormat.first.equals(dataFormat.first)
-						|| guessDataFormat.second.equals(dataFormat.first)) {
-					first = dataFormat.first;
-				}
-				if (dataFormat.second != -1 && (guessDataFormat.first.equals(dataFormat.second)
-						|| guessDataFormat.second.equals(dataFormat.second))) {
-					second = dataFormat.second;
-				}
-				if (first == -1 && second == -1) {
-					return new Pair<>(IFD_FORMAT_STRING, -1);
-				}
-				if (first == -1) {
-					dataFormat = new Pair<>(second, -1);
-					continue;
-				}
-				if (second == -1) {
-					dataFormat = new Pair<>(first, -1);
-					continue;
-				}
-			}
-			return dataFormat;
-		}
-
-		if (entryValue.contains("/")) {
-			String[] rationalNumber = entryValue.split("/", -1);
-			if (rationalNumber.length == 2) {
-				try {
-					long numerator = (long) Double.parseDouble(rationalNumber[0]);
-					long denominator = (long) Double.parseDouble(rationalNumber[1]);
-					if (numerator < 0L || denominator < 0L) {
-						return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
-					}
-					if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
-						return new Pair<>(IFD_FORMAT_URATIONAL, -1);
-					}
-					return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
-				} catch (NumberFormatException e)  {
-					// Ignored
+				// There are two types of headers for Olympus MakerNotes
+				// See http://www.exiv2.org/makernote.html#R1
+				byte[] makerNoteHeader1Bytes = new byte[ORF_MAKER_NOTE_HEADER_1.length];
+				makerNoteDataInputStream.readFully(makerNoteHeader1Bytes);
+				makerNoteDataInputStream.seek(0);
+				byte[] makerNoteHeader2Bytes = new byte[ORF_MAKER_NOTE_HEADER_2.length];
+				makerNoteDataInputStream.readFully(makerNoteHeader2Bytes);
+				// Skip the corresponding amount of bytes for each header type
+				if (Arrays.equals(makerNoteHeader1Bytes, ORF_MAKER_NOTE_HEADER_1)) {
+					makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_1_SIZE);
+				} else if (Arrays.equals(makerNoteHeader2Bytes, ORF_MAKER_NOTE_HEADER_2)) {
+					makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_2_SIZE);
 				}
-			}
-			return new Pair<>(IFD_FORMAT_STRING, -1);
-		}
-		try {
-			Long longValue = Long.parseLong(entryValue);
-			if (longValue >= 0 && longValue <= 65535) {
-				return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
-			}
-			if (longValue < 0) {
-				return new Pair<>(IFD_FORMAT_SLONG, -1);
-			}
-			return new Pair<>(IFD_FORMAT_ULONG, -1);
-		} catch (NumberFormatException e) {
-			// Ignored
-		}
-		try {
-			Double.parseDouble(entryValue);
-			return new Pair<>(IFD_FORMAT_DOUBLE, -1);
-		} catch (NumberFormatException e) {
-			// Ignored
-		}
-		return new Pair<>(IFD_FORMAT_STRING, -1);
-	}
-
-	// An input stream to parse EXIF data area, which can be written in either little or big endian
-	// order.
-	private static class ByteOrderedDataInputStream extends InputStream implements DataInput {
-		private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
-		private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
-
-		private DataInputStream mDataInputStream;
-		private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-		final int mLength;
-		@SuppressWarnings("WeakerAccess") /* synthetic access */
-				int mPosition;
-
-		public ByteOrderedDataInputStream(InputStream in) throws IOException {
-			mDataInputStream = new DataInputStream(in);
-			mLength = mDataInputStream.available();
-			mPosition = 0;
-			mDataInputStream.mark(mLength);
-		}
-
-		public ByteOrderedDataInputStream(byte[] bytes) throws IOException {
-			this(new ByteArrayInputStream(bytes));
-		}
-
-		public void setByteOrder(ByteOrder byteOrder) {
-			mByteOrder = byteOrder;
-		}
-
-		public void seek(long byteCount) throws IOException {
-			if (mPosition > byteCount) {
-				mPosition = 0;
-				mDataInputStream.reset();
-				mDataInputStream.mark(mLength);
-			} else {
-				byteCount -= mPosition;
-			}
-
-			if (skipBytes((int) byteCount) != (int) byteCount) {
-				throw new IOException("Couldn't seek up to the byteCount");
-			}
-		}
-
-		public int peek() {
-			return mPosition;
-		}
-
-		@Override
-		public int available() throws IOException {
-			return mDataInputStream.available();
-		}
-
-		@Override
-		public int read() throws IOException {
-			++mPosition;
-			return mDataInputStream.read();
-		}
-
-		@Override
-		public int read(byte[] b, int off, int len) throws IOException {
-			int bytesRead = mDataInputStream.read(b, off, len);
-			mPosition += bytesRead;
-			return bytesRead;
-		}
-
-		@Override
-		public int readUnsignedByte() throws IOException {
-			++mPosition;
-			return mDataInputStream.readUnsignedByte();
-		}
-
-		@Override
-		public String readLine() throws IOException {
-			logger.debug("Currently unsupported");
-			return null;
-		}
-
-		@Override
-		public boolean readBoolean() throws IOException {
-			++mPosition;
-			return mDataInputStream.readBoolean();
-		}
-
-		@Override
-		public char readChar() throws IOException {
-			mPosition += 2;
-			return mDataInputStream.readChar();
-		}
-
-		@Override
-		public String readUTF() throws IOException {
-			mPosition += 2;
-			return mDataInputStream.readUTF();
-		}
-
-		@Override
-		public void readFully(byte[] buffer, int offset, int length) throws IOException {
-			mPosition += length;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			if (mDataInputStream.read(buffer, offset, length) != length) {
-				throw new IOException("Couldn't read up to the length of buffer");
-			}
-		}
-
-		@Override
-		public void readFully(byte[] buffer) throws IOException {
-			mPosition += buffer.length;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) {
-				throw new IOException("Couldn't read up to the length of buffer");
-			}
-		}
-
-		@Override
-		public byte readByte() throws IOException {
-			++mPosition;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			int ch = mDataInputStream.read();
-			if (ch < 0) {
-				throw new EOFException();
-			}
-			return (byte) ch;
-		}
-
-		@Override
-		public short readShort() throws IOException {
-			mPosition += 2;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			int ch1 = mDataInputStream.read();
-			int ch2 = mDataInputStream.read();
-			if ((ch1 | ch2) < 0) {
-				throw new EOFException();
-			}
-			if (mByteOrder == LITTLE_ENDIAN) {
-				return (short) ((ch2 << 8) + (ch1));
-			} else if (mByteOrder == BIG_ENDIAN) {
-				return (short) ((ch1 << 8) + (ch2));
-			}
-			throw new IOException("Invalid byte order: " + mByteOrder);
-		}
-
-		@Override
-		public int readInt() throws IOException {
-			mPosition += 4;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			int ch1 = mDataInputStream.read();
-			int ch2 = mDataInputStream.read();
-			int ch3 = mDataInputStream.read();
-			int ch4 = mDataInputStream.read();
-			if ((ch1 | ch2 | ch3 | ch4) < 0) {
-				throw new EOFException();
-			}
-			if (mByteOrder == LITTLE_ENDIAN) {
-				return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
-			} else if (mByteOrder == BIG_ENDIAN) {
-				return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
-			}
-			throw new IOException("Invalid byte order: " + mByteOrder);
-		}
-
-		@Override
-		public int skipBytes(int byteCount) throws IOException {
-			int totalSkip = Math.min(byteCount, mLength - mPosition);
-			int skipped = 0;
-			while (skipped < totalSkip) {
-				skipped += mDataInputStream.skipBytes(totalSkip - skipped);
-			}
-			mPosition += skipped;
-			return skipped;
-		}
-
-		@Override
-		public int readUnsignedShort() throws IOException {
-			mPosition += 2;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			int ch1 = mDataInputStream.read();
-			int ch2 = mDataInputStream.read();
-			if ((ch1 | ch2) < 0) {
-				throw new EOFException();
-			}
-			if (mByteOrder == LITTLE_ENDIAN) {
-				return ((ch2 << 8) + (ch1));
-			} else if (mByteOrder == BIG_ENDIAN) {
-				return ((ch1 << 8) + (ch2));
-			}
-			throw new IOException("Invalid byte order: " + mByteOrder);
-		}
-
-		public long readUnsignedInt() throws IOException {
-			return readInt() & 0xffffffffL;
-		}
-
-		@Override
-		public long readLong() throws IOException {
-			mPosition += 8;
-			if (mPosition > mLength) {
-				throw new EOFException();
-			}
-			int ch1 = mDataInputStream.read();
-			int ch2 = mDataInputStream.read();
-			int ch3 = mDataInputStream.read();
-			int ch4 = mDataInputStream.read();
-			int ch5 = mDataInputStream.read();
-			int ch6 = mDataInputStream.read();
-			int ch7 = mDataInputStream.read();
-			int ch8 = mDataInputStream.read();
-			if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
-				throw new EOFException();
-			}
-			if (mByteOrder == LITTLE_ENDIAN) {
-				return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
-						+ ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
-						+ ((long) ch2 << 8) + (long) ch1);
-			} else if (mByteOrder == BIG_ENDIAN) {
-				return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
-						+ ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
-						+ ((long) ch7 << 8) + (long) ch8);
-			}
-			throw new IOException("Invalid byte order: " + mByteOrder);
-		}
-
-		@Override
-		public float readFloat() throws IOException {
-			return Float.intBitsToFloat(readInt());
-		}
-
-		@Override
-		public double readDouble() throws IOException {
-			return Double.longBitsToDouble(readLong());
-		}
-	}
-
-	// An output stream to write EXIF data area, which can be written in either little or big endian
-	// order.
-	private static class ByteOrderedDataOutputStream extends FilterOutputStream {
-		private final OutputStream mOutputStream;
-		private ByteOrder mByteOrder;
-
-		public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
-			super(out);
-			mOutputStream = out;
-			mByteOrder = byteOrder;
-		}
-
-		public void setByteOrder(ByteOrder byteOrder) {
-			mByteOrder = byteOrder;
-		}
-
-		@Override
-		public void write(byte[] bytes) throws IOException {
-			mOutputStream.write(bytes);
-		}
-
-		@Override
-		public void write(byte[] bytes, int offset, int length) throws IOException {
-			mOutputStream.write(bytes, offset, length);
-		}
-
-		public void writeByte(int val) throws IOException {
-			mOutputStream.write(val);
-		}
-
-		public void writeShort(short val) throws IOException {
-			if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
-				mOutputStream.write((val >>> 0) & 0xFF);
-				mOutputStream.write((val >>> 8) & 0xFF);
-			} else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
-				mOutputStream.write((val >>> 8) & 0xFF);
-				mOutputStream.write((val >>> 0) & 0xFF);
-			}
-		}
-
-		public void writeInt(int val) throws IOException {
-			if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
-				mOutputStream.write((val >>> 0) & 0xFF);
-				mOutputStream.write((val >>> 8) & 0xFF);
-				mOutputStream.write((val >>> 16) & 0xFF);
-				mOutputStream.write((val >>> 24) & 0xFF);
-			} else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
-				mOutputStream.write((val >>> 24) & 0xFF);
-				mOutputStream.write((val >>> 16) & 0xFF);
-				mOutputStream.write((val >>> 8) & 0xFF);
-				mOutputStream.write((val >>> 0) & 0xFF);
-			}
-		}
-
-		public void writeUnsignedShort(int val) throws IOException {
-			writeShort((short) val);
-		}
-
-		public void writeUnsignedInt(long val) throws IOException {
-			writeInt((int) val);
-		}
-	}
-
-	// Swaps image data based on image size
-	private void swapBasedOnImageSize(@IfdType int firstIfdType, @IfdType int secondIfdType)
-			throws IOException {
-		if (mAttributes[firstIfdType].isEmpty() || mAttributes[secondIfdType].isEmpty()) {
-			if (DEBUG) {
-				logger.debug("Cannot perform swap since only one image data exists");
-			}
-			return;
-		}
-
-		ExifAttribute firstImageLengthAttribute =
-				(ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_LENGTH);
-		ExifAttribute firstImageWidthAttribute =
-				(ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_WIDTH);
-		ExifAttribute secondImageLengthAttribute =
-				(ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_LENGTH);
-		ExifAttribute secondImageWidthAttribute =
-				(ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_WIDTH);
-
-		if (firstImageLengthAttribute == null || firstImageWidthAttribute == null) {
-			if (DEBUG) {
-				logger.debug("First image does not contain valid size information");
-			}
-		} else if (secondImageLengthAttribute == null || secondImageWidthAttribute == null) {
-			if (DEBUG) {
-				logger.debug("Second image does not contain valid size information");
-			}
-		} else {
-			int firstImageLengthValue = firstImageLengthAttribute.getIntValue(mExifByteOrder);
-			int firstImageWidthValue = firstImageWidthAttribute.getIntValue(mExifByteOrder);
-			int secondImageLengthValue = secondImageLengthAttribute.getIntValue(mExifByteOrder);
-			int secondImageWidthValue = secondImageWidthAttribute.getIntValue(mExifByteOrder);
-
-			if (firstImageLengthValue < secondImageLengthValue &&
-					firstImageWidthValue < secondImageWidthValue) {
-				HashMap<String, ExifAttribute> tempMap = mAttributes[firstIfdType];
-				mAttributes[firstIfdType] = mAttributes[secondIfdType];
-				mAttributes[secondIfdType] = tempMap;
-			}
-		}
-	}
 
-	/**
-	 * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
-	 */
-	private static void closeQuietly(Closeable closeable) {
-		if (closeable != null) {
-			try {
-				closeable.close();
-			} catch (RuntimeException rethrown) {
-				throw rethrown;
-			} catch (Exception ignored) {
+				// Read IFD data from MakerNote
+				readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_ORF_MAKER_NOTE);
 			}
-		}
-	}
-
-	/**
-	 * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
-	 * Returns the total number of bytes transferred.
-	 */
-	private static int copy(InputStream in, OutputStream out) throws IOException {
-		int total = 0;
-		byte[] buffer = new byte[8192];
-		int c;
-		while ((c = in.read(buffer)) != -1) {
-			total += c;
-			out.write(buffer, 0, c);
-		}
-		return total;
-	}
 
-	/**
-	 * Convert given int[] to long[]. If long[] is given, just return it.
-	 * Return null for other types of input.
-	 */
-	private static long[] convertToLongArray(Object inputObj) {
-		if (inputObj instanceof int[]) {
-			int[] input = (int[]) inputObj;
-			long[] result = new long[input.length];
-			for (int i = 0; i < input.length; i++) {
-				result[i] = input[i];
-			}
-			return result;
-		} else if (inputObj instanceof long[]) {
-			return (long[]) inputObj;
-		}
-		return null;
-	}
+            // Retrieve & update preview image offset & length values
+            ExifAttribute imageStartAttribute =
+                    mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_START);
+            ExifAttribute imageLengthAttribute =
+                    mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_LENGTH);
+
+            if (imageStartAttribute != null && imageLengthAttribute != null) {
+                mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT,
+                        imageStartAttribute);
+                mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+                        imageLengthAttribute);
+            }
+
+            // TODO: Check this behavior in other ORF files
+            // Retrieve primary image length & width values
+            // See piex.cc GetOlympusPreviewImage()
+            ExifAttribute aspectFrameAttribute =
+                    mAttributes[IFD_TYPE_ORF_IMAGE_PROCESSING].get(TAG_ORF_ASPECT_FRAME);
+            if (aspectFrameAttribute != null) {
+                int[] aspectFrameValues = (int[]) aspectFrameAttribute.getValue(mExifByteOrder);
+                if (aspectFrameValues == null || aspectFrameValues.length != 4) {
+                    logger.warn("Invalid aspect frame values. frame="
+                            + Arrays.toString(aspectFrameValues));
+                    return;
+                }
+                if (aspectFrameValues[2] > aspectFrameValues[0] &&
+                        aspectFrameValues[3] > aspectFrameValues[1]) {
+                    int primaryImageWidth = aspectFrameValues[2] - aspectFrameValues[0] + 1;
+                    int primaryImageLength = aspectFrameValues[3] - aspectFrameValues[1] + 1;
+                    // Swap width & length values
+                    if (primaryImageWidth < primaryImageLength) {
+                        primaryImageWidth += primaryImageLength;
+                        primaryImageLength = primaryImageWidth - primaryImageLength;
+                        primaryImageWidth -= primaryImageLength;
+                    }
+                    ExifAttribute primaryImageWidthAttribute =
+                            ExifAttribute.createUShort(primaryImageWidth, mExifByteOrder);
+                    ExifAttribute primaryImageLengthAttribute =
+                            ExifAttribute.createUShort(primaryImageLength, mExifByteOrder);
+
+                    mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, primaryImageWidthAttribute);
+                    mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, primaryImageLengthAttribute);
+                }
+            }
+        }
+    }
+
+    // RW2 contains the primary image data in IFD0 and the preview and/or thumbnail image data in
+    // the JpgFromRaw tag
+    // See https://libopenraw.freedesktop.org/wiki/Panasonic_RAW/ and piex.cc Rw2GetPreviewData()
+    private void getRw2Attributes(SeekableByteOrderedDataInputStream in) throws IOException {
+        if (DEBUG) {
+            logger.debug("getRw2Attributes starting with: " + in);
+        }
+        // Retrieve primary image data
+        getRawAttributes(in);
+
+        // Retrieve preview and/or thumbnail image data
+        ExifAttribute jpgFromRawAttribute =
+                mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_JPG_FROM_RAW);
+        if (jpgFromRawAttribute != null) {
+            ByteOrderedDataInputStream jpegInputStream =
+                    new ByteOrderedDataInputStream(jpgFromRawAttribute.bytes);
+            getJpegAttributes(jpegInputStream, (int) jpgFromRawAttribute.bytesOffset,
+                    IFD_TYPE_PREVIEW);
+        }
+
+        // Set ISO tag value if necessary
+        ExifAttribute rw2IsoAttribute =
+                mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_ISO);
+        ExifAttribute exifIsoAttribute =
+                mAttributes[IFD_TYPE_EXIF].get(TAG_PHOTOGRAPHIC_SENSITIVITY);
+        if (rw2IsoAttribute != null && exifIsoAttribute == null) {
+            // Place this attribute only if it doesn't exist
+            mAttributes[IFD_TYPE_EXIF].put(TAG_PHOTOGRAPHIC_SENSITIVITY, rw2IsoAttribute);
+        }
+    }
+
+    // PNG contains the EXIF data as a Special-Purpose Chunk
+    private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException {
+        if (DEBUG) {
+            logger.debug("getPngAttributes starting with: " + in);
+        }
+        // PNG uses Big Endian by default.
+        // See PNG (Portable Network Graphics) Specification, Version 1.2,
+        // 2.1. Integers and byte order
+        in.setByteOrder(BIG_ENDIAN);
+
+        int bytesRead = 0;
+
+        // Skip the signature bytes
+        in.skipFully(PNG_SIGNATURE.length);
+        bytesRead += PNG_SIGNATURE.length;
+
+        // Each chunk is made up of four parts:
+        //   1) Length: 4-byte unsigned integer indicating the number of bytes in the
+        //   Chunk Data field. Excludes Chunk Type and CRC bytes.
+        //   2) Chunk Type: 4-byte chunk type code.
+        //   3) Chunk Data: The data bytes. Can be zero-length.
+        //   4) CRC: 4-byte data calculated on the preceding bytes in the chunk. Always
+        //   present.
+        // --> 4 (length bytes) + 4 (type bytes) + X (data bytes) + 4 (CRC bytes)
+        // See PNG (Portable Network Graphics) Specification, Version 1.2,
+        // 3.2. Chunk layout
+        try {
+            while (true) {
+                int length = in.readInt();
+                bytesRead += 4;
+
+                byte[] type = new byte[PNG_CHUNK_TYPE_BYTE_LENGTH];
+                in.readFully(type);
+                bytesRead += PNG_CHUNK_TYPE_BYTE_LENGTH;
+
+                // The first chunk must be the IHDR chunk
+                if (bytesRead == 16 && !Arrays.equals(type, PNG_CHUNK_TYPE_IHDR)) {
+                    throw new IOException("Encountered invalid PNG file--IHDR chunk should appear"
+                            + "as the first chunk");
+                }
+
+                if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
+                    // IEND marks the end of the image.
+                    break;
+                } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
+                    // TODO: Need to handle potential OutOfMemoryError
+                    byte[] data = new byte[length];
+                    in.readFully(data);
+
+                    // Compare CRC values for potential data corruption.
+                    int dataCrcValue = in.readInt();
+                    // Cyclic Redundancy Code used to check for corruption of the data
+                    CRC32 crc = new CRC32();
+                    crc.update(type);
+                    crc.update(data);
+                    if ((int) crc.getValue() != dataCrcValue) {
+                        throw new IOException("Encountered invalid CRC value for PNG-EXIF chunk."
+                                + "\n recorded CRC value: " + dataCrcValue + ", calculated CRC "
+                                + "value: " + crc.getValue());
+                    }
+                    // Save offset to EXIF data for handling thumbnail and attribute offsets.
+                    mOffsetToExifData = bytesRead;
+                    readExifSegment(data, IFD_TYPE_PRIMARY);
+                    validateImages();
+
+                    setThumbnailData(new ByteOrderedDataInputStream(data));
+                    break;
+                } else {
+                    // Skip to next chunk
+                    in.skipFully(length + PNG_CHUNK_CRC_BYTE_LENGTH);
+                    bytesRead += length + PNG_CHUNK_CRC_BYTE_LENGTH;
+                }
+            }
+        } catch (EOFException e) {
+            // Should not reach here. Will only reach here if the file is corrupted or
+            // does not follow the PNG specifications
+            throw new IOException("Encountered corrupt PNG file.");
+        }
+    }
+
+    // WebP contains EXIF data as a RIFF File Format Chunk
+    // All references below can be found in the following link.
+    // https://developers.google.com/speed/webp/docs/riff_container
+    private void getWebpAttributes(ByteOrderedDataInputStream in) throws IOException {
+        if (DEBUG) {
+            logger.debug("getWebpAttributes starting with: " + in);
+        }
+        // WebP uses little-endian by default.
+        // See Section "Terminology & Basics"
+        in.setByteOrder(LITTLE_ENDIAN);
+
+        in.skipFully(WEBP_SIGNATURE_1.length);
+        // File size corresponds to the size of the entire file from offset 8.
+        // See Section "WebP File Header"
+        int fileSize = in.readInt() + 8;
+        int bytesRead = 8;
+
+        in.skipFully(WEBP_SIGNATURE_2.length);
+        bytesRead += WEBP_SIGNATURE_2.length;
+
+        try {
+            while (true) {
+                // TODO: Check the first Chunk Type, and if it is VP8X, check if the chunks are
+                // ordered properly.
+
+                // Each chunk is made up of three parts:
+                //   1) Chunk FourCC: 4-byte concatenating four ASCII characters.
+                //   2) Chunk Size: 4-byte unsigned integer indicating the size of the chunk.
+                //                  Excludes Chunk FourCC and Chunk Size bytes.
+                //   3) Chunk Payload: data payload. A single padding byte ('0') is added if
+                //                     Chunk Size is odd.
+                // See Section "RIFF File Format"
+                byte[] code = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+                in.readFully(code);
+                bytesRead += WEBP_CHUNK_TYPE_BYTE_LENGTH;
+
+                int chunkSize = in.readInt();
+                bytesRead += 4;
+
+                if (Arrays.equals(WEBP_CHUNK_TYPE_EXIF, code)) {
+                    // TODO: Need to handle potential OutOfMemoryError
+                    byte[] payload = new byte[chunkSize];
+                    in.readFully(payload);
+
+                    // Skip a JPEG APP1 marker that some image libraries incorrectly include in the
+                    // Exif data in WebP images (e.g.
+                    // https://github.com/ImageMagick/ImageMagick/issues/3140)
+                    if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
+                        int adjustedChunkSize = chunkSize - IDENTIFIER_EXIF_APP1.length;
+                        payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length,
+                                adjustedChunkSize);
+                    }
+
+                    // Save offset to EXIF data for handling thumbnail and attribute offsets.
+                    mOffsetToExifData = bytesRead;
+                    readExifSegment(payload, IFD_TYPE_PRIMARY);
+
+                    setThumbnailData(new ByteOrderedDataInputStream(payload));
+                    break;
+                } else {
+                    // Add a single padding byte at end if chunk size is odd
+                    chunkSize = (chunkSize % 2 == 1) ? chunkSize + 1 : chunkSize;
+
+                    // Check if skipping to next chunk is necessary
+                    if (bytesRead + chunkSize == fileSize) {
+                        // Reached end of file
+                        break;
+                    } else if (bytesRead + chunkSize > fileSize) {
+                        throw new IOException("Encountered WebP file with invalid chunk size");
+                    }
+
+                    // Skip to next chunk
+                    in.skipFully(chunkSize);
+                    bytesRead += chunkSize;
+                }
+            }
+        } catch (EOFException e) {
+            // Should not reach here. Will only reach here if the file is corrupted or
+            // does not follow the WebP specifications
+            throw new IOException("Encountered corrupt WebP file.");
+        }
+    }
+
+    // Stores a new JPEG image with EXIF attributes into a given output stream.
+	// Threema-modified: add stripExif parameter
+    private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream, boolean stripExif)
+            throws IOException {
+        // See JPEG File Interchange Format Specification, "JFIF Specification"
+        if (DEBUG) {
+            logger.debug("saveJpegAttributes starting with (inputStream: " + inputStream
+                    + ", outputStream: " + outputStream + ")");
+        }
+        ByteOrderedDataInputStream dataInputStream = new ByteOrderedDataInputStream(inputStream);
+        ByteOrderedDataOutputStream dataOutputStream =
+                new ByteOrderedDataOutputStream(outputStream, BIG_ENDIAN);
+        if (dataInputStream.readByte() != MARKER) {
+            throw new IOException("Invalid marker");
+        }
+        dataOutputStream.writeByte(MARKER);
+        if (dataInputStream.readByte() != MARKER_SOI) {
+            throw new IOException("Invalid marker");
+        }
+        dataOutputStream.writeByte(MARKER_SOI);
+
+        // Remove XMP data if it is from a separate marker (IDENTIFIER_XMP_APP1, not
+        // IDENTIFIER_EXIF_APP1)
+        // Will re-add it later after the rest of the file is written
+        ExifAttribute xmpAttribute = null;
+        if (getAttribute(TAG_XMP) != null && mXmpIsFromSeparateMarker) {
+            xmpAttribute = mAttributes[IFD_TYPE_PRIMARY].remove(TAG_XMP);
+        }
+
+        // Write EXIF APP1 segment
+				if (!stripExif) {
+			dataOutputStream.writeByte(MARKER);
+			dataOutputStream.writeByte(MARKER_APP1);
+			writeExifSegment(dataOutputStream);
+		}
+
+        // Re-add previously removed XMP data.
+        if (xmpAttribute != null) {
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_XMP, xmpAttribute);
+        }
+
+        byte[] bytes = new byte[4096];
+
+        while (true) {
+            byte marker = dataInputStream.readByte();
+            if (marker != MARKER) {
+                throw new IOException("Invalid marker");
+            }
+            marker = dataInputStream.readByte();
+            switch (marker) {
+                case MARKER_APP1: {
+                    int length = dataInputStream.readUnsignedShort() - 2;
+                    if (length < 0) {
+                        throw new IOException("Invalid length");
+                    }
+                    byte[] identifier = new byte[6];
+                    if (length >= 6) {
+                        dataInputStream.readFully(identifier);
+                        if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
+                            // Skip the original EXIF APP1 segment.
+                            dataInputStream.skipFully(length - 6);
+                            break;
+                        }
+                    }
+                    // Copy non-EXIF APP1 segment.
+                    dataOutputStream.writeByte(MARKER);
+                    dataOutputStream.writeByte(marker);
+                    dataOutputStream.writeUnsignedShort(length + 2);
+                    if (length >= 6) {
+                        length -= 6;
+                        dataOutputStream.write(identifier);
+                    }
+                    int read;
+                    while (length > 0 && (read = dataInputStream.read(
+                            bytes, 0, Math.min(length, bytes.length))) >= 0) {
+                        dataOutputStream.write(bytes, 0, read);
+                        length -= read;
+                    }
+                    break;
+                }
+                case MARKER_EOI:
+                case MARKER_SOS: {
+                    dataOutputStream.writeByte(MARKER);
+                    dataOutputStream.writeByte(marker);
+                    // Copy all the remaining data
+                    copy(dataInputStream, dataOutputStream);
+                    return;
+                }
+                default: {
+                    // Copy JPEG segment
+                    dataOutputStream.writeByte(MARKER);
+                    dataOutputStream.writeByte(marker);
+                    int length = dataInputStream.readUnsignedShort();
+                    dataOutputStream.writeUnsignedShort(length);
+                    length -= 2;
+                    if (length < 0) {
+                        throw new IOException("Invalid length");
+                    }
+                    int read;
+                    while (length > 0 && (read = dataInputStream.read(
+                            bytes, 0, Math.min(length, bytes.length))) >= 0) {
+                        dataOutputStream.write(bytes, 0, read);
+                        length -= read;
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
+    private void savePngAttributes(InputStream inputStream, OutputStream outputStream)
+            throws IOException {
+        if (DEBUG) {
+            logger.debug("savePngAttributes starting with (inputStream: " + inputStream
+                    + ", outputStream: " + outputStream + ")");
+        }
+        ByteOrderedDataInputStream dataInputStream = new ByteOrderedDataInputStream(inputStream);
+        ByteOrderedDataOutputStream dataOutputStream =
+                new ByteOrderedDataOutputStream(outputStream, BIG_ENDIAN);
+
+        // Copy PNG signature bytes
+        copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
+
+        // EXIF chunk can appear anywhere between the first (IHDR) and last (IEND) chunks, except
+        // between IDAT chunks.
+        // Adhering to these rules,
+        //   1) if EXIF chunk did not exist in the original file, it will be stored right after the
+        //      first chunk,
+        //   2) if EXIF chunk existed in the original file, it will be stored in the same location.
+        if (mOffsetToExifData == 0) {
+            // Copy IHDR chunk bytes
+            int ihdrChunkLength = dataInputStream.readInt();
+            dataOutputStream.writeInt(ihdrChunkLength);
+            copy(dataInputStream, dataOutputStream, PNG_CHUNK_TYPE_BYTE_LENGTH
+                    + ihdrChunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
+        } else {
+            // Copy up until the point where EXIF chunk length information is stored.
+            int copyLength = mOffsetToExifData - PNG_SIGNATURE.length
+                    - 4 /* PNG EXIF chunk length bytes */
+                    - PNG_CHUNK_TYPE_BYTE_LENGTH;
+            copy(dataInputStream, dataOutputStream, copyLength);
+
+            // Skip to the start of the chunk after the EXIF chunk
+            int exifChunkLength = dataInputStream.readInt();
+            dataInputStream.skipFully(PNG_CHUNK_TYPE_BYTE_LENGTH + exifChunkLength
+                    + PNG_CHUNK_CRC_BYTE_LENGTH);
+        }
+
+        // Write EXIF data
+        ByteArrayOutputStream exifByteArrayOutputStream = null;
+        try {
+            // A byte array is needed to calculate the CRC value of this chunk which requires
+            // the chunk type bytes and the chunk data bytes.
+            exifByteArrayOutputStream = new ByteArrayOutputStream();
+            ByteOrderedDataOutputStream exifDataOutputStream =
+                    new ByteOrderedDataOutputStream(exifByteArrayOutputStream, BIG_ENDIAN);
+
+            // Store Exif data in separate byte array
+            writeExifSegment(exifDataOutputStream);
+            byte[] exifBytes =
+                    ((ByteArrayOutputStream) exifDataOutputStream.mOutputStream).toByteArray();
+
+            // Write EXIF chunk data
+            dataOutputStream.write(exifBytes);
+
+            // Write EXIF chunk CRC
+            CRC32 crc = new CRC32();
+            crc.update(exifBytes, 4 /* skip length bytes */, exifBytes.length - 4);
+            dataOutputStream.writeInt((int) crc.getValue());
+        } finally {
+            closeQuietly(exifByteArrayOutputStream);
+        }
+
+        // Copy the rest of the file
+        copy(dataInputStream, dataOutputStream);
+    }
+
+    // A WebP file has a header and a series of chunks.
+    // The header is composed of:
+    //   "RIFF" + File Size + "WEBP"
+    //
+    // The structure of the chunks can be divided largely into two categories:
+    //   1) Contains only image data,
+    //   2) Contains image data and extra data.
+    // In the first category, there is only one chunk: type "VP8" (compression with loss) or "VP8L"
+    // (lossless compression).
+    // In the second category, the first chunk will be of type "VP8X", which contains flags
+    // indicating which extra data exist in later chunks. The proceeding chunks must conform to
+    // the following order based on type (if they exist):
+    //   Color Profile ("ICCP") + Animation Control Data ("ANIM") + Image Data ("VP8"/"VP8L")
+    //   + Exif metadata ("EXIF") + XMP metadata ("XMP")
+    //
+    // And in order to have EXIF data, a WebP file must be of the second structure and thus follow
+    // the following rules:
+    //   1) "VP8X" chunk as the first chunk,
+    //   2) flag for EXIF inside "VP8X" chunk set to 1, and
+    //   3) contain the "EXIF" chunk in the correct order amongst other chunks.
+    //
+    // Based on these rules, this API will support three different cases depending on the contents
+    // of the original file:
+    //   1) "EXIF" chunk already exists
+    //     -> replace it with the new "EXIF" chunk
+    //   2) "EXIF" chunk does not exist and the first chunk is "VP8" or "VP8L"
+    //     -> add "VP8X" before the "VP8"/"VP8L" chunk (with EXIF flag set to 1), and add new
+    //     "EXIF" chunk after the "VP8"/"VP8L" chunk.
+    //   3) "EXIF" chunk does not exist and the first chunk is "VP8X"
+    //     -> set EXIF flag in "VP8X" chunk to 1, and add new "EXIF" chunk at the proper location.
+    //
+    // See https://developers.google.com/speed/webp/docs/riff_container for more details.
+    private void saveWebpAttributes(InputStream inputStream, OutputStream outputStream)
+            throws IOException {
+        if (DEBUG) {
+            logger.debug("saveWebpAttributes starting with (inputStream: " + inputStream
+                    + ", outputStream: " + outputStream + ")");
+        }
+        ByteOrderedDataInputStream totalInputStream =
+                new ByteOrderedDataInputStream(inputStream, LITTLE_ENDIAN);
+        ByteOrderedDataOutputStream totalOutputStream =
+                new ByteOrderedDataOutputStream(outputStream, LITTLE_ENDIAN);
+
+        // WebP signature
+        copy(totalInputStream, totalOutputStream, WEBP_SIGNATURE_1.length);
+        // File length will be written after all the chunks have been written
+        totalInputStream.skipFully(WEBP_FILE_SIZE_BYTE_LENGTH + WEBP_SIGNATURE_2.length);
+
+        // Create a separate byte array to calculate file length
+        ByteArrayOutputStream nonHeaderByteArrayOutputStream = null;
+        try {
+            nonHeaderByteArrayOutputStream = new ByteArrayOutputStream();
+            ByteOrderedDataOutputStream nonHeaderOutputStream =
+                    new ByteOrderedDataOutputStream(nonHeaderByteArrayOutputStream, LITTLE_ENDIAN);
+
+            if (mOffsetToExifData != 0) {
+                // EXIF chunk exists in the original file
+                // Tested by webp_with_exif.webp
+                int bytesRead = WEBP_SIGNATURE_1.length + WEBP_FILE_SIZE_BYTE_LENGTH
+                        + WEBP_SIGNATURE_2.length;
+                copy(totalInputStream, nonHeaderOutputStream,
+                        mOffsetToExifData - bytesRead - WEBP_CHUNK_TYPE_BYTE_LENGTH
+                                - WEBP_CHUNK_SIZE_BYTE_LENGTH);
+
+                // Skip input stream to the end of the EXIF chunk
+                totalInputStream.skipFully(WEBP_CHUNK_TYPE_BYTE_LENGTH);
+                int exifChunkLength = totalInputStream.readInt();
+                // RIFF chunks have a single padding byte at the end if the declared chunk size is
+                // odd.
+                if (exifChunkLength % 2 != 0) {
+                    exifChunkLength++;
+                }
+                totalInputStream.skipFully(exifChunkLength);
+
+                // Write new EXIF chunk to output stream
+                writeExifSegment(nonHeaderOutputStream);
+            } else {
+                // EXIF chunk does not exist in the original file
+                byte[] firstChunkType = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+                totalInputStream.readFully(firstChunkType);
+
+                if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8X)) {
+                    // Original file already includes other extra data
+                    int size = totalInputStream.readInt();
+                    // WebP files have a single padding byte at the end if the chunk size is odd.
+                    byte[] data = new byte[(size % 2) == 1 ? size + 1 : size];
+                    totalInputStream.readFully(data);
+
+                    // Set the EXIF flag to 1
+                    data[0] = (byte) (data[0] | (1 << 3));
+
+                    // Retrieve Animation flag--in order to check where EXIF data should start
+                    boolean containsAnimation = ((data[0] >> 1) & 1) == 1;
+
+                    // Write the original VP8X chunk
+                    nonHeaderOutputStream.write(WEBP_CHUNK_TYPE_VP8X);
+                    nonHeaderOutputStream.writeInt(size);
+                    nonHeaderOutputStream.write(data);
+
+                    // Animation control data is composed of 1 ANIM chunk and multiple ANMF
+                    // chunks and since the image data (VP8/VP8L) chunks are included in the ANMF
+                    // chunks, EXIF data should come after the last ANMF chunk.
+                    // Also, because there is no value indicating the amount of ANMF chunks, we need
+                    // to keep iterating through chunks until we either reach the end of the file or
+                    // the XMP chunk (if it exists).
+                    // Tested by webp_with_anim_without_exif.webp
+                    if (containsAnimation) {
+                        copyChunksUpToGivenChunkType(totalInputStream, nonHeaderOutputStream,
+                                WEBP_CHUNK_TYPE_ANIM, null);
+
+                        while (true) {
+                            byte[] type = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+                            boolean animationFinished = false;
+                            try {
+                                totalInputStream.readFully(type);
+                                animationFinished = !Arrays.equals(type, WEBP_CHUNK_TYPE_ANMF);
+                            } catch (EOFException e) {
+                                animationFinished = true;
+                            }
+                            if (animationFinished) {
+                                writeExifSegment(nonHeaderOutputStream);
+                                break;
+                            }
+                            copyWebPChunk(totalInputStream, nonHeaderOutputStream, type);
+                        }
+                    } else {
+                        // Skip until we find the VP8 or VP8L chunk
+                        copyChunksUpToGivenChunkType(totalInputStream, nonHeaderOutputStream,
+                                WEBP_CHUNK_TYPE_VP8, WEBP_CHUNK_TYPE_VP8L);
+                        writeExifSegment(nonHeaderOutputStream);
+                    }
+                } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)
+                        || Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
+                    int size = totalInputStream.readInt();
+                    int bytesToRead = size;
+                    // WebP files have a single padding byte at the end if the chunk size is odd.
+                    if (size % 2 == 1) {
+                        bytesToRead += 1;
+                    }
+
+                    // Retrieve image width/height
+                    int widthAndHeight = 0;
+                    int width = 0;
+                    int height = 0;
+                    boolean alpha = false;
+                    // Save VP8 frame data for later
+                    byte[] vp8Frame = new byte[3];
+
+                    if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)) {
+                        totalInputStream.readFully(vp8Frame);
+
+                        // Check signature
+                        byte[] vp8Signature = new byte[3];
+                        totalInputStream.readFully(vp8Signature);
+                        if (!Arrays.equals(WEBP_VP8_SIGNATURE, vp8Signature)) {
+                            throw new IOException("Error checking VP8 signature");
+                        }
+
+                        // Retrieve image width/height
+                        widthAndHeight = totalInputStream.readInt();
+                        width = (widthAndHeight << 18) >> 18;
+                        height = (widthAndHeight << 2) >> 18;
+                        bytesToRead -= (vp8Frame.length + vp8Signature.length + 4);
+                    } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
+                        // Check signature
+                        byte vp8lSignature = totalInputStream.readByte();
+                        if (vp8lSignature != WEBP_VP8L_SIGNATURE) {
+                            throw new IOException("Error checking VP8L signature");
+                        }
+
+                        // Retrieve image width/height
+                        widthAndHeight = totalInputStream.readInt();
+                        // VP8L stores 14-bit 'width - 1' and 'height - 1' values. See "RIFF Header"
+                        // of "WebP Lossless Bitstream Specification".
+                        width = (widthAndHeight & 0x3FFF) + 1;  // Read bits 0 - 13
+                        height = ((widthAndHeight & 0xFFFC000) >>> 14) + 1;  // Read bits 14 - 27
+                        // Retrieve alpha bit 28
+                        alpha = (widthAndHeight & 1 << 28) != 0;
+                        bytesToRead -= (1 /* VP8L signature */ + 4);
+                    }
+
+                    // Create VP8X with Exif flag set to 1
+                    nonHeaderOutputStream.write(WEBP_CHUNK_TYPE_VP8X);
+                    nonHeaderOutputStream.writeInt(WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH);
+                    byte[] data = new byte[WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH];
+                    // ALPHA flag
+                    if (alpha) {
+                        data[0] = (byte) (data[0] | (1 << 4));
+                    }
+                    // EXIF flag
+                    data[0] = (byte) (data[0] | (1 << 3));
+                    // VP8X stores Width - 1 and Height - 1 values
+                    width -= 1;
+                    height -= 1;
+                    data[4] = (byte) width;
+                    data[5] = (byte) (width >> 8);
+                    data[6] = (byte) (width >> 16);
+                    data[7] = (byte) height;
+                    data[8] = (byte) (height >> 8);
+                    data[9] = (byte) (height >> 16);
+                    nonHeaderOutputStream.write(data);
+
+                    // Write VP8 or VP8L data
+                    nonHeaderOutputStream.write(firstChunkType);
+                    nonHeaderOutputStream.writeInt(size);
+                    if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)) {
+                        nonHeaderOutputStream.write(vp8Frame);
+                        nonHeaderOutputStream.write(WEBP_VP8_SIGNATURE);
+                        nonHeaderOutputStream.writeInt(widthAndHeight);
+                    } else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
+                        nonHeaderOutputStream.write(WEBP_VP8L_SIGNATURE);
+                        nonHeaderOutputStream.writeInt(widthAndHeight);
+                    }
+                    copy(totalInputStream, nonHeaderOutputStream, bytesToRead);
+
+                    // Write EXIF chunk
+                    writeExifSegment(nonHeaderOutputStream);
+                }
+            }
+
+            // Copy the rest of the file
+            copy(totalInputStream, nonHeaderOutputStream);
+
+            // Write file length + second signature
+            totalOutputStream.writeInt(nonHeaderByteArrayOutputStream.size()
+                    + WEBP_SIGNATURE_2.length);
+            totalOutputStream.write(WEBP_SIGNATURE_2);
+            nonHeaderByteArrayOutputStream.writeTo(totalOutputStream);
+        } catch (Exception e) {
+            throw new IOException("Failed to save WebP file", e);
+        } finally {
+            closeQuietly(nonHeaderByteArrayOutputStream);
+        }
+    }
+
+    private void copyChunksUpToGivenChunkType(ByteOrderedDataInputStream inputStream,
+            ByteOrderedDataOutputStream outputStream, byte[] firstGivenType,
+            byte[] secondGivenType) throws IOException {
+        while (true) {
+            byte[] type = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
+            inputStream.readFully(type);
+            copyWebPChunk(inputStream, outputStream, type);
+            if (Arrays.equals(type, firstGivenType)
+                    || (secondGivenType != null && Arrays.equals(type, secondGivenType))) {
+                break;
+            }
+        }
+    }
+
+    private void copyWebPChunk(ByteOrderedDataInputStream inputStream,
+            ByteOrderedDataOutputStream outputStream, byte[] type) throws IOException {
+        int size = inputStream.readInt();
+        outputStream.write(type);
+        outputStream.writeInt(size);
+        // WebP files have a single padding byte at the end if the chunk size is odd.
+        copy(inputStream, outputStream, (size % 2) == 1 ? size + 1 : size);
+    }
+
+    // Reads the given EXIF byte area and save its tag data into attributes.
+    private void readExifSegment(byte[] exifBytes, int imageType) throws IOException {
+        SeekableByteOrderedDataInputStream dataInputStream =
+                new SeekableByteOrderedDataInputStream(exifBytes);
+
+        // Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
+        parseTiffHeaders(dataInputStream);
+
+        // Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
+        readImageFileDirectory(dataInputStream, imageType);
+    }
+
+    private void addDefaultValuesForCompatibility() {
+        // If DATETIME tag has no value, then set the value to DATETIME_ORIGINAL tag's.
+        String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL);
+        if (valueOfDateTimeOriginal != null && getAttribute(TAG_DATETIME) == null) {
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_DATETIME,
+                    ExifAttribute.createString(valueOfDateTimeOriginal));
+        }
+
+        // Add the default value.
+        if (getAttribute(TAG_IMAGE_WIDTH) == null) {
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+        if (getAttribute(TAG_IMAGE_LENGTH) == null) {
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+        if (getAttribute(TAG_ORIENTATION) == null) {
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+        if (getAttribute(TAG_LIGHT_SOURCE) == null) {
+            mAttributes[IFD_TYPE_EXIF].put(TAG_LIGHT_SOURCE,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+    }
+
+    private ByteOrder readByteOrder(ByteOrderedDataInputStream dataInputStream)
+            throws IOException {
+        // Read byte order.
+        short byteOrder = dataInputStream.readShort();
+        switch (byteOrder) {
+            case BYTE_ALIGN_II:
+                if (DEBUG) {
+                    logger.debug("readExifSegment: Byte Align II");
+                }
+                return LITTLE_ENDIAN;
+            case BYTE_ALIGN_MM:
+                if (DEBUG) {
+                    logger.debug("readExifSegment: Byte Align MM");
+                }
+                return BIG_ENDIAN;
+            default:
+                throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder));
+        }
+    }
+
+    private void parseTiffHeaders(ByteOrderedDataInputStream dataInputStream) throws IOException {
+        // Read byte order
+        mExifByteOrder = readByteOrder(dataInputStream);
+        // Set byte order
+        dataInputStream.setByteOrder(mExifByteOrder);
+
+        // Check start code
+        int startCode = dataInputStream.readUnsignedShort();
+        if (mMimeType != IMAGE_TYPE_ORF && mMimeType != IMAGE_TYPE_RW2 && startCode != START_CODE) {
+            throw new IOException("Invalid start code: " + Integer.toHexString(startCode));
+        }
+
+        // Read and skip to first ifd offset
+        int firstIfdOffset = dataInputStream.readInt();
+        if (firstIfdOffset < 8) {
+            throw new IOException("Invalid first Ifd offset: " + firstIfdOffset);
+        }
+        firstIfdOffset -= 8;
+        if (firstIfdOffset > 0) {
+            dataInputStream.skipFully(firstIfdOffset);
+        }
+    }
+
+    // Reads image file directory, which is a tag group in EXIF.
+    private void readImageFileDirectory(SeekableByteOrderedDataInputStream dataInputStream,
+            @IfdType int ifdType) throws IOException {
+        // Save offset of current IFD to prevent reading an IFD that is already read.
+        mAttributesOffsets.add(dataInputStream.position());
+
+        // See TIFF 6.0 Section 2: TIFF Structure, Figure 1.
+        short numberOfDirectoryEntry = dataInputStream.readShort();
+        if (DEBUG) {
+            logger.debug("numberOfDirectoryEntry: " + numberOfDirectoryEntry);
+        }
+        if (numberOfDirectoryEntry <= 0) {
+            // Return if the size of entries is negative.
+            return;
+        }
+
+        // See TIFF 6.0 Section 2: TIFF Structure, "Image File Directory".
+        for (short i = 0; i < numberOfDirectoryEntry; ++i) {
+            int tagNumber = dataInputStream.readUnsignedShort();
+            int dataFormat = dataInputStream.readUnsignedShort();
+            int numberOfComponents = dataInputStream.readInt();
+            // Next four bytes is for data offset or value.
+            long nextEntryOffset = dataInputStream.position() + 4L;
+
+            // Look up a corresponding tag from tag number
+            ExifTag tag = sExifTagMapsForReading[ifdType].get(tagNumber);
+
+            if (DEBUG) {
+                logger.debug(String.format("ifdType: %d, tagNumber: %d, tagName: %s, dataFormat: %d, "
+                        + "numberOfComponents: %d", ifdType, tagNumber,
+                        tag != null ? tag.name : null, dataFormat, numberOfComponents));
+            }
+
+            long byteCount = 0;
+            boolean valid = false;
+            if (tag == null) {
+                if (DEBUG) {
+                    logger.debug("Skip the tag entry since tag number is not defined: " + tagNumber);
+                }
+            } else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
+                if (DEBUG) {
+                    logger.debug("Skip the tag entry since data format is invalid: " + dataFormat);
+                }
+            } else if (!tag.isFormatCompatible(dataFormat)) {
+                if (DEBUG) {
+                    logger.debug("Skip the tag entry since data format ("
+                            + IFD_FORMAT_NAMES[dataFormat] + ") is unexpected for tag: "
+                            + tag.name);
+                }
+            } else {
+                if (dataFormat == IFD_FORMAT_UNDEFINED) {
+                    dataFormat = tag.primaryFormat;
+                }
+                byteCount = (long) numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
+                if (byteCount < 0 || byteCount > Integer.MAX_VALUE) {
+                    if (DEBUG) {
+                        logger.debug("Skip the tag entry since the number of components is invalid: "
+                                + numberOfComponents);
+                    }
+                } else {
+                    valid = true;
+                }
+            }
+            if (!valid) {
+                dataInputStream.seek(nextEntryOffset);
+                continue;
+            }
+
+            // Read a value from data field or seek to the value offset which is stored in data
+            // field if the size of the entry value is bigger than 4.
+            if (byteCount > 4) {
+                int offset = dataInputStream.readInt();
+                if (DEBUG) {
+                    logger.debug("seek to data offset: " + offset);
+                }
+                if (mMimeType == IMAGE_TYPE_ORF) {
+                    if (TAG_MAKER_NOTE.equals(tag.name)) {
+                        // Save offset value for reading thumbnail
+                        mOrfMakerNoteOffset = offset;
+                    } else if (ifdType == IFD_TYPE_ORF_MAKER_NOTE
+                            && TAG_ORF_THUMBNAIL_IMAGE.equals(tag.name)) {
+                        // Retrieve & update values for thumbnail offset and length values for ORF
+                        mOrfThumbnailOffset = offset;
+                        mOrfThumbnailLength = numberOfComponents;
+
+                        ExifAttribute compressionAttribute =
+                                ExifAttribute.createUShort(DATA_JPEG, mExifByteOrder);
+                        ExifAttribute jpegInterchangeFormatAttribute =
+                                ExifAttribute.createULong(mOrfThumbnailOffset, mExifByteOrder);
+                        ExifAttribute jpegInterchangeFormatLengthAttribute =
+                                ExifAttribute.createULong(mOrfThumbnailLength, mExifByteOrder);
+
+                        mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_COMPRESSION, compressionAttribute);
+                        mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
+                                jpegInterchangeFormatAttribute);
+                        mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+                                jpegInterchangeFormatLengthAttribute);
+                    }
+                }
+                dataInputStream.seek(offset);
+            }
+
+            // Recursively parse IFD when a IFD pointer tag appears.
+            Integer nextIfdType = sExifPointerTagMap.get(tagNumber);
+            if (DEBUG) {
+                logger.debug("nextIfdType: " + nextIfdType + " byteCount: " + byteCount);
+            }
+
+            if (nextIfdType != null) {
+                long offset = -1L;
+                // Get offset from data field
+                switch (dataFormat) {
+                    case IFD_FORMAT_USHORT: {
+                        offset = dataInputStream.readUnsignedShort();
+                        break;
+                    }
+                    case IFD_FORMAT_SSHORT: {
+                        offset = dataInputStream.readShort();
+                        break;
+                    }
+                    case IFD_FORMAT_ULONG: {
+                        offset = dataInputStream.readUnsignedInt();
+                        break;
+                    }
+                    case IFD_FORMAT_SLONG:
+                    case IFD_FORMAT_IFD: {
+                        offset = dataInputStream.readInt();
+                        break;
+                    }
+                    default: {
+                        // Nothing to do
+                        break;
+                    }
+                }
+                if (DEBUG) {
+                    logger.debug(String.format("Offset: %d, tagName: %s", offset, tag.name));
+                }
+
+                // Check if the next IFD offset
+                // 1. Is a non-negative value (within the length of the input, if known), and
+                // 2. Does not point to a previously read IFD.
+                if (offset > 0L
+                        && (dataInputStream.length() == ByteOrderedDataInputStream.LENGTH_UNSET
+                                || offset < dataInputStream.length())) {
+                    if (!mAttributesOffsets.contains((int) offset)) {
+                        dataInputStream.seek(offset);
+                        readImageFileDirectory(dataInputStream, nextIfdType);
+                    } else {
+                        if (DEBUG) {
+                            logger.debug("Skip jump into the IFD since it has already been read: "
+                                    + "IfdType " + nextIfdType + " (at " + offset + ")");
+                        }
+                    }
+                } else {
+                    if (DEBUG) {
+                        String message =
+                                "Skip jump into the IFD since its offset is invalid: " + offset;
+                        if (dataInputStream.length() != ByteOrderedDataInputStream.LENGTH_UNSET) {
+                            message += " (total length: " + dataInputStream.length() + ")";
+                        }
+                        logger.debug(message);
+                    }
+                }
+
+                dataInputStream.seek(nextEntryOffset);
+                continue;
+            }
+
+            final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
+            final byte[] bytes = new byte[(int) byteCount];
+            dataInputStream.readFully(bytes);
+            ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,
+                    bytesOffset, bytes);
+            mAttributes[ifdType].put(tag.name, attribute);
+
+            // DNG files have a DNG Version tag specifying the version of specifications that the
+            // image file is following.
+            // See http://fileformats.archiveteam.org/wiki/DNG
+            if (TAG_DNG_VERSION.equals(tag.name)) {
+                mMimeType = IMAGE_TYPE_DNG;
+            }
+
+            // PEF files have a Make or Model tag that begins with "PENTAX" or a compression tag
+            // that is 65535.
+            // See http://fileformats.archiveteam.org/wiki/Pentax_PEF
+            if (((TAG_MAKE.equals(tag.name) || TAG_MODEL.equals(tag.name))
+                    && attribute.getStringValue(mExifByteOrder).contains(PEF_SIGNATURE))
+                    || (TAG_COMPRESSION.equals(tag.name)
+                    && attribute.getIntValue(mExifByteOrder) == 65535)) {
+                mMimeType = IMAGE_TYPE_PEF;
+            }
+
+            // Seek to next tag offset
+            if (dataInputStream.position() != nextEntryOffset) {
+                dataInputStream.seek(nextEntryOffset);
+            }
+        }
+
+        int nextIfdOffset = dataInputStream.readInt();
+        if (DEBUG) {
+            logger.debug(String.format("nextIfdOffset: %d", nextIfdOffset));
+        }
+        // Check if the next IFD offset
+        // 1. Is a non-negative value, and
+        // 2. Does not point to a previously read IFD.
+        if (nextIfdOffset > 0L) {
+            if (!mAttributesOffsets.contains(nextIfdOffset)) {
+                dataInputStream.seek(nextIfdOffset);
+                if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
+                    // Do not overwrite thumbnail IFD data if it already exists.
+                    readImageFileDirectory(dataInputStream, IFD_TYPE_THUMBNAIL);
+                } else if (mAttributes[IFD_TYPE_PREVIEW].isEmpty()) {
+                    readImageFileDirectory(dataInputStream, IFD_TYPE_PREVIEW);
+                }
+            } else {
+                if (DEBUG) {
+                    logger.debug("Stop reading file since re-reading an IFD may cause an "
+                            + "infinite loop: " + nextIfdOffset);
+                }
+            }
+        } else {
+            if (DEBUG) {
+                logger.debug("Stop reading file since a wrong offset may cause an infinite loop: "
+                        + nextIfdOffset);
+            }
+        }
+    }
+
+    /**
+     * JPEG compressed images do not contain IMAGE_LENGTH & IMAGE_WIDTH tags.
+     * This value uses JpegInterchangeFormat(JPEG data offset) value, and calls getJpegAttributes()
+     * to locate SOF(Start of Frame) marker and update the image length & width values.
+     * See JEITA CP-3451C Table 5 and Section 4.8.1. B.
+     */
+    private void retrieveJpegImageSize(SeekableByteOrderedDataInputStream in, int imageType)
+            throws IOException {
+        // Check if image already has IMAGE_LENGTH & IMAGE_WIDTH values
+        ExifAttribute imageLengthAttribute =
+                mAttributes[imageType].get(TAG_IMAGE_LENGTH);
+        ExifAttribute imageWidthAttribute =
+                mAttributes[imageType].get(TAG_IMAGE_WIDTH);
+
+        if (imageLengthAttribute == null || imageWidthAttribute == null) {
+            // Find if offset for JPEG data exists
+            ExifAttribute jpegInterchangeFormatAttribute =
+                    mAttributes[imageType].get(TAG_JPEG_INTERCHANGE_FORMAT);
+            ExifAttribute jpegInterchangeFormatLengthAttribute =
+                    mAttributes[imageType].get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+            if (jpegInterchangeFormatAttribute != null
+                    && jpegInterchangeFormatLengthAttribute != null) {
+                int jpegInterchangeFormat =
+                        jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
+                int jpegInterchangeFormatLength =
+                        jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
+
+                // Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags
+                in.seek(jpegInterchangeFormat);
+                byte[] jpegBytes = new byte[jpegInterchangeFormatLength];
+                in.readFully(jpegBytes);
+                getJpegAttributes(new ByteOrderedDataInputStream(jpegBytes), jpegInterchangeFormat,
+                        imageType);
+            }
+        }
+    }
+
+    // Sets thumbnail offset & length attributes based on JpegInterchangeFormat or StripOffsets tags
+    private void setThumbnailData(ByteOrderedDataInputStream in) throws IOException {
+        HashMap<String, ExifAttribute> thumbnailData = mAttributes[IFD_TYPE_THUMBNAIL];
+
+        ExifAttribute compressionAttribute =
+                thumbnailData.get(TAG_COMPRESSION);
+        if (compressionAttribute != null) {
+            mThumbnailCompression = compressionAttribute.getIntValue(mExifByteOrder);
+            switch (mThumbnailCompression) {
+                case DATA_JPEG: {
+                    handleThumbnailFromJfif(in, thumbnailData);
+                    break;
+                }
+                case DATA_UNCOMPRESSED:
+                case DATA_JPEG_COMPRESSED: {
+                    if (isSupportedDataType(thumbnailData)) {
+                        handleThumbnailFromStrips(in, thumbnailData);
+                    }
+                    break;
+                }
+            }
+        } else {
+            // Thumbnail data may not contain Compression tag value
+            mThumbnailCompression = DATA_JPEG;
+            handleThumbnailFromJfif(in, thumbnailData);
+        }
+    }
+
+    // Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values
+    // and reads the corresponding bytes if stream does not support seek function
+    private void handleThumbnailFromJfif(ByteOrderedDataInputStream in,
+            HashMap<String, ExifAttribute> thumbnailData) throws IOException {
+        ExifAttribute jpegInterchangeFormatAttribute =
+                thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT);
+        ExifAttribute jpegInterchangeFormatLengthAttribute =
+                thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+        if (jpegInterchangeFormatAttribute != null
+                && jpegInterchangeFormatLengthAttribute != null) {
+            int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
+            int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);
+
+            if (mMimeType == IMAGE_TYPE_ORF) {
+                // Update offset value since RAF files have IFD data preceding MakerNote data.
+                thumbnailOffset += mOrfMakerNoteOffset;
+            }
+
+            if (thumbnailOffset > 0 && thumbnailLength > 0) {
+                mHasThumbnail = true;
+                if (mFilename == null && mAssetInputStream == null
+                        && mSeekableFileDescriptor == null) {
+                    // TODO: Need to handle potential OutOfMemoryError
+                    // Save the thumbnail in memory if the input doesn't support reading again.
+                    byte[] thumbnailBytes = new byte[thumbnailLength];
+                    in.skipFully(thumbnailOffset);
+                    in.readFully(thumbnailBytes);
+                    mThumbnailBytes = thumbnailBytes;
+                }
+                mThumbnailOffset = thumbnailOffset;
+                mThumbnailLength = thumbnailLength;
+            }
+            if (DEBUG) {
+                logger.debug("Setting thumbnail attributes with offset: " + thumbnailOffset
+                        + ", length: " + thumbnailLength);
+            }
+        }
+    }
+
+    // Check StripOffsets & StripByteCounts tags to retrieve thumbnail offset & length values
+    private void handleThumbnailFromStrips(ByteOrderedDataInputStream in,
+            HashMap<String, ExifAttribute> thumbnailData) throws IOException {
+        ExifAttribute stripOffsetsAttribute =
+                thumbnailData.get(TAG_STRIP_OFFSETS);
+        ExifAttribute stripByteCountsAttribute =
+                thumbnailData.get(TAG_STRIP_BYTE_COUNTS);
+
+        if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
+            long[] stripOffsets =
+                    convertToLongArray(stripOffsetsAttribute.getValue(mExifByteOrder));
+            long[] stripByteCounts =
+                    convertToLongArray(stripByteCountsAttribute.getValue(mExifByteOrder));
+
+            if (stripOffsets == null || stripOffsets.length == 0) {
+                logger.warn("stripOffsets should not be null or have zero length.");
+                return;
+            }
+            if (stripByteCounts == null || stripByteCounts.length == 0) {
+                logger.warn("stripByteCounts should not be null or have zero length.");
+                return;
+            }
+            if (stripOffsets.length != stripByteCounts.length) {
+                logger.warn("stripOffsets and stripByteCounts should have same length.");
+                return;
+            }
+
+            long totalStripByteCount = 0;
+            for (long byteCount : stripByteCounts) {
+                totalStripByteCount += byteCount;
+            }
+
+            // TODO: Need to handle potential OutOfMemoryError
+            // Set thumbnail byte array data for non-consecutive strip bytes
+            byte[] totalStripBytes = new byte[(int) totalStripByteCount];
+
+            int bytesRead = 0;
+            int bytesAdded = 0;
+            mHasThumbnail = mHasThumbnailStrips = mAreThumbnailStripsConsecutive = true;
+            for (int i = 0; i < stripOffsets.length; i++) {
+                int stripOffset = (int) stripOffsets[i];
+                int stripByteCount = (int) stripByteCounts[i];
+
+                // Check if strips are consecutive
+                // TODO: Add test for non-consecutive thumbnail image
+                if (i < stripOffsets.length - 1
+                        && stripOffset + stripByteCount != stripOffsets[i + 1]) {
+                    mAreThumbnailStripsConsecutive = false;
+                }
+
+                // Skip to offset
+                int bytesToSkip = stripOffset - bytesRead;
+                if (bytesToSkip < 0) {
+                    logger.debug("Invalid strip offset value");
+                    return;
+                }
+                try {
+                    in.skipFully(bytesToSkip);
+                } catch (EOFException e) {
+                    logger.debug("Failed to skip " + bytesToSkip + " bytes.");
+                    return;
+                }
+                bytesRead += bytesToSkip;
+                // TODO: Need to handle potential OutOfMemoryError
+                byte[] stripBytes = new byte[stripByteCount];
+                try {
+                    in.readFully(stripBytes);
+                } catch (EOFException e) {
+                    logger.debug("Failed to read " + stripByteCount + " bytes.");
+                    return;
+                }
+                bytesRead += stripByteCount;
+
+                // Add bytes to array
+                System.arraycopy(stripBytes, 0, totalStripBytes, bytesAdded,
+                        stripBytes.length);
+                bytesAdded += stripBytes.length;
+            }
+            mThumbnailBytes = totalStripBytes;
+
+            if (mAreThumbnailStripsConsecutive) {
+                mThumbnailOffset = (int) stripOffsets[0];
+                mThumbnailLength = totalStripBytes.length;
+            }
+        }
+    }
+
+    // Check if thumbnail data type is currently supported or not
+    private boolean isSupportedDataType(HashMap<String, ExifAttribute> thumbnailData) {
+        ExifAttribute bitsPerSampleAttribute =
+                thumbnailData.get(TAG_BITS_PER_SAMPLE);
+        if (bitsPerSampleAttribute != null) {
+            int[] bitsPerSampleValue = (int[]) bitsPerSampleAttribute.getValue(mExifByteOrder);
+
+            if (Arrays.equals(BITS_PER_SAMPLE_RGB, bitsPerSampleValue)) {
+                return true;
+            }
+
+            // See DNG Specification 1.4.0.0. Section 3, Compression.
+            if (mMimeType == IMAGE_TYPE_DNG) {
+                ExifAttribute photometricInterpretationAttribute =
+                        thumbnailData.get(TAG_PHOTOMETRIC_INTERPRETATION);
+                if (photometricInterpretationAttribute != null) {
+                    int photometricInterpretationValue
+                            = photometricInterpretationAttribute.getIntValue(mExifByteOrder);
+                    if ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO
+                            && Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_GREYSCALE_2))
+                            || ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_YCBCR)
+                            && Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_RGB))) {
+                        return true;
+                    } else {
+                        // TODO: Add support for lossless Huffman JPEG data
+                    }
+                }
+            }
+        }
+        if (DEBUG) {
+            logger.debug("Unsupported data type value");
+        }
+        return false;
+    }
+
+    // Returns true if the image length and width values are <= 512.
+    // See Section 4.8 of http://standardsproposals.bsigroup.com/Home/getPDF/567
+    private boolean isThumbnail(HashMap<String, ExifAttribute> map) {
+        ExifAttribute imageLengthAttribute = map.get(TAG_IMAGE_LENGTH);
+        ExifAttribute imageWidthAttribute = map.get(TAG_IMAGE_WIDTH);
+
+        if (imageLengthAttribute != null && imageWidthAttribute != null) {
+            int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder);
+            int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder);
+            if (imageLengthValue <= MAX_THUMBNAIL_SIZE && imageWidthValue <= MAX_THUMBNAIL_SIZE) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // Validate primary, preview, thumbnail image data by comparing image size
+    private void validateImages() throws IOException {
+        // Swap images based on size (primary > preview > thumbnail)
+        swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_PREVIEW);
+        swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_THUMBNAIL);
+        swapBasedOnImageSize(IFD_TYPE_PREVIEW, IFD_TYPE_THUMBNAIL);
+
+        // TODO (b/142296453): Revise image width/height setting logic
+        // Check if image has PixelXDimension/PixelYDimension tags, which contain valid image
+        // sizes, excluding padding at the right end or bottom end of the image to make sure that
+        // the values are multiples of 64. See JEITA CP-3451C Table 5 and Section 4.8.1. B.
+        ExifAttribute pixelXDimAttribute =
+                mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_X_DIMENSION);
+        ExifAttribute pixelYDimAttribute =
+                mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_Y_DIMENSION);
+        if (pixelXDimAttribute != null && pixelYDimAttribute != null) {
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, pixelXDimAttribute);
+            mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, pixelYDimAttribute);
+        }
+
+        // Check whether thumbnail image exists and whether preview image satisfies the thumbnail
+        // image requirements
+        if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
+            if (isThumbnail(mAttributes[IFD_TYPE_PREVIEW])) {
+                mAttributes[IFD_TYPE_THUMBNAIL] = mAttributes[IFD_TYPE_PREVIEW];
+                mAttributes[IFD_TYPE_PREVIEW] = new HashMap<>();
+            }
+        }
+
+        // Check if the thumbnail image satisfies the thumbnail size requirements
+        if (!isThumbnail(mAttributes[IFD_TYPE_THUMBNAIL])) {
+            logger.debug("No image meets the size requirements of a thumbnail image.");
+        }
+
+        // TAG_THUMBNAIL_* tags should be replaced with TAG_* equivalents and vice versa if needed.
+        replaceInvalidTags(IFD_TYPE_PRIMARY, TAG_THUMBNAIL_ORIENTATION, TAG_ORIENTATION);
+        replaceInvalidTags(IFD_TYPE_PRIMARY, TAG_THUMBNAIL_IMAGE_LENGTH, TAG_IMAGE_LENGTH);
+        replaceInvalidTags(IFD_TYPE_PRIMARY, TAG_THUMBNAIL_IMAGE_WIDTH, TAG_IMAGE_WIDTH);
+        replaceInvalidTags(IFD_TYPE_PREVIEW, TAG_THUMBNAIL_ORIENTATION, TAG_ORIENTATION);
+        replaceInvalidTags(IFD_TYPE_PREVIEW, TAG_THUMBNAIL_IMAGE_LENGTH, TAG_IMAGE_LENGTH);
+        replaceInvalidTags(IFD_TYPE_PREVIEW, TAG_THUMBNAIL_IMAGE_WIDTH, TAG_IMAGE_WIDTH);
+        replaceInvalidTags(IFD_TYPE_THUMBNAIL, TAG_ORIENTATION, TAG_THUMBNAIL_ORIENTATION);
+        replaceInvalidTags(IFD_TYPE_THUMBNAIL, TAG_IMAGE_LENGTH, TAG_THUMBNAIL_IMAGE_LENGTH);
+        replaceInvalidTags(IFD_TYPE_THUMBNAIL, TAG_IMAGE_WIDTH, TAG_THUMBNAIL_IMAGE_WIDTH);
+    }
+
+    /**
+     * If image is uncompressed, ImageWidth/Length tags are used to store size info.
+     * However, uncompressed images often store extra pixels around the edges of the final image,
+     * which results in larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags.
+     * This method corrects those tag values by checking first the values of TAG_DEFAULT_CROP_SIZE
+     * See DNG Specification 1.4.0.0. Section 4. (DefaultCropSize)
+     *
+     * If image is a RW2 file, valid image sizes are stored in SensorBorder tags.
+     * See tiff_parser.cc GetFullDimension32()
+     * */
+    private void updateImageSizeValues(SeekableByteOrderedDataInputStream in, int imageType)
+            throws IOException {
+        // Uncompressed image valid image size values
+        ExifAttribute defaultCropSizeAttribute =
+                mAttributes[imageType].get(TAG_DEFAULT_CROP_SIZE);
+        // RW2 image valid image size values
+        ExifAttribute topBorderAttribute =
+                mAttributes[imageType].get(TAG_RW2_SENSOR_TOP_BORDER);
+        ExifAttribute leftBorderAttribute =
+                mAttributes[imageType].get(TAG_RW2_SENSOR_LEFT_BORDER);
+        ExifAttribute bottomBorderAttribute =
+                mAttributes[imageType].get(TAG_RW2_SENSOR_BOTTOM_BORDER);
+        ExifAttribute rightBorderAttribute =
+                mAttributes[imageType].get(TAG_RW2_SENSOR_RIGHT_BORDER);
+
+        if (defaultCropSizeAttribute != null) {
+            // Update for uncompressed image
+            ExifAttribute defaultCropSizeXAttribute, defaultCropSizeYAttribute;
+            if (defaultCropSizeAttribute.format == IFD_FORMAT_URATIONAL) {
+                Rational[] defaultCropSizeValue =
+                        (Rational[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
+                if (defaultCropSizeValue == null || defaultCropSizeValue.length != 2) {
+                    logger.warn("Invalid crop size values. cropSize="
+                            + Arrays.toString(defaultCropSizeValue));
+                    return;
+                }
+                defaultCropSizeXAttribute =
+                        ExifAttribute.createURational(defaultCropSizeValue[0], mExifByteOrder);
+                defaultCropSizeYAttribute =
+                        ExifAttribute.createURational(defaultCropSizeValue[1], mExifByteOrder);
+            } else {
+                int[] defaultCropSizeValue =
+                        (int[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
+                if (defaultCropSizeValue == null || defaultCropSizeValue.length != 2) {
+                    logger.warn("Invalid crop size values. cropSize="
+                            + Arrays.toString(defaultCropSizeValue));
+                    return;
+                }
+                defaultCropSizeXAttribute =
+                        ExifAttribute.createUShort(defaultCropSizeValue[0], mExifByteOrder);
+                defaultCropSizeYAttribute =
+                        ExifAttribute.createUShort(defaultCropSizeValue[1], mExifByteOrder);
+            }
+            mAttributes[imageType].put(TAG_IMAGE_WIDTH, defaultCropSizeXAttribute);
+            mAttributes[imageType].put(TAG_IMAGE_LENGTH, defaultCropSizeYAttribute);
+        } else if (topBorderAttribute != null && leftBorderAttribute != null &&
+                bottomBorderAttribute != null && rightBorderAttribute != null) {
+            // Update for RW2 image
+            int topBorderValue = topBorderAttribute.getIntValue(mExifByteOrder);
+            int bottomBorderValue = bottomBorderAttribute.getIntValue(mExifByteOrder);
+            int rightBorderValue = rightBorderAttribute.getIntValue(mExifByteOrder);
+            int leftBorderValue = leftBorderAttribute.getIntValue(mExifByteOrder);
+            if (bottomBorderValue > topBorderValue && rightBorderValue > leftBorderValue) {
+                int length = bottomBorderValue - topBorderValue;
+                int width = rightBorderValue - leftBorderValue;
+                ExifAttribute imageLengthAttribute =
+                        ExifAttribute.createUShort(length, mExifByteOrder);
+                ExifAttribute imageWidthAttribute =
+                        ExifAttribute.createUShort(width, mExifByteOrder);
+                mAttributes[imageType].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
+                mAttributes[imageType].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
+            }
+        } else {
+            retrieveJpegImageSize(in, imageType);
+        }
+    }
+
+    // Writes an Exif segment into the given output stream.
+    private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) throws IOException {
+        // The following variables are for calculating each IFD tag group size in bytes.
+        int[] ifdOffsets = new int[EXIF_TAGS.length];
+        int[] ifdDataSizes = new int[EXIF_TAGS.length];
+
+        // Remove IFD pointer tags (we'll re-add it later.)
+        for (ExifTag tag : EXIF_POINTER_TAGS) {
+            removeAttribute(tag.name);
+        }
+        // Remove old thumbnail data
+        if (mHasThumbnail) {
+            if (mHasThumbnailStrips) {
+                removeAttribute(TAG_STRIP_OFFSETS);
+                removeAttribute(TAG_STRIP_BYTE_COUNTS);
+            } else {
+                removeAttribute(TAG_JPEG_INTERCHANGE_FORMAT);
+                removeAttribute(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+            }
+        }
+
+        // Remove null value tags.
+        for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+            Iterator<Map.Entry<String, ExifAttribute>> entrySetIterator =
+                    mAttributes[ifdType].entrySet().iterator();
+            while (entrySetIterator.hasNext()) {
+                Map.Entry<String, ExifAttribute> entry = entrySetIterator.next();
+                if (entry.getValue() == null) {
+                    entrySetIterator.remove();
+                }
+            }
+        }
+
+        // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
+        // offset when there is one or more tags in the thumbnail IFD.
+        if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
+            mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+        if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
+            mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+        if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
+            mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name,
+                    ExifAttribute.createULong(0, mExifByteOrder));
+        }
+        if (mHasThumbnail) {
+            if (mHasThumbnailStrips) {
+                mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_STRIP_OFFSETS,
+                        ExifAttribute.createUShort(0, mExifByteOrder));
+                mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_STRIP_BYTE_COUNTS,
+                        ExifAttribute.createUShort(mThumbnailLength, mExifByteOrder));
+            } else {
+                mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
+                        ExifAttribute.createULong(0, mExifByteOrder));
+                mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+                        ExifAttribute.createULong(mThumbnailLength, mExifByteOrder));
+            }
+        }
+
+        // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
+        // value which has a bigger size than 4 bytes.
+        for (int i = 0; i < EXIF_TAGS.length; ++i) {
+            int sum = 0;
+            for (Map.Entry<String, ExifAttribute> entry : mAttributes[i].entrySet()) {
+                final ExifAttribute exifAttribute = entry.getValue();
+                final int size = exifAttribute.size();
+                if (size > 4) {
+                    sum += size;
+                }
+            }
+            ifdDataSizes[i] += sum;
+        }
+
+        // Calculate IFD offsets.
+        // 8 bytes are for TIFF headers: 2 bytes (byte order) + 2 bytes (identifier) + 4 bytes
+        // (offset of IFDs)
+        int position = 8;
+        for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+            if (!mAttributes[ifdType].isEmpty()) {
+                ifdOffsets[ifdType] = position;
+                position += 2 + mAttributes[ifdType].size() * 12 + 4 + ifdDataSizes[ifdType];
+            }
+        }
+        if (mHasThumbnail) {
+            int thumbnailOffset = position;
+            if (mHasThumbnailStrips) {
+                mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_STRIP_OFFSETS,
+                        ExifAttribute.createUShort(thumbnailOffset, mExifByteOrder));
+            } else {
+                mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
+                        ExifAttribute.createULong(thumbnailOffset, mExifByteOrder));
+            }
+            mThumbnailOffset = thumbnailOffset;
+            position += mThumbnailLength;
+        }
+
+        int totalSize = position;
+        if (mMimeType == IMAGE_TYPE_JPEG) {
+            // Add 8 bytes for APP1 size and identifier data
+            totalSize += 8;
+        }
+        if (DEBUG) {
+            for (int i = 0; i < EXIF_TAGS.length; ++i) {
+                logger.debug(String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d, "
+                                + "total size: %d", i, ifdOffsets[i], mAttributes[i].size(),
+                        ifdDataSizes[i], totalSize));
+            }
+        }
+
+        // Update IFD pointer tags with the calculated offsets.
+        if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
+            mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
+                    ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifByteOrder));
+        }
+        if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
+            mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
+                    ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifByteOrder));
+        }
+        if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
+            mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name, ExifAttribute.createULong(
+                    ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifByteOrder));
+        }
+
+        switch (mMimeType) {
+            case IMAGE_TYPE_JPEG:
+                if (totalSize > 0xFFFF) {
+                    throw new IllegalStateException(
+                            "Size of exif data (" + totalSize + " bytes) exceeds the max size of a "
+                            + "JPEG APP1 segment (65536 bytes)");
+                }
+                // Write JPEG specific data (APP1 size, APP1 identifier)
+                dataOutputStream.writeUnsignedShort(totalSize);
+                dataOutputStream.write(IDENTIFIER_EXIF_APP1);
+                break;
+            case IMAGE_TYPE_PNG:
+                // Write PNG specific data (chunk size, chunk type)
+                dataOutputStream.writeInt(totalSize);
+                dataOutputStream.write(PNG_CHUNK_TYPE_EXIF);
+                break;
+            case IMAGE_TYPE_WEBP:
+                // Write WebP specific data (chunk type, chunk size)
+                dataOutputStream.write(WEBP_CHUNK_TYPE_EXIF);
+                dataOutputStream.writeInt(totalSize);
+                break;
+        }
+
+        // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
+        dataOutputStream.writeShort(mExifByteOrder == BIG_ENDIAN ? BYTE_ALIGN_MM : BYTE_ALIGN_II);
+        dataOutputStream.setByteOrder(mExifByteOrder);
+        dataOutputStream.writeUnsignedShort(START_CODE);
+        dataOutputStream.writeUnsignedInt(IFD_OFFSET);
+
+        // Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9.
+        for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+            if (!mAttributes[ifdType].isEmpty()) {
+                // See JEITA CP-3451C Section 4.6.2: IFD structure.
+                // Write entry count
+                dataOutputStream.writeUnsignedShort(mAttributes[ifdType].size());
+
+                // Write entry info
+                int dataOffset = ifdOffsets[ifdType] + 2 + mAttributes[ifdType].size() * 12 + 4;
+                for (Map.Entry<String, ExifAttribute> entry : mAttributes[ifdType].entrySet()) {
+                    // Convert tag name to tag number.
+                    final ExifTag tag = sExifTagMapsForWriting[ifdType].get(entry.getKey());
+                    final int tagNumber = tag.number;
+                    final ExifAttribute attribute = entry.getValue();
+                    final int size = attribute.size();
+
+                    dataOutputStream.writeUnsignedShort(tagNumber);
+                    dataOutputStream.writeUnsignedShort(attribute.format);
+                    dataOutputStream.writeInt(attribute.numberOfComponents);
+                    if (size > 4) {
+                        dataOutputStream.writeUnsignedInt(dataOffset);
+                        dataOffset += size;
+                    } else {
+                        dataOutputStream.write(attribute.bytes);
+                        // Fill zero up to 4 bytes
+                        if (size < 4) {
+                            for (int i = size; i < 4; ++i) {
+                                dataOutputStream.writeByte(0);
+                            }
+                        }
+                    }
+                }
+
+                // Write the next offset. It writes the offset of thumbnail IFD if there is one or
+                // more tags in the thumbnail IFD when the current IFD is the primary image TIFF
+                // IFD; Otherwise 0.
+                if (ifdType == 0 && !mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
+                    dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_TYPE_THUMBNAIL]);
+                } else {
+                    dataOutputStream.writeUnsignedInt(0);
+                }
+
+                // Write values of data field exceeding 4 bytes after the next offset.
+                for (Map.Entry<String, ExifAttribute> entry : mAttributes[ifdType].entrySet()) {
+                    ExifAttribute attribute = entry.getValue();
+
+                    if (attribute.bytes.length > 4) {
+                        dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length);
+                    }
+                }
+            }
+        }
+
+        // Write thumbnail
+        if (mHasThumbnail) {
+            dataOutputStream.write(getThumbnailBytes());
+        }
+
+        // For WebP files, add a single padding byte at end if chunk size is odd
+        if (mMimeType == IMAGE_TYPE_WEBP && totalSize % 2 == 1) {
+            dataOutputStream.writeByte(0);
+        }
+
+        // Reset the byte order to big endian in order to write remaining parts of the JPEG file.
+        dataOutputStream.setByteOrder(BIG_ENDIAN);
+
+        return totalSize;
+    }
+
+    /**
+     * Determines the data format of EXIF entry value.
+     *
+     * @param entryValue The value to be determined.
+     * @return Returns two data formats guessed as a pair in integer. If there is no two candidate
+               data formats for the given entry value, returns {@code -1} in the second of the pair.
+     */
+    private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
+        // See TIFF 6.0 Section 2, "Image File Directory".
+        // Take the first component if there are more than one component.
+        if (entryValue.contains(",")) {
+            String[] entryValues = entryValue.split(",", -1);
+            Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
+            if (dataFormat.first == IFD_FORMAT_STRING) {
+                return dataFormat;
+            }
+            for (int i = 1; i < entryValues.length; ++i) {
+                final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
+                int first = -1, second = -1;
+                if (guessDataFormat.first.equals(dataFormat.first)
+                        || guessDataFormat.second.equals(dataFormat.first)) {
+                    first = dataFormat.first;
+                }
+                if (dataFormat.second != -1 && (guessDataFormat.first.equals(dataFormat.second)
+                        || guessDataFormat.second.equals(dataFormat.second))) {
+                    second = dataFormat.second;
+                }
+                if (first == -1 && second == -1) {
+                    return new Pair<>(IFD_FORMAT_STRING, -1);
+                }
+                if (first == -1) {
+                    dataFormat = new Pair<>(second, -1);
+                    continue;
+                }
+                if (second == -1) {
+                    dataFormat = new Pair<>(first, -1);
+                    continue;
+                }
+            }
+            return dataFormat;
+        }
+
+        if (entryValue.contains("/")) {
+            String[] rationalNumber = entryValue.split("/", -1);
+            if (rationalNumber.length == 2) {
+                try {
+                    long numerator = (long) Double.parseDouble(rationalNumber[0]);
+                    long denominator = (long) Double.parseDouble(rationalNumber[1]);
+                    if (numerator < 0L || denominator < 0L) {
+                        return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
+                    }
+                    if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
+                        return new Pair<>(IFD_FORMAT_URATIONAL, -1);
+                    }
+                    return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
+                } catch (NumberFormatException e)  {
+                    // Ignored
+                }
+            }
+            return new Pair<>(IFD_FORMAT_STRING, -1);
+        }
+        try {
+            Long longValue = Long.parseLong(entryValue);
+            if (longValue >= 0 && longValue <= 65535) {
+                return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
+            }
+            if (longValue < 0) {
+                return new Pair<>(IFD_FORMAT_SLONG, -1);
+            }
+            return new Pair<>(IFD_FORMAT_ULONG, -1);
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        try {
+            Double.parseDouble(entryValue);
+            return new Pair<>(IFD_FORMAT_DOUBLE, -1);
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        return new Pair<>(IFD_FORMAT_STRING, -1);
+    }
+
+    // An input stream class that can parse both little and big endian order data and also
+    // supports seeking to any position in the stream via mark/reset.
+    private static class SeekableByteOrderedDataInputStream extends ByteOrderedDataInputStream {
+        SeekableByteOrderedDataInputStream(byte[] bytes) throws IOException {
+            super(bytes);
+            // No need to check if mark is supported here since ByteOrderedDataInputStream will
+            // create a ByteArrayInputStream, which supports mark by default.
+            mDataInputStream.mark(Integer.MAX_VALUE);
+        }
+
+        /**
+         * Given input stream should support mark/reset, and should be set to the beginning of
+         * the stream.
+         */
+        SeekableByteOrderedDataInputStream(InputStream in) throws IOException {
+            super(in);
+            if (!in.markSupported()) {
+                throw new IllegalArgumentException("Cannot create "
+                        + "SeekableByteOrderedDataInputStream with stream that does not support "
+                        + "mark/reset");
+            }
+            // Mark given InputStream to the maximum value (we can't know the length of the
+            // stream for certain) so that InputStream.reset() may be called at any point in the
+            // stream to reset the stream to an earlier position.
+            mDataInputStream.mark(Integer.MAX_VALUE);
+        }
+
+        /**
+         * Seek to the given absolute position in the stream (i.e. the number of bytes from the
+         * beginning of the stream).
+         */
+        public void seek(long position) throws IOException {
+            if (mPosition > position) {
+                mPosition = 0;
+                mDataInputStream.reset();
+            } else {
+                position -= mPosition;
+            }
+            skipFully((int) position);
+        }
+    }
+
+    // An input stream class that can parse both little and big endian order data.
+    private static class ByteOrderedDataInputStream extends InputStream implements DataInput {
+
+        public static final int LENGTH_UNSET = -1;
+        protected final DataInputStream mDataInputStream;
+        protected int mPosition;
+
+        private ByteOrder mByteOrder;
+        private byte[] mSkipBuffer;
+        private int mLength;
+
+        ByteOrderedDataInputStream(byte[] bytes) throws IOException {
+            this(new ByteArrayInputStream(bytes), BIG_ENDIAN);
+            this.mLength = bytes.length;
+        }
+
+        ByteOrderedDataInputStream(InputStream in) throws IOException {
+            this(in, BIG_ENDIAN);
+        }
+
+        ByteOrderedDataInputStream(InputStream in, ByteOrder byteOrder) {
+            mDataInputStream = new DataInputStream(in);
+            mDataInputStream.mark(0);
+            mPosition = 0;
+            mByteOrder = byteOrder;
+            this.mLength = in instanceof ByteOrderedDataInputStream
+                    ? ((ByteOrderedDataInputStream) in).length()
+                    : LENGTH_UNSET;
+        }
+
+        public void setByteOrder(ByteOrder byteOrder) {
+            mByteOrder = byteOrder;
+        }
+
+        public int position() {
+            return mPosition;
+        }
+
+        /** Reads all remaining data. */
+        public byte[] readToEnd() throws IOException {
+            byte[] data = new byte[1024];
+            int bytesRead = 0;
+            while (true) {
+                if (bytesRead == data.length) {
+                    data = Arrays.copyOf(data, data.length * 2);
+                }
+                int readResult = mDataInputStream.read(data, bytesRead, data.length - bytesRead);
+                if (readResult != -1) {
+                    bytesRead += readResult;
+                    mPosition += readResult;
+                } else {
+                    break;
+                }
+            }
+            return Arrays.copyOf(data, bytesRead);
+        }
+
+        @Override
+        public int available() throws IOException {
+            return mDataInputStream.available();
+        }
+
+        @Override
+        public int read() throws IOException {
+            ++mPosition;
+            return mDataInputStream.read();
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            int bytesRead = mDataInputStream.read(b, off, len);
+            mPosition += bytesRead;
+            return bytesRead;
+        }
+
+        @Override
+        public int readUnsignedByte() throws IOException {
+            ++mPosition;
+            return mDataInputStream.readUnsignedByte();
+        }
+
+        @Override
+        public String readLine() throws IOException {
+            logger.debug("Currently unsupported");
+            return null;
+        }
+
+        @Override
+        public boolean readBoolean() throws IOException {
+            ++mPosition;
+            return mDataInputStream.readBoolean();
+        }
+
+        @Override
+        public char readChar() throws IOException {
+            mPosition += 2;
+            return mDataInputStream.readChar();
+        }
+
+        @Override
+        public String readUTF() throws IOException {
+            mPosition += 2;
+            return mDataInputStream.readUTF();
+        }
+
+        @Override
+        public void readFully(byte[] buffer, int offset, int length) throws IOException {
+            mPosition += length;
+            mDataInputStream.readFully(buffer, offset, length);
+        }
+
+        @Override
+        public void readFully(byte[] buffer) throws IOException {
+            mPosition += buffer.length;
+            mDataInputStream.readFully(buffer);
+        }
+
+        @Override
+        public byte readByte() throws IOException {
+            ++mPosition;
+            int ch = mDataInputStream.read();
+            if (ch < 0) {
+                throw new EOFException();
+            }
+            return (byte) ch;
+        }
+
+        @Override
+        public short readShort() throws IOException {
+            mPosition += 2;
+            int ch1 = mDataInputStream.read();
+            int ch2 = mDataInputStream.read();
+            if ((ch1 | ch2) < 0) {
+                throw new EOFException();
+            }
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return (short) ((ch2 << 8) + ch1);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return (short) ((ch1 << 8) + ch2);
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        @Override
+        public int readInt() throws IOException {
+            mPosition += 4;
+            int ch1 = mDataInputStream.read();
+            int ch2 = mDataInputStream.read();
+            int ch3 = mDataInputStream.read();
+            int ch4 = mDataInputStream.read();
+            if ((ch1 | ch2 | ch3 | ch4) < 0) {
+                throw new EOFException();
+            }
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        @Override
+        public int skipBytes(int n) throws IOException {
+            throw new UnsupportedOperationException("skipBytes is currently unsupported");
+        }
+
+        /**
+         * Discards n bytes of data from the input stream. This method will block until either
+         * the full amount has been skipped or the end of the stream is reached, whichever happens
+         * first.
+         */
+        public void skipFully(int n) throws IOException {
+            int totalSkipped = 0;
+            while (totalSkipped < n) {
+                int skipped = (int) mDataInputStream.skip(n - totalSkipped);
+                if (skipped <= 0) {
+                    if (mSkipBuffer == null) {
+                        mSkipBuffer = new byte[SKIP_BUFFER_SIZE];
+                    }
+                    int bytesToSkip = Math.min(SKIP_BUFFER_SIZE, n - totalSkipped);
+                    if ((skipped = mDataInputStream.read(mSkipBuffer, 0, bytesToSkip)) == -1) {
+                        throw new EOFException("Reached EOF while skipping " + n + " bytes.");
+                    }
+                }
+                totalSkipped += skipped;
+            }
+            mPosition += totalSkipped;
+        }
+
+        @Override
+        public int readUnsignedShort() throws IOException {
+            mPosition += 2;
+            int ch1 = mDataInputStream.read();
+            int ch2 = mDataInputStream.read();
+            if ((ch1 | ch2) < 0) {
+                throw new EOFException();
+            }
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return ((ch2 << 8) + ch1);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return ((ch1 << 8) + ch2);
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        public long readUnsignedInt() throws IOException {
+            return readInt() & 0xffffffffL;
+        }
+
+        @Override
+        public long readLong() throws IOException {
+            mPosition += 8;
+            int ch1 = mDataInputStream.read();
+            int ch2 = mDataInputStream.read();
+            int ch3 = mDataInputStream.read();
+            int ch4 = mDataInputStream.read();
+            int ch5 = mDataInputStream.read();
+            int ch6 = mDataInputStream.read();
+            int ch7 = mDataInputStream.read();
+            int ch8 = mDataInputStream.read();
+            if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
+                throw new EOFException();
+            }
+            if (mByteOrder == LITTLE_ENDIAN) {
+                return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
+                        + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
+                        + ((long) ch2 << 8) + (long) ch1);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
+                        + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
+                        + ((long) ch7 << 8) + (long) ch8);
+            }
+            throw new IOException("Invalid byte order: " + mByteOrder);
+        }
+
+        @Override
+        public float readFloat() throws IOException {
+            return Float.intBitsToFloat(readInt());
+        }
+
+        @Override
+        public double readDouble() throws IOException {
+            return Double.longBitsToDouble(readLong());
+        }
+
+        @Override
+        public void mark(int readlimit) {
+            throw new UnsupportedOperationException("Mark is currently unsupported");
+        }
+
+        @Override
+        public void reset() {
+            throw new UnsupportedOperationException("Reset is currently unsupported");
+        }
+
+        /** Return the total length (in bytes) of the underlying stream if known, otherwise
+         *  {@link #LENGTH_UNSET}. */
+        public int length() {
+            return mLength;
+        }
+    }
+
+    // An output stream to write EXIF data area, which can be written in either little or big endian
+    // order.
+    private static class ByteOrderedDataOutputStream extends FilterOutputStream {
+        final OutputStream mOutputStream;
+        private ByteOrder mByteOrder;
+
+        public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
+            super(out);
+            mOutputStream = out;
+            mByteOrder = byteOrder;
+        }
+
+        public void setByteOrder(ByteOrder byteOrder) {
+            mByteOrder = byteOrder;
+        }
+
+        @Override
+        public void write(byte[] bytes) throws IOException {
+            mOutputStream.write(bytes);
+        }
+
+        @Override
+        public void write(byte[] bytes, int offset, int length) throws IOException {
+            mOutputStream.write(bytes, offset, length);
+        }
+
+        public void writeByte(int val) throws IOException {
+            mOutputStream.write(val);
+        }
+
+        public void writeShort(short val) throws IOException {
+            if (mByteOrder == LITTLE_ENDIAN) {
+                mOutputStream.write((val >>> 0) & 0xFF);
+                mOutputStream.write((val >>> 8) & 0xFF);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                mOutputStream.write((val >>> 8) & 0xFF);
+                mOutputStream.write((val >>> 0) & 0xFF);
+            }
+        }
+
+        public void writeInt(int val) throws IOException {
+            if (mByteOrder == LITTLE_ENDIAN) {
+                mOutputStream.write((val >>> 0) & 0xFF);
+                mOutputStream.write((val >>> 8) & 0xFF);
+                mOutputStream.write((val >>> 16) & 0xFF);
+                mOutputStream.write((val >>> 24) & 0xFF);
+            } else if (mByteOrder == BIG_ENDIAN) {
+                mOutputStream.write((val >>> 24) & 0xFF);
+                mOutputStream.write((val >>> 16) & 0xFF);
+                mOutputStream.write((val >>> 8) & 0xFF);
+                mOutputStream.write((val >>> 0) & 0xFF);
+            }
+        }
+
+        public void writeUnsignedShort(int val) throws IOException {
+            if (val > 0xFFFF) {
+                throw new IllegalArgumentException("val is larger than the maximum value of a "
+                        + "16-bit unsigned integer");
+            }
+            writeShort((short) val);
+        }
+
+        public void writeUnsignedInt(long val) throws IOException {
+            if (val > 0xFFFF_FFFFL) {
+                throw new IllegalArgumentException("val is larger than the maximum value of a "
+                        + "32-bit unsigned integer");
+            }
+            writeInt((int) val);
+        }
+    }
+
+    // Swaps image data based on image size
+    private void swapBasedOnImageSize(@IfdType int firstIfdType, @IfdType int secondIfdType)
+            throws IOException {
+        if (mAttributes[firstIfdType].isEmpty() || mAttributes[secondIfdType].isEmpty()) {
+            if (DEBUG) {
+                logger.debug("Cannot perform swap since only one image data exists");
+            }
+            return;
+        }
+
+        ExifAttribute firstImageLengthAttribute =
+                mAttributes[firstIfdType].get(TAG_IMAGE_LENGTH);
+        ExifAttribute firstImageWidthAttribute =
+                mAttributes[firstIfdType].get(TAG_IMAGE_WIDTH);
+        ExifAttribute secondImageLengthAttribute =
+                mAttributes[secondIfdType].get(TAG_IMAGE_LENGTH);
+        ExifAttribute secondImageWidthAttribute =
+                mAttributes[secondIfdType].get(TAG_IMAGE_WIDTH);
+
+        if (firstImageLengthAttribute == null || firstImageWidthAttribute == null) {
+            if (DEBUG) {
+                logger.debug("First image does not contain valid size information");
+            }
+        } else if (secondImageLengthAttribute == null || secondImageWidthAttribute == null) {
+            if (DEBUG) {
+                logger.debug("Second image does not contain valid size information");
+            }
+        } else {
+            int firstImageLengthValue = firstImageLengthAttribute.getIntValue(mExifByteOrder);
+            int firstImageWidthValue = firstImageWidthAttribute.getIntValue(mExifByteOrder);
+            int secondImageLengthValue = secondImageLengthAttribute.getIntValue(mExifByteOrder);
+            int secondImageWidthValue = secondImageWidthAttribute.getIntValue(mExifByteOrder);
+
+            if (firstImageLengthValue < secondImageLengthValue &&
+                    firstImageWidthValue < secondImageWidthValue) {
+                HashMap<String, ExifAttribute> tempMap = mAttributes[firstIfdType];
+                mAttributes[firstIfdType] = mAttributes[secondIfdType];
+                mAttributes[secondIfdType] = tempMap;
+            }
+        }
+    }
+
+    private void replaceInvalidTags(@IfdType int ifdType, String invalidTag, String validTag) {
+        if (!mAttributes[ifdType].isEmpty()) {
+            if (mAttributes[ifdType].get(invalidTag) != null) {
+                mAttributes[ifdType].put(validTag,
+                        mAttributes[ifdType].get(invalidTag));
+                mAttributes[ifdType].remove(invalidTag);
+            }
+        }
+    }
+
+    /**
+     * Parsing EXIF data requires seek (moving to any position in the stream), so all MIME
+     * types should support seek via mark/reset, unless the MIME type specifies the position and
+     * length of the EXIF data and the EXIF data can be read from the file and wrapped with a
+     * ByteArrayInputStream.
+     */
+    private static boolean shouldSupportSeek(int mimeType) {
+        if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_RAF || mimeType == IMAGE_TYPE_PNG
+                || mimeType == IMAGE_TYPE_WEBP) {
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean isSupportedFormatForSavingAttributes(int mimeType) {
+        if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_PNG
+                || mimeType == IMAGE_TYPE_WEBP) {
+            return true;
+        }
+        return false;
+    }
 }

+ 204 - 0
app/src/main/java/ch/threema/app/utils/ExifInterfaceUtils.java

@@ -0,0 +1,204 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2019-2024 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Based on AOSP code with modifications by Threema
+ *
+ * (C) Google Inc. All Rights Reserved
+ * Licensed under the Apache License, version 2.0
+ */
+
+package ch.threema.app.utils;
+
+import android.media.MediaDataSource;
+import android.media.MediaMetadataRetriever;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.RequiresApi;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class ExifInterfaceUtils {
+    private static final String TAG = "ExifInterfaceUtils";
+
+    private ExifInterfaceUtils() {
+        // Prevent instantiation
+    }
+    /**
+     * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
+     * Returns the total number of bytes transferred.
+     */
+    static int copy(InputStream in, OutputStream out) throws IOException {
+        int total = 0;
+        byte[] buffer = new byte[8192];
+        int c;
+        while ((c = in.read(buffer)) != -1) {
+            total += c;
+            out.write(buffer, 0, c);
+        }
+        return total;
+    }
+
+    /**
+     * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
+     * closed.
+     */
+    static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
+        int remainder = numBytes;
+        byte[] buffer = new byte[8192];
+        while (remainder > 0) {
+            int bytesToRead = Math.min(remainder, 8192);
+            int bytesRead = in.read(buffer, 0, bytesToRead);
+            if (bytesRead != bytesToRead) {
+                throw new IOException("Failed to copy the given amount of bytes from the input"
+                        + "stream to the output stream.");
+            }
+            remainder -= bytesRead;
+            out.write(buffer, 0, bytesRead);
+        }
+    }
+
+    /**
+     * Convert given int[] to long[]. If long[] is given, just return it.
+     * Return null for other types of input.
+     */
+    static long[] convertToLongArray(Object inputObj) {
+        if (inputObj instanceof int[]) {
+            int[] input = (int[]) inputObj;
+            long[] result = new long[input.length];
+            for (int i = 0; i < input.length; i++) {
+                result[i] = input[i];
+            }
+            return result;
+        } else if (inputObj instanceof long[]) {
+            return (long[]) inputObj;
+        }
+        return null;
+    }
+
+    static boolean startsWith(byte[] cur, byte[] val) {
+        if (cur == null || val == null) {
+            return false;
+        }
+        if (cur.length < val.length) {
+            return false;
+        }
+        for (int i = 0; i < val.length; i++) {
+            if (cur[i] != val[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static String byteArrayToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder(bytes.length * 2);
+        for (int i = 0; i < bytes.length; i++) {
+            sb.append(String.format("%02x", bytes[i]));
+        }
+        return sb.toString();
+    }
+
+    static long parseSubSeconds(String subSec) {
+        try {
+            final int len = Math.min(subSec.length(), 3);
+            long sub = Long.parseLong(subSec.substring(0, len));
+            for (int i = len; i < 3; i++) {
+                sub *= 10;
+            }
+            return sub;
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        return 0L;
+    }
+
+
+    /**
+     * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
+     */
+    static void closeQuietly(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    /**
+     * Closes a file descriptor that has been duplicated.
+     */
+    static void closeFileDescriptor(FileDescriptor fd) {
+        // Os.dup and Os.close was introduced in API 21 so this method shouldn't be called
+        // in API < 21.
+        if (Build.VERSION.SDK_INT >= 21) {
+            try {
+                Api21Impl.close(fd);
+                // Catching ErrnoException will raise error in API < 21
+            } catch (Exception ex) {
+                Log.e(TAG, "Error closing fd.");
+            }
+        } else {
+            Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
+        }
+    }
+
+    @RequiresApi(21)
+    static class Api21Impl {
+        private Api21Impl() {}
+
+        @DoNotInline
+        static FileDescriptor dup(FileDescriptor fileDescriptor) throws ErrnoException {
+            return Os.dup(fileDescriptor);
+        }
+
+        @DoNotInline
+        static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
+            return Os.lseek(fd, offset, whence);
+        }
+
+        @DoNotInline
+        static void close(FileDescriptor fd) throws ErrnoException {
+            Os.close(fd);
+        }
+    }
+
+    @RequiresApi(23)
+    static class Api23Impl {
+        private Api23Impl() {}
+
+        @DoNotInline
+        static void setDataSource(MediaMetadataRetriever retriever, MediaDataSource dataSource) {
+            retriever.setDataSource(dataSource);
+        }
+    }
+}

+ 73 - 39
app/src/main/java/ch/threema/app/utils/FileUtil.java

@@ -50,10 +50,13 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.reflect.Array;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
+import java.util.stream.Stream;
 
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.annotation.NonNull;
@@ -190,7 +193,7 @@ public class FileUtil {
 
 	private static @NonNull Intent getGetContentIntent(Context context, String[] mimeTypes, String initialPath) {
 		Intent intent = new Intent();
-		if (MimeUtil.isVideoFile(mimeTypes[0]) || MimeUtil.isImageFile(mimeTypes[0])) {
+		if (MimeUtil.isVideoFile(mimeTypes[0]) || MimeUtil.isSupportedImageFile(mimeTypes[0])) {
 			intent.setAction(Intent.ACTION_GET_CONTENT);
 		} else {
 			intent = new Intent(context, FilePickerActivity.class);
@@ -387,12 +390,10 @@ public class FileUtil {
 		// Pick up provider with action string
 		final Intent i = new Intent(DocumentsContract.PROVIDER_INTERFACE);
 		final List<ResolveInfo> providers = pm.queryIntentContentProviders(i, 0);
-		for (ResolveInfo info : providers)
-		{
-			if(info != null && info.providerInfo != null)
-			{
+		for (ResolveInfo info : providers) {
+			if (info != null && info.providerInfo != null) {
 				final String authority = info.providerInfo.authority;
-				if(isMediaDocument(Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + authority)))
+				if (isMediaDocument(Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + authority)))
 					return true;
 			}
 		}
@@ -400,8 +401,8 @@ public class FileUtil {
 	}
 
 	/*
-	* Some content uri returned by systemUI file picker create intermittent permission problems
-	* To fix this, we convert it in a file uri
+	 * Some content uri returned by systemUI file picker create intermittent permission problems
+	 * To fix this, we convert it in a file uri
 	 */
 	public static Uri getFixedContentUri(Context context, Uri inUri) {
 		if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
@@ -471,7 +472,7 @@ public class FileUtil {
 
 				final String selection = "_id=?";
 				final String[] selectionArgs = new String[]{
-						split[1]
+					split[1]
 				};
 
 				return getDataColumn(context, contentUri, selection, selectionArgs);
@@ -509,18 +510,18 @@ public class FileUtil {
 
 	@Nullable
 	private static String getDataColumn(Context context, Uri uri, String selection,
-								 String[] selectionArgs) {
+										String[] selectionArgs) {
 
 		String data = null;
 		Cursor cursor = null;
 		final String column = "_data";
 		final String[] projection = {
-				column
+			column
 		};
 
 		try {
 			cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
-					null);
+				null);
 			if (cursor != null && cursor.moveToFirst()) {
 				final int column_index = cursor.getColumnIndexOrThrow(column);
 				data = cursor.getString(column_index);
@@ -606,9 +607,8 @@ public class FileUtil {
 
 	@WorkerThread
 	public static boolean copyFile(@NonNull File source, @NonNull File dest) {
-		try (InputStream  inputStream = new FileInputStream(source);
-		     OutputStream outputStream = new FileOutputStream(dest))
-		{
+		try (InputStream inputStream = new FileInputStream(source);
+			 OutputStream outputStream = new FileOutputStream(dest)) {
 			IOUtils.copy(inputStream, outputStream);
 			return true;
 		} catch (Exception e) {
@@ -619,9 +619,8 @@ public class FileUtil {
 
 	@WorkerThread
 	public static boolean copyFile(@NonNull Uri source, @NonNull File dest, @NonNull ContentResolver contentResolver) {
-		try (InputStream  inputStream = contentResolver.openInputStream(source);
-		     OutputStream outputStream = new FileOutputStream(dest))
-		{
+		try (InputStream inputStream = contentResolver.openInputStream(source);
+			 OutputStream outputStream = new FileOutputStream(dest)) {
 			if (inputStream != null) {
 				IOUtils.copy(inputStream, outputStream);
 				return true;
@@ -637,9 +636,9 @@ public class FileUtil {
 	 *
 	 * Note: Do not use this if error recovery is important!
 	 *
-	 * @param file The file that should be deleted
+	 * @param file        The file that should be deleted
 	 * @param description The description of the file (e.g. "message queue database")
-	 * @param logger The logger to use
+	 * @param logger      The logger to use
 	 */
 	public static void deleteFileOrWarn(
 		@NonNull File file,
@@ -664,7 +663,7 @@ public class FileUtil {
 
 	/**
 	 * Create a new file or re-use existing file. Log if file already exists.
-	 * @param file The file that should be created or re-used
+	 * @param file   The file that should be created or re-used
 	 * @param logger The logger facility to use
 	 */
 	public static void createNewFileOrLog(
@@ -679,7 +678,7 @@ public class FileUtil {
 	/**
 	 * Try to generated a File with the given filename in the given path
 	 * If a file of the same name exists, add a number to the filename (possibly between name and extension)
-	 * @param destPath Destination path
+	 * @param destPath     Destination path
 	 * @param destFilename Desired filename
 	 * @return File object
 	 */
@@ -707,7 +706,7 @@ public class FileUtil {
 	/**
 	 * Returns the filename of the object referred to by mediaItem. If no filename can be found, generate one
 	 * @param contentResolver ContentResolver
-	 * @param mediaItem MediaItem representing the source file
+	 * @param mediaItem       MediaItem representing the source file
 	 * @return A filename
 	 */
 	public static @NonNull String getFilenameFromUri(@NonNull ContentResolver contentResolver, @NonNull MediaItem mediaItem) {
@@ -722,7 +721,7 @@ public class FileUtil {
 	/**
 	 * Returns the filename of the object referred to by uri by querying the content resolver
 	 * @param contentResolver ContentResolver
-	 * @param uri Uri pointing at the object
+	 * @param uri             Uri pointing at the object
 	 * @return A filename or null if none is found
 	 */
 	@Nullable
@@ -771,9 +770,9 @@ public class FileUtil {
 	/**
 	 * Select a file from a gallery app. Shows a selector first to allow for choosing the desired gallery app or SystemUIs file picker.
 	 * Does not necessarily need file permissions as a modern gallery app will return a content Uri with a temporary permission to access the file
- 	 * @param activity Activity where the result of the selection should end up
-	 * @param fragment Fragment where the result of the selection should end up
-	 * @param requestCode Request code to use for result
+	 * @param activity     Activity where the result of the selection should end up
+	 * @param fragment     Fragment where the result of the selection should end up
+	 * @param requestCode  Request code to use for result
 	 * @param includeVideo Whether to include the possibility to select video files (if supported by app)
 	 */
 	public static void selectFromGallery(@Nullable Activity activity, @Nullable Fragment fragment, int requestCode, boolean includeVideo) {
@@ -781,29 +780,36 @@ public class FileUtil {
 			activity = fragment.getActivity();
 		}
 
+		final String imageMimeTypes = String.join(",", MimeUtil.getSupportedImageMimeTypes());
+
 		try {
-			Intent startIntent;
-			Intent getContentIntent = new Intent();
+			final Intent startIntent;
+			final Intent getContentIntent = new Intent();
+
+			getContentIntent.setAction(Intent.ACTION_GET_CONTENT);
+			getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
+			getContentIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+			getContentIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, MAX_BLOB_SIZE);
+
 			if (includeVideo && (
-				ConfigUtils.isXiaomiDevice() ||
+					ConfigUtils.isXiaomiDevice() ||
 					Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)) {
-				getContentIntent.setType(MimeUtil.MIME_TYPE_IMAGE + ","+ MimeUtil.MIME_TYPE_VIDEO);
-				String[] mimetypes = {MimeUtil.MIME_TYPE_IMAGE, MimeUtil.MIME_TYPE_VIDEO};
+				getContentIntent.setType(MimeUtil.MIME_TYPE_IMAGE + "," + MimeUtil.MIME_TYPE_VIDEO);
+				String[] mimetypes = Stream.concat(Arrays.stream(MimeUtil.getSupportedImageMimeTypes()), Arrays.stream(new String[]{MimeUtil.MIME_TYPE_VIDEO})).toArray(
+					size -> (String[]) Array.newInstance(String.class, size)
+				);
 				getContentIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes);
 			} else {
-				getContentIntent.setType(includeVideo ? MimeUtil.MIME_TYPE_VIDEO : MimeUtil.MIME_TYPE_IMAGE);
+				getContentIntent.setType(MimeUtil.MIME_TYPE_IMAGE);
+				getContentIntent.putExtra(Intent.EXTRA_MIME_TYPES, imageMimeTypes);
 			}
-			getContentIntent.setAction(Intent.ACTION_GET_CONTENT);
-			getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
-			getContentIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
-			getContentIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, MAX_BLOB_SIZE);
 
 			if (includeVideo) {
-				Intent pickIntent = new Intent(Intent.ACTION_PICK);
-				pickIntent.setType(MimeUtil.MIME_TYPE_IMAGE);
 				if (ConfigUtils.isXiaomiDevice()) {
 					startIntent = getContentIntent;
 				} else {
+					Intent pickIntent = new Intent(Intent.ACTION_PICK);
+					pickIntent.setType(imageMimeTypes);
 					startIntent = Intent.createChooser(pickIntent, activity.getString(R.string.select_from_gallery));
 					startIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{getContentIntent});
 				}
@@ -820,4 +826,32 @@ public class FileUtil {
 			Toast.makeText(activity, R.string.no_activity_for_mime_type, Toast.LENGTH_SHORT).show();
 		}
 	}
+
+	/**
+	 * Check if the file at the provided Uri is an animated WebP file by looking at the file header
+	 * @param uri A File Uri pointing to an image file
+	 * @return true if the file is an animated WebP file, false if it is not animated, in another format, corrupt or not readable
+	 */
+	private static boolean isAnimatedWebPFile(@NonNull Uri uri) {
+		try (InputStream inputStream = StreamUtil.getFromUri(ThreemaApplication.getAppContext(), uri)) {
+			byte[] buffer = new byte[34];
+			return inputStream != null
+				&& inputStream.read(buffer) == 34
+				&& Arrays.equals(Arrays.copyOfRange(buffer, 0, 4), new byte[]{'R', 'I', 'F', 'F'})
+				&& Arrays.equals(Arrays.copyOfRange(buffer, 8, 12), new byte[]{'W', 'E', 'B', 'P'})
+				&& Arrays.equals(Arrays.copyOfRange(buffer, 12, 15), new byte[]{'V', 'P', '8'})
+				&& Arrays.equals(Arrays.copyOfRange(buffer, 30, 34), new byte[]{'A', 'N', 'I', 'M'});
+		} catch (IOException ignore) {
+			return false;
+		}
+	}
+
+	/**
+	 * Check if the file at the provided Uri is an animation. Currently, only animated WebP is supported
+	 * @param uri A File Uri pointing to an image file
+	 * @return true if the file an animated image
+	 */
+	public static boolean isAnimatedImageFile(@NonNull Uri uri) {
+		return isAnimatedWebPFile(uri);
+	}
 }

+ 34 - 9
app/src/main/java/ch/threema/app/utils/ImageViewUtil.java

@@ -53,7 +53,34 @@ public class ImageViewUtil {
 		@Nullable Bitmap bitmap,
 		int width
 	) {
-		showRoundedBitmapOrPlaceholder(
+		if (blockView != null && imageView != null) {
+			ViewGroup.LayoutParams params = blockView.getLayoutParams();
+			params.width = width;
+			params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+			blockView.setLayoutParams(params);
+
+			Bitmap bitmapOrPlaceholder = bitmap == null
+				? getPlaceholderImage(context, width, R.drawable.ic_image_outline)
+				: bitmap;
+
+			RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory
+				.create(context.getResources(), bitmapOrPlaceholder);
+			roundedBitmapDrawable.setCornerRadius(getCornerRadius(context));
+
+			if (bitmapOrPlaceholder != null) {
+				showBitmap(imageView, bitmapOrPlaceholder, roundedBitmapDrawable, width);
+			}
+		}
+	}
+
+	public static void showBitmapOrImagePlaceholder(
+		@NonNull Context context,
+		@Nullable View blockView,
+		@Nullable ImageView imageView,
+		@Nullable Bitmap bitmap,
+		int width
+	) {
+		showBitmapOrPlaceholder(
 			context,
 			blockView,
 			imageView,
@@ -63,14 +90,14 @@ public class ImageViewUtil {
 		);
 	}
 
-	public static void showRoundedBitmapOrMoviePlaceholder(
+	public static void showBitmapOrMoviePlaceholder(
 		@NonNull Context context,
 		@Nullable View blockView,
 		@Nullable ImageView imageView,
 		@Nullable Bitmap bitmap,
 		int width
 	) {
-		showRoundedBitmapOrPlaceholder(
+		showBitmapOrPlaceholder(
 			context,
 			blockView,
 			imageView,
@@ -80,7 +107,7 @@ public class ImageViewUtil {
 		);
 	}
 
-	private static void showRoundedBitmapOrPlaceholder(
+	public static void showBitmapOrPlaceholder(
 		@NonNull Context context,
 		@Nullable View blockView,
 		@Nullable ImageView imageView,
@@ -98,11 +125,9 @@ public class ImageViewUtil {
 				? getPlaceholderImage(context, width, drawableId)
 				: bitmap;
 
-			RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory
-				.create(context.getResources(), bitmapOrPlaceholder);
-			roundedBitmapDrawable.setCornerRadius(getCornerRadius(context));
-
-			showBitmap(imageView, bitmapOrPlaceholder, roundedBitmapDrawable, width);
+			if (bitmapOrPlaceholder != null) {
+				showBitmap(imageView, bitmapOrPlaceholder, null, width);
+			}
 		}
 	}
 

+ 44 - 19
app/src/main/java/ch/threema/app/utils/MimeUtil.java

@@ -22,14 +22,13 @@
 package ch.threema.app.utils;
 
 import android.content.Context;
+import android.net.Uri;
 import android.provider.DocumentsContract;
 
-import java.lang.annotation.Retention;
 import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.Map;
 
-import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
@@ -46,7 +45,6 @@ import static ch.threema.app.ui.MediaItem.TYPE_IMAGE;
 import static ch.threema.app.ui.MediaItem.TYPE_VIDEO;
 import static ch.threema.app.ui.MediaItem.TYPE_VOICEMESSAGE;
 import static ch.threema.domain.protocol.csp.messages.file.FileData.RENDERING_MEDIA;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 public class MimeUtil {
 
@@ -57,9 +55,11 @@ public class MimeUtil {
 	public static final String MIME_TYPE_IMAGE_JPG = "image/jpeg";
 	public static final String MIME_TYPE_IMAGE_PNG = "image/png";
 	public static final String MIME_TYPE_IMAGE_GIF = "image/gif";
+	public static final String MIME_TYPE_IMAGE_WEBP = "image/webp";
 	public static final String MIME_TYPE_IMAGE_HEIF = "image/heif";
 	public static final String MIME_TYPE_IMAGE_HEIC = "image/heic";
 	public static final String MIME_TYPE_IMAGE_TIFF = "image/tiff";
+	public static final String MIME_TYPE_IMAGE_SVG = "image/svg+xml";
 	public static final String MIME_TYPE_VIDEO_MPEG = "video/mpeg";
 	public static final String MIME_TYPE_VIDEO_MP4 = "video/mp4";
 	public static final String MIME_TYPE_VIDEO_AVC = "video/avc";
@@ -84,13 +84,6 @@ public class MimeUtil {
 	public static final String MIME_VIDEO = "video/";
 	public static final String MIME_AUDIO = "audio/";
 
-	@Retention(SOURCE)
-	@IntDef({MIME_TYPE_VIDEO_IND, MIME_TYPE_IMAGES_IND, MIME_TYPE_GIF_IND})
-	public @interface NavigationMode {}
-	public static final int MIME_TYPE_VIDEO_IND = 101;
-	public static final int MIME_TYPE_IMAGES_IND = 102;
-	public static final int MIME_TYPE_GIF_IND = 103;
-
 	// map from icon resource id to string resource id
 	protected static final EnumMap<MimeCategory, Integer> mimeToDescription = new EnumMap<>(MimeCategory.class);
 
@@ -266,6 +259,17 @@ public class MimeUtil {
 		mimeMap.put("application/vnd.openxmlformats-officedocument.presentationml.slideshow", MimeCategory.POWERPOINT);
 	}
 
+	/**
+	 * List of bitmap image mime types natively supported by android
+	 */
+	private static final String[] supportedImageMimeTypes = {
+		MIME_TYPE_IMAGE_JPG,
+		MIME_TYPE_IMAGE_PNG,
+		MIME_TYPE_IMAGE_GIF,
+		MIME_TYPE_IMAGE_WEBP,
+		MIME_TYPE_IMAGE_HEIC
+	};
+
 	public enum MimeCategory {
 		APK,
 		AUDIO,
@@ -328,17 +332,27 @@ public class MimeUtil {
 	}
 
 	public static boolean isImageFile(@Nullable String mimeType) {
-		return mimeType != null && mimeType.startsWith("image/");
+		return mimeType != null && mimeType.startsWith("image/") && !MimeUtil.isSVGFile(mimeType);
 	}
 
-	public static boolean isLabelableImageFile(@Nullable String mimeType) {
-		return mimeType != null && (mimeType.startsWith(MIME_TYPE_IMAGE_PNG) || mimeType.startsWith(MIME_TYPE_IMAGE_JPG)
-			|| mimeType.startsWith(MIME_TYPE_IMAGE_GIF) || mimeType.startsWith(MIME_TYPE_IMAGE_HEIF) || mimeType.startsWith(MIME_TYPE_IMAGE_HEIC));
+	/**
+	 * Check if the current mime type hints to an Android-natively supported bitmap image format
+	 * @param mimeType Mime Type to check such as "image/png"
+	 * @return true if format is supported, false otherwise
+	 */
+	public static boolean isSupportedImageFile(@Nullable String mimeType) {
+		if (mimeType != null && mimeType.startsWith("image/")) {
+			for (String type: supportedImageMimeTypes) {
+				if (mimeType.startsWith(type)) {
+					return true;
+				}
+			}
+		}
+		return false;
 	}
 
-	public static boolean isStaticImageFile(@Nullable String mimeType) {
-		return mimeType != null && (mimeType.startsWith(MIME_TYPE_IMAGE_PNG) || mimeType.startsWith(MIME_TYPE_IMAGE_JPG)
-			|| mimeType.startsWith(MIME_TYPE_IMAGE_HEIF) || mimeType.startsWith(MIME_TYPE_IMAGE_HEIC) || mimeType.startsWith(MIME_TYPE_IMAGE_TIFF));
+	public static String[] getSupportedImageMimeTypes() {
+		return supportedImageMimeTypes;
 	}
 
 	public static boolean isVideoFile(@Nullable String mimeType) {
@@ -361,6 +375,10 @@ public class MimeUtil {
 		return mimeType != null && mimeType.startsWith(MIME_TYPE_IMAGE_GIF);
 	}
 
+	public static boolean isWebPFile(@Nullable String mimeType) {
+		return mimeType != null && mimeType.startsWith(MIME_TYPE_IMAGE_WEBP);
+	}
+
 	public static boolean isPdfFile(@Nullable String mimeType) {
 		return mimeType != null && mimeType.startsWith(MIME_TYPE_PDF);
 	}
@@ -369,6 +387,10 @@ public class MimeUtil {
 		return mimeType != null && (mimeType.startsWith(MIME_TYPE_VCARD) || mimeType.startsWith(MIME_TYPE_VCARD_ALT));
 	}
 
+	public static boolean isSVGFile(String mimeType) {
+		return mimeType != null && mimeType.startsWith(MIME_TYPE_IMAGE_SVG);
+	}
+
 	@NonNull
 	private static String getType(String mimeType) throws MalformedMimeTypeException {
 		if (mimeType != null) {
@@ -462,11 +484,14 @@ public class MimeUtil {
 		return mimeType;
 	}
 
-	public static @MediaItem.MediaType int getMediaTypeFromMimeType(String mimeType) {
-		if (MimeUtil.isImageFile(mimeType)) {
+	public static @MediaItem.MediaType int getMediaTypeFromMimeType(String mimeType, Uri uri) {
+		if (MimeUtil.isSupportedImageFile(mimeType)) {
 			if (MimeUtil.isGifFile(mimeType)) {
 				return TYPE_GIF;
 			} else {
+				if (ConfigUtils.isSupportedAnimatedImageFormat(mimeType) && FileUtil.isAnimatedImageFile(uri)) {
+					return MediaItem.TYPE_IMAGE_ANIMATED;
+				}
 				return TYPE_IMAGE;
 			}
 		} else if (MimeUtil.isVideoFile(mimeType)) {

+ 1 - 1
app/src/main/java/ch/threema/app/utils/ThumbnailUtil.java

@@ -79,7 +79,7 @@ public class ThumbnailUtil {
 
 		Uri uri = Uri.fromFile(file);
 
-		switch (MimeUtil.getMediaTypeFromMimeType(mimeType)) {
+		switch (MimeUtil.getMediaTypeFromMimeType(mimeType, uri)) {
 			case MediaItem.TYPE_IMAGE:
 				return BitmapUtil.safeGetBitmapFromUri(context, uri, MessageServiceImpl.THUMBNAIL_SIZE_PX, false, true, true);
 			case MediaItem.TYPE_GIF:

+ 1 - 0
app/src/main/java/ch/threema/app/voip/Config.java

@@ -61,6 +61,7 @@ public class Config {
 		"Samsung;SM-A320FL;8.", // Galaxy A3 (2017), Ticket #926673
 		"Samsung;SM-G930F;7.", // Galaxy S7, Ticket #573851
 		"Samsung;SM-G960F;8.", // Galaxy S9, Ticket #379708
+		"Fairphone;FP5;13.", // Fairphone 5, Ticket #641579
 	};
 
 	/**

+ 48 - 10
app/src/main/java/ch/threema/app/voip/VoipBluetoothManager.java

@@ -60,6 +60,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.content.ContextCompat;
 import ch.threema.app.ThreemaApplication;
+import ch.threema.app.utils.TestUtil;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.util.AppRTCUtils;
 import ch.threema.app.voip.util.VoipUtil;
@@ -108,6 +109,7 @@ public class VoipBluetoothManager {
 	int scoConnectionAttempts;
 	private State bluetoothState;
 	private Long bluetoothAudioConnectedAt;
+	private String connectedBluetoothDeviceAddress;
 	private final BluetoothProfile.ServiceListener bluetoothServiceListener;
 	private BluetoothAdapter bluetoothAdapter;
 	private BluetoothHeadset bluetoothHeadset;
@@ -190,8 +192,8 @@ public class VoipBluetoothManager {
 		private void onConnectionStateChange(Intent intent) {
 			final int state =
 				intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
-			logger.info("BluetoothHeadsetBroadcastReceiver.onReceive: a=ACTION_CONNECTION_STATE_CHANGED, s={}, sb={}, BT state={}, d={}",
-				headsetStateToString(state), isInitialStickyBroadcast(), bluetoothState, getDeviceNameFromIntent(intent));
+			logger.info("BluetoothHeadsetBroadcastReceiver.onReceive while in BT state={}: a=ACTION_CONNECTION_STATE_CHANGED, headsetState={}, sb={}, d={}",
+				bluetoothState, headsetStateToString(state), isInitialStickyBroadcast(), getDeviceNameFromIntent(intent));
 			switch (state) {
 				case BluetoothHeadset.STATE_CONNECTED:
 					scoConnectionAttempts = 0;
@@ -202,9 +204,23 @@ public class VoipBluetoothManager {
 					// No action needed
 					break;
 				case BluetoothHeadset.STATE_DISCONNECTED:
-					// Bluetooth is probably powered off during the call.
-					stopScoAudio();
-					updateAudioDeviceState();
+					String disconnectedBluetoothDeviceAddress = getDeviceAddressFromIntent(intent);
+					if (TestUtil.compare(connectedBluetoothDeviceAddress, disconnectedBluetoothDeviceAddress)) {
+						logger.info(
+							"The connected bluetooth device '{}' is now disconnected",
+							disconnectedBluetoothDeviceAddress
+						);
+						// Bluetooth is probably powered off during the call.
+						stopScoAudio();
+						updateAudioDeviceState();
+						connectedBluetoothDeviceAddress = null;
+					} else {
+						logger.info(
+							"Another bluetooth device '{}' has been disconnected. Connected device: '{}'",
+							disconnectedBluetoothDeviceAddress,
+							connectedBluetoothDeviceAddress
+						);
+					}
 					break;
 			}
 		}
@@ -217,8 +233,8 @@ public class VoipBluetoothManager {
 			final int state = intent.getIntExtra(
 				BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
 
-			logger.info("BluetoothHeadsetBroadcastReceiver.onReceive: a=ACTION_AUDIO_STATE_CHANGED, s={}, sb={}, BT state={}, d={}",
-				headsetStateToString(state), isInitialStickyBroadcast(), bluetoothState, getDeviceNameFromIntent(intent));
+			logger.info("BluetoothHeadsetBroadcastReceiver.onReceive while in BT state={}: a=ACTION_AUDIO_STATE_CHANGED, headsetState={}, sb={}, d={}",
+				bluetoothState, headsetStateToString(state), isInitialStickyBroadcast(), getDeviceNameFromIntent(intent));
 
 			// Switch BluetoothHeadsetBroadcastReceiver.onReceive: a=ACTION_AUDIO_STATE_CHANGED, s=A_DISCONNECTED, sb=false, BT state: HEADSET_AVAILABLE
 			// Btn BluetoothHeadsetBroadcastReceiver.onReceive: a=ACTION_AUDIO_STATE_CHANGED, s=A_DISCONNECTED, sb=false, BT state: SCO_CONNECTED
@@ -229,6 +245,11 @@ public class VoipBluetoothManager {
 					logger.debug("+++ Bluetooth audio SCO is now connected");
 					bluetoothState = State.SCO_CONNECTED;
 					bluetoothAudioConnectedAt = System.nanoTime();
+					connectedBluetoothDeviceAddress = getDeviceAddressFromIntent(intent);
+					logger.info(
+						"Bluetooth audio SCO is now connected with device {}",
+						connectedBluetoothDeviceAddress
+					);
 					scoConnectionAttempts = 0;
 					updateAudioDeviceState();
 				} else {
@@ -282,13 +303,30 @@ public class VoipBluetoothManager {
 
 		@Nullable
 		private String getDeviceNameFromIntent(@NonNull Intent intent) {
-			final BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-			if (device != null
-				&& ContextCompat.checkSelfPermission(ThreemaApplication.getAppContext(), Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
+			final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+
+			// On versions prior to S, devices already have this permission by default
+			boolean hasBluetoothConnectPermission =
+				Build.VERSION.SDK_INT < Build.VERSION_CODES.S
+					|| ContextCompat.checkSelfPermission(
+					ThreemaApplication.getAppContext(),
+					Manifest.permission.BLUETOOTH_CONNECT
+				) == PackageManager.PERMISSION_GRANTED;
+
+			if (device != null && hasBluetoothConnectPermission) {
 				return device.getName();
 			}
 			return "";
 		}
+
+		@Nullable
+		private String getDeviceAddressFromIntent(@NonNull Intent intent) {
+			final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+			if (device != null) {
+				return device.getAddress();
+			}
+			return "";
+		}
 	}
 
 	/**

+ 7 - 4
app/src/main/java/ch/threema/app/voip/groupcall/GroupCallManagerImpl.kt

@@ -53,6 +53,7 @@ import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.*
 import org.json.JSONArray
 import org.json.JSONObject
+import java.lang.ref.WeakReference
 import java.security.SecureRandom
 import java.util.*
 import kotlin.math.max
@@ -88,7 +89,7 @@ class GroupCallManagerImpl(
 	}
 
 	private val generalCallObservers: MutableSet<GroupCallObserver> = Collections.synchronizedSet(mutableSetOf())
-	private val callObservers: MutableMap<LocalGroupId, MutableSet<GroupCallObserver>> = mutableMapOf()
+	private val callObservers: MutableMap<LocalGroupId, MutableSet<WeakReference<GroupCallObserver>>> = mutableMapOf()
 	private val callRefreshTimers: MutableMap<LocalGroupId, Job> = Collections.synchronizedMap(mutableMapOf())
 
 	// TODO(ANDR-1957): Unsure if this is guarded properly for use outside of the GC thread. There
@@ -142,7 +143,7 @@ class GroupCallManagerImpl(
 				callObservers[groupId] = Collections.synchronizedSet(mutableSetOf())
 			}
 		}
-		if (callObservers[groupId]?.add(observer) == true) {
+		if (callObservers[groupId]?.add(WeakReference(observer)) == true) {
 			observer.onGroupCallUpdate(chosenCalls[groupId])
 		}
 	}
@@ -155,7 +156,9 @@ class GroupCallManagerImpl(
 	@AnyThread
 	override fun removeGroupCallObserver(groupId: LocalGroupId, observer: GroupCallObserver) {
 		synchronized(callObservers) {
-			callObservers[groupId]?.remove(observer)
+			callObservers[groupId]?.removeIf {
+				it.get()?.equals(observer) ?: false
+			}
 		}
 	}
 
@@ -776,7 +779,7 @@ class GroupCallManagerImpl(
 
 	private fun notifyCallObservers(groupId: LocalGroupId, call: GroupCallDescription?) {
 		synchronized(callObservers) {
-			callObservers[groupId]?.forEach { it.onGroupCallUpdate(call) }
+			callObservers[groupId]?.forEach { it.get()?.onGroupCallUpdate(call) }
 		}
 		notifyGeneralCallObservers(call)
 	}

+ 4 - 13
app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallService.kt

@@ -30,7 +30,6 @@ import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.graphics.Bitmap
-import android.os.Binder
 import android.os.Build
 import android.telephony.PhoneStateListener
 import android.telephony.TelephonyManager
@@ -112,7 +111,7 @@ class GroupCallService : Service() {
 
     private val serviceRunning = AtomicBoolean(false)
 
-    private val binder = GroupCallServiceBinder()
+    private var binder : GroupCallServiceBinder? = null
 
     private var groupCallController: GroupCallControllerImpl? = null
     private val controllerDeferred = CompletableDeferred<GroupCallController>()
@@ -145,12 +144,13 @@ class GroupCallService : Service() {
         }
     }
 
-    override fun onBind(intent: Intent?): GroupCallServiceBinder {
+    override fun onBind(intent: Intent?): GroupCallServiceBinder? {
         return binder
     }
 
     override fun onCreate() {
         super.onCreate()
+        binder = GroupCallServiceBinder(controllerDeferred, audioManagerDeferred)
         initDependencies()
     }
 
@@ -374,20 +374,11 @@ class GroupCallService : Service() {
         audioManagerDeferred.completeExceptionally(exception)
         audioManager?.stop()
         audioManager = null
+        binder = null
         getJoinCallPendingIntent(PendingIntent.FLAG_NO_CREATE)?.cancel()
         getLeaveCallPendingIntent(PendingIntent.FLAG_NO_CREATE)?.cancel()
     }
 
-    inner class GroupCallServiceBinder : Binder() {
-        suspend fun getGroupCallController(): GroupCallController {
-            return controllerDeferred.await()
-        }
-
-        suspend fun getCallAudioManager(): CallAudioManager {
-            return audioManagerDeferred.await()
-        }
-    }
-
     // wrapper for group id to make it an object and `lateinit` can be used
     private data class ServiceGroupId(val id: Int) {
         val localGroupId

+ 39 - 0
app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallServiceBinder.kt

@@ -0,0 +1,39 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2022-2024 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.voip.groupcall.service
+
+import android.os.Binder
+import ch.threema.app.voip.CallAudioManager
+import ch.threema.app.voip.groupcall.sfu.GroupCallController
+import kotlinx.coroutines.CompletableDeferred
+
+class GroupCallServiceBinder(private val controllerDeferred : CompletableDeferred<GroupCallController>, private val audioManagerDeferred : CompletableDeferred<CallAudioManager>) :
+    Binder() {
+
+    suspend fun getGroupCallController(): GroupCallController {
+        return controllerDeferred.await()
+    }
+
+    suspend fun getCallAudioManager(): CallAudioManager {
+        return audioManagerDeferred.await()
+    }
+}

+ 2 - 2
app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallServiceConnection.kt

@@ -36,12 +36,12 @@ private val logger = LoggingUtil.getThreemaLogger("GroupCallServiceConnection")
 
 class GroupCallServiceConnection : ServiceConnection {
     private var groupCallController: GroupCallController? = null
-    private val deferredServiceBinder: CompletableDeferred<GroupCallService.GroupCallServiceBinder> = CompletableDeferred()
+    private val deferredServiceBinder: CompletableDeferred<GroupCallServiceBinder> = CompletableDeferred()
     private var disconnected = false
 
     override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
         logger.debug("Service connected")
-        if (serviceBinder !is GroupCallService.GroupCallServiceBinder) {
+        if (serviceBinder !is GroupCallServiceBinder) {
             deferredServiceBinder.completeExceptionally(ThreemaException("Bound to incompatible service"))
         } else {
             deferredServiceBinder.complete(serviceBinder)

+ 2 - 2
app/src/main/java/ch/threema/app/webclient/converter/Message.java

@@ -198,7 +198,7 @@ public class Message extends Converter {
 					// Nothing to be done
 					break;
 				case FileData.RENDERING_MEDIA:
-					if (MimeUtil.isImageFile(mediaType) && !MimeUtil.isGifFile(mediaType)) {
+					if (MimeUtil.isSupportedImageFile(mediaType) && !MimeUtil.isGifFile(mediaType)) {
 						virtualMessageType = ch.threema.storage.models.MessageType.IMAGE;
 					} else if (MimeUtil.isAudioFile(mediaType)) {
 						virtualMessageType = ch.threema.storage.models.MessageType.VOICEMESSAGE;
@@ -207,7 +207,7 @@ public class Message extends Converter {
 					}
 					break;
 				case FileData.RENDERING_STICKER:
-					if (MimeUtil.isImageFile(mediaType)) {
+					if (MimeUtil.isSupportedImageFile(mediaType)) {
 						virtualMessageType = ch.threema.storage.models.MessageType.IMAGE;
 					}
 					break;

+ 1 - 0
app/src/main/java/ch/threema/storage/models/data/media/FileDataModel.java

@@ -48,6 +48,7 @@ public class FileDataModel implements MediaMessageDataInterface {
 	public static final String METADATA_KEY_DURATION = "d";
 	public static final String METADATA_KEY_WIDTH = "w";
 	public static final String METADATA_KEY_HEIGHT = "h";
+	public static final String METADATA_KEY_ANIMATED = "a";
 
 	private byte[] fileBlobId;
 	private byte[] encryptionKey;

+ 10 - 0
app/src/main/res/drawable/ic_ink_highlighter_outline.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M544,560L440,456L240,656Q240,656 240,656Q240,656 240,656L344,760Q344,760 344,760Q344,760 344,760L544,560ZM497,399L601,503L800,304Q800,304 800,304Q800,304 800,304L696,200Q696,200 696,200Q696,200 696,200L497,399ZM413,371L629,587L400,816Q376,840 344,840Q312,840 288,816L286,814L260,840L60,840L186,714L184,712Q160,688 160,656Q160,624 184,600L413,371ZM413,371L640,144Q664,120 696,120Q728,120 752,144L856,248Q880,272 880,304Q880,336 856,360L629,587L413,371Z"/>
+</vector>

+ 22 - 0
app/src/main/res/drawable/ic_webp.xml

@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="m238.61,599.12h-43.92l-11.33,-47.28q-0.89,-3.36 -2.13,-7.97 -1.24,-4.6 -2.66,-9.74 -1.24,-5.31 -2.66,-10.8 -1.24,-5.67 -2.48,-10.98 -3.01,-12.4 -6.02,-25.85 -3.19,13.64 -6.2,26.03 -1.24,5.31 -2.66,10.98 -1.24,5.49 -2.66,10.8 -1.24,5.14 -2.48,9.74 -1.24,4.43 -2.13,7.79l-11.69,47.28L95.7,599.12L58.34,360.23h45.86l15.76,123.07q0.89,6.91 1.42,15.05 0.53,8.15 0.89,15.41 0.35,8.5 0.53,16.65 1.06,-6.38 2.66,-13.46 1.42,-6.2 3.19,-13.81 1.77,-7.79 3.9,-15.94l17.53,-60.03h34.53l17.53,60.03q2.3,7.97 3.9,15.76 1.77,7.61 3.19,13.81 1.59,7.08 2.66,13.64 0.18,0 0.35,-4.78 0.18,-4.78 0.53,-11.86 0.35,-7.08 0.89,-15.23 0.53,-8.32 1.42,-15.23l14.88,-123.07h45.86z"
+      android:strokeWidth="40"/>
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="m300.23,599.12v-238.89h171.42v40.38L352.47,400.61v57.38h108.38v40.38L352.47,498.36v60.39h126.44v40.38z"
+      android:strokeWidth="40"/>
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="m703.63,530.94q0,18.24 -6.73,31.17 -6.73,12.93 -18.06,21.25 -11.33,8.15 -26.39,12.04 -14.88,3.72 -31.52,3.72L517.87,599.12v-238.89h92.97q18.42,0 32.94,3.72 14.52,3.54 24.61,10.98 10.09,7.44 15.41,18.77 5.49,11.33 5.49,26.56 0,20.36 -11.69,34.35 -11.69,13.81 -35.95,18.95 30.28,3.19 46.04,18.06 15.94,14.88 15.94,39.31zM636.69,427.35q0,-15.41 -7.97,-21.78 -7.97,-6.55 -21.6,-6.55h-37.01v56.49h37.36q14.34,0 21.78,-7.08 7.44,-7.08 7.44,-21.07zM651.03,526.51q0,-9.21 -3.01,-15.41 -3.01,-6.2 -8.15,-9.92 -4.96,-3.9 -11.69,-5.49 -6.55,-1.59 -13.81,-1.59h-44.27v66.23h45.51q7.08,0 13.46,-1.59 6.55,-1.59 11.33,-5.31 4.96,-3.9 7.79,-10.45 2.83,-6.55 2.83,-16.47z"
+      android:strokeWidth="40"/>
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="m913.47,435.67q0,16.11 -5.49,30.64 -5.31,14.34 -16.47,25.15 -11.16,10.8 -28.33,17.18 -17.18,6.2 -40.73,6.2h-34.89v84.29h-52.24v-238.89h85q23.91,0 41.44,5.31 17.53,5.14 29.04,14.88 11.51,9.74 17,23.73 5.67,13.99 5.67,31.52zM860.88,436.55q0,-19.3 -11.69,-27.45 -11.51,-8.32 -34.71,-8.32h-26.92v73.84h28.33q23.38,0 34.18,-9.56 10.8,-9.74 10.8,-28.51z"
+      android:strokeWidth="40"/>
+</vector>

+ 2 - 1
app/src/main/res/layout/activity_global_search.xml

@@ -21,7 +21,7 @@
 		app:behavior_peekHeight="256dp"
 		app:behavior_saveFlags="all"
 		android:elevation="2dp"
-		app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+		app:layout_behavior="@string/bottom_sheet_behavior">
 
 		<ImageView
 			style="@style/Threema.BottomSheet.DragHandle"
@@ -138,6 +138,7 @@
 			android:layout_height="0dp"
 			app:layout_constraintLeft_toLeftOf="parent"
 			app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
 			app:layout_constraintTop_toBottomOf="@+id/chip_group" />
 
 	</androidx.constraintlayout.widget.ConstraintLayout>

+ 4 - 3
app/src/main/res/layout/activity_media_viewer.xml

@@ -41,8 +41,8 @@
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_gravity="bottom|center_horizontal"
-		app:contentPaddingTop="8dp"
-		app:contentPaddingBottom="8dp"
+		app:contentPaddingTop="@dimen/mediaviewer_caption_container_padding_vertical"
+		app:contentPaddingBottom="@dimen/mediaviewer_caption_container_padding_vertical"
 		app:contentPaddingLeft="16dp"
 		app:contentPaddingRight="16dp"
 		android:visibility="gone">
@@ -53,8 +53,9 @@
 			android:layout_height="wrap_content"
 			android:layout_gravity="center_horizontal"
 			android:text=""
+			android:ellipsize="end"
 			android:textColor="@android:color/white"
-			android:textSize="18sp"/>
+			android:textSize="@dimen/mediaviewer_caption_text_size"/>
 
 	</com.google.android.material.card.MaterialCardView>
 

+ 1 - 2
app/src/main/res/layout/conversation_bubble_footer_groupack_recv.xml

@@ -6,8 +6,7 @@
 	android:layout_height="match_parent"
 	android:orientation="horizontal"
 	android:layout_marginRight="3dp"
-	android:visibility="gone"
-	android:animateLayoutChanges="true">
+	android:visibility="gone">
 
 	<ImageView
 		android:id="@+id/groupack_thumbsup"

+ 2 - 4
app/src/main/res/layout/conversation_bubble_footer_groupack_send.xml

@@ -6,8 +6,7 @@
 	android:layout_height="match_parent"
 	android:layout_marginLeft="3dp"
 	android:orientation="horizontal"
-	android:visibility="gone"
-	android:animateLayoutChanges="true">
+	android:visibility="gone">
 
 	<ImageView
 		android:id="@+id/groupack_thumbsup"
@@ -17,8 +16,7 @@
 		android:layout_gravity="center_vertical"
 		android:src="@drawable/ic_thumb_up_grey600_24dp"
 		app:tint="@color/material_green"
-		android:contentDescription="@string/message_acknowledged"
-		/>
+		android:contentDescription="@string/message_acknowledged" />
 
 	<TextView
 		android:id="@+id/groupack_thumbsup_count"

+ 1 - 1
app/src/main/res/layout/conversation_list_item_media.xml

@@ -6,7 +6,7 @@
 			 android:foreground="@drawable/bubble_overlay_selector"
              android:minHeight="@dimen/media_thumbnail_min_height">
 
-	<ImageView
+	<com.google.android.material.imageview.ShapeableImageView
 			android:id="@+id/attachment_image_view"
 			android:layout_height="wrap_content"
 			android:layout_width="fill_parent"

+ 0 - 1
app/src/main/res/layout/conversation_list_item_media_recv.xml

@@ -4,7 +4,6 @@
 -->
 <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/msg_list_item_recv"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"

+ 0 - 1
app/src/main/res/layout/conversation_list_item_media_send.xml

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
 	android:id="@+id/msg_list_item_send"
 	android:layout_width="wrap_content"
 	android:layout_height="wrap_content"

+ 1 - 1
app/src/main/res/layout/conversation_list_item_video_send.xml

@@ -29,7 +29,7 @@
 			android:foreground="@drawable/bubble_overlay_selector"
 			android:minHeight="@dimen/media_thumbnail_min_height">
 
-			<ImageView
+			<com.google.android.material.imageview.ShapeableImageView
 				android:id="@+id/attachment_image_view"
 				android:layout_height="wrap_content"
 				android:layout_width="fill_parent"

+ 2 - 2
app/src/main/res/layout/fragment_backup_data.xml

@@ -59,7 +59,7 @@
 								android:layout_width="128dp"
 								android:layout_height="128dp"
 								android:src="@drawable/ic_circle_white"
-								android:tint="@color/md_theme_light_surfaceVariant"
+								android:tint="?attr/colorSecondaryContainer"
 								android:importantForAccessibility="no"/>
 
 							<ImageView
@@ -69,7 +69,7 @@
 								android:adjustViewBounds="true"
 								android:src="@drawable/ic_backup_data_black_192dp"
 								android:importantForAccessibility="no"
-								app:tint="@color/md_theme_light_onSurfaceVariant" />
+								app:tint="?attr/colorOnSecondaryContainer" />
 
 						</FrameLayout>
 

+ 1 - 1
app/src/main/res/layout/fragment_backup_threema_safe.xml

@@ -230,7 +230,7 @@
 					android:layout_width="180dp"
 					android:layout_height="180dp"
 					android:src="@drawable/ic_circle_white"
-					android:tint="@color/md_theme_light_surfaceVariant"/>
+					android:tint="?attr/colorSecondaryContainer"/>
 
 				<ImageView
 					android:layout_width="180dp"

+ 9 - 1
app/src/main/res/layout/fragment_image_preview.xml

@@ -13,8 +13,16 @@
 			android:adjustViewBounds="true"
 			android:scaleType="fitCenter"/>
 
+	<ImageView
+		android:id="@+id/image_view"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:adjustViewBounds="true"
+		android:scaleType="fitCenter"
+		android:contentDescription="@string/image_placeholder" />
+
 	<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-		android:id="@+id/thumbnail_view"
+		android:id="@+id/scale_image_view"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent"
 		android:adjustViewBounds="true"

+ 2 - 2
app/src/main/res/layout/item_media_attach_gallery.xml

@@ -34,7 +34,7 @@
 			/>
 
 		<LinearLayout
-			android:id="@+id/gif_marker_container"
+			android:id="@+id/animated_format_label_container"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:layout_gravity="bottom"
@@ -44,7 +44,7 @@
 			android:visibility="gone">
 
 			<ImageView
-				android:id="@+id/gif_icon"
+				android:id="@+id/animated_format_label_icon"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
 				android:contentDescription="@string/attach_gif"

+ 2 - 2
app/src/main/res/layout/item_media_gallery.xml

@@ -28,7 +28,7 @@
             android:visibility="gone" />
 
         <LinearLayout
-            android:id="@+id/gif_marker_container"
+            android:id="@+id/animated_format_label_container"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="bottom"
@@ -40,7 +40,7 @@
             android:visibility="gone">
 
             <ImageView
-                android:id="@+id/gif_icon"
+                android:id="@+id/animated_format_label_icon"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@string/attach_gif"

+ 14 - 21
app/src/main/res/layout/item_message_list.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<com.google.android.material.card.MaterialCardView
+<FrameLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
 	style="@style/Threema.CardView.MessageList"
@@ -9,13 +9,9 @@
 	android:layout_height="@dimen/messagelist_item_height"
 	android:clickable="true"
 	android:focusable="true"
-	android:foreground="@drawable/listitem_background_selector"
-	app:cardCornerRadius="@dimen/messagelist_card_corner_radius"
-	app:cardElevation="0dp"
-	app:cardPreventCornerOverlap="false"
-	app:cardUseCompatPadding="false">
+	android:foreground="@drawable/listitem_background_selector">
 
-	<androidx.constraintlayout.widget.ConstraintLayout
+	<RelativeLayout
 		android:id="@+id/list_item_fg"
 		android:layout_width="match_parent"
 		android:layout_height="@dimen/messagelist_item_height"
@@ -27,9 +23,8 @@
 			android:id="@+id/avatar_view"
 			android:layout_width="@dimen/avatar_size_small"
 			android:layout_height="@dimen/avatar_size_small"
-			app:layout_constraintTop_toTopOf="parent"
-			app:layout_constraintBottom_toBottomOf="parent"
-			app:layout_constraintStart_toStartOf="parent"
+			android:layout_centerVertical="true"
+			android:layout_alignParentLeft="true"
 			android:clickable="true"
 			android:contentDescription="@string/show_contact"
 			android:foreground="@drawable/selector_avatar"
@@ -37,12 +32,11 @@
 
 		<RelativeLayout
 			android:id="@+id/content_container"
-			android:layout_width="0dp"
+			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
-			app:layout_constraintTop_toTopOf="parent"
-			app:layout_constraintStart_toEndOf="@id/avatar_view"
-			app:layout_constraintEnd_toStartOf="@id/join_group_call_button"
-			app:layout_constraintBottom_toBottomOf="parent"
+			android:layout_toRightOf="@id/avatar_view"
+			android:layout_toLeftOf="@id/join_group_call_button"
+			android:layout_centerVertical="true"
 			android:layout_marginLeft="@dimen/messagelist_avatar_padding">
 
 			<ImageView
@@ -228,12 +222,11 @@
 			android:minWidth="40dp"
 			android:text="@string/voip_gc_join_call"
 			android:visibility="gone"
-			app:layout_constraintTop_toTopOf="parent"
-			app:layout_constraintBottom_toBottomOf="parent"
-			app:layout_constraintStart_toEndOf="@id/content_container"
-			app:layout_constraintEnd_toEndOf="parent" />
+			android:layout_centerVertical="true"
+			android:layout_alignParentRight="true"
+			/>
 
-	</androidx.constraintlayout.widget.ConstraintLayout>
+	</RelativeLayout>
 
 	<LinearLayout
 		android:layout_width="wrap_content"
@@ -262,4 +255,4 @@
 		android:layout_gravity="right|top"
 		android:contentDescription="@string/private_chat_subject"/>
 
-</com.google.android.material.card.MaterialCardView>
+</FrameLayout>

+ 11 - 2
app/src/main/res/menu/activity_image_paint.xml

@@ -71,7 +71,7 @@
 					android:orderInCategory="40"
 					android:title="@string/brush"
 					app:iconTint="?attr/colorOnSurface"
-					app:showAsAction="always" />
+					app:showAsAction="never" />
 
 				<item
 					android:id="@+id/item_pencil"
@@ -80,7 +80,16 @@
 					android:orderInCategory="60"
 					android:title="@string/pencil"
 					app:iconTint="?attr/colorOnSurface"
-					app:showAsAction="always" />
+					app:showAsAction="never" />
+
+				<item
+					android:id="@+id/item_highlighter"
+					android:enabled="true"
+					android:icon="@drawable/ic_ink_highlighter_outline"
+					android:orderInCategory="80"
+					android:title="@string/highlighter"
+					app:iconTint="?attr/colorOnSurface"
+					app:showAsAction="never" />
 
 			</menu>
 

+ 38 - 6
app/src/main/res/values-be-rBY/strings.xml

@@ -65,7 +65,7 @@
     <string name="prefs_inapp_vibrate">Вібрацыя ў праграме</string>
     <string name="prefs_inapp_vibrate_on">Вібраваць пры атрыманні паведамленняў</string>
     <string name="prefs_inapp_vibrate_off">Ня вібраваць пры атрыманні паведамленняў</string>
-    <string name="workarounds">Патчы</string>
+    <string name="workarounds">Абыходныя шляхі</string>
     <string name="prefs_title_threema_push_switch">Выкарыст. Threema Push</string>
     <string name="prefs_sum_threema_push_on">Выкарыст. функцыю Threema Push для атрым. апавяшч. аб новых паведамленнях</string>
     <string name="prefs_sum_threema_push_off">Правярайце наяўн. новых паведамл. з дапамогай сіст. службы push-паведамл</string>
@@ -665,7 +665,7 @@
     <string name="select_all">Выбраць усе</string>
     <string name="deleting_messages">Выдаленне паведамленняў</string>
     <string name="media_gallery_files">Файлы</string>
-    <string name="prefs_gif_autoplay">Аўтаматыч. прайгр. анім. GIF-файлаў</string>
+    <string name="prefs_gif_autoplay">Аўта-прайграванне анімаваных відарысаў</string>
     <string name="media_gallery_audio">Галасавыя паведамленні</string>
     <string name="action_clone_group">Кланаваць суполку</string>
     <string name="clone_group_message">Будзе створаны клон суполкі з вамі, у якасці адміністратара.  Працягнуць?</string>
@@ -1008,6 +1008,10 @@
     <string name="open_in_maps_app">Адк-ць у праграме \"Мапы\"</string>
     <string name="delete">Выдаліць</string>
     <string name="continue_recording">Працягнуць запіс</string>
+    <string name="whatsnew_title">Што новага ў %1$s 5.2?</string>
+    <string name="whatsnew_headline"><![CDATA[<p>Пазначаныя зорачкай паведамленні: каб лёгка знайсці іх пазней, пазначце важныя паведамленні зорачкай доўгім націскам.</p>
+ <p>Аўтаматычнае выдаленне: вашыя чаты ачышчаюцца аўтаматычна праз пэўны перыяд часу.  Функцыю аўтаматычнага выдалення паведамленняў можна актываваць у кіраванні сховішчам.</p>
+ <p>Шматлікія іншыя паляпшэнні і аптымізацыі.</p>]]></string>
     <string name="tap_to_start">Націсьніце тут, каб запусціць %s.</string>
     <string name="two_years">2 года</string>
     <string name="invalid_backup_path">Нядзейнічае.шлях да рэз.копіі</string>
@@ -1057,7 +1061,6 @@
     <string name="prefs_working_days_enable_title">Правілы для непрац. часу</string>
     <string name="prefs_working_days_enable_sum">Адключыць апавяшчэння і адхіляць званкі ў непрацоўны час</string>
     <string name="work_life_dnd_active">Цяпер непрацоўны час</string>
-    <string name="pencil">Аловак</string>
     <string name="warning">Увага!</string>
     <string name="password_remember_warning">Запомніце, што вы тут уводзіце!  Паколькі %s не захоўвае ніякіх пароляў на серверах, мы не можам дапамагчы вам, калі вы забудзеце PIN-код або пароль.</string>
     <string name="safe_backup_tap_to_restart">Націсніце на апавяшчэнне, каб перазапусціць праграму зараз.</string>
@@ -1146,6 +1149,8 @@
     <string name="notes">Нататкі</string>
     <string name="blur_faces">Размыццё твару</string>
     <string name="brush">Пэндзаль</string>
+    <string name="pencil">Аловак</string>
+    <string name="highlighter">Хайлайтер</string>
     <string name="error_detecting_faces">Памылка распазнання твараў</string>
     <string name="no_faces_detected">Твары не распазнаныя</string>
     <string name="smiley">Смайл</string>
@@ -1453,7 +1458,7 @@
     <string name="permission_microphone">Мікрафон</string>
     <string name="permission_read_phone_state">Тэлефон</string>
     <string name="permission_enable_in_settings_rationale">Уключыце дазвол у наладах.  Выберыце «Дазволы», а потым дазвольце «%s».</string>
-	<string name="group_name">Назва суполкі</string>
+    <string name="group_name">Назва суполкі</string>
     <string name="star_message">Паведамленне з зоркай</string>
     <string name="starred_message">Пазначанае паведамленне</string>
     <string name="starred">З зоркай</string>
@@ -1478,7 +1483,7 @@
     <string name="keep_messages">Захоўваць паведамленні для:</string>
     <string name="clean_up_manually">Ачысціць ўручную</string>
     <string name="forever">Назаўжды</string>
-    <string name="autodelete_explain">Калі ўсталявана што-небудзь іншае, чым \"назаўсёды\", уваходныя і выходныя паведамленні будуць захоўвацца толькі на працягу вызначанага часу, а потым беззваротна выдаляцца.  Гэта ўключае ў сябе непрачытаныя і неадпраўленыя паведамленні.  Паведамленні ў групах нататак і пазначаныя зорачкай ніколі не будуць выдалены.  Працэс выдалення адбываецца перыядычна і неабавязкова адразу пасля актывацыі.</string>
+    <string name="autodelete_explain">Калі ўсталявана што-небудзь іншае, акрамя «назаўсёды», уваходныя і выходныя паведамленні будуць беззваротна выдаляцца праз зададзены прамежак часу.  Гэта ўключае ў сябе непрачытаныя і неадпраўленыя паведамленні.  Аднак паведамленні ў  суполках нататак і пазначаныя зорачкай паведамленні не будуць выдалены.  Працэс выдалення запускаецца перыядычна і неабавязкова адразу пасля актывацыі.</string>
     <string name="autodelete_confirm">Паведамленні будуць выдаляцца аўтаматычна пасля %s.  Працягнуць?</string>
     <string name="autodelete_activated">Аўтаматычнае выдаленне актывавана.</string>
     <string name="autodelete_disabled">Аўтаматычнае выдаленне адключана.</string>
@@ -1491,6 +1496,28 @@
     <string name="group_dissolved_notice">Гэтая суполка была распушчана і больш не можа выкарыстоўвацца.</string>
     <string name="directory_request_failed">Не ўдалося запытаць каталог.  Паспрабуй яшчэ.</string>
     <string name="add_shortcut_exists">Ярлык для гэтай мэты ўжо існуе.</string>
+    <string name="work_lost_credentials_help">Забыліся ўліковыя даныя?  Звярніцеся да адміністратара %1$s.</string>
+    <string name="problemsolver_intro">Некаторыя праблемы канфігурацыі сістэмы, магчыма, перашкаджаюць карэктнай працы %s.  Вырашыце гэтыя праблемы, выконваючы інструкцыі ніжэй.</string>
+    <string name="problemsolver_title">Выправіць праблемы канфігурацыі</string>
+    <string name="problemsolver_title_background">Фонавае выкарыстанне абмежавана</string>
+    <string name="problemsolver_title_background_data">Фонавыя дадзеныя абмежаваныя</string>
+    <string name="problemsolver_title_notifications">Апавяшчэнні адключаны</string>
+    <string name="problemsolver_title_fullscreen_notifications">Поўнаэкранныя апавяшчэнні забароненыя</string>
+    <string name="problemsolver_explain_fullscreen_notifications">Праграме забаронена паказваць поўнаэкранныя апавяшчэнні.  Вы можаце прапусціць уваходныя выклікі, калі прылада заблакіравана.  Каб выправіць гэта, актывуйце «Дазволіць поўнаэкранныя апавяшчэнні».</string>
+    <string name="problemsolver_explain_notifications">Апавяшчэнні цалкам адключаны для гэтай праграмы.  Вы не будзеце інфармаваны аб уваходных паведамленнях або выкліках, і праграма можа працаваць не так, як чакалася.  Каб выправіць гэта, актывуйце «Усе апавяшчэнні».</string>
+    <string name="problemsolver_explain_background_data">Праграма не можа выкарыстоўваць мабільную перадачу дадзеных у фонавым рэжыме.  Вы не будзеце атрымліваць інфармацыю аб уваходных паведамленнях, пакуль вы актыўна не адкрыеце праграму.  Каб выправіць гэта, актывуйце «Фонавыя дадзеныя».</string>
+    <string name="problemsolver_explain_background">Фонавае выкарыстанне праграмы абмежавана.  Вы не будзеце атрымліваць ніякіх апавяшчэнняў, і многія функцыі не будуць працаваць належным чынам.  Каб выправіць гэта, націсніце «Выкарыстанне батарэі праграмы» і выберыце «Без абмежаванняў».</string>
+    <string name="problemsolver_info_text">Назвы некаторых параметраў сістэмных налад, згаданых вышэй, могуць адрознівацца ў залежнасці ад версіі Android і вытворцы прылады.</string>
+    <string name="problemsolver_title_app_battery_usgae_optimized">Выкарыстанне акумулятара усталявана як «аптымізаванае»</string>
+    <string name="problemsolver_explain_app_battery_usgae_optimized">Праграме не дазваляецца падтрымліваць пастаяннае злучэнне з серверамі, якое патрабуецца для працы Threema Push.  Вы не будзеце атрымліваць інфармацыю аб новых паведамленнях, калі праграма працуе ў фонавым рэжыме.  Каб выправіць гэта, націсніце «Выкарыстанне батарэі праграмы» і выберыце «Без абмежаванняў».</string>
+    <string name="problemsolver_to_settings">Да налад</string>
+    <string name="prefs_advanced_options">Дадатковыя параметры</string>
+    <string name="push_service">Паслуга Push</string>
+    <string name="zoom_out">Зменшыць</string>
+    <string name="zoom_in">Павялічыць</string>
+    <string name="edit_and_send">Адрэдагуйце і адпраўце</string>
+    <string name="media_gallery_animated_webps">Аніміраваныя WebP</string>
+    <string name="unsupported_image_type">Тып відарыса не падтрымліваецца: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">%d кантактаў</item>
         <item quantity="many">%d кантакты</item>
@@ -1527,6 +1554,12 @@
         <item quantity="one">%d новае паведамленне</item>
         <item quantity="other">%d новых паведамленняў</item>
     </plurals>
+    <plurals name="unread_messages">
+        <item quantity="few">%d непрачытаных паведамленняў</item>
+        <item quantity="many">%d непрачытаных паведамленняў</item>
+        <item quantity="one">%d непрачытанае паведамленне</item>
+        <item quantity="other">%d непрачытаных паведамленняў</item>
+    </plurals>
     <plurals name="file_saved">
         <item quantity="few">%d файлы паспяхова захаваны.</item>
         <item quantity="many">%d файлы паспяхова захаваны.</item>
@@ -1632,5 +1665,4 @@
         <item quantity="one">%d удзельнік суполкі</item>
         <item quantity="other">%d удзельнікаў суполкі</item>
     </plurals>
-    <string name="edit_and_send">Рэдагаваць</string>
 </resources>

+ 20 - 3
app/src/main/res/values-cs/strings.xml

@@ -65,6 +65,7 @@
     <string name="prefs_inapp_vibrate">Vibrace v aplikaci</string>
     <string name="prefs_inapp_vibrate_on">Vibrovat po přijetí zprávy</string>
     <string name="prefs_inapp_vibrate_off">Nevibrovat po přijetí zprávy</string>
+    <string name="workarounds">Náhradní řešení</string>
     <string name="prefs_title_threema_push_switch">Použít službu Threema Push</string>
     <string name="prefs_sum_threema_push_on">Používat službu Threema Push k upozorňování na nové zprávy</string>
     <string name="prefs_sum_threema_push_off">Zjišťovat nové zprávy použitím systémové služby push</string>
@@ -667,7 +668,7 @@ přátelům vás automaticky najít, pokud vás mají v adresáři svého telef
     <string name="select_all">Vybrat vše</string>
     <string name="deleting_messages">Odstraňování zpráv</string>
     <string name="media_gallery_files">Soubory</string>
-    <string name="prefs_gif_autoplay">Automaticky přehrávat animované GIFy</string>
+    <string name="prefs_gif_autoplay">Automaticky přehrávat animované obrázky</string>
     <string name="media_gallery_audio">Hlasové zprávy</string>
     <string name="action_clone_group">Kopírovat skupinu</string>
     <string name="clone_group_message">Tímto vytvoříte kopii této skupiny, ve které budete správcem. Přejete si pokračovat?</string>
@@ -1064,7 +1065,6 @@ přátelům vás automaticky najít, pokud vás mají v adresáři svého telef
     <string name="prefs_working_days_enable_title">Zásady po pracovní době</string>
     <string name="prefs_working_days_enable_sum">Zakázat oznámení a odmítat hovory mimo pracovní dobu</string>
     <string name="work_life_dnd_active">Pravidla po pracovní době aktivní</string>
-    <string name="pencil">Tužka</string>
     <string name="warning">Varování</string>
     <string name="password_remember_warning">Pamatujte si, co zde zadáte! Vzhledem k tomu, že aplikace %s neukládá žádná hesla na serverech, nemůžeme vám pomoci, pokud zapomenete svůj PIN nebo heslo.</string>
     <string name="safe_backup_tap_to_restart">Klepnutím na zobrazené oznámení restartujete aplikaci ihned. Jestliže oznámení nevidíte, přejeďte prosím na vašem zařízení dolů přes panel oznámení.</string>
@@ -1153,6 +1153,8 @@ přátelům vás automaticky najít, pokud vás mají v adresáři svého telef
     <string name="notes">Poznámky</string>
     <string name="blur_faces">Rozmazat tváře</string>
     <string name="brush">Štětec</string>
+    <string name="pencil">Tužka</string>
+    <string name="highlighter">Zvýrazňovač</string>
     <string name="error_detecting_faces">Chyba během detekce tváří</string>
     <string name="no_faces_detected">Nebyly nalezeny žádné tváře</string>
     <string name="smiley">Smajlík</string>
@@ -1495,11 +1497,27 @@ přátelům vás automaticky najít, pokud vás mají v adresáři svého telef
     <string name="directory_request_failed">Požadavek složky selhal. Zkuste to znovu.</string>
     <string name="add_shortcut_exists">Zástupce pro tento cíl již existuje.</string>
     <string name="work_lost_credentials_help">Zapomněli jste své přihlašovací údaje? Obraťte se na svého správce %1$s.</string>
+    <string name="problemsolver_intro">Nějaké problémy s nastavením systému pravděpodobně způsobují, že aplikace %s nefunguje správně. Opravte prosím tyto problémy podle níže uvedených pokynů.</string>
+    <string name="problemsolver_title">Opravte problémy s nastavením</string>
+    <string name="problemsolver_title_background">Použití na pozadí je omezeno</string>
     <string name="problemsolver_title_background_data">Data na pozadí omezena</string>
     <string name="problemsolver_title_notifications">Oznámení vypnuta</string>
+    <string name="problemsolver_title_fullscreen_notifications">Oznámení na celou obrazovku jsou zakázána</string>
+    <string name="problemsolver_explain_fullscreen_notifications">Aplikaci není dovoleno zobrazovat oznámení na celou obrazovku. Když je zařízení zamknuté, můžete zmeškat příchozí hovory. Chcete‑li tento problém opravit, aktivujte možnost „Povolit oznámení na celou obrazovku“.</string>
+    <string name="problemsolver_explain_notifications">Oznámení jsou pro tuto aplikaci zcela zakázána. Nebudete informováni o příchozích zprávách nebo hovorech a aplikace nemusí fungovat podle očekávání. Chcete‑li tento problém opravit, aktivujte možnost „Všechna oznámení“.</string>
+    <string name="problemsolver_explain_background_data">Aplikace nemůže využívat mobilní data, když je na pozadí. Nebudete informováni o příchozích zprávách, pokud aplikaci aktivně neotevřete. Chcete‑li tento problém opravit, aktivujte možnost „Data na pozadí“.</string>
+    <string name="problemsolver_explain_background">Využití aplikace na pozadí je omezeno. Nebudete dostávat žádná oznámení a mnoho funkcí nebude fungovat podle očekávání. Chcete‑li tento problém opravit, klepněte na „Využití baterie“ a vyberte „Neomezeno“.</string>
+    <string name="problemsolver_info_text">Pojmenování některých voleb systémových nastavení uvedených výše se může lišit v závislosti na verzi systému Android a výrobci zařízení.</string>
+    <string name="problemsolver_title_app_battery_usgae_optimized">Využití baterie aplikace nastaveno na „optimalizované“</string>
+    <string name="problemsolver_explain_app_battery_usgae_optimized">Aplikaci není dovoleno udržovat trvalé připojení k serverům, které je nutné pro fungování Threema Push. Když bude aplikace na pozadí, nebudete informováni o nových zprávách. Chcete‑li tento problém opravit, klepněte na „Využití baterie“ a vyberte „Neomezeno“.</string>
     <string name="problemsolver_to_settings">Do nastavení</string>
     <string name="prefs_advanced_options">Pokročilé možnosti</string>
     <string name="push_service">Služba push</string>
+    <string name="zoom_out">Oddálit</string>
+    <string name="zoom_in">Přiblížit</string>
+    <string name="edit_and_send">Upravit a odeslat</string>
+    <string name="media_gallery_animated_webps">Animované WebP</string>
+    <string name="unsupported_image_type">Nepodporovaný typ obrázku: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">%d kontakty</item>
         <item quantity="many">%d kontaktů</item>
@@ -1656,5 +1674,4 @@ přátelům vás automaticky najít, pokud vás mají v adresáři svého telef
         <item quantity="one">%d člen skupiny</item>
         <item quantity="other">%d členů skupiny</item>
     </plurals>
-    <string name="edit_and_send">Upravit</string>
 </resources>

+ 6 - 3
app/src/main/res/values-de/strings.xml

@@ -775,7 +775,7 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="select_all">Alle auswählen</string>
 	<string name="deleting_messages">Nachrichten werden gelöscht</string>
 	<string name="media_gallery_files">Dateien</string>
-	<string name="prefs_gif_autoplay">Anim-GIFs autom. abspielen</string>
+	<string name="prefs_gif_autoplay">Animierte Bilder autom. abspielen</string>
 	<string name="media_gallery_audio">Sprachnachrichten</string>
 	<string name="action_clone_group">Gruppe klonen</string>
 	<string name="clone_group_message">Es wird eine Kopie der aktuellen Gruppe mit Ihnen als Administrator erstellt. Fortfahren?</string>
@@ -1176,7 +1176,6 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="prefs_working_days_enable_title">Ruhezeit-Regelung</string>
 	<string name="prefs_working_days_enable_sum">Ausserhalb der Arbeitszeit keine Benachrichtigungen anzeigen und Threema-Anrufe ablehnen</string>
 	<string name="work_life_dnd_active">Ruhezeit aktiv</string>
-	<string name="pencil">Bleistift</string>
 	<string name="warning">Warnung</string>
 	<string name="password_remember_warning">Merken Sie sich, was Sie hier eingeben! Da %s keine Passwörter auf Servern speichert, können wir Ihnen nicht helfen, wenn Sie die PIN oder die Passphrase vergessen haben.</string>
 	<string name="safe_backup_tap_to_restart">Tippen Sie auf die angezeigte System-Benachrichtigung, um die App jetzt neu zu starten. Wenn Sie die Benachrichtigung nicht sehen, ziehen Sie bitte die Benachrichtigungsleiste Ihres Handys herunter.</string>
@@ -1258,7 +1257,9 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="group_create_no_members">Sind Sie sicher, dass Sie eine leere Gruppe erstellen möchten?</string>
 	<string name="notes">Notizen</string>
 	<string name="blur_faces">Gesichter verwischen</string>
+	<string name="pencil">Bleistift</string>
 	<string name="brush">Pinsel</string>
+	<string name="highlighter">Textmarker</string>
 	<string name="error_detecting_faces">Bei der Gesichtserkennung ist ein Fehler aufgetreten</string>
 	<string name="no_faces_detected">Keine Gesichter gefunden</string>
 	<string name="smiley">Smiley</string>
@@ -1623,6 +1624,9 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="push_service">Push-Dienst</string>
     <string name="zoom_out">Vergrössern</string>
 	<string name="zoom_in">Verkleinern</string>
+	<string name="edit_and_send">Bearbeiten &amp; senden</string>
+	<string name="media_gallery_animated_webps">Animierte WebPs</string>
+	<string name="unsupported_image_type">Nicht unterstütztes Bildformat: %s</string>
 	<plurals name="contacts_counter_label">
 		<item quantity="one">%d Kontakt</item>
 		<item quantity="other">%d Kontakte</item>
@@ -1735,5 +1739,4 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 		<item quantity="one">%d Gruppenmitglied</item>
 		<item quantity="other">%d Gruppenmitglieder</item>
 	</plurals>
-    <string name="edit_and_send">Bearbeiten und senden</string>
 </resources>

+ 7 - 2
app/src/main/res/values-es/strings.xml

@@ -1072,7 +1072,6 @@ almacenado.</string>
     <string name="prefs_working_days_enable_title">Política fuera de horario</string>
     <string name="prefs_working_days_enable_sum">Deshabilite las notificaciones y rechace llamadas fuera del horario laborable.</string>
     <string name="work_life_dnd_active">Fuera de horario activo</string>
-    <string name="pencil">Lápiz</string>
     <string name="warning">¡Atención!</string>
     <string name="password_remember_warning">¡Recuerda lo que escribes aquí! Dado que %s no almacena contraseñas en los servidores, no podemos ayudarte si olvidas tu PIN o frase de contraseña.</string>
     <string name="safe_backup_tap_to_restart">Pulse en la notificación para reiniciar la aplicación ahora.</string>
@@ -1161,6 +1160,8 @@ almacenado.</string>
     <string name="notes">Notas</string>
     <string name="blur_faces">Cara difuminada</string>
     <string name="brush">Pincel</string>
+    <string name="pencil">Lápiz</string>
+    <string name="highlighter">Subrayador</string>
     <string name="error_detecting_faces">Error al detectar caras</string>
     <string name="no_faces_detected">No se detectan caras</string>
     <string name="smiley">Sonrisa</string>
@@ -1520,6 +1521,11 @@ almacenado.</string>
     <string name="problemsolver_to_settings">A la configuración</string>
     <string name="prefs_advanced_options">Opciones avanzadas</string>
     <string name="push_service">Servicio Push</string>
+    <string name="zoom_out">Alejar</string>
+    <string name="zoom_in">Acercar</string>
+    <string name="edit_and_send">Editar y enviar</string>
+    <string name="media_gallery_animated_webps">WebP animados</string>
+    <string name="unsupported_image_type">Tipo de imagen no admitido: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contacto</item>
         <item quantity="other">%d contactos</item>
@@ -1596,5 +1602,4 @@ almacenado.</string>
         <item quantity="one">%d miembro del grupo</item>
         <item quantity="other">%d miembros del grupo</item>
     </plurals>
-    <string name="edit_and_send">Editar</string>
 </resources>

+ 26 - 21
app/src/main/res/values-fr/strings.xml

@@ -1064,7 +1064,6 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="prefs_working_days_enable_title">Politique temps libre</string>
     <string name="prefs_working_days_enable_sum">Désactiver les notifications et rejeter les appels en dehors des heures de travail</string>
     <string name="work_life_dnd_active">Temps libre activé</string>
-    <string name="pencil">Crayon</string>
     <string name="warning">Attention !</string>
     <string name="password_remember_warning">Souvenez-vous de ce que vous entrez ici ! Comme %s ne stocke pas les mots de passe sur les serveurs, nous ne pouvons pas vous aider si vous oubliez votre code NIP ou votre phrase secrète.</string>
     <string name="safe_backup_tap_to_restart">Tapez sur la notification pour redémarrer l\'application maintenant.</string>
@@ -1153,6 +1152,8 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="notes">Notes</string>
     <string name="blur_faces">Brouiller les visages</string>
     <string name="brush">Pinceau</string>
+    <string name="pencil">Crayon</string>
+    <string name="highlighter">Surbrillance</string>
     <string name="error_detecting_faces">Erreur de détection des visages</string>
     <string name="no_faces_detected">Aucun visage détecté</string>
     <string name="smiley">Émoticône</string>
@@ -1511,81 +1512,85 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="problemsolver_to_settings">Aux paramètres</string>
     <string name="prefs_advanced_options">Options avancées</string>
     <string name="push_service">Service Push</string>
-    <plurals name="contacts_counter_label" tools:ignore="MissingQuantity">
+    <string name="zoom_out">Zoom arrière</string>
+    <string name="zoom_in">Zoom avant</string>
+    <string name="edit_and_send">Modifier et envoyer</string>
+    <string name="media_gallery_animated_webps">WebP animés</string>
+    <string name="unsupported_image_type">Type d\'image non pris en charge : %s</string>
+    <plurals name="contacts_counter_label">
         <item quantity="one">%d contact</item>
         <item quantity="other">%d contacts</item>
     </plurals>
-    <plurals name="really_delete_thread_message" tools:ignore="MissingQuantity">
+    <plurals name="really_delete_thread_message">
         <item quantity="one" tools:ignore="ImpliedQuantity">Souhaitez-vous vraiment supprimer ce fil?</item>
         <item quantity="other">Souhaitez-vous vraiment supprimer %d fils?</item>
     </plurals>
-    <plurals name="sending_message_failed" tools:ignore="MissingQuantity">
+    <plurals name="sending_message_failed">
         <item quantity="one" tools:ignore="ImpliedQuantity">Impossible d\'envoyer un message</item>
         <item quantity="other">Impossible d\'envoyer %1$d messages</item>
     </plurals>
-    <plurals name="selection_counter_label" tools:ignore="MissingQuantity">
+    <plurals name="selection_counter_label">
         <item quantity="one">%d image sélectionnée</item>
         <item quantity="other">%d images sélectionnées</item>
     </plurals>
-    <plurals name="message_copied" tools:ignore="MissingQuantity">
+    <plurals name="message_copied">
         <item quantity="one" tools:ignore="ImpliedQuantity">Message copié</item>
         <item quantity="other">Messages copiés</item>
     </plurals>
-    <plurals name="new_messages" tools:ignore="MissingQuantity">
+    <plurals name="new_messages">
         <item quantity="one">%d nouveau message</item>
         <item quantity="other">%d nouveaux messages</item>
     </plurals>
-    <plurals name="file_saved" tools:ignore="MissingQuantity">
+    <plurals name="file_saved">
         <item quantity="one">%d fichier enregistré avec succès.</item>
         <item quantity="other">%d fichiers enregistrés avec succès.</item>
     </plurals>
-    <plurals name="saving_media"  tools:ignore="MissingQuantity">
+    <plurals name="saving_media">
         <item quantity="one" tools:ignore="ImpliedQuantity">Enregistrement du média</item>
         <item quantity="other">Enregistrement de %d fichiers média</item>
     </plurals>
-    <plurals name="ballot_really_delete" tools:ignore="MissingQuantity">
+    <plurals name="ballot_really_delete">
         <item quantity="one" tools:ignore="ImpliedQuantity">Retirer l\'enquête</item>
         <item quantity="other">Retirer les enquêtes</item>
     </plurals>
-    <plurals name="ballot_really_delete_text" tools:ignore="MissingQuantity">
+    <plurals name="ballot_really_delete_text">
         <item quantity="one">Voulez-vous vraiment supprimer %1$d enquête ? Vous ne pourrez pas récupérer les votes.</item>
         <item quantity="other">Voulez-vous vraiment supprimer %1$d enquêtes ? Vous ne pourrez pas récupérer les votes.</item>
     </plurals>
-    <plurals name="message_deleted" tools:ignore="MissingQuantity">
+    <plurals name="message_deleted">
         <item quantity="one">%d message supprimé</item>
         <item quantity="other">%d messages supprimés</item>
     </plurals>
-    <plurals name="notifications_for_x_hours" tools:ignore="MissingQuantity">
+    <plurals name="notifications_for_x_hours">
         <item quantity="one">Pendant %d heure</item>
         <item quantity="other">Pendant %d heures</item>
     </plurals>
-    <plurals name="some_contacts_not_deleted" tools:ignore="MissingQuantity">
+    <plurals name="some_contacts_not_deleted">
         <item quantity="one">%d contact n\'a pas pu être supprimé, car il fait encore partie d\'un groupe.</item>
         <item quantity="other">%d contacts n\'ont pas pu être supprimés, car il font encore partie d\'un groupe.</item>
     </plurals>
-    <plurals name="message_archived" tools:ignore="MissingQuantity">
+    <plurals name="message_archived">
         <item quantity="one">%d chat archivé</item>
         <item quantity="other">%d chats archivés</item>
     </plurals>
-    <plurals name="webclient_running_sessions" tools:ignore="MissingQuantity">
+    <plurals name="webclient_running_sessions">
         <item quantity="one">%d session en cours d\'exécution</item>
         <item quantity="other">%d sessions en cours d\'exécution</item>
     </plurals>
-    <plurals name="chat_deleted" tools:ignore="MissingQuantity">
+    <plurals name="chat_deleted">
         <item quantity="one">%d chat supprimé.</item>
         <item quantity="other">%d chats supprimés.</item>
     </plurals>
-    <plurals name="forward_security_messages_skipped" tools:ignore="MissingQuantity">
+    <plurals name="forward_security_messages_skipped">
         <item quantity="one" tools:ignore="ImpliedQuantity">Un message a été perdu depuis le dernier message.</item>
         <item quantity="other">%d messages ont été perdus depuis le dernier message.</item>
     </plurals>
-    <plurals name="delete_messages" tools:ignore="MissingQuantity">
+    <plurals name="delete_messages">
         <item quantity="one">Supprimer %d message sur cet appareil ?</item>
         <item quantity="other">Supprimer %d messages sur cet appareil ?</item>
     </plurals>
-    <plurals name="number_of_group_members" tools:ignore="MissingQuantity">
+    <plurals name="number_of_group_members">
         <item quantity="one">%d membre de groupe</item>
         <item quantity="other">%d membres de groupes</item>
     </plurals>
-    <string name="edit_and_send">Modifier</string>
 </resources>

+ 8 - 3
app/src/main/res/values-it/strings.xml

@@ -685,7 +685,7 @@ automaticamente in caso di inattività dopo un intervallo predefinito (solo cara
     <string name="select_all">Seleziona tutto</string>
     <string name="deleting_messages">Eliminazione messaggi</string>
     <string name="media_gallery_files">File</string>
-    <string name="prefs_gif_autoplay">Riproduci automaticamente le GIF animate</string>
+    <string name="prefs_gif_autoplay">Riproduci autom. le immagini animate</string>
     <string name="media_gallery_audio">Messaggi vocali</string>
     <string name="action_clone_group">Clona gruppo</string>
     <string name="clone_group_message">Verrà creato un gruppo clone che ti vedrà come amministratore. Vuoi continuare?</string>
@@ -1082,7 +1082,6 @@ Numerosi altri miglioramenti e ottimizzazioni.</p>]]></string>
     <string name="prefs_working_days_enable_title">Politica straordinari</string>
     <string name="prefs_working_days_enable_sum">Disattiva notifiche e rifiuta telefonate al di fuori dell\'orario di lavoro</string>
     <string name="work_life_dnd_active">Attivo straordinari</string>
-    <string name="pencil">Matita</string>
     <string name="warning">Attenzione!</string>
     <string name="password_remember_warning">Ricordati cosa inserisci qui! Poiché %s non memorizza le password sui server, non possiamo aiutarti se dimentichi il PIN o la password.</string>
     <string name="safe_backup_tap_to_restart">Toccare la notifica per riavviare l\'applicazione.</string>
@@ -1171,6 +1170,8 @@ Numerosi altri miglioramenti e ottimizzazioni.</p>]]></string>
     <string name="notes">Appunti</string>
     <string name="blur_faces">Sfocare i visi</string>
     <string name="brush">Pennello</string>
+    <string name="pencil">Matita</string>
+    <string name="highlighter">Evidenziatore</string>
     <string name="error_detecting_faces">Si è verificato un errore durante il riconoscimento facciale</string>
     <string name="no_faces_detected">Nessun viso trovato</string>
     <string name="smiley">Smiley</string>
@@ -1529,6 +1530,11 @@ Numerosi altri miglioramenti e ottimizzazioni.</p>]]></string>
     <string name="problemsolver_to_settings">Alle impostazioni</string>
     <string name="prefs_advanced_options">Opzioni avanzate</string>
     <string name="push_service">Servizio push</string>
+    <string name="zoom_out">Ingrandisci</string>
+    <string name="zoom_in">Diminuisci</string>
+    <string name="edit_and_send">Modifica e invia</string>
+    <string name="media_gallery_animated_webps">WebP animati</string>
+    <string name="unsupported_image_type">Tipo di immagine non supportato: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contatto</item>
         <item quantity="other">%d contatti</item>
@@ -1617,5 +1623,4 @@ Numerosi altri miglioramenti e ottimizzazioni.</p>]]></string>
         <item quantity="one">%d membro del gruppo</item>
         <item quantity="other">%d membri del gruppo</item>
     </plurals>
-    <string name="edit_and_send">Modifica</string>
 </resources>

+ 7 - 2
app/src/main/res/values-nl-rNL/strings.xml

@@ -1066,7 +1066,6 @@ Weet u zeker dat u Threema anoniem wil gebruiken?</string>
     <string name="prefs_working_days_enable_title">Buiten bedrijfstijden</string>
     <string name="prefs_working_days_enable_sum">Meldingen uitschakelen en oproepen buiten kantooruren afwijzen</string>
     <string name="work_life_dnd_active">Actief buiten kantooruren</string>
-    <string name="pencil">Potlood</string>
     <string name="warning">Waarschuwing!</string>
     <string name="password_remember_warning">Onthoud wat je hier binnenkomt! Aangezien %s geen wachtwoorden opslaat op servers, kunnen we u niet helpen als u uw PIN of passphrase vergeet.</string>
     <string name="safe_backup_tap_to_restart">Tik op de melding om de app nu opnieuw te starten.</string>
@@ -1155,6 +1154,8 @@ Weet u zeker dat u Threema anoniem wil gebruiken?</string>
     <string name="notes">Notities</string>
     <string name="blur_faces">Gezichten vervagen</string>
     <string name="brush">Penseel</string>
+    <string name="pencil">Potlood</string>
+    <string name="highlighter">Markeerstift</string>
     <string name="error_detecting_faces">Fout tijdens detecteren van gezichten</string>
     <string name="no_faces_detected">Geen gezichten gedetecteerd</string>
     <string name="smiley">Smiley</string>
@@ -1513,6 +1514,11 @@ Weet u zeker dat u Threema anoniem wil gebruiken?</string>
     <string name="problemsolver_to_settings">Naar de instellingen</string>
     <string name="prefs_advanced_options">Geavanceerde opties</string>
     <string name="push_service">Pushdienst</string>
+    <string name="zoom_out">Uitzoomen</string>
+    <string name="zoom_in">Inzoomen</string>
+    <string name="edit_and_send">Bewerken en verzenden</string>
+    <string name="media_gallery_animated_webps">Geanimeerde WebP\'s</string>
+    <string name="unsupported_image_type">Niet-ondersteund afbeeldingstype: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contactpersoon</item>
         <item quantity="other">%d contactpersonen</item>
@@ -1589,5 +1595,4 @@ Weet u zeker dat u Threema anoniem wil gebruiken?</string>
         <item quantity="one">%d groepslid</item>
         <item quantity="other">%d groepsleden</item>
     </plurals>
-    <string name="edit_and_send">Bewerken</string>
 </resources>

+ 12 - 3
app/src/main/res/values-no/strings.xml

@@ -664,7 +664,7 @@ http://www.7-zip.org eller https://itunes.apple.com/us/app/the-unarchiver/id4254
     <string name="select_all">Velg alle</string>
     <string name="deleting_messages">Sletter meldinger</string>
     <string name="media_gallery_files">Filer</string>
-    <string name="prefs_gif_autoplay">Autoavspill animerte GIF-er</string>
+    <string name="prefs_gif_autoplay">Autoavspill animerte bilder</string>
     <string name="media_gallery_audio">Lydmeldinger</string>
     <string name="action_clone_group">Klone gruppe</string>
     <string name="clone_group_message">Det blir laget en klone av denne gruppen, med deg som administrator. Vil du fortsette?</string>
@@ -1065,7 +1065,6 @@ http://www.7-zip.org eller https://itunes.apple.com/us/app/the-unarchiver/id4254
     <string name="prefs_working_days_enable_title">Regler utenom arbeidstid</string>
     <string name="prefs_working_days_enable_sum">Slå av varsler og avvis anrop utenom arbeidstid</string>
     <string name="work_life_dnd_active">Utenom arbiedstid: Aktiv</string>
-    <string name="pencil">Blyant</string>
     <string name="warning">Varsel</string>
     <string name="password_remember_warning">Husk det du skriver inn her! Fordi %s ikke lagrer passord på serveren, så kan vi ikke hjelpe om du glemmer passord eller PIN-kode.</string>
     <string name="safe_backup_tap_to_restart">Trykk på system-varselet for å restarte appen nå. Om du ikke kan se varselet, vennligst sveip ned fra telefonens varselområde.</string>
@@ -1154,6 +1153,8 @@ http://www.7-zip.org eller https://itunes.apple.com/us/app/the-unarchiver/id4254
     <string name="notes">Notater</string>
     <string name="blur_faces">Gjør uskarpt</string>
     <string name="brush">Børste</string>
+    <string name="pencil">Blyant</string>
+    <string name="highlighter">Markeringstusj</string>
     <string name="error_detecting_faces">Feil ved gjenkjenning av ansikter</string>
     <string name="no_faces_detected">Ingen ansikter gjenkjent</string>
     <string name="smiley">Smilefjes</string>
@@ -1520,6 +1521,11 @@ Om du ikke har brukt din Threema ID på en annen enhet eller brukt en eldre vers
     <string name="problemsolver_to_settings">Til innstillinger</string>
     <string name="prefs_advanced_options">Avanserte innstillinger</string>
     <string name="push_service">Push-tjeneste</string>
+    <string name="zoom_out">Zoom ut</string>
+    <string name="zoom_in">Zoom inn</string>
+    <string name="edit_and_send">Rediger og send</string>
+    <string name="media_gallery_animated_webps">Animerte WebP-er</string>
+    <string name="unsupported_image_type">Ustøttet bildeformat: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kontakt</item>
         <item quantity="other">%d kontakter</item>
@@ -1544,6 +1550,10 @@ Om du ikke har brukt din Threema ID på en annen enhet eller brukt en eldre vers
         <item quantity="one">%d ny melding</item>
         <item quantity="other">%d nye meldinger</item>
     </plurals>
+    <plurals name="unread_messages">
+        <item quantity="one">%d ulest melding</item>
+        <item quantity="other">%d uleste meldinger</item>
+    </plurals>
     <plurals name="file_saved">
         <item quantity="one">%d fil lagret vellykket</item>
         <item quantity="other">%d filer lagret vellykket</item>
@@ -1622,5 +1632,4 @@ Tidligere mottatt forespørsler kan likevel fortsatt bli avvist eller godtatt.</
         <item quantity="one">%d gruppemedlem</item>
         <item quantity="other">%d gruppemedlemmer</item>
     </plurals>
-    <string name="edit_and_send">Rediger</string>
 </resources>

+ 7 - 2
app/src/main/res/values-pl/strings.xml

@@ -1080,7 +1080,6 @@ anonimowo?</string>
     <string name="prefs_working_days_enable_title">Zasady po godzinach</string>
     <string name="prefs_working_days_enable_sum">Wyłącz powiadomienia i odrzucaj połączenia po godzinach pracy</string>
     <string name="work_life_dnd_active">Aktywne zasady po godz.</string>
-    <string name="pencil">Ołówek</string>
     <string name="warning">Ostrzeżenie!</string>
     <string name="password_remember_warning">Pamiętajcie, co tu wchodzicie! Ponieważ %s nie przechowuje haseł na serwerach, nie możemy Ci pomóc, jeśli zapomnisz swojego kodu PIN lub hasła.</string>
     <string name="safe_backup_tap_to_restart">Dotknij powiadomienia, aby zrestartować aplikację teraz.</string>
@@ -1169,6 +1168,8 @@ anonimowo?</string>
     <string name="notes">Uwagi</string>
     <string name="blur_faces">Rozmyj twarze</string>
     <string name="brush">Pędzel</string>
+    <string name="pencil">Ołówek</string>
+    <string name="highlighter">Marker</string>
     <string name="error_detecting_faces">Błąd podczas wykrywania twarzy</string>
     <string name="no_faces_detected">Nie wykryto twarzy</string>
     <string name="smiley">Buźka</string>
@@ -1527,6 +1528,11 @@ anonimowo?</string>
     <string name="problemsolver_to_settings">Do ustawień</string>
     <string name="prefs_advanced_options">Opcje zaawansowane</string>
     <string name="push_service">Usługa push</string>
+    <string name="zoom_out">Pomniejsz</string>
+    <string name="zoom_in">Powiększ</string>
+    <string name="edit_and_send">Edytuj i wyślij</string>
+    <string name="media_gallery_animated_webps">Animowane obrazy WebP</string>
+    <string name="unsupported_image_type">Nieobsługiwany typ obrazu: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">%d kontakty</item>
         <item quantity="many">%d kontaktów</item>
@@ -1641,5 +1647,4 @@ anonimowo?</string>
         <item quantity="one">%d członek grupy</item>
         <item quantity="other">%d członków grupy</item>
     </plurals>
-    <string name="edit_and_send">Edytuj</string>
 </resources>

+ 7 - 2
app/src/main/res/values-pt-rBR/strings.xml

@@ -1065,7 +1065,6 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="prefs_working_days_enable_title">Política de folga</string>
     <string name="prefs_working_days_enable_sum">Desativar notificações e rejeitar chamadas fora do horário de trabalho</string>
     <string name="work_life_dnd_active">Folga ativa</string>
-    <string name="pencil">Lápis</string>
     <string name="warning">Atenção!</string>
     <string name="password_remember_warning">Lembre-se do que você entra aqui! Como o %s não armazena senhas em servidores, não podemos ajudá-lo se você esquecer seu PIN ou senha.</string>
     <string name="safe_backup_tap_to_restart">Toque na notificação para reiniciar o aplicativo agora.</string>
@@ -1154,6 +1153,8 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="notes">Anotações</string>
     <string name="blur_faces">Desfocar rostos</string>
     <string name="brush">Pincel</string>
+    <string name="pencil">Lápis</string>
+    <string name="highlighter">Marcador</string>
     <string name="error_detecting_faces">Erro ao detectar rostos</string>
     <string name="no_faces_detected">Nenhum rosto detectado</string>
     <string name="smiley">Smiley</string>
@@ -1512,6 +1513,11 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="problemsolver_to_settings">Ir a Configurações</string>
     <string name="prefs_advanced_options">Opções avançadas</string>
     <string name="push_service">Serviço de push</string>
+    <string name="zoom_out">Diminuir</string>
+    <string name="zoom_in">Ampliar</string>
+    <string name="edit_and_send">Editar e enviar</string>
+    <string name="media_gallery_animated_webps">WebPs animados</string>
+    <string name="unsupported_image_type">Tipo de imagem não suportado: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contato</item>
         <item quantity="other">%d contatos</item>
@@ -1600,5 +1606,4 @@ Por favor, insira uma pergunta para a sua enquete.</string>
         <item quantity="one">%d membro do grupo</item>
         <item quantity="other">%d membros do grupo</item>
     </plurals>
-    <string name="edit_and_send">Editar</string>
 </resources>

+ 63 - 5
app/src/main/res/values-rm/strings.xml

@@ -65,7 +65,6 @@
     <string name="prefs_inapp_vibrate">Vibrar en l\'app</string>
     <string name="prefs_inapp_vibrate_on">Activar la vibraziun cun retschaiver in messadi</string>
     <string name="prefs_inapp_vibrate_off">Nagina vibraziun cura che jau retschaiv in messadi</string>
-    <string name="workarounds">Soluziuns</string>
     <string name="prefs_title_threema_push_switch">Duvrar Threema Push</string>
     <string name="prefs_sum_threema_push_on">Duvrar Threema Push per survegnir infurmaziuns davart novs messadis</string>
     <string name="prefs_sum_threema_push_off">Controllar, sch\'i dat novs messadis cun agid dal servetsch push dal sistem</string>
@@ -1037,7 +1036,6 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="prefs_working_days_enable_title">Regulaziun dal temp da paus</string>
     <string name="prefs_working_days_enable_sum">Na mussar nagins messadis e refusar telefonats da Threema ordaifer il temp da lavur</string>
     <string name="work_life_dnd_active">Temp da paus activ</string>
-    <string name="pencil">Rispli</string>
     <string name="warning">Avertiment</string>
     <string name="password_remember_warning">Tegna endament quai che ti endateschas qua! Cunquai che %s na memorisescha nagins pleds-clav sin servers, na pudain nus betg gidar tai, sche ti has emblidà il pin u la frasa d\'access.</string>
     <string name="safe_backup_tap_to_restart">Smatga sin il messadi per reaviar l\'app.</string>
@@ -1124,6 +1122,7 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="notes">Notizias</string>
     <string name="blur_faces">Disfar las fatschas</string>
     <string name="brush">Penel</string>
+    <string name="pencil">Rispli</string>
     <string name="error_detecting_faces">Igl ha dà in\'errur tar l\'identificaziun da fatschas</string>
     <string name="no_faces_detected">Chattà naginas fatschas</string>
     <string name="smiley">Smiley</string>
@@ -1197,7 +1196,11 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="group_link_rename">Dar in auter num a la colliaziun</string>
     <string name="group_link_rename_tag">Nov num</string>
     <string name="tap_here_for_more">Clicca qua per dapli infurmaziuns</string>
-    <string name="another_connection_instructions"><![CDATA[Il server ha identifitgà dus u plirs apparats colliads cun la medema ID da Threema.<br><br>I n\'è betg pussaivel d\'utilisar in\'ID da Threema il medem mument sin plirs apparats. Novs messadis vegnan tramess sulettamain a l\'apparat ch\'è s\'annunzià sco ultim sin il server.<br><br>Sche ti midas sin in nov apparat, deinstallescha u deactivescha p.pl. %s sin l\'apparat vegl e reaviescha suenter il nov apparat.]]></string>
+    <string name="another_connection_instructions"><![CDATA[Il server ha identifitgà dus u plirs apparats colliads cun la medema ID da Threema.
+
+I n\'è betg pussaivel d\'utilisar in\'ID da Threema il medem mument sin plirs apparats. Novs messadis vegnan tramess sulettamain a l\'apparat ch\'è s\'annunzià sco ultim sin il server.
+
+Sche ti midas sin in nov apparat, deinstallescha u deactivescha p.pl. %s sin l\'apparat vegl e reaviescha suenter il nov apparat.]]></string>
     <string name="app_store_error_code">Code dal sbagl da l\'App Store: %d</string>
     <string name="backup_restore_type"><![CDATA[Tge tip da backup vuls ti restaurar? <br/><br/><a href=%s>Leger dapli davart backups en Threema</a>]]></string>
     <string name="data_backup_info">Restaurar tut incl. chats</string>
@@ -1356,9 +1359,64 @@ Endatescha in pled-clav per tes backup da datas.</string>
         <item quantity="one">Tschernì %d maletg</item>
         <item quantity="other">Tschernì %d maletgs</item>
     </plurals>
+    <plurals name="message_copied">
+        <item quantity="one">Copiar messadi</item>
+        <item quantity="other">Copiar messadis</item>
+    </plurals>
+    <plurals name="new_messages">
+        <item quantity="one">%d nov messadi</item>
+        <item quantity="other">%d novs messadis</item>
+    </plurals>
+    <plurals name="file_saved">
+        <item quantity="one">Memorisà la %d datoteca.</item>
+        <item quantity="other">Memorisà las %d datotecas.</item>
+    </plurals>
     <plurals name="saving_media">
         <item quantity="one">Il medium vegn memorisà</item>
-        <item quantity="other"></item>
+        <item quantity="other">Il medium vegn memorisà</item>
+    </plurals>
+    <plurals name="ballot_really_delete">
+        <item quantity="one">Stizzar l\'enquista</item>
+        <item quantity="other">Stizzar las enquistas</item>
+    </plurals>
+    <plurals name="ballot_really_delete_text">
+        <item quantity="one">Vulst ti propi stizzar %1$d enquista? Las vuschs na pon betg pli vegnir restauradas.</item>
+        <item quantity="other">Vulst ti propi stizzar %1$d enquistas? Las vuschs na pon betg pli vegnir restauradas.</item>
+    </plurals>
+    <plurals name="message_deleted">
+        <item quantity="one">Stizzà %d messadi</item>
+        <item quantity="other">Stizzà %d messadis</item>
+    </plurals>
+    <plurals name="notifications_for_x_hours">
+        <item quantity="one">Per %d ura</item>
+        <item quantity="other">Per %d uras</item>
+    </plurals>
+    <plurals name="some_contacts_not_deleted">
+        <item quantity="one">%d contact n\'ha betg pudì vegnir stizzads, perquai ch\'el èn anc en ina gruppa</item>
+        <item quantity="other">%d contacts n\'han betg pudì vegnir stizzads, perquai ch\'els èn anc en ina gruppa</item>
+    </plurals>
+    <plurals name="message_archived">
+        <item quantity="one">Archivà %d chat</item>
+        <item quantity="other">Archivà %d chats</item>
+    </plurals>
+    <plurals name="webclient_running_sessions">
+        <item quantity="one">%d sesida activa</item>
+        <item quantity="other">%d sesidas activas</item>
+    </plurals>
+    <plurals name="really_delete_outgoing_request">
+        <item quantity="one">Vuls ti propi stizzar %d dumonda da gruppa? La dumonda averta na vegn betg retratga e ti pos pli tard tuttina aderir a la gruppa.</item>
+        <item quantity="other">Vuls ti propi stizzar %d dumondas da gruppa? La dumonda averta na vegn betg retratga e ti pos pli tard tuttina aderir a la gruppa.</item>
+    </plurals>
+    <plurals name="really_delete_incoming_request">
+        <item quantity="one">Vuls ti propi stizzar %d dumonda da gruppa? Sche la dumonda è stizzada, na pos ti betg pli respunder ad ellas.</item>
+        <item quantity="other">Vuls ti propi stizzar %d dumondas da gruppa? Sche las dumondas èn stizzadas, na pos ti betg pli respunder ad ellas.</item>
+    </plurals>
+    <plurals name="really_delete_group_link">
+        <item quantity="one">Vuls ti propi stizzar %d colliaziun da gruppa? Sche la colliaziun è stizzada, na pon utilisaders betg pli trametter dumondas valaivlas. Dumondas gia tramessas pon però anc vegnir acceptadas u refusadas.</item>
+        <item quantity="other">Vuls ti propi stizzar %d colliaziuns da gruppa? Sche la colliaziun è stizzada, na pon utilisaders betg pli trametter dumondas valaivlas. Dumondas gia tramessas pon però anc vegnir acceptadas u refusadas.</item>
+    </plurals>
+    <plurals name="chat_deleted">
+        <item quantity="one">Stizzà %d chat</item>
+        <item quantity="other">Stizzà %d chats</item>
     </plurals>
-    <string name="edit_and_send">Elavurar</string>
 </resources>

+ 10 - 5
app/src/main/res/values-ru/strings.xml

@@ -666,7 +666,7 @@
     <string name="select_all">Выбрать все</string>
     <string name="deleting_messages">Удаление сообщений</string>
     <string name="media_gallery_files">Файлы</string>
-    <string name="prefs_gif_autoplay">Автоматическое воспроизведение анимированных GIF-файлов</string>
+    <string name="prefs_gif_autoplay">Автоматическое воспроизведение анимированных изображений</string>
     <string name="media_gallery_audio">Голосовые сообщения</string>
     <string name="action_clone_group">Клонировать группу</string>
     <string name="clone_group_message">Будет создан клон группы с вами, в качестве администратора. Продолжить?</string>
@@ -1063,7 +1063,6 @@
     <string name="prefs_working_days_enable_title">Правила для нерабочего времени</string>
     <string name="prefs_working_days_enable_sum">Отключить уведомления и отклонять звонки в нерабочее время</string>
     <string name="work_life_dnd_active">Сейчас нерабочее время</string>
-    <string name="pencil">Карандаш</string>
     <string name="warning">Внимание!</string>
     <string name="password_remember_warning">Запомните, что вы вводите здесь! Поскольку %s не хранит пароли на серверах, мы не сможем помочь вам, если вы забудете свой PIN-код или парольную фразу.</string>
     <string name="safe_backup_tap_to_restart">Нажмите на уведомление, чтобы перезапустить приложение сейчас.</string>
@@ -1152,6 +1151,8 @@
     <string name="notes">Примечания</string>
     <string name="blur_faces">Размытие лиц</string>
     <string name="brush">Кисть</string>
+    <string name="pencil">Карандаш</string>
+    <string name="highlighter">Маркер</string>
     <string name="error_detecting_faces">Ошибка распознавания лиц</string>
     <string name="no_faces_detected">Лица не распознаны</string>
     <string name="smiley">Смайл</string>
@@ -1502,15 +1503,20 @@
     <string name="problemsolver_title_notifications">Уведомления отключены</string>
     <string name="problemsolver_title_fullscreen_notifications">Полноэкранные уведомления запрещены</string>
     <string name="problemsolver_explain_fullscreen_notifications">Приложению запрещено отображать полноэкранные уведомления, поэтому вы можете пропустить входящие вызовы, когда телефон заблокирован. Исправьте это, включив переключатель рядом с «Разрешить полноэкранные уведомления для этого приложения».</string>
-    <string name="problemsolver_explain_notifications">Уведомления для этого приложения полностью отключены. Вы не будете получать информацию о входящих сообщениях или звонках асинхронно, приложение может работать не так, как ожидалось. Чтобы это исправить, включите переключатель рядом с «Все уведомления».</string>
+    <string name="problemsolver_explain_notifications">Уведомления для этого приложения полностью отключены. Вы не будете получать информацию о входящих сообщениях или звонках, приложение может работать не так, как ожидалось. Чтобы это исправить, включите переключатель рядом с «Все уведомления».</string>
     <string name="problemsolver_explain_background_data">Приложение не может использовать мобильные данные в фоновом режиме. Вы не будете получать уведомления о входящих сообщениях, если приложение не активно. Чтобы это исправить, включите переключатель «Фоновая передача данных».</string>
     <string name="problemsolver_explain_background">Приложению запрещено работать в фоновом режиме. Вы не будете получать никаких уведомлений и многие функции не будут работать должным образом. Чтобы это исправить, нажмите «Использование батареи приложением» и выберите «Неограниченно».</string>
-    <string name="problemsolver_info_text">Названия некоторых из упомянутых выше параметров настройки системы могут различаться в зависимости от версии Android и производителя телефона.</string>
+    <string name="problemsolver_info_text">Названия некоторых из упомянутых выше параметров настройки системы могут различаться в зависимости от версии Android и производителя устройства.</string>
     <string name="problemsolver_title_app_battery_usgae_optimized">Использование батареи приложением установлено на «оптимальное»</string>
     <string name="problemsolver_explain_app_battery_usgae_optimized">Приложению не разрешено поддерживать постоянное подключение к серверам, которое требуется для работы Threema Push. Вы не будете получать уведомления о новых сообщениях, когда приложение работает в фоновом режиме. Чтобы это исправить, нажмите «Использование батареи приложением» и выберите «Неограниченно».</string>
     <string name="problemsolver_to_settings">К настройкам</string>
     <string name="prefs_advanced_options">Дополнительные настройки</string>
     <string name="push_service">Push-сервис</string>
+    <string name="zoom_out">Уменьшить</string>
+    <string name="zoom_in">Увеличить</string>
+    <string name="edit_and_send">Изменить и отправить</string>
+    <string name="media_gallery_animated_webps">Анимированные WebPs</string>
+    <string name="unsupported_image_type">Неподдерживаемый тип изображения: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">%d контакта</item>
         <item quantity="many">%d контактов</item>
@@ -1689,5 +1695,4 @@
         <item quantity="one">%d участник группы</item>
         <item quantity="other">%d участников группы</item>
     </plurals>
-    <string name="edit_and_send">Редактировать</string>
 </resources>

+ 15 - 4
app/src/main/res/values-sk/strings.xml

@@ -675,7 +675,7 @@ Už ich nebude možné obnoviť.</string>
     <string name="select_all">Vybrať všetko</string>
     <string name="deleting_messages">Správy sa odstraňujú</string>
     <string name="media_gallery_files">Súbory</string>
-    <string name="prefs_gif_autoplay">Prehrávať animované GIF obrázky</string>
+    <string name="prefs_gif_autoplay">Autom. prehráv. animovaných obrázkov</string>
     <string name="media_gallery_audio">Hlasové správy</string>
     <string name="action_clone_group">Klonovať skupinu</string>
     <string name="clone_group_message">Týmto vytvoríte kópiu tejto skupiny s vami ako administrátorom. Pokračovať?</string>
@@ -1022,7 +1022,7 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="whatsnew_headline"><![CDATA[
 <p>Hviezdičkové správy: </p> <p>Dôležité správy môžete označiť hviezdičkou dlhým ťuknutím, aby ste ich neskôr ľahko našli.</p>
  <p>Automatické vymazanie: Po uplynutí zvoleného času môžete svoje konverzácie automaticky vyčistiť. Funkciu automatického mazania správ môžete aktivovať v správe úložiska.</p>
-  <p>Množstvo ďalších vylepšení a optimalizácií.</p>
+  <p>Množstvo ďalších vylepšení a optimalizácií.</p> 
  ]]></string>
     <string name="tap_to_start">Kliknutím sem spustíte aplikáciu %s.</string>
     <string name="two_years">2 roky</string>
@@ -1073,7 +1073,6 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="prefs_working_days_enable_title">Voľba po pracovnej dobe</string>
     <string name="prefs_working_days_enable_sum">Vypnúť upozornenia a odmietať hovory mimo pracovnej doby</string>
     <string name="work_life_dnd_active">Aktívne po pracovnej dobe</string>
-    <string name="pencil">Ceruzka</string>
     <string name="warning">Varovanie</string>
     <string name="password_remember_warning">Pamätajte si, čo tu zadávate! Keďže %s neukladá na serveri žiadne heslá, nebudeme vám vedieť pomôcť, ak zabudnete svoj PIN alebo heslo.</string>
     <string name="safe_backup_tap_to_restart">Klepnutím na notifikáciu reštartujte aplikáciu. Ak toto oznámenie nevidíte, odkryte notifikačnú lištu potiahnutím prstom nadol.</string>
@@ -1162,6 +1161,8 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="notes">Poznámky</string>
     <string name="blur_faces">Rozmazať tváre</string>
     <string name="brush">Štetec</string>
+    <string name="pencil">Ceruzka</string>
+    <string name="highlighter">Zvýrazňovač</string>
     <string name="error_detecting_faces">Chyba pri hľadaní tváre</string>
     <string name="no_faces_detected">Tváre neboli nájdené</string>
     <string name="smiley">Smajlík</string>
@@ -1520,6 +1521,11 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="problemsolver_to_settings">K nastaveniam</string>
     <string name="prefs_advanced_options">Rozšírené možnosti</string>
     <string name="push_service">Služba Push</string>
+    <string name="zoom_out">Zväčšiť</string>
+    <string name="zoom_in">Zmenšiť</string>
+    <string name="edit_and_send">Upraviť a odoslať</string>
+    <string name="media_gallery_animated_webps">Animované WebP</string>
+    <string name="unsupported_image_type">Nepodporovaný typ obrázka: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">kontakty</item>
         <item quantity="many">kontakty</item>
@@ -1556,6 +1562,12 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
         <item quantity="one">%d nová správa</item>
         <item quantity="other">%d nových správ</item>
     </plurals>
+    <plurals name="unread_messages">
+        <item quantity="few">%d neprečítané správy</item>
+        <item quantity="many">%d neprečítaných správ</item>
+        <item quantity="one">%d neprečítaná správa</item>
+        <item quantity="other">%d neprečítaných správ</item>
+    </plurals>
     <plurals name="file_saved">
         <item quantity="few">%d súbory úspešne uložené.</item>
         <item quantity="many">%d súborov úspešne uložených.</item>
@@ -1668,5 +1680,4 @@ Predtým prijaté žiadosti však možno stále prijať alebo odmietnuť.</item>
         <item quantity="one">%d člen skupiny</item>
         <item quantity="other">%d členov skupiny</item>
     </plurals>
-    <string name="edit_and_send">Upraviť</string>
 </resources>

+ 17 - 8
app/src/main/res/values-tr/strings.xml

@@ -4,17 +4,17 @@
     <string name="title_section1">Sohbetler</string>
     <string name="title_compose_message">Sohbet başlat</string>
     <string name="title_choose_recipient">Alıcı Seçin</string>
-    <string name="title_keyfingerprint">Parmak izi anahtarı</string>
-    <string name="title_mythreemaid">Threema kimliğim</string>
+    <string name="title_keyfingerprint">Anahtar Parmak İzi</string>
+    <string name="title_mythreemaid">Threema Kimliğim</string>
     <string name="title_threemaid">Threema kimliği</string>
     <string name="title_adduser">Yeni kişi ekle</string>
     <string name="title_enter_id">Kimlik gir</string>
-    <string name="title_invite_friend">Threema\'ya davet et</string>
+    <string name="title_invite_friend">Arkadaşını davet et</string>
     <string name="invite_via">Davet bağlantısı paylaş</string>
     <string name="invite_email_body">Merhaba,\n\nI kullanıcıların gizliliğini koruyan güvenli anlık mesajlaşma programı olan %1$s kullanın.\n\nThreema kimliğim: https://threema.id/%2$s\n\n%1$s üzerinden haberleşelim!\n\nGörüşmek üzere,\n</string>
     <string name="invite_sms_body">Merhaba! Güvenli ve gizliliğe uygun bir şekilde iletişim kurmak için %1$s kullanalım! Threema ID kimliğim: https:// threema.id/%2$s</string>
     <string name="invite_email_subject">Threema. Gizliliğinizi koruyan güvenli mesajlaşma programı.</string>
-    <string name="enter_id_hint">Threema kimliğini gir</string>
+    <string name="enter_id_hint">Threema kimliğini girin</string>
     <string name="account_links">Bağlı hesaplar</string>
     <string name="menu_settings">Ayarlar</string>
     <string name="menu_about">Threema Hakkında</string>
@@ -37,7 +37,7 @@
     <string name="prefs_sum_sync_contacts_on">%s kullanıcıyı cihazın adres defteriyle eşitle</string>
     <string name="prefs_sum_sync_contacts_off">%s kullanıcıyı cihazın adres defteriyle eşitleme</string>
     <string name="prefs_title_sync_contacts">Kişileri eşitle</string>
-    <string name="prefs_sum_block_unknown_off">Herhangi biri size mesaj gönderebilir. İlk mesaj geldiğinde yeni kişiler otomatik olarak eklenecektir.</string>
+    <string name="prefs_sum_block_unknown_off">Herhangi biri size mesaj gönderebilir. İlk mesaj geldiğinde yeni kişilere otomatik olarak eklenecektir.</string>
     <string name="prefs_sum_block_unknown_on">Yalnızca adres defterinizdeki kişiler size mesaj gönderebilir.</string>
     <string name="prefs_title_block_unknown">Bilinmeyeni engelle</string>
     <string name="prefs_title_read_receipts">Okundu bilgisini gönder</string>
@@ -664,7 +664,7 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="select_all">Hepsini seç</string>
     <string name="deleting_messages">Mesajlar siliniyor</string>
     <string name="media_gallery_files">Dosyalar</string>
-    <string name="prefs_gif_autoplay">Hareketli GIF\'leri otomatik oynat</string>
+    <string name="prefs_gif_autoplay">Animasyonlu görüntüleri otomatik oynat</string>
     <string name="media_gallery_audio">Sesli mesajlar</string>
     <string name="action_clone_group">Grubu kopyala</string>
     <string name="clone_group_message">Bu, yönetici olarak sizinle birlikte bu grubun bir kopyasını oluşturacaktır. Devam edilsin mi?</string>
@@ -1059,7 +1059,6 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="prefs_working_days_enable_title">Mesai dışı politikası</string>
     <string name="prefs_working_days_enable_sum">Bildirimleri devre dışı bırak ve çalışma saatleri dışındaki aramaları reddet</string>
     <string name="work_life_dnd_active">Mesai saatleri dışı aktif</string>
-    <string name="pencil">Kalem</string>
     <string name="warning">Uyarı</string>
     <string name="password_remember_warning">Buraya ne yazdığınızı unutmayın! %s sunucularda herhangi bir parola kaydetmediğinden, PIN kodunuzu veya parolanızı unutursanız size yardımcı olamayız.</string>
     <string name="safe_backup_tap_to_restart">Uygulamayı şimdi yeniden başlatmak için gösterilen sistem bildirimine dokunun. Bildirimi göremiyorsanız, lütfen telefonunuzun bildirim çubuğunu aşağı kaydırın.</string>
@@ -1148,6 +1147,8 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="notes">Notlar</string>
     <string name="blur_faces">Yüzleri bulanıklaştır</string>
     <string name="brush">Fırça</string>
+    <string name="pencil">Kalem</string>
+    <string name="highlighter">Fosforlu Kalem</string>
     <string name="error_detecting_faces">Yüzler algılanırken hata oluştu</string>
     <string name="no_faces_detected">Yüz algılanmadı</string>
     <string name="smiley">Gülenyüz</string>
@@ -1506,6 +1507,11 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="problemsolver_to_settings">Ayarlara</string>
     <string name="prefs_advanced_options">Gelişmiş seçenekler</string>
     <string name="push_service">Push hizmeti</string>
+    <string name="zoom_out">Uzaklaştır</string>
+    <string name="zoom_in">Yakınlaştır</string>
+    <string name="edit_and_send">Düzenle ve gönder</string>
+    <string name="media_gallery_animated_webps">Animasyonlu WebP\'ler</string>
+    <string name="unsupported_image_type">Desteklenmeyen görüntü türü: %s</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kişi</item>
         <item quantity="other">%d kişi</item>
@@ -1530,6 +1536,10 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
         <item quantity="one">%d yeni mesaj</item>
         <item quantity="other">%d yeni mesaj</item>
     </plurals>
+    <plurals name="unread_messages">
+        <item quantity="one">%d okunmamış Mesaj</item>
+        <item quantity="other">%d okunmamış Mesaj</item>
+    </plurals>
     <plurals name="file_saved">
         <item quantity="one">%d dosya başarıyla kaydedildi.</item>
         <item quantity="other">%d dosya başarıyla kaydedildi.</item>
@@ -1608,5 +1618,4 @@ Daha önce alınan istekler yine de kabul edilebilir veya reddedilebilir.</item>
         <item quantity="one">%d grup üyesi</item>
         <item quantity="other">%d grup üyesi</item>
     </plurals>
-    <string name="edit_and_send">Düzenle</string>
 </resources>

Some files were not shown because too many files changed in this diff