From e739d9ca5469ed30129d0fa228e3d0f2878671ac Mon Sep 17 00:00:00 2001 From: Sungsoo Lim Date: Wed, 13 Jul 2016 09:31:16 +0900 Subject: DO NOT MERGE: Remove the use of JHEAD in ExifInterface Bug: 29270469 Change-Id: I6a6c8aeab2a842ff1646316363d614851625e78f --- media/java/android/media/ExifInterface.java | 2451 ++++++++++++++++++++++++--- media/jni/Android.mk | 4 - 2 files changed, 2258 insertions(+), 197 deletions(-) (limited to 'media') diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 6bf5721..c5e978f 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -16,85 +16,307 @@ package android.media; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import android.util.Pair; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; import java.text.ParsePosition; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * This is a class for reading and writing Exif tags in a JPEG file. */ 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"; + /** Type is String. @hide */ + public static final String TAG_ARTIST = "Artist"; + /** Type is int. @hide */ + public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample"; + /** Type is int. @hide */ + public static final String TAG_COMPRESSION = "Compression"; + /** Type is String. @hide */ + public static final String TAG_COPYRIGHT = "Copyright"; /** Type is String. */ public static final String TAG_DATETIME = "DateTime"; - /** Type is String. */ - public static final String TAG_MAKE = "Make"; - /** Type is String. */ - public static final String TAG_MODEL = "Model"; + /** Type is String. @hide */ + public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription"; /** Type is int. */ - public static final String TAG_FLASH = "Flash"; + public static final String TAG_IMAGE_LENGTH = "ImageLength"; /** Type is int. */ public static final String TAG_IMAGE_WIDTH = "ImageWidth"; - /** Type is int. */ - public static final String TAG_IMAGE_LENGTH = "ImageLength"; - /** String. Format is "num1/denom1,num2/denom2,num3/denom3". */ - public static final String TAG_GPS_LATITUDE = "GPSLatitude"; - /** String. Format is "num1/denom1,num2/denom2,num3/denom3". */ - public static final String TAG_GPS_LONGITUDE = "GPSLongitude"; + /** Type is int. @hide */ + public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat"; + /** Type is int. @hide */ + public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength"; /** Type is String. */ - public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"; + public static final String TAG_MAKE = "Make"; /** Type is String. */ - public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"; + public static final String TAG_MODEL = "Model"; + /** Type is int. */ + public static final String TAG_ORIENTATION = "Orientation"; + /** Type is int. @hide */ + public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"; + /** Type is int. @hide */ + public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration"; + /** Type is rational. @hide */ + public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities"; + /** Type is rational. @hide */ + public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite"; + /** Type is int. @hide */ + public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit"; + /** Type is int. @hide */ + public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip"; + /** Type is int. @hide */ + public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel"; + /** Type is String. @hide */ + public static final String TAG_SOFTWARE = "Software"; + /** Type is int. @hide */ + public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts"; + /** Type is int. @hide */ + public static final String TAG_STRIP_OFFSETS = "StripOffsets"; + /** Type is int. @hide */ + public static final String TAG_TRANSFER_FUNCTION = "TransferFunction"; + /** Type is rational. @hide */ + public static final String TAG_WHITE_POINT = "WhitePoint"; + /** Type is rational. @hide */ + public static final String TAG_X_RESOLUTION = "XResolution"; + /** Type is rational. @hide */ + public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients"; + /** Type is int. @hide */ + public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning"; + /** Type is int. @hide */ + public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling"; + /** Type is rational. @hide */ + public static final String TAG_Y_RESOLUTION = "YResolution"; + /** Type is rational. @hide */ + public static final String TAG_APERTURE_VALUE = "ApertureValue"; + /** Type is rational. @hide */ + public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue"; + /** Type is String. @hide */ + public static final String TAG_CFA_PATTERN = "CFAPattern"; + /** Type is int. @hide */ + public static final String TAG_COLOR_SPACE = "ColorSpace"; + /** Type is String. @hide */ + public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration"; + /** Type is rational. @hide */ + public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel"; + /** Type is int. @hide */ + public static final String TAG_CONTRAST = "Contrast"; + /** Type is int. @hide */ + public static final String TAG_CUSTOM_RENDERED = "CustomRendered"; /** Type is String. */ + public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized"; + /** Type is String. @hide */ + public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal"; + /** Type is String. @hide */ + public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription"; + /** Type is double. @hide */ + public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio"; + /** Type is String. @hide */ + public static final String TAG_EXIF_VERSION = "ExifVersion"; + /** Type is double. @hide */ + public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue"; + /** Type is rational. @hide */ + public static final String TAG_EXPOSURE_INDEX = "ExposureIndex"; + /** Type is int. @hide */ + public static final String TAG_EXPOSURE_MODE = "ExposureMode"; + /** Type is int. @hide */ + public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram"; + /** Type is double. */ public static final String TAG_EXPOSURE_TIME = "ExposureTime"; - /** Type is String. */ + /** Type is double. */ public static final String TAG_APERTURE = "FNumber"; - /** Type is String. */ + /** Type is String. @hide */ + public static final String TAG_FILE_SOURCE = "FileSource"; + /** Type is int. */ + public static final String TAG_FLASH = "Flash"; + /** Type is rational. @hide */ + public static final String TAG_FLASH_ENERGY = "FlashEnergy"; + /** Type is String. @hide */ + public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion"; + /** Type is rational. */ + public static final String TAG_FOCAL_LENGTH = "FocalLength"; + /** Type is int. @hide */ + public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm"; + /** Type is int. @hide */ + public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit"; + /** Type is rational. @hide */ + public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution"; + /** Type is rational. @hide */ + public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution"; + /** Type is int. @hide */ + public static final String TAG_GAIN_CONTROL = "GainControl"; + /** Type is int. */ public static final String TAG_ISO = "ISOSpeedRatings"; + /** Type is String. @hide */ + public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID"; + /** Type is int. @hide */ + public static final String TAG_LIGHT_SOURCE = "LightSource"; + /** Type is String. @hide */ + public static final String TAG_MAKER_NOTE = "MakerNote"; + /** Type is rational. @hide */ + public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue"; + /** Type is int. @hide */ + public static final String TAG_METERING_MODE = "MeteringMode"; + /** Type is String. @hide */ + public static final String TAG_OECF = "OECF"; + /** Type is int. @hide */ + public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension"; + /** Type is int. @hide */ + public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension"; + /** Type is String. @hide */ + public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile"; + /** Type is int. @hide */ + public static final String TAG_SATURATION = "Saturation"; + /** Type is int. @hide */ + public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType"; + /** Type is String. @hide */ + public static final String TAG_SCENE_TYPE = "SceneType"; + /** Type is int. @hide */ + public static final String TAG_SENSING_METHOD = "SensingMethod"; + /** Type is int. @hide */ + public static final String TAG_SHARPNESS = "Sharpness"; + /** Type is rational. @hide */ + public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue"; + /** Type is String. @hide */ + public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse"; + /** Type is String. @hide */ + public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity"; /** Type is String. */ - public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized"; - /** Type is int. */ public static final String TAG_SUBSEC_TIME = "SubSecTime"; - /** Type is int. */ + /** Type is String. */ + public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized"; + /** Type is String. */ public static final String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal"; + /** Type is int. @hide */ + public static final String TAG_SUBJECT_AREA = "SubjectArea"; + /** Type is double. @hide */ + public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance"; + /** Type is int. @hide */ + public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange"; + /** Type is int. @hide */ + public static final String TAG_SUBJECT_LOCATION = "SubjectLocation"; + /** Type is String. @hide */ + public static final String TAG_USER_COMMENT = "UserComment"; /** Type is int. */ - public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized"; - - - - /** - * @hide - */ - public static final String TAG_SUBSECTIME = "SubSecTime"; - + public static final String TAG_WHITE_BALANCE = "WhiteBalance"; /** * The altitude (in meters) based on the reference in TAG_GPS_ALTITUDE_REF. * Type is rational. */ public static final String TAG_GPS_ALTITUDE = "GPSAltitude"; - /** * 0 if the altitude is above sea level. 1 if the altitude is below sea * level. Type is int. */ public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef"; - - /** Type is String. */ - public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp"; + /** Type is String. @hide */ + public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DOP = "GPSDOP"; /** Type is String. */ public static final String TAG_GPS_DATESTAMP = "GPSDateStamp"; - /** Type is int. */ - public static final String TAG_WHITE_BALANCE = "WhiteBalance"; - /** Type is rational. */ - public static final String TAG_FOCAL_LENGTH = "FocalLength"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef"; + /** Type is rational. @hide */ + public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude"; + /** Type is String. @hide */ + public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef"; + /** Type is int. @hide */ + public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential"; + /** Type is rational. @hide */ + public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; + /** Type is String. @hide */ + public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef"; + /** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */ + public static final String TAG_GPS_LATITUDE = "GPSLatitude"; + /** Type is String. */ + public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"; + /** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */ + public static final String TAG_GPS_LONGITUDE = "GPSLongitude"; + /** Type is String. */ + public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"; + /** Type is String. @hide */ + public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum"; + /** Type is String. @hide */ + public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode"; /** Type is String. Name of GPS processing method used for location finding. */ public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod"; + /** Type is String. @hide */ + public static final String TAG_GPS_SATELLITES = "GPSSatellites"; + /** Type is rational. @hide */ + public static final String TAG_GPS_SPEED = "GPSSpeed"; + /** Type is String. @hide */ + public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef"; + /** Type is String. @hide */ + public static final String TAG_GPS_STATUS = "GPSStatus"; + /** Type is String. Format is "hh:mm:ss". */ + public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp"; + /** Type is rational. @hide */ + public static final String TAG_GPS_TRACK = "GPSTrack"; + /** Type is String. @hide */ + public static final String TAG_GPS_TRACK_REF = "GPSTrackRef"; + /** Type is String. @hide */ + public static final String TAG_GPS_VERSION_ID = "GPSVersionID"; + /** Type is String. @hide */ + public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex"; + /** Type is int. @hide */ + public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength"; + /** Type is int. @hide */ + public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth"; + + // Private tags used for pointing the other IFD offset. The types of the following tags are int. + private static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer"; + private static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer"; + private static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer"; + + // Private tags used for thumbnail information. + private static final String TAG_HAS_THUMBNAIL = "HasThumbnail"; + private static final String TAG_THUMBNAIL_OFFSET = "ThumbnailOffset"; + private static final String TAG_THUMBNAIL_LENGTH = "ThumbnailLength"; + private static final String TAG_THUMBNAIL_DATA = "ThumbnailData"; // Constants used for the Orientation Exif tag. public static final int ORIENTATION_UNDEFINED = 0; @@ -102,34 +324,730 @@ public class ExifInterface { public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // left right reversed mirror public static final int ORIENTATION_ROTATE_180 = 3; public static final int ORIENTATION_FLIP_VERTICAL = 4; // upside down mirror - public static final int ORIENTATION_TRANSPOSE = 5; // flipped about top-left <--> bottom-right axis + // flipped about top-left <--> bottom-right axis + public static final int ORIENTATION_TRANSPOSE = 5; public static final int ORIENTATION_ROTATE_90 = 6; // rotate 90 cw to right it - public static final int ORIENTATION_TRANSVERSE = 7; // flipped about top-right <--> bottom-left axis + // flipped about top-right <--> bottom-left axis + public static final int ORIENTATION_TRANSVERSE = 7; public static final int ORIENTATION_ROTATE_270 = 8; // rotate 270 to right it // 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; + // Names for the data formats for debugging purpose. + private static final String[] IFD_FORMAT_NAMES = new String[] { + "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT", + "SLONG", "SRATIONAL", "SINGLE", "DOUBLE" + }; + // Sizes of the components of each IFD value format + 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 rational type. + private static class Rational { + public final long numerator; + public final long denominator; + + private Rational(long numerator, long denominator) { + // Handle erroneous case + if (denominator == 0) { + this.numerator = 0; + this.denominator = 1; + return; + } + this.numerator = numerator; + this.denominator = denominator; + } + + @Override + public String toString() { + return numerator + "/" + denominator; + } + + public double calculate() { + return (double) numerator / denominator; + } + } + + // A class for indicating EXIF attribute. + private static class ExifAttribute { + public final int format; + public final int numberOfComponents; + public final byte[] bytes; + + private ExifAttribute(int format, int numberOfComponents, byte[] bytes) { + this.format = format; + this.numberOfComponents = numberOfComponents; + this.bytes = bytes; + } + + public static ExifAttribute createUShort(int[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]); + buffer.order(byteOrder); + for (int value : values) { + buffer.putShort((short) value); + } + return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array()); + } + + public static ExifAttribute createUShort(int value, ByteOrder byteOrder) { + return createUShort(new int[] {value}, byteOrder); + } + + public static ExifAttribute createULong(long[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]); + buffer.order(byteOrder); + for (long value : values) { + buffer.putInt((int) value); + } + return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array()); + } + + public static ExifAttribute createULong(long value, ByteOrder byteOrder) { + return createULong(new long[] {value}, byteOrder); + } + + public static ExifAttribute createSLong(int[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]); + buffer.order(byteOrder); + for (int value : values) { + buffer.putInt(value); + } + return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array()); + } + + public static ExifAttribute createSLong(int value, ByteOrder byteOrder) { + return createSLong(new int[] {value}, byteOrder); + } + + public static ExifAttribute createByte(String value) { + // Exception for GPSAltitudeRef tag + if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') { + final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') }; + return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes); + } + final byte[] ascii = value.getBytes(ASCII); + return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii); + } + + public static ExifAttribute createString(String value) { + final byte[] ascii = (value + '\0').getBytes(ASCII); + return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii); + } + + public static ExifAttribute createURational(Rational[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]); + buffer.order(byteOrder); + for (Rational value : values) { + buffer.putInt((int) value.numerator); + buffer.putInt((int) value.denominator); + } + return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array()); + } + + public static ExifAttribute createURational(Rational value, ByteOrder byteOrder) { + return createURational(new Rational[] {value}, byteOrder); + } + + public static ExifAttribute createSRational(Rational[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]); + buffer.order(byteOrder); + for (Rational value : values) { + buffer.putInt((int) value.numerator); + buffer.putInt((int) value.denominator); + } + return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array()); + } + + public static ExifAttribute createSRational(Rational value, ByteOrder byteOrder) { + return createSRational(new Rational[] {value}, byteOrder); + } + + public static ExifAttribute createDouble(double[] values, ByteOrder byteOrder) { + final ByteBuffer buffer = ByteBuffer.wrap( + new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]); + buffer.order(byteOrder); + for (double value : values) { + buffer.putDouble(value); + } + return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array()); + } + + public static ExifAttribute createDouble(double value, ByteOrder byteOrder) { + return createDouble(new double[] {value}, byteOrder); + } + + @Override + public String toString() { + return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")"; + } + + private Object getValue(ByteOrder byteOrder) { + try { + ByteOrderAwarenessDataInputStream inputStream = + new ByteOrderAwarenessDataInputStream(bytes); + inputStream.setByteOrder(byteOrder); + switch (format) { + case IFD_FORMAT_BYTE: + case IFD_FORMAT_SBYTE: { + // Exception for GPSAltitudeRef tag + if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) { + return new String(new char[] { (char) (bytes[0] + '0') }); + } + return new String(bytes, ASCII); + } + case IFD_FORMAT_UNDEFINED: + case IFD_FORMAT_STRING: { + int index = 0; + if (numberOfComponents >= EXIF_ASCII_PREFIX.length) { + boolean same = true; + for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) { + if (bytes[i] != EXIF_ASCII_PREFIX[i]) { + same = false; + break; + } + } + if (same) { + index = EXIF_ASCII_PREFIX.length; + } + } + + StringBuilder stringBuilder = new StringBuilder(); + while (index < numberOfComponents) { + int ch = bytes[index]; + if (ch == 0) { + break; + } + if (ch >= 32) { + stringBuilder.append((char) ch); + } else { + stringBuilder.append('?'); + } + ++index; + } + return stringBuilder.toString(); + } + case IFD_FORMAT_USHORT: { + final int[] values = new int[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readUnsignedShort(); + } + return values; + } + case IFD_FORMAT_ULONG: { + final long[] values = new long[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readUnsignedInt(); + } + return values; + } + case IFD_FORMAT_URATIONAL: { + final Rational[] values = new Rational[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + final long numerator = inputStream.readUnsignedInt(); + final long denominator = inputStream.readUnsignedInt(); + values[i] = new Rational(numerator, denominator); + } + return values; + } + case IFD_FORMAT_SSHORT: { + final int[] values = new int[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readShort(); + } + return values; + } + case IFD_FORMAT_SLONG: { + final int[] values = new int[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readInt(); + } + return values; + } + case IFD_FORMAT_SRATIONAL: { + final Rational[] values = new Rational[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + final long numerator = inputStream.readInt(); + final long denominator = inputStream.readInt(); + values[i] = new Rational(numerator, denominator); + } + return values; + } + case IFD_FORMAT_SINGLE: { + final double[] values = new double[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readFloat(); + } + return values; + } + case IFD_FORMAT_DOUBLE: { + final double[] values = new double[numberOfComponents]; + for (int i = 0; i < numberOfComponents; ++i) { + values[i] = inputStream.readDouble(); + } + return values; + } + default: + return null; + } + } catch (IOException e) { + Log.w(TAG, "IOException occurred during reading a value", e); + return null; + } + } + + public double getDoubleValue(ByteOrder byteOrder) { + Object value = getValue(byteOrder); + if (value == null) { + throw new NumberFormatException("NULL can't be converted to a double value"); + } + if (value instanceof String) { + return Double.parseDouble((String) value); + } + if (value instanceof long[]) { + long[] array = (long[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof int[]) { + int[] array = (int[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof double[]) { + double[] array = (double[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof Rational[]) { + Rational[] array = (Rational[]) value; + if (array.length == 1) { + return array[0].calculate(); + } + throw new NumberFormatException("There are more than one component"); + } + throw new NumberFormatException("Couldn't find a double value"); + } + + public int getIntValue(ByteOrder byteOrder) { + Object value = getValue(byteOrder); + if (value == null) { + throw new NumberFormatException("NULL can't be converted to a integer value"); + } + if (value instanceof String) { + return Integer.parseInt((String) value); + } + if (value instanceof long[]) { + long[] array = (long[]) value; + if (array.length == 1) { + return (int) array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + if (value instanceof int[]) { + int[] array = (int[]) value; + if (array.length == 1) { + return array[0]; + } + throw new NumberFormatException("There are more than one component"); + } + throw new NumberFormatException("Couldn't find a integer value"); + } + + public String getStringValue(ByteOrder byteOrder) { + Object value = getValue(byteOrder); + if (value == null) { + return null; + } + if (value instanceof String) { + return (String) value; + } + + final StringBuilder stringBuilder = new StringBuilder(); + if (value instanceof long[]) { + long[] array = (long[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i]); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + if (value instanceof int[]) { + int[] array = (int[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i]); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + if (value instanceof double[]) { + double[] array = (double[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i]); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + if (value instanceof Rational[]) { + Rational[] array = (Rational[]) value; + for (int i = 0; i < array.length; ++i) { + stringBuilder.append(array[i].numerator); + stringBuilder.append('/'); + stringBuilder.append(array[i].denominator); + if (i + 1 != array.length) { + stringBuilder.append(","); + } + } + return stringBuilder.toString(); + } + return null; + } + + public int size() { + return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents; + } + } + + // A class for indicating EXIF tag. + private static class ExifTag { + public final int number; + public final String name; + public final int primaryFormat; + public final int secondaryFormat; + + private ExifTag(String name, int number, int format) { + this.name = name; + this.number = number; + this.primaryFormat = format; + this.secondaryFormat = -1; + } + + private ExifTag(String name, int number, int primaryFormat, int secondaryFormat) { + this.name = name; + this.number = number; + this.primaryFormat = primaryFormat; + this.secondaryFormat = secondaryFormat; + } + } + + // Primary image IFD TIFF tags (See JEITA CP-3451 Table 14. page 54). + private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] { + new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT), + new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT), + new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT), + new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING), + new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING), + new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING), + new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT), + new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT), + new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT), + new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT), + new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT), + new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING), + new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING), + new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG), + new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), + new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), + new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), + new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), + new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), + }; + + // 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(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_APERTURE, 33437, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT), + new ExifTag(TAG_SPECTRAL_SENSITIVITY, 34852, IFD_FORMAT_STRING), + new ExifTag(TAG_ISO, 34855, IFD_FORMAT_USHORT), + new ExifTag(TAG_OECF, 34856, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING), + new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_COMPRESSED_BITS_PER_PIXEL, 37122, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL), + new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL), + new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL), + new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SUBJECT_DISTANCE, 37382, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT), + new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT), + new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT), + new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SUBJECT_AREA, 37396, IFD_FORMAT_USHORT), + new ExifTag(TAG_MAKER_NOTE, 37500, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_USER_COMMENT, 37510, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING), + new ExifTag(TAG_SUBSEC_TIME_ORIG, 37521, IFD_FORMAT_STRING), + new ExifTag(TAG_SUBSEC_TIME_DIG, 37522, IFD_FORMAT_STRING), + new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT), + new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_RELATED_SOUND_FILE, 40964, IFD_FORMAT_STRING), + new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG), + new ExifTag(TAG_FLASH_ENERGY, 41483, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SPATIAL_FREQUENCY_RESPONSE, 41484, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_FOCAL_PLANE_X_RESOLUTION, 41486, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_FOCAL_PLANE_Y_RESOLUTION, 41487, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT), + new ExifTag(TAG_SUBJECT_LOCATION, 41492, IFD_FORMAT_USHORT), + new ExifTag(TAG_EXPOSURE_INDEX, 41493, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT), + new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_CFA_PATTERN, 41730, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT), + new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT), + new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT), + new ExifTag(TAG_DIGITAL_ZOOM_RATIO, 41988, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_FOCAL_LENGTH_IN_35MM_FILM, 41989, IFD_FORMAT_USHORT), + new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT), + new ExifTag(TAG_GAIN_CONTROL, 41991, IFD_FORMAT_USHORT), + new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT), + new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT), + new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT), + new ExifTag(TAG_DEVICE_SETTING_DESCRIPTION, 41995, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_SUBJECT_DISTANCE_RANGE, 41996, IFD_FORMAT_USHORT), + new ExifTag(TAG_IMAGE_UNIQUE_ID, 42016, IFD_FORMAT_STRING), + }; + + // 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(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE), + new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE), + new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_SATELLITES, 8, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_STATUS, 9, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_MEASURE_MODE, 10, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DOP, 11, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_SPEED, 13, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_TRACK, 15, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_IMG_DIRECTION, 17, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_MAP_DATUM, 18, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_LATITUDE_REF, 19, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_LATITUDE, 20, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_DEST_LONGITUDE_REF, 21, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_LONGITUDE, 22, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_BEARING, 24, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DEST_DISTANCE, 26, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_GPS_PROCESSING_METHOD, 27, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_GPS_AREA_INFORMATION, 28, IFD_FORMAT_UNDEFINED), + new ExifTag(TAG_GPS_DATESTAMP, 29, IFD_FORMAT_STRING), + new ExifTag(TAG_GPS_DIFFERENTIAL, 30, IFD_FORMAT_USHORT), + }; + // Primary image IFD Interoperability tag (See JEITA CP-3451 Table 17. page 56). + private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] { + new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING), + }; + // IFD Thumbnail tags (See JEITA CP-3451 Table 18. page 57). + private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] { + new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT), + new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT), + new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT), + new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING), + new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING), + new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING), + new ExifTag(TAG_STRIP_OFFSETS, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT), + new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT), + new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), + new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT), + new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT), + new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT), + new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING), + new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING), + new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING), + new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG), + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG), + new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), + new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), + new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), + new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), + new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), + new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), + }; + + // 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(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), + new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), + new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG), + }; + // 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(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG); + private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG = + new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG); + + // Mappings from tag number to tag name and each item represents one IFD tag group. + private static final HashMap[] sExifTagMapsForReading = new HashMap[EXIF_TAGS.length]; + // Mappings from tag name to tag number and each item represents one IFD tag group. + private static final HashMap[] sExifTagMapsForWriting = new HashMap[EXIF_TAGS.length]; + private static final HashSet sTagSetForCompatibility = new HashSet<>(Arrays.asList( + TAG_APERTURE, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE, + TAG_GPS_TIMESTAMP)); + + // 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. + + private static final Charset ASCII = Charset.forName("US-ASCII"); + // Identifier for EXIF APP1 segment in JPEG + private static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(ASCII); + // JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with + // the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start + // of frame(baseline DCT) and the image size info exists in its beginning part. + 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"); 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(); + sExifTagMapsForWriting[hint] = new HashMap(); + for (ExifTag tag : EXIF_TAGS[hint]) { + sExifTagMapsForReading[hint].put(tag.number, tag); + sExifTagMapsForWriting[hint].put(tag.name, tag); + } + } } - private String mFilename; - private HashMap mAttributes; + private final String mFilename; + private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length]; + private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN; private boolean mHasThumbnail; + // The following values used for indicating a thumbnail position. + private int mThumbnailOffset; + private int mThumbnailLength; + private byte[] mThumbnailBytes; - // 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(); + // Pattern to check non zero timestamp + private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*"); + // Pattern to check gps timestamp + private static final Pattern sGpsTimestampPattern = + Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"); /** - * Reads Exif tags from the specified JPEG file. + * Reads Exif tags from the specified image file. */ public ExifInterface(String filename) throws IOException { if (filename == null) { @@ -139,53 +1057,99 @@ public class ExifInterface { loadAttributes(); } + + /** + * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag in + * the image file. + * + * @param tag the name of the tag. + */ + private ExifAttribute getExifAttribute(String tag) { + // Retrieves all tag groups. The value from primary image tag group has a higher priority + // than the value from the thumbnail tag group if there are more than one candidates. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + Object value = mAttributes[i].get(tag); + if (value != null) { + return (ExifAttribute) value; + } + } + return null; + } + /** * Returns the value of the specified tag or {@code null} if there - * is no such tag in the JPEG file. + * is no such tag in the image file. * * @param tag the name of the tag. */ public String getAttribute(String tag) { - return mAttributes.get(tag); + ExifAttribute attribute = getExifAttribute(tag); + if (attribute != null) { + if (!sTagSetForCompatibility.contains(tag)) { + return attribute.getStringValue(mExifByteOrder); + } + if (tag.equals(TAG_GPS_TIMESTAMP)) { + // Convert the rational values to the custom formats for backwards compatibility. + if (attribute.format != IFD_FORMAT_URATIONAL + && attribute.format != IFD_FORMAT_SRATIONAL) { + return null; + } + Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder); + if (array.length != 3) { + return null; + } + return String.format("%02d:%02d:%02d", + (int) ((float) array[0].numerator / array[0].denominator), + (int) ((float) array[1].numerator / array[1].denominator), + (int) ((float) array[2].numerator / array[2].denominator)); + } + try { + return Double.toString(attribute.getDoubleValue(mExifByteOrder)); + } catch (NumberFormatException e) { + return null; + } + } + return null; } /** * Returns the integer value of the specified tag. If there is no such tag - * in the JPEG file or the value cannot be parsed as integer, return + * in the image file or the value cannot be parsed as integer, return * defaultValue. * * @param tag the name of the tag. * @param defaultValue the value to return if the tag is not available. */ public int getAttributeInt(String tag, int defaultValue) { - String value = mAttributes.get(tag); - if (value == null) return defaultValue; + ExifAttribute exifAttribute = getExifAttribute(tag); + if (exifAttribute == null) { + return defaultValue; + } + try { - return Integer.valueOf(value); - } catch (NumberFormatException ex) { + return exifAttribute.getIntValue(mExifByteOrder); + } catch (NumberFormatException e) { return defaultValue; } } /** - * Returns the double value of the specified rational tag. If there is no - * such tag in the JPEG file or the value cannot be parsed as double, return - * defaultValue. + * Returns the double value of the tag that is specified as rational or contains a + * double-formatted value. If there is no such tag in the image file or the value cannot be + * parsed as double, return defaultValue. * * @param tag the name of the tag. * @param defaultValue the value to return if the tag is not available. */ public double getAttributeDouble(String tag, double defaultValue) { - String value = mAttributes.get(tag); - if (value == null) return defaultValue; + ExifAttribute exifAttribute = getExifAttribute(tag); + if (exifAttribute == null) { + return defaultValue; + } + try { - int index = value.indexOf("/"); - if (index == -1) return defaultValue; - double denom = Double.parseDouble(value.substring(index + 1)); - if (denom == 0) return defaultValue; - double num = Double.parseDouble(value.substring(0, index)); - return num / denom; - } catch (NumberFormatException ex) { + return exifAttribute.getDoubleValue(mExifByteOrder); + } catch (NumberFormatException e) { return defaultValue; } } @@ -197,126 +1161,299 @@ public class ExifInterface { * @param value the value of the tag. */ public void setAttribute(String tag, String value) { - mAttributes.put(tag, value); - } - - /** - * Initialize mAttributes with the attributes from the file mFilename. - * - * mAttributes is a HashMap which stores the Exif attributes of the file. - * The key is the standard tag name and the value is the tag's value: e.g. - * Model -> Nikon. Numeric values are stored as strings. - * - * This function also initialize mHasThumbnail to indicate whether the - * file has a thumbnail inside. - */ - private void loadAttributes() throws IOException { - // 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" - mAttributes = new HashMap(); - - String attrStr; - synchronized (sLock) { - attrStr = getAttributesNative(mFilename); - } - - // get count - int ptr = attrStr.indexOf(' '); - int count = Integer.parseInt(attrStr.substring(0, ptr)); - // skip past the space between item count and the rest of the attributes - ++ptr; - - for (int i = 0; i < count; i++) { - // extract the attribute name - int equalPos = attrStr.indexOf('=', ptr); - String attrName = attrStr.substring(ptr, equalPos); - ptr = equalPos + 1; // skip past = - - // extract the attribute value length - int lenPos = attrStr.indexOf(' ', ptr); - int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos)); - ptr = lenPos + 1; // skip pas the space - - // extract the attribute value - String attrValue = attrStr.substring(ptr, ptr + attrLen); - ptr += attrLen; - - if (attrName.equals("hasThumbnail")) { - mHasThumbnail = attrValue.equalsIgnoreCase("true"); + // Convert the given value to rational values for backwards compatibility. + if (value != null && sTagSetForCompatibility.contains(tag)) { + if (tag.equals(TAG_GPS_TIMESTAMP)) { + Matcher m = sGpsTimestampPattern.matcher(value); + if (!m.find()) { + Log.w(TAG, "Invalid value for " + tag + " : " + value); + return; + } + value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1," + + Integer.parseInt(m.group(3)) + "/1"; } else { - mAttributes.put(attrName, attrValue); + try { + double doubleValue = Double.parseDouble(value); + value = (long) (doubleValue * 10000L) + "/10000"; + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid value for " + tag + " : " + value); + return; + } } } - } - /** - * Save the tag data into the JPEG file. This is expensive because it involves - * copying all the JPG data from one file to another and deleting the old file - * and renaming the other. It's best to use {@link #setAttribute(String,String)} - * to set all attributes to write and make a single call rather than multiple - * calls for each attribute. - */ - public void saveAttributes() throws IOException { - // 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("hasThumbnail")) { - --size; - } - sb.append(size + " "); - for (Map.Entry iter : mAttributes.entrySet()) { - String key = iter.getKey(); - if (key.equals("hasThumbnail")) { - // this is a fake attribute not saved as an exif tag + for (int i = 0 ; i < EXIF_TAGS.length; ++i) { + if (i == IFD_THUMBNAIL_HINT && !mHasThumbnail) { continue; } - String val = iter.getValue(); - sb.append(key + "="); - sb.append(val.length() + " "); - sb.append(val); - } - String s = sb.toString(); - synchronized (sLock) { - saveAttributesNative(mFilename, s); - commitChangesNative(mFilename); + final Object obj = sExifTagMapsForWriting[i].get(tag); + if (obj != null) { + if (value == null) { + mAttributes[i].remove(tag); + continue; + } + final ExifTag exifTag = (ExifTag) obj; + Pair guess = guessDataFormat(value); + int dataFormat; + if (exifTag.primaryFormat == guess.first || exifTag.primaryFormat == guess.second) { + dataFormat = exifTag.primaryFormat; + } else if (exifTag.secondaryFormat != -1 && (exifTag.secondaryFormat == guess.first + || exifTag.secondaryFormat == guess.second)) { + dataFormat = exifTag.secondaryFormat; + } else if (exifTag.primaryFormat == IFD_FORMAT_BYTE + || exifTag.primaryFormat == IFD_FORMAT_UNDEFINED + || exifTag.primaryFormat == IFD_FORMAT_STRING) { + dataFormat = exifTag.primaryFormat; + } else { + Log.w(TAG, "Given tag (" + tag + ") value didn't match with one of expected " + + "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat] + + (exifTag.secondaryFormat == -1 ? "" : ", " + + IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: " + + IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? "" : ", " + + IFD_FORMAT_NAMES[guess.second]) + ")"); + continue; + } + switch (dataFormat) { + case IFD_FORMAT_BYTE: { + mAttributes[i].put(tag, ExifAttribute.createByte(value)); + break; + } + case IFD_FORMAT_UNDEFINED: + case IFD_FORMAT_STRING: { + mAttributes[i].put(tag, ExifAttribute.createString(value)); + break; + } + case IFD_FORMAT_USHORT: { + final String[] values = value.split(","); + final int[] intArray = new int[values.length]; + for (int j = 0; j < values.length; ++j) { + intArray[j] = Integer.parseInt(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createUShort(intArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_SLONG: { + final String[] values = value.split(","); + final int[] intArray = new int[values.length]; + for (int j = 0; j < values.length; ++j) { + intArray[j] = Integer.parseInt(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createSLong(intArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_ULONG: { + final String[] values = value.split(","); + final long[] longArray = new long[values.length]; + for (int j = 0; j < values.length; ++j) { + longArray[j] = Long.parseLong(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createULong(longArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_URATIONAL: { + final String[] values = value.split(","); + final Rational[] rationalArray = new Rational[values.length]; + for (int j = 0; j < values.length; ++j) { + final String[] numbers = values[j].split("/"); + rationalArray[j] = new Rational(Long.parseLong(numbers[0]), + Long.parseLong(numbers[1])); + } + mAttributes[i].put(tag, + ExifAttribute.createURational(rationalArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_SRATIONAL: { + final String[] values = value.split(","); + final Rational[] rationalArray = new Rational[values.length]; + for (int j = 0; j < values.length; ++j) { + final String[] numbers = values[j].split("/"); + rationalArray[j] = new Rational(Long.parseLong(numbers[0]), + Long.parseLong(numbers[1])); + } + mAttributes[i].put(tag, + ExifAttribute.createSRational(rationalArray, mExifByteOrder)); + break; + } + case IFD_FORMAT_DOUBLE: { + final String[] values = value.split(","); + final double[] doubleArray = new double[values.length]; + for (int j = 0; j < values.length; ++j) { + doubleArray[j] = Double.parseDouble(values[j]); + } + mAttributes[i].put(tag, + ExifAttribute.createDouble(doubleArray, mExifByteOrder)); + break; + } + default: + Log.w(TAG, "Data format isn't one of expected formats: " + dataFormat); + continue; + } + } } } /** - * Returns true if the JPEG file has a thumbnail. + * Update the values of the tags in the tag groups if any value for the tag already was stored. + * + * @param tag the name of the tag. + * @param value the value of the tag in a form of {@link ExifAttribute}. + * @return Returns {@code true} if updating is placed. */ - public boolean hasThumbnail() { - return mHasThumbnail; + private boolean updateAttribute(String tag, ExifAttribute value) { + boolean updated = false; + for (int i = 0 ; i < EXIF_TAGS.length; ++i) { + if (mAttributes[i].containsKey(tag)) { + mAttributes[i].put(tag, value); + updated = true; + } + } + return updated; } /** - * Returns the thumbnail inside the JPEG file, or {@code null} if there is no thumbnail. - * The returned data is in JPEG format and can be decoded using - * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)} + * Remove any values of the specified tag. + * + * @param tag the name of the tag. */ - public byte[] getThumbnail() { - synchronized (sLock) { - return getThumbnailNative(mFilename); + private void removeAttribute(String tag) { + for (int i = 0 ; i < EXIF_TAGS.length; ++i) { + mAttributes[i].remove(tag); } } /** - * Returns the offset and length of thumbnail inside the JPEG file, or - * {@code null} if there is no thumbnail. - * - * @return two-element array, the offset in the first value, and length in - * the second, or {@code null} if no thumbnail was found. - * @hide + * This function decides which parser to read the image data according to the given input stream + * type and the content of the input stream. In each case, it reads the first three bytes to + * determine whether the image data format is JPEG or not. + */ + private void loadAttributes() throws IOException { + try { + InputStream in = new FileInputStream(mFilename); + // Initialize mAttributes. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + mAttributes[i] = new HashMap(); + } + getJpegAttributes(in); + } catch (IOException e) { + // Ignore exceptions in order to keep the compatibility with the old versions of + // ExifInterface. + Log.w(TAG, "Invalid image.", e); + } finally { + addDefaultValuesForCompatibility(); + if (DEBUG) { + printAttributes(); + } + } + } + + // Prints out attributes for debugging. + private void printAttributes() { + for (int i = 0; i < mAttributes.length; ++i) { + Log.d(TAG, "The size of tag group[" + i + "]: " + mAttributes[i].size()); + for (Map.Entry entry : (Set) mAttributes[i].entrySet()) { + final ExifAttribute tagValue = (ExifAttribute) entry.getValue(); + Log.d(TAG, "tagName: " + entry.getKey() + ", tagType: " + tagValue.toString() + + ", tagValue: '" + tagValue.getStringValue(mExifByteOrder) + "'"); + } + } + } + + /** + * Save the tag data into the original image file. This is expensive because it involves + * copying all the data from one file to another and deleting the old file and renaming the + * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write + * and make a single call rather than multiple calls for each attribute. + */ + public void saveAttributes() throws IOException { + // Keep the thumbnail in memory + mThumbnailBytes = getThumbnail(); + + File tempFile = null; + // Move the original file to temporary file. + tempFile = new File(mFilename + ".tmp"); + File originalFile = new File(mFilename); + if (!originalFile.renameTo(tempFile)) { + throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath()); + } + + FileInputStream in = null; + FileOutputStream out = null; + try { + // Save the new file. + in = new FileInputStream(tempFile); + out = new FileOutputStream(mFilename); + saveJpegAttributes(in, out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + tempFile.delete(); + } + + // Discard the thumbnail in memory + mThumbnailBytes = null; + } + + /** + * Returns true if the image file has a thumbnail. + */ + public boolean hasThumbnail() { + return mHasThumbnail; + } + + /** + * Returns the thumbnail inside the image file, or {@code null} if there is no thumbnail. + * The returned data is in JPEG format and can be decoded using + * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)} + */ + public byte[] getThumbnail() { + if (!mHasThumbnail) { + return null; + } + if (mThumbnailBytes != null) { + return mThumbnailBytes; + } + + // Read the thumbnail. + FileInputStream in = null; + try { + in = new FileInputStream(mFilename); + 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 e) { + // Couldn't get a thumbnail image. + } finally { + IoUtils.closeQuietly(in); + } + return null; + } + + /** + * Returns the offset and length of thumbnail inside the image file, or + * {@code null} if there is no thumbnail. + * + * @return two-element array, the offset in the first value, and length in + * the second, or {@code null} if no thumbnail was found. + * @hide */ public long[] getThumbnailRange() { - synchronized (sLock) { - return getThumbnailRangeNative(mFilename); + if (!mHasThumbnail) { + return null; } + + long[] range = new long[2]; + range[0] = mThumbnailOffset; + range[1] = mThumbnailLength; + + return range; } /** @@ -325,10 +1462,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 = getAttribute(TAG_GPS_LATITUDE); + String latRef = getAttribute(TAG_GPS_LATITUDE_REF); + String lngValue = getAttribute(TAG_GPS_LONGITUDE); + String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF); if (latValue != null && latRef != null && lngValue != null && lngRef != null) { try { @@ -354,7 +1491,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; } @@ -366,8 +1503,9 @@ public class ExifInterface { * @hide */ public long getDateTime() { - String dateTimeString = mAttributes.get(TAG_DATETIME); - if (dateTimeString == null) return -1; + String dateTimeString = getAttribute(TAG_DATETIME); + if (dateTimeString == null + || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1; ParsePosition pos = new ParsePosition(0); try { @@ -377,7 +1515,7 @@ public class ExifInterface { if (datetime == null) return -1; long msecs = datetime.getTime(); - String subSecs = mAttributes.get(TAG_SUBSECTIME); + String subSecs = getAttribute(TAG_SUBSEC_TIME); if (subSecs != null) { try { long sub = Long.valueOf(subSecs); @@ -386,10 +1524,11 @@ public class ExifInterface { } msecs += sub; } catch (NumberFormatException e) { + // Ignored } } return msecs; - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException e) { return -1; } } @@ -400,9 +1539,13 @@ public class ExifInterface { * @hide */ public long getGpsDateTime() { - String date = mAttributes.get(TAG_GPS_DATESTAMP); - String time = mAttributes.get(TAG_GPS_TIMESTAMP); - if (date == null || time == null) return -1; + String date = getAttribute(TAG_GPS_DATESTAMP); + String time = getAttribute(TAG_GPS_TIMESTAMP); + if (date == null || time == null + || (!sNonZeroTimePattern.matcher(date).matches() + && !sNonZeroTimePattern.matcher(time).matches())) { + return -1; + } String dateTimeString = date + ' ' + time; @@ -411,13 +1554,12 @@ public class ExifInterface { Date datetime = sFormatter.parse(dateTimeString, pos); if (datetime == null) return -1; return datetime.getTime(); - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException e) { return -1; } } - private static float convertRationalLatLonToFloat( - String rationalString, String ref) { + private static float convertRationalLatLonToFloat(String rationalString, String ref) { try { String [] parts = rationalString.split(","); @@ -439,26 +1581,949 @@ public class ExifInterface { return (float) -result; } return (float) result; - } catch (NumberFormatException e) { - // Some of the nubmers are not valid - throw new IllegalArgumentException(); - } catch (ArrayIndexOutOfBoundsException e) { - // Some of the rational does not follow the correct format + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + // Not valid throw new IllegalArgumentException(); } } - private 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; + 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)); + } + ++bytesRead; + while (true) { + 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)); + } + ++bytesRead; + + // EOI indicates the end of an image and in case of SOS, JPEG image stream starts and + // the image data will terminate right after. + if (marker == MARKER_EOI || marker == MARKER_SOS) { + break; + } + int length = dataInputStream.readUnsignedShort() - 2; + bytesRead += 2; + if (DEBUG) { + Log.d(TAG, "JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: " + + (length + 2) + ")"); + } + if (length < 0) { + throw new IOException("Invalid length"); + } + switch (marker) { + case MARKER_APP1: { + if (DEBUG) { + Log.d(TAG, "MARKER_APP1"); + } + if (length < 6) { + // Skip if it's not an EXIF APP1 segment. + break; + } + byte[] identifier = new byte[6]; + if (inputStream.read(identifier) != 6) { + throw new IOException("Invalid exif"); + } + bytesRead += 6; + length -= 6; + if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { + // Skip if it's not an EXIF APP1 segment. + break; + } + if (length <= 0) { + throw new IOException("Invalid exif"); + } + if (DEBUG) { + Log.d(TAG, "readExifSegment with a byte array (length: " + length + ")"); + } + 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"); + } + length = 0; + if (getAttribute(TAG_USER_COMMENT) == null) { + mAttributes[IFD_EXIF_HINT].put(TAG_USER_COMMENT, ExifAttribute.createString( + new String(bytes, ASCII))); + } + break; + } + + case MARKER_SOF0: + case MARKER_SOF1: + case MARKER_SOF2: + case MARKER_SOF3: + case MARKER_SOF5: + case MARKER_SOF6: + case MARKER_SOF7: + case MARKER_SOF9: + case MARKER_SOF10: + case MARKER_SOF11: + case MARKER_SOF13: + case MARKER_SOF14: + case MARKER_SOF15: { + if (dataInputStream.skipBytes(1) != 1) { + throw new IOException("Invalid SOFx"); + } + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong( + dataInputStream.readUnsignedShort(), mExifByteOrder)); + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong( + dataInputStream.readUnsignedShort(), mExifByteOrder)); + length -= 5; + break; + } + + default: { + break; + } + } + if (length < 0) { + throw new IOException("Invalid length"); + } + if (dataInputStream.skipBytes(length) != length) { + throw new IOException("Invalid JPEG segment"); + } + bytesRead += 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); + ByteOrderAwarenessDataOutputStream dataOutputStream = + new ByteOrderAwarenessDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN); + if (dataInputStream.readByte() != MARKER) { + throw new IOException("Invalid marker"); + } + dataOutputStream.writeByte(MARKER); + if (dataInputStream.readByte() != MARKER_SOI) { + throw new IOException("Invalid marker"); + } + dataOutputStream.writeByte(MARKER_SOI); + + // Write EXIF APP1 segment + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(MARKER_APP1); + writeExifSegment(dataOutputStream, 6); + + byte[] bytes = new byte[4096]; + + while (true) { + byte marker = dataInputStream.readByte(); + if (marker != MARKER) { + throw new IOException("Invalid marker"); + } + marker = dataInputStream.readByte(); + switch (marker) { + case MARKER_APP1: { + int length = dataInputStream.readUnsignedShort() - 2; + if (length < 0) { + throw new IOException("Invalid length"); + } + byte[] identifier = new byte[6]; + if (length >= 6) { + if (dataInputStream.read(identifier) != 6) { + throw new IOException("Invalid exif"); + } + if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { + // Skip the original EXIF APP1 segment. + if (dataInputStream.skip(length - 6) != length - 6) { + throw new IOException("Invalid length"); + } + break; + } + } + // Copy non-EXIF APP1 segment. + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(marker); + dataOutputStream.writeUnsignedShort(length + 2); + if (length >= 6) { + length -= 6; + dataOutputStream.write(identifier); + } + int read; + while (length > 0 && (read = dataInputStream.read( + bytes, 0, Math.min(length, bytes.length))) >= 0) { + dataOutputStream.write(bytes, 0, read); + length -= read; + } + break; + } + case MARKER_EOI: + case MARKER_SOS: { + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(marker); + // Copy all the remaining data + Streams.copy(dataInputStream, dataOutputStream); + return; + } + default: { + // Copy JPEG segment + dataOutputStream.writeByte(MARKER); + dataOutputStream.writeByte(marker); + int length = dataInputStream.readUnsignedShort(); + dataOutputStream.writeUnsignedShort(length); + length -= 2; + if (length < 0) { + throw new IOException("Invalid length"); + } + int read; + while (length > 0 && (read = dataInputStream.read( + bytes, 0, Math.min(length, bytes.length))) >= 0) { + dataOutputStream.write(bytes, 0, read); + length -= read; + } + break; + } + } + } + } + + // Reads the given EXIF byte area and save its tag data into attributes. + private void readExifSegment(byte[] exifBytes, int 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"); + } + mExifByteOrder = ByteOrder.LITTLE_ENDIAN; + break; + case BYTE_ALIGN_MM: + if (DEBUG) { + Log.d(TAG, "readExifSegment: Byte Align MM"); + } + mExifByteOrder = ByteOrder.BIG_ENDIAN; + break; + default: + throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder)); + } + + // Set byte order. + dataInputStream.setByteOrder(mExifByteOrder); + + 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. + String jpegInterchangeFormatString = getAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name); + String jpegInterchangeFormatLengthString = + getAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name); + if (jpegInterchangeFormatString != null && jpegInterchangeFormatLengthString != null) { + try { + int jpegInterchangeFormat = Integer.parseInt(jpegInterchangeFormatString); + int jpegInterchangeFormatLength = Integer + .parseInt(jpegInterchangeFormatLengthString); + // The following code limits the size of thumbnail size not to overflow EXIF data area. + jpegInterchangeFormatLength = Math.min(jpegInterchangeFormat + + jpegInterchangeFormatLength, exifBytes.length) - jpegInterchangeFormat; + if (jpegInterchangeFormat > 0 && jpegInterchangeFormatLength > 0) { + mHasThumbnail = true; + mThumbnailOffset = exifOffsetFromBeginning + jpegInterchangeFormat; + mThumbnailLength = jpegInterchangeFormatLength; + } + } catch (NumberFormatException e) { + // Ignored the corrupted image. + } + } + } + + private void addDefaultValuesForCompatibility() { + // The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag. + String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL); + if (valueOfDateTimeOriginal != null) { + mAttributes[IFD_TIFF_HINT].put(TAG_DATETIME, + ExifAttribute.createString(valueOfDateTimeOriginal)); + } + + // Add the default value. + if (getAttribute(TAG_IMAGE_WIDTH) == null) { + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (getAttribute(TAG_IMAGE_LENGTH) == null) { + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (getAttribute(TAG_ORIENTATION) == null) { + mAttributes[IFD_TIFF_HINT].put(TAG_ORIENTATION, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (getAttribute(TAG_LIGHT_SOURCE) == null) { + mAttributes[IFD_EXIF_HINT].put(TAG_LIGHT_SOURCE, + ExifAttribute.createULong(0, mExifByteOrder)); + } + } + + // Reads image file directory, which is a tag group in EXIF. + private void readImageFileDirectory(ByteOrderAwarenessDataInputStream dataInputStream, int hint) + throws IOException { + if (dataInputStream.peek() + 2 > dataInputStream.mLength) { + // Return if there is no data from the offset. + return; + } + // See JEITA CP-3451 Figure 5. page 9. + short numberOfDirectoryEntry = dataInputStream.readShort(); + if (dataInputStream.peek() + 12 * numberOfDirectoryEntry > dataInputStream.mLength) { + // Return if the size of entries is too big. + return; + } + + 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. + // Look up a corresponding tag from tag number + final ExifTag tag = (ExifTag) sExifTagMapsForReading[hint].get(tagNumber); + + if (DEBUG) { + Log.d(TAG, String.format("hint: %d, tagNumber: %d, tagName: %s, dataFormat: %d, " + + "numberOfComponents: %d", hint, tagNumber, tag != null ? tag.name : null, + dataFormat, numberOfComponents)); + } + + if (tag == null || dataFormat <= 0 || + dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) { + // Skip if the parsed tag number is not defined or invalid data format. + if (tag == null) { + Log.w(TAG, "Skip the tag entry since tag number is not defined: " + tagNumber); + } else { + Log.w(TAG, "Skip the tag entry since data format is invalid: " + dataFormat); + } + dataInputStream.seek(nextEntryOffset); + continue; + } + + // Read a value from data field or seek to the value offset which is stored in data + // field if the size of the entry value is bigger than 4. + 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); + } + if (offset + byteCount <= dataInputStream.mLength) { + dataInputStream.seek(offset); + } else { + // Skip if invalid data offset. + Log.w(TAG, "Skip the tag entry since data offset is invalid: " + offset); + dataInputStream.seek(nextEntryOffset); + continue; + } + } + + // Recursively parse IFD when a IFD pointer tag appears. + int innerIfdHint = getIfdHintFromTagNumber(tagNumber); + if (DEBUG) { + Log.d(TAG, "innerIfdHint: " + innerIfdHint + " byteCount: " + byteCount); + } + + 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, tag.name)); + } + if (offset > 0L && offset < dataInputStream.mLength) { + dataInputStream.seek(offset); + readImageFileDirectory(dataInputStream, innerIfdHint); + } else { + Log.w(TAG, "Skip jump into the IFD since its offset is invalid: " + offset); + } + + dataInputStream.seek(nextEntryOffset); + continue; + } + + byte[] bytes = new byte[numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat]]; + dataInputStream.readFully(bytes); + mAttributes[hint].put( + tag.name, new ExifAttribute(dataFormat, numberOfComponents, bytes)); + if (dataInputStream.peek() != nextEntryOffset) { + dataInputStream.seek(nextEntryOffset); + } + } + + if (dataInputStream.peek() + 4 <= dataInputStream.mLength) { + 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 && nextIfdOffset < dataInputStream.mLength) { + dataInputStream.seek(nextIfdOffset); + readImageFileDirectory(dataInputStream, IFD_THUMBNAIL_HINT); + } + } + } + + // 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(ByteOrderAwarenessDataOutputStream dataOutputStream, + int exifOffsetFromBeginning) throws IOException { + // The following variables are for calculating each IFD tag group size in bytes. + int[] ifdOffsets = new int[EXIF_TAGS.length]; + int[] ifdDataSizes = new int[EXIF_TAGS.length]; + + // Remove IFD pointer tags (we'll re-add it later.) + for (ExifTag tag : IFD_POINTER_TAGS) { + removeAttribute(tag.name); + } + // Remove old thumbnail data + removeAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name); + removeAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name); + + // Remove null value tags. + for (int hint = 0; hint < EXIF_TAGS.length; ++hint) { + for (Object obj : mAttributes[hint].entrySet().toArray()) { + final Map.Entry entry = (Map.Entry) obj; + if (entry.getValue() == null) { + mAttributes[hint].remove(entry.getKey()); + } + } + } - private native void saveAttributesNative(String fileName, - String compressedAttributes); + // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD + // offset when there is one or more tags in the thumbnail IFD. + if (!mAttributes[IFD_INTEROPERABILITY_HINT].isEmpty()) { + mAttributes[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].name, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (!mAttributes[IFD_EXIF_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].name, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (!mAttributes[IFD_GPS_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].name, + ExifAttribute.createULong(0, mExifByteOrder)); + } + if (mHasThumbnail) { + mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, + ExifAttribute.createULong(0, mExifByteOrder)); + mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name, + ExifAttribute.createULong(mThumbnailLength, mExifByteOrder)); + } - private native String getAttributesNative(String fileName); + // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry + // value which has a bigger size than 4 bytes. + for (int i = 0; i < EXIF_TAGS.length; ++i) { + int sum = 0; + for (Map.Entry entry : (Set) mAttributes[i].entrySet()) { + final ExifAttribute exifAttribute = (ExifAttribute) entry.getValue(); + final int size = exifAttribute.size(); + if (size > 4) { + sum += size; + } + } + ifdDataSizes[i] += sum; + } - private native void commitChangesNative(String fileName); + // Calculate IFD offsets. + int position = 8; + for (int hint = 0; hint < EXIF_TAGS.length; ++hint) { + if (!mAttributes[hint].isEmpty()) { + ifdOffsets[hint] = position; + position += 2 + mAttributes[hint].size() * 12 + 4 + ifdDataSizes[hint]; + } + } + if (mHasThumbnail) { + int thumbnailOffset = position; + mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, + ExifAttribute.createULong(thumbnailOffset, mExifByteOrder)); + mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset; + position += mThumbnailLength; + } - private native byte[] getThumbnailNative(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 < EXIF_TAGS.length; ++i) { + Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d", + i, ifdOffsets[i], mAttributes[i].size(), ifdDataSizes[i])); + } + } - private native long[] getThumbnailRangeNative(String fileName); + // Update IFD pointer tags with the calculated offsets. + if (!mAttributes[IFD_EXIF_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[0].name, + ExifAttribute.createULong(ifdOffsets[IFD_EXIF_HINT], mExifByteOrder)); + } + if (!mAttributes[IFD_GPS_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT].put(IFD_POINTER_TAGS[1].name, + ExifAttribute.createULong(ifdOffsets[IFD_GPS_HINT], mExifByteOrder)); + } + if (!mAttributes[IFD_INTEROPERABILITY_HINT].isEmpty()) { + mAttributes[IFD_EXIF_HINT].put(IFD_POINTER_TAGS[2].name, ExifAttribute.createULong( + ifdOffsets[IFD_INTEROPERABILITY_HINT], mExifByteOrder)); + } + + // Write TIFF Headers. See JEITA CP-3451C Table 1. page 10. + dataOutputStream.writeUnsignedShort(totalSize); + dataOutputStream.write(IDENTIFIER_EXIF_APP1); + dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN + ? BYTE_ALIGN_MM : BYTE_ALIGN_II); + dataOutputStream.setByteOrder(mExifByteOrder); + dataOutputStream.writeUnsignedShort(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 (!mAttributes[hint].isEmpty()) { + // See JEITA CP-3451C 4.6.2 IFD structure. page 13. + // Write entry count + dataOutputStream.writeUnsignedShort(mAttributes[hint].size()); + + // Write entry info + int dataOffset = ifdOffsets[hint] + 2 + mAttributes[hint].size() * 12 + 4; + for (Map.Entry entry : (Set) mAttributes[hint].entrySet()) { + // Convert tag name to tag number. + final ExifTag tag = (ExifTag) sExifTagMapsForWriting[hint].get(entry.getKey()); + final int tagNumber = tag.number; + final ExifAttribute attribute = (ExifAttribute) entry.getValue(); + final int size = attribute.size(); + + dataOutputStream.writeUnsignedShort(tagNumber); + dataOutputStream.writeUnsignedShort(attribute.format); + dataOutputStream.writeInt(attribute.numberOfComponents); + if (size > 4) { + dataOutputStream.writeUnsignedInt(dataOffset); + dataOffset += size; + } else { + dataOutputStream.write(attribute.bytes); + // Fill zero up to 4 bytes + if (size < 4) { + for (int i = size; i < 4; ++i) { + dataOutputStream.writeByte(0); + } + } + } + } + + // Write the next offset. It writes the offset of thumbnail IFD if there is one or + // more tags in the thumbnail IFD when the current IFD is the primary image TIFF + // IFD; Otherwise 0. + if (hint == 0 && !mAttributes[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 (Map.Entry entry : (Set) mAttributes[hint].entrySet()) { + ExifAttribute attribute = (ExifAttribute) entry.getValue(); + + if (attribute.bytes.length > 4) { + dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length); + } + } + } + } + + // Write thumbnail + if (mHasThumbnail) { + dataOutputStream.write(getThumbnail()); + } + + // Reset the byte order to big endian in order to write remaining parts of the JPEG file. + dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); + + return totalSize; + } + + /** + * Determines the data format of EXIF entry value. + * + * @param entryValue The value to be determined. + * @return Returns two data formats gussed as a pair in integer. If there is no two candidate + data formats for the given entry value, returns {@code -1} in the second of the pair. + */ + private static Pair guessDataFormat(String entryValue) { + // See TIFF 6.0 spec Types. page 15. + // Take the first component if there are more than one component. + if (entryValue.contains(",")) { + String[] entryValues = entryValue.split(","); + Pair dataFormat = guessDataFormat(entryValues[0]); + if (dataFormat.first == IFD_FORMAT_STRING) { + return dataFormat; + } + for (int i = 1; i < entryValues.length; ++i) { + final Pair guessDataFormat = guessDataFormat(entryValues[i]); + int first = -1, second = -1; + if (guessDataFormat.first == dataFormat.first + || guessDataFormat.second == dataFormat.first) { + first = dataFormat.first; + } + if (dataFormat.second != -1 && (guessDataFormat.first == dataFormat.second + || guessDataFormat.second == dataFormat.second)) { + second = dataFormat.second; + } + if (first == -1 && second == -1) { + return new Pair<>(IFD_FORMAT_STRING, -1); + } + if (first == -1) { + dataFormat = new Pair<>(second, -1); + continue; + } + if (second == -1) { + dataFormat = new Pair<>(first, -1); + continue; + } + } + return dataFormat; + } + + if (entryValue.contains("/")) { + String[] rationalNumber = entryValue.split("/"); + if (rationalNumber.length == 2) { + try { + long numerator = Long.parseLong(rationalNumber[0]); + long denominator = Long.parseLong(rationalNumber[1]); + if (numerator < 0L || denominator < 0L) { + return new Pair<>(IFD_FORMAT_SRATIONAL, - 1); + } + if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) { + return new Pair<>(IFD_FORMAT_URATIONAL, -1); + } + return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL); + } catch (NumberFormatException e) { + // Ignored + } + } + return new Pair<>(IFD_FORMAT_STRING, -1); + } + try { + Long longValue = Long.parseLong(entryValue); + if (longValue >= 0 && longValue <= 65535) { + return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG); + } + if (longValue < 0) { + return new Pair<>(IFD_FORMAT_SLONG, -1); + } + return new Pair<>(IFD_FORMAT_ULONG, -1); + } catch (NumberFormatException e) { + // Ignored + } + try { + Double.parseDouble(entryValue); + return new Pair<>(IFD_FORMAT_DOUBLE, -1); + } catch (NumberFormatException e) { + // Ignored + } + return new Pair<>(IFD_FORMAT_STRING, -1); + } + + // An input stream 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) + 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) + 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, which can be written in either little or big endian + // order. + private static class ByteOrderAwarenessDataOutputStream extends FilterOutputStream { + private final OutputStream mOutputStream; + private ByteOrder mByteOrder; + + public ByteOrderAwarenessDataOutputStream(OutputStream out, ByteOrder byteOrder) { + super(out); + mOutputStream = out; + mByteOrder = byteOrder; + } + + public void setByteOrder(ByteOrder byteOrder) { + mByteOrder = byteOrder; + } + + public void write(byte[] bytes) throws IOException { + mOutputStream.write(bytes); + } + + public void write(byte[] bytes, int offset, int length) throws IOException { + mOutputStream.write(bytes, offset, length); + } + + public void writeByte(int val) throws IOException { + mOutputStream.write(val); + } + + public void writeShort(short val) throws IOException { + if (mByteOrder == ByteOrder.LITTLE_ENDIAN) { + mOutputStream.write((val >>> 0) & 0xFF); + mOutputStream.write((val >>> 8) & 0xFF); + } else if (mByteOrder == ByteOrder.BIG_ENDIAN) { + mOutputStream.write((val >>> 8) & 0xFF); + mOutputStream.write((val >>> 0) & 0xFF); + } + } + + public void writeInt(int val) throws IOException { + if (mByteOrder == ByteOrder.LITTLE_ENDIAN) { + mOutputStream.write((val >>> 0) & 0xFF); + mOutputStream.write((val >>> 8) & 0xFF); + mOutputStream.write((val >>> 16) & 0xFF); + mOutputStream.write((val >>> 24) & 0xFF); + } else if (mByteOrder == ByteOrder.BIG_ENDIAN) { + mOutputStream.write((val >>> 24) & 0xFF); + mOutputStream.write((val >>> 16) & 0xFF); + mOutputStream.write((val >>> 8) & 0xFF); + mOutputStream.write((val >>> 0) & 0xFF); + } + } + + public void writeUnsignedShort(int val) throws IOException { + writeShort((short) val); + } + + public void writeUnsignedInt(long val) throws IOException { + writeInt((int) val); + } + } } diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 79557bc..241df15 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -42,13 +42,9 @@ LOCAL_SHARED_LIBRARIES := \ libcamera_client \ libmtp \ libusbhost \ - libjhead \ libexif \ libstagefright_amrnb_common -LOCAL_REQUIRED_MODULES := \ - libjhead_jni - LOCAL_STATIC_LIBRARIES := \ libstagefright_amrnbenc -- cgit v1.1