Java tutorial
/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import android.annotation.NonNull; import android.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.util.Log; import android.util.Pair; import android.annotation.IntDef; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.DataInput; 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.nio.charset.StandardCharsets; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.LinkedList; 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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import libcore.io.IoUtils; import libcore.io.Streams; /** * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file. * <p> * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF. * <p> * Attribute mutation is supported for JPEG image files. */ public class ExifInterface { private static final String TAG = "ExifInterface"; private static final boolean DEBUG = false; // The Exif tag names. See Tiff 6.0 Section 3 and Section 8. /** Type is String. */ public static final String TAG_ARTIST = "Artist"; /** Type is int. */ public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample"; /** Type is int. */ public static final String TAG_COMPRESSION = "Compression"; /** Type is String. */ 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_IMAGE_DESCRIPTION = "ImageDescription"; /** Type is int. */ 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_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat"; /** Type is int. */ public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength"; /** Type is String. */ public static final String TAG_MAKE = "Make"; /** Type is String. */ public static final String TAG_MODEL = "Model"; /** Type is int. */ public static final String TAG_ORIENTATION = "Orientation"; /** Type is int. */ public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"; /** Type is int. */ public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration"; /** Type is rational. */ public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities"; /** Type is rational. */ public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite"; /** Type is int. */ public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit"; /** Type is int. */ public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip"; /** Type is int. */ public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel"; /** Type is String. */ public static final String TAG_SOFTWARE = "Software"; /** Type is int. */ public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts"; /** Type is int. */ public static final String TAG_STRIP_OFFSETS = "StripOffsets"; /** Type is int. */ public static final String TAG_TRANSFER_FUNCTION = "TransferFunction"; /** Type is rational. */ public static final String TAG_WHITE_POINT = "WhitePoint"; /** Type is rational. */ public static final String TAG_X_RESOLUTION = "XResolution"; /** Type is rational. */ public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients"; /** Type is int. */ public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning"; /** Type is int. */ public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling"; /** Type is rational. */ public static final String TAG_Y_RESOLUTION = "YResolution"; /** Type is rational. */ public static final String TAG_APERTURE_VALUE = "ApertureValue"; /** Type is rational. */ public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue"; /** Type is String. */ public static final String TAG_CFA_PATTERN = "CFAPattern"; /** Type is int. */ public static final String TAG_COLOR_SPACE = "ColorSpace"; /** Type is String. */ public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration"; /** Type is rational. */ public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel"; /** Type is int. */ public static final String TAG_CONTRAST = "Contrast"; /** Type is int. */ public static final String TAG_CUSTOM_RENDERED = "CustomRendered"; /** Type is String. */ public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized"; /** Type is String. */ public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal"; /** Type is String. */ public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription"; /** Type is double. */ public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio"; /** Type is String. */ public static final String TAG_EXIF_VERSION = "ExifVersion"; /** Type is double. */ public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue"; /** Type is rational. */ public static final String TAG_EXPOSURE_INDEX = "ExposureIndex"; /** Type is int. */ public static final String TAG_EXPOSURE_MODE = "ExposureMode"; /** Type is int. */ public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram"; /** Type is double. */ public static final String TAG_EXPOSURE_TIME = "ExposureTime"; /** Type is double. */ public static final String TAG_F_NUMBER = "FNumber"; /** * Type is double. * * @deprecated use {@link #TAG_F_NUMBER} instead */ @Deprecated public static final String TAG_APERTURE = "FNumber"; /** Type is String. */ public static final String TAG_FILE_SOURCE = "FileSource"; /** Type is int. */ public static final String TAG_FLASH = "Flash"; /** Type is rational. */ public static final String TAG_FLASH_ENERGY = "FlashEnergy"; /** Type is String. */ public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion"; /** Type is rational. */ public static final String TAG_FOCAL_LENGTH = "FocalLength"; /** Type is int. */ public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm"; /** Type is int. */ public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit"; /** Type is rational. */ public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution"; /** Type is rational. */ public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution"; /** Type is int. */ public static final String TAG_GAIN_CONTROL = "GainControl"; /** Type is int. */ public static final String TAG_ISO_SPEED_RATINGS = "ISOSpeedRatings"; /** * Type is int. * * @deprecated use {@link #TAG_ISO_SPEED_RATINGS} instead */ @Deprecated public static final String TAG_ISO = "ISOSpeedRatings"; /** Type is String. */ public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID"; /** Type is int. */ public static final String TAG_LIGHT_SOURCE = "LightSource"; /** Type is String. */ public static final String TAG_MAKER_NOTE = "MakerNote"; /** Type is rational. */ public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue"; /** Type is int. */ public static final String TAG_METERING_MODE = "MeteringMode"; /** Type is int. */ public static final String TAG_NEW_SUBFILE_TYPE = "NewSubfileType"; /** Type is String. */ public static final String TAG_OECF = "OECF"; /** Type is int. */ public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension"; /** Type is int. */ public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension"; /** Type is String. */ public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile"; /** Type is int. */ public static final String TAG_SATURATION = "Saturation"; /** Type is int. */ public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType"; /** Type is String. */ public static final String TAG_SCENE_TYPE = "SceneType"; /** Type is int. */ public static final String TAG_SENSING_METHOD = "SensingMethod"; /** Type is int. */ public static final String TAG_SHARPNESS = "Sharpness"; /** Type is rational. */ public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue"; /** Type is String. */ public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse"; /** Type is String. */ public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity"; /** Type is int. */ public static final String TAG_SUBFILE_TYPE = "SubfileType"; /** Type is String. */ public static final String TAG_SUBSEC_TIME = "SubSecTime"; /** * Type is String. * * @deprecated use {@link #TAG_SUBSEC_TIME_DIGITIZED} instead */ public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized"; /** Type is String. */ public static final String TAG_SUBSEC_TIME_DIGITIZED = "SubSecTimeDigitized"; /** * Type is String. * * @deprecated use {@link #TAG_SUBSEC_TIME_ORIGINAL} instead */ public static final String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal"; /** Type is String. */ public static final String TAG_SUBSEC_TIME_ORIGINAL = "SubSecTimeOriginal"; /** Type is int. */ public static final String TAG_SUBJECT_AREA = "SubjectArea"; /** Type is double. */ public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance"; /** Type is int. */ public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange"; /** Type is int. */ public static final String TAG_SUBJECT_LOCATION = "SubjectLocation"; /** Type is String. */ public static final String TAG_USER_COMMENT = "UserComment"; /** Type is int. */ 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_AREA_INFORMATION = "GPSAreaInformation"; /** Type is rational. */ public static final String TAG_GPS_DOP = "GPSDOP"; /** Type is String. */ public static final String TAG_GPS_DATESTAMP = "GPSDateStamp"; /** Type is rational. */ public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing"; /** Type is String. */ public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef"; /** Type is rational. */ public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance"; /** Type is String. */ public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef"; /** Type is rational. */ public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude"; /** Type is String. */ public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef"; /** Type is rational. */ public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude"; /** Type is String. */ public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef"; /** Type is int. */ public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential"; /** Type is rational. */ public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection"; /** Type is String. */ 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. */ public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum"; /** Type is String. */ 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. */ public static final String TAG_GPS_SATELLITES = "GPSSatellites"; /** Type is rational. */ public static final String TAG_GPS_SPEED = "GPSSpeed"; /** Type is String. */ public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef"; /** Type is String. */ 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. */ public static final String TAG_GPS_TRACK = "GPSTrack"; /** Type is String. */ public static final String TAG_GPS_TRACK_REF = "GPSTrackRef"; /** Type is String. */ public static final String TAG_GPS_VERSION_ID = "GPSVersionID"; /** Type is String. */ public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex"; /** Type is int. */ public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength"; /** Type is int. */ public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth"; /** Type is int. DNG Specification 1.4.0.0. Section 4 */ public static final String TAG_DNG_VERSION = "DNGVersion"; /** Type is int. DNG Specification 1.4.0.0. Section 4 */ public static final String TAG_DEFAULT_CROP_SIZE = "DefaultCropSize"; /** Type is undefined. See Olympus MakerNote tags in http://www.exiv2.org/tags-olympus.html. */ public static final String TAG_ORF_THUMBNAIL_IMAGE = "ThumbnailImage"; /** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */ public static final String TAG_ORF_PREVIEW_IMAGE_START = "PreviewImageStart"; /** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */ public static final String TAG_ORF_PREVIEW_IMAGE_LENGTH = "PreviewImageLength"; /** Type is int. See Olympus Image Processing tags in http://www.exiv2.org/tags-olympus.html. */ public static final String TAG_ORF_ASPECT_FRAME = "AspectFrame"; /** * Type is int. See PanasonicRaw tags in * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html */ public static final String TAG_RW2_SENSOR_BOTTOM_BORDER = "SensorBottomBorder"; /** * Type is int. See PanasonicRaw tags in * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html */ public static final String TAG_RW2_SENSOR_LEFT_BORDER = "SensorLeftBorder"; /** * Type is int. See PanasonicRaw tags in * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html */ public static final String TAG_RW2_SENSOR_RIGHT_BORDER = "SensorRightBorder"; /** * Type is int. See PanasonicRaw tags in * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html */ public static final String TAG_RW2_SENSOR_TOP_BORDER = "SensorTopBorder"; /** * Type is int. See PanasonicRaw tags in * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html */ public static final String TAG_RW2_ISO = "ISO"; /** * Type is undefined. See PanasonicRaw tags in * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html */ public static final String TAG_RW2_JPG_FROM_RAW = "JpgFromRaw"; /** * Private tags used for pointing the other IFD offsets. * The types of the following tags are int. * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD. * For SubIFD, see Note 1 of Adobe PageMaker 6.0 TIFF Technical Notes. */ 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 static final String TAG_SUB_IFD_POINTER = "SubIFDPointer"; // Proprietary pointer tags used for ORF files. // See http://www.exiv2.org/tags-olympus.html private static final String TAG_ORF_CAMERA_SETTINGS_IFD_POINTER = "CameraSettingsIFDPointer"; private static final String TAG_ORF_IMAGE_PROCESSING_IFD_POINTER = "ImageProcessingIFDPointer"; // 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"; private static final int MAX_THUMBNAIL_SIZE = 512; // Constants used for the Orientation Exif tag. public static final int ORIENTATION_UNDEFINED = 0; public static final int ORIENTATION_NORMAL = 1; 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 // 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 // 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; // Maximum size for checking file type signature (see image_type_recognition_lite.cc) private static final int SIGNATURE_CHECK_SIZE = 5000; private static final byte[] JPEG_SIGNATURE = new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff }; private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW"; private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84; private static final int RAF_INFO_SIZE = 160; private static final int RAF_JPEG_LENGTH_VALUE_SIZE = 4; private static final byte[] HEIF_TYPE_FTYP = new byte[] { 'f', 't', 'y', 'p' }; private static final byte[] HEIF_BRAND_MIF1 = new byte[] { 'm', 'i', 'f', '1' }; private static final byte[] HEIF_BRAND_HEIC = new byte[] { 'h', 'e', 'i', 'c' }; // See http://fileformats.archiveteam.org/wiki/Olympus_ORF private static final short ORF_SIGNATURE_1 = 0x4f52; private static final short ORF_SIGNATURE_2 = 0x5352; // There are two formats for Olympus Makernote Headers. Each has different identifiers and // offsets to the actual data. // See http://www.exiv2.org/makernote.html#R1 private static final byte[] ORF_MAKER_NOTE_HEADER_1 = new byte[] { (byte) 0x4f, (byte) 0x4c, (byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x00 }; // "OLYMP\0" private static final byte[] ORF_MAKER_NOTE_HEADER_2 = new byte[] { (byte) 0x4f, (byte) 0x4c, (byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x55, (byte) 0x53, (byte) 0x00, (byte) 0x49, (byte) 0x49 }; // "OLYMPUS\0II" private static final int ORF_MAKER_NOTE_HEADER_1_SIZE = 8; private static final int ORF_MAKER_NOTE_HEADER_2_SIZE = 12; // See http://fileformats.archiveteam.org/wiki/RW2 private static final short RW2_SIGNATURE = 0x0055; // See http://fileformats.archiveteam.org/wiki/Pentax_PEF private static final String PEF_SIGNATURE = "PENTAX"; // See http://www.exiv2.org/makernote.html#R11 private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 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-3451C Section 4.5.2) private static final short BYTE_ALIGN_II = 0x4949; // II: Intel order private static final short BYTE_ALIGN_MM = 0x4d4d; // MM: Motorola order // TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2) private static final byte START_CODE = 0x2a; // 42 private static final int IFD_OFFSET = 8; // Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".) private static final int IFD_FORMAT_BYTE = 1; private static final int IFD_FORMAT_STRING = 2; private static final int IFD_FORMAT_USHORT = 3; private static final int IFD_FORMAT_ULONG = 4; private static final int IFD_FORMAT_URATIONAL = 5; private static final int IFD_FORMAT_SBYTE = 6; private static final int IFD_FORMAT_UNDEFINED = 7; private static final int IFD_FORMAT_SSHORT = 8; private static final int IFD_FORMAT_SLONG = 9; private static final int IFD_FORMAT_SRATIONAL = 10; private static final int IFD_FORMAT_SINGLE = 11; private static final int IFD_FORMAT_DOUBLE = 12; // Format indicating a new IFD entry (See Adobe PageMaker 6.0 TIFF Technical Notes, "New Tag") private static final int IFD_FORMAT_IFD = 13; // 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, 1 }; private static final byte[] EXIF_ASCII_PREFIX = new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0 }; /** * Constants used for Compression tag. * For Value 1, 2, 32773, see TIFF 6.0 Spec Section 3: Bilevel Images, Compression * For Value 6, see TIFF 6.0 Spec Section 22: JPEG Compression, Extensions to Existing Fields * For Value 7, 8, 34892, see DNG Specification 1.4.0.0. Section 3, Compression */ private static final int DATA_UNCOMPRESSED = 1; private static final int DATA_HUFFMAN_COMPRESSED = 2; private static final int DATA_JPEG = 6; private static final int DATA_JPEG_COMPRESSED = 7; private static final int DATA_DEFLATE_ZIP = 8; private static final int DATA_PACK_BITS_COMPRESSED = 32773; private static final int DATA_LOSSY_JPEG = 34892; /** * Constants used for BitsPerSample tag. * For RGB, see TIFF 6.0 Spec Section 6, Differences from Palette Color Images * For Greyscale, see TIFF 6.0 Spec Section 4, Differences from Bilevel Images */ private static final int[] BITS_PER_SAMPLE_RGB = new int[] { 8, 8, 8 }; private static final int[] BITS_PER_SAMPLE_GREYSCALE_1 = new int[] { 4 }; private static final int[] BITS_PER_SAMPLE_GREYSCALE_2 = new int[] { 8 }; /** * Constants used for PhotometricInterpretation tag. * For White/Black, see Section 3, Color. * See TIFF 6.0 Spec Section 22, Minimum Requirements for TIFF with JPEG Compression. */ private static final int PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO = 0; private static final int PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1; private static final int PHOTOMETRIC_INTERPRETATION_RGB = 2; private static final int PHOTOMETRIC_INTERPRETATION_YCBCR = 6; /** * Constants used for NewSubfileType tag. * See TIFF 6.0 Spec Section 8 * */ private static final int ORIGINAL_RESOLUTION_IMAGE = 0; private static final int REDUCED_RESOLUTION_IMAGE = 1; // 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 { ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(bytes); inputStream.setByteOrder(byteOrder); switch (format) { case IFD_FORMAT_BYTE: case IFD_FORMAT_SBYTE: { // Exception for GPSAltitudeRef tag if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) { return new String(new char[] { (char) (bytes[0] + '0') }); } return new String(bytes, ASCII); } case IFD_FORMAT_UNDEFINED: case IFD_FORMAT_STRING: { int index = 0; if (numberOfComponents >= EXIF_ASCII_PREFIX.length) { boolean same = true; for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) { if (bytes[i] != EXIF_ASCII_PREFIX[i]) { same = false; break; } } if (same) { index = EXIF_ASCII_PREFIX.length; } } StringBuilder stringBuilder = new StringBuilder(); while (index < numberOfComponents) { int ch = bytes[index]; if (ch == 0) { break; } if (ch >= 32) { stringBuilder.append((char) ch); } else { stringBuilder.append('?'); } ++index; } return stringBuilder.toString(); } case IFD_FORMAT_USHORT: { final int[] values = new int[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { values[i] = inputStream.readUnsignedShort(); } return values; } case IFD_FORMAT_ULONG: { final long[] values = new long[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { values[i] = inputStream.readUnsignedInt(); } return values; } case IFD_FORMAT_URATIONAL: { final Rational[] values = new Rational[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { final long numerator = inputStream.readUnsignedInt(); final long denominator = inputStream.readUnsignedInt(); values[i] = new Rational(numerator, denominator); } return values; } case IFD_FORMAT_SSHORT: { final int[] values = new int[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { values[i] = inputStream.readShort(); } return values; } case IFD_FORMAT_SLONG: { final int[] values = new int[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { values[i] = inputStream.readInt(); } return values; } case IFD_FORMAT_SRATIONAL: { final Rational[] values = new Rational[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { final long numerator = inputStream.readInt(); final long denominator = inputStream.readInt(); values[i] = new Rational(numerator, denominator); } return values; } case IFD_FORMAT_SINGLE: { final double[] values = new double[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { values[i] = inputStream.readFloat(); } return values; } case IFD_FORMAT_DOUBLE: { final double[] values = new double[numberOfComponents]; for (int i = 0; i < numberOfComponents; ++i) { values[i] = inputStream.readDouble(); } return values; } default: return null; } } catch (IOException e) { Log.w(TAG, "IOException occurred during reading a value", e); return null; } } 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-3451C Section 4.6.8 Tag Support Levels) private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] { // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images. new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG), new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG), new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT), new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT), new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT), new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING), new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING), new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING), new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT), new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT), new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL), new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL), new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT), new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT), new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT), new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING), new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING), new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING), new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL), new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL), // See Adobe PageMaker 6.0 TIFF Technical Notes, Note 1. new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG), new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG), new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG), new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL), new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), // RW2 file tags // See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html) new ExifTag(TAG_RW2_SENSOR_TOP_BORDER, 4, IFD_FORMAT_ULONG), new ExifTag(TAG_RW2_SENSOR_LEFT_BORDER, 5, IFD_FORMAT_ULONG), new ExifTag(TAG_RW2_SENSOR_BOTTOM_BORDER, 6, IFD_FORMAT_ULONG), new ExifTag(TAG_RW2_SENSOR_RIGHT_BORDER, 7, IFD_FORMAT_ULONG), new ExifTag(TAG_RW2_ISO, 23, IFD_FORMAT_USHORT), new ExifTag(TAG_RW2_JPG_FROM_RAW, 46, IFD_FORMAT_UNDEFINED) }; // Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels) private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] { new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL), new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL), new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT), new ExifTag(TAG_SPECTRAL_SENSITIVITY, 34852, IFD_FORMAT_STRING), new ExifTag(TAG_ISO_SPEED_RATINGS, 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), new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE), new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG) }; // Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels) private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] { new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE), new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING), 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-3451C Section 4.6.8 Tag Support Levels) private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] { new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING) }; // IFD Thumbnail tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels) private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] { // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images. new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG), new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG), new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT), new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT), new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT), new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING), new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING), new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING), new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT), new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT), new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG), new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL), new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL), new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT), new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT), new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT), new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING), new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING), new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING), new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL), new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL), // See Adobe PageMaker 6.0 TIFF Technical Notes, Note 1. new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG), new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG), new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG), new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL), new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE), new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG) }; // RAF file tag (See piex.cc line 372) private static final ExifTag TAG_RAF_IMAGE_SIZE = new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT); // ORF file tags (See http://www.exiv2.org/tags-olympus.html) private static final ExifTag[] ORF_MAKER_NOTE_TAGS = new ExifTag[] { new ExifTag(TAG_ORF_THUMBNAIL_IMAGE, 256, IFD_FORMAT_UNDEFINED), new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_ULONG), new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_ULONG) }; private static final ExifTag[] ORF_CAMERA_SETTINGS_TAGS = new ExifTag[] { new ExifTag(TAG_ORF_PREVIEW_IMAGE_START, 257, IFD_FORMAT_ULONG), new ExifTag(TAG_ORF_PREVIEW_IMAGE_LENGTH, 258, IFD_FORMAT_ULONG) }; private static final ExifTag[] ORF_IMAGE_PROCESSING_TAGS = new ExifTag[] { new ExifTag(TAG_ORF_ASPECT_FRAME, 4371, IFD_FORMAT_USHORT) }; // PEF file tag (See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Pentax.html) private static final ExifTag[] PEF_TAGS = new ExifTag[] { new ExifTag(TAG_COLOR_SPACE, 55, IFD_FORMAT_USHORT) }; // See JEITA CP-3451C Section 4.6.3: Exif-specific IFD. // The following values are used for indicating pointers to the other Image File Directories. // Indices of Exif Ifd tag groups /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({ IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY, IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE, IFD_TYPE_ORF_CAMERA_SETTINGS, IFD_TYPE_ORF_IMAGE_PROCESSING, IFD_TYPE_PEF }) public @interface IfdType { } private static final int IFD_TYPE_PRIMARY = 0; private static final int IFD_TYPE_EXIF = 1; private static final int IFD_TYPE_GPS = 2; private static final int IFD_TYPE_INTEROPERABILITY = 3; private static final int IFD_TYPE_THUMBNAIL = 4; private static final int IFD_TYPE_PREVIEW = 5; private static final int IFD_TYPE_ORF_MAKER_NOTE = 6; private static final int IFD_TYPE_ORF_CAMERA_SETTINGS = 7; private static final int IFD_TYPE_ORF_IMAGE_PROCESSING = 8; private static final int IFD_TYPE_PEF = 9; // List of Exif tag groups private static final ExifTag[][] EXIF_TAGS = new ExifTag[][] { IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS, IFD_THUMBNAIL_TAGS, IFD_TIFF_TAGS, ORF_MAKER_NOTE_TAGS, ORF_CAMERA_SETTINGS_TAGS, ORF_IMAGE_PROCESSING_TAGS, PEF_TAGS }; // List of tags for pointing to the other image file directory offset. private static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[] { new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG), new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG), new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_BYTE), new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_BYTE) }; // 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<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE, TAG_GPS_TIMESTAMP)); // Mappings from tag number to IFD type for pointer tags. private static final HashMap<Integer, Integer> sExifPointerTagMap = new HashMap(); // See JPEG File Interchange Format Version 1.02. // The following values are defined for handling JPEG streams. In this implementation, we are // not only getting information from EXIF but also from some JPEG special segments such as // MARKER_COM for user comment and MARKER_SOFx for image width and height. 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; // Supported Image File Types private static final int IMAGE_TYPE_UNKNOWN = 0; private static final int IMAGE_TYPE_ARW = 1; private static final int IMAGE_TYPE_CR2 = 2; private static final int IMAGE_TYPE_DNG = 3; private static final int IMAGE_TYPE_JPEG = 4; private static final int IMAGE_TYPE_NEF = 5; private static final int IMAGE_TYPE_NRW = 6; private static final int IMAGE_TYPE_ORF = 7; private static final int IMAGE_TYPE_PEF = 8; private static final int IMAGE_TYPE_RAF = 9; private static final int IMAGE_TYPE_RW2 = 10; private static final int IMAGE_TYPE_SRW = 11; private static final int IMAGE_TYPE_HEIF = 12; static { 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 ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) { sExifTagMapsForReading[ifdType] = new HashMap(); sExifTagMapsForWriting[ifdType] = new HashMap(); for (ExifTag tag : EXIF_TAGS[ifdType]) { sExifTagMapsForReading[ifdType].put(tag.number, tag); sExifTagMapsForWriting[ifdType].put(tag.name, tag); } } // Build up the hash table to look up Exif pointer tags. sExifPointerTagMap.put(EXIF_POINTER_TAGS[0].number, IFD_TYPE_PREVIEW); // 330 sExifPointerTagMap.put(EXIF_POINTER_TAGS[1].number, IFD_TYPE_EXIF); // 34665 sExifPointerTagMap.put(EXIF_POINTER_TAGS[2].number, IFD_TYPE_GPS); // 34853 sExifPointerTagMap.put(EXIF_POINTER_TAGS[3].number, IFD_TYPE_INTEROPERABILITY); // 40965 sExifPointerTagMap.put(EXIF_POINTER_TAGS[4].number, IFD_TYPE_ORF_CAMERA_SETTINGS); // 8224 sExifPointerTagMap.put(EXIF_POINTER_TAGS[5].number, IFD_TYPE_ORF_IMAGE_PROCESSING); // 8256 } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final String mFilename; private final FileDescriptor mSeekableFileDescriptor; private final AssetManager.AssetInputStream mAssetInputStream; private final boolean mIsInputStream; private int mMimeType; @UnsupportedAppUsage private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length]; private Set<Integer> mAttributesOffsets = new HashSet<>(EXIF_TAGS.length); private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private boolean mHasThumbnail; // The following values used for indicating a thumbnail position. private int mThumbnailOffset; private int mThumbnailLength; private byte[] mThumbnailBytes; private int mThumbnailCompression; private int mExifOffset; private int mOrfMakerNoteOffset; private int mOrfThumbnailOffset; private int mOrfThumbnailLength; private int mRw2JpgFromRawOffset; private boolean mIsSupportedFile; // 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 image file. */ public ExifInterface(String filename) throws IOException { if (filename == null) { throw new IllegalArgumentException("filename cannot be null"); } FileInputStream in = null; mAssetInputStream = null; mFilename = filename; mIsInputStream = false; try { in = new FileInputStream(filename); if (isSeekableFD(in.getFD())) { mSeekableFileDescriptor = in.getFD(); } else { mSeekableFileDescriptor = null; } loadAttributes(in); } finally { IoUtils.closeQuietly(in); } } /** * Reads Exif tags from the specified image file descriptor. Attribute mutation is supported * for writable and seekable file descriptors only. This constructor will not rewind the offset * of the given file descriptor. Developers should close the file descriptor after use. */ public ExifInterface(FileDescriptor fileDescriptor) throws IOException { if (fileDescriptor == null) { throw new IllegalArgumentException("fileDescriptor cannot be null"); } mAssetInputStream = null; mFilename = null; if (isSeekableFD(fileDescriptor)) { mSeekableFileDescriptor = fileDescriptor; // Keep the original file descriptor in order to save attributes when it's seekable. // Otherwise, just close the given file descriptor after reading it because the save // feature won't be working. try { fileDescriptor = Os.dup(fileDescriptor); } catch (ErrnoException e) { throw e.rethrowAsIOException(); } } else { mSeekableFileDescriptor = null; } mIsInputStream = false; FileInputStream in = null; try { in = new FileInputStream(fileDescriptor); loadAttributes(in); } finally { IoUtils.closeQuietly(in); } } /** * Reads Exif tags from the specified image input stream. Attribute mutation is not supported * for input streams. The given input stream will proceed its current position. Developers * should close the input stream after use. */ public ExifInterface(InputStream inputStream) throws IOException { if (inputStream == null) { throw new IllegalArgumentException("inputStream cannot be null"); } mFilename = null; if (inputStream instanceof AssetManager.AssetInputStream) { mAssetInputStream = (AssetManager.AssetInputStream) inputStream; mSeekableFileDescriptor = null; } else if (inputStream instanceof FileInputStream && isSeekableFD(((FileInputStream) inputStream).getFD())) { mAssetInputStream = null; mSeekableFileDescriptor = ((FileInputStream) inputStream).getFD(); } else { mAssetInputStream = null; mSeekableFileDescriptor = null; } mIsInputStream = true; loadAttributes(inputStream); } /** * 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 image file. * * @param tag the name of the tag. */ public String getAttribute(String 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 image file or the value cannot be parsed as integer, return * <var>defaultValue</var>. * * @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) { ExifAttribute exifAttribute = getExifAttribute(tag); if (exifAttribute == null) { return defaultValue; } try { return exifAttribute.getIntValue(mExifByteOrder); } catch (NumberFormatException e) { 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 <var>defaultValue</var>. * * @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) { ExifAttribute exifAttribute = getExifAttribute(tag); if (exifAttribute == null) { return defaultValue; } try { return exifAttribute.getDoubleValue(mExifByteOrder); } catch (NumberFormatException e) { return defaultValue; } } /** * Set the value of the specified tag. * * @param tag the name of the tag. * @param value the value of the tag. */ public void setAttribute(String tag, String value) { // 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 { try { double doubleValue = Double.parseDouble(value); value = (long) (doubleValue * 10000L) + "/10000"; } catch (NumberFormatException e) { Log.w(TAG, "Invalid value for " + tag + " : " + value); return; } } } for (int i = 0; i < EXIF_TAGS.length; ++i) { if (i == IFD_TYPE_THUMBNAIL && !mHasThumbnail) { continue; } final Object obj = sExifTagMapsForWriting[i].get(tag); if (obj != null) { if (value == null) { mAttributes[i].remove(tag); continue; } final ExifTag exifTag = (ExifTag) obj; Pair<Integer, Integer> 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) Double.parseDouble(numbers[0]), (long) Double.parseDouble(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) Double.parseDouble(numbers[0]), (long) Double.parseDouble(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; } } } } /** * 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. */ 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; } /** * Remove any values of the specified tag. * * @param tag the name of the tag. */ private void removeAttribute(String tag) { for (int i = 0; i < EXIF_TAGS.length; ++i) { mAttributes[i].remove(tag); } } /** * 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(@NonNull InputStream in) throws IOException { try { // Initialize mAttributes. for (int i = 0; i < EXIF_TAGS.length; ++i) { mAttributes[i] = new HashMap(); } // Check file type in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE); mMimeType = getMimeType((BufferedInputStream) in); // Create byte-ordered input stream ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in); switch (mMimeType) { case IMAGE_TYPE_JPEG: { getJpegAttributes(inputStream, 0, IFD_TYPE_PRIMARY); // 0 is offset break; } case IMAGE_TYPE_RAF: { getRafAttributes(inputStream); break; } case IMAGE_TYPE_HEIF: { getHeifAttributes(inputStream); break; } case IMAGE_TYPE_ORF: { getOrfAttributes(inputStream); break; } case IMAGE_TYPE_RW2: { getRw2Attributes(inputStream); break; } case IMAGE_TYPE_ARW: case IMAGE_TYPE_CR2: case IMAGE_TYPE_DNG: case IMAGE_TYPE_NEF: case IMAGE_TYPE_NRW: case IMAGE_TYPE_PEF: case IMAGE_TYPE_SRW: case IMAGE_TYPE_UNKNOWN: { getRawAttributes(inputStream); break; } default: { break; } } // Set thumbnail image offset and length setThumbnailData(inputStream); mIsSupportedFile = true; } catch (IOException e) { // Ignore exceptions in order to keep the compatibility with the old versions of // ExifInterface. mIsSupportedFile = false; if (DEBUG) { Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file" + "(ExifInterface supports JPEG and some RAW image formats only) " + "or a corrupted JPEG file to ExifInterface.", e); } } finally { addDefaultValuesForCompatibility(); if (DEBUG) { printAttributes(); } } } private static boolean isSeekableFD(FileDescriptor fd) throws IOException { try { Os.lseek(fd, 0, OsConstants.SEEK_CUR); return true; } catch (ErrnoException e) { return false; } } // 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<Map.Entry>) 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. * <p> * This method is only supported for JPEG files. * </p> */ public void saveAttributes() throws IOException { if (!mIsSupportedFile || mMimeType != IMAGE_TYPE_JPEG) { throw new IOException("ExifInterface only supports saving attributes on JPEG formats."); } if (mIsInputStream || (mSeekableFileDescriptor == null && mFilename == null)) { throw new IOException("ExifInterface does not support saving attributes for the current input."); } // Keep the thumbnail in memory mThumbnailBytes = getThumbnail(); FileInputStream in = null; FileOutputStream out = null; File tempFile = null; try { // Move the original file to temporary file. if (mFilename != null) { tempFile = new File(mFilename + ".tmp"); File originalFile = new File(mFilename); if (!originalFile.renameTo(tempFile)) { throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath()); } } else if (mSeekableFileDescriptor != null) { tempFile = File.createTempFile("temp", "jpg"); Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET); in = new FileInputStream(mSeekableFileDescriptor); out = new FileOutputStream(tempFile); Streams.copy(in, out); } } catch (ErrnoException e) { throw e.rethrowAsIOException(); } finally { IoUtils.closeQuietly(in); IoUtils.closeQuietly(out); } in = null; out = null; try { // Save the new file. in = new FileInputStream(tempFile); if (mFilename != null) { out = new FileOutputStream(mFilename); } else if (mSeekableFileDescriptor != null) { Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET); out = new FileOutputStream(mSeekableFileDescriptor); } saveJpegAttributes(in, out); } catch (ErrnoException e) { throw e.rethrowAsIOException(); } 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 JPEG compressed thumbnail inside the image file, or {@code null} if there is no * JPEG compressed thumbnail. * The returned data can be decoded using * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)} */ public byte[] getThumbnail() { if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) { return getThumbnailBytes(); } return null; } /** * Returns the thumbnail bytes inside the image file, regardless of the compression type of the * thumbnail image. */ public byte[] getThumbnailBytes() { if (!mHasThumbnail) { return null; } if (mThumbnailBytes != null) { return mThumbnailBytes; } // Read the thumbnail. InputStream in = null; try { if (mAssetInputStream != null) { in = mAssetInputStream; if (in.markSupported()) { in.reset(); } else { Log.d(TAG, "Cannot read thumbnail from inputstream without mark/reset support"); return null; } } else if (mFilename != null) { in = new FileInputStream(mFilename); } else if (mSeekableFileDescriptor != null) { FileDescriptor fileDescriptor = Os.dup(mSeekableFileDescriptor); Os.lseek(fileDescriptor, 0, OsConstants.SEEK_SET); in = new FileInputStream(fileDescriptor); } if (in == null) { // Should not be reached this. throw new FileNotFoundException(); } if (in.skip(mThumbnailOffset) != mThumbnailOffset) { throw new IOException("Corrupted image"); } byte[] buffer = new byte[mThumbnailLength]; if (in.read(buffer) != mThumbnailLength) { throw new IOException("Corrupted image"); } mThumbnailBytes = buffer; return buffer; } catch (IOException | ErrnoException e) { // Couldn't get a thumbnail image. Log.d(TAG, "Encountered exception while getting thumbnail", e); } finally { IoUtils.closeQuietly(in); } return null; } /** * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the * thumbnail compression value, or {@code null} if the compression type is unsupported. */ public Bitmap getThumbnailBitmap() { if (!mHasThumbnail) { return null; } else if (mThumbnailBytes == null) { mThumbnailBytes = getThumbnailBytes(); } if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) { return BitmapFactory.decodeByteArray(mThumbnailBytes, 0, mThumbnailLength); } else if (mThumbnailCompression == DATA_UNCOMPRESSED) { int[] rgbValues = new int[mThumbnailBytes.length / 3]; byte alpha = (byte) 0xff000000; for (int i = 0; i < rgbValues.length; i++) { rgbValues[i] = alpha + (mThumbnailBytes[3 * i] << 16) + (mThumbnailBytes[3 * i + 1] << 8) + mThumbnailBytes[3 * i + 2]; } ExifAttribute imageLengthAttribute = (ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL] .get(TAG_IMAGE_LENGTH); ExifAttribute imageWidthAttribute = (ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL] .get(TAG_IMAGE_WIDTH); if (imageLengthAttribute != null && imageWidthAttribute != null) { int imageLength = imageLengthAttribute.getIntValue(mExifByteOrder); int imageWidth = imageWidthAttribute.getIntValue(mExifByteOrder); return Bitmap.createBitmap(rgbValues, imageWidth, imageLength, Bitmap.Config.ARGB_8888); } } return null; } /** * Returns true if thumbnail image is JPEG Compressed, or false if either thumbnail image does * not exist or thumbnail image is uncompressed. */ public boolean isThumbnailCompressed() { if (!mHasThumbnail) { return false; } if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) { return true; } return false; } /** * Returns the offset and length of thumbnail inside the image file, or * {@code null} if 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. */ public long[] getThumbnailRange() { if (!mHasThumbnail) { return null; } long[] range = new long[2]; range[0] = mThumbnailOffset; range[1] = mThumbnailLength; return range; } /** * Stores the latitude and longitude value in a float array. The first element is * the latitude, and the second element is the longitude. Returns false if the * Exif tags are not available. */ public boolean getLatLong(float output[]) { 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 { output[0] = convertRationalLatLonToFloat(latValue, latRef); output[1] = convertRationalLatLonToFloat(lngValue, lngRef); return true; } catch (IllegalArgumentException e) { // if values are not parseable } } return false; } /** * Return the altitude in meters. If the exif tag does not exist, return * <var>defaultValue</var>. * * @param defaultValue the value to return if the tag is not available. */ public double getAltitude(double defaultValue) { double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1); int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1); if (altitude >= 0 && ref >= 0) { return (altitude * ((ref == 1) ? -1 : 1)); } else { return defaultValue; } } /** * Returns number of milliseconds since Jan. 1, 1970, midnight local time. * Returns -1 if the date time information if not available. * @hide */ @UnsupportedAppUsage public long getDateTime() { String dateTimeString = getAttribute(TAG_DATETIME); if (dateTimeString == null || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1; ParsePosition pos = new ParsePosition(0); try { // The exif field is in local time. Parsing it as if it is UTC will yield time // since 1/1/1970 local time Date datetime = sFormatter.parse(dateTimeString, pos); if (datetime == null) return -1; long msecs = datetime.getTime(); String subSecs = getAttribute(TAG_SUBSEC_TIME); if (subSecs != null) { try { long sub = Long.parseLong(subSecs); while (sub > 1000) { sub /= 10; } msecs += sub; } catch (NumberFormatException e) { // Ignored } } return msecs; } catch (IllegalArgumentException e) { return -1; } } /** * Returns number of milliseconds since Jan. 1, 1970, midnight UTC. * Returns -1 if the date time information if not available. * @hide */ @UnsupportedAppUsage public long getGpsDateTime() { 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; ParsePosition pos = new ParsePosition(0); try { Date datetime = sFormatter.parse(dateTimeString, pos); if (datetime == null) return -1; return datetime.getTime(); } catch (IllegalArgumentException e) { return -1; } } /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public static float convertRationalLatLonToFloat(String rationalString, String ref) { try { String[] parts = rationalString.split(","); String[] pair; pair = parts[0].split("/"); double degrees = Double.parseDouble(pair[0].trim()) / Double.parseDouble(pair[1].trim()); pair = parts[1].split("/"); double minutes = Double.parseDouble(pair[0].trim()) / Double.parseDouble(pair[1].trim()); pair = parts[2].split("/"); double seconds = Double.parseDouble(pair[0].trim()) / Double.parseDouble(pair[1].trim()); double result = degrees + (minutes / 60.0) + (seconds / 3600.0); if ((ref.equals("S") || ref.equals("W"))) { return (float) -result; } return (float) result; } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { // Not valid throw new IllegalArgumentException(); } } // Checks the type of image file private int getMimeType(BufferedInputStream in) throws IOException { in.mark(SIGNATURE_CHECK_SIZE); byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE]; in.read(signatureCheckBytes); in.reset(); if (isJpegFormat(signatureCheckBytes)) { return IMAGE_TYPE_JPEG; } else if (isRafFormat(signatureCheckBytes)) { return IMAGE_TYPE_RAF; } else if (isHeifFormat(signatureCheckBytes)) { return IMAGE_TYPE_HEIF; } else if (isOrfFormat(signatureCheckBytes)) { return IMAGE_TYPE_ORF; } else if (isRw2Format(signatureCheckBytes)) { return IMAGE_TYPE_RW2; } // Certain file formats (PEF) are identified in readImageFileDirectory() return IMAGE_TYPE_UNKNOWN; } /** * This method looks at the first 3 bytes to determine if this file is a JPEG file. * See http://www.media.mit.edu/pia/Research/deepview/exif.html, "JPEG format and Marker" */ private static boolean isJpegFormat(byte[] signatureCheckBytes) throws IOException { for (int i = 0; i < JPEG_SIGNATURE.length; i++) { if (signatureCheckBytes[i] != JPEG_SIGNATURE[i]) { return false; } } return true; } /** * This method looks at the first 15 bytes to determine if this file is a RAF file. * There is no official specification for RAF files from Fuji, but there is an online archive of * image file specifications: * http://fileformats.archiveteam.org/wiki/Fujifilm_RAF */ private boolean isRafFormat(byte[] signatureCheckBytes) throws IOException { byte[] rafSignatureBytes = RAF_SIGNATURE.getBytes(); for (int i = 0; i < rafSignatureBytes.length; i++) { if (signatureCheckBytes[i] != rafSignatureBytes[i]) { return false; } } return true; } private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException { ByteOrderedDataInputStream signatureInputStream = null; try { signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes); signatureInputStream.setByteOrder(ByteOrder.BIG_ENDIAN); long chunkSize = signatureInputStream.readInt(); byte[] chunkType = new byte[4]; signatureInputStream.read(chunkType); if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) { return false; } long chunkDataOffset = 8; if (chunkSize == 1) { // This indicates that the next 8 bytes represent the chunk size, // and chunk data comes after that. chunkSize = signatureInputStream.readLong(); if (chunkSize < 16) { // The smallest valid chunk is 16 bytes long in this case. return false; } chunkDataOffset += 8; } // only sniff up to signatureCheckBytes.length if (chunkSize > signatureCheckBytes.length) { chunkSize = signatureCheckBytes.length; } long chunkDataSize = chunkSize - chunkDataOffset; // It should at least have major brand (4-byte) and minor version (4-byte). // The rest of the chunk (if any) is a list of (4-byte) compatible brands. if (chunkDataSize < 8) { return false; } byte[] brand = new byte[4]; boolean isMif1 = false; boolean isHeic = false; for (long i = 0; i < chunkDataSize / 4; ++i) { if (signatureInputStream.read(brand) != brand.length) { return false; } if (i == 1) { // Skip this index, it refers to the minorVersion, not a brand. continue; } if (Arrays.equals(brand, HEIF_BRAND_MIF1)) { isMif1 = true; } else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) { isHeic = true; } if (isMif1 && isHeic) { return true; } } } catch (Exception e) { if (DEBUG) { Log.d(TAG, "Exception parsing HEIF file type box.", e); } } finally { if (signatureInputStream != null) { signatureInputStream.close(); signatureInputStream = null; } } return false; } /** * ORF has a similar structure to TIFF but it contains a different signature at the TIFF Header. * This method looks at the 2 bytes following the Byte Order bytes to determine if this file is * an ORF file. * There is no official specification for ORF files from Olympus, but there is an online archive * of image file specifications: * http://fileformats.archiveteam.org/wiki/Olympus_ORF */ private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException { ByteOrderedDataInputStream signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes); // Read byte order mExifByteOrder = readByteOrder(signatureInputStream); // Set byte order signatureInputStream.setByteOrder(mExifByteOrder); short orfSignature = signatureInputStream.readShort(); if (orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2) { return true; } return false; } /** * RW2 is TIFF-based, but stores 0x55 signature byte instead of 0x42 at the header * See http://lclevy.free.fr/raw/ */ private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException { ByteOrderedDataInputStream signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes); // Read byte order mExifByteOrder = readByteOrder(signatureInputStream); // Set byte order signatureInputStream.setByteOrder(mExifByteOrder); short signatureByte = signatureInputStream.readShort(); if (signatureByte == RW2_SIGNATURE) { return true; } return false; } /** * Loads EXIF attributes from a JPEG input stream. * * @param in The input stream that starts with the JPEG data. * @param jpegOffset The offset value in input stream for JPEG data. * @param imageType The image type from which to retrieve metadata. Use IFD_TYPE_PRIMARY for * primary image, IFD_TYPE_PREVIEW for preview image, and * IFD_TYPE_THUMBNAIL for thumbnail image. * @throws IOException If the data contains invalid JPEG markers, offsets, or length values. */ private void getJpegAttributes(ByteOrderedDataInputStream in, int jpegOffset, int imageType) throws IOException { // See JPEG File Interchange Format Specification, "JFIF Specification" if (DEBUG) { Log.d(TAG, "getJpegAttributes starting with: " + in); } // JPEG uses Big Endian by default. See https://people.cs.umass.edu/~verts/cs32/endian.html in.setByteOrder(ByteOrder.BIG_ENDIAN); // Skip to JPEG data in.seek(jpegOffset); int bytesRead = jpegOffset; byte marker; if ((marker = in.readByte()) != MARKER) { throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff)); } ++bytesRead; if (in.readByte() != MARKER_SOI) { throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff)); } ++bytesRead; while (true) { marker = in.readByte(); if (marker != MARKER) { throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff)); } ++bytesRead; marker = in.readByte(); if (DEBUG) { Log.d(TAG, "Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff)); } ++bytesRead; // EOI indicates the end of an image and in case of SOS, JPEG image stream starts and // the image data will terminate right after. if (marker == MARKER_EOI || marker == MARKER_SOS) { break; } int length = in.readUnsignedShort() - 2; bytesRead += 2; if (DEBUG) { Log.d(TAG, "JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: " + (length + 2) + ")"); } if (length < 0) { throw new IOException("Invalid length"); } switch (marker) { case MARKER_APP1: { 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 (in.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 + ")"); } // Save offset values for createJpegThumbnailBitmap() function mExifOffset = bytesRead; byte[] bytes = new byte[length]; if (in.read(bytes) != length) { throw new IOException("Invalid exif"); } bytesRead += length; length = 0; readExifSegment(bytes, imageType); break; } case MARKER_COM: { byte[] bytes = new byte[length]; if (in.read(bytes) != length) { throw new IOException("Invalid exif"); } length = 0; if (getAttribute(TAG_USER_COMMENT) == null) { mAttributes[IFD_TYPE_EXIF].put(TAG_USER_COMMENT, ExifAttribute.createString(new String(bytes, ASCII))); } break; } case MARKER_SOF0: case MARKER_SOF1: case MARKER_SOF2: case MARKER_SOF3: case MARKER_SOF5: case MARKER_SOF6: case MARKER_SOF7: case MARKER_SOF9: case MARKER_SOF10: case MARKER_SOF11: case MARKER_SOF13: case MARKER_SOF14: case MARKER_SOF15: { if (in.skipBytes(1) != 1) { throw new IOException("Invalid SOFx"); } mAttributes[imageType].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong(in.readUnsignedShort(), mExifByteOrder)); mAttributes[imageType].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong(in.readUnsignedShort(), mExifByteOrder)); length -= 5; break; } default: { break; } } if (length < 0) { throw new IOException("Invalid length"); } if (in.skipBytes(length) != length) { throw new IOException("Invalid JPEG segment"); } bytesRead += length; } // Restore original byte order in.setByteOrder(mExifByteOrder); } private void getRawAttributes(ByteOrderedDataInputStream in) throws IOException { // Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1. parseTiffHeaders(in, in.available()); // Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6. readImageFileDirectory(in, IFD_TYPE_PRIMARY); // Update ImageLength/Width tags for all image data. updateImageSizeValues(in, IFD_TYPE_PRIMARY); updateImageSizeValues(in, IFD_TYPE_PREVIEW); updateImageSizeValues(in, IFD_TYPE_THUMBNAIL); // Check if each image data is in valid position. validateImages(in); if (mMimeType == IMAGE_TYPE_PEF) { // PEF files contain a MakerNote data, which contains the data for ColorSpace tag. // See http://lclevy.free.fr/raw/ and piex.cc PefGetPreviewData() ExifAttribute makerNoteAttribute = (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE); if (makerNoteAttribute != null) { // Create an ordered DataInputStream for MakerNote ByteOrderedDataInputStream makerNoteDataInputStream = new ByteOrderedDataInputStream( makerNoteAttribute.bytes); makerNoteDataInputStream.setByteOrder(mExifByteOrder); // Seek to MakerNote data makerNoteDataInputStream.seek(PEF_MAKER_NOTE_SKIP_SIZE); // Read IFD data from MakerNote readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_PEF); // Update ColorSpace tag ExifAttribute colorSpaceAttribute = (ExifAttribute) mAttributes[IFD_TYPE_PEF].get(TAG_COLOR_SPACE); if (colorSpaceAttribute != null) { mAttributes[IFD_TYPE_EXIF].put(TAG_COLOR_SPACE, colorSpaceAttribute); } } } } /** * RAF files contains a JPEG and a CFA data. * The JPEG contains two images, a preview and a thumbnail, while the CFA contains a RAW image. * This method looks at the first 160 bytes of a RAF file to retrieve the offset and length * values for the JPEG and CFA data. * Using that data, it parses the JPEG data to retrieve the preview and thumbnail image data, * then parses the CFA metadata to retrieve the primary image length/width values. * For data format details, see http://fileformats.archiveteam.org/wiki/Fujifilm_RAF */ private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException { // Retrieve offset & length values in.skipBytes(RAF_OFFSET_TO_JPEG_IMAGE_OFFSET); byte[] jpegOffsetBytes = new byte[4]; byte[] cfaHeaderOffsetBytes = new byte[4]; in.read(jpegOffsetBytes); // Skip JPEG length value since it is not needed in.skipBytes(RAF_JPEG_LENGTH_VALUE_SIZE); in.read(cfaHeaderOffsetBytes); int rafJpegOffset = ByteBuffer.wrap(jpegOffsetBytes).getInt(); int rafCfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt(); // Retrieve JPEG image metadata getJpegAttributes(in, rafJpegOffset, IFD_TYPE_PREVIEW); // Skip to CFA header offset. in.seek(rafCfaHeaderOffset); // Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists in.setByteOrder(ByteOrder.BIG_ENDIAN); int numberOfDirectoryEntry = in.readInt(); if (DEBUG) { Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry); } // CFA stores some metadata about the RAW image. Since CFA uses proprietary tags, can only // find and retrieve image size information tags, while skipping others. // See piex.cc RafGetDimension() for (int i = 0; i < numberOfDirectoryEntry; ++i) { int tagNumber = in.readUnsignedShort(); int numberOfBytes = in.readUnsignedShort(); if (tagNumber == TAG_RAF_IMAGE_SIZE.number) { int imageLength = in.readShort(); int imageWidth = in.readShort(); ExifAttribute imageLengthAttribute = ExifAttribute.createUShort(imageLength, mExifByteOrder); ExifAttribute imageWidthAttribute = ExifAttribute.createUShort(imageWidth, mExifByteOrder); mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, imageLengthAttribute); mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, imageWidthAttribute); if (DEBUG) { Log.d(TAG, "Updated to length: " + imageLength + ", width: " + imageWidth); } return; } in.skipBytes(numberOfBytes); } } private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { retriever.setDataSource(new MediaDataSource() { long mPosition; @Override public void close() throws IOException { } @Override public int readAt(long position, byte[] buffer, int offset, int size) throws IOException { if (size == 0) { return 0; } if (position < 0) { return -1; } try { if (mPosition != position) { // We don't allow seek to positions after the available bytes, // the input stream won't be able to seek back then. // However, if we hit an exception before (mPosition set to -1), // let it try the seek in hope it might recover. if (mPosition >= 0 && position >= mPosition + in.available()) { return -1; } in.seek(position); mPosition = position; } // If the read will cause us to go over the available bytes, // reduce the size so that we stay in the available range. // Otherwise the input stream may not be able to seek back. if (size > in.available()) { size = in.available(); } int bytesRead = in.read(buffer, offset, size); if (bytesRead >= 0) { mPosition += bytesRead; return bytesRead; } } catch (IOException e) { } mPosition = -1; // need to seek on next read return -1; } @Override public long getSize() throws IOException { return -1; } }); String exifOffsetStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET); String exifLengthStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH); String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE); String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); String width = null; String height = null; String rotation = null; final String METADATA_VALUE_YES = "yes"; // If the file has both image and video, prefer image info over video info. // App querying ExifInterface is most likely using the bitmap path which // picks the image first. if (METADATA_VALUE_YES.equals(hasImage)) { width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH); height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT); rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION); } else if (METADATA_VALUE_YES.equals(hasVideo)) { width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); } if (width != null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder)); } if (height != null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); } if (rotation != null) { int orientation = ExifInterface.ORIENTATION_NORMAL; // all rotation angles in CW switch (Integer.parseInt(rotation)) { case 90: orientation = ExifInterface.ORIENTATION_ROTATE_90; break; case 180: orientation = ExifInterface.ORIENTATION_ROTATE_180; break; case 270: orientation = ExifInterface.ORIENTATION_ROTATE_270; break; } mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, ExifAttribute.createUShort(orientation, mExifByteOrder)); } if (exifOffsetStr != null && exifLengthStr != null) { int offset = Integer.parseInt(exifOffsetStr); int length = Integer.parseInt(exifLengthStr); if (length <= 6) { throw new IOException("Invalid exif length"); } in.seek(offset); byte[] identifier = new byte[6]; if (in.read(identifier) != 6) { throw new IOException("Can't read identifier"); } offset += 6; length -= 6; if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { throw new IOException("Invalid identifier"); } byte[] bytes = new byte[length]; if (in.read(bytes) != length) { throw new IOException("Can't read exif"); } readExifSegment(bytes, IFD_TYPE_PRIMARY); } if (DEBUG) { Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); } } finally { retriever.release(); } } /** * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail * images. Both data takes the form of IFDs and can therefore be read with the * readImageFileDirectory() method. * This method reads all the necessary data and updates the primary/preview/thumbnail image * information according to the GetOlympusPreviewImage() method in piex.cc. * For data format details, see the following: * http://fileformats.archiveteam.org/wiki/Olympus_ORF * https://libopenraw.freedesktop.org/wiki/Olympus_ORF */ private void getOrfAttributes(ByteOrderedDataInputStream in) throws IOException { // Retrieve primary image data // Other Exif data will be located in the Makernote. getRawAttributes(in); // Additionally retrieve preview/thumbnail information from MakerNote tag, which contains // proprietary tags and therefore does not have offical documentation // See GetOlympusPreviewImage() in piex.cc & http://www.exiv2.org/tags-olympus.html ExifAttribute makerNoteAttribute = (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE); if (makerNoteAttribute != null) { // Create an ordered DataInputStream for MakerNote ByteOrderedDataInputStream makerNoteDataInputStream = new ByteOrderedDataInputStream( makerNoteAttribute.bytes); makerNoteDataInputStream.setByteOrder(mExifByteOrder); // There are two types of headers for Olympus MakerNotes // See http://www.exiv2.org/makernote.html#R1 byte[] makerNoteHeader1Bytes = new byte[ORF_MAKER_NOTE_HEADER_1.length]; makerNoteDataInputStream.readFully(makerNoteHeader1Bytes); makerNoteDataInputStream.seek(0); byte[] makerNoteHeader2Bytes = new byte[ORF_MAKER_NOTE_HEADER_2.length]; makerNoteDataInputStream.readFully(makerNoteHeader2Bytes); // Skip the corresponding amount of bytes for each header type if (Arrays.equals(makerNoteHeader1Bytes, ORF_MAKER_NOTE_HEADER_1)) { makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_1_SIZE); } else if (Arrays.equals(makerNoteHeader2Bytes, ORF_MAKER_NOTE_HEADER_2)) { makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_2_SIZE); } // Read IFD data from MakerNote readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_ORF_MAKER_NOTE); // Retrieve & update preview image offset & length values ExifAttribute imageLengthAttribute = (ExifAttribute) mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS] .get(TAG_ORF_PREVIEW_IMAGE_START); ExifAttribute bitsPerSampleAttribute = (ExifAttribute) mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS] .get(TAG_ORF_PREVIEW_IMAGE_LENGTH); if (imageLengthAttribute != null && bitsPerSampleAttribute != null) { mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT, imageLengthAttribute); mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, bitsPerSampleAttribute); } // TODO: Check this behavior in other ORF files // Retrieve primary image length & width values // See piex.cc GetOlympusPreviewImage() ExifAttribute aspectFrameAttribute = (ExifAttribute) mAttributes[IFD_TYPE_ORF_IMAGE_PROCESSING] .get(TAG_ORF_ASPECT_FRAME); if (aspectFrameAttribute != null) { int[] aspectFrameValues = new int[4]; aspectFrameValues = (int[]) aspectFrameAttribute.getValue(mExifByteOrder); if (aspectFrameValues[2] > aspectFrameValues[0] && aspectFrameValues[3] > aspectFrameValues[1]) { int primaryImageWidth = aspectFrameValues[2] - aspectFrameValues[0] + 1; int primaryImageLength = aspectFrameValues[3] - aspectFrameValues[1] + 1; // Swap width & length values if (primaryImageWidth < primaryImageLength) { primaryImageWidth += primaryImageLength; primaryImageLength = primaryImageWidth - primaryImageLength; primaryImageWidth -= primaryImageLength; } ExifAttribute primaryImageWidthAttribute = ExifAttribute.createUShort(primaryImageWidth, mExifByteOrder); ExifAttribute primaryImageLengthAttribute = ExifAttribute.createUShort(primaryImageLength, mExifByteOrder); mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, primaryImageWidthAttribute); mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, primaryImageLengthAttribute); } } } } // RW2 contains the primary image data in IFD0 and the preview and/or thumbnail image data in // the JpgFromRaw tag // See https://libopenraw.freedesktop.org/wiki/Panasonic_RAW/ and piex.cc Rw2GetPreviewData() private void getRw2Attributes(ByteOrderedDataInputStream in) throws IOException { // Retrieve primary image data getRawAttributes(in); // Retrieve preview and/or thumbnail image data ExifAttribute jpgFromRawAttribute = (ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_JPG_FROM_RAW); if (jpgFromRawAttribute != null) { getJpegAttributes(in, mRw2JpgFromRawOffset, IFD_TYPE_PREVIEW); } // Set ISO tag value if necessary ExifAttribute rw2IsoAttribute = (ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_ISO); ExifAttribute exifIsoAttribute = (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_ISO_SPEED_RATINGS); if (rw2IsoAttribute != null && exifIsoAttribute == null) { // Place this attribute only if it doesn't exist mAttributes[IFD_TYPE_EXIF].put(TAG_ISO_SPEED_RATINGS, rw2IsoAttribute); } } // Stores a new JPEG image with EXIF attributes into a given output stream. private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream) throws IOException { // See JPEG File Interchange Format Specification, "JFIF Specification" if (DEBUG) { Log.d(TAG, "saveJpegAttributes starting with (inputStream: " + inputStream + ", outputStream: " + outputStream + ")"); } DataInputStream dataInputStream = new DataInputStream(inputStream); ByteOrderedDataOutputStream dataOutputStream = new ByteOrderedDataOutputStream(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.skipBytes(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 imageType) throws IOException { ByteOrderedDataInputStream dataInputStream = new ByteOrderedDataInputStream(exifBytes); // Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1. parseTiffHeaders(dataInputStream, exifBytes.length); // Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6. readImageFileDirectory(dataInputStream, imageType); } private void addDefaultValuesForCompatibility() { // If DATETIME tag has no value, then set the value to DATETIME_ORIGINAL tag's. String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL); if (valueOfDateTimeOriginal != null && getAttribute(TAG_DATETIME) == null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_DATETIME, ExifAttribute.createString(valueOfDateTimeOriginal)); } // Add the default value. if (getAttribute(TAG_IMAGE_WIDTH) == null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong(0, mExifByteOrder)); } if (getAttribute(TAG_IMAGE_LENGTH) == null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong(0, mExifByteOrder)); } if (getAttribute(TAG_ORIENTATION) == null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, ExifAttribute.createUShort(0, mExifByteOrder)); } if (getAttribute(TAG_LIGHT_SOURCE) == null) { mAttributes[IFD_TYPE_EXIF].put(TAG_LIGHT_SOURCE, ExifAttribute.createULong(0, mExifByteOrder)); } } private ByteOrder readByteOrder(ByteOrderedDataInputStream dataInputStream) throws IOException { // Read byte order. short byteOrder = dataInputStream.readShort(); switch (byteOrder) { case BYTE_ALIGN_II: if (DEBUG) { Log.d(TAG, "readExifSegment: Byte Align II"); } return ByteOrder.LITTLE_ENDIAN; case BYTE_ALIGN_MM: if (DEBUG) { Log.d(TAG, "readExifSegment: Byte Align MM"); } return ByteOrder.BIG_ENDIAN; default: throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder)); } } private void parseTiffHeaders(ByteOrderedDataInputStream dataInputStream, int exifBytesLength) throws IOException { // Read byte order mExifByteOrder = readByteOrder(dataInputStream); // Set byte order dataInputStream.setByteOrder(mExifByteOrder); // Check start code int startCode = dataInputStream.readUnsignedShort(); if (mMimeType != IMAGE_TYPE_ORF && mMimeType != IMAGE_TYPE_RW2 && startCode != START_CODE) { throw new IOException("Invalid start code: " + Integer.toHexString(startCode)); } // Read and skip to first ifd offset int firstIfdOffset = dataInputStream.readInt(); if (firstIfdOffset < 8 || firstIfdOffset >= exifBytesLength) { throw new IOException("Invalid first Ifd offset: " + firstIfdOffset); } firstIfdOffset -= 8; if (firstIfdOffset > 0) { if (dataInputStream.skipBytes(firstIfdOffset) != firstIfdOffset) { throw new IOException("Couldn't jump to first Ifd: " + firstIfdOffset); } } } // Reads image file directory, which is a tag group in EXIF. private void readImageFileDirectory(ByteOrderedDataInputStream dataInputStream, @IfdType int ifdType) throws IOException { if (dataInputStream.mPosition + 2 > dataInputStream.mLength) { // Return if there is no data from the offset. return; } // See TIFF 6.0 Section 2: TIFF Structure, Figure 1. short numberOfDirectoryEntry = dataInputStream.readShort(); if (dataInputStream.mPosition + 12 * numberOfDirectoryEntry > dataInputStream.mLength || numberOfDirectoryEntry <= 0) { // Return if the size of entries is either too big or negative. return; } if (DEBUG) { Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry); } // See TIFF 6.0 Section 2: TIFF Structure, "Image File Directory". for (short i = 0; i < numberOfDirectoryEntry; ++i) { int tagNumber = dataInputStream.readUnsignedShort(); int dataFormat = dataInputStream.readUnsignedShort(); int numberOfComponents = dataInputStream.readInt(); // Next four bytes is for data offset or value. long nextEntryOffset = dataInputStream.peek() + 4; // Look up a corresponding tag from tag number ExifTag tag = (ExifTag) sExifTagMapsForReading[ifdType].get(tagNumber); if (DEBUG) { Log.d(TAG, String.format( "ifdType: %d, tagNumber: %d, tagName: %s, dataFormat: %d, " + "numberOfComponents: %d", ifdType, tagNumber, tag != null ? tag.name : null, dataFormat, numberOfComponents)); } long byteCount = 0; boolean valid = false; if (tag == null) { Log.w(TAG, "Skip the tag entry since tag number is not defined: " + tagNumber); } else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) { Log.w(TAG, "Skip the tag entry since data format is invalid: " + dataFormat); } else { byteCount = (long) numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat]; if (byteCount < 0 || byteCount > Integer.MAX_VALUE) { Log.w(TAG, "Skip the tag entry since the number of components is invalid: " + numberOfComponents); } else { valid = true; } } if (!valid) { dataInputStream.seek(nextEntryOffset); continue; } // Read a value from data field or seek to the value offset which is stored in data // field if the size of the entry value is bigger than 4. if (byteCount > 4) { int offset = dataInputStream.readInt(); if (DEBUG) { Log.d(TAG, "seek to data offset: " + offset); } if (mMimeType == IMAGE_TYPE_ORF) { if (tag.name == TAG_MAKER_NOTE) { // Save offset value for reading thumbnail mOrfMakerNoteOffset = offset; } else if (ifdType == IFD_TYPE_ORF_MAKER_NOTE && tag.name == TAG_ORF_THUMBNAIL_IMAGE) { // Retrieve & update values for thumbnail offset and length values for ORF mOrfThumbnailOffset = offset; mOrfThumbnailLength = numberOfComponents; ExifAttribute compressionAttribute = ExifAttribute.createUShort(DATA_JPEG, mExifByteOrder); ExifAttribute jpegInterchangeFormatAttribute = ExifAttribute .createULong(mOrfThumbnailOffset, mExifByteOrder); ExifAttribute jpegInterchangeFormatLengthAttribute = ExifAttribute .createULong(mOrfThumbnailLength, mExifByteOrder); mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_COMPRESSION, compressionAttribute); mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT, jpegInterchangeFormatAttribute); mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, jpegInterchangeFormatLengthAttribute); } } else if (mMimeType == IMAGE_TYPE_RW2) { if (tag.name == TAG_RW2_JPG_FROM_RAW) { mRw2JpgFromRawOffset = 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. Integer nextIfdType = sExifPointerTagMap.get(tagNumber); if (DEBUG) { Log.d(TAG, "nextIfdType: " + nextIfdType + " byteCount: " + byteCount); } if (nextIfdType != null) { long offset = -1L; // Get offset from data field switch (dataFormat) { case IFD_FORMAT_USHORT: { offset = dataInputStream.readUnsignedShort(); break; } case IFD_FORMAT_SSHORT: { offset = dataInputStream.readShort(); break; } case IFD_FORMAT_ULONG: { offset = dataInputStream.readUnsignedInt(); break; } case IFD_FORMAT_SLONG: case IFD_FORMAT_IFD: { offset = dataInputStream.readInt(); break; } default: { // Nothing to do break; } } if (DEBUG) { Log.d(TAG, String.format("Offset: %d, tagName: %s", offset, tag.name)); } // Check if the next IFD offset // 1. Exists within the boundaries of the input stream // 2. Does not point to a previously read IFD. if (offset > 0L && offset < dataInputStream.mLength) { if (!mAttributesOffsets.contains((int) offset)) { // Save offset of current IFD to prevent reading an IFD that is already read mAttributesOffsets.add(dataInputStream.mPosition); dataInputStream.seek(offset); readImageFileDirectory(dataInputStream, nextIfdType); } else { Log.w(TAG, "Skip jump into the IFD since it has already been read: " + "IfdType " + nextIfdType + " (at " + offset + ")"); } } else { Log.w(TAG, "Skip jump into the IFD since its offset is invalid: " + offset); } dataInputStream.seek(nextEntryOffset); continue; } byte[] bytes = new byte[(int) byteCount]; dataInputStream.readFully(bytes); ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, bytes); mAttributes[ifdType].put(tag.name, attribute); // DNG files have a DNG Version tag specifying the version of specifications that the // image file is following. // See http://fileformats.archiveteam.org/wiki/DNG if (tag.name == TAG_DNG_VERSION) { mMimeType = IMAGE_TYPE_DNG; } // PEF files have a Make or Model tag that begins with "PENTAX" or a compression tag // that is 65535. // See http://fileformats.archiveteam.org/wiki/Pentax_PEF if (((tag.name == TAG_MAKE || tag.name == TAG_MODEL) && attribute.getStringValue(mExifByteOrder).contains(PEF_SIGNATURE)) || (tag.name == TAG_COMPRESSION && attribute.getIntValue(mExifByteOrder) == 65535)) { mMimeType = IMAGE_TYPE_PEF; } // Seek to next tag offset if (dataInputStream.peek() != nextEntryOffset) { dataInputStream.seek(nextEntryOffset); } } if (dataInputStream.peek() + 4 <= dataInputStream.mLength) { int nextIfdOffset = dataInputStream.readInt(); if (DEBUG) { Log.d(TAG, String.format("nextIfdOffset: %d", nextIfdOffset)); } // Check if the next IFD offset // 1. Exists within the boundaries of the input stream // 2. Does not point to a previously read IFD. if (nextIfdOffset > 0L && nextIfdOffset < dataInputStream.mLength) { if (!mAttributesOffsets.contains(nextIfdOffset)) { // Save offset of current IFD to prevent reading an IFD that is already read. mAttributesOffsets.add(dataInputStream.mPosition); dataInputStream.seek(nextIfdOffset); // Do not overwrite thumbnail IFD data if it alreay exists. if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) { readImageFileDirectory(dataInputStream, IFD_TYPE_THUMBNAIL); } else if (mAttributes[IFD_TYPE_PREVIEW].isEmpty()) { readImageFileDirectory(dataInputStream, IFD_TYPE_PREVIEW); } } else { Log.w(TAG, "Stop reading file since re-reading an IFD may cause an " + "infinite loop: " + nextIfdOffset); } } else { Log.w(TAG, "Stop reading file since a wrong offset may cause an infinite loop: " + nextIfdOffset); } } } /** * JPEG compressed images do not contain IMAGE_LENGTH & IMAGE_WIDTH tags. * This value uses JpegInterchangeFormat(JPEG data offset) value, and calls getJpegAttributes() * to locate SOF(Start of Frame) marker and update the image length & width values. * See JEITA CP-3451C Table 5 and Section 4.8.1. B. */ private void retrieveJpegImageSize(ByteOrderedDataInputStream in, int imageType) throws IOException { // Check if image already has IMAGE_LENGTH & IMAGE_WIDTH values ExifAttribute imageLengthAttribute = (ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_LENGTH); ExifAttribute imageWidthAttribute = (ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_WIDTH); if (imageLengthAttribute == null || imageWidthAttribute == null) { // Find if offset for JPEG data exists ExifAttribute jpegInterchangeFormatAttribute = (ExifAttribute) mAttributes[imageType] .get(TAG_JPEG_INTERCHANGE_FORMAT); if (jpegInterchangeFormatAttribute != null) { int jpegInterchangeFormat = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder); // Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags getJpegAttributes(in, jpegInterchangeFormat, imageType); } } } // Sets thumbnail offset & length attributes based on JpegInterchangeFormat or StripOffsets tags private void setThumbnailData(ByteOrderedDataInputStream in) throws IOException { HashMap thumbnailData = mAttributes[IFD_TYPE_THUMBNAIL]; ExifAttribute compressionAttribute = (ExifAttribute) thumbnailData.get(TAG_COMPRESSION); if (compressionAttribute != null) { mThumbnailCompression = compressionAttribute.getIntValue(mExifByteOrder); switch (mThumbnailCompression) { case DATA_JPEG: { handleThumbnailFromJfif(in, thumbnailData); break; } case DATA_UNCOMPRESSED: case DATA_JPEG_COMPRESSED: { if (isSupportedDataType(thumbnailData)) { handleThumbnailFromStrips(in, thumbnailData); } break; } } } else { // Thumbnail data may not contain Compression tag value handleThumbnailFromJfif(in, thumbnailData); } } // Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values // and reads the corresponding bytes if stream does not support seek function private void handleThumbnailFromJfif(ByteOrderedDataInputStream in, HashMap thumbnailData) throws IOException { ExifAttribute jpegInterchangeFormatAttribute = (ExifAttribute) thumbnailData .get(TAG_JPEG_INTERCHANGE_FORMAT); ExifAttribute jpegInterchangeFormatLengthAttribute = (ExifAttribute) thumbnailData .get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); if (jpegInterchangeFormatAttribute != null && jpegInterchangeFormatLengthAttribute != null) { int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder); int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder); // The following code limits the size of thumbnail size not to overflow EXIF data area. thumbnailLength = Math.min(thumbnailLength, in.available() - thumbnailOffset); if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_RAF || mMimeType == IMAGE_TYPE_RW2) { thumbnailOffset += mExifOffset; } else if (mMimeType == IMAGE_TYPE_ORF) { // Update offset value since RAF files have IFD data preceding MakerNote data. thumbnailOffset += mOrfMakerNoteOffset; } if (DEBUG) { Log.d(TAG, "Setting thumbnail attributes with offset: " + thumbnailOffset + ", length: " + thumbnailLength); } if (thumbnailOffset > 0 && thumbnailLength > 0) { mHasThumbnail = true; mThumbnailOffset = thumbnailOffset; mThumbnailLength = thumbnailLength; mThumbnailCompression = DATA_JPEG; if (mFilename == null && mAssetInputStream == null && mSeekableFileDescriptor == null) { // Save the thumbnail in memory if the input doesn't support reading again. byte[] thumbnailBytes = new byte[thumbnailLength]; in.seek(thumbnailOffset); in.readFully(thumbnailBytes); mThumbnailBytes = thumbnailBytes; } } } } // Check StripOffsets & StripByteCounts tags to retrieve thumbnail offset & length values private void handleThumbnailFromStrips(ByteOrderedDataInputStream in, HashMap thumbnailData) throws IOException { ExifAttribute stripOffsetsAttribute = (ExifAttribute) thumbnailData.get(TAG_STRIP_OFFSETS); ExifAttribute stripByteCountsAttribute = (ExifAttribute) thumbnailData.get(TAG_STRIP_BYTE_COUNTS); if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) { long[] stripOffsets = convertToLongArray(stripOffsetsAttribute.getValue(mExifByteOrder)); long[] stripByteCounts = convertToLongArray(stripByteCountsAttribute.getValue(mExifByteOrder)); if (stripOffsets == null) { Log.w(TAG, "stripOffsets should not be null."); return; } if (stripByteCounts == null) { Log.w(TAG, "stripByteCounts should not be null."); return; } // Set thumbnail byte array data for non-consecutive strip bytes byte[] totalStripBytes = new byte[(int) Arrays.stream(stripByteCounts).sum()]; int bytesRead = 0; int bytesAdded = 0; for (int i = 0; i < stripOffsets.length; i++) { int stripOffset = (int) stripOffsets[i]; int stripByteCount = (int) stripByteCounts[i]; // Skip to offset int skipBytes = stripOffset - bytesRead; if (skipBytes < 0) { Log.d(TAG, "Invalid strip offset value"); } in.seek(skipBytes); bytesRead += skipBytes; // Read strip bytes byte[] stripBytes = new byte[stripByteCount]; in.read(stripBytes); bytesRead += stripByteCount; // Add bytes to array System.arraycopy(stripBytes, 0, totalStripBytes, bytesAdded, stripBytes.length); bytesAdded += stripBytes.length; } mHasThumbnail = true; mThumbnailBytes = totalStripBytes; mThumbnailLength = totalStripBytes.length; } } // Check if thumbnail data type is currently supported or not private boolean isSupportedDataType(HashMap thumbnailData) throws IOException { ExifAttribute bitsPerSampleAttribute = (ExifAttribute) thumbnailData.get(TAG_BITS_PER_SAMPLE); if (bitsPerSampleAttribute != null) { int[] bitsPerSampleValue = (int[]) bitsPerSampleAttribute.getValue(mExifByteOrder); if (Arrays.equals(BITS_PER_SAMPLE_RGB, bitsPerSampleValue)) { return true; } // See DNG Specification 1.4.0.0. Section 3, Compression. if (mMimeType == IMAGE_TYPE_DNG) { ExifAttribute photometricInterpretationAttribute = (ExifAttribute) thumbnailData .get(TAG_PHOTOMETRIC_INTERPRETATION); if (photometricInterpretationAttribute != null) { int photometricInterpretationValue = photometricInterpretationAttribute .getIntValue(mExifByteOrder); if ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_GREYSCALE_2)) || ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_YCBCR) && (Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_RGB)))) { return true; } else { // TODO: Add support for lossless Huffman JPEG data } } } } if (DEBUG) { Log.d(TAG, "Unsupported data type value"); } return false; } // Returns true if the image length and width values are <= 512. // See Section 4.8 of http://standardsproposals.bsigroup.com/Home/getPDF/567 private boolean isThumbnail(HashMap map) throws IOException { ExifAttribute imageLengthAttribute = (ExifAttribute) map.get(TAG_IMAGE_LENGTH); ExifAttribute imageWidthAttribute = (ExifAttribute) map.get(TAG_IMAGE_WIDTH); if (imageLengthAttribute != null && imageWidthAttribute != null) { int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder); int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder); if (imageLengthValue <= MAX_THUMBNAIL_SIZE && imageWidthValue <= MAX_THUMBNAIL_SIZE) { return true; } } return false; } // Validate primary, preview, thumbnail image data by comparing image size private void validateImages(InputStream in) throws IOException { // Swap images based on size (primary > preview > thumbnail) swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_PREVIEW); swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_THUMBNAIL); swapBasedOnImageSize(IFD_TYPE_PREVIEW, IFD_TYPE_THUMBNAIL); // Check if image has PixelXDimension/PixelYDimension tags, which contain valid image // sizes, excluding padding at the right end or bottom end of the image to make sure that // the values are multiples of 64. See JEITA CP-3451C Table 5 and Section 4.8.1. B. ExifAttribute pixelXDimAttribute = (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_X_DIMENSION); ExifAttribute pixelYDimAttribute = (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_Y_DIMENSION); if (pixelXDimAttribute != null && pixelYDimAttribute != null) { mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, pixelXDimAttribute); mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, pixelYDimAttribute); } // Check whether thumbnail image exists and whether preview image satisfies the thumbnail // image requirements if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) { if (isThumbnail(mAttributes[IFD_TYPE_PREVIEW])) { mAttributes[IFD_TYPE_THUMBNAIL] = mAttributes[IFD_TYPE_PREVIEW]; mAttributes[IFD_TYPE_PREVIEW] = new HashMap(); } } // Check if the thumbnail image satisfies the thumbnail size requirements if (!isThumbnail(mAttributes[IFD_TYPE_THUMBNAIL])) { Log.d(TAG, "No image meets the size requirements of a thumbnail image."); } } /** * If image is uncompressed, ImageWidth/Length tags are used to store size info. * However, uncompressed images often store extra pixels around the edges of the final image, * which results in larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags. * This method corrects those tag values by checking first the values of TAG_DEFAULT_CROP_SIZE * See DNG Specification 1.4.0.0. Section 4. (DefaultCropSize) * * If image is a RW2 file, valid image sizes are stored in SensorBorder tags. * See tiff_parser.cc GetFullDimension32() * */ private void updateImageSizeValues(ByteOrderedDataInputStream in, int imageType) throws IOException { // Uncompressed image valid image size values ExifAttribute defaultCropSizeAttribute = (ExifAttribute) mAttributes[imageType].get(TAG_DEFAULT_CROP_SIZE); // RW2 image valid image size values ExifAttribute topBorderAttribute = (ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_TOP_BORDER); ExifAttribute leftBorderAttribute = (ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_LEFT_BORDER); ExifAttribute bottomBorderAttribute = (ExifAttribute) mAttributes[imageType] .get(TAG_RW2_SENSOR_BOTTOM_BORDER); ExifAttribute rightBorderAttribute = (ExifAttribute) mAttributes[imageType] .get(TAG_RW2_SENSOR_RIGHT_BORDER); if (defaultCropSizeAttribute != null) { // Update for uncompressed image ExifAttribute defaultCropSizeXAttribute, defaultCropSizeYAttribute; if (defaultCropSizeAttribute.format == IFD_FORMAT_URATIONAL) { Rational[] defaultCropSizeValue = (Rational[]) defaultCropSizeAttribute.getValue(mExifByteOrder); defaultCropSizeXAttribute = ExifAttribute.createURational(defaultCropSizeValue[0], mExifByteOrder); defaultCropSizeYAttribute = ExifAttribute.createURational(defaultCropSizeValue[1], mExifByteOrder); } else { int[] defaultCropSizeValue = (int[]) defaultCropSizeAttribute.getValue(mExifByteOrder); defaultCropSizeXAttribute = ExifAttribute.createUShort(defaultCropSizeValue[0], mExifByteOrder); defaultCropSizeYAttribute = ExifAttribute.createUShort(defaultCropSizeValue[1], mExifByteOrder); } mAttributes[imageType].put(TAG_IMAGE_WIDTH, defaultCropSizeXAttribute); mAttributes[imageType].put(TAG_IMAGE_LENGTH, defaultCropSizeYAttribute); } else if (topBorderAttribute != null && leftBorderAttribute != null && bottomBorderAttribute != null && rightBorderAttribute != null) { // Update for RW2 image int topBorderValue = topBorderAttribute.getIntValue(mExifByteOrder); int bottomBorderValue = bottomBorderAttribute.getIntValue(mExifByteOrder); int rightBorderValue = rightBorderAttribute.getIntValue(mExifByteOrder); int leftBorderValue = leftBorderAttribute.getIntValue(mExifByteOrder); if (bottomBorderValue > topBorderValue && rightBorderValue > leftBorderValue) { int length = bottomBorderValue - topBorderValue; int width = rightBorderValue - leftBorderValue; ExifAttribute imageLengthAttribute = ExifAttribute.createUShort(length, mExifByteOrder); ExifAttribute imageWidthAttribute = ExifAttribute.createUShort(width, mExifByteOrder); mAttributes[imageType].put(TAG_IMAGE_LENGTH, imageLengthAttribute); mAttributes[imageType].put(TAG_IMAGE_WIDTH, imageWidthAttribute); } } else { retrieveJpegImageSize(in, imageType); } } // Writes an Exif segment into the given output stream. private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream, 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 : EXIF_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 ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) { for (Object obj : mAttributes[ifdType].entrySet().toArray()) { final Map.Entry entry = (Map.Entry) obj; if (entry.getValue() == null) { mAttributes[ifdType].remove(entry.getKey()); } } } // 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_TYPE_EXIF].isEmpty()) { mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name, ExifAttribute.createULong(0, mExifByteOrder)); } if (!mAttributes[IFD_TYPE_GPS].isEmpty()) { mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name, ExifAttribute.createULong(0, mExifByteOrder)); } if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) { mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name, ExifAttribute.createULong(0, mExifByteOrder)); } if (mHasThumbnail) { mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name, ExifAttribute.createULong(0, mExifByteOrder)); mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name, ExifAttribute.createULong(mThumbnailLength, mExifByteOrder)); } // 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<Map.Entry>) mAttributes[i].entrySet()) { final ExifAttribute exifAttribute = (ExifAttribute) entry.getValue(); final int size = exifAttribute.size(); if (size > 4) { sum += size; } } ifdDataSizes[i] += sum; } // Calculate IFD offsets. int position = 8; for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) { if (!mAttributes[ifdType].isEmpty()) { ifdOffsets[ifdType] = position; position += 2 + mAttributes[ifdType].size() * 12 + 4 + ifdDataSizes[ifdType]; } } if (mHasThumbnail) { int thumbnailOffset = position; mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name, ExifAttribute.createULong(thumbnailOffset, mExifByteOrder)); mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset; position += mThumbnailLength; } // Calculate the total size int totalSize = position + 8; // eight bytes is for header part. if (DEBUG) { Log.d(TAG, "totalSize length: " + totalSize); for (int i = 0; i < 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])); } } // Update IFD pointer tags with the calculated offsets. if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) { mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name, ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifByteOrder)); } if (!mAttributes[IFD_TYPE_GPS].isEmpty()) { mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name, ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifByteOrder)); } if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) { mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name, ExifAttribute.createULong(ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifByteOrder)); } // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1. 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(START_CODE); dataOutputStream.writeUnsignedInt(IFD_OFFSET); // Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9. for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) { if (!mAttributes[ifdType].isEmpty()) { // See JEITA CP-3451C Section 4.6.2: IFD structure. // Write entry count dataOutputStream.writeUnsignedShort(mAttributes[ifdType].size()); // Write entry info int dataOffset = ifdOffsets[ifdType] + 2 + mAttributes[ifdType].size() * 12 + 4; for (Map.Entry entry : (Set<Map.Entry>) mAttributes[ifdType].entrySet()) { // Convert tag name to tag number. final ExifTag tag = (ExifTag) sExifTagMapsForWriting[ifdType].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 (ifdType == 0 && !mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) { dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_TYPE_THUMBNAIL]); } else { dataOutputStream.writeUnsignedInt(0); } // Write values of data field exceeding 4 bytes after the next offset. for (Map.Entry entry : (Set<Map.Entry>) mAttributes[ifdType].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(getThumbnailBytes()); } // 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<Integer, Integer> guessDataFormat(String entryValue) { // See TIFF 6.0 Section 2, "Image File Directory". // Take the first component if there are more than one component. if (entryValue.contains(",")) { String[] entryValues = entryValue.split(","); Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]); if (dataFormat.first == IFD_FORMAT_STRING) { return dataFormat; } for (int i = 1; i < entryValues.length; ++i) { final Pair<Integer, Integer> 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) Double.parseDouble(rationalNumber[0]); long denominator = (long) Double.parseDouble(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 ByteOrderedDataInputStream extends InputStream implements DataInput { private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN; private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN; private DataInputStream mDataInputStream; private InputStream mInputStream; private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN; private final int mLength; private int mPosition; public ByteOrderedDataInputStream(InputStream in) throws IOException { mInputStream = in; mDataInputStream = new DataInputStream(in); mLength = mDataInputStream.available(); mPosition = 0; mDataInputStream.mark(mLength); } public ByteOrderedDataInputStream(byte[] bytes) throws IOException { this(new ByteArrayInputStream(bytes)); } public void setByteOrder(ByteOrder byteOrder) { mByteOrder = byteOrder; } public void seek(long byteCount) throws IOException { if (mPosition > byteCount) { mPosition = 0; mDataInputStream.reset(); mDataInputStream.mark(mLength); } else { byteCount -= mPosition; } if (skipBytes((int) byteCount) != (int) byteCount) { throw new IOException("Couldn't seek up to the byteCount"); } } public int peek() { return mPosition; } @Override public int available() throws IOException { return mDataInputStream.available(); } @Override public int read() throws IOException { ++mPosition; return mDataInputStream.read(); } @Override public int readUnsignedByte() throws IOException { ++mPosition; return mDataInputStream.readUnsignedByte(); } @Override public String readLine() throws IOException { Log.d(TAG, "Currently unsupported"); return null; } @Override public boolean readBoolean() throws IOException { ++mPosition; return mDataInputStream.readBoolean(); } @Override public char readChar() throws IOException { mPosition += 2; return mDataInputStream.readChar(); } @Override public String readUTF() throws IOException { mPosition += 2; return mDataInputStream.readUTF(); } @Override public void readFully(byte[] buffer, int offset, int length) throws IOException { mPosition += length; if (mPosition > mLength) { throw new EOFException(); } if (mDataInputStream.read(buffer, offset, length) != length) { throw new IOException("Couldn't read up to the length of buffer"); } } @Override public void readFully(byte[] buffer) throws IOException { mPosition += buffer.length; if (mPosition > mLength) { throw new EOFException(); } if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) { throw new IOException("Couldn't read up to the length of buffer"); } } @Override public byte readByte() throws IOException { ++mPosition; if (mPosition > mLength) { throw new EOFException(); } int ch = mDataInputStream.read(); if (ch < 0) { throw new EOFException(); } return (byte) ch; } @Override public short readShort() throws IOException { mPosition += 2; if (mPosition > mLength) { throw new EOFException(); } int ch1 = mDataInputStream.read(); int ch2 = mDataInputStream.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); } @Override public int readInt() throws IOException { mPosition += 4; if (mPosition > mLength) { throw new EOFException(); } int ch1 = mDataInputStream.read(); int ch2 = mDataInputStream.read(); int ch3 = mDataInputStream.read(); int ch4 = mDataInputStream.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 int skipBytes(int byteCount) throws IOException { int totalSkip = Math.min(byteCount, mLength - mPosition); int skipped = 0; while (skipped < totalSkip) { skipped += mDataInputStream.skipBytes(totalSkip - skipped); } mPosition += skipped; return skipped; } public int readUnsignedShort() throws IOException { mPosition += 2; if (mPosition > mLength) { throw new EOFException(); } int ch1 = mDataInputStream.read(); int ch2 = mDataInputStream.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; } @Override public long readLong() throws IOException { mPosition += 8; if (mPosition > mLength) { throw new EOFException(); } int ch1 = mDataInputStream.read(); int ch2 = mDataInputStream.read(); int ch3 = mDataInputStream.read(); int ch4 = mDataInputStream.read(); int ch5 = mDataInputStream.read(); int ch6 = mDataInputStream.read(); int ch7 = mDataInputStream.read(); int ch8 = mDataInputStream.read(); if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) { throw new EOFException(); } if (mByteOrder == LITTLE_ENDIAN) { return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40) + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16) + ((long) ch2 << 8) + (long) ch1); } else if (mByteOrder == BIG_ENDIAN) { return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40) + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16) + ((long) ch7 << 8) + (long) ch8); } throw new IOException("Invalid byte order: " + mByteOrder); } @Override public float readFloat() throws IOException { return Float.intBitsToFloat(readInt()); } @Override 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 ByteOrderedDataOutputStream extends FilterOutputStream { private final OutputStream mOutputStream; private ByteOrder mByteOrder; public ByteOrderedDataOutputStream(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); } } // Swaps image data based on image size private void swapBasedOnImageSize(@IfdType int firstIfdType, @IfdType int secondIfdType) throws IOException { if (mAttributes[firstIfdType].isEmpty() || mAttributes[secondIfdType].isEmpty()) { if (DEBUG) { Log.d(TAG, "Cannot perform swap since only one image data exists"); } return; } ExifAttribute firstImageLengthAttribute = (ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_LENGTH); ExifAttribute firstImageWidthAttribute = (ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_WIDTH); ExifAttribute secondImageLengthAttribute = (ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_LENGTH); ExifAttribute secondImageWidthAttribute = (ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_WIDTH); if (firstImageLengthAttribute == null || firstImageWidthAttribute == null) { if (DEBUG) { Log.d(TAG, "First image does not contain valid size information"); } } else if (secondImageLengthAttribute == null || secondImageWidthAttribute == null) { if (DEBUG) { Log.d(TAG, "Second image does not contain valid size information"); } } else { int firstImageLengthValue = firstImageLengthAttribute.getIntValue(mExifByteOrder); int firstImageWidthValue = firstImageWidthAttribute.getIntValue(mExifByteOrder); int secondImageLengthValue = secondImageLengthAttribute.getIntValue(mExifByteOrder); int secondImageWidthValue = secondImageWidthAttribute.getIntValue(mExifByteOrder); if (firstImageLengthValue < secondImageLengthValue && firstImageWidthValue < secondImageWidthValue) { HashMap tempMap = mAttributes[firstIfdType]; mAttributes[firstIfdType] = mAttributes[secondIfdType]; mAttributes[secondIfdType] = tempMap; } } } // Checks if there is a match private boolean containsMatch(byte[] mainBytes, byte[] findBytes) { for (int i = 0; i < mainBytes.length - findBytes.length; i++) { for (int j = 0; j < findBytes.length; j++) { if (mainBytes[i + j] != findBytes[j]) { break; } if (j == findBytes.length - 1) { return true; } } } return false; } /** * Convert given int[] to long[]. If long[] is given, just return it. * Return null for other types of input. */ private static long[] convertToLongArray(Object inputObj) { if (inputObj instanceof int[]) { int[] input = (int[]) inputObj; long[] result = new long[input.length]; for (int i = 0; i < input.length; i++) { result[i] = input[i]; } return result; } else if (inputObj instanceof long[]) { return (long[]) inputObj; } return null; } }