diff options
| -rw-r--r-- | api/current.txt | 9 | ||||
| -rw-r--r-- | api/system-current.txt | 9 | ||||
| -rw-r--r-- | api/test-current.txt | 9 | ||||
| -rw-r--r-- | media/java/android/media/ExifInterface.java | 1724 | ||||
| -rw-r--r-- | media/jni/Android.mk | 4 | ||||
| -rw-r--r-- | media/jni/android_media_ExifInterface.cpp | 17 | ||||
| -rw-r--r-- | media/jni/android_media_Utils.cpp | 13 | ||||
| -rw-r--r-- | media/jni/android_media_Utils.h | 1 | ||||
| -rw-r--r-- | media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg | bin | 0 -> 105271 bytes | |||
| -rw-r--r-- | media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg | bin | 0 -> 8610 bytes | |||
| -rw-r--r-- | media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng | bin | 0 -> 129312 bytes | |||
| -rw-r--r-- | media/tests/MediaFrameworkTest/res/values/exifinterface.xml | 108 | ||||
| -rw-r--r-- | media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java | 5 | ||||
| -rw-r--r-- | media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ExifInterfaceTest.java | 418 |
14 files changed, 2167 insertions, 150 deletions
diff --git a/api/current.txt b/api/current.txt index 8edab677ba4b..d6a1660604c6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20188,6 +20188,8 @@ package android.media { 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 @@ package android.media { 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 @@ package android.media { 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 54ddf49d879b..4b4bee81ce71 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -21687,6 +21687,8 @@ package android.media { 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 @@ package android.media { 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 @@ package android.media { 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 662650150ef9..baadc152dfcb 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -20197,6 +20197,8 @@ package android.media { 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 @@ package android.media { 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 @@ package android.media { 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 7fb67ee5581d..1a387be21183 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 class ExifInterface { 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 @@ public class ExifInterface { // 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 @@ public class ExifInterface { 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 @@ public class ExifInterface { } /** - * 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 @@ public class ExifInterface { 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 @@ public class ExifInterface { * @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 @@ public class ExifInterface { * 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(); - - 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; - } + FileInputStream in = null; + try { + if (mFilename != null) { + in = new FileInputStream(mFilename); } - return; - } + 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(); - // 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" + 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; + } + } - String attrStr; - synchronized (sLock) { - attrStr = getAttributesNative(mFilename); + if (DEBUG) { + printAttributes(); + } + return; + } + } + } catch (ErrnoException e) { + e.rethrowAsIOException(); + } finally { + IoUtils.closeQuietly(in); } - // 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 = + try { + if (mFileDescriptor != null) { + Os.lseek(mFileDescriptor, 0, OsConstants.SEEK_SET); + } - // 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 + getJpegAttributes(mInputStream); + } catch (ErrnoException e) { + e.rethrowAsIOException(); + } finally { + IoUtils.closeQuietly(mInputStream); + } - // extract the attribute value - String attrValue = attrStr.substring(ptr, ptr + attrLen); - ptr += attrLen; + if (DEBUG) { + printAttributes(); + } + } - if (attrName.equals(TAG_HAS_THUMBNAIL)) { - mHasThumbnail = attrValue.equalsIgnoreCase("true"); - } else { - mAttributes.put(attrName, attrValue); - } + // 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 @@ public class ExifInterface { throw new UnsupportedOperationException( "ExifInterface does not support saving attributes on RAW formats."); } - - // 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; + if (mFileDescriptor == null && mFilename == null) { + throw new UnsupportedOperationException( + "ExifInterface does not support saving attributes for input streams."); } - 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()); + } + } + 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); } - String val = entry.getValue(); - sb.append(key).append("="); - sb.append(val.length()).append(" "); - sb.append(val); + } 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 @@ public class ExifInterface { * {@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; } + if (mThumbnailBytes != null) { + return mThumbnailBytes; + } - synchronized (sLock) { - return getThumbnailNative(mFilename); + // 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 @@ public class ExifInterface { * @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 @@ public class ExifInterface { * 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 @@ public class ExifInterface { 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 @@ public class ExifInterface { } msecs += sub; } catch (NumberFormatException e) { + // Ignored } } return msecs; @@ -493,8 +904,7 @@ public class ExifInterface { } } - 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 @@ public class ExifInterface { } } - // 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)); + } + + // 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; + } + + 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; + } + + 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; + } + + default: { + break; + } + } + if (length < 0) { + throw new IOException("Invalid length"); + } + dataInputStream.skipBytes(length); + } + } + + // 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); + } + } - private static native void saveAttributesNative(String fileName, - String compressedAttributes); + 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(); + } - private static native String getAttributesNative(String fileName); + // Remove IFD pointer tags (we'll re-add it later.) + for (ExifTag tag : IFD_POINTER_TAGS) { + mAttributes.remove(tag.name); + } - private static native void commitChangesNative(String fileName); + // 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()); + } + } - private static native byte[] getThumbnailNative(String fileName); + // 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; + } - private static native long[] getThumbnailRangeNative(String fileName); + // 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 a326f6feba45..fa9c48c59af0 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -43,14 +43,10 @@ LOCAL_SHARED_LIBRARIES := \ 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 ba38569af8e8..a1fcb07ca3fa 100644 --- a/media/jni/android_media_ExifInterface.cpp +++ b/media/jni/android_media_ExifInterface.cpp @@ -83,19 +83,18 @@ static void ExifInterface_initRaw(JNIEnv *env) { } 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 jobject ExifInterface_getRawMetadata( 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 c08a5e3daa5a..9c4f7c4b2aff 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 762c90458d01..a30e1be586f9 100644 --- a/media/jni/android_media_Utils.h +++ b/media/jni/android_media_Utils.h @@ -35,6 +35,7 @@ private: 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 Binary files differnew file mode 100644 index 000000000000..477cd3a574c3 --- /dev/null +++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_ii.jpg 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 Binary files differnew file mode 100644 index 000000000000..78ac703850a1 --- /dev/null +++ b/media/tests/MediaFrameworkTest/res/raw/image_exif_byte_order_mm.jpg diff --git a/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng Binary files differnew file mode 100644 index 000000000000..5fcc720a5f8f --- /dev/null +++ b/media/tests/MediaFrameworkTest/res/raw/lg_g4_iso_800.dng diff --git a/media/tests/MediaFrameworkTest/res/values/exifinterface.xml b/media/tests/MediaFrameworkTest/res/values/exifinterface.xml new file mode 100644 index 000000000000..8fc6adcb0dd9 --- /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 11d90700bffd..61dede34bb5c 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 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner { addMediaScannerUnitTests(suite); addCameraUnitTests(suite); addImageReaderTests(suite); + addExifInterfaceTests(suite); return suite; } @@ -109,4 +110,8 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner { 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 000000000000..1c80746eb445 --- /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); + } +} |