Revert "Revert "ExifInterface: switch from using jhead to java implementation""
This reverts commit 3c233ee291cfe1b765184920b6b7a69cb1bb82d9.
Bug: 27349581
Change-Id: Ib9f687e79ec36692fa7505ba3852099eb3e44573
diff --git a/api/current.txt b/api/current.txt
index 8edab67..d6a1660 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20188,6 +20188,8 @@
public class ExifInterface {
ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+ ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+ ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
method public double getAltitude(double);
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
@@ -20209,6 +20211,10 @@
field public static final java.lang.String TAG_APERTURE = "FNumber";
field public static final java.lang.String TAG_DATETIME = "DateTime";
field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+ field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+ field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
field public static final java.lang.String TAG_FLASH = "Flash";
field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -20224,9 +20230,12 @@
field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+ field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
field public static final java.lang.String TAG_MAKE = "Make";
+ field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
field public static final java.lang.String TAG_MODEL = "Model";
field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+ field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/api/system-current.txt b/api/system-current.txt
index 54ddf49..4b4bee8 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -21687,6 +21687,8 @@
public class ExifInterface {
ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+ ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+ ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
method public double getAltitude(double);
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
@@ -21708,6 +21710,10 @@
field public static final java.lang.String TAG_APERTURE = "FNumber";
field public static final java.lang.String TAG_DATETIME = "DateTime";
field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+ field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+ field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
field public static final java.lang.String TAG_FLASH = "Flash";
field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -21723,9 +21729,12 @@
field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+ field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
field public static final java.lang.String TAG_MAKE = "Make";
+ field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
field public static final java.lang.String TAG_MODEL = "Model";
field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+ field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/api/test-current.txt b/api/test-current.txt
index 6626501..baadc15 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -20197,6 +20197,8 @@
public class ExifInterface {
ctor public ExifInterface(java.lang.String) throws java.io.IOException;
+ ctor public ExifInterface(java.io.FileDescriptor) throws java.io.IOException;
+ ctor public ExifInterface(java.io.InputStream) throws java.io.IOException;
method public double getAltitude(double);
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
@@ -20218,6 +20220,10 @@
field public static final java.lang.String TAG_APERTURE = "FNumber";
field public static final java.lang.String TAG_DATETIME = "DateTime";
field public static final java.lang.String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
+ field public static final java.lang.String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ field public static final java.lang.String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ field public static final java.lang.String TAG_EXPOSURE_MODE = "ExposureMode";
+ field public static final java.lang.String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
field public static final java.lang.String TAG_EXPOSURE_TIME = "ExposureTime";
field public static final java.lang.String TAG_FLASH = "Flash";
field public static final java.lang.String TAG_FOCAL_LENGTH = "FocalLength";
@@ -20233,9 +20239,12 @@
field public static final java.lang.String TAG_IMAGE_LENGTH = "ImageLength";
field public static final java.lang.String TAG_IMAGE_WIDTH = "ImageWidth";
field public static final java.lang.String TAG_ISO = "ISOSpeedRatings";
+ field public static final java.lang.String TAG_LIGHT_SOURCE = "LightSource";
field public static final java.lang.String TAG_MAKE = "Make";
+ field public static final java.lang.String TAG_METERING_MODE = "MeteringMode";
field public static final java.lang.String TAG_MODEL = "Model";
field public static final java.lang.String TAG_ORIENTATION = "Orientation";
+ field public static final java.lang.String TAG_SUBJECT_DISTANCE = "SubjectDistance";
field public static final java.lang.String TAG_SUBSEC_TIME = "SubSecTime";
field public static final java.lang.String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
field public static final java.lang.String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 7fb67ee..1a387be 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -16,22 +16,51 @@
package android.media;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.regex.Pattern;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
/**
* This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
* <p>
* Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF and RAF.
+ * <p>
+ * Attribute mutation is supported for JPEG image files.
*/
public class ExifInterface {
+ private static final String TAG = "ExifInterface";
+ private static final boolean DEBUG = false;
+
// The Exif tag names
/** Type is int. */
public static final String TAG_ORIENTATION = "Orientation";
@@ -97,6 +126,20 @@
public static final String TAG_FOCAL_LENGTH = "FocalLength";
/** Type is String. Name of GPS processing method used for location finding. */
public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
+ /** Type is double. */
+ public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+ /** Type is double. */
+ public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
+ /** Type is double. */
+ public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
+ /** Type is int. */
+ public static final String TAG_LIGHT_SOURCE = "LightSource";
+ /** Type is int. */
+ public static final String TAG_METERING_MODE = "MeteringMode";
+ /** Type is int. */
+ public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
+ /** Type is int. */
+ public static final String TAG_EXPOSURE_MODE = "ExposureMode";
// Private tags used for thumbnail information.
private static final String TAG_HAS_THUMBNAIL = "hasThumbnail";
@@ -119,30 +162,321 @@
// Constants used for white balance
public static final int WHITEBALANCE_AUTO = 0;
public static final int WHITEBALANCE_MANUAL = 1;
+
private static SimpleDateFormat sFormatter;
+ // See Exchangeable image file format for digital still cameras: Exif version 2.2.
+ // The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
+ // They are called "Image File Directory". They have multiple data formats to cover various
+ // image metadata from GPS longitude to camera model name.
+
+ // Types of Exif byte alignments (see JEITA CP-3451 page 10)
+ private static final short BYTE_ALIGN_II = 0x4949; // II: Intel order
+ private static final short BYTE_ALIGN_MM = 0x4d4d; // MM: Motorola order
+
+ // Formats for the value in IFD entry (See TIFF 6.0 spec Types page 15).
+ 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;
+ // Sizes of the components of each IFD value format
+ private static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
+ 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8
+ };
+ private static final byte[] EXIF_ASCII_PREFIX = new byte[] {
+ 0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
+ };
+
+ // A class for indicating EXIF tag.
+ private static class ExifTag {
+ public final int number;
+ public final String name;
+
+ private ExifTag(String name, int number) {
+ this.name = name;
+ this.number = number;
+ }
+ }
+
+ // Primary image IFD TIFF tags (See JEITA CP-3451 Table 14. page 54).
+ private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] {
+ new ExifTag("ImageWidth", 256),
+ new ExifTag("ImageLength", 257),
+ new ExifTag("BitsPerSample", 258),
+ new ExifTag("Compression", 259),
+ new ExifTag("PhotometricInterpretation", 262),
+ new ExifTag("ImageDescription", 270),
+ new ExifTag("Make", 271),
+ new ExifTag("Model", 272),
+ new ExifTag("StripOffsets", 273),
+ new ExifTag("Orientation", 274),
+ new ExifTag("SamplesPerPixel", 277),
+ new ExifTag("RowsPerStrip", 278),
+ new ExifTag("StripByteCounts", 279),
+ new ExifTag("XResolution", 282),
+ new ExifTag("YResolution", 283),
+ new ExifTag("PlanarConfiguration", 284),
+ new ExifTag("ResolutionUnit", 296),
+ new ExifTag("TransferFunction", 301),
+ new ExifTag("Software", 305),
+ new ExifTag("DateTime", 306),
+ new ExifTag("Artist", 315),
+ new ExifTag("WhitePoint", 318),
+ new ExifTag("PrimaryChromaticities", 319),
+ new ExifTag("JPEGInterchangeFormat", 513),
+ new ExifTag("JPEGInterchangeFormatLength", 514),
+ new ExifTag("YCbCrCoefficients", 529),
+ new ExifTag("YCbCrSubSampling", 530),
+ new ExifTag("YCbCrPositioning", 531),
+ new ExifTag("ReferenceBlackWhite", 532),
+ new ExifTag("Copyright", 33432),
+ new ExifTag("ExifIFDPointer", 34665),
+ new ExifTag("GPSInfoIFDPointer", 34853),
+ };
+ // Primary image IFD Exif Private tags (See JEITA CP-3451 Table 15. page 55).
+ private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] {
+ new ExifTag("ExposureTime", 33434),
+ new ExifTag("FNumber", 33437),
+ new ExifTag("ExposureProgram", 34850),
+ new ExifTag("SpectralSensitivity", 34852),
+ new ExifTag("ISOSpeedRatings", 34855),
+ new ExifTag("OECF", 34856),
+ new ExifTag("ExifVersion", 36864),
+ new ExifTag("DateTimeOriginal", 36867),
+ new ExifTag("DateTimeDigitized", 36868),
+ new ExifTag("ComponentsConfiguration", 37121),
+ new ExifTag("CompressedBitsPerPixel", 37122),
+ new ExifTag("ShutterSpeedValue", 37377),
+ new ExifTag("ApertureValue", 37378),
+ new ExifTag("BrightnessValue", 37379),
+ new ExifTag("ExposureBiasValue", 37380),
+ new ExifTag("MaxApertureValue", 37381),
+ new ExifTag("SubjectDistance", 37382),
+ new ExifTag("MeteringMode", 37383),
+ new ExifTag("LightSource", 37384),
+ new ExifTag("Flash", 37385),
+ new ExifTag("FocalLength", 37386),
+ new ExifTag("SubjectArea", 37396),
+ new ExifTag("MakerNote", 37500),
+ new ExifTag("UserComment", 37510),
+ new ExifTag("SubSecTime", 37520),
+ new ExifTag("SubSecTimeOriginal", 37521),
+ new ExifTag("SubSecTimeDigitized", 37522),
+ new ExifTag("FlashpixVersion", 40960),
+ new ExifTag("ColorSpace", 40961),
+ new ExifTag("PixelXDimension", 40962),
+ new ExifTag("PixelYDimension", 40963),
+ new ExifTag("RelatedSoundFile", 40964),
+ new ExifTag("InteroperabilityIFDPointer", 40965),
+ new ExifTag("FlashEnergy", 41483),
+ new ExifTag("SpatialFrequencyResponse", 41484),
+ new ExifTag("FocalPlaneXResolution", 41486),
+ new ExifTag("FocalPlaneYResolution", 41487),
+ new ExifTag("FocalPlaneResolutionUnit", 41488),
+ new ExifTag("SubjectLocation", 41492),
+ new ExifTag("ExposureIndex", 41493),
+ new ExifTag("SensingMethod", 41495),
+ new ExifTag("FileSource", 41728),
+ new ExifTag("SceneType", 41729),
+ new ExifTag("CFAPattern", 41730),
+ new ExifTag("CustomRendered", 41985),
+ new ExifTag("ExposureMode", 41986),
+ new ExifTag("WhiteBalance", 41987),
+ new ExifTag("DigitalZoomRatio", 41988),
+ new ExifTag("FocalLengthIn35mmFilm", 41989),
+ new ExifTag("SceneCaptureType", 41990),
+ new ExifTag("GainControl", 41991),
+ new ExifTag("Contrast", 41992),
+ new ExifTag("Saturation", 41993),
+ new ExifTag("Sharpness", 41994),
+ new ExifTag("DeviceSettingDescription", 41995),
+ new ExifTag("SubjectDistanceRange", 41996),
+ new ExifTag("ImageUniqueID", 42016),
+ };
+ // Primary image IFD GPS Info tags (See JEITA CP-3451 Table 16. page 56).
+ private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
+ new ExifTag("GPSVersionID", 0),
+ new ExifTag("GPSLatitudeRef", 1),
+ new ExifTag("GPSLatitude", 2),
+ new ExifTag("GPSLongitudeRef", 3),
+ new ExifTag("GPSLongitude", 4),
+ new ExifTag("GPSAltitudeRef", 5),
+ new ExifTag("GPSAltitude", 6),
+ new ExifTag("GPSTimeStamp", 7),
+ new ExifTag("GPSSatellites", 8),
+ new ExifTag("GPSStatus", 9),
+ new ExifTag("GPSMeasureMode", 10),
+ new ExifTag("GPSDOP", 11),
+ new ExifTag("GPSSpeedRef", 12),
+ new ExifTag("GPSSpeed", 13),
+ new ExifTag("GPSTrackRef", 14),
+ new ExifTag("GPSTrack", 15),
+ new ExifTag("GPSImgDirectionRef", 16),
+ new ExifTag("GPSImgDirection", 17),
+ new ExifTag("GPSMapDatum", 18),
+ new ExifTag("GPSDestLatitudeRef", 19),
+ new ExifTag("GPSDestLatitude", 20),
+ new ExifTag("GPSDestLongitudeRef", 21),
+ new ExifTag("GPSDestLongitude", 22),
+ new ExifTag("GPSDestBearingRef", 23),
+ new ExifTag("GPSDestBearing", 24),
+ new ExifTag("GPSDestDistanceRef", 25),
+ new ExifTag("GPSDestDistance", 26),
+ new ExifTag("GPSProcessingMethod", 27),
+ new ExifTag("GPSAreaInformation", 28),
+ new ExifTag("GPSDateStamp", 29),
+ new ExifTag("GPSDifferential", 30),
+ };
+ // Primary image IFD Interoperability tag (See JEITA CP-3451 Table 17. page 56).
+ private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
+ new ExifTag("InteroperabilityIndex", 1),
+ };
+ // IFD Thumbnail tags (See JEITA CP-3451 Table 18. page 57).
+ private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
+ new ExifTag("ThumbnailImageWidth", 256),
+ new ExifTag("ThumbnailImageLength", 257),
+ new ExifTag("BitsPerSample", 258),
+ new ExifTag("Compression", 259),
+ new ExifTag("PhotometricInterpretation", 262),
+ new ExifTag("ImageDescription", 270),
+ new ExifTag("Make", 271),
+ new ExifTag("Model", 272),
+ new ExifTag("StripOffsets", 273),
+ new ExifTag("Orientation", 274),
+ new ExifTag("SamplesPerPixel", 277),
+ new ExifTag("RowsPerStrip", 278),
+ new ExifTag("StripByteCounts", 279),
+ new ExifTag("XResolution", 282),
+ new ExifTag("YResolution", 283),
+ new ExifTag("PlanarConfiguration", 284),
+ new ExifTag("ResolutionUnit", 296),
+ new ExifTag("TransferFunction", 301),
+ new ExifTag("Software", 305),
+ new ExifTag("DateTime", 306),
+ new ExifTag("Artist", 315),
+ new ExifTag("WhitePoint", 318),
+ new ExifTag("PrimaryChromaticities", 319),
+ new ExifTag("JPEGInterchangeFormat", 513),
+ new ExifTag("JPEGInterchangeFormatLength", 514),
+ new ExifTag("YCbCrCoefficients", 529),
+ new ExifTag("YCbCrSubSampling", 530),
+ new ExifTag("YCbCrPositioning", 531),
+ new ExifTag("ReferenceBlackWhite", 532),
+ new ExifTag("Copyright", 33432),
+ new ExifTag("ExifIFDPointer", 34665),
+ new ExifTag("GPSInfoIFDPointer", 34853),
+ };
+
+ // See JEITA CP-3451 Figure 5. page 9.
+ // The following values are used for indicating pointers to the other Image File Directorys.
+
+ // Indices of Exif Ifd tag groups
+ private static final int IFD_TIFF_HINT = 0;
+ private static final int IFD_EXIF_HINT = 1;
+ private static final int IFD_GPS_HINT = 2;
+ private static final int IFD_INTEROPERABILITY_HINT = 3;
+ private static final int IFD_THUMBNAIL_HINT = 4;
+ // List of Exif tag groups
+ private static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
+ IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
+ IFD_THUMBNAIL_TAGS
+ };
+ // List of tags for pointing to the other image file directory offset.
+ private static final ExifTag[] IFD_POINTER_TAGS = new ExifTag[] {
+ new ExifTag("ExifIFDPointer", 34665),
+ new ExifTag("GPSInfoPointer", 34853),
+ new ExifTag("InteroperabilityIFDPointer", 40965),
+ };
+ // List of indices of the indicated tag groups according to the IFD_POINTER_TAGS
+ private static final int[] IFD_POINTER_TAG_HINTS = new int[] {
+ IFD_EXIF_HINT, IFD_GPS_HINT, IFD_INTEROPERABILITY_HINT
+ };
+ // Tags for indicating the thumbnail offset and length
+ private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
+ new ExifTag("JPEGInterchangeFormat", 513);
+ private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
+ new ExifTag("JPEGInterchangeFormatLength", 514);
+
+ // Mappings from tag number to tag name and each item represents one IFD tag group.
+ private static final HashMap[] sExifTagMapsForReading = new HashMap[EXIF_TAGS.length];
+ // Mapping from tag name to tag number and the corresponding tag group.
+ private static final HashMap<String, Pair<Integer, Integer>> sExifTagMapForWriting
+ = new HashMap<>();
+
+ // See JPEG File Interchange Format Version 1.02.
+ // The following values are defined for handling JPEG streams. In this implementation, we are
+ // not only getting information from EXIF but also from some JPEG special segments such as
+ // MARKER_COM for user comment and MARKER_SOFx for image width and height.
+
+ // Identifier for APP1 segment in JPEG
+ private static final byte[] IDENTIFIER_APP1 = "Exif\0\0".getBytes(Charset.forName("US-ASCII"));
+ // JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
+ // the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
+ // of frame(baseline DCT) and the image size info exists in its beginning part.
+ 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;
+ private static final byte MARKER_SOF2 = (byte) 0xc2;
+ private static final byte MARKER_SOF3 = (byte) 0xc3;
+ private static final byte MARKER_SOF5 = (byte) 0xc5;
+ private static final byte MARKER_SOF6 = (byte) 0xc6;
+ private static final byte MARKER_SOF7 = (byte) 0xc7;
+ private static final byte MARKER_SOF9 = (byte) 0xc9;
+ private static final byte MARKER_SOF10 = (byte) 0xca;
+ private static final byte MARKER_SOF11 = (byte) 0xcb;
+ private static final byte MARKER_SOF13 = (byte) 0xcd;
+ private static final byte MARKER_SOF14 = (byte) 0xce;
+ private static final byte MARKER_SOF15 = (byte) 0xcf;
+ private static final byte MARKER_SOS = (byte) 0xda;
+ private static final byte MARKER_APP1 = (byte) 0xe1;
+ private static final byte MARKER_COM = (byte) 0xfe;
+ private static final byte MARKER_EOI = (byte) 0xd9;
+
static {
- System.loadLibrary("jhead_jni");
System.loadLibrary("media_jni");
initRawNative();
-
sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ // Build up the hash tables to look up Exif tags for reading Exif tags.
+ for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+ sExifTagMapsForReading[hint] = new HashMap();
+ for (ExifTag tag : EXIF_TAGS[hint]) {
+ sExifTagMapsForReading[hint].put(tag.number, tag.name);
+ }
+ }
+
+ // Build up the hash tables to look up Exif tags for writing Exif tags.
+ // There are some tags that have the same tag name in the different group. For that tags,
+ // Primary image TIFF IFD and Exif private IFD have a higher priority to map than the other
+ // tag groups. For the same tags, it writes one tag in the only one IFD group, which has the
+ // higher priority group.
+ for (int hint = EXIF_TAGS.length - 1; hint >= 0; --hint) {
+ for (ExifTag tag : EXIF_TAGS[hint]) {
+ sExifTagMapForWriting.put(tag.name, new Pair<>(tag.number, hint));
+ }
+ }
}
private final String mFilename;
- private final HashMap<String, String> mAttributes = new HashMap<>();
+ private final FileDescriptor mFileDescriptor;
+ private final InputStream mInputStream;
private boolean mIsRaw;
+ private final HashMap<String, String> mAttributes = new HashMap<>();
private boolean mHasThumbnail;
// The following values used for indicating a thumbnail position.
private int mThumbnailOffset;
private int mThumbnailLength;
-
- // Because the underlying implementation (jhead) uses static variables,
- // there can only be one user at a time for the native functions (and
- // they cannot keep state in the native code across function calls). We
- // use sLock to serialize the accesses.
- private static final Object sLock = new Object();
+ private byte[] mThumbnailBytes;
// Pattern to check non zero timestamp
private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
@@ -155,7 +489,35 @@
throw new IllegalArgumentException("filename cannot be null");
}
mFilename = filename;
- // First test whether a given file is a one of RAW format or not.
+ mFileDescriptor = null;
+ mInputStream = new FileInputStream(filename);
+ loadAttributes();
+ }
+
+ /**
+ * Reads Exif tags from the specified image file descriptor.
+ */
+ public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
+ if (fileDescriptor == null) {
+ throw new IllegalArgumentException("parcelFileDescriptor cannot be null");
+ }
+ mFilename = null;
+ mFileDescriptor = fileDescriptor;
+ mInputStream = new FileInputStream(fileDescriptor);
+ loadAttributes();
+ }
+
+ /**
+ * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
+ * for input streams.
+ */
+ public ExifInterface(InputStream inputStream) throws IOException {
+ if (inputStream == null) {
+ throw new IllegalArgumentException("inputStream cannot be null");
+ }
+ mFilename = null;
+ mFileDescriptor = null;
+ mInputStream = inputStream;
loadAttributes();
}
@@ -188,9 +550,9 @@
}
/**
- * Returns the double value of the specified rational tag. If there is no
- * such tag in the image file or the value cannot be parsed as double, return
- * <var>defaultValue</var>.
+ * Returns the double value of the tag that is specified as rational or contains a
+ * double-formatted value. If there is no such tag in the image file or the value cannot be
+ * parsed as double, return <var>defaultValue</var>.
*
* @param tag the name of the tag.
* @param defaultValue the value to return if the tag is not available.
@@ -200,7 +562,7 @@
if (value == null) return defaultValue;
try {
int index = value.indexOf("/");
- if (index == -1) return defaultValue;
+ if (index == -1) return Double.parseDouble(value);
double denom = Double.parseDouble(value.substring(index + 1));
if (denom == 0) return defaultValue;
double num = Double.parseDouble(value.substring(0, index));
@@ -217,6 +579,10 @@
* @param value the value of the tag.
*/
public void setAttribute(String tag, String value) {
+ if (value == null) {
+ mAttributes.remove(tag);
+ return;
+ }
mAttributes.put(tag, value);
}
@@ -231,68 +597,74 @@
* file has a thumbnail inside.
*/
private void loadAttributes() throws IOException {
- HashMap map = getRawAttributesNative(mFilename);
- mIsRaw = map != null;
- if (mIsRaw) {
- for (Object o : map.entrySet()) {
- Map.Entry entry = (Map.Entry) o;
- String attrName = (String) entry.getKey();
- String attrValue = (String) entry.getValue();
+ FileInputStream in = null;
+ try {
+ if (mFilename != null) {
+ in = new FileInputStream(mFilename);
+ }
+ if (mFileDescriptor != null) {
+ in = new FileInputStream(mFileDescriptor);
+ }
+ if (in != null) {
+ // First test whether a given file is a one of RAW format or not.
+ HashMap map = getRawAttributesNative(Os.dup(in.getFD()));
+ mIsRaw = map != null;
+ if (mIsRaw) {
+ for (Object obj : map.entrySet()) {
+ Map.Entry entry = (Map.Entry) obj;
+ String attrName = (String) entry.getKey();
+ String attrValue = (String) entry.getValue();
- switch (attrName) {
- case TAG_HAS_THUMBNAIL:
- mHasThumbnail = attrValue.equalsIgnoreCase("true");
- break;
- case TAG_THUMBNAIL_OFFSET:
- mThumbnailOffset = Integer.parseInt(attrValue);
- break;
- case TAG_THUMBNAIL_LENGTH:
- mThumbnailLength = Integer.parseInt(attrValue);
- break;
- default:
- mAttributes.put(attrName, attrValue);
- break;
+ switch (attrName) {
+ case TAG_HAS_THUMBNAIL:
+ mHasThumbnail = attrValue.equalsIgnoreCase("true");
+ break;
+ case TAG_THUMBNAIL_OFFSET:
+ mThumbnailOffset = Integer.parseInt(attrValue);
+ break;
+ case TAG_THUMBNAIL_LENGTH:
+ mThumbnailLength = Integer.parseInt(attrValue);
+ break;
+ default:
+ mAttributes.put(attrName, attrValue);
+ break;
+ }
+ }
+
+ if (DEBUG) {
+ printAttributes();
+ }
+ return;
}
}
- return;
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(in);
}
- // format of string passed from native C code:
- // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
- // example:
- // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
-
- String attrStr;
- synchronized (sLock) {
- attrStr = getAttributesNative(mFilename);
- }
-
- // get count
- int ptr = attrStr.indexOf(' ');
- int count = Integer.parseInt(attrStr.substring(0, ptr));
- // skip past the space between item count and the rest of the attributes
- ++ptr;
-
- for (int i = 0; i < count; i++) {
- // extract the attribute name
- int equalPos = attrStr.indexOf('=', ptr);
- String attrName = attrStr.substring(ptr, equalPos);
- ptr = equalPos + 1; // skip past =
-
- // extract the attribute value length
- int lenPos = attrStr.indexOf(' ', ptr);
- int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
- ptr = lenPos + 1; // skip pas the space
-
- // extract the attribute value
- String attrValue = attrStr.substring(ptr, ptr + attrLen);
- ptr += attrLen;
-
- if (attrName.equals(TAG_HAS_THUMBNAIL)) {
- mHasThumbnail = attrValue.equalsIgnoreCase("true");
- } else {
- mAttributes.put(attrName, attrValue);
+ try {
+ if (mFileDescriptor != null) {
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
}
+
+ getJpegAttributes(mInputStream);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(mInputStream);
+ }
+
+ if (DEBUG) {
+ printAttributes();
+ }
+ }
+
+ // Prints out attributes for debugging.
+ private void printAttributes() {
+ Log.d(TAG, "The size of tags: " + mAttributes.size());
+ for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
+ Log.d(TAG, "tagName: " + entry.getKey() + ", tagValue: " + entry.getValue());
}
}
@@ -307,33 +679,63 @@
throw new UnsupportedOperationException(
"ExifInterface does not support saving attributes on RAW formats.");
}
+ if (mFileDescriptor == null && mFilename == null) {
+ throw new UnsupportedOperationException(
+ "ExifInterface does not support saving attributes for input streams.");
+ }
- // format of string passed to native C code:
- // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
- // example:
- // "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
- StringBuilder sb = new StringBuilder();
- int size = mAttributes.size();
- if (mAttributes.containsKey(TAG_HAS_THUMBNAIL)) {
- --size;
- }
- sb.append(size).append(" ");
- for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
- String key = entry.getKey();
- if (key.equals(TAG_HAS_THUMBNAIL)) {
- // this is a fake attribute not saved as an exif tag
- continue;
+ // Keep the thumbnail in memory
+ mThumbnailBytes = getThumbnail();
+
+ FileInputStream in = null;
+ FileOutputStream out = null;
+ File tempFile = null;
+ try {
+ // Move the original file to temporary file.
+ if (mFilename != null) {
+ tempFile = new File(mFilename + ".tmp");
+ File originalFile = new File(mFilename);
+ if (!originalFile.renameTo(tempFile)) {
+ throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath());
+ }
}
- String val = entry.getValue();
- sb.append(key).append("=");
- sb.append(val.length()).append(" ");
- sb.append(val);
+ if (mFileDescriptor != null) {
+ tempFile = File.createTempFile("temp", "jpg");
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+ in = new FileInputStream(mFileDescriptor);
+ out = new FileOutputStream(tempFile);
+ Streams.copy(in, out);
+ }
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(in);
+ IoUtils.closeQuietly(out);
}
- String s = sb.toString();
- synchronized (sLock) {
- saveAttributesNative(mFilename, s);
- commitChangesNative(mFilename);
+
+ in = null;
+ out = null;
+ try {
+ // Save the new file.
+ in = new FileInputStream(tempFile);
+ if (mFilename != null) {
+ out = new FileOutputStream(mFilename);
+ }
+ if (mFileDescriptor != null) {
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+ out = new FileOutputStream(mFileDescriptor);
+ }
+ saveJpegAttributes(in, out);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ } finally {
+ IoUtils.closeQuietly(in);
+ IoUtils.closeQuietly(out);
+ tempFile.delete();
}
+
+ // Discard the thumbnail in memory
+ mThumbnailBytes = null;
}
/**
@@ -349,27 +751,41 @@
* {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
*/
public byte[] getThumbnail() {
- if (mIsRaw) {
- if (mHasThumbnail) {
- try (RandomAccessFile file = new RandomAccessFile(mFilename, "r")) {
- if (file.length() < mThumbnailLength + mThumbnailOffset) {
- throw new IOException("Corrupted image.");
- }
- file.seek(mThumbnailOffset);
-
- byte[] buffer = new byte[mThumbnailLength];
- file.readFully(buffer);
- return buffer;
- } catch (IOException e) {
- // Couldn't get a thumbnail image.
- }
- }
+ if (!mHasThumbnail) {
return null;
}
-
- synchronized (sLock) {
- return getThumbnailNative(mFilename);
+ if (mThumbnailBytes != null) {
+ return mThumbnailBytes;
}
+
+ // Read the thumbnail.
+ FileInputStream in = null;
+ try {
+ if (mFileDescriptor != null) {
+ Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET);
+ in = new FileInputStream(mFileDescriptor);
+ }
+ if (mFilename != null) {
+ in = new FileInputStream(mFilename);
+ }
+ if (in == null) {
+ // Should not be reached this.
+ throw new FileNotFoundException();
+ }
+ if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
+ throw new IOException("Corrupted image");
+ }
+ byte[] buffer = new byte[mThumbnailLength];
+ if (in.read(buffer) != mThumbnailLength) {
+ throw new IOException("Corrupted image");
+ }
+ return buffer;
+ } catch (IOException | ErrnoException e) {
+ // Couldn't get a thumbnail image.
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ return null;
}
/**
@@ -381,16 +797,10 @@
* @hide
*/
public long[] getThumbnailRange() {
- if (mIsRaw) {
- long[] range = new long[2];
- range[0] = mThumbnailOffset;
- range[1] = mThumbnailLength;
- return range;
- }
-
- synchronized (sLock) {
- return getThumbnailRangeNative(mFilename);
- }
+ long[] range = new long[2];
+ range[0] = mThumbnailOffset;
+ range[1] = mThumbnailLength;
+ return range;
}
/**
@@ -399,10 +809,10 @@
* Exif tags are not available.
*/
public boolean getLatLong(float output[]) {
- String latValue = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE);
- String latRef = mAttributes.get(ExifInterface.TAG_GPS_LATITUDE_REF);
- String lngValue = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE);
- String lngRef = mAttributes.get(ExifInterface.TAG_GPS_LONGITUDE_REF);
+ String latValue = mAttributes.get(TAG_GPS_LATITUDE);
+ String latRef = mAttributes.get(TAG_GPS_LATITUDE_REF);
+ String lngValue = mAttributes.get(TAG_GPS_LONGITUDE);
+ String lngRef = mAttributes.get(TAG_GPS_LONGITUDE_REF);
if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
try {
@@ -428,7 +838,7 @@
int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
if (altitude >= 0 && ref >= 0) {
- return (double) (altitude * ((ref == 1) ? -1 : 1));
+ return (altitude * ((ref == 1) ? -1 : 1));
} else {
return defaultValue;
}
@@ -461,6 +871,7 @@
}
msecs += sub;
} catch (NumberFormatException e) {
+ // Ignored
}
}
return msecs;
@@ -493,8 +904,7 @@
}
}
- private static float convertRationalLatLonToFloat(
- String rationalString, String ref) {
+ private static float convertRationalLatLonToFloat(String rationalString, String ref) {
try {
String [] parts = rationalString.split(",");
@@ -522,22 +932,1062 @@
}
}
- // JNI methods for JPEG.
- private static native boolean appendThumbnailNative(String fileName,
- String thumbnailFileName);
+ // Loads EXIF attributes from a JPEG input stream.
+ private void getJpegAttributes(InputStream inputStream) throws IOException {
+ // See JPEG File Interchange Format Specification page 5.
+ if (DEBUG) {
+ Log.d(TAG, "getJpegAttributes starting with: " + inputStream);
+ }
+ DataInputStream dataInputStream = new DataInputStream(inputStream);
+ byte marker;
+ int bytesRead = 0;
+ ++bytesRead;
+ if ((marker = dataInputStream.readByte()) != MARKER) {
+ throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+ }
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER_SOI) {
+ throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
+ }
+ while (true) {
+ ++bytesRead;
+ marker = dataInputStream.readByte();
+ if (marker != MARKER) {
+ throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
+ }
+ ++bytesRead;
+ marker = dataInputStream.readByte();
+ if (DEBUG) {
+ Log.d(TAG, "Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
+ }
- private static native void saveAttributesNative(String fileName,
- String compressedAttributes);
+ // 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;
+ }
+ bytesRead += 2;
+ int length = dataInputStream.readUnsignedShort() - 2;
+ if (length < 0)
+ throw new IOException("Invalid length");
+ bytesRead += length;
+ switch (marker) {
+ case MARKER_APP1: {
+ if (DEBUG) {
+ Log.d(TAG, "MARKER_APP1");
+ }
+ bytesRead -= length;
+ if (length < 6) {
+ throw new IOException("Invalid exif");
+ }
+ byte[] identifier = new byte[6];
+ if (inputStream.read(identifier) != 6) {
+ throw new IOException("Invalid exif");
+ }
+ if (!Arrays.equals(identifier, IDENTIFIER_APP1)) {
+ throw new IOException("Invalid app1 identifier");
+ }
+ bytesRead += 6;
+ length -= 6;
+ if (length <= 0) {
+ throw new IOException("Invalid exif");
+ }
+ byte[] bytes = new byte[length];
+ if (dataInputStream.read(bytes) != length) {
+ throw new IOException("Invalid exif");
+ }
+ readExifSegment(bytes, bytesRead);
+ bytesRead += length;
+ length = 0;
+ break;
+ }
- private static native String getAttributesNative(String fileName);
+ case MARKER_COM: {
+ byte[] bytes = new byte[length];
+ if (dataInputStream.read(bytes) != length) {
+ throw new IOException("Invalid exif");
+ }
+ mAttributes.put("UserComment",
+ new String(bytes, Charset.forName("US-ASCII")));
+ break;
+ }
- private static native void commitChangesNative(String fileName);
+ 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: {
+ dataInputStream.skipBytes(1);
+ mAttributes.put("ImageLength",
+ String.valueOf(dataInputStream.readUnsignedShort()));
+ mAttributes.put("ImageWidth",
+ String.valueOf(dataInputStream.readUnsignedShort()));
+ length -= 5;
+ break;
+ }
- private static native byte[] getThumbnailNative(String fileName);
+ default: {
+ break;
+ }
+ }
+ if (length < 0) {
+ throw new IOException("Invalid length");
+ }
+ dataInputStream.skipBytes(length);
+ }
+ }
- private static native long[] getThumbnailRangeNative(String fileName);
+ // 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 page 5.
+ if (DEBUG) {
+ Log.d(TAG, "saveJpegAttributes starting with (inputStream: " + inputStream
+ + ", outputStream: " + outputStream + ")");
+ }
+ DataInputStream dataInputStream = new DataInputStream(inputStream);
+ ExifDataOutputStream dataOutputStream = new ExifDataOutputStream(outputStream);
+ int bytesRead = 0;
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER) {
+ throw new IOException("Invalid marker");
+ }
+ dataOutputStream.writeByte(MARKER);
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER_SOI) {
+ throw new IOException("Invalid marker");
+ }
+ dataOutputStream.writeByte(MARKER_SOI);
+
+ byte[] bytes = new byte[4096];
+
+ while (true) {
+ ++bytesRead;
+ if (dataInputStream.readByte() != MARKER) {
+ throw new IOException("Invalid marker");
+ }
+ dataOutputStream.writeByte(MARKER);
+ ++bytesRead;
+ byte marker = dataInputStream.readByte();
+ dataOutputStream.writeByte(marker);
+ switch (marker) {
+ case MARKER_APP1: {
+ // Rewrite EXIF segment
+ int length = dataInputStream.readUnsignedShort() - 2;
+ if (length < 0)
+ throw new IOException("Invalid length");
+ bytesRead += 2;
+ int read;
+ while ((read = dataInputStream.read(
+ bytes, 0, Math.min(length, bytes.length))) > 0) {
+ length -= read;
+ }
+ bytesRead += length;
+ writeExifSegment(dataOutputStream, bytesRead);
+ break;
+ }
+ case MARKER_EOI:
+ case MARKER_SOS: {
+ // Copy all the remaining data
+ Streams.copy(dataInputStream, dataOutputStream);
+ return;
+ }
+ default: {
+ // Copy JPEG segment
+ int length = dataInputStream.readUnsignedShort();
+ dataOutputStream.writeUnsignedShort(length);
+ if (length < 0)
+ throw new IOException("Invalid length");
+ length -= 2;
+ bytesRead += 2;
+ int read;
+ while ((read = dataInputStream.read(
+ bytes, 0, Math.min(length, bytes.length))) > 0) {
+ dataOutputStream.write(bytes, 0, read);
+ length -= read;
+ }
+ bytesRead += length;
+ break;
+ }
+ }
+ }
+ }
+
+ // Reads the given EXIF byte area and save its tag data into attributes.
+ private void readExifSegment(byte[] exifBytes, int exifOffsetFromBeginning) throws IOException {
+ // Parse TIFF Headers. See JEITA CP-3451C Table 1. page 10.
+ ByteOrderAwarenessDataInputStream dataInputStream =
+ new ByteOrderAwarenessDataInputStream(exifBytes);
+
+ // Read byte align
+ short byteOrder = dataInputStream.readShort();
+ switch (byteOrder) {
+ case BYTE_ALIGN_II:
+ if (DEBUG) {
+ Log.d(TAG, "readExifSegment: Byte Align II");
+ }
+ dataInputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ break;
+ case BYTE_ALIGN_MM:
+ if (DEBUG) {
+ Log.d(TAG, "readExifSegment: Byte Align MM");
+ }
+ dataInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ break;
+ default:
+ throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder));
+ }
+
+ int startCode = dataInputStream.readUnsignedShort();
+ if (startCode != 0x2a) {
+ throw new IOException("Invalid exif start: " + Integer.toHexString(startCode));
+ }
+
+ // Read first ifd offset
+ long firstIfdOffset = dataInputStream.readUnsignedInt();
+ if (firstIfdOffset < 8 || firstIfdOffset >= exifBytes.length) {
+ throw new IOException("Invalid first Ifd offset: " + firstIfdOffset);
+ }
+ firstIfdOffset -= 8;
+ if (firstIfdOffset > 0) {
+ if (dataInputStream.skip(firstIfdOffset) != firstIfdOffset)
+ throw new IOException("Couldn't jump to first Ifd: " + firstIfdOffset);
+ }
+
+ // Read primary image TIFF image file directory.
+ readImageFileDirectory(dataInputStream, IFD_TIFF_HINT);
+
+ // Process thumbnail.
+ try {
+ int jpegInterchangeFormat = Integer.parseInt(
+ mAttributes.get(JPEG_INTERCHANGE_FORMAT_TAG.name));
+ int jpegInterchangeFormatLength = Integer.parseInt(
+ mAttributes.get(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name));
+ // The following code limits the size of thumbnail size not to overflow EXIF data area.
+ jpegInterchangeFormatLength = Math.min(jpegInterchangeFormat
+ + jpegInterchangeFormatLength, exifOffsetFromBeginning + exifBytes.length)
+ - jpegInterchangeFormat;
+ if (jpegInterchangeFormat > 0 && jpegInterchangeFormatLength > 0) {
+ mHasThumbnail = true;
+ mThumbnailOffset = exifOffsetFromBeginning + jpegInterchangeFormat;
+ mThumbnailLength = jpegInterchangeFormatLength;
+
+ // Do not store a thumbnail in memory if the given input can be re-read.
+ if (mFileDescriptor == null && mFilename == null) {
+ byte[] thumbnailBytes = new byte[jpegInterchangeFormatLength];
+ dataInputStream.seek(jpegInterchangeFormat);
+ dataInputStream.readFully(thumbnailBytes);
+ mThumbnailBytes = thumbnailBytes;
+
+ if (DEBUG) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(
+ thumbnailBytes, 0, thumbnailBytes.length);
+ Log.d(TAG, "Thumbnail offset: " + mThumbnailOffset + ", length: "
+ + mThumbnailLength + ", width: " + bitmap.getWidth() + ", height: "
+ + bitmap.getHeight());
+ }
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Ignored the corrupted image.
+ }
+
+ // For compatibility, keep data formats as follows.
+ convertToInt(TAG_IMAGE_WIDTH);
+ convertToInt(TAG_IMAGE_LENGTH);
+ convertToInt(TAG_ORIENTATION);
+ convertToInt(TAG_FLASH);
+ convertToRational(TAG_FOCAL_LENGTH);
+ convertToDouble(TAG_DIGITAL_ZOOM_RATIO);
+ convertToDouble(TAG_EXPOSURE_TIME);
+ convertToDouble(TAG_APERTURE);
+ convertToDouble(TAG_SUBJECT_DISTANCE);
+ convertToInt(TAG_ISO);
+ convertToDouble(TAG_EXPOSURE_BIAS_VALUE);
+ convertToInt(TAG_WHITE_BALANCE);
+ convertToInt(TAG_LIGHT_SOURCE);
+ convertToInt(TAG_METERING_MODE);
+ convertToInt(TAG_EXPOSURE_PROGRAM);
+ convertToInt(TAG_EXPOSURE_MODE);
+ convertToRational(TAG_GPS_ALTITUDE);
+ convertToInt(TAG_GPS_ALTITUDE_REF);
+ convertToRational(TAG_GPS_LONGITUDE);
+ convertToRational(TAG_GPS_LATITUDE);
+ convertToTimetamp(TAG_GPS_TIMESTAMP);
+
+ // The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag.
+ String valueOfDateTimeOriginal = mAttributes.get("DateTimeOriginal");
+ if (valueOfDateTimeOriginal != null) {
+ mAttributes.put(TAG_DATETIME, valueOfDateTimeOriginal);
+ }
+
+ // Add the default value.
+ if (!mAttributes.containsKey(TAG_IMAGE_WIDTH)) {
+ mAttributes.put(TAG_IMAGE_WIDTH, "0");
+ }
+ if (!mAttributes.containsKey(TAG_IMAGE_LENGTH)) {
+ mAttributes.put(TAG_IMAGE_LENGTH, "0");
+ }
+ if (!mAttributes.containsKey(TAG_ORIENTATION)) {
+ mAttributes.put(TAG_ORIENTATION, "0");
+ }
+ if (!mAttributes.containsKey(TAG_LIGHT_SOURCE)) {
+ mAttributes.put(TAG_LIGHT_SOURCE, "0");
+ }
+ }
+
+ // Converts the tag value to timestamp; Otherwise deletes the given tag.
+ private void convertToTimetamp(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ String[] components = entryValue.split(",");
+ if (dataFormat == IFD_FORMAT_SRATIONAL && components.length == 3) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String component : components) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(":");
+ }
+ String[] rationalNumber = component.split("/");
+ int numerator = Integer.parseInt(rationalNumber[0]);
+ int denominator = Integer.parseInt(rationalNumber[1]);
+ if (denominator == 0) {
+ numerator = 0;
+ denominator = 1;
+ }
+ int value = numerator / denominator;
+ stringBuilder.append(String.format("%02d", value));
+ }
+ mAttributes.put(tagName, stringBuilder.toString());
+ } else if (dataFormat != IFD_FORMAT_STRING) {
+ mAttributes.remove(tagName);
+ }
+ }
+
+ // Checks the tag value of a given tag formatted in double type; Otherwise try to convert it to
+ // double type or delete it.
+ private void convertToDouble(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ switch (dataFormat) {
+ case IFD_FORMAT_SRATIONAL: {
+ StringBuilder stringBuilder = new StringBuilder();
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(",");
+ }
+ String[] rationalNumber = component.split("/");
+ int numerator = Integer.parseInt(rationalNumber[0]);
+ int denominator = Integer.parseInt(rationalNumber[1]);
+ if (denominator == 0) {
+ numerator = 0;
+ denominator = 1;
+ }
+ stringBuilder.append((double) numerator / denominator);
+ }
+ mAttributes.put(tagName, stringBuilder.toString());
+ break;
+ }
+ case IFD_FORMAT_DOUBLE:
+ // Keep it as is.
+ break;
+ default:
+ mAttributes.remove(tagName);
+ break;
+ }
+ }
+
+ // Checks the tag value of a given tag formatted in int type; Otherwise deletes the tag value.
+ private void convertToRational(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ switch (dataFormat) {
+ case IFD_FORMAT_SLONG:
+ case IFD_FORMAT_DOUBLE: {
+ StringBuilder stringBuilder = new StringBuilder();
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ if (stringBuilder.length() > 0) {
+ stringBuilder.append(",");
+ }
+ double doubleValue = Double.parseDouble(component);
+ stringBuilder.append((int) (doubleValue * 10000.0)).append("/").append(10000);
+ }
+ mAttributes.put(tagName, stringBuilder.toString());
+ break;
+ }
+ case IFD_FORMAT_SRATIONAL:
+ // Keep it as is.
+ break;
+ default:
+ mAttributes.remove(tagName);
+ break;
+ }
+ }
+
+ // Checks the tag value of a given tag formatted in int type; Otherwise deletes the tag value.
+ private void convertToInt(String tagName) {
+ String entryValue = mAttributes.get(tagName);
+ if (entryValue == null) return;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ if (dataFormat != IFD_FORMAT_SLONG) {
+ mAttributes.remove(tagName);
+ }
+ }
+
+ // Reads image file directory, which is a tag group in EXIF.
+ private void readImageFileDirectory(ByteOrderAwarenessDataInputStream dataInputStream, int hint)
+ throws IOException {
+ // See JEITA CP-3451 Figure 5. page 9.
+ short numberOfDirectoryEntry = dataInputStream.readShort();
+
+ if (DEBUG) {
+ Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry);
+ }
+
+ for (short i = 0; i < numberOfDirectoryEntry; ++i) {
+ int tagNumber = dataInputStream.readUnsignedShort();
+ int dataFormat = dataInputStream.readUnsignedShort();
+ int numberOfComponents = dataInputStream.readInt();
+ long nextEntryOffset = dataInputStream.peek() + 4; // next four bytes is for data
+ // offset or value.
+
+ if (DEBUG) {
+ Log.d(TAG, String.format("tagNumber: %d, dataFormat: %d, numberOfComponents: %d",
+ tagNumber, dataFormat, numberOfComponents));
+ }
+
+ // 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.
+ int byteCount = numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
+ if (byteCount > 4) {
+ long offset = dataInputStream.readUnsignedInt();
+ if (DEBUG) {
+ Log.d(TAG, "seek to data offset: " + offset);
+ }
+ dataInputStream.seek(offset);
+ }
+
+ // Look up a corresponding tag from tag number
+ String tagName = (String) sExifTagMapsForReading[hint].get(tagNumber);
+ // Skip if the parsed tag number is not defined.
+ if (tagName == null) {
+ dataInputStream.seek(nextEntryOffset);
+ continue;
+ }
+
+ // Recursively parse IFD when a IFD pointer tag appears.
+ int innerIfdHint = getIfdHintFromTagNumber(tagNumber);
+ if (innerIfdHint >= 0) {
+ 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: {
+ offset = dataInputStream.readInt();
+ break;
+ }
+ default: {
+ // Nothing to do
+ break;
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("Offset: %d, tagName: %s", offset, tagName));
+ }
+ if (offset > 0L) {
+ dataInputStream.seek(offset);
+ readImageFileDirectory(dataInputStream, innerIfdHint);
+ }
+
+ dataInputStream.seek(nextEntryOffset);
+ continue;
+ }
+
+ if (numberOfComponents == 1 || dataFormat == IFD_FORMAT_STRING
+ || dataFormat == IFD_FORMAT_UNDEFINED) {
+ String entryValue = readExifEntryValue(
+ dataInputStream, dataFormat, numberOfComponents);
+ if (entryValue != null) {
+ mAttributes.put(tagName, entryValue);
+ }
+ } else {
+ StringBuilder entryValueBuilder = new StringBuilder();
+ for (int c = 0; c < numberOfComponents; ++c) {
+ if (entryValueBuilder.length() > 0) {
+ entryValueBuilder.append(",");
+ }
+ entryValueBuilder.append(readExifEntryValue(
+ dataInputStream, dataFormat, numberOfComponents));
+ }
+ mAttributes.put(tagName, entryValueBuilder.toString());
+ }
+
+ if (dataInputStream.peek() != nextEntryOffset) {
+ dataInputStream.seek(nextEntryOffset);
+ }
+ }
+
+ long nextIfdOffset = dataInputStream.readUnsignedInt();
+ if (DEBUG) {
+ Log.d(TAG, String.format("nextIfdOffset: %d", nextIfdOffset));
+ }
+ // The next IFD offset needs to be bigger than 8 since the first IFD offset is at least 8.
+ if (nextIfdOffset > 8) {
+ dataInputStream.seek(nextIfdOffset);
+ readImageFileDirectory(dataInputStream, IFD_THUMBNAIL_HINT);
+ }
+ }
+
+ // Reads a value from where the entry value are stored.
+ private String readExifEntryValue(ByteOrderAwarenessDataInputStream dataInputStream,
+ int dataFormat, int numberOfComponents) throws IOException {
+ // See TIFF 6.0 spec Types. page 15.
+ switch (dataFormat) {
+ case IFD_FORMAT_BYTE: {
+ return String.valueOf(dataInputStream.readByte());
+ }
+ case IFD_FORMAT_SBYTE: {
+ return String.valueOf(dataInputStream.readByte() & 0xff);
+ }
+ case IFD_FORMAT_USHORT: {
+ return String.valueOf(dataInputStream.readUnsignedShort());
+ }
+ case IFD_FORMAT_SSHORT: {
+ return String.valueOf(dataInputStream.readUnsignedInt());
+ }
+ case IFD_FORMAT_ULONG: {
+ return String.valueOf(dataInputStream.readInt());
+ }
+ case IFD_FORMAT_SLONG: {
+ return String.valueOf(dataInputStream.readInt());
+ }
+ case IFD_FORMAT_URATIONAL:
+ case IFD_FORMAT_SRATIONAL: {
+ int numerator = dataInputStream.readInt();
+ int denominator = dataInputStream.readInt();
+ return numerator + "/" + denominator;
+ }
+ case IFD_FORMAT_SINGLE: {
+ return String.valueOf(dataInputStream.readFloat());
+ }
+ case IFD_FORMAT_DOUBLE: {
+ return String.valueOf(dataInputStream.readDouble());
+ }
+ case IFD_FORMAT_UNDEFINED: // Usually UNDEFINED format is ASCII.
+ case IFD_FORMAT_STRING: {
+ byte[] bytes = new byte[numberOfComponents];
+ dataInputStream.readFully(bytes);
+ 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 (true) {
+ int ch = bytes[index];
+ if (ch < 0)
+ throw new EOFException();
+ if (ch == 0)
+ break;
+ if (ch >= 32)
+ stringBuilder.append((char) ch);
+ else
+ stringBuilder.append('?');
+ ++index;
+ if (index == numberOfComponents)
+ break;
+ }
+ return stringBuilder.toString();
+ }
+ default: {
+ // Nothing to do
+ return null;
+ }
+ }
+ }
+
+ // Gets the corresponding IFD group index of the given tag number for writing Exif Tags.
+ private static int getIfdHintFromTagNumber(int tagNumber) {
+ for (int i = 0; i < IFD_POINTER_TAG_HINTS.length; ++i) {
+ if (IFD_POINTER_TAGS[i].number == tagNumber)
+ return IFD_POINTER_TAG_HINTS[i];
+ }
+ return -1;
+ }
+
+ // Writes an Exif segment into the given output stream.
+ private int writeExifSegment(ExifDataOutputStream dataOutputStream, int exifOffsetFromBeginning)
+ throws IOException {
+ // The following variables are for calculating each IFD tag group size in bytes.
+ int[] ifdOffsets = new int[EXIF_TAGS.length];
+ int[] ifdDataSizes = new int[EXIF_TAGS.length];
+
+ // Maps to store tags per IFD tag group
+ HashMap[] ifdTags = new HashMap[EXIF_TAGS.length];
+ for (int i = 0; i < EXIF_TAGS.length; ++i) {
+ ifdTags[i] = new HashMap();
+ }
+
+ // Remove IFD pointer tags (we'll re-add it later.)
+ for (ExifTag tag : IFD_POINTER_TAGS) {
+ mAttributes.remove(tag.name);
+ }
+
+ // Assign tags to the corresponding group
+ for (Map.Entry<String, String> entry : mAttributes.entrySet()) {
+ Pair<Integer, Integer> pair = sExifTagMapForWriting.get(entry.getKey());
+ if (pair != null) {
+ int tagNumber = pair.first;
+ int hint = pair.second;
+ ifdTags[hint].put(tagNumber, entry.getValue());
+ }
+ }
+
+ // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
+ // offset when there is one or more tags in the thumbnail IFD.
+ if (!ifdTags[IFD_INTEROPERABILITY_HINT].isEmpty()) {
+ ifdTags[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].number, "0");
+ }
+ if (!ifdTags[IFD_EXIF_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].number, "0");
+ }
+ if (!ifdTags[IFD_GPS_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].number, "0");
+ }
+ if (mHasThumbnail) {
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.number, "0");
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.number,
+ String.valueOf(mThumbnailLength));
+ }
+
+ // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
+ // value which has a bigger size than 4 bytes.
+ for (int i = 0; i < 5; ++i) {
+ int sum = 0;
+ for (Object entry : ifdTags[i].entrySet()) {
+ String entryValue = (String) ((Map.Entry) entry).getValue();
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ int size = getSizeOfExifEntryValue(dataFormat, entryValue);
+ if (size > 4) {
+ sum += size;
+ }
+ }
+ ifdDataSizes[i] += sum;
+ }
+
+ // Calculate IFD offsets.
+ int position = 8;
+ for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+ if (!ifdTags[hint].isEmpty()) {
+ ifdOffsets[hint] = position;
+ position += 2 + ifdTags[hint].size() * 12 + 4 + ifdDataSizes[hint];
+ }
+ }
+ if (mHasThumbnail) {
+ int thumbnailOffset = position;
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.number,
+ String.valueOf(thumbnailOffset));
+ ifdTags[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.number,
+ String.valueOf(mThumbnailLength));
+ mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset;
+ position += mThumbnailLength;
+ }
+
+ // Calculate the total size
+ int totalSize = position + 8; // eight bytes is for header part.
+ if (DEBUG) {
+ Log.d(TAG, "totalSize length: " + totalSize);
+ for (int i = 0; i < 5; ++i) {
+ Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
+ i, ifdOffsets[i], ifdTags[i].size(), ifdDataSizes[i]));
+ }
+ }
+
+ // Update IFD pointer tags with the calculated offsets.
+ if (!ifdTags[IFD_EXIF_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].number,
+ String.valueOf(ifdOffsets[IFD_EXIF_HINT]));
+ }
+ if (!ifdTags[IFD_GPS_HINT].isEmpty()) {
+ ifdTags[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].number,
+ String.valueOf(ifdOffsets[IFD_GPS_HINT]));
+ }
+ if (!ifdTags[IFD_INTEROPERABILITY_HINT].isEmpty()) {
+ ifdTags[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].number,
+ String.valueOf(ifdOffsets[IFD_INTEROPERABILITY_HINT]));
+ }
+
+ // Write TIFF Headers. See JEITA CP-3451C Table 1. page 10.
+ dataOutputStream.writeUnsignedShort(totalSize);
+ dataOutputStream.write(IDENTIFIER_APP1);
+ dataOutputStream.writeShort(BYTE_ALIGN_MM);
+ dataOutputStream.writeUnsignedShort(0x2a);
+ dataOutputStream.writeUnsignedInt(8);
+
+ // Write IFD groups. See JEITA CP-3451C Figure 7. page 12.
+ for (int hint = 0; hint < EXIF_TAGS.length; ++hint) {
+ if (!ifdTags[hint].isEmpty()) {
+ // See JEITA CP-3451C 4.6.2 IFD structure. page 13.
+ // Write entry count
+ dataOutputStream.writeUnsignedShort(ifdTags[hint].size());
+
+ // Write entry info
+ int dataOffset = ifdOffsets[hint] + 2 + ifdTags[hint].size() * 12 + 4;
+ for (Object obj : ifdTags[hint].entrySet()) {
+ Map.Entry entry = (Map.Entry) obj;
+ int tagNumber = (int) entry.getKey();
+ String entryValue = (String) entry.getValue();
+
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ int numberOfComponents = getNumberOfComponentsInExifEntryValue(dataFormat,
+ entryValue);
+ int byteCount = getSizeOfExifEntryValue(dataFormat, entryValue);
+
+ dataOutputStream.writeUnsignedShort(tagNumber);
+ dataOutputStream.writeUnsignedShort(dataFormat);
+ dataOutputStream.writeInt(numberOfComponents);
+ if (byteCount > 4) {
+ dataOutputStream.writeUnsignedInt(dataOffset);
+ dataOffset += byteCount;
+ } else {
+ int bytesWritten = writeExifEntryValue(dataOutputStream, entryValue);
+ // Fill zero up to 4 bytes
+ if (bytesWritten < 4) {
+ for (int i = bytesWritten; i < 4; ++i) {
+ dataOutputStream.write(0);
+ }
+ }
+ }
+ }
+
+ // Write the next offset. It writes the offset of thumbnail IFD if there is one or
+ // more tags in the thumbnail IFD when the current IFD is the primary image TIFF
+ // IFD; Otherwise 0.
+ if (hint == 0 && !ifdTags[IFD_THUMBNAIL_HINT].isEmpty()) {
+ dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_THUMBNAIL_HINT]);
+ } else {
+ dataOutputStream.writeUnsignedInt(0);
+ }
+
+ // Write values of data field exceeding 4 bytes after the next offset.
+ for (Object obj : ifdTags[hint].entrySet()) {
+ Map.Entry entry = (Map.Entry) obj;
+ String entryValue = (String) entry.getValue();
+
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+ int byteCount = getSizeOfExifEntryValue(dataFormat, entryValue);
+ if (byteCount > 4) {
+ writeExifEntryValue(dataOutputStream, entryValue);
+ }
+ }
+ }
+ }
+
+ // Write thumbnail
+ if (mHasThumbnail) {
+ dataOutputStream.write(getThumbnail());
+ }
+
+ return totalSize;
+ }
+
+ // Writes EXIF entry value and its entry value type will be automatically determined.
+ private static int writeExifEntryValue(ExifDataOutputStream dataOutputStream, String entryValue)
+ throws IOException {
+ int bytesWritten = 0;
+ int dataFormat = getDataFormatOfExifEntryValue(entryValue);
+
+ // Values can be composed of several components. Each component is separated by char ','.
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ switch (dataFormat) {
+ case IFD_FORMAT_SLONG:
+ dataOutputStream.writeInt(Integer.parseInt(component));
+ bytesWritten += 4;
+ break;
+ case IFD_FORMAT_DOUBLE:
+ dataOutputStream.writeDouble(Double.parseDouble(component));
+ bytesWritten += 8;
+ break;
+ case IFD_FORMAT_STRING:
+ byte[] asciiArray = (component + '\0').getBytes(Charset.forName("US-ASCII"));
+ dataOutputStream.write(asciiArray);
+ bytesWritten += asciiArray.length;
+ break;
+ case IFD_FORMAT_SRATIONAL:
+ String[] rationalNumber = component.split("/");
+ dataOutputStream.writeInt(Integer.parseInt(rationalNumber[0]));
+ dataOutputStream.writeInt(Integer.parseInt(rationalNumber[1]));
+ bytesWritten += 8;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+ return bytesWritten;
+ }
+
+ // Determines the data format of EXIF entry value.
+ private static int getDataFormatOfExifEntryValue(String entryValue) {
+ // See TIFF 6.0 spec Types. page 15.
+ // Take the first component if there are more than one component.
+ if (entryValue.contains(",")) {
+ entryValue = entryValue.split(",")[0];
+ }
+
+ if (entryValue.contains("/")) {
+ return IFD_FORMAT_SRATIONAL;
+ }
+ try {
+ Integer.parseInt(entryValue);
+ return IFD_FORMAT_SLONG;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ try {
+ Double.parseDouble(entryValue);
+ return IFD_FORMAT_DOUBLE;
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ return IFD_FORMAT_STRING;
+ }
+
+ // Determines the size of EXIF entry value.
+ private static int getSizeOfExifEntryValue(int dataFormat, String entryValue) {
+ // See TIFF 6.0 spec Types page 15.
+ int bytesEstimated = 0;
+ String[] components = entryValue.split(",");
+ for (String component : components) {
+ switch (dataFormat) {
+ case IFD_FORMAT_SLONG:
+ bytesEstimated += 4;
+ break;
+ case IFD_FORMAT_DOUBLE:
+ bytesEstimated += 8;
+ break;
+ case IFD_FORMAT_STRING:
+ bytesEstimated
+ += (component + '\0').getBytes(Charset.forName("US-ASCII")).length;
+ break;
+ case IFD_FORMAT_SRATIONAL:
+ bytesEstimated += 8;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+ return bytesEstimated;
+ }
+
+ // Determines the number of components of EXIF entry value.
+ private static int getNumberOfComponentsInExifEntryValue(int dataFormat, String entryValue) {
+ if (dataFormat == IFD_FORMAT_STRING) {
+ return (entryValue + '\0').getBytes(Charset.forName("US-ASCII")).length;
+ }
+ int count = 1;
+ for (int i = 0; i < entryValue.length(); ++i) {
+ if (entryValue.charAt(i) == ',') {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ // An input stream to parse EXIF data area, which can be written in either little or big endian
+ // order.
+ private static class ByteOrderAwarenessDataInputStream extends ByteArrayInputStream {
+ private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
+ private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
+
+ private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+ private final long mLength;
+ private long mPosition;
+
+ public ByteOrderAwarenessDataInputStream(byte[] bytes) {
+ super(bytes);
+ mLength = bytes.length;
+ mPosition = 0L;
+ }
+
+ public void setByteOrder(ByteOrder byteOrder) {
+ mByteOrder = byteOrder;
+ }
+
+ public void seek(long byteCount) throws IOException {
+ mPosition = 0L;
+ reset();
+ if (skip(byteCount) != byteCount)
+ throw new IOException("Couldn't seek up to the byteCount");
+ }
+
+ public long peek() {
+ return mPosition;
+ }
+
+ public void readFully(byte[] buffer) throws IOException {
+ mPosition += buffer.length;
+ if (mPosition > mLength)
+ throw new EOFException();
+ if (super.read(buffer, 0, buffer.length) != buffer.length) {
+ throw new IOException("Couldn't read up to the length of buffer");
+ }
+ }
+
+ public byte readByte() throws IOException {
+ ++mPosition;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch = super.read();
+ if (ch < 0)
+ throw new EOFException();
+ return (byte) ch;
+ }
+
+ public short readShort() throws IOException {
+ mPosition += 2;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return (short) ((ch2 << 8) + (ch1));
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return (short) ((ch1 << 8) + (ch2));
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public int readInt() throws IOException {
+ mPosition += 4;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ int ch3 = super.read();
+ int ch4 = super.read();
+ if ((ch1 | ch2 | ch3 | ch4) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ @Override
+ public long skip(long byteCount) {
+ long skipped = super.skip(Math.min(byteCount, mLength - mPosition));
+ mPosition += skipped;
+ return skipped;
+ }
+
+ public int readUnsignedShort() throws IOException {
+ mPosition += 2;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ if ((ch1 | ch2) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return ((ch2 << 8) + (ch1));
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return ((ch1 << 8) + (ch2));
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ public long readLong() throws IOException {
+ mPosition += 8;
+ if (mPosition > mLength)
+ throw new EOFException();
+ int ch1 = super.read();
+ int ch2 = super.read();
+ int ch3 = super.read();
+ int ch4 = super.read();
+ int ch5 = super.read();
+ int ch6 = super.read();
+ int ch7 = super.read();
+ int ch8 = super.read();
+ if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0)
+ throw new EOFException();
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
+ + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
+ + ((long) ch2 << 8) + (long) ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
+ + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
+ + ((long) ch7 << 8) + (long) ch8);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public float readFloat() throws IOException {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ public double readDouble() throws IOException {
+ return Double.longBitsToDouble(readLong());
+ }
+ }
+
+ // An output stream to write EXIF data area, that will be written in big endian byte order.
+ private static class ExifDataOutputStream extends DataOutputStream {
+ public ExifDataOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public void writeUnsignedShort(int val) throws IOException {
+ writeShort((short) val);
+ }
+
+ public void writeUnsignedInt(long val) throws IOException {
+ writeInt((int) val);
+ }
+ }
// JNI methods for RAW formats.
private static native void initRawNative();
- private static native HashMap getRawAttributesNative(String filename);
+ private static native HashMap getRawAttributesNative(FileDescriptor fileDescriptor);
}
diff --git a/media/jni/Android.mk b/media/jni/Android.mk
index a326f6f..fa9c48c 100644
--- a/media/jni/Android.mk
+++ b/media/jni/Android.mk
@@ -43,14 +43,10 @@
libcamera_client \
libmtp \
libusbhost \
- libjhead \
libexif \
libpiex \
libstagefright_amrnb_common
-LOCAL_REQUIRED_MODULES := \
- libjhead_jni
-
LOCAL_STATIC_LIBRARIES := \
libstagefright_amrnbenc
diff --git a/media/jni/android_media_ExifInterface.cpp b/media/jni/android_media_ExifInterface.cpp
index ba38569..a1fcb07 100644
--- a/media/jni/android_media_ExifInterface.cpp
+++ b/media/jni/android_media_ExifInterface.cpp
@@ -83,19 +83,18 @@
}
static jobject ExifInterface_getRawMetadata(
- JNIEnv* env, jclass /* clazz */, jstring jfilename) {
- const char* filenameChars = env->GetStringUTFChars(jfilename, NULL);
- if (filenameChars == NULL) {
+ JNIEnv* env, jclass /* clazz */, jobject jfileDescriptor) {
+ int fd = jniGetFDFromFileDescriptor(env, jfileDescriptor);
+ if (fd < 0) {
+ ALOGI("Invalid file descriptor");
return NULL;
}
- String8 filename(filenameChars);
- env->ReleaseStringUTFChars(jfilename, filenameChars);
piex::PreviewImageData image_data;
- std::unique_ptr<FileStream> stream(new FileStream(filename));
+ std::unique_ptr<FileStream> stream(new FileStream(fd));
- if (!GetExifFromRawImage(stream.get(), filename, image_data)) {
- ALOGI("Raw image not detected: %s", filename.string());
+ if (!GetExifFromRawImage(stream.get(), String8("[file descriptor]"), image_data)) {
+ ALOGI("Raw image not detected");
return NULL;
}
@@ -263,7 +262,7 @@
static JNINativeMethod gMethods[] = {
{ "initRawNative", "()V", (void *)ExifInterface_initRaw },
- { "getRawAttributesNative", "(Ljava/lang/String;)Ljava/util/HashMap;",
+ { "getRawAttributesNative", "(Ljava/io/FileDescriptor;)Ljava/util/HashMap;",
(void*)ExifInterface_getRawMetadata },
};
diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp
index c08a5e3..9c4f7c4 100644
--- a/media/jni/android_media_Utils.cpp
+++ b/media/jni/android_media_Utils.cpp
@@ -28,6 +28,19 @@
namespace android {
+FileStream::FileStream(const int fd)
+ : mPosition(0),
+ mSize(0) {
+ mFile = fdopen(fd, "r");
+ if (mFile == NULL) {
+ return;
+ }
+ // Get the size.
+ fseek(mFile, 0l, SEEK_END);
+ mSize = ftell(mFile);
+ fseek(mFile, 0l, SEEK_SET);
+}
+
FileStream::FileStream(const String8 filename)
: mPosition(0),
mSize(0) {
diff --git a/media/jni/android_media_Utils.h b/media/jni/android_media_Utils.h
index 762c904..a30e1be 100644
--- a/media/jni/android_media_Utils.h
+++ b/media/jni/android_media_Utils.h
@@ -35,6 +35,7 @@
size_t mSize;
public:
+ FileStream(const int fd);
FileStream(const String8 filename);
~FileStream();
diff --git a/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg
new file mode 100644
index 0000000..477cd3a
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg
new file mode 100644
index 0000000..78ac703
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng
new file mode 100644
index 0000000..5fcc720
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/res/values/exifinterface.xml b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml
new file mode 100644
index 0000000..8fc6adc
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <array name="exifbyteorderii_jpg">
+ <item>true</item>
+ <item>512</item>
+ <item>288</item>
+ <item>false</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>SAMSUNG</item>
+ <item>SM-N900S</item>
+ <item>2.200</item>
+ <item>2016:01:29 18:32:27</item>
+ <item>0.033</item>
+ <item>0</item>
+ <item>413/100</item>
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item />
+ <item>480</item>
+ <item>640</item>
+ <item>50</item>
+ <item>6</item>
+ <item>0</item>
+ </array>
+ <array name="exifbyteordermm_jpg">
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>true</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>LGE</item>
+ <item>Nexus 5</item>
+ <item>2.400</item>
+ <item>2016:01:29 15:44:58</item>
+ <item>0.017</item>
+ <item>0</item>
+ <item>3970/1000</item>
+ <item>0/1000</item>
+ <item>0</item>
+ <item>1970:01:01</item>
+ <item>0/1,0/1,0/10000</item>
+ <item>N</item>
+ <item>0/1,0/1,0/10000</item>
+ <item>E</item>
+ <item>GPS</item>
+ <item>00:00:00</item>
+ <item>176</item>
+ <item>144</item>
+ <item>146</item>
+ <item>0</item>
+ <item>0</item>
+ </array>
+ <array name="lg_g4_iso_800_dng">
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>true</item>
+ <item>53.834507</item>
+ <item>10.69585</item>
+ <item>0.0</item>
+ <item>LGE</item>
+ <item>LG-H815</item>
+ <item>1.800</item>
+ <item>2015:11:12 16:46:18</item>
+ <item>0.0040</item>
+ <item>0.0</item>
+ <item>442/100</item>
+ <item>0/1</item>
+ <item>0</item>
+ <item>1970:01:17</item>
+ <item>53/1,50/1,423/100</item>
+ <item>N</item>
+ <item>10/1,41/1,4506/100</item>
+ <item>E</item>
+ <item />
+ <item>18:08:10</item>
+ <item>337</item>
+ <item>600</item>
+ <item>800</item>
+ <item>1</item>
+ <item />
+ </array>
+</resources>
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
index 11d9070..61dede3 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java
@@ -50,6 +50,7 @@
addMediaScannerUnitTests(suite);
addCameraUnitTests(suite);
addImageReaderTests(suite);
+ addExifInterfaceTests(suite);
return suite;
}
@@ -109,4 +110,8 @@
private void addMediaScannerUnitTests(TestSuite suite) {
suite.addTestSuite(MediaInserterTest.class);
}
+
+ private void addExifInterfaceTests(TestSuite suite) {
+ suite.addTestSuite(ExifInterfaceTest.class);
+ }
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
new file mode 100644
index 0000000..1c80746
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mediaframeworktest.unit;
+
+import com.android.mediaframeworktest.R;
+
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ExifInterface;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+public class ExifInterfaceTest extends AndroidTestCase {
+ private static final String TAG = ExifInterface.class.getSimpleName();
+ private static final boolean VERBOSE = false; // lots of logging
+
+ private static final double DIFFERENCE_TOLERANCE = .005;
+ private static final int BUFFER_SIZE = 32768;
+
+ // List of files.
+ private static final String EXIF_BYTE_ORDER_II_JPEG = "ExifByteOrderII.jpg";
+ private static final String EXIF_BYTE_ORDER_MM_JPEG = "ExifByteOrderMM.jpg";
+ private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng";
+ private static final int[] IMAGE_RESOURCES = new int[] {
+ R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800 };
+ private static final String[] IMAGE_FILENAMES = new String[] {
+ EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG };
+
+ private static final String[] EXIF_TAGS = {
+ ExifInterface.TAG_MAKE,
+ ExifInterface.TAG_MODEL,
+ ExifInterface.TAG_APERTURE,
+ ExifInterface.TAG_DATETIME,
+ ExifInterface.TAG_EXPOSURE_TIME,
+ ExifInterface.TAG_FLASH,
+ ExifInterface.TAG_FOCAL_LENGTH,
+ ExifInterface.TAG_GPS_ALTITUDE,
+ ExifInterface.TAG_GPS_ALTITUDE_REF,
+ ExifInterface.TAG_GPS_DATESTAMP,
+ ExifInterface.TAG_GPS_LATITUDE,
+ ExifInterface.TAG_GPS_LATITUDE_REF,
+ ExifInterface.TAG_GPS_LONGITUDE,
+ ExifInterface.TAG_GPS_LONGITUDE_REF,
+ ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ ExifInterface.TAG_GPS_TIMESTAMP,
+ ExifInterface.TAG_IMAGE_LENGTH,
+ ExifInterface.TAG_IMAGE_WIDTH,
+ ExifInterface.TAG_ISO,
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.TAG_WHITE_BALANCE
+ };
+
+ private static class ExpectedValue {
+ // Thumbnail information.
+ public final boolean hasThumbnail;
+ public final int thumbnailWidth;
+ public final int thumbnailHeight;
+
+ // GPS information.
+ public final boolean hasLatLong;
+ public final float latitude;
+ public final float longitude;
+ public final float altitude;
+
+ // Values.
+ public final String make;
+ public final String model;
+ public final float aperture;
+ public final String datetime;
+ public final float exposureTime;
+ public final float flash;
+ public final String focalLength;
+ public final String gpsAltitude;
+ public final String gpsAltitudeRef;
+ public final String gpsDatestamp;
+ public final String gpsLatitude;
+ public final String gpsLatitudeRef;
+ public final String gpsLongitude;
+ public final String gpsLongitudeRef;
+ public final String gpsProcessingMethod;
+ public final String gpsTimestamp;
+ public final String imageLength;
+ public final String imageWidth;
+ public final String iso;
+ public final String whiteBalance;
+ public final String orientation;
+
+ private static String getString(TypedArray typedArray, int index) {
+ String stringValue = typedArray.getString(index);
+ if (stringValue == null || stringValue.equals("")) {
+ return null;
+ }
+ return stringValue.trim();
+ }
+
+ public ExpectedValue(TypedArray typedArray) {
+ // Reads thumbnail information.
+ hasThumbnail = typedArray.getBoolean(0, false);
+ thumbnailWidth = typedArray.getInt(1, 0);
+ thumbnailHeight = typedArray.getInt(2, 0);
+
+ // Reads GPS information.
+ hasLatLong = typedArray.getBoolean(3, false);
+ latitude = typedArray.getFloat(4, 0f);
+ longitude = typedArray.getFloat(5, 0f);
+ altitude = typedArray.getFloat(6, 0f);
+
+ // Read values.
+ make = getString(typedArray, 7);
+ model = getString(typedArray, 8);
+ aperture = typedArray.getFloat(9, 0f);
+ datetime = getString(typedArray, 10);
+ exposureTime = typedArray.getFloat(11, 0f);
+ flash = typedArray.getFloat(12, 0f);
+ focalLength = getString(typedArray, 13);
+ gpsAltitude = getString(typedArray, 14);
+ gpsAltitudeRef = getString(typedArray, 15);
+ gpsDatestamp = getString(typedArray, 16);
+ gpsLatitude = getString(typedArray, 17);
+ gpsLatitudeRef = getString(typedArray, 18);
+ gpsLongitude = getString(typedArray, 19);
+ gpsLongitudeRef = getString(typedArray, 20);
+ gpsProcessingMethod = getString(typedArray, 21);
+ gpsTimestamp = getString(typedArray, 22);
+ imageLength = getString(typedArray, 23);
+ imageWidth = getString(typedArray, 24);
+ iso = getString(typedArray, 25);
+ orientation = getString(typedArray, 26);
+ whiteBalance = getString(typedArray, 27);
+
+ typedArray.recycle();
+ }
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String outputPath = new File(Environment.getExternalStorageDirectory(),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ try (InputStream inputStream = getContext().getResources().openRawResource(
+ IMAGE_RESOURCES[i])) {
+ try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
+ Streams.copy(inputStream, outputStream);
+ }
+ }
+ }
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String imageFilePath = new File(Environment.getExternalStorageDirectory(),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ File imageFile = new File(imageFilePath);
+ if (imageFile.exists()) {
+ imageFile.delete();
+ }
+ }
+
+ super.tearDown();
+ }
+
+ private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
+ // Prints thumbnail information.
+ if (exifInterface.hasThumbnail()) {
+ byte[] thumbnailBytes = exifInterface.getThumbnail();
+ if (thumbnailBytes != null) {
+ Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(
+ thumbnailBytes, 0, thumbnailBytes.length);
+ if (bitmap == null) {
+ Log.e(TAG, fileName + " Corrupted thumbnail!");
+ } else {
+ Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", "
+ + bitmap.getHeight());
+ }
+ } else {
+ Log.e(TAG, fileName + " Corrupted image (no thumbnail)");
+ }
+ } else {
+ if (exifInterface.getThumbnail() != null) {
+ Log.e(TAG, fileName + " Corrupted image (a thumbnail exists)");
+ } else {
+ Log.v(TAG, fileName + " No thumbnail");
+ }
+ }
+
+ // Prints GPS information.
+ Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
+
+ float[] latLong = new float[2];
+ if (exifInterface.getLatLong(latLong)) {
+ Log.v(TAG, fileName + " Latitude = " + latLong[0]);
+ Log.v(TAG, fileName + " Longitude = " + latLong[1]);
+ } else {
+ Log.v(TAG, fileName + "No latlong data");
+ }
+
+ // Prints values.
+ for (String tagKey : EXIF_TAGS) {
+ String tagValue = exifInterface.getAttribute(tagKey);
+ Log.v(TAG, fileName + "Key{" + tagKey + "} = '" + tagValue + "'");
+ }
+ }
+
+ private void compareFloatTag(ExifInterface exifInterface, String tag, float expectedValue) {
+ String stringValue = exifInterface.getAttribute(tag);
+ float floatValue = 0f;
+
+ if (stringValue != null) {
+ floatValue = Float.parseFloat(stringValue);
+ }
+
+ assertEquals(expectedValue, floatValue, DIFFERENCE_TOLERANCE);
+ }
+
+ private void compareStringTag(ExifInterface exifInterface, String tag, String expectedValue) {
+ String stringValue = exifInterface.getAttribute(tag);
+ if (stringValue != null) {
+ stringValue = stringValue.trim();
+ }
+
+ assertEquals(expectedValue, stringValue);
+ }
+
+ private void compareWithExpectedValue(ExifInterface exifInterface,
+ ExpectedValue expectedValue) {
+ // Checks a thumbnail image.
+ assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail());
+ if (expectedValue.hasThumbnail) {
+ byte[] thumbnailBytes = exifInterface.getThumbnail();
+ assertNotNull(thumbnailBytes);
+ Bitmap thumbnailBitmap =
+ BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.length);
+ assertNotNull(thumbnailBitmap);
+ assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
+ assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
+ } else {
+ assertNull(exifInterface.getThumbnail());
+ }
+
+ // Checks GPS information.
+ float[] latLong = new float[2];
+ assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
+ if (expectedValue.hasLatLong) {
+ assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
+ assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
+ }
+ assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE);
+
+ // Checks values.
+ compareStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make);
+ compareStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model);
+ compareFloatTag(exifInterface, ExifInterface.TAG_APERTURE, expectedValue.aperture);
+ compareStringTag(exifInterface, ExifInterface.TAG_DATETIME, expectedValue.datetime);
+ compareFloatTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime);
+ compareFloatTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash);
+ compareStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF,
+ expectedValue.gpsAltitudeRef);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP,
+ expectedValue.gpsDatestamp);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF,
+ expectedValue.gpsLatitudeRef);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE,
+ expectedValue.gpsLongitude);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF,
+ expectedValue.gpsLongitudeRef);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ expectedValue.gpsProcessingMethod);
+ compareStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP,
+ expectedValue.gpsTimestamp);
+ compareStringTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength);
+ compareStringTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth);
+ compareStringTag(exifInterface, ExifInterface.TAG_ISO, expectedValue.iso);
+ compareStringTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation);
+ compareStringTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE,
+ expectedValue.whiteBalance);
+ }
+
+ private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
+ throws IOException {
+ ExpectedValue expectedValue = new ExpectedValue(
+ getContext().getResources().obtainTypedArray(typedArrayResourceId));
+ File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+
+ // Created via path.
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+
+ // Created via InputStream.
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(imageFile.getAbsolutePath());
+ exifInterface = new ExifInterface(in);
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+
+ // Created via FileDescriptor.
+ try {
+ FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDONLY, 0600);
+ exifInterface = new ExifInterface(fd);
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+
+ // Test for saving attributes.
+ try {
+ FileDescriptor fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
+ exifInterface = new ExifInterface(fd);
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(fd);
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+
+ // Test for modifying one attribute.
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
+ }
+
+ private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId)
+ throws IOException {
+ ExpectedValue expectedValue = new ExpectedValue(
+ getContext().getResources().obtainTypedArray(typedArrayResourceId));
+ File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+
+ // Created via path.
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+
+ // Created via FileDescriptor.
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(imageFile);
+ exifInterface = new ExifInterface(in.getFD());
+ if (VERBOSE) {
+ printExifTagsAndValues(fileName, exifInterface);
+ }
+ compareWithExpectedValue(exifInterface, expectedValue);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+
+ public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
+ testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
+ }
+
+ public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
+ testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
+ }
+
+ public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
+ testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
+ }
+}