diff --git a/android/app/build.gradle b/android/app/build.gradle
index ad88be50b..85f700135 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -214,7 +214,6 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.core:core-ktx:1.12.0'
- implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
diff --git a/android/exifinterface/.gitignore b/android/exifinterface/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/android/exifinterface/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/exifinterface/LICENSE.txt b/android/exifinterface/LICENSE.txt
new file mode 100644
index 000000000..e454a5258
--- /dev/null
+++ b/android/exifinterface/LICENSE.txt
@@ -0,0 +1,178 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/android/exifinterface/OWNERS b/android/exifinterface/OWNERS
new file mode 100644
index 000000000..92d0e3f59
--- /dev/null
+++ b/android/exifinterface/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 461278
+file: ../media/OWNERS
diff --git a/android/exifinterface/build.gradle b/android/exifinterface/build.gradle
new file mode 100644
index 000000000..48a08ec5c
--- /dev/null
+++ b/android/exifinterface/build.gradle
@@ -0,0 +1,30 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ namespace 'androidx.exifinterface.media'
+ compileSdk 34
+
+ defaultConfig {
+ minSdk 19
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'androidx.annotation:annotation:1.7.1'
+}
\ No newline at end of file
diff --git a/android/exifinterface/consumer-rules.pro b/android/exifinterface/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/android/exifinterface/proguard-rules.pro b/android/exifinterface/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/android/exifinterface/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/android/exifinterface/src/main/AndroidManifest.xml b/android/exifinterface/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a5918e68a
--- /dev/null
+++ b/android/exifinterface/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+ * Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF. + *
+ * Supported for writing: JPEG, PNG, WebP. + *
+ * 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 { + // TLAD threshold for safer Exif attribute parsing + private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB + + private static final String TAG = "ExifInterface"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // The Exif tag names. See JEITA CP-3451C specifications (Exif 2.3) Section 3-8. + // A. Tags related to image data structure + /** + *
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.
+ * + *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.
+ * + *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.
+ * + *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.
+ * + *+ * @see #DATA_UNCOMPRESSED + * @see #DATA_JPEG + */ + public static final String TAG_COMPRESSION = "Compression"; + /** + *
The pixel composition. In JPEG compressed data, this tag shall not be used because a JPEG + * marker is used instead of it.
+ * + *+ * @see #PHOTOMETRIC_INTERPRETATION_RGB + * @see #PHOTOMETRIC_INTERPRETATION_YCBCR + */ + public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"; + /** + *
The image orientation viewed in terms of rows and columns.
+ * + *+ * @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"; + /** + *
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.
+ * + *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.
+ * + *+ * @see #FORMAT_CHUNKY + * @see #FORMAT_PLANAR + */ + public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration"; + /** + *
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.
+ * + *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.
+ * + *+ * @see #Y_CB_CR_POSITIONING_CENTERED + * @see #Y_CB_CR_POSITIONING_CO_SITED + */ + public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning"; + /** + *
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.
+ * + *+ * @see #TAG_Y_RESOLUTION + * @see #TAG_RESOLUTION_UNIT + */ + public static final String TAG_X_RESOLUTION = "XResolution"; + /** + *
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.
+ * + *+ * @see #TAG_X_RESOLUTION + * @see #TAG_RESOLUTION_UNIT + */ + public static final String TAG_Y_RESOLUTION = "YResolution"; + /** + *
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.
+ * + *+ * @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 + /** + *
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.
+ * + *StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1) + * / {@link #TAG_ROWS_PER_STRIP})
+ *+ * @see #TAG_ROWS_PER_STRIP + * @see #TAG_STRIP_BYTE_COUNTS + */ + public static final String TAG_STRIP_OFFSETS = "StripOffsets"; + /** + *
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.
+ * + *+ * @see #TAG_STRIP_OFFSETS + * @see #TAG_STRIP_BYTE_COUNTS + */ + public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip"; + /** + *
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.
+ * + *StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1) + * / {@link #TAG_ROWS_PER_STRIP})
+ */ + public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts"; + /** + *The offset to the start byte (SOI) of JPEG compressed thumbnail data. This shall not be + * used for primary image JPEG data.
+ * + *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.
+ * + *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}.
+ * + *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}.
+ * + *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}.
+ * + *The matrix coefficients for transformation from RGB to YCbCr image data. About + * the default value, please refer to JEITA CP-3451C Spec, Annex D.
+ * + *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
+ * + *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.
+ * + *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"; + /** + *
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.
+ * + *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.
+ * + *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.
+ * + *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.
+ * + *Ex.) "Exif Software Version 1.00a".
+ * + *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.
+ * + *Ex.) "Camera owner, John Smith; Photographer, Michael Brown; Image creator, + * Ken James"
+ * + *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.
+ * + *Ex. 1) When both the photographer copyright and editor copyright are given. + *
Ex. 2) When only the photographer copyright is given. + *
Ex. 3) When only the editor copyright is given. + *
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.
+ * + *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.
+ * + *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.
+ * + *+ * @see #COLOR_SPACE_S_RGB + * @see #COLOR_SPACE_UNCALIBRATED + */ + public static final String TAG_COLOR_SPACE = "ColorSpace"; + /** + *
Indicates the value of coefficient gamma. The formula of transfer function used for image + * reproduction is expressed as follows.
+ * + *(Reproduced value) = (Input value) ^ gamma
+ * + *Both reproduced value and input value indicate normalized value, whose minimum value is + * 0 and maximum value is 1.
+ * + *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.
+ * + *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.
+ * + *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.
+ * + *Information specific to compressed data. The compression mode used for a compressed image + * is indicated in unit bits per pixel.
+ * + *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.
+ * + *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.
+ * + *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.
+ * + *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.
+ * + *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.
+ * + *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"; + /** + *
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.
+ * + *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"; + /** + *
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.
+ * + *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.
+ * + *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.
+ * + *A tag used to record fractions of seconds for {@link #TAG_DATETIME}.
+ * + *A tag used to record fractions of seconds for {@link #TAG_DATETIME_ORIGINAL}.
+ * + *A tag used to record fractions of seconds for {@link #TAG_DATETIME_DIGITIZED}.
+ * + *Exposure time, given in seconds.
+ * + *The F number.
+ * + *TThe class of the program used by the camera to set exposure when the picture is taken. + * The tag values are as follows.
+ * + *+ * @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"; + /** + *
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.
+ * + *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.
+ * + *Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. OECF is + * the relationship between the camera optical input and the image values.
+ * + *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.
+ * + *+ * @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"; + /** + *
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.
+ * + *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.
+ * + *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.
+ * + *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}.
+ * + *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}.
+ * + *Shutter speed. The unit is the APEX setting.
+ * + *The lens aperture. The unit is the APEX value.
+ * + *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.
+ * + *The exposure bias. The unit is the APEX value. Ordinarily it is given in the range of + * -99.99 to 99.99.
+ * + *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.
+ * + *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.
+ * + *The metering mode.
+ * + *+ * @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"; + /** + *
The kind of light source.
+ * + *+ * @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"; + /** + *
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.
+ * + *+ * @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"; + /** + *
This tag indicates the location and area of the main subject in the overall scene.
+ * + *The subject location and area are defined by Count values as follows.
+ * + *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}.
+ */ + public static final String TAG_SUBJECT_AREA = "SubjectArea"; + /** + *The actual focal length of the lens, in mm. Conversion is not made to the focal length + * of a 35mm film camera.
+ * + *Indicates the strobe energy at the time the image is captured, as measured in Beam Candle + * Power Seconds (BCPS).
+ * + *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.
+ * + *Indicates the number of pixels in the image width (X) direction per + * {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.
+ * + *Indicates the number of pixels in the image height (Y) direction per + * {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.
+ * + *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}.
+ * + *+ * @see #TAG_RESOLUTION_UNIT + * @see #RESOLUTION_UNIT_INCHES + * @see #RESOLUTION_UNIT_CENTIMETERS + */ + public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit"; + /** + *
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.
+ * + *Indicates the exposure index selected on the camera or input device at the time the image + * is captured.
+ * + *Indicates the image sensor type on the camera or input device.
+ * + *+ * @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"; + /** + *
Indicates the image source. If a DSC recorded the image, this tag value always shall + * be set to {@link #FILE_SOURCE_DSC}.
+ * + *+ * @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"; + /** + *
Indicates the type of scene. If a DSC recorded the image, this tag value shall always + * be set to {@link #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED}.
+ * + *+ * @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED + */ + public static final String TAG_SCENE_TYPE = "SceneType"; + /** + *
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.
+ * + *+ * @see #TAG_SENSING_METHOD + * @see #SENSOR_TYPE_ONE_CHIP + */ + public static final String TAG_CFA_PATTERN = "CFAPattern"; + /** + *
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.
+ * + *+ * @see #RENDERED_PROCESS_NORMAL + * @see #RENDERED_PROCESS_CUSTOM + */ + public static final String TAG_CUSTOM_RENDERED = "CustomRendered"; + /** + *
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.
+ * + *+ * @see #EXPOSURE_MODE_AUTO + * @see #EXPOSURE_MODE_MANUAL + * @see #EXPOSURE_MODE_AUTO_BRACKET + */ + public static final String TAG_EXPOSURE_MODE = "ExposureMode"; + /** + *
This tag indicates the white balance mode set when the image was shot.
+ * + *+ * @see #WHITEBALANCE_AUTO + * @see #WHITEBALANCE_MANUAL + */ + public static final String TAG_WHITE_BALANCE = "WhiteBalance"; + /** + *
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.
+ * + *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}.
+ * + *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}.
+ * + *+ * @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"; + /** + *
This tag indicates the degree of overall image gain adjustment.
+ * + *+ * @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"; + /** + *
This tag indicates the direction of contrast processing applied by the camera when + * the image was shot.
+ * + *+ * @see #CONTRAST_NORMAL + * @see #CONTRAST_SOFT + * @see #CONTRAST_HARD + */ + public static final String TAG_CONTRAST = "Contrast"; + /** + *
This tag indicates the direction of saturation processing applied by the camera when + * the image was shot.
+ * + *+ * @see #SATURATION_NORMAL + * @see #SATURATION_LOW + * @see #SATURATION_HIGH + */ + public static final String TAG_SATURATION = "Saturation"; + /** + *
This tag indicates the direction of sharpness processing applied by the camera when + * the image was shot.
+ * + *+ * @see #SHARPNESS_NORMAL + * @see #SHARPNESS_SOFT + * @see #SHARPNESS_HARD + */ + public static final String TAG_SHARPNESS = "Sharpness"; + /** + *
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.
+ * + *This tag indicates the distance to the subject.
+ * + *+ * @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 + /** + *
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.
+ * + *This tag records the owner of a camera used in photography as an ASCII string.
+ * + *+ * @deprecated Use {@link #TAG_CAMERA_OWNER_NAME} instead. + */ + @Deprecated + public static final String TAG_CAMARA_OWNER_NAME = "CameraOwnerName"; + /** + *
This tag records the owner of a camera used in photography as an ASCII string.
+ * + *This tag records the serial number of the body of the camera that was used in photography + * as an ASCII string.
+ * + *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.
+ * + *This tag records the lens manufacturer as an ASCII string.
+ * + *This tag records the lens’s model name and model number as an ASCII string.
+ * + *This tag records the serial number of the interchangeable lens that was used in + * photography as an ASCII string.
+ * + *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}.
+ * + *Indicates whether the latitude is north or south latitude.
+ * + *+ * @see #LATITUDE_NORTH + * @see #LATITUDE_SOUTH + */ + public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"; + /** + *
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.
+ * + *Indicates whether the longitude is east or west longitude.
+ * + *+ * @see #LONGITUDE_EAST + * @see #LONGITUDE_WEST + */ + public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"; + /** + *
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.
+ * + *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}.
+ * + *+ * @see #ALTITUDE_ABOVE_SEA_LEVEL + * @see #ALTITUDE_BELOW_SEA_LEVEL + */ + public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef"; + /** + *
Indicates the altitude based on the reference in {@link #TAG_GPS_ALTITUDE_REF}. + * The reference unit is meters.
+ * + *Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three + * unsigned rational values giving the hour, minute, and second.
+ * + *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}.
+ * + *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.
+ * + *+ * @see #GPS_MEASUREMENT_IN_PROGRESS + * @see #GPS_MEASUREMENT_INTERRUPTED + */ + public static final String TAG_GPS_STATUS = "GPSStatus"; + /** + *
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.
+ * + *+ * @see #GPS_MEASUREMENT_2D + * @see #GPS_MEASUREMENT_3D + */ + public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode"; + /** + *
Indicates the GPS DOP (data degree of precision). An HDOP value is written during + * two-dimensional measurement, and PDOP during three-dimensional measurement.
+ * + *Indicates the unit used to express the GPS receiver speed of movement.
+ * + *+ * @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"; + /** + *
Indicates the speed of GPS receiver movement.
+ * + *Indicates the reference for giving the direction of GPS receiver movement.
+ * + *+ * @see #GPS_DIRECTION_TRUE + * @see #GPS_DIRECTION_MAGNETIC + */ + public static final String TAG_GPS_TRACK_REF = "GPSTrackRef"; + /** + *
Indicates the direction of GPS receiver movement. + * The range of values is from 0.00 to 359.99.
+ * + *Indicates the reference for giving the direction of the image when it is captured.
+ * + *+ * @see #GPS_DIRECTION_TRUE + * @see #GPS_DIRECTION_MAGNETIC + */ + public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; + /** + *
ndicates the direction of the image when it was captured. + * The range of values is from 0.00 to 359.99.
+ * + *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.
+ * + *Indicates whether the latitude of the destination point is north or south latitude.
+ * + *+ * @see #LATITUDE_NORTH + * @see #LATITUDE_SOUTH + */ + public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef"; + /** + *
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.
+ * + *Indicates whether the longitude of the destination point is east or west longitude.
+ * + *+ * @see #LONGITUDE_EAST + * @see #LONGITUDE_WEST + */ + public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef"; + /** + *
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.
+ * + *Indicates the reference used for giving the bearing to the destination point.
+ * + *+ * @see #GPS_DIRECTION_TRUE + * @see #GPS_DIRECTION_MAGNETIC + */ + public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef"; + /** + *
Indicates the bearing to the destination point. + * The range of values is from 0.00 to 359.99.
+ * + *Indicates the unit used to express the distance to the destination point.
+ * + *+ * @see #GPS_DISTANCE_KILOMETERS + * @see #GPS_DISTANCE_MILES + * @see #GPS_DISTANCE_NAUTICAL_MILES + */ + public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef"; + /** + *
Indicates the distance to the destination point.
+ * + *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.
+ * + *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.
+ * + *A character string recording date and time information relative to UTC (Coordinated + * Universal Time). The format is "YYYY:MM:DD".
+ * + *Indicates whether differential correction is applied to the GPS receiver.
+ * + *+ * @see #GPS_MEASUREMENT_NO_DIFFERENTIAL + * @see #GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED + */ + public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential"; + /** + *
This tag indicates horizontal positioning errors in meters.
+ * + *Indicates the identification of the Interoperability rule.
+ * + *+ * 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. + *
+ * 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) {
+ Log.w(TAG, "IOException occurred during reading a value", e);
+ return null;
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "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 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
+ * This method is supported for JPEG, PNG, and WebP formats.
+ *
+ * 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.
+ *
+ * For WebP format, the Exif data will be stored as an Extended File Format, and it may not be
+ * supported for older readers.
+ *
+ * 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) {
+ saveJpegAttributes(bufferedIn, bufferedOut);
+ } 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;
+ }
+
+ /**
+ * 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 {
+ Log.d(TAG, "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.
+ Log.d(TAG, "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.
+ *
+ * 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) {
+ Log.w(TAG, "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
+ * defaultValue.
+ *
+ * @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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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 1970-01-01 00:00:00 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"
+ */
+ private 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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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
+ SeekableByteOrderedDataInputStream makerNoteDataInputStream =
+ new SeekableByteOrderedDataInputStream(makerNoteAttribute.bytes);
+ makerNoteDataInputStream.setByteOrder(mExifByteOrder);
+
+ // Skip to MakerNote data
+ makerNoteDataInputStream.skipFully(PEF_MAKER_NOTE_SKIP_SIZE);
+
+ // Read IFD data from MakerNote
+ readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_PEF);
+
+ // 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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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 = null, exifLengthStr = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ exifOffsetStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET);
+ exifLengthStr = retriever.extractMetadata(
+ 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 = null, xmpLengthStr = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ xmpOffsetStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_XMP_OFFSET);
+ xmpLengthStr = retriever.extractMetadata(
+ 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) {
+ Log.d(TAG, "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)) {
+ Log.w(TAG, "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
+ SeekableByteOrderedDataInputStream makerNoteDataInputStream =
+ new SeekableByteOrderedDataInputStream(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 =
+ 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) {
+ Log.w(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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.
+ private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream)
+ throws IOException {
+ // See JPEG File Interchange Format Specification, "JFIF Specification"
+ if (DEBUG) {
+ Log.d(TAG, "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
+ 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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "readExifSegment: Byte Align II");
+ }
+ return LITTLE_ENDIAN;
+ case BYTE_ALIGN_MM:
+ if (DEBUG) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, 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) {
+ Log.d(TAG, "Skip the tag entry since tag number is not defined: " + tagNumber);
+ }
+ } else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
+ if (DEBUG) {
+ Log.d(TAG, "Skip the tag entry since data format is invalid: " + dataFormat);
+ }
+ } else if (!tag.isFormatCompatible(dataFormat)) {
+ if (DEBUG) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, "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) {
+ Log.d(TAG, 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) {
+ Log.d(TAG, "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() + ")";
+ }
+ Log.d(TAG, message);
+ }
+ }
+
+ dataInputStream.seek(nextEntryOffset);
+ continue;
+ }
+
+ final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
+ if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
+ throw new IOException("dangerous attribute size=" + byteCount);
+ }
+ 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) {
+ Log.d(TAG, 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) {
+ Log.d(TAG, "Stop reading file since re-reading an IFD may cause an "
+ + "infinite loop: " + nextIfdOffset);
+ }
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "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
+ * 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) {
+ Log.w(TAG, "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) {
+ Log.w(TAG, "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