diff --git a/android/app/build.gradle b/android/app/build.gradle
index eb8171a4a..2eade1cc3 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -154,12 +154,12 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation "androidx.appcompat:appcompat:1.7.0"
- implementation 'androidx.core:core-ktx:1.13.1'
- implementation 'androidx.lifecycle:lifecycle-process:2.8.6'
+ implementation 'androidx.core:core-ktx:1.15.0'
+ implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
- implementation 'androidx.work:work-runtime-ktx:2.9.1'
+ implementation 'androidx.work:work-runtime-ktx:2.10.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
@@ -181,7 +181,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.3'
- kapt 'androidx.annotation:annotation:1.8.2'
+ kapt 'androidx.annotation:annotation:1.9.1'
ksp "com.github.bumptech.glide:ksp:$glide_version"
compileOnly rootProject.findProject(':streams_channel')
diff --git a/android/exifinterface/build.gradle b/android/exifinterface/build.gradle
index 46bfa54d8..7d83b50a2 100644
--- a/android/exifinterface/build.gradle
+++ b/android/exifinterface/build.gradle
@@ -26,5 +26,6 @@ android {
}
dependencies {
- implementation 'androidx.annotation:annotation:1.8.2'
+ implementation 'androidx.annotation:annotation:1.9.1'
+ implementation 'org.jspecify:jspecify:1.0.0'
}
\ No newline at end of file
diff --git a/android/exifinterface/src/main/AndroidManifest.xml b/android/exifinterface/src/main/AndroidManifest.xml
index a5918e68a..568741e54 100644
--- a/android/exifinterface/src/main/AndroidManifest.xml
+++ b/android/exifinterface/src/main/AndroidManifest.xml
@@ -1,4 +1,2 @@
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
index 934ee16b5..07bab0f4c 100644
--- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
+++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
@@ -22,6 +22,8 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongA
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
@@ -33,17 +35,19 @@ import android.location.Location;
import android.media.MediaDataSource;
import android.media.MediaMetadataRetriever;
import android.os.Build;
+import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
+import androidx.annotation.VisibleForTesting;
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@@ -62,12 +66,14 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -83,20 +89,48 @@ import java.util.regex.Pattern;
import java.util.zip.CRC32;
/*
- * Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21
+ * Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
* Named differently to let ExifInterface be loaded as subdependency.
+ * cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
*/
/**
* This is a class for reading and writing Exif tags in various image file formats.
+ *
+ *
Supported for reading: JPEG, PNG, WebP, HEIC, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW,
+ * RAF, AVIF (on API 31+).
+ *
+ *
- * Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF.
+ *
+ *
- * 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.
+ * This class can read raw XMP data from the supported image file formats.
+ *
+ *
XMP data can be stored within Exif data (under tag 700), but many of the formats also define a
+ * separate storage location for XMP. ExifInterface handles this ambiguity as follows:
+ *
+ *
Exposure time, given in seconds.
+ * Exposure time, given in seconds.
+ *
+ * Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
*
*
The F number.
+ * The F number.
+ *
+ * Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
*
*
TThe class of the program used by the camera to set exposure when the picture is taken.
+ *
The class of the program used by the camera to set exposure when the picture is taken.
* The tag values are as follows.
*
* 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 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.
+ *
+ *
Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
*
*
The metering mode.
*
@@ -1451,18 +1508,26 @@ public class ExifInterfaceFork {
* @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 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.
+ *
+ * Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
*
*
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
@@ -1792,18 +1857,24 @@ public class ExifInterfaceFork {
*
*/
public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
+
/**
- *
Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
- * unsigned rational values giving the hour, minute, and second.
+ * Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
+ * unsigned rational values giving the hour, minute, and second.
+ *
+ * Note: This attribute is returned from {@link #getAttribute(String)} and accepted into
+ * {@link #setAttribute(String, String)} as 3 colon-separated integers, e.g. {@code "11:05:32"}.
+ * Decimal or rational hours, minutes or seconds parts are not supported.
*
*
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
@@ -1876,7 +1947,8 @@ public class ExifInterfaceFork {
*/
public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
/**
- *
Indicates the speed of GPS receiver movement.
+ * Indicates the speed of GPS receiver movement. The units are indicated by {@link
+ * #TAG_GPS_SPEED_REF}.
*
*
* - Tag = 13
@@ -2214,8 +2286,11 @@ public class ExifInterfaceFork {
public static final String TAG_RW2_JPG_FROM_RAW = "JpgFromRaw";
/**
* Type is byte[]. See Extensible
- * Metadata Platform (XMP) for details on contents.
+ * "https://en.wikipedia.org/wiki/Extensible_Metadata_Platform">Extensible Metadata Platform
+ * (XMP) for details on contents.
+ *
+ * See also notes about XMP handling in different containers in the class-level javadoc of
+ * this class.
*/
public static final String TAG_XMP = "Xmp";
/**
@@ -2984,6 +3059,37 @@ public class ExifInterfaceFork {
public @interface ExifStreamType {
}
+ @Retention(RetentionPolicy.SOURCE)
+ @Target(TYPE_USE)
+ @IntDef({
+ XMP_HANDLING_TIFF_700_ONLY,
+ XMP_HANDLING_PREFER_SEPARATE,
+ XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
+ })
+ private @interface XmpHandling {
+ }
+
+ /**
+ * The format only supports XMP data stored in the TIFF/Exif 700 tag (or it supports storing XMP
+ * in a separate segment but this should never be used).
+ */
+ private static final int XMP_HANDLING_TIFF_700_ONLY = 1;
+
+ /**
+ * The format supports XMP data stored in a separate segment, and this should always be
+ * preferred if present.
+ *
+ *
If XMP data is only present in the TIFF/Exif 700 tag it will be read and written from
+ * there.
+ */
+ private static final int XMP_HANDLING_PREFER_SEPARATE = 2;
+
+ /**
+ * The format supports XMP data stored in a separate segment, and this should be preferred
+ * unless XMP is also present in the TIFF/Exif 700 tag.
+ */
+ private static final int XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT = 3;
+
// Maximum size for checking file type signature (see image_type_recognition_lite.cc)
private static final int SIGNATURE_CHECK_SIZE = 5000;
@@ -2994,6 +3100,8 @@ public class ExifInterfaceFork {
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'};
+ private static final byte[] HEIF_BRAND_AVIF = new byte[]{'a', 'v', 'i', 'f'};
+ private static final byte[] HEIF_BRAND_AVIS = new byte[]{'a', 'v', 'i', 's'};
// See http://fileformats.archiveteam.org/wiki/Olympus_ORF
private static final short ORF_SIGNATURE_1 = 0x4f52;
@@ -3023,12 +3131,9 @@ public class ExifInterfaceFork {
(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_EXIF = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f';
+ private static final int PNG_CHUNK_TYPE_IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R';
+ private static final int PNG_CHUNK_TYPE_IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D';
private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
@@ -3050,8 +3155,8 @@ public class ExifInterfaceFork {
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;
+ private static final SimpleDateFormat sFormatterPrimary;
+ private static final 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.
@@ -3085,32 +3190,27 @@ public class ExifInterfaceFork {
private static final int SKIP_BUFFER_SIZE = 8192;
// Names for the data formats for debugging purpose.
- static final String[] IFD_FORMAT_NAMES = new String[]{
+ private 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[]{
+ private 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[]{
+ private 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 {
+ // TODO: b/308978831 - Migrate to android.util.Rational when the min API is 21.
+ @VisibleForTesting
+ 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) {
+ private Rational(long numerator, long denominator) {
// Handle erroneous case
if (denominator == 0) {
this.numerator = 0;
@@ -3121,9 +3221,44 @@ public class ExifInterfaceFork {
this.denominator = denominator;
}
- @NonNull
+ /**
+ * Creates a new {@code Rational} which approximates the provided {@code double} value by
+ * using continued fractions.
+ */
+ public static @NonNull Rational createFromDouble(double value) {
+ if (value >= Long.MAX_VALUE || value <= Long.MIN_VALUE) {
+ // value is too large to represent as a long, so just return the max/min value.
+ return new Rational(
+ /* numerator= */ value > 0 ? Long.MAX_VALUE : Long.MIN_VALUE,
+ /* denominator= */ 1);
+ }
+
+ double absoluteValue = Math.abs(value);
+ double threshold = 0.00000001 * absoluteValue;
+ double remainingValue = absoluteValue;
+ long numerator = 1;
+ long previousNumerator = 0;
+ long denominator = 0;
+ long previousDenominator = 1;
+ do {
+ double remainder = remainingValue % 1;
+ long wholePart = (long) (remainingValue - remainder);
+ long tmp = numerator;
+ numerator = wholePart * numerator + previousNumerator;
+ previousNumerator = tmp;
+
+ tmp = denominator;
+ denominator = wholePart * denominator + previousDenominator;
+ previousDenominator = tmp;
+
+ remainingValue = 1 / remainder;
+ } while ((Math.abs(absoluteValue - numerator / (double) denominator) > threshold));
+
+ return new Rational(value < 0 ? -numerator : numerator, denominator);
+ }
+
@Override
- public String toString() {
+ public @NonNull String toString() {
return numerator + "/" + denominator;
}
@@ -3141,12 +3276,10 @@ public class ExifInterfaceFork {
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;
@@ -3243,13 +3376,11 @@ public class ExifInterfaceFork {
return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
Object getValue(ByteOrder byteOrder) {
ByteOrderedDataInputStream inputStream = null;
try {
@@ -3497,13 +3628,12 @@ public class ExifInterfaceFork {
}
// A class for indicating EXIF tag.
- static class ExifTag {
+ private 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;
@@ -3511,7 +3641,6 @@ public class ExifInterfaceFork {
this.secondaryFormat = -1;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
ExifTag(String name, int number, int primaryFormat, int secondaryFormat) {
this.name = name;
this.number = number;
@@ -3519,7 +3648,6 @@ public class ExifInterfaceFork {
this.secondaryFormat = secondaryFormat;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
boolean isFormatCompatible(int format) {
if (primaryFormat == IFD_FORMAT_UNDEFINED || format == IFD_FORMAT_UNDEFINED) {
return true;
@@ -3785,23 +3913,24 @@ public class ExifInterfaceFork {
public @interface IfdType {
}
- static final int IFD_TYPE_PRIMARY = 0;
+ private 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_THUMBNAIL = 4;
+ private 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 Exif tag groups, indexed by the IDF_TYPE_* constants above.
+ 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),
@@ -3820,9 +3949,30 @@ public class ExifInterfaceFork {
@SuppressWarnings("unchecked")
private static final HashMap[] sExifTagMapsForWriting =
new HashMap[EXIF_TAGS.length];
- private static final HashSet sTagSetForCompatibility = new HashSet<>(Arrays.asList(
- TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE,
- TAG_GPS_TIMESTAMP));
+
+ /**
+ * These are tags of type 'Unsigned rational' but which are handled in decimal form.
+ *
+ * This means they are output from {@link #getAttribute(String)}, and accepted into {@link
+ * #setAttribute(String, String)}, as strings in decimal form (e.g. {@code "0.125"}, {@code
+ * "6.25E-4"}).
+ *
+ *
This is to maintain backwards compatibility with a previous implementation of the {@link
+ * android.media.ExifInterface} (the platform variant of this class).
+ *
+ *
See this
+ * internal code review comment from 2016 for more details.
+ */
+ private static final Set RATIONAL_TAGS_HANDLED_AS_DECIMALS_FOR_COMPATIBILITY =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ TAG_F_NUMBER,
+ TAG_DIGITAL_ZOOM_RATIO,
+ TAG_EXPOSURE_TIME,
+ TAG_SUBJECT_DISTANCE)));
+
// Mappings from tag number to IFD type for pointer tags.
private static final HashMap sExifPointerTagMap = new HashMap<>();
@@ -3830,9 +3980,9 @@ public class ExifInterfaceFork {
// The following values are defined for handling JPEG streams. In this implementation, we are
// not only getting information from EXIF but also from some JPEG special segments such as
// MARKER_COM for user comment and MARKER_SOFx for image width and height.
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static final Charset ASCII = Charset.forName("US-ASCII");
+ private static final Charset ASCII = Charset.forName("US-ASCII");
// Identifier for EXIF APP1 segment in JPEG
+ @VisibleForTesting
static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(ASCII);
// Identifier for XMP APP1 segment in JPEG
private static final byte[] IDENTIFIER_XMP_APP1 =
@@ -3840,7 +3990,7 @@ public class ExifInterfaceFork {
// JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
// the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
// of frame(baseline DCT) and the image size info exists in its beginning part.
- static final byte MARKER = (byte) 0xff;
+ private static final byte MARKER = (byte) 0xff;
private static final byte MARKER_SOI = (byte) 0xd8;
private static final byte MARKER_SOF0 = (byte) 0xc0;
private static final byte MARKER_SOF1 = (byte) 0xc1;
@@ -3856,26 +4006,23 @@ public class ExifInterfaceFork {
private static final byte MARKER_SOF14 = (byte) 0xce;
private static final byte MARKER_SOF15 = (byte) 0xcf;
private static final byte MARKER_SOS = (byte) 0xda;
+ @VisibleForTesting
static final byte MARKER_APP1 = (byte) 0xe1;
private static final byte MARKER_COM = (byte) 0xfe;
- static final byte MARKER_EOI = (byte) 0xd9;
+ private static final byte MARKER_EOI = (byte) 0xd9;
// Supported Image File Types
- static final int IMAGE_TYPE_UNKNOWN = 0;
- static final int IMAGE_TYPE_ARW = 1;
- static final int IMAGE_TYPE_CR2 = 2;
- static final int IMAGE_TYPE_DNG = 3;
- static final int IMAGE_TYPE_JPEG = 4;
- static final int IMAGE_TYPE_NEF = 5;
- static final int IMAGE_TYPE_NRW = 6;
- static final int IMAGE_TYPE_ORF = 7;
- static final int IMAGE_TYPE_PEF = 8;
- static final int IMAGE_TYPE_RAF = 9;
- static final int IMAGE_TYPE_RW2 = 10;
- static final int IMAGE_TYPE_SRW = 11;
- static final int IMAGE_TYPE_HEIF = 12;
- static final int IMAGE_TYPE_PNG = 13;
- static final int IMAGE_TYPE_WEBP = 14;
+ private static final int IMAGE_TYPE_UNKNOWN = 0;
+ private static final int IMAGE_TYPE_DNG = 3;
+ private static final int IMAGE_TYPE_JPEG = 4;
+ private static final int IMAGE_TYPE_ORF = 7;
+ private static final int IMAGE_TYPE_PEF = 8;
+ private static final int IMAGE_TYPE_RAF = 9;
+ private static final int IMAGE_TYPE_RW2 = 10;
+ private static final int IMAGE_TYPE_HEIC = 12;
+ private static final int IMAGE_TYPE_PNG = 13;
+ private static final int IMAGE_TYPE_WEBP = 14;
+ private static final int IMAGE_TYPE_AVIF = 15;
static {
sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
@@ -3925,9 +4072,16 @@ public class ExifInterfaceFork {
private int mOrfThumbnailOffset;
private int mOrfThumbnailLength;
private boolean mModified;
- // XMP data can be contained as either part of the EXIF data (tag number 700), or as a
- // separate data marker (a separate MARKER_APP1).
- private boolean mXmpIsFromSeparateMarker;
+
+ /**
+ * XMP data can occur as either part of the TIFF/Exif data (tag number 700), or as a separate
+ * section of the file (e.g. a separate APP1 segment in JPEG). XMP read from within the
+ * TIFF/Exif data is stored in {@link #mAttributes}, while XMP read from a separate section is
+ * here. If both are present, the disambiguation rules vary per file format, see
+ * {@link #getXmpHandlingForImageType(int)}.
+ */
+ @Nullable
+ private ExifAttribute mXmpFromSeparateMarker;
// Pattern to check non zero timestamp
private static final Pattern NON_ZERO_TIME_PATTERN = Pattern.compile(".*[1-9].*");
@@ -3989,13 +4143,13 @@ public class ExifInterfaceFork {
mFilename = null;
boolean isFdDuped = false;
- if (Build.VERSION.SDK_INT >= 21 && isSeekableFD(fileDescriptor)) {
+ if (isSeekableFD(fileDescriptor)) {
mSeekableFileDescriptor = fileDescriptor;
// Keep the original file descriptor in order to save attributes when it's seekable.
// Otherwise, just close the given file descriptor after reading it because the save
// feature won't be working.
try {
- fileDescriptor = Api21Impl.dup(fileDescriptor);
+ fileDescriptor = Os.dup(fileDescriptor);
isFdDuped = true;
} catch (Exception e) {
throw new IOException("Failed to duplicate file descriptor", e);
@@ -4106,9 +4260,7 @@ public class ExifInterfaceFork {
*
* @param tag the name of the tag.
*/
- @SuppressWarnings("deprecation")
- @Nullable
- private ExifAttribute getExifAttribute(@NonNull String tag) {
+ private @Nullable ExifAttribute getExifAttribute(@NonNull String tag) {
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
@@ -4120,6 +4272,11 @@ public class ExifInterfaceFork {
}
tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
}
+ if (TAG_XMP.equals(tag)
+ && getXmpHandlingForImageType(mMimeType) == XMP_HANDLING_PREFER_SEPARATE
+ && mXmpFromSeparateMarker != null) {
+ return mXmpFromSeparateMarker;
+ }
// Retrieves all tag groups. The value from primary image tag group has a higher priority
// than the value from the thumbnail tag group if there are more than one candidates.
for (int i = 0; i < EXIF_TAGS.length; ++i) {
@@ -4128,49 +4285,80 @@ public class ExifInterfaceFork {
return value;
}
}
+ if (TAG_XMP.equals(tag) && mXmpFromSeparateMarker != null) {
+ return mXmpFromSeparateMarker;
+ }
return null;
}
+ private static @XmpHandling int getXmpHandlingForImageType(int imageType) {
+ switch (imageType) {
+ // ExifInterface has a documented (but spec-violating) preference for reading and
+ // writing JPEG XMP data from Exif/TIFF tag 700 instead of a separate XMP APP1 segment
+ // if both are present.
+ case IMAGE_TYPE_JPEG:
+ return XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT;
+ case IMAGE_TYPE_AVIF:
+ case IMAGE_TYPE_HEIC:
+ // RAF stores XMP/Exif in JPEG, but we have no documented backwards-compat obligations
+ // so we can implement the spec to store XMP in a separate APP1 segment.
+ case IMAGE_TYPE_RAF:
+ return XMP_HANDLING_PREFER_SEPARATE;
+ case IMAGE_TYPE_DNG:
+ case IMAGE_TYPE_ORF:
+ case IMAGE_TYPE_PEF:
+ case IMAGE_TYPE_RW2:
+ case IMAGE_TYPE_UNKNOWN:
+ // PNG and WebP support a separate XMP chunk (so should be
+ // XMP_HANDLING_PREFER_SEPARATE), but ExifInterface doesn't currently read or write
+ // them.
+ case IMAGE_TYPE_PNG:
+ case IMAGE_TYPE_WEBP:
+ default:
+ return XMP_HANDLING_TIFF_700_ONLY;
+ }
+ }
+
/**
* Returns the value of the specified tag or {@code null} if there
* is no such tag in the image file.
*
* @param tag the name of the tag.
*/
- @Nullable
- public String getAttribute(@NonNull String tag) {
+ public @Nullable String getAttribute(@NonNull String tag) {
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
ExifAttribute attribute = getExifAttribute(tag);
- if (attribute != null) {
- if (!sTagSetForCompatibility.contains(tag)) {
- return attribute.getStringValue(mExifByteOrder);
+ if (attribute == null) {
+ return null;
+ }
+ if (tag.equals(TAG_GPS_TIMESTAMP)) {
+ // Convert GPS timestamp value to a custom format for backwards compatibility.
+ if (attribute.format != IFD_FORMAT_URATIONAL
+ && attribute.format != IFD_FORMAT_SRATIONAL) {
+ Log.w(TAG, "GPS Timestamp format is not rational. format=" + attribute.format);
+ return null;
}
- if (tag.equals(TAG_GPS_TIMESTAMP)) {
- // Convert the rational values to the custom formats for backwards compatibility.
- if (attribute.format != IFD_FORMAT_URATIONAL
- && attribute.format != IFD_FORMAT_SRATIONAL) {
- Log.w(TAG, "GPS Timestamp format is not rational. format=" + attribute.format);
- return null;
- }
- Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
- if (array == null || array.length != 3) {
- Log.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
- return null;
- }
- return String.format(Locale.ROOT, "%02d:%02d:%02d",
- (int) ((float) array[0].numerator / array[0].denominator),
- (int) ((float) array[1].numerator / array[1].denominator),
- (int) ((float) array[2].numerator / array[2].denominator));
+ Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
+ if (array == null || array.length != 3) {
+ Log.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
+ return null;
}
+ return String.format("%02d:%02d:%02d",
+ (int) ((float) array[0].numerator / array[0].denominator),
+ (int) ((float) array[1].numerator / array[1].denominator),
+ (int) ((float) array[2].numerator / array[2].denominator));
+ } else if (RATIONAL_TAGS_HANDLED_AS_DECIMALS_FOR_COMPATIBILITY.contains(tag)) {
+ // Convert the rational values to the custom formats for backwards compatibility.
try {
return Double.toString(attribute.getDoubleValue(mExifByteOrder));
} catch (NumberFormatException e) {
return null;
}
+ } else {
+ return attribute.getStringValue(mExifByteOrder);
}
- return null;
}
/**
@@ -4232,10 +4420,45 @@ public class ExifInterfaceFork {
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
- // Validate and convert if necessary.
- if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
- || TAG_DATETIME_DIGITIZED.equals(tag)) {
- if (value != null) {
+
+ // Maintain compatibility.
+ if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+ if (DEBUG) {
+ Log.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+ + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+ }
+ tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+ }
+ // Maybe convert the given value for backwards compatibility.
+ if (value != null) {
+ if (RATIONAL_TAGS_HANDLED_AS_DECIMALS_FOR_COMPATIBILITY.contains(tag)
+ && !value.contains("/")) {
+ // Convert floating point values to rational for rational tags that are emitted and
+ // consumed as floating point values for backwards compatibility.
+ try {
+ double doubleValue = Double.parseDouble(value);
+ value = Rational.createFromDouble(doubleValue).toString();
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ } else if (tag.equals(TAG_GPS_TIMESTAMP)) {
+ Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
+ if (!m.find()) {
+ Log.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ value =
+ Integer.parseInt(m.group(1))
+ + "/1,"
+ + Integer.parseInt(m.group(2))
+ + "/1,"
+ + Integer.parseInt(m.group(3))
+ + "/1";
+ } else if (TAG_DATETIME.equals(tag)
+ || TAG_DATETIME_ORIGINAL.equals(tag)
+ || TAG_DATETIME_DIGITIZED.equals(tag)) {
+ // Validate and convert datetime values if necessary.
boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(value).find();
// Validate
@@ -4245,8 +4468,8 @@ public class ExifInterfaceFork {
return;
}
// If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it to
- // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
- // official documentation.
+ // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the official
+ // documentation.
// See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
if (isSecondaryFormat) {
// Replace "-" with ":" to match the primary format.
@@ -4254,32 +4477,18 @@ public class ExifInterfaceFork {
}
}
}
- // Maintain compatibility.
- if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
- if (DEBUG) {
- Log.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
- + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
- }
- tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
- }
- // Convert the given value to rational values for backwards compatibility.
- if (value != null && sTagSetForCompatibility.contains(tag)) {
- if (tag.equals(TAG_GPS_TIMESTAMP)) {
- Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
- if (!m.find()) {
- Log.w(TAG, "Invalid value for " + tag + " : " + value);
- return;
- }
- value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1,"
- + Integer.parseInt(m.group(3)) + "/1";
- } else {
- try {
- double doubleValue = Double.parseDouble(value);
- value = new Rational(doubleValue).toString();
- } catch (NumberFormatException e) {
- Log.w(TAG, "Invalid value for " + tag + " : " + value);
- return;
- }
+
+ if (TAG_XMP.equals(tag)) {
+ boolean containsTiff700Xmp =
+ mAttributes[IFD_TYPE_PRIMARY].containsKey(TAG_XMP)
+ || mAttributes[IFD_TYPE_PREVIEW].containsKey(TAG_XMP);
+ @XmpHandling int xmpHandling = getXmpHandlingForImageType(mMimeType);
+ if ((xmpHandling == XMP_HANDLING_PREFER_SEPARATE
+ && (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
+ || (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
+ && !containsTiff700Xmp)) {
+ mXmpFromSeparateMarker = ExifAttribute.createByte(value);
+ return;
}
}
@@ -4600,8 +4809,8 @@ public class ExifInterfaceFork {
return;
}
} else {
- if (mMimeType == IMAGE_TYPE_HEIF) {
- getHeifAttributes(inputStream);
+ if (mMimeType == IMAGE_TYPE_HEIC || mMimeType == IMAGE_TYPE_AVIF) {
+ getHeifAttributes(inputStream, mMimeType);
} else if (mMimeType == IMAGE_TYPE_ORF) {
getOrfAttributes(inputStream);
} else if (mMimeType == IMAGE_TYPE_RW2) {
@@ -4630,7 +4839,7 @@ public class ExifInterfaceFork {
// Ignore exceptions in order to keep the compatibility with the old versions of
// ExifInterface.
if (DEBUG) {
- Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file"
+ Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file "
+ "(ExifInterface supports JPEG and some RAW image formats only) "
+ "or a corrupted JPEG file to ExifInterface.", e);
}
@@ -4644,18 +4853,15 @@ public class ExifInterfaceFork {
}
private static boolean isSeekableFD(FileDescriptor fd) {
- if (Build.VERSION.SDK_INT >= 21) {
- try {
- Api21Impl.lseek(fd, 0, OsConstants.SEEK_CUR);
- return true;
- } catch (Exception e) {
- if (DEBUG) {
- Log.d(TAG, "The file descriptor for the given input is not seekable");
- }
- return false;
+ try {
+ Os.lseek(fd, /* offset= */ 0, /* whence= */ OsConstants.SEEK_CUR);
+ return true;
+ } catch (Exception e) {
+ if (DEBUG) {
+ Log.d(TAG, "The file descriptor for the given input is not seekable");
}
+ return false;
}
- return false;
}
// Prints out attributes for debugging.
@@ -4719,12 +4925,11 @@ public class ExifInterfaceFork {
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);
- }
+ Os.lseek(
+ mSeekableFileDescriptor,
+ /* offset= */ 0,
+ /* whence= */ OsConstants.SEEK_SET);
+ in = new FileInputStream(mSeekableFileDescriptor);
}
out = new FileOutputStream(tempFile);
copy(in, out);
@@ -4746,12 +4951,9 @@ public class ExifInterfaceFork {
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);
- }
+ FileDescriptor fd = mSeekableFileDescriptor;
+ Os.lseek(fd, /* offset= */ 0, /* whence= */ OsConstants.SEEK_SET);
+ out = new FileOutputStream(mSeekableFileDescriptor);
}
bufferedIn = new BufferedInputStream(in);
bufferedOut = new BufferedOutputStream(out);
@@ -4769,12 +4971,11 @@ public class ExifInterfaceFork {
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);
- }
+ Os.lseek(
+ mSeekableFileDescriptor,
+ /* offset= */ 0,
+ /* whence= */ OsConstants.SEEK_SET);
+ out = new FileOutputStream(mSeekableFileDescriptor);
}
copy(in, out);
} catch (Exception exception) {
@@ -4820,8 +5021,7 @@ public class ExifInterfaceFork {
* The returned data can be decoded using
* {@link BitmapFactory#decodeByteArray(byte[], int, int)}
*/
- @Nullable
- public byte[] getThumbnail() {
+ public byte @Nullable [] getThumbnail() {
if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
return getThumbnailBytes();
}
@@ -4832,8 +5032,7 @@ public class ExifInterfaceFork {
* Returns the thumbnail bytes inside the image file, regardless of the compression type of the
* thumbnail image.
*/
- @Nullable
- public byte[] getThumbnailBytes() {
+ public byte @Nullable [] getThumbnailBytes() {
if (!mHasThumbnail) {
return null;
}
@@ -4856,13 +5055,9 @@ public class ExifInterfaceFork {
} 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);
- }
+ newFileDescriptor = Os.dup(mSeekableFileDescriptor);
+ Os.lseek(newFileDescriptor, /* offset= */ 0, /* whence= */ OsConstants.SEEK_SET);
+ in = new FileInputStream(newFileDescriptor);
}
if (in == null) {
// Should not be reached this.
@@ -4892,8 +5087,7 @@ public class ExifInterfaceFork {
* 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() {
+ public @Nullable Bitmap getThumbnailBitmap() {
if (!mHasThumbnail) {
return null;
} else if (mThumbnailBytes == null) {
@@ -4950,8 +5144,7 @@ public class ExifInterfaceFork {
* called since the underlying file was initially parsed, since
* that means offsets may have changed.
*/
- @Nullable
- public long[] getThumbnailRange() {
+ public long @Nullable [] getThumbnailRange() {
if (mModified) {
throw new IllegalStateException(
"The underlying file has been modified since being parsed");
@@ -4976,8 +5169,7 @@ public class ExifInterfaceFork {
* called since the underlying file was initially parsed, since
* that means offsets may have changed.
*/
- @Nullable
- public long[] getAttributeRange(@NonNull String tag) {
+ public long @Nullable [] getAttributeRange(@NonNull String tag) {
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
@@ -5001,8 +5193,7 @@ public class ExifInterfaceFork {
* @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) {
+ public byte @Nullable [] getAttributeBytes(@NonNull String tag) {
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
@@ -5039,8 +5230,7 @@ public class ExifInterfaceFork {
* array where the first element is the latitude and the second element is the longitude.
* Otherwise, it returns null.
*/
- @Nullable
- public double[] getLatLong() {
+ public double @Nullable [] getLatLong() {
String latValue = getAttribute(TAG_GPS_LATITUDE);
String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
String lngValue = getAttribute(TAG_GPS_LONGITUDE);
@@ -5077,8 +5267,10 @@ public class ExifInterfaceFork {
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());
+ setAttribute(
+ TAG_GPS_SPEED,
+ Rational.createFromDouble(location.getSpeed() * TimeUnit.HOURS.toSeconds(1) / 1000)
+ .toString());
String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
@@ -5130,7 +5322,7 @@ public class ExifInterfaceFork {
*/
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, Rational.createFromDouble(Math.abs(altitude)).toString());
setAttribute(TAG_GPS_ALTITUDE_REF, ref);
}
@@ -5168,8 +5360,7 @@ public class ExifInterfaceFork {
* @return null if date time information is unavailable or invalid.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- @Nullable
- public Long getDateTime() {
+ public @Nullable Long getDateTime() {
return parseDateTime(getAttribute(TAG_DATETIME),
getAttribute(TAG_SUBSEC_TIME),
getAttribute(TAG_OFFSET_TIME));
@@ -5185,8 +5376,7 @@ public class ExifInterfaceFork {
* @return null if digitized date time information is unavailable or invalid.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- @Nullable
- public Long getDateTimeDigitized() {
+ public @Nullable Long getDateTimeDigitized() {
return parseDateTime(getAttribute(TAG_DATETIME_DIGITIZED),
getAttribute(TAG_SUBSEC_TIME_DIGITIZED),
getAttribute(TAG_OFFSET_TIME_DIGITIZED));
@@ -5202,8 +5392,7 @@ public class ExifInterfaceFork {
* @return null if original date time information is unavailable or invalid.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- @Nullable
- public Long getDateTimeOriginal() {
+ public @Nullable Long getDateTimeOriginal() {
return parseDateTime(getAttribute(TAG_DATETIME_ORIGINAL),
getAttribute(TAG_SUBSEC_TIME_ORIGINAL),
getAttribute(TAG_OFFSET_TIME_ORIGINAL));
@@ -5253,8 +5442,7 @@ public class ExifInterfaceFork {
* @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() {
+ public @Nullable Long getGpsDateTime() {
String date = getAttribute(TAG_GPS_DATESTAMP);
String time = getAttribute(TAG_GPS_TIMESTAMP);
if (date == null || time == null
@@ -5328,7 +5516,7 @@ public class ExifInterfaceFork {
}
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
// Not valid
- throw new IllegalArgumentException();
+ throw new IllegalArgumentException(e);
}
}
@@ -5347,17 +5535,24 @@ public class ExifInterfaceFork {
in.reset();
if (isJpegFormat(signatureCheckBytes)) {
return IMAGE_TYPE_JPEG;
- } else if (isRafFormat(signatureCheckBytes)) {
+ }
+ if (isRafFormat(signatureCheckBytes)) {
return IMAGE_TYPE_RAF;
- } else if (isHeifFormat(signatureCheckBytes)) {
- return IMAGE_TYPE_HEIF;
- } else if (isOrfFormat(signatureCheckBytes)) {
+ }
+ int heicOrAvifImageType = isHeicOrAvifFormat(signatureCheckBytes);
+ if (heicOrAvifImageType != IMAGE_TYPE_UNKNOWN) {
+ return heicOrAvifImageType;
+ }
+ if (isOrfFormat(signatureCheckBytes)) {
return IMAGE_TYPE_ORF;
- } else if (isRw2Format(signatureCheckBytes)) {
+ }
+ if (isRw2Format(signatureCheckBytes)) {
return IMAGE_TYPE_RW2;
- } else if (isPngFormat(signatureCheckBytes)) {
+ }
+ if (isPngFormat(signatureCheckBytes)) {
return IMAGE_TYPE_PNG;
- } else if (isWebpFormat(signatureCheckBytes)) {
+ }
+ if (isWebpFormat(signatureCheckBytes)) {
return IMAGE_TYPE_WEBP;
}
// Certain file formats (PEF) are identified in readImageFileDirectory()
@@ -5393,7 +5588,7 @@ public class ExifInterfaceFork {
return true;
}
- private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
+ private int isHeicOrAvifFormat(byte[] signatureCheckBytes) throws IOException {
ByteOrderedDataInputStream signatureInputStream = null;
try {
signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
@@ -5403,7 +5598,7 @@ public class ExifInterfaceFork {
signatureInputStream.readFully(chunkType);
if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) {
- return false;
+ return IMAGE_TYPE_UNKNOWN;
}
long chunkDataOffset = 8;
@@ -5413,7 +5608,7 @@ public class ExifInterfaceFork {
chunkSize = signatureInputStream.readLong();
if (chunkSize < 16) {
// The smallest valid chunk is 16 bytes long in this case.
- return false;
+ return IMAGE_TYPE_UNKNOWN;
}
chunkDataOffset += 8;
}
@@ -5428,17 +5623,18 @@ public class ExifInterfaceFork {
// 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;
+ return IMAGE_TYPE_UNKNOWN;
}
byte[] brand = new byte[4];
boolean isMif1 = false;
boolean isHeic = false;
+ boolean isAvif = false;
for (long i = 0; i < chunkDataSize / 4; ++i) {
try {
signatureInputStream.readFully(brand);
} catch (EOFException e) {
- return false;
+ return IMAGE_TYPE_UNKNOWN;
}
if (i == 1) {
// Skip this index, it refers to the minorVersion, not a brand.
@@ -5448,9 +5644,16 @@ public class ExifInterfaceFork {
isMif1 = true;
} else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) {
isHeic = true;
+ } else if (Arrays.equals(brand, HEIF_BRAND_AVIF)
+ || Arrays.equals(brand, HEIF_BRAND_AVIS)) {
+ isAvif = true;
}
- if (isMif1 && isHeic) {
- return true;
+ if (isMif1) {
+ if (isHeic) {
+ return IMAGE_TYPE_HEIC;
+ } else if (isAvif) {
+ return IMAGE_TYPE_AVIF;
+ }
}
}
} catch (Exception e) {
@@ -5463,7 +5666,7 @@ public class ExifInterfaceFork {
signatureInputStream = null;
}
}
- return false;
+ return IMAGE_TYPE_UNKNOWN;
}
/**
@@ -5636,13 +5839,8 @@ public class ExifInterfaceFork {
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;
- }
+ mXmpFromSeparateMarker =
+ new ExifAttribute(IFD_FORMAT_BYTE, value.length, offset, value);
}
break;
}
@@ -5805,9 +6003,15 @@ public class ExifInterfaceFork {
}
// 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 {
+ // MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH was added in SDK 28 for HEIC and in SDK 31
+ // for AVIF.
+ private void getHeifAttributes(final SeekableByteOrderedDataInputStream in, int imageType)
+ throws IOException {
if (Build.VERSION.SDK_INT >= 28) {
+ if (imageType == IMAGE_TYPE_AVIF && Build.VERSION.SDK_INT < 31) {
+ throw new UnsupportedOperationException("Reading EXIF from AVIF files "
+ + "is supported from SDK 31 and above");
+ }
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
Api23Impl.setDataSource(retriever, new MediaDataSource() {
@@ -5864,14 +6068,10 @@ public class ExifInterfaceFork {
}
});
- 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 exifOffsetStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET);
+ String exifLengthStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH);
String hasImage = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
String hasVideo = retriever.extractMetadata(
@@ -5953,23 +6153,18 @@ public class ExifInterfaceFork {
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);
- }
+ String xmpOffsetStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_XMP_OFFSET);
+ String 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));
- }
+ mXmpFromSeparateMarker =
+ new ExifAttribute(IFD_FORMAT_BYTE, xmpBytes.length, offset, xmpBytes);
}
if (DEBUG) {
@@ -5977,7 +6172,7 @@ public class ExifInterfaceFork {
}
} catch (RuntimeException e) {
throw new UnsupportedOperationException("Failed to read EXIF from HEIF file. "
- + "Given stream is either malformed or unsupported.");
+ + "Given stream is either malformed or unsupported.", e);
} finally {
try {
retriever.release();
@@ -5986,7 +6181,7 @@ public class ExifInterfaceFork {
}
}
} else {
- throw new UnsupportedOperationException("Reading EXIF from HEIF files "
+ throw new UnsupportedOperationException("Reading EXIF from HEIC files "
+ "is supported from SDK 28 and above");
}
}
@@ -6141,11 +6336,10 @@ public class ExifInterfaceFork {
// 2.1. Integers and byte order
in.setByteOrder(BIG_ENDIAN);
- int bytesRead = 0;
+ int startPosition = in.position();
// 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
@@ -6160,27 +6354,30 @@ public class ExifInterfaceFork {
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;
+ int type = in.readInt();
// 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 (in.position() - startPosition == 16 && 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)) {
+ if (type == PNG_CHUNK_TYPE_IEND) {
// IEND marks the end of the image.
break;
- } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
+ } else if (type == PNG_CHUNK_TYPE_EXIF) {
+ // Save offset to EXIF data for handling thumbnail and attribute offsets.
+ mOffsetToExifData = in.position() - startPosition;
+
// TLAD start
if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous exif chunk size=" + length);
}
// TLAD end
+
+ // TODO: Need to handle potential OutOfMemoryError
byte[] data = new byte[length];
in.readFully(data);
@@ -6188,15 +6385,13 @@ public class ExifInterfaceFork {
int dataCrcValue = in.readInt();
// Cyclic Redundancy Code used to check for corruption of the data
CRC32 crc = new CRC32();
- crc.update(type);
+ updateCrcWithInt(crc, 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();
@@ -6205,16 +6400,22 @@ public class ExifInterfaceFork {
} 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.");
+ throw new IOException("Encountered corrupt PNG file.", e);
}
}
+ private static void updateCrcWithInt(CRC32 crc, int value) {
+ crc.update(value >>> 24);
+ crc.update(value >>> 16);
+ crc.update(value >>> 8);
+ crc.update(value);
+ }
+
// 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
@@ -6294,7 +6495,7 @@ public class ExifInterfaceFork {
} 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.");
+ throw new IOException("Encountered corrupt WebP file.", e);
}
}
@@ -6318,22 +6519,20 @@ public class ExifInterfaceFork {
}
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);
+ if (mXmpFromSeparateMarker != null) {
+ // Write XMP APP1 segment. The XMP spec (part 3, section 1.1.3) recommends for this to
+ // directly follow the Exif APP1 segment.
+ dataOutputStream.write(MARKER);
+ dataOutputStream.writeByte(MARKER_APP1);
+ int length = 2 + IDENTIFIER_XMP_APP1.length + mXmpFromSeparateMarker.bytes.length;
+ dataOutputStream.writeUnsignedShort(length);
+ dataOutputStream.write(IDENTIFIER_XMP_APP1);
+ dataOutputStream.write(mXmpFromSeparateMarker.bytes);
}
byte[] bytes = new byte[4096];
@@ -6350,12 +6549,22 @@ public class ExifInterfaceFork {
if (length < 0) {
throw new IOException("Invalid length");
}
- byte[] identifier = new byte[6];
- if (length >= 6) {
+ // If the length is long enough, we read enough bytes for the XMP identifier,
+ // because it's longer than the EXIF one.
+ byte[] identifier;
+ if (length >= IDENTIFIER_XMP_APP1.length) {
+ identifier = new byte[IDENTIFIER_XMP_APP1.length];
+ } else if (length >= IDENTIFIER_EXIF_APP1.length) {
+ identifier = new byte[IDENTIFIER_EXIF_APP1.length];
+ } else {
+ identifier = null;
+ }
+ if (identifier != null) {
dataInputStream.readFully(identifier);
- if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
- // Skip the original EXIF APP1 segment.
- dataInputStream.skipFully(length - 6);
+ if (startsWith(identifier, IDENTIFIER_EXIF_APP1)
+ || startsWith(identifier, IDENTIFIER_XMP_APP1)) {
+ // Skip the original EXIF or XMP APP1 segment.
+ dataInputStream.skipFully(length - identifier.length);
break;
}
}
@@ -6363,8 +6572,8 @@ public class ExifInterfaceFork {
dataOutputStream.writeByte(MARKER);
dataOutputStream.writeByte(marker);
dataOutputStream.writeUnsignedShort(length + 2);
- if (length >= 6) {
- length -= 6;
+ if (identifier != null) {
+ length -= identifier.length;
dataOutputStream.write(identifier);
}
int read;
@@ -6632,8 +6841,8 @@ public class ExifInterfaceFork {
// Retrieve image width/height
widthAndHeight = totalInputStream.readInt();
- width = (widthAndHeight << 18) >> 18;
- height = (widthAndHeight << 2) >> 18;
+ width = widthAndHeight & 0x3FFF;
+ height = (widthAndHeight >> 16) & 0x3FFF;
bytesToRead -= (vp8Frame.length + vp8Signature.length + 4);
} else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
// Check signature
@@ -7554,7 +7763,7 @@ public class ExifInterfaceFork {
case IMAGE_TYPE_PNG:
// Write PNG specific data (chunk size, chunk type)
dataOutputStream.writeInt(totalSize);
- dataOutputStream.write(PNG_CHUNK_TYPE_EXIF);
+ dataOutputStream.writeInt(PNG_CHUNK_TYPE_EXIF);
break;
case IMAGE_TYPE_WEBP:
// Write WebP specific data (chunk type, chunk size)
diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
index df7ed9320..301e306fa 100644
--- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
+++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
@@ -18,12 +18,10 @@ package androidx.exifinterface.media;
import android.media.MediaDataSource;
import android.media.MediaMetadataRetriever;
-import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
-import androidx.annotation.DoNotInline;
import androidx.annotation.RequiresApi;
import java.io.Closeable;
@@ -32,12 +30,19 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+/*
+ * Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
+ * Named differently to let ExifInterface be loaded as subdependency.
+ * cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
+ */
+
class ExifInterfaceUtilsFork {
private static final String TAG = "ExifInterfaceUtils";
private ExifInterfaceUtilsFork() {
// Prevent instantiation
}
+
/**
* Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
* Returns the total number of bytes transferred.
@@ -146,45 +151,18 @@ class ExifInterfaceUtilsFork {
* Closes a file descriptor that has been duplicated.
*/
static void closeFileDescriptor(FileDescriptor fd) {
- // Os.dup and Os.close was introduced in API 21 so this method shouldn't be called
- // in API < 21.
- if (Build.VERSION.SDK_INT >= 21) {
- try {
- Api21Impl.close(fd);
- // Catching ErrnoException will raise error in API < 21
- } catch (Exception ex) {
- Log.e(TAG, "Error closing fd.");
- }
- } else {
- Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
- }
- }
-
- @RequiresApi(21)
- static class Api21Impl {
- private Api21Impl() {}
-
- @DoNotInline
- static FileDescriptor dup(FileDescriptor fileDescriptor) throws ErrnoException {
- return Os.dup(fileDescriptor);
- }
-
- @DoNotInline
- static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
- return Os.lseek(fd, offset, whence);
- }
-
- @DoNotInline
- static void close(FileDescriptor fd) throws ErrnoException {
+ try {
Os.close(fd);
+ } catch (ErrnoException ex) {
+ Log.e(TAG, "Error closing fd.", ex);
}
}
@RequiresApi(23)
static class Api23Impl {
- private Api23Impl() {}
+ private Api23Impl() {
+ }
- @DoNotInline
static void setDataSource(MediaMetadataRetriever retriever, MediaDataSource dataSource) {
retriever.setDataSource(dataSource);
}