/*
 * 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.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import android.util.Pair;
import 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.

Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF.

Attribute mutation is supported for JPEG image files.

/** * 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.
/** Type is String. */
public static final String TAG_ARTIST = "Artist";
Type is int.
/** Type is int. */
public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample";
Type is int.
/** Type is int. */
public static final String TAG_COMPRESSION = "Compression";
Type is String.
/** Type is String. */
public static final String TAG_COPYRIGHT = "Copyright";
Type is String.
/** Type is String. */
public static final String TAG_DATETIME = "DateTime";
Type is String.
/** Type is String. */
public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription";
Type is int.
/** Type is int. */
public static final String TAG_IMAGE_LENGTH = "ImageLength";
Type is int.
/** Type is int. */
public static final String TAG_IMAGE_WIDTH = "ImageWidth";
Type is int.
/** Type is int. */
public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat";
Type is int.
/** Type is int. */
public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength";
Type is String.
/** Type is String. */
public static final String TAG_MAKE = "Make";
Type is String.
/** Type is String. */
public static final String TAG_MODEL = "Model";
Type is int.
/** Type is int. */
public static final String TAG_ORIENTATION = "Orientation";
Type is int.
/** Type is int. */
public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
Type is int.
/** Type is int. */
public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration";
Type is rational.
/** Type is rational. */
public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities";
Type is rational.
/** Type is rational. */
public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite";
Type is int.
/** Type is int. */
public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit";
Type is int.
/** Type is int. */
public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip";
Type is int.
/** Type is int. */
public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel";
Type is String.
/** Type is String. */
public static final String TAG_SOFTWARE = "Software";
Type is int.
/** Type is int. */
public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts";
Type is int.
/** Type is int. */
public static final String TAG_STRIP_OFFSETS = "StripOffsets";
Type is int.
/** Type is int. */
public static final String TAG_TRANSFER_FUNCTION = "TransferFunction";
Type is rational.
/** Type is rational. */
public static final String TAG_WHITE_POINT = "WhitePoint";
Type is rational.
/** Type is rational. */
public static final String TAG_X_RESOLUTION = "XResolution";
Type is rational.
/** Type is rational. */
public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients";
Type is int.
/** Type is int. */
public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning";
Type is int.
/** Type is int. */
public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling";
Type is rational.
/** Type is rational. */
public static final String TAG_Y_RESOLUTION = "YResolution";
Type is rational.
/** Type is rational. */
public static final String TAG_APERTURE_VALUE = "ApertureValue";
Type is rational.
/** Type is rational. */
public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue";
Type is String.
/** Type is String. */
public static final String TAG_CFA_PATTERN = "CFAPattern";
Type is int.
/** Type is int. */
public static final String TAG_COLOR_SPACE = "ColorSpace";
Type is String.
/** Type is String. */
public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration";
Type is rational.
/** Type is rational. */
public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel";
Type is int.
/** Type is int. */
public static final String TAG_CONTRAST = "Contrast";
Type is int.
/** Type is int. */
public static final String TAG_CUSTOM_RENDERED = "CustomRendered";
Type is String.
/** Type is String. */
public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
Type is String.
/** Type is String. */
public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
Type is String.
/** Type is String. */
public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription";
Type is double.
/** Type is double. */
public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
Type is String.
/** Type is String. */
public static final String TAG_EXIF_VERSION = "ExifVersion";
Type is double.
/** Type is double. */
public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
Type is rational.
/** Type is rational. */
public static final String TAG_EXPOSURE_INDEX = "ExposureIndex";
Type is int.
/** Type is int. */
public static final String TAG_EXPOSURE_MODE = "ExposureMode";
Type is int.
/** Type is int. */
public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
Type is double.
/** Type is double. */
public static final String TAG_EXPOSURE_TIME = "ExposureTime";
Type is double.
/** Type is double. */
public static final String TAG_F_NUMBER = "FNumber";
Type is double.
Deprecated:use TAG_F_NUMBER instead
/** * Type is double. * * @deprecated use {@link #TAG_F_NUMBER} instead */
@Deprecated public static final String TAG_APERTURE = "FNumber";
Type is String.
/** Type is String. */
public static final String TAG_FILE_SOURCE = "FileSource";
Type is int.
/** Type is int. */
public static final String TAG_FLASH = "Flash";
Type is rational.
/** Type is rational. */
public static final String TAG_FLASH_ENERGY = "FlashEnergy";
Type is String.
/** Type is String. */
public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion";
Type is rational.
/** Type is rational. */
public static final String TAG_FOCAL_LENGTH = "FocalLength";
Type is int.
/** Type is int. */
public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm";
Type is int.
/** Type is int. */
public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit";
Type is rational.
/** Type is rational. */
public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution";
Type is rational.
/** Type is rational. */
public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution";
Type is int.
/** Type is int. */
public static final String TAG_GAIN_CONTROL = "GainControl";
Type is int.
/** Type is int. */
public static final String TAG_ISO_SPEED_RATINGS = "ISOSpeedRatings";
Type is int.
Deprecated:use TAG_ISO_SPEED_RATINGS instead
/** * Type is int. * * @deprecated use {@link #TAG_ISO_SPEED_RATINGS} instead */
@Deprecated public static final String TAG_ISO = "ISOSpeedRatings";
Type is String.
/** Type is String. */
public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID";
Type is int.
/** Type is int. */
public static final String TAG_LIGHT_SOURCE = "LightSource";
Type is String.
/** Type is String. */
public static final String TAG_MAKER_NOTE = "MakerNote";
Type is rational.
/** Type is rational. */
public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue";
Type is int.
/** Type is int. */
public static final String TAG_METERING_MODE = "MeteringMode";
Type is int.
/** Type is int. */
public static final String TAG_NEW_SUBFILE_TYPE = "NewSubfileType";
Type is String.
/** Type is String. */
public static final String TAG_OECF = "OECF";
Type is int.
/** Type is int. */
public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension";
Type is int.
/** Type is int. */
public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension";
Type is String.
/** Type is String. */
public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile";
Type is int.
/** Type is int. */
public static final String TAG_SATURATION = "Saturation";
Type is int.
/** Type is int. */
public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType";
Type is String.
/** Type is String. */
public static final String TAG_SCENE_TYPE = "SceneType";
Type is int.
/** Type is int. */
public static final String TAG_SENSING_METHOD = "SensingMethod";
Type is int.
/** Type is int. */
public static final String TAG_SHARPNESS = "Sharpness";
Type is rational.
/** Type is rational. */
public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue";
Type is String.
/** Type is String. */
public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse";
Type is String.
/** Type is String. */
public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity";
Type is int.
/** Type is int. */
public static final String TAG_SUBFILE_TYPE = "SubfileType";
Type is String.
/** Type is String. */
public static final String TAG_SUBSEC_TIME = "SubSecTime";
Type is String.
Deprecated:use TAG_SUBSEC_TIME_DIGITIZED instead
/** * Type is String. * * @deprecated use {@link #TAG_SUBSEC_TIME_DIGITIZED} instead */
public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
Type is String.
/** Type is String. */
public static final String TAG_SUBSEC_TIME_DIGITIZED = "SubSecTimeDigitized";
Type is String.
Deprecated:use TAG_SUBSEC_TIME_ORIGINAL instead
/** * Type is String. * * @deprecated use {@link #TAG_SUBSEC_TIME_ORIGINAL} instead */
public static final String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
Type is String.
/** Type is String. */
public static final String TAG_SUBSEC_TIME_ORIGINAL = "SubSecTimeOriginal";
Type is int.
/** Type is int. */
public static final String TAG_SUBJECT_AREA = "SubjectArea";
Type is double.
/** Type is double. */
public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
Type is int.
/** Type is int. */
public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange";
Type is int.
/** Type is int. */
public static final String TAG_SUBJECT_LOCATION = "SubjectLocation";
Type is String.
/** Type is String. */
public static final String TAG_USER_COMMENT = "UserComment";
Type is int.
/** 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.
/** * 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.
/** * 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.
/** Type is String. */
public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_DOP = "GPSDOP";
Type is String.
/** Type is String. */
public static final String TAG_GPS_DATESTAMP = "GPSDateStamp";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing";
Type is String.
/** Type is String. */
public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance";
Type is String.
/** Type is String. */
public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude";
Type is String.
/** Type is String. */
public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude";
Type is String.
/** Type is String. */
public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef";
Type is int.
/** Type is int. */
public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection";
Type is String.
/** Type is String. */
public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef";
Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3".
/** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */
public static final String TAG_GPS_LATITUDE = "GPSLatitude";
Type is String.
/** Type is String. */
public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3".
/** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */
public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
Type is String.
/** Type is String. */
public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
Type is String.
/** Type is String. */
public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum";
Type is String.
/** Type is String. */
public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode";
Type is String. Name of GPS processing method used for location finding.
/** Type is String. Name of GPS processing method used for location finding. */
public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
Type is String.
/** Type is String. */
public static final String TAG_GPS_SATELLITES = "GPSSatellites";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_SPEED = "GPSSpeed";
Type is String.
/** Type is String. */
public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
Type is String.
/** Type is String. */
public static final String TAG_GPS_STATUS = "GPSStatus";
Type is String. Format is "hh:mm:ss".
/** Type is String. Format is "hh:mm:ss". */
public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
Type is rational.
/** Type is rational. */
public static final String TAG_GPS_TRACK = "GPSTrack";
Type is String.
/** Type is String. */
public static final String TAG_GPS_TRACK_REF = "GPSTrackRef";
Type is String.
/** Type is String. */
public static final String TAG_GPS_VERSION_ID = "GPSVersionID";
Type is String.
/** Type is String. */
public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex";
Type is int.
/** Type is int. */
public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength";
Type is int.
/** Type is int. */
public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth";
Type is int. DNG Specification 1.4.0.0. Section 4
/** 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
/** 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.
/** 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.
/** 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.
/** 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.
/** 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
/** * 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
/** * 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
/** * 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
/** * 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
/** * 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
/** * 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 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; 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
/** * 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
/** * 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.
/** * 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
/** * 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
/** @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 } private final String mFilename; private final FileDescriptor mSeekableFileDescriptor; private final AssetManager.AssetInputStream mAssetInputStream; private final boolean mIsInputStream; private int mMimeType; private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length]; private Set<Integer> mAttributesOffsets = new HashSet<>(EXIF_TAGS.length); private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN; private boolean mHasThumbnail; // The following values used for indicating a thumbnail position. private int mThumbnailOffset; private int mThumbnailLength; private byte[] mThumbnailBytes; 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.
/** * 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.
/** * 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.
/** * 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 null if there is no such tag in the image file.
Params:
  • tag – the name of the tag.
/** * 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 null if there is no such tag in the image file.
Params:
  • tag – the name of the tag.
/** * 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 defaultValue.
Params:
  • tag – the name of the tag.
  • defaultValue – the value to return if the tag is not available.
/** * 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 defaultValue.
Params:
  • tag – the name of the tag.
  • defaultValue – the value to return if the tag is not available.
/** * 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.
Params:
  • tag – the name of the tag.
  • value – the value of the tag.
/** * 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.
Params:
  • tag – the name of the tag.
  • value – the value of the tag in a form of ExifAttribute.
Returns:Returns true if updating is placed.
/** * 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.
Params:
  • tag – the name of the tag.
/** * 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.
/** * 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 setAttribute(String, String) to set all attributes to write and make a single call rather than multiple calls for each attribute.

This method is only supported for JPEG files.

/** * 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.
/** * Returns true if the image file has a thumbnail. */
public boolean hasThumbnail() { return mHasThumbnail; }
Returns the JPEG compressed thumbnail inside the image file, or null if there is no JPEG compressed thumbnail. The returned data can be decoded using BitmapFactory.decodeByteArray(byte[], int, int)
/** * 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.
/** * 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 null if the compression type is unsupported.
/** * 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.
/** * 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 null if there is no thumbnail.
Returns:two-element array, the offset in the first value, and length in the second, or null if no thumbnail was found.
/** * 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.
/** * 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 defaultValue.
Params:
  • defaultValue – the value to return if the tag is not available.
/** * 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
/** * Returns number of milliseconds since Jan. 1, 1970, midnight local time. * Returns -1 if the date time information if not available. * @hide */
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
/** * Returns number of milliseconds since Jan. 1, 1970, midnight UTC. * Returns -1 if the date time information if not available. * @hide */
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}
/** {@hide} */
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"
/** * 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
/** * 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
/** * 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/
/** * 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.
Params:
  • in – The input stream that starts with the JPEG data.
  • jpegOffset – The offset value in input stream for JPEG data.
  • 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.
/** * 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
/** * 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) { in.seek(position); mPosition = position; } 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
/** * 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.
/** * 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()
/** * 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.
Params:
  • entryValue – The value to be determined.
Returns:Returns two data formats gussed as a pair in integer. If there is no two candidate data formats for the given entry value, returns -1 in the second of the pair.
/** * 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.
/** * 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; } }