content: /*
   * 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.CurrentTimeMillisLong;
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.content.res.AssetManager;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.os.Build;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.OsConstants;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.internal.util.ArrayUtils;
  
  import libcore.io.IoUtils;
  import libcore.io.Streams;
  
  import java.io.BufferedInputStream;
  import java.io.ByteArrayInputStream;
  import java.io.DataInput;
  import java.io.DataInputStream;
  import java.io.EOFException;
  import java.io.File;
  import java.io.FileDescriptor;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.FilterOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  import java.nio.charset.Charset;
  import java.text.ParsePosition;
  import java.text.SimpleDateFormat;
  import java.util.Arrays;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Map;
  import java.util.Set;
  import java.util.TimeZone;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * This is a class for reading and writing Exif tags in a JPEG file 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.
   * <p>
   * Note: It is recommended to use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
   * <a href="{@docRoot}reference/androidx/exifinterface/media/ExifInterface.html">ExifInterface
   * Library</a> since it is a superset of this class. In addition to the functionalities of this
   * class, it supports parsing extra metadata such as exposure and data compression information
   * as well as setting extra metadata such as GPS and datetime information.
   */
  public class ExifInterface {
      private static final String TAG = "ExifInterface";
      private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
  
      // The Exif tag names. See Tiff 6.0 Section 3 and Section 8.
      /** Type is String. */
      public static final String TAG_ARTIST = "Artist";
      /** Type is int. */
      public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample";
      /** Type is int. */
      public static final String TAG_COMPRESSION = "Compression";
      /** Type is String. */
      public static final String TAG_COPYRIGHT = "Copyright";
      /** Type is String. */
      public static final String TAG_DATETIME = "DateTime";
      /** Type is String. */
      public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription";
      /** Type is int. */
      public static final String TAG_IMAGE_LENGTH = "ImageLength";
      /** Type is int. */
      public static final String TAG_IMAGE_WIDTH = "ImageWidth";
      /** Type is int. */
      public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat";
      /** Type is int. */
      public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength";
      /** Type is String. */
      public static final String TAG_MAKE = "Make";
      /** Type is String. */
      public static final String TAG_MODEL = "Model";
      /** Type is int. */
      public static final String TAG_ORIENTATION = "Orientation";
      /** Type is int. */
      public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
      /** Type is int. */
      public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration";
      /** Type is rational. */
      public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities";
      /** Type is rational. */
      public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite";
      /** Type is int. */
      public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit";
      /** Type is int. */
      public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip";
      /** Type is int. */
      public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel";
      /** Type is String. */
      public static final String TAG_SOFTWARE = "Software";
      /** Type is int. */
      public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts";
      /** Type is int. */
      public static final String TAG_STRIP_OFFSETS = "StripOffsets";
      /** Type is int. */
      public static final String TAG_TRANSFER_FUNCTION = "TransferFunction";
      /** Type is rational. */
      public static final String TAG_WHITE_POINT = "WhitePoint";
      /** Type is rational. */
      public static final String TAG_X_RESOLUTION = "XResolution";
      /** Type is rational. */
      public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients";
      /** Type is int. */
      public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning";
      /** Type is int. */
      public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling";
      /** Type is rational. */
      public static final String TAG_Y_RESOLUTION = "YResolution";
      /** Type is rational. */
      public static final String TAG_APERTURE_VALUE = "ApertureValue";
      /** Type is rational. */
      public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue";
      /** Type is String. */
      public static final String TAG_CFA_PATTERN = "CFAPattern";
      /** Type is int. */
      public static final String TAG_COLOR_SPACE = "ColorSpace";
      /** Type is String. */
      public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration";
      /** Type is rational. */
      public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel";
      /** Type is int. */
      public static final String TAG_CONTRAST = "Contrast";
      /** Type is int. */
      public static final String TAG_CUSTOM_RENDERED = "CustomRendered";
      /** Type is String. */
      public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
      /** Type is String. */
      public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
      /** Type is String. */
      public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription";
      /** Type is double. */
      public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
      /** Type is String. */
      public static final String TAG_EXIF_VERSION = "ExifVersion";
      /** Type is double. */
      public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
      /** Type is rational. */
      public static final String TAG_EXPOSURE_INDEX = "ExposureIndex";
      /** Type is int. */
      public static final String TAG_EXPOSURE_MODE = "ExposureMode";
      /** Type is int. */
      public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
      /** Type is double. */
      public static final String TAG_EXPOSURE_TIME = "ExposureTime";
      /** Type is double. */
      public static final String TAG_F_NUMBER = "FNumber";
      /**
       * Type is double.
       *
       * @deprecated use {@link #TAG_F_NUMBER} instead
       */
      @Deprecated
      public static final String TAG_APERTURE = "FNumber";
      /** Type is String. */
      public static final String TAG_FILE_SOURCE = "FileSource";
      /** Type is int. */
      public static final String TAG_FLASH = "Flash";
      /** Type is rational. */
      public static final String TAG_FLASH_ENERGY = "FlashEnergy";
      /** Type is String. */
      public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion";
      /** Type is rational. */
      public static final String TAG_FOCAL_LENGTH = "FocalLength";
      /** Type is int. */
      public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm";
      /** Type is int. */
      public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit";
      /** Type is rational. */
      public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution";
      /** Type is rational. */
      public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution";
      /** Type is int. */
      public static final String TAG_GAIN_CONTROL = "GainControl";
      /** Type is int. */
      public static final String TAG_ISO_SPEED_RATINGS = "ISOSpeedRatings";
      /**
       * Type is int.
       *
       * @deprecated use {@link #TAG_ISO_SPEED_RATINGS} instead
       */
      @Deprecated
      public static final String TAG_ISO = "ISOSpeedRatings";
      /** Type is String. */
      public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID";
      /** Type is int. */
      public static final String TAG_LIGHT_SOURCE = "LightSource";
      /** Type is String. */
      public static final String TAG_MAKER_NOTE = "MakerNote";
      /** Type is rational. */
      public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue";
      /** Type is int. */
      public static final String TAG_METERING_MODE = "MeteringMode";
      /** Type is int. */
      public static final String TAG_NEW_SUBFILE_TYPE = "NewSubfileType";
      /** Type is String. */
      public static final String TAG_OECF = "OECF";
      /** Type is String. {@hide} */
      public static final String TAG_OFFSET_TIME = "OffsetTime";
      /** Type is String. {@hide} */
      public static final String TAG_OFFSET_TIME_ORIGINAL = "OffsetTimeOriginal";
      /** Type is String. {@hide} */
      public static final String TAG_OFFSET_TIME_DIGITIZED = "OffsetTimeDigitized";
      /** Type is int. */
      public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension";
      /** Type is int. */
      public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension";
      /** Type is String. */
      public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile";
      /** Type is int. */
      public static final String TAG_SATURATION = "Saturation";
      /** Type is int. */
      public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType";
      /** Type is String. */
      public static final String TAG_SCENE_TYPE = "SceneType";
      /** Type is int. */
      public static final String TAG_SENSING_METHOD = "SensingMethod";
      /** Type is int. */
      public static final String TAG_SHARPNESS = "Sharpness";
      /** Type is rational. */
      public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue";
      /** Type is String. */
      public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse";
      /** Type is String. */
      public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity";
      /** Type is int. */
      public static final String TAG_SUBFILE_TYPE = "SubfileType";
      /** Type is String. */
      public static final String TAG_SUBSEC_TIME = "SubSecTime";
      /**
       * Type is String.
       *
       * @deprecated use {@link #TAG_SUBSEC_TIME_DIGITIZED} instead
       */
      public static final String TAG_SUBSEC_TIME_DIG = "SubSecTimeDigitized";
      /** Type is String. */
      public static final String TAG_SUBSEC_TIME_DIGITIZED = "SubSecTimeDigitized";
      /**
       * Type is String.
       *
       * @deprecated use {@link #TAG_SUBSEC_TIME_ORIGINAL} instead
       */
      public static final String TAG_SUBSEC_TIME_ORIG = "SubSecTimeOriginal";
      /** Type is String. */
      public static final String TAG_SUBSEC_TIME_ORIGINAL = "SubSecTimeOriginal";
      /** Type is int. */
      public static final String TAG_SUBJECT_AREA = "SubjectArea";
      /** Type is double. */
      public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
      /** Type is int. */
      public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange";
      /** Type is int. */
      public static final String TAG_SUBJECT_LOCATION = "SubjectLocation";
      /** Type is String. */
      public static final String TAG_USER_COMMENT = "UserComment";
      /** Type is int. */
      public static final String TAG_WHITE_BALANCE = "WhiteBalance";
      /**
       * The altitude (in meters) based on the reference in TAG_GPS_ALTITUDE_REF.
       * Type is rational.
       */
      public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
      /**
       * 0 if the altitude is above sea level. 1 if the altitude is below sea
       * level. Type is int.
       */
      public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef";
      /** Type is String. */
      public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation";
      /** Type is rational. */
      public static final String TAG_GPS_DOP = "GPSDOP";
      /** Type is String. */
      public static final String TAG_GPS_DATESTAMP = "GPSDateStamp";
      /** Type is rational. */
      public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing";
      /** Type is String. */
      public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef";
      /** Type is rational. */
      public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance";
      /** Type is String. */
      public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef";
      /** Type is rational. */
      public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude";
      /** Type is String. */
      public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef";
      /** Type is rational. */
      public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude";
      /** Type is String. */
      public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef";
      /** Type is int. */
      public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential";
      /** Type is rational. */
      public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection";
      /** Type is String. */
      public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef";
      /** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */
      public static final String TAG_GPS_LATITUDE = "GPSLatitude";
      /** Type is String. */
      public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
      /** Type is rational. Format is "num1/denom1,num2/denom2,num3/denom3". */
      public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
      /** Type is String. */
      public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
      /** Type is String. */
      public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum";
      /** Type is String. */
      public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode";
      /** Type is String. Name of GPS processing method used for location finding. */
      public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
      /** Type is String. */
      public static final String TAG_GPS_SATELLITES = "GPSSatellites";
      /** Type is rational. */
      public static final String TAG_GPS_SPEED = "GPSSpeed";
      /** Type is String. */
      public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
      /** Type is String. */
      public static final String TAG_GPS_STATUS = "GPSStatus";
      /** Type is String. Format is "hh:mm:ss". */
      public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
      /** Type is rational. */
      public static final String TAG_GPS_TRACK = "GPSTrack";
      /** Type is String. */
      public static final String TAG_GPS_TRACK_REF = "GPSTrackRef";
      /** Type is String. */
      public static final String TAG_GPS_VERSION_ID = "GPSVersionID";
      /** Type is String. */
      public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex";
      /** Type is int. */
      public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength";
      /** Type is int. */
      public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth";
      /** Type is int. DNG Specification 1.4.0.0. Section 4 */
      public static final String TAG_DNG_VERSION = "DNGVersion";
      /** Type is int. DNG Specification 1.4.0.0. Section 4 */
      public static final String TAG_DEFAULT_CROP_SIZE = "DefaultCropSize";
      /** Type is undefined. See Olympus MakerNote tags in http://www.exiv2.org/tags-olympus.html. */
      public static final String TAG_ORF_THUMBNAIL_IMAGE = "ThumbnailImage";
      /** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
      public static final String TAG_ORF_PREVIEW_IMAGE_START = "PreviewImageStart";
      /** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
      public static final String TAG_ORF_PREVIEW_IMAGE_LENGTH = "PreviewImageLength";
      /** Type is int. See Olympus Image Processing tags in http://www.exiv2.org/tags-olympus.html. */
      public static final String TAG_ORF_ASPECT_FRAME = "AspectFrame";
      /**
       * Type is int. See PanasonicRaw tags in
       * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
       */
      public static final String TAG_RW2_SENSOR_BOTTOM_BORDER = "SensorBottomBorder";
      /**
       * Type is int. See PanasonicRaw tags in
       * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
       */
      public static final String TAG_RW2_SENSOR_LEFT_BORDER = "SensorLeftBorder";
      /**
       * Type is int. See PanasonicRaw tags in
       * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
       */
      public static final String TAG_RW2_SENSOR_RIGHT_BORDER = "SensorRightBorder";
      /**
       * Type is int. See PanasonicRaw tags in
       * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
       */
      public static final String TAG_RW2_SENSOR_TOP_BORDER = "SensorTopBorder";
      /**
       * Type is int. See PanasonicRaw tags in
       * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
       */
      public static final String TAG_RW2_ISO = "ISO";
      /**
       * Type is undefined. See PanasonicRaw tags in
       * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
       */
      public static final String TAG_RW2_JPG_FROM_RAW = "JpgFromRaw";
      /**
       * Type is byte[]. See <a href=
       * "https://en.wikipedia.org/wiki/Extensible_Metadata_Platform">Extensible
       * Metadata Platform (XMP)</a> for details on contents.
       */
      public static final String TAG_XMP = "Xmp";
  
      /**
       * Private tags used for pointing the other IFD offsets.
       * The types of the following tags are int.
       * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
       * For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
       */
      private static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
      private static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
      private static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
      private static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
      // Proprietary pointer tags used for ORF files.
      // See http://www.exiv2.org/tags-olympus.html
      private static final String TAG_ORF_CAMERA_SETTINGS_IFD_POINTER = "CameraSettingsIFDPointer";
      private static final String TAG_ORF_IMAGE_PROCESSING_IFD_POINTER = "ImageProcessingIFDPointer";
  
      // Private tags used for thumbnail information.
      private static final String TAG_HAS_THUMBNAIL = "HasThumbnail";
      private static final String TAG_THUMBNAIL_OFFSET = "ThumbnailOffset";
      private static final String TAG_THUMBNAIL_LENGTH = "ThumbnailLength";
      private static final String TAG_THUMBNAIL_DATA = "ThumbnailData";
      private static final int MAX_THUMBNAIL_SIZE = 512;
  
      // Constants used for the Orientation Exif tag.
      public static final int ORIENTATION_UNDEFINED = 0;
      public static final int ORIENTATION_NORMAL = 1;
      public static final int ORIENTATION_FLIP_HORIZONTAL = 2;  // left right reversed mirror
      public static final int ORIENTATION_ROTATE_180 = 3;
      public static final int ORIENTATION_FLIP_VERTICAL = 4;  // upside down mirror
      // flipped about top-left <--> bottom-right axis
      public static final int ORIENTATION_TRANSPOSE = 5;
      public static final int ORIENTATION_ROTATE_90 = 6;  // rotate 90 cw to right it
      // flipped about top-right <--> bottom-left axis
      public static final int ORIENTATION_TRANSVERSE = 7;
      public static final int ORIENTATION_ROTATE_270 = 8;  // rotate 270 to right it
  
      // Constants used for white balance
      public static final int WHITEBALANCE_AUTO = 0;
      public static final int WHITEBALANCE_MANUAL = 1;
  
      // Maximum size for checking file type signature (see image_type_recognition_lite.cc)
      private static final int SIGNATURE_CHECK_SIZE = 5000;
  
      private static final byte[] JPEG_SIGNATURE = new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff};
      private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW";
      private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84;
      private static final int RAF_INFO_SIZE = 160;
      private static final int RAF_JPEG_LENGTH_VALUE_SIZE = 4;
  
      private static final byte[] HEIF_TYPE_FTYP = new byte[] {'f', 't', 'y', 'p'};
      private static final byte[] HEIF_BRAND_MIF1 = new byte[] {'m', 'i', 'f', '1'};
      private static final byte[] HEIF_BRAND_HEIC = new byte[] {'h', 'e', 'i', 'c'};
  
      // See http://fileformats.archiveteam.org/wiki/Olympus_ORF
      private static final short ORF_SIGNATURE_1 = 0x4f52;
      private static final short ORF_SIGNATURE_2 = 0x5352;
      // There are two formats for Olympus Makernote Headers. Each has different identifiers and
      // offsets to the actual data.
      // See http://www.exiv2.org/makernote.html#R1
      private static final byte[] ORF_MAKER_NOTE_HEADER_1 = new byte[] {(byte) 0x4f, (byte) 0x4c,
              (byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x00}; // "OLYMP\0"
      private static final byte[] ORF_MAKER_NOTE_HEADER_2 = new byte[] {(byte) 0x4f, (byte) 0x4c,
              (byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x55, (byte) 0x53, (byte) 0x00,
              (byte) 0x49, (byte) 0x49}; // "OLYMPUS\0II"
      private static final int ORF_MAKER_NOTE_HEADER_1_SIZE = 8;
      private static final int ORF_MAKER_NOTE_HEADER_2_SIZE = 12;
  
      // See http://fileformats.archiveteam.org/wiki/RW2
      private static final short RW2_SIGNATURE = 0x0055;
  
      // See http://fileformats.archiveteam.org/wiki/Pentax_PEF
      private static final String PEF_SIGNATURE = "PENTAX";
      // See http://www.exiv2.org/makernote.html#R11
      private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6;
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private static SimpleDateFormat sFormatter;
      private static SimpleDateFormat sFormatterTz;
  
      // See Exchangeable image file format for digital still cameras: Exif version 2.2.
      // The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
      // They are called "Image File Directory". They have multiple data formats to cover various
      // image metadata from GPS longitude to camera model name.
  
      // Types of Exif byte alignments (see JEITA CP-3451C Section 4.5.2)
      private static final short BYTE_ALIGN_II = 0x4949;  // II: Intel order
      private static final short BYTE_ALIGN_MM = 0x4d4d;  // MM: Motorola order
  
      // TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2)
      private static final byte START_CODE = 0x2a; // 42
      private static final int IFD_OFFSET = 8;
  
      // Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
      private static final int IFD_FORMAT_BYTE = 1;
      private static final int IFD_FORMAT_STRING = 2;
      private static final int IFD_FORMAT_USHORT = 3;
      private static final int IFD_FORMAT_ULONG = 4;
      private static final int IFD_FORMAT_URATIONAL = 5;
      private static final int IFD_FORMAT_SBYTE = 6;
      private static final int IFD_FORMAT_UNDEFINED = 7;
      private static final int IFD_FORMAT_SSHORT = 8;
      private static final int IFD_FORMAT_SLONG = 9;
      private static final int IFD_FORMAT_SRATIONAL = 10;
      private static final int IFD_FORMAT_SINGLE = 11;
      private static final int IFD_FORMAT_DOUBLE = 12;
      // Format indicating a new IFD entry (See Adobe PageMaker® 6.0 TIFF Technical Notes, "New Tag")
      private static final int IFD_FORMAT_IFD = 13;
      // Names for the data formats for debugging purpose.
      private static final String[] IFD_FORMAT_NAMES = new String[] {
              "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
              "SLONG", "SRATIONAL", "SINGLE", "DOUBLE"
      };
      // Sizes of the components of each IFD value format
      private static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
              0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
      };
      private static final byte[] EXIF_ASCII_PREFIX = new byte[] {
              0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
      };
  
      /**
       * Constants used for Compression tag.
       * For Value 1, 2, 32773, see TIFF 6.0 Spec Section 3: Bilevel Images, Compression
       * For Value 6, see TIFF 6.0 Spec Section 22: JPEG Compression, Extensions to Existing Fields
       * For Value 7, 8, 34892, see DNG Specification 1.4.0.0. Section 3, Compression
       */
      private static final int DATA_UNCOMPRESSED = 1;
      private static final int DATA_HUFFMAN_COMPRESSED = 2;
      private static final int DATA_JPEG = 6;
      private static final int DATA_JPEG_COMPRESSED = 7;
      private static final int DATA_DEFLATE_ZIP = 8;
      private static final int DATA_PACK_BITS_COMPRESSED = 32773;
      private static final int DATA_LOSSY_JPEG = 34892;
  
      /**
       * Constants used for BitsPerSample tag.
       * For RGB, see TIFF 6.0 Spec Section 6, Differences from Palette Color Images
       * For Greyscale, see TIFF 6.0 Spec Section 4, Differences from Bilevel Images
       */
      private static final int[] BITS_PER_SAMPLE_RGB = new int[] { 8, 8, 8 };
      private static final int[] BITS_PER_SAMPLE_GREYSCALE_1 = new int[] { 4 };
      private static final int[] BITS_PER_SAMPLE_GREYSCALE_2 = new int[] { 8 };
  
      /**
       * Constants used for PhotometricInterpretation tag.
       * For White/Black, see Section 3, Color.
       * See TIFF 6.0 Spec Section 22, Minimum Requirements for TIFF with JPEG Compression.
       */
      private static final int PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO = 0;
      private static final int PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1;
      private static final int PHOTOMETRIC_INTERPRETATION_RGB = 2;
      private static final int PHOTOMETRIC_INTERPRETATION_YCBCR = 6;
  
      /**
       * Constants used for NewSubfileType tag.
       * See TIFF 6.0 Spec Section 8
       * */
      private static final int ORIGINAL_RESOLUTION_IMAGE = 0;
      private static final int REDUCED_RESOLUTION_IMAGE = 1;
  
      // A class for indicating EXIF rational type.
      private static class Rational {
          public final long numerator;
          public final long denominator;
  
          private Rational(long numerator, long denominator) {
              // Handle erroneous case
              if (denominator == 0) {
                  this.numerator = 0;
                  this.denominator = 1;
                  return;
              }
              this.numerator = numerator;
              this.denominator = denominator;
          }
  
          @Override
          public String toString() {
              return numerator + "/" + denominator;
          }
  
          public double calculate() {
              return (double) numerator / denominator;
          }
      }
  
      // A class for indicating EXIF attribute.
      private static class ExifAttribute {
          public final int format;
          public final int numberOfComponents;
          public final long bytesOffset;
          public final byte[] bytes;
  
          public static final long BYTES_OFFSET_UNKNOWN = -1;
  
          private ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
              this(format, numberOfComponents, BYTES_OFFSET_UNKNOWN, bytes);
          }
  
          private ExifAttribute(int format, int numberOfComponents, long bytesOffset, byte[] bytes) {
              this.format = format;
              this.numberOfComponents = numberOfComponents;
              this.bytesOffset = bytesOffset;
              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),
              new ExifTag(TAG_XMP, 700, IFD_FORMAT_BYTE),
      };
  
      // 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_OFFSET_TIME, 36880, IFD_FORMAT_STRING),
              new ExifTag(TAG_OFFSET_TIME_ORIGINAL, 36881, IFD_FORMAT_STRING),
              new ExifTag(TAG_OFFSET_TIME_DIGITIZED, 36882, IFD_FORMAT_STRING),
              new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_COMPRESSED_BITS_PER_PIXEL, 37122, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
              new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
              new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
              new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_SUBJECT_DISTANCE, 37382, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
              new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
              new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
              new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_SUBJECT_AREA, 37396, IFD_FORMAT_USHORT),
              new ExifTag(TAG_MAKER_NOTE, 37500, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_USER_COMMENT, 37510, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
              new ExifTag(TAG_SUBSEC_TIME_ORIG, 37521, IFD_FORMAT_STRING),
              new ExifTag(TAG_SUBSEC_TIME_DIG, 37522, IFD_FORMAT_STRING),
              new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
              new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_RELATED_SOUND_FILE, 40964, IFD_FORMAT_STRING),
              new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
              new ExifTag(TAG_FLASH_ENERGY, 41483, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_SPATIAL_FREQUENCY_RESPONSE, 41484, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_FOCAL_PLANE_X_RESOLUTION, 41486, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_FOCAL_PLANE_Y_RESOLUTION, 41487, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
              new ExifTag(TAG_SUBJECT_LOCATION, 41492, IFD_FORMAT_USHORT),
              new ExifTag(TAG_EXPOSURE_INDEX, 41493, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
              new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_CFA_PATTERN, 41730, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
              new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
              new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
              new ExifTag(TAG_DIGITAL_ZOOM_RATIO, 41988, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_FOCAL_LENGTH_IN_35MM_FILM, 41989, IFD_FORMAT_USHORT),
              new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
              new ExifTag(TAG_GAIN_CONTROL, 41991, IFD_FORMAT_USHORT),
              new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
              new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
              new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT),
              new ExifTag(TAG_DEVICE_SETTING_DESCRIPTION, 41995, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_SUBJECT_DISTANCE_RANGE, 41996, IFD_FORMAT_USHORT),
              new ExifTag(TAG_IMAGE_UNIQUE_ID, 42016, IFD_FORMAT_STRING),
              new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
              new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
      };
  
      // Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
      private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
              new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
              new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
              new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_SATELLITES, 8, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_STATUS, 9, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_MEASURE_MODE, 10, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DOP, 11, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_SPEED, 13, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_TRACK, 15, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_IMG_DIRECTION, 17, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_MAP_DATUM, 18, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DEST_LATITUDE_REF, 19, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DEST_LATITUDE, 20, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_DEST_LONGITUDE_REF, 21, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DEST_LONGITUDE, 22, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DEST_BEARING, 24, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DEST_DISTANCE, 26, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_GPS_PROCESSING_METHOD, 27, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_GPS_AREA_INFORMATION, 28, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_GPS_DATESTAMP, 29, IFD_FORMAT_STRING),
              new ExifTag(TAG_GPS_DIFFERENTIAL, 30, IFD_FORMAT_USHORT)
      };
      // Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
      private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
              new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
      };
      // IFD Thumbnail tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
      private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
              // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
              new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
              new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
              new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
              new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
              new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
              new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
              new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
              new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
              new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
              new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
              new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
              new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
              new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
              new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
              new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
              new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
              new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
              new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
              // See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
              new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
              new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
              new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
              new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
              new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
              new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
              new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
              new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
              new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
              new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
              new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
      };
  
      // RAF file tag (See piex.cc line 372)
      private static final ExifTag TAG_RAF_IMAGE_SIZE =
              new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT);
  
      // ORF file tags (See http://www.exiv2.org/tags-olympus.html)
      private static final ExifTag[] ORF_MAKER_NOTE_TAGS = new ExifTag[] {
              new ExifTag(TAG_ORF_THUMBNAIL_IMAGE, 256, IFD_FORMAT_UNDEFINED),
              new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_ULONG),
              new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_ULONG)
      };
      private static final ExifTag[] ORF_CAMERA_SETTINGS_TAGS = new ExifTag[] {
              new ExifTag(TAG_ORF_PREVIEW_IMAGE_START, 257, IFD_FORMAT_ULONG),
              new ExifTag(TAG_ORF_PREVIEW_IMAGE_LENGTH, 258, IFD_FORMAT_ULONG)
      };
      private static final ExifTag[] ORF_IMAGE_PROCESSING_TAGS = new ExifTag[] {
              new ExifTag(TAG_ORF_ASPECT_FRAME, 4371, IFD_FORMAT_USHORT)
      };
      // PEF file tag (See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Pentax.html)
      private static final ExifTag[] PEF_TAGS = new ExifTag[] {
              new ExifTag(TAG_COLOR_SPACE, 55, IFD_FORMAT_USHORT)
      };
  
      // See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
      // The following values are used for indicating pointers to the other Image File Directories.
  
      // Indices of Exif Ifd tag groups
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
              IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
              IFD_TYPE_ORF_CAMERA_SETTINGS, IFD_TYPE_ORF_IMAGE_PROCESSING, IFD_TYPE_PEF})
      public @interface IfdType {}
  
      private static final int IFD_TYPE_PRIMARY = 0;
      private static final int IFD_TYPE_EXIF = 1;
      private static final int IFD_TYPE_GPS = 2;
      private static final int IFD_TYPE_INTEROPERABILITY = 3;
      private static final int IFD_TYPE_THUMBNAIL = 4;
      private static final int IFD_TYPE_PREVIEW = 5;
      private static final int IFD_TYPE_ORF_MAKER_NOTE = 6;
      private static final int IFD_TYPE_ORF_CAMERA_SETTINGS = 7;
      private static final int IFD_TYPE_ORF_IMAGE_PROCESSING = 8;
      private static final int IFD_TYPE_PEF = 9;
  
      // List of Exif tag groups
      private static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
              IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
              IFD_THUMBNAIL_TAGS, IFD_TIFF_TAGS, ORF_MAKER_NOTE_TAGS, ORF_CAMERA_SETTINGS_TAGS,
              ORF_IMAGE_PROCESSING_TAGS, PEF_TAGS
      };
      // List of tags for pointing to the other image file directory offset.
      private static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[] {
              new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
              new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
              new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
              new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
              new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_BYTE),
              new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_BYTE)
      };
  
      // Tags for indicating the thumbnail offset and length
      private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
              new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG);
      private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
              new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG);
  
      // Mappings from tag number to tag name and each item represents one IFD tag group.
      private static final HashMap[] sExifTagMapsForReading = new HashMap[EXIF_TAGS.length];
      // Mappings from tag name to tag number and each item represents one IFD tag group.
      private static final HashMap[] sExifTagMapsForWriting = new HashMap[EXIF_TAGS.length];
      private static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
              TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE,
              TAG_GPS_TIMESTAMP));
      // Mappings from tag number to IFD type for pointer tags.
      private static final HashMap<Integer, Integer> sExifPointerTagMap = new HashMap();
  
      // See JPEG File Interchange Format Version 1.02.
      // The following values are defined for handling JPEG streams. In this implementation, we are
      // not only getting information from EXIF but also from some JPEG special segments such as
      // MARKER_COM for user comment and MARKER_SOFx for image width and height.
  
      private static final Charset ASCII = Charset.forName("US-ASCII");
      // Identifier for EXIF APP1 segment in JPEG
      private static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(ASCII);
      // Identifier for XMP APP1 segment in JPEG
      private static final byte[] IDENTIFIER_XMP_APP1 = "http://ns.adobe.com/xap/1.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"));
          sFormatterTz = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss XXX");
          sFormatterTz.setTimeZone(TimeZone.getTimeZone("UTC"));
  
          // Build up the hash tables to look up Exif tags for reading Exif tags.
          for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
              sExifTagMapsForReading[ifdType] = new HashMap();
              sExifTagMapsForWriting[ifdType] = new HashMap();
              for (ExifTag tag : EXIF_TAGS[ifdType]) {
                  sExifTagMapsForReading[ifdType].put(tag.number, tag);
                  sExifTagMapsForWriting[ifdType].put(tag.name, tag);
              }
          }
  
          // Build up the hash table to look up Exif pointer tags.
          sExifPointerTagMap.put(EXIF_POINTER_TAGS[0].number, IFD_TYPE_PREVIEW); // 330
          sExifPointerTagMap.put(EXIF_POINTER_TAGS[1].number, IFD_TYPE_EXIF); // 34665
          sExifPointerTagMap.put(EXIF_POINTER_TAGS[2].number, IFD_TYPE_GPS); // 34853
          sExifPointerTagMap.put(EXIF_POINTER_TAGS[3].number, IFD_TYPE_INTEROPERABILITY); // 40965
          sExifPointerTagMap.put(EXIF_POINTER_TAGS[4].number, IFD_TYPE_ORF_CAMERA_SETTINGS); // 8224
          sExifPointerTagMap.put(EXIF_POINTER_TAGS[5].number, IFD_TYPE_ORF_IMAGE_PROCESSING); // 8256
      }
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private String mFilename;
      private FileDescriptor mSeekableFileDescriptor;
      private AssetManager.AssetInputStream mAssetInputStream;
      private boolean mIsInputStream;
      private int mMimeType;
      @UnsupportedAppUsage
      private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length];
      private Set<Integer> mHandledIfdOffsets = new HashSet<>(EXIF_TAGS.length);
      private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN;
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private boolean mHasThumbnail;
      private boolean mHasThumbnailStrips;
      private boolean mAreThumbnailStripsConsecutive;
      // Used to indicate the position of the thumbnail (includes offset to EXIF data segment).
      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;
      private boolean mModified;
  
      // Pattern to check non zero timestamp
      private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
      // Pattern to check gps timestamp
      private static final Pattern sGpsTimestampPattern =
              Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$");
  
      /**
       * Reads Exif tags from the specified image file.
       */
      public ExifInterface(@NonNull File file) throws IOException {
          if (file == null) {
              throw new NullPointerException("file cannot be null");
          }
          initForFilename(file.getAbsolutePath());
      }
  
      /**
       * Reads Exif tags from the specified image file.
       */
      public ExifInterface(@NonNull String filename) throws IOException {
          if (filename == null) {
              throw new NullPointerException("filename cannot be null");
          }
          initForFilename(filename);
      }
  
      /**
       * 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(@NonNull FileDescriptor fileDescriptor) throws IOException {
          if (fileDescriptor == null) {
              throw new NullPointerException("fileDescriptor cannot be null");
          }
          mAssetInputStream = null;
          mFilename = null;
          // When FileDescriptor is duplicated and set to FileInputStream, ownership needs to be
          // clarified in order for garbage collection to take place.
          boolean isFdOwner = false;
          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);
                  isFdOwner = true;
              } catch (ErrnoException e) {
                  throw e.rethrowAsIOException();
              }
          } else {
              mSeekableFileDescriptor = null;
          }
          mIsInputStream = false;
          FileInputStream in = null;
          try {
              in = new FileInputStream(fileDescriptor, isFdOwner);
              loadAttributes(in);
          } finally {
              IoUtils.closeQuietly(in);
          }
      }
  
      /**
       * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
       * for input streams. The given input stream will proceed its current position. Developers
       * should close the input stream after use.
       */
      public ExifInterface(@NonNull InputStream inputStream) throws IOException {
          if (inputStream == null) {
              throw new NullPointerException("inputStream cannot be null");
          }
          mFilename = null;
          if (inputStream instanceof AssetManager.AssetInputStream) {
              mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
              mSeekableFileDescriptor = null;
          } else if (inputStream instanceof FileInputStream
                  && isSeekableFD(((FileInputStream) inputStream).getFD())) {
              mAssetInputStream = null;
              mSeekableFileDescriptor = ((FileInputStream) inputStream).getFD();
          } else {
              mAssetInputStream = null;
              mSeekableFileDescriptor = null;
          }
          mIsInputStream = true;
          loadAttributes(inputStream);
      }
  
      /**
       * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag in
       * the image file.
       *
       * @param tag the name of the tag.
       */
      private @Nullable ExifAttribute getExifAttribute(@NonNull String tag) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          // Retrieves all tag groups. The value from primary image tag group has a higher priority
          // than the value from the thumbnail tag group if there are more than one candidates.
          for (int i = 0; i < EXIF_TAGS.length; ++i) {
              Object value = mAttributes[i].get(tag);
              if (value != null) {
                  return (ExifAttribute) value;
              }
          }
          return null;
      }
  
      /**
       * Returns the value of the specified tag or {@code null} if there
       * is no such tag in the image file.
       *
       * @param tag the name of the tag.
       */
      public @Nullable String getAttribute(@NonNull String tag) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          ExifAttribute attribute = getExifAttribute(tag);
          if (attribute != null) {
              if (!sTagSetForCompatibility.contains(tag)) {
                  return attribute.getStringValue(mExifByteOrder);
              }
              if (tag.equals(TAG_GPS_TIMESTAMP)) {
                  // Convert the rational values to the custom formats for backwards compatibility.
                  if (attribute.format != IFD_FORMAT_URATIONAL
                          && attribute.format != IFD_FORMAT_SRATIONAL) {
                      return null;
                  }
                  Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
                  if (array.length != 3) {
                      return null;
                  }
                  return String.format("%02d:%02d:%02d",
                          (int) ((float) array[0].numerator / array[0].denominator),
                          (int) ((float) array[1].numerator / array[1].denominator),
                          (int) ((float) array[2].numerator / array[2].denominator));
              }
              try {
                  return Double.toString(attribute.getDoubleValue(mExifByteOrder));
              } catch (NumberFormatException e) {
                  return null;
              }
          }
          return null;
      }
  
      /**
       * Returns the integer value of the specified tag. If there is no such tag
       * in the image file or the value cannot be parsed as integer, return
       * <var>defaultValue</var>.
       *
       * @param tag the name of the tag.
       * @param defaultValue the value to return if the tag is not available.
       */
      public int getAttributeInt(@NonNull String tag, int defaultValue) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          ExifAttribute exifAttribute = getExifAttribute(tag);
          if (exifAttribute == null) {
              return defaultValue;
          }
  
          try {
              return exifAttribute.getIntValue(mExifByteOrder);
          } catch (NumberFormatException e) {
              return defaultValue;
          }
      }
  
      /**
       * Returns the double value of the tag that is specified as rational or contains a
       * double-formatted value. If there is no such tag in the image file or the value cannot be
       * parsed as double, return <var>defaultValue</var>.
       *
       * @param tag the name of the tag.
       * @param defaultValue the value to return if the tag is not available.
       */
      public double getAttributeDouble(@NonNull String tag, double defaultValue) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          ExifAttribute exifAttribute = getExifAttribute(tag);
          if (exifAttribute == null) {
              return defaultValue;
          }
  
          try {
              return exifAttribute.getDoubleValue(mExifByteOrder);
          } catch (NumberFormatException e) {
              return defaultValue;
          }
      }
  
      /**
       * Set the value of the specified tag.
       *
       * @param tag the name of the tag.
       * @param value the value of the tag.
       */
      public void setAttribute(@NonNull String tag, @Nullable String value) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          // 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 {
                      if (DEBUG) {
                          Log.d(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:
                          if (DEBUG) {
                              Log.d(TAG, "Data format isn't one of expected formats: " + dataFormat);
                          }
                          continue;
                  }
              }
          }
      }
  
      /**
       * Update the values of the tags in the tag groups if any value for the tag already was stored.
       *
       * @param tag the name of the tag.
       * @param value the value of the tag in a form of {@link ExifAttribute}.
       * @return Returns {@code true} if updating is placed.
       */
      private boolean updateAttribute(String tag, ExifAttribute value) {
          boolean updated = false;
          for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
              if (mAttributes[i].containsKey(tag)) {
                  mAttributes[i].put(tag, value);
                  updated = true;
              }
          }
          return updated;
      }
  
      /**
       * Remove any values of the specified tag.
       *
       * @param tag the name of the tag.
       */
      private void removeAttribute(String tag) {
          for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
              mAttributes[i].remove(tag);
          }
      }
  
      /**
       * This function decides which parser to read the image data according to the given input stream
       * type and the content of the input stream. In each case, it reads the first three bytes to
       * determine whether the image data format is JPEG or not.
       */
      private void loadAttributes(@NonNull InputStream in) throws IOException {
          if (in == null) {
              throw new NullPointerException("inputstream shouldn't be null");
          }
          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 | OutOfMemoryError e) {
              // Ignore exceptions in order to keep the compatibility with the old versions of
              // ExifInterface.
              mIsSupportedFile = false;
              Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file"
                      + "(ExifInterface supports JPEG and some RAW image formats only) "
                      + "or a corrupted JPEG file to ExifInterface.", e);
          } finally {
              addDefaultValuesForCompatibility();
  
              if (DEBUG) {
                  printAttributes();
              }
          }
      }
  
      private static boolean isSeekableFD(FileDescriptor fd) throws IOException {
          try {
              Os.lseek(fd, 0, OsConstants.SEEK_CUR);
              return true;
          } catch (ErrnoException e) {
              return false;
          }
      }
  
      // Prints out attributes for debugging.
      private void printAttributes() {
          for (int i = 0; i < mAttributes.length; ++i) {
              Log.d(TAG, "The size of tag group[" + i + "]: " + mAttributes[i].size());
              for (Map.Entry entry : (Set<Map.Entry>) mAttributes[i].entrySet()) {
                  final ExifAttribute tagValue = (ExifAttribute) entry.getValue();
                  Log.d(TAG, "tagName: " + entry.getKey() + ", tagType: " + tagValue.toString()
                          + ", tagValue: '" + tagValue.getStringValue(mExifByteOrder) + "'");
              }
          }
      }
  
      /**
       * Save the tag data into the original image file. This is expensive because
       * it involves copying all the data from one file to another and deleting
       * the old file and renaming the other. It's best to use
       * {@link #setAttribute(String,String)} to set all attributes to write and
       * make a single call rather than multiple calls for each attribute.
       * <p>
       * This method is only supported for JPEG files.
       * <p class="note">
       * Note: after calling this method, any attempts to obtain range information
       * from {@link #getAttributeRange(String)} or {@link #getThumbnailRange()}
       * will throw {@link IllegalStateException}, since the offsets may have
       * changed in the newly written file.
       * </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.");
          }
  
          // Remember the fact that we've changed the file on disk from what was
          // originally parsed, meaning we can't answer range questions
          mModified = true;
  
          // Keep the thumbnail in memory
          mThumbnailBytes = getThumbnail();
  
          FileInputStream in = null;
          FileOutputStream out = null;
          File tempFile = null;
          try {
              // Move the original file to temporary file.
              if (mFilename != null) {
                  tempFile = new File(mFilename + ".tmp");
                  File originalFile = new File(mFilename);
                  if (!originalFile.renameTo(tempFile)) {
                      throw new IOException("Could'nt rename to " + tempFile.getAbsolutePath());
                  }
              } else if (mSeekableFileDescriptor != null) {
                  tempFile = File.createTempFile("temp", "jpg");
                  Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
                  in = new FileInputStream(mSeekableFileDescriptor);
                  out = new FileOutputStream(tempFile);
                  Streams.copy(in, out);
              }
          } catch (ErrnoException e) {
              throw e.rethrowAsIOException();
          } finally {
              IoUtils.closeQuietly(in);
              IoUtils.closeQuietly(out);
          }
  
          in = null;
          out = null;
          try {
              // Save the new file.
              in = new FileInputStream(tempFile);
              if (mFilename != null) {
                  out = new FileOutputStream(mFilename);
              } else if (mSeekableFileDescriptor != null) {
                  Os.lseek(mSeekableFileDescriptor, 0, OsConstants.SEEK_SET);
                  out = new FileOutputStream(mSeekableFileDescriptor);
              }
              saveJpegAttributes(in, out);
          } catch (ErrnoException e) {
              throw e.rethrowAsIOException();
          } finally {
              IoUtils.closeQuietly(in);
              IoUtils.closeQuietly(out);
              tempFile.delete();
          }
  
          // Discard the thumbnail in memory
          mThumbnailBytes = null;
      }
  
      /**
       * Returns true if the image file has a thumbnail.
       */
      public boolean hasThumbnail() {
          return mHasThumbnail;
      }
  
      /**
       * Returns true if the image file has the given attribute defined.
       *
       * @param tag the name of the tag.
       */
      public boolean hasAttribute(@NonNull String tag) {
          return (getExifAttribute(tag) != null);
      }
  
      /**
       * Returns the JPEG compressed thumbnail inside the image file, or {@code null} if there is no
       * JPEG compressed thumbnail.
       * The returned data can be decoded using
       * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
       */
      public byte[] getThumbnail() {
          if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
              return getThumbnailBytes();
          }
          return null;
      }
  
      /**
       * Returns the thumbnail bytes inside the image file, regardless of the compression type of the
       * thumbnail image.
       */
      public byte[] getThumbnailBytes() {
          if (!mHasThumbnail) {
              return null;
          }
          if (mThumbnailBytes != null) {
              return mThumbnailBytes;
          }
  
          // Read the thumbnail.
          InputStream in = null;
          try {
              if (mAssetInputStream != null) {
                  in = mAssetInputStream;
                  if (in.markSupported()) {
                      in.reset();
                  } else {
                      Log.d(TAG, "Cannot read thumbnail from inputstream without mark/reset support");
                      return null;
                  }
              } else if (mFilename != null) {
                  in = new FileInputStream(mFilename);
              } else if (mSeekableFileDescriptor != null) {
                  FileDescriptor fileDescriptor = Os.dup(mSeekableFileDescriptor);
                  Os.lseek(fileDescriptor, 0, OsConstants.SEEK_SET);
                  in = new FileInputStream(fileDescriptor, true);
              }
              if (in == null) {
                  // Should not be reached this.
                  throw new FileNotFoundException();
              }
              if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
                  throw new IOException("Corrupted image");
              }
              byte[] buffer = new byte[mThumbnailLength];
              if (in.read(buffer) != mThumbnailLength) {
                  throw new IOException("Corrupted image");
              }
              mThumbnailBytes = buffer;
              return buffer;
          } catch (IOException | ErrnoException e) {
              // Couldn't get a thumbnail image.
              Log.d(TAG, "Encountered exception while getting thumbnail", e);
          } finally {
              IoUtils.closeQuietly(in);
          }
          return null;
      }
  
      /**
       * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the
       * thumbnail compression value, or {@code null} if the compression type is unsupported.
       */
      public Bitmap getThumbnailBitmap() {
          if (!mHasThumbnail) {
              return null;
          } else if (mThumbnailBytes == null) {
              mThumbnailBytes = getThumbnailBytes();
          }
  
          if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
              return BitmapFactory.decodeByteArray(mThumbnailBytes, 0, mThumbnailLength);
          } else if (mThumbnailCompression == DATA_UNCOMPRESSED) {
              int[] rgbValues = new int[mThumbnailBytes.length / 3];
              byte alpha = (byte) 0xff000000;
              for (int i = 0; i < rgbValues.length; i++) {
                  rgbValues[i] = alpha + (mThumbnailBytes[3 * i] << 16)
                          + (mThumbnailBytes[3 * i + 1] << 8) + mThumbnailBytes[3 * i + 2];
              }
  
              ExifAttribute imageLengthAttribute =
                      (ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_IMAGE_LENGTH);
              ExifAttribute imageWidthAttribute =
                      (ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_IMAGE_WIDTH);
              if (imageLengthAttribute != null && imageWidthAttribute != null) {
                  int imageLength = imageLengthAttribute.getIntValue(mExifByteOrder);
                  int imageWidth = imageWidthAttribute.getIntValue(mExifByteOrder);
                  return Bitmap.createBitmap(
                          rgbValues, imageWidth, imageLength, Bitmap.Config.ARGB_8888);
              }
          }
          return null;
      }
  
      /**
       * Returns true if thumbnail image is JPEG Compressed, or false if either thumbnail image does
       * not exist or thumbnail image is uncompressed.
       */
      public boolean isThumbnailCompressed() {
          if (!mHasThumbnail) {
              return false;
          }
          if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
              return true;
          }
          return false;
      }
  
      /**
       * Returns the offset and length of thumbnail inside the image file, or
       * {@code null} if either there is no thumbnail or the thumbnail bytes are stored
       * non-consecutively.
       *
       * @return two-element array, the offset in the first value, and length in
       *         the second, or {@code null} if no thumbnail was found or the thumbnail strips are
       *         not placed consecutively.
       * @throws IllegalStateException if {@link #saveAttributes()} has been
       *             called since the underlying file was initially parsed, since
       *             that means offsets may have changed.
       */
      public @Nullable long[] getThumbnailRange() {
          if (mModified) {
              throw new IllegalStateException(
                      "The underlying file has been modified since being parsed");
          }
  
          if (mHasThumbnail) {
              if (mHasThumbnailStrips && !mAreThumbnailStripsConsecutive) {
                  return null;
              }
              return new long[] { mThumbnailOffset, mThumbnailLength };
          }
          return null;
      }
  
      /**
       * Returns the offset and length of the requested tag inside the image file,
       * or {@code null} if the tag is not contained.
       *
       * @return two-element array, the offset in the first value, and length in
       *         the second, or {@code null} if no tag was found.
       * @throws IllegalStateException if {@link #saveAttributes()} has been
       *             called since the underlying file was initially parsed, since
       *             that means offsets may have changed.
       */
      public @Nullable long[] getAttributeRange(@NonNull String tag) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          if (mModified) {
              throw new IllegalStateException(
                      "The underlying file has been modified since being parsed");
          }
  
          final ExifAttribute attribute = getExifAttribute(tag);
          if (attribute != null) {
              return new long[] { attribute.bytesOffset, attribute.bytes.length };
          } else {
              return null;
          }
      }
  
      /**
       * Returns the raw bytes for the value of the requested tag inside the image
       * file, or {@code null} if the tag is not contained.
       *
       * @return raw bytes for the value of the requested tag, or {@code null} if
       *         no tag was found.
       */
      public @Nullable byte[] getAttributeBytes(@NonNull String tag) {
          if (tag == null) {
              throw new NullPointerException("tag shouldn't be null");
          }
          final ExifAttribute attribute = getExifAttribute(tag);
          if (attribute != null) {
              return attribute.bytes;
          } else {
              return null;
          }
      }
  
      /**
       * Stores the latitude and longitude value in a float array. The first element is
       * the latitude, and the second element is the longitude. Returns false if the
       * Exif tags are not available.
       */
      public boolean getLatLong(float output[]) {
          String latValue = getAttribute(TAG_GPS_LATITUDE);
          String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
          String lngValue = getAttribute(TAG_GPS_LONGITUDE);
          String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF);
  
          if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
              try {
                  output[0] = convertRationalLatLonToFloat(latValue, latRef);
                  output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
                  return true;
              } catch (IllegalArgumentException e) {
                  // if values are not parseable
              }
          }
  
          return false;
      }
  
      /**
       * Return the altitude in meters. If the exif tag does not exist, return
       * <var>defaultValue</var>.
       *
       * @param defaultValue the value to return if the tag is not available.
       */
      public double getAltitude(double defaultValue) {
          double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
          int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);
  
          if (altitude >= 0 && ref >= 0) {
              return (altitude * ((ref == 1) ? -1 : 1));
          } else {
              return defaultValue;
          }
      }
  
      /**
       * Returns parsed {@code DateTime} value, or -1 if unavailable or invalid.
       * 
       * @hide
       */
      @UnsupportedAppUsage
      public @CurrentTimeMillisLong long getDateTime() {
          return parseDateTime(getAttribute(TAG_DATETIME),
                  getAttribute(TAG_SUBSEC_TIME),
                  getAttribute(TAG_OFFSET_TIME));
      }
  
      /**
       * Returns parsed {@code DateTimeDigitized} value, or -1 if unavailable or
       * invalid.
       *
       * @hide
       */
      public @CurrentTimeMillisLong long getDateTimeDigitized() {
          return parseDateTime(getAttribute(TAG_DATETIME_DIGITIZED),
                  getAttribute(TAG_SUBSEC_TIME_DIGITIZED),
                  getAttribute(TAG_OFFSET_TIME_DIGITIZED));
      }
  
      /**
       * Returns parsed {@code DateTimeOriginal} value, or -1 if unavailable or
       * invalid.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public @CurrentTimeMillisLong long getDateTimeOriginal() {
          return parseDateTime(getAttribute(TAG_DATETIME_ORIGINAL),
                  getAttribute(TAG_SUBSEC_TIME_ORIGINAL),
                  getAttribute(TAG_OFFSET_TIME_ORIGINAL));
      }
  
      private static @CurrentTimeMillisLong long parseDateTime(@Nullable String dateTimeString,
              @Nullable String subSecs, @Nullable String offsetString) {
          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 (offsetString != null) {
                  dateTimeString = dateTimeString + " " + offsetString;
                  ParsePosition position = new ParsePosition(0);
                  datetime = sFormatterTz.parse(dateTimeString, position);
              }
  
              if (datetime == null) return -1;
              long msecs = datetime.getTime();
  
              if (subSecs != null) {
                  try {
                      long sub = Long.parseLong(subSecs);
                      while (sub > 1000) {
                          sub /= 10;
                      }
                      msecs += sub;
                  } catch (NumberFormatException e) {
                      // Ignored
                  }
              }
              return msecs;
          } catch (IllegalArgumentException e) {
              return -1;
          }
      }
  
      /**
       * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
       * Returns -1 if the date time information if not available.
       * @hide
       */
      @UnsupportedAppUsage
      public long getGpsDateTime() {
          String date = getAttribute(TAG_GPS_DATESTAMP);
          String time = getAttribute(TAG_GPS_TIMESTAMP);
          if (date == null || time == null
                  || (!sNonZeroTimePattern.matcher(date).matches()
                  && !sNonZeroTimePattern.matcher(time).matches())) {
              return -1;
          }
  
          String dateTimeString = date + ' ' + time;
  
          ParsePosition pos = new ParsePosition(0);
          try {
              Date datetime = sFormatter.parse(dateTimeString, pos);
              if (datetime == null) return -1;
              return datetime.getTime();
          } catch (IllegalArgumentException e) {
              return -1;
          }
      }
  
      /** {@hide} */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      public static float convertRationalLatLonToFloat(String rationalString, String ref) {
          try {
              String [] parts = rationalString.split(",");
  
              String [] pair;
              pair = parts[0].split("/");
              double degrees = Double.parseDouble(pair[0].trim())
                      / Double.parseDouble(pair[1].trim());
  
              pair = parts[1].split("/");
              double minutes = Double.parseDouble(pair[0].trim())
                      / Double.parseDouble(pair[1].trim());
  
              pair = parts[2].split("/");
              double seconds = Double.parseDouble(pair[0].trim())
                      / Double.parseDouble(pair[1].trim());
  
              double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
              if ((ref.equals("S") || ref.equals("W"))) {
                  return (float) -result;
              }
              return (float) result;
          } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
              // Not valid
              throw new IllegalArgumentException();
          }
      }
  
      private void initForFilename(String filename) throws IOException {
          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);
          }
      }
  
      // Checks the type of image file
      private int getMimeType(BufferedInputStream in) throws IOException {
          in.mark(SIGNATURE_CHECK_SIZE);
          byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE];
          in.read(signatureCheckBytes);
          in.reset();
          if (isJpegFormat(signatureCheckBytes)) {
              return IMAGE_TYPE_JPEG;
          } else if (isRafFormat(signatureCheckBytes)) {
              return IMAGE_TYPE_RAF;
          } else if (isHeifFormat(signatureCheckBytes)) {
              return IMAGE_TYPE_HEIF;
          } else if (isOrfFormat(signatureCheckBytes)) {
              return IMAGE_TYPE_ORF;
          } else if (isRw2Format(signatureCheckBytes)) {
              return IMAGE_TYPE_RW2;
          }
          // Certain file formats (PEF) are identified in readImageFileDirectory()
          return IMAGE_TYPE_UNKNOWN;
      }
  
      /**
       * This method looks at the first 3 bytes to determine if this file is a JPEG file.
       * See http://www.media.mit.edu/pia/Research/deepview/exif.html, "JPEG format and Marker"
       */
      private static boolean isJpegFormat(byte[] signatureCheckBytes) throws IOException {
          for (int i = 0; i < JPEG_SIGNATURE.length; i++) {
              if (signatureCheckBytes[i] != JPEG_SIGNATURE[i]) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * This method looks at the first 15 bytes to determine if this file is a RAF file.
       * There is no official specification for RAF files from Fuji, but there is an online archive of
       * image file specifications:
       * http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
       */
      private boolean isRafFormat(byte[] signatureCheckBytes) throws IOException {
          byte[] rafSignatureBytes = RAF_SIGNATURE.getBytes();
          for (int i = 0; i < rafSignatureBytes.length; i++) {
              if (signatureCheckBytes[i] != rafSignatureBytes[i]) {
                  return false;
              }
          }
          return true;
      }
  
      private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
          ByteOrderedDataInputStream signatureInputStream = null;
          try {
              signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
              signatureInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
  
              long chunkSize = signatureInputStream.readInt();
              byte[] chunkType = new byte[4];
              signatureInputStream.read(chunkType);
  
              if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) {
                  return false;
              }
  
              long chunkDataOffset = 8;
              if (chunkSize == 1) {
                  // This indicates that the next 8 bytes represent the chunk size,
                  // and chunk data comes after that.
                  chunkSize = signatureInputStream.readLong();
                  if (chunkSize < 16) {
                      // The smallest valid chunk is 16 bytes long in this case.
                      return false;
                  }
                  chunkDataOffset += 8;
              }
  
              // only sniff up to signatureCheckBytes.length
              if (chunkSize > signatureCheckBytes.length) {
                  chunkSize = signatureCheckBytes.length;
              }
  
              long chunkDataSize = chunkSize - chunkDataOffset;
  
              // It should at least have major brand (4-byte) and minor version (4-byte).
              // The rest of the chunk (if any) is a list of (4-byte) compatible brands.
              if (chunkDataSize < 8) {
                  return false;
              }
  
              byte[] brand = new byte[4];
              boolean isMif1 = false;
              boolean isHeic = false;
              for (long i = 0; i < chunkDataSize / 4;  ++i) {
                  if (signatureInputStream.read(brand) != brand.length) {
                      return false;
                  }
                  if (i == 1) {
                      // Skip this index, it refers to the minorVersion, not a brand.
                      continue;
                  }
                  if (Arrays.equals(brand, HEIF_BRAND_MIF1)) {
                      isMif1 = true;
                  } else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) {
                      isHeic = true;
                  }
                  if (isMif1 && isHeic) {
                      return true;
                  }
              }
          } catch (Exception e) {
              if (DEBUG) {
                  Log.d(TAG, "Exception parsing HEIF file type box.", e);
              }
          } finally {
              if (signatureInputStream != null) {
                  signatureInputStream.close();
                  signatureInputStream = null;
              }
          }
          return false;
      }
  
      /**
       * ORF has a similar structure to TIFF but it contains a different signature at the TIFF Header.
       * This method looks at the 2 bytes following the Byte Order bytes to determine if this file is
       * an ORF file.
       * There is no official specification for ORF files from Olympus, but there is an online archive
       * of image file specifications:
       * http://fileformats.archiveteam.org/wiki/Olympus_ORF
       */
      private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException {
          ByteOrderedDataInputStream signatureInputStream =
                  new ByteOrderedDataInputStream(signatureCheckBytes);
          // Read byte order
          mExifByteOrder = readByteOrder(signatureInputStream);
          // Set byte order
          signatureInputStream.setByteOrder(mExifByteOrder);
  
          short orfSignature = signatureInputStream.readShort();
          if (orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2) {
              return true;
          }
          return false;
      }
  
      /**
       * RW2 is TIFF-based, but stores 0x55 signature byte instead of 0x42 at the header
       * See http://lclevy.free.fr/raw/
       */
      private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException {
          ByteOrderedDataInputStream signatureInputStream =
                  new ByteOrderedDataInputStream(signatureCheckBytes);
          // Read byte order
          mExifByteOrder = readByteOrder(signatureInputStream);
          // Set byte order
          signatureInputStream.setByteOrder(mExifByteOrder);
  
          short signatureByte = signatureInputStream.readShort();
          if (signatureByte == RW2_SIGNATURE) {
              return true;
          }
          return false;
      }
  
      /**
       * Loads EXIF attributes from a JPEG input stream.
       *
       * @param in The input stream that starts with the JPEG data.
       * @param jpegOffset The offset value in input stream for JPEG data.
       * @param imageType The image type from which to retrieve metadata. Use IFD_TYPE_PRIMARY for
       *                   primary image, IFD_TYPE_PREVIEW for preview image, and
       *                   IFD_TYPE_THUMBNAIL for thumbnail image.
       * @throws IOException If the data contains invalid JPEG markers, offsets, or length values.
       */
      private void getJpegAttributes(ByteOrderedDataInputStream in, int jpegOffset, int imageType)
              throws IOException {
          // See JPEG File Interchange Format Specification, "JFIF Specification"
          if (DEBUG) {
              Log.d(TAG, "getJpegAttributes starting with: " + in);
          }
  
          // JPEG uses Big Endian by default. See https://people.cs.umass.edu/~verts/cs32/endian.html
          in.setByteOrder(ByteOrder.BIG_ENDIAN);
  
          // Skip to JPEG data
          in.seek(jpegOffset);
          int bytesRead = jpegOffset;
  
          byte marker;
          if ((marker = in.readByte()) != MARKER) {
              throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
          }
          ++bytesRead;
          if (in.readByte() != MARKER_SOI) {
              throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
          }
          ++bytesRead;
          while (true) {
              marker = in.readByte();
              if (marker != MARKER) {
                  throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
              }
              ++bytesRead;
              marker = in.readByte();
              if (DEBUG) {
                  Log.d(TAG, "Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
              }
              ++bytesRead;
  
              // EOI indicates the end of an image and in case of SOS, JPEG image stream starts and
              // the image data will terminate right after.
              if (marker == MARKER_EOI || marker == MARKER_SOS) {
                  break;
              }
              int length = in.readUnsignedShort() - 2;
              bytesRead += 2;
              if (DEBUG) {
                  Log.d(TAG, "JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: "
                          + (length + 2) + ")");
              }
              if (length < 0) {
                  throw new IOException("Invalid length");
              }
              switch (marker) {
                  case MARKER_APP1: {
                      final int start = bytesRead;
                      final byte[] bytes = new byte[length];
                      in.readFully(bytes);
                      bytesRead += length;
                      length = 0;
  
                      if (ArrayUtils.startsWith(bytes, IDENTIFIER_EXIF_APP1)) {
                          final long offset = start + IDENTIFIER_EXIF_APP1.length;
                          final byte[] value = Arrays.copyOfRange(bytes,
                                  IDENTIFIER_EXIF_APP1.length, bytes.length);
  
                          // Save offset values for createJpegThumbnailBitmap() function
                          mExifOffset = (int) offset;
                          readExifSegment(value, imageType);
                      } else if (ArrayUtils.startsWith(bytes, IDENTIFIER_XMP_APP1)) {
                          // See XMP Specification Part 3: Storage in Files, 1.1.3 JPEG, Table 6
                          final long offset = start + IDENTIFIER_XMP_APP1.length;
                          final byte[] value = Arrays.copyOfRange(bytes,
                                  IDENTIFIER_XMP_APP1.length, bytes.length);
  
                          if (getAttribute(TAG_XMP) == null) {
                              mAttributes[IFD_TYPE_PRIMARY].put(TAG_XMP, new ExifAttribute(
                                      IFD_FORMAT_BYTE, value.length, offset, value));
                          }
                      }
                      break;
                  }
  
                  case MARKER_COM: {
                      byte[] bytes = new byte[length];
                      if (in.read(bytes) != length) {
                          throw new IOException("Invalid exif");
                      }
                      length = 0;
                      if (getAttribute(TAG_USER_COMMENT) == null) {
                          mAttributes[IFD_TYPE_EXIF].put(TAG_USER_COMMENT, ExifAttribute.createString(
                                  new String(bytes, ASCII)));
                      }
                      break;
                  }
  
                  case MARKER_SOF0:
                  case MARKER_SOF1:
                  case MARKER_SOF2:
                  case MARKER_SOF3:
                  case MARKER_SOF5:
                  case MARKER_SOF6:
                  case MARKER_SOF7:
                  case MARKER_SOF9:
                  case MARKER_SOF10:
                  case MARKER_SOF11:
                  case MARKER_SOF13:
                  case MARKER_SOF14:
                  case MARKER_SOF15: {
                      if (in.skipBytes(1) != 1) {
                          throw new IOException("Invalid SOFx");
                      }
                      mAttributes[imageType].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong(
                              in.readUnsignedShort(), mExifByteOrder));
                      mAttributes[imageType].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong(
                              in.readUnsignedShort(), mExifByteOrder));
                      length -= 5;
                      break;
                  }
  
                  default: {
                      break;
                  }
              }
              if (length < 0) {
                  throw new IOException("Invalid length");
              }
              if (in.skipBytes(length) != length) {
                  throw new IOException("Invalid JPEG segment");
              }
              bytesRead += length;
          }
          // Restore original byte order
          in.setByteOrder(mExifByteOrder);
      }
  
      private void getRawAttributes(ByteOrderedDataInputStream in) throws IOException {
          // Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
          parseTiffHeaders(in, in.available());
  
          // Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
          readImageFileDirectory(in, IFD_TYPE_PRIMARY);
  
          // Update ImageLength/Width tags for all image data.
          updateImageSizeValues(in, IFD_TYPE_PRIMARY);
          updateImageSizeValues(in, IFD_TYPE_PREVIEW);
          updateImageSizeValues(in, IFD_TYPE_THUMBNAIL);
  
          // Check if each image data is in valid position.
          validateImages(in);
  
          if (mMimeType == IMAGE_TYPE_PEF) {
              // PEF files contain a MakerNote data, which contains the data for ColorSpace tag.
              // See http://lclevy.free.fr/raw/ and piex.cc PefGetPreviewData()
              ExifAttribute makerNoteAttribute =
                      (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
              if (makerNoteAttribute != null) {
                  // Create an ordered DataInputStream for MakerNote
                  ByteOrderedDataInputStream makerNoteDataInputStream =
                          new ByteOrderedDataInputStream(makerNoteAttribute.bytes);
                  makerNoteDataInputStream.setByteOrder(mExifByteOrder);
  
                  // Seek to MakerNote data
                  makerNoteDataInputStream.seek(PEF_MAKER_NOTE_SKIP_SIZE);
  
                  // Read IFD data from MakerNote
                  readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_PEF);
  
                  // Update ColorSpace tag
                  ExifAttribute colorSpaceAttribute =
                          (ExifAttribute) mAttributes[IFD_TYPE_PEF].get(TAG_COLOR_SPACE);
                  if (colorSpaceAttribute != null) {
                      mAttributes[IFD_TYPE_EXIF].put(TAG_COLOR_SPACE, colorSpaceAttribute);
                  }
              }
          }
      }
  
      /**
       * RAF files contains a JPEG and a CFA data.
       * The JPEG contains two images, a preview and a thumbnail, while the CFA contains a RAW image.
       * This method looks at the first 160 bytes of a RAF file to retrieve the offset and length
       * values for the JPEG and CFA data.
       * Using that data, it parses the JPEG data to retrieve the preview and thumbnail image data,
       * then parses the CFA metadata to retrieve the primary image length/width values.
       * For data format details, see http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
       */
      private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException {
          // Retrieve offset & length values
          in.skipBytes(RAF_OFFSET_TO_JPEG_IMAGE_OFFSET);
          byte[] jpegOffsetBytes = new byte[4];
          byte[] cfaHeaderOffsetBytes = new byte[4];
          in.read(jpegOffsetBytes);
          // Skip JPEG length value since it is not needed
          in.skipBytes(RAF_JPEG_LENGTH_VALUE_SIZE);
          in.read(cfaHeaderOffsetBytes);
          int rafJpegOffset = ByteBuffer.wrap(jpegOffsetBytes).getInt();
          int rafCfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt();
  
          // Retrieve JPEG image metadata
          getJpegAttributes(in, rafJpegOffset, IFD_TYPE_PREVIEW);
  
          // Skip to CFA header offset.
          in.seek(rafCfaHeaderOffset);
  
          // Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists
          in.setByteOrder(ByteOrder.BIG_ENDIAN);
          int numberOfDirectoryEntry = in.readInt();
          if (DEBUG) {
              Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry);
          }
          // CFA stores some metadata about the RAW image. Since CFA uses proprietary tags, can only
          // find and retrieve image size information tags, while skipping others.
          // See piex.cc RafGetDimension()
          for (int i = 0; i < numberOfDirectoryEntry; ++i) {
              int tagNumber = in.readUnsignedShort();
              int numberOfBytes = in.readUnsignedShort();
              if (tagNumber == TAG_RAF_IMAGE_SIZE.number) {
                  int imageLength = in.readShort();
                  int imageWidth = in.readShort();
                  ExifAttribute imageLengthAttribute =
                          ExifAttribute.createUShort(imageLength, mExifByteOrder);
                  ExifAttribute imageWidthAttribute =
                          ExifAttribute.createUShort(imageWidth, mExifByteOrder);
                  mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
                  mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
                  if (DEBUG) {
                      Log.d(TAG, "Updated to length: " + imageLength + ", width: " + imageWidth);
                  }
                  return;
              }
              in.skipBytes(numberOfBytes);
          }
      }
  
      private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException {
          MediaMetadataRetriever retriever = new MediaMetadataRetriever();
          try {
              retriever.setDataSource(new MediaDataSource() {
                  long mPosition;
  
                  @Override
                  public void close() throws IOException {}
  
                  @Override
                  public int readAt(long position, byte[] buffer, int offset, int size)
                          throws IOException {
                      if (size == 0) {
                          return 0;
                      }
                      if (position < 0) {
                          return -1;
                      }
                      try {
                          if (mPosition != position) {
                              // We don't allow seek to positions after the available bytes,
                              // the input stream won't be able to seek back then.
                              // However, if we hit an exception before (mPosition set to -1),
                              // let it try the seek in hope it might recover.
                              if (mPosition >= 0 && position >= mPosition + in.available()) {
                                  return -1;
                              }
                              in.seek(position);
                              mPosition = position;
                          }
  
                          // If the read will cause us to go over the available bytes,
                          // reduce the size so that we stay in the available range.
                          // Otherwise the input stream may not be able to seek back.
                          if (size > in.available()) {
                              size = in.available();
                          }
  
                          int bytesRead = in.read(buffer, offset, size);
                          if (bytesRead >= 0) {
                              mPosition += bytesRead;
                              return bytesRead;
                          }
                      } catch (IOException e) {}
                      mPosition = -1; // need to seek on next read
                      return -1;
                  }
  
                  @Override
                  public long getSize() throws IOException {
                      return -1;
                  }
              });
  
              String exifOffsetStr = retriever.extractMetadata(
                      MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET);
              String exifLengthStr = retriever.extractMetadata(
                      MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH);
              String hasImage = retriever.extractMetadata(
                      MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
              String hasVideo = retriever.extractMetadata(
                      MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
  
              String width = null;
              String height = null;
              String rotation = null;
              final String METADATA_VALUE_YES = "yes";
              // If the file has both image and video, prefer image info over video info.
              // App querying ExifInterface is most likely using the bitmap path which
              // picks the image first.
              if (METADATA_VALUE_YES.equals(hasImage)) {
                  width = retriever.extractMetadata(
                          MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
                  height = retriever.extractMetadata(
                          MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
                  rotation = retriever.extractMetadata(
                          MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
              } else if (METADATA_VALUE_YES.equals(hasVideo)) {
                  width = retriever.extractMetadata(
                          MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
                  height = retriever.extractMetadata(
                          MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
                  rotation = retriever.extractMetadata(
                          MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
              }
  
              if (width != null) {
                  mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
                          ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder));
              }
  
              if (height != null) {
                  mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
                          ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder));
              }
  
              if (rotation != null) {
                  int orientation = ExifInterface.ORIENTATION_NORMAL;
  
                  // all rotation angles in CW
                  switch (Integer.parseInt(rotation)) {
                      case 90:
                          orientation = ExifInterface.ORIENTATION_ROTATE_90;
                          break;
                      case 180:
                          orientation = ExifInterface.ORIENTATION_ROTATE_180;
                          break;
                      case 270:
                          orientation = ExifInterface.ORIENTATION_ROTATE_270;
                          break;
                  }
  
                  mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
                          ExifAttribute.createUShort(orientation, mExifByteOrder));
              }
  
              if (exifOffsetStr != null && exifLengthStr != null) {
                  int offset = Integer.parseInt(exifOffsetStr);
                  int length = Integer.parseInt(exifLengthStr);
                  if (length <= 6) {
                      throw new IOException("Invalid exif length");
                  }
                  in.seek(offset);
                  byte[] identifier = new byte[6];
                  if (in.read(identifier) != 6) {
                      throw new IOException("Can't read identifier");
                  }
                  offset += 6;
                  length -= 6;
                  if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
                      throw new IOException("Invalid identifier");
                  }
  
                  byte[] bytes = new byte[length];
                  if (in.read(bytes) != length) {
                      throw new IOException("Can't read exif");
                  }
                  // Save offset values for handling thumbnail and attribute offsets.
                  mExifOffset = offset;
                  readExifSegment(bytes, IFD_TYPE_PRIMARY);
              }
  
              if (DEBUG) {
                  Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation);
              }
          } finally {
              retriever.release();
          }
      }
  
      /**
       * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail
       * images. Both data takes the form of IFDs and can therefore be read with the
       * readImageFileDirectory() method.
       * This method reads all the necessary data and updates the primary/preview/thumbnail image
       * information according to the GetOlympusPreviewImage() method in piex.cc.
       * For data format details, see the following:
       * http://fileformats.archiveteam.org/wiki/Olympus_ORF
       * https://libopenraw.freedesktop.org/wiki/Olympus_ORF
       */
      private void getOrfAttributes(ByteOrderedDataInputStream in) throws IOException {
          // Retrieve primary image data
          // Other Exif data will be located in the Makernote.
          getRawAttributes(in);
  
          // Additionally retrieve preview/thumbnail information from MakerNote tag, which contains
          // proprietary tags and therefore does not have offical documentation
          // See GetOlympusPreviewImage() in piex.cc & http://www.exiv2.org/tags-olympus.html
          ExifAttribute makerNoteAttribute =
                  (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
          if (makerNoteAttribute != null) {
              // Create an ordered DataInputStream for MakerNote
              ByteOrderedDataInputStream makerNoteDataInputStream =
                      new ByteOrderedDataInputStream(makerNoteAttribute.bytes);
              makerNoteDataInputStream.setByteOrder(mExifByteOrder);
  
              // There are two types of headers for Olympus MakerNotes
              // See http://www.exiv2.org/makernote.html#R1
              byte[] makerNoteHeader1Bytes = new byte[ORF_MAKER_NOTE_HEADER_1.length];
              makerNoteDataInputStream.readFully(makerNoteHeader1Bytes);
              makerNoteDataInputStream.seek(0);
              byte[] makerNoteHeader2Bytes = new byte[ORF_MAKER_NOTE_HEADER_2.length];
              makerNoteDataInputStream.readFully(makerNoteHeader2Bytes);
              // Skip the corresponding amount of bytes for each header type
              if (Arrays.equals(makerNoteHeader1Bytes, ORF_MAKER_NOTE_HEADER_1)) {
                  makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_1_SIZE);
              } else if (Arrays.equals(makerNoteHeader2Bytes, ORF_MAKER_NOTE_HEADER_2)) {
                  makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_2_SIZE);
              }
  
              // Read IFD data from MakerNote
              readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_ORF_MAKER_NOTE);
  
              // Retrieve & update preview image offset & length values
              ExifAttribute imageLengthAttribute = (ExifAttribute)
                      mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_START);
              ExifAttribute bitsPerSampleAttribute = (ExifAttribute)
                      mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_LENGTH);
  
              if (imageLengthAttribute != null && bitsPerSampleAttribute != null) {
                  mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT,
                          imageLengthAttribute);
                  mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
                          bitsPerSampleAttribute);
              }
  
              // TODO: Check this behavior in other ORF files
              // Retrieve primary image length & width values
              // See piex.cc GetOlympusPreviewImage()
              ExifAttribute aspectFrameAttribute = (ExifAttribute)
                      mAttributes[IFD_TYPE_ORF_IMAGE_PROCESSING].get(TAG_ORF_ASPECT_FRAME);
              if (aspectFrameAttribute != null) {
                  int[] aspectFrameValues = new int[4];
                  aspectFrameValues = (int[]) aspectFrameAttribute.getValue(mExifByteOrder);
                  if (aspectFrameValues[2] > aspectFrameValues[0] &&
                          aspectFrameValues[3] > aspectFrameValues[1]) {
                      int primaryImageWidth = aspectFrameValues[2] - aspectFrameValues[0] + 1;
                      int primaryImageLength = aspectFrameValues[3] - aspectFrameValues[1] + 1;
                      // Swap width & length values
                      if (primaryImageWidth < primaryImageLength) {
                          primaryImageWidth += primaryImageLength;
                          primaryImageLength = primaryImageWidth - primaryImageLength;
                          primaryImageWidth -= primaryImageLength;
                      }
                      ExifAttribute primaryImageWidthAttribute =
                              ExifAttribute.createUShort(primaryImageWidth, mExifByteOrder);
                      ExifAttribute primaryImageLengthAttribute =
                              ExifAttribute.createUShort(primaryImageLength, mExifByteOrder);
  
                      mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, primaryImageWidthAttribute);
                      mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, primaryImageLengthAttribute);
                  }
              }
          }
      }
  
      // RW2 contains the primary image data in IFD0 and the preview and/or thumbnail image data in
      // the JpgFromRaw tag
      // See https://libopenraw.freedesktop.org/wiki/Panasonic_RAW/ and piex.cc Rw2GetPreviewData()
      private void getRw2Attributes(ByteOrderedDataInputStream in) throws IOException {
          // Retrieve primary image data
          getRawAttributes(in);
  
          // Retrieve preview and/or thumbnail image data
          ExifAttribute jpgFromRawAttribute =
                  (ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_JPG_FROM_RAW);
          if (jpgFromRawAttribute != null) {
              getJpegAttributes(in, mRw2JpgFromRawOffset, IFD_TYPE_PREVIEW);
          }
  
          // Set ISO tag value if necessary
          ExifAttribute rw2IsoAttribute =
                  (ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_ISO);
          ExifAttribute exifIsoAttribute =
                  (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_ISO_SPEED_RATINGS);
          if (rw2IsoAttribute != null && exifIsoAttribute == null) {
              // Place this attribute only if it doesn't exist
              mAttributes[IFD_TYPE_EXIF].put(TAG_ISO_SPEED_RATINGS, rw2IsoAttribute);
          }
      }
  
      // Stores a new JPEG image with EXIF attributes into a given output stream.
      private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream)
              throws IOException {
          // See JPEG File Interchange Format Specification, "JFIF Specification"
          if (DEBUG) {
              Log.d(TAG, "saveJpegAttributes starting with (inputStream: " + inputStream
                      + ", outputStream: " + outputStream + ")");
          }
          DataInputStream dataInputStream = new DataInputStream(inputStream);
          ByteOrderedDataOutputStream dataOutputStream =
                  new ByteOrderedDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN);
          if (dataInputStream.readByte() != MARKER) {
              throw new IOException("Invalid marker");
          }
          dataOutputStream.writeByte(MARKER);
          if (dataInputStream.readByte() != MARKER_SOI) {
              throw new IOException("Invalid marker");
          }
          dataOutputStream.writeByte(MARKER_SOI);
  
          // Write EXIF APP1 segment
          dataOutputStream.writeByte(MARKER);
          dataOutputStream.writeByte(MARKER_APP1);
          writeExifSegment(dataOutputStream);
  
          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 {
          // Save offset of current IFD to prevent reading an IFD that is already read.
          mHandledIfdOffsets.add(dataInputStream.mPosition);
  
          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) {
                  if (DEBUG) {
                      Log.d(TAG, "Skip the tag entry since tag number is not defined: " + tagNumber);
                  }
              } else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
                  if (DEBUG) {
                      Log.d(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) {
                      if (DEBUG) {
                          Log.d(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.
                      if (DEBUG) {
                          Log.d(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 (!mHandledIfdOffsets.contains((int) offset)) {
                          dataInputStream.seek(offset);
                          readImageFileDirectory(dataInputStream, nextIfdType);
                      } else {
                          if (DEBUG) {
                              Log.d(TAG, "Skip jump into the IFD since it has already been read: "
                                      + "IfdType " + nextIfdType + " (at " + offset + ")");
                          }
                      }
                  } else {
                      if (DEBUG) {
                          Log.d(TAG, "Skip jump into the IFD since its offset is invalid: " + offset);
                      }
                  }
  
                  dataInputStream.seek(nextEntryOffset);
                  continue;
              }
  
              final int bytesOffset = dataInputStream.peek() + mExifOffset;
              final byte[] bytes = new byte[(int) byteCount];
              dataInputStream.readFully(bytes);
              ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,
                      bytesOffset, 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 (!mHandledIfdOffsets.contains(nextIfdOffset)) {
                      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 {
                      if (DEBUG) {
                          Log.d(TAG, "Stop reading file since re-reading an IFD may cause an "
                                  + "infinite loop: " + nextIfdOffset);
                      }
                  }
              } else {
                  if (DEBUG) {
                      Log.d(TAG, "Stop reading file since a wrong offset may cause an infinite loop: "
                              + nextIfdOffset);
                  }
              }
          }
      }
  
      /**
       * JPEG compressed images do not contain IMAGE_LENGTH & IMAGE_WIDTH tags.
       * This value uses JpegInterchangeFormat(JPEG data offset) value, and calls getJpegAttributes()
       * to locate SOF(Start of Frame) marker and update the image length & width values.
       * See JEITA CP-3451C Table 5 and Section 4.8.1. B.
       */
      private void retrieveJpegImageSize(ByteOrderedDataInputStream in, int imageType)
              throws IOException {
          // Check if image already has IMAGE_LENGTH & IMAGE_WIDTH values
          ExifAttribute imageLengthAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_LENGTH);
          ExifAttribute imageWidthAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_WIDTH);
  
          if (imageLengthAttribute == null || imageWidthAttribute == null) {
              // Find if offset for JPEG data exists
              ExifAttribute jpegInterchangeFormatAttribute =
                      (ExifAttribute) mAttributes[imageType].get(TAG_JPEG_INTERCHANGE_FORMAT);
              if (jpegInterchangeFormatAttribute != null) {
                  int jpegInterchangeFormat =
                          jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
  
                  // Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags
                  getJpegAttributes(in, jpegInterchangeFormat, imageType);
              }
          }
      }
  
      // Sets thumbnail offset & length attributes based on JpegInterchangeFormat or StripOffsets tags
      private void setThumbnailData(ByteOrderedDataInputStream in) throws IOException {
          HashMap thumbnailData = mAttributes[IFD_TYPE_THUMBNAIL];
  
          ExifAttribute compressionAttribute =
                  (ExifAttribute) thumbnailData.get(TAG_COMPRESSION);
          if (compressionAttribute != null) {
              mThumbnailCompression = compressionAttribute.getIntValue(mExifByteOrder);
              switch (mThumbnailCompression) {
                  case DATA_JPEG: {
                      handleThumbnailFromJfif(in, thumbnailData);
                      break;
                  }
                  case DATA_UNCOMPRESSED:
                  case DATA_JPEG_COMPRESSED: {
                      if (isSupportedDataType(thumbnailData)) {
                          handleThumbnailFromStrips(in, thumbnailData);
                      }
                      break;
                  }
              }
          } else {
              // Thumbnail data may not contain Compression tag value
              handleThumbnailFromJfif(in, thumbnailData);
          }
      }
  
      // Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values
      // and reads the corresponding bytes if stream does not support seek function
      private void handleThumbnailFromJfif(ByteOrderedDataInputStream in, HashMap thumbnailData)
              throws IOException {
          ExifAttribute jpegInterchangeFormatAttribute =
                  (ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT);
          ExifAttribute jpegInterchangeFormatLengthAttribute =
                  (ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
          if (jpegInterchangeFormatAttribute != null
                  && jpegInterchangeFormatLengthAttribute != null) {
              int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
              int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);
  
              // The following code limits the size of thumbnail size not to overflow EXIF data area.
              thumbnailLength = Math.min(thumbnailLength, in.getLength() - thumbnailOffset);
              if (mMimeType == IMAGE_TYPE_ORF) {
                  // Update offset value since RAF files have IFD data preceding MakerNote data.
                  thumbnailOffset += mOrfMakerNoteOffset;
              }
              if (thumbnailOffset > 0 && thumbnailLength > 0) {
                  mHasThumbnail = true;
                  mThumbnailOffset = thumbnailOffset + mExifOffset;
                  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[mThumbnailLength];
                      in.seek(mThumbnailOffset);
                      in.readFully(thumbnailBytes);
                      mThumbnailBytes = thumbnailBytes;
                  }
                  if (DEBUG) {
                      Log.d(TAG, "Setting thumbnail attributes with offset: " + thumbnailOffset
                              + ", length: " + thumbnailLength);
                  }
              }
          }
      }
  
      // 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 || stripOffsets.length == 0) {
                  Log.w(TAG, "stripOffsets should not be null or have zero length.");
                  return;
              }
              if (stripByteCounts == null || stripByteCounts.length == 0) {
                  Log.w(TAG, "stripByteCounts should not be null or have zero length.");
                  return;
              }
              if (stripOffsets.length != stripByteCounts.length) {
                  Log.w(TAG, "stripOffsets and stripByteCounts should have same length.");
                  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;
              mHasThumbnail = mHasThumbnailStrips = mAreThumbnailStripsConsecutive = true;
              for (int i = 0; i < stripOffsets.length; i++) {
                  int stripOffset = (int) stripOffsets[i];
                  int stripByteCount = (int) stripByteCounts[i];
  
                  // Check if strips are consecutive
                  // TODO: Add test for non-consecutive thumbnail image
                  if (i < stripOffsets.length - 1
                          && stripOffset + stripByteCount != stripOffsets[i + 1]) {
                      mAreThumbnailStripsConsecutive = false;
                  }
  
                  // 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;
              }
              mThumbnailBytes = totalStripBytes;
  
              if (mAreThumbnailStripsConsecutive) {
                  // Need to add mExifOffset, which is the offset to the EXIF data segment
                  mThumbnailOffset = (int) stripOffsets[0] + mExifOffset;
                  mThumbnailLength = totalStripBytes.length;
              }
          }
      }
  
      // Check if thumbnail data type is currently supported or not
      private boolean isSupportedDataType(HashMap thumbnailData) throws IOException {
          ExifAttribute bitsPerSampleAttribute =
                  (ExifAttribute) thumbnailData.get(TAG_BITS_PER_SAMPLE);
          if (bitsPerSampleAttribute != null) {
              int[] bitsPerSampleValue = (int[]) bitsPerSampleAttribute.getValue(mExifByteOrder);
  
              if (Arrays.equals(BITS_PER_SAMPLE_RGB, bitsPerSampleValue)) {
                  return true;
              }
  
              // See DNG Specification 1.4.0.0. Section 3, Compression.
              if (mMimeType == IMAGE_TYPE_DNG) {
                  ExifAttribute photometricInterpretationAttribute =
                          (ExifAttribute) thumbnailData.get(TAG_PHOTOMETRIC_INTERPRETATION);
                  if (photometricInterpretationAttribute != null) {
                      int photometricInterpretationValue
                              = photometricInterpretationAttribute.getIntValue(mExifByteOrder);
                      if ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO
                              && Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_GREYSCALE_2))
                              || ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_YCBCR)
                              && (Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_RGB)))) {
                          return true;
                      } else {
                          // TODO: Add support for lossless Huffman JPEG data
                      }
                  }
              }
          }
          if (DEBUG) {
              Log.d(TAG, "Unsupported data type value");
          }
          return false;
      }
  
      // Returns true if the image length and width values are <= 512.
      // See Section 4.8 of http://standardsproposals.bsigroup.com/Home/getPDF/567
      private boolean isThumbnail(HashMap map) throws IOException {
          ExifAttribute imageLengthAttribute = (ExifAttribute) map.get(TAG_IMAGE_LENGTH);
          ExifAttribute imageWidthAttribute = (ExifAttribute) map.get(TAG_IMAGE_WIDTH);
  
          if (imageLengthAttribute != null && imageWidthAttribute != null) {
              int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder);
              int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder);
              if (imageLengthValue <= MAX_THUMBNAIL_SIZE && imageWidthValue <= MAX_THUMBNAIL_SIZE) {
                  return true;
              }
          }
          return false;
      }
  
      // Validate primary, preview, thumbnail image data by comparing image size
      private void validateImages(InputStream in) throws IOException {
          // Swap images based on size (primary > preview > thumbnail)
          swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_PREVIEW);
          swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_THUMBNAIL);
          swapBasedOnImageSize(IFD_TYPE_PREVIEW, IFD_TYPE_THUMBNAIL);
  
          // Check if image has PixelXDimension/PixelYDimension tags, which contain valid image
          // sizes, excluding padding at the right end or bottom end of the image to make sure that
          // the values are multiples of 64. See JEITA CP-3451C Table 5 and Section 4.8.1. B.
          ExifAttribute pixelXDimAttribute =
                  (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_X_DIMENSION);
          ExifAttribute pixelYDimAttribute =
                  (ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_Y_DIMENSION);
          if (pixelXDimAttribute != null && pixelYDimAttribute != null) {
              mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, pixelXDimAttribute);
              mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, pixelYDimAttribute);
          }
  
          // Check whether thumbnail image exists and whether preview image satisfies the thumbnail
          // image requirements
          if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
              if (isThumbnail(mAttributes[IFD_TYPE_PREVIEW])) {
                  mAttributes[IFD_TYPE_THUMBNAIL] = mAttributes[IFD_TYPE_PREVIEW];
                  mAttributes[IFD_TYPE_PREVIEW] = new HashMap();
              }
          }
  
          // Check if the thumbnail image satisfies the thumbnail size requirements
          if (!isThumbnail(mAttributes[IFD_TYPE_THUMBNAIL])) {
              Log.d(TAG, "No image meets the size requirements of a thumbnail image.");
          }
      }
  
      /**
       * If image is uncompressed, ImageWidth/Length tags are used to store size info.
       * However, uncompressed images often store extra pixels around the edges of the final image,
       * which results in larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags.
       * This method corrects those tag values by checking first the values of TAG_DEFAULT_CROP_SIZE
       * See DNG Specification 1.4.0.0. Section 4. (DefaultCropSize)
       *
       * If image is a RW2 file, valid image sizes are stored in SensorBorder tags.
       * See tiff_parser.cc GetFullDimension32()
       * */
      private void updateImageSizeValues(ByteOrderedDataInputStream in, int imageType)
              throws IOException {
          // Uncompressed image valid image size values
          ExifAttribute defaultCropSizeAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_DEFAULT_CROP_SIZE);
          // RW2 image valid image size values
          ExifAttribute topBorderAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_TOP_BORDER);
          ExifAttribute leftBorderAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_LEFT_BORDER);
          ExifAttribute bottomBorderAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_BOTTOM_BORDER);
          ExifAttribute rightBorderAttribute =
                  (ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_RIGHT_BORDER);
  
          if (defaultCropSizeAttribute != null) {
              // Update for uncompressed image
              ExifAttribute defaultCropSizeXAttribute, defaultCropSizeYAttribute;
              if (defaultCropSizeAttribute.format == IFD_FORMAT_URATIONAL) {
                  Rational[] defaultCropSizeValue =
                          (Rational[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
                  defaultCropSizeXAttribute =
                          ExifAttribute.createURational(defaultCropSizeValue[0], mExifByteOrder);
                  defaultCropSizeYAttribute =
                          ExifAttribute.createURational(defaultCropSizeValue[1], mExifByteOrder);
              } else {
                  int[] defaultCropSizeValue =
                          (int[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
                  defaultCropSizeXAttribute =
                          ExifAttribute.createUShort(defaultCropSizeValue[0], mExifByteOrder);
                  defaultCropSizeYAttribute =
                          ExifAttribute.createUShort(defaultCropSizeValue[1], mExifByteOrder);
              }
              mAttributes[imageType].put(TAG_IMAGE_WIDTH, defaultCropSizeXAttribute);
              mAttributes[imageType].put(TAG_IMAGE_LENGTH, defaultCropSizeYAttribute);
          } else if (topBorderAttribute != null && leftBorderAttribute != null &&
                  bottomBorderAttribute != null && rightBorderAttribute != null) {
              // Update for RW2 image
              int topBorderValue = topBorderAttribute.getIntValue(mExifByteOrder);
              int bottomBorderValue = bottomBorderAttribute.getIntValue(mExifByteOrder);
              int rightBorderValue = rightBorderAttribute.getIntValue(mExifByteOrder);
              int leftBorderValue = leftBorderAttribute.getIntValue(mExifByteOrder);
              if (bottomBorderValue > topBorderValue && rightBorderValue > leftBorderValue) {
                  int length = bottomBorderValue - topBorderValue;
                  int width = rightBorderValue - leftBorderValue;
                  ExifAttribute imageLengthAttribute =
                          ExifAttribute.createUShort(length, mExifByteOrder);
                  ExifAttribute imageWidthAttribute =
                          ExifAttribute.createUShort(width, mExifByteOrder);
                  mAttributes[imageType].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
                  mAttributes[imageType].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
              }
          } else {
              retrieveJpegImageSize(in, imageType);
          }
      }
  
      // Writes an Exif segment into the given output stream.
      private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) 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.
          // 8 bytes are for TIFF headers: 2 bytes (byte order) + 2 bytes (identifier) + 4 bytes
          // (offset of IFDs)
          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));
              // Need to add mExifOffset, which is the offset to the EXIF data segment
              mThumbnailOffset = thumbnailOffset + mExifOffset;
              position += mThumbnailLength;
          }
  
          // Calculate the total size
          int totalSize = position + 8;  // eight bytes is for header part.
          if (DEBUG) {
              Log.d(TAG, "totalSize length: " + totalSize);
              for (int i = 0; i < EXIF_TAGS.length; ++i) {
                  Log.d(TAG, String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
                          i, ifdOffsets[i], mAttributes[i].size(), ifdDataSizes[i]));
              }
          }
  
          // Update IFD pointer tags with the calculated offsets.
          if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
              mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
                      ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifByteOrder));
          }
          if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
              mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
                      ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifByteOrder));
          }
          if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
              mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name, ExifAttribute.createULong(
                      ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifByteOrder));
          }
  
          // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
          dataOutputStream.writeUnsignedShort(totalSize);
          dataOutputStream.write(IDENTIFIER_EXIF_APP1);
          dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN
                  ? BYTE_ALIGN_MM : BYTE_ALIGN_II);
          dataOutputStream.setByteOrder(mExifByteOrder);
          dataOutputStream.writeUnsignedShort(START_CODE);
          dataOutputStream.writeUnsignedInt(IFD_OFFSET);
  
          // Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9.
          for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
              if (!mAttributes[ifdType].isEmpty()) {
                  // See JEITA CP-3451C Section 4.6.2: IFD structure.
                  // Write entry count
                  dataOutputStream.writeUnsignedShort(mAttributes[ifdType].size());
  
                  // Write entry info
                  int dataOffset = ifdOffsets[ifdType] + 2 + mAttributes[ifdType].size() * 12 + 4;
                  for (Map.Entry entry : (Set<Map.Entry>) mAttributes[ifdType].entrySet()) {
                      // Convert tag name to tag number.
                      final ExifTag tag =
                              (ExifTag) sExifTagMapsForWriting[ifdType].get(entry.getKey());
                      final int tagNumber = tag.number;
                      final ExifAttribute attribute = (ExifAttribute) entry.getValue();
                      final int size = attribute.size();
  
                      dataOutputStream.writeUnsignedShort(tagNumber);
                      dataOutputStream.writeUnsignedShort(attribute.format);
                      dataOutputStream.writeInt(attribute.numberOfComponents);
                      if (size > 4) {
                          dataOutputStream.writeUnsignedInt(dataOffset);
                          dataOffset += size;
                      } else {
                          dataOutputStream.write(attribute.bytes);
                          // Fill zero up to 4 bytes
                          if (size < 4) {
                              for (int i = size; i < 4; ++i) {
                                  dataOutputStream.writeByte(0);
                              }
                          }
                      }
                  }
  
                  // Write the next offset. It writes the offset of thumbnail IFD if there is one or
                  // more tags in the thumbnail IFD when the current IFD is the primary image TIFF
                  // IFD; Otherwise 0.
                  if (ifdType == 0 && !mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
                      dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_TYPE_THUMBNAIL]);
                  } else {
                      dataOutputStream.writeUnsignedInt(0);
                  }
  
                  // Write values of data field exceeding 4 bytes after the next offset.
                  for (Map.Entry entry : (Set<Map.Entry>) mAttributes[ifdType].entrySet()) {
                      ExifAttribute attribute = (ExifAttribute) entry.getValue();
  
                      if (attribute.bytes.length > 4) {
                          dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length);
                      }
                  }
              }
          }
  
          // Write thumbnail
          if (mHasThumbnail) {
              dataOutputStream.write(getThumbnailBytes());
          }
  
          // Reset the byte order to big endian in order to write remaining parts of the JPEG file.
          dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
  
          return totalSize;
      }
  
      /**
       * Determines the data format of EXIF entry value.
       *
       * @param entryValue The value to be determined.
       * @return Returns two data formats gussed as a pair in integer. If there is no two candidate
                 data formats for the given entry value, returns {@code -1} in the second of the pair.
       */
      private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
          // See TIFF 6.0 Section 2, "Image File Directory".
          // Take the first component if there are more than one component.
          if (entryValue.contains(",")) {
              String[] entryValues = entryValue.split(",");
              Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
              if (dataFormat.first == IFD_FORMAT_STRING) {
                  return dataFormat;
              }
              for (int i = 1; i < entryValues.length; ++i) {
                  final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
                  int first = -1, second = -1;
                  if (guessDataFormat.first == dataFormat.first
                          || guessDataFormat.second == dataFormat.first) {
                      first = dataFormat.first;
                  }
                  if (dataFormat.second != -1 && (guessDataFormat.first == dataFormat.second
                          || guessDataFormat.second == dataFormat.second)) {
                      second = dataFormat.second;
                  }
                  if (first == -1 && second == -1) {
                      return new Pair<>(IFD_FORMAT_STRING, -1);
                  }
                  if (first == -1) {
                      dataFormat = new Pair<>(second, -1);
                      continue;
                  }
                  if (second == -1) {
                      dataFormat = new Pair<>(first, -1);
                      continue;
                  }
              }
              return dataFormat;
          }
  
          if (entryValue.contains("/")) {
              String[] rationalNumber = entryValue.split("/");
              if (rationalNumber.length == 2) {
                  try {
                      long numerator = (long) Double.parseDouble(rationalNumber[0]);
                      long denominator = (long) Double.parseDouble(rationalNumber[1]);
                      if (numerator < 0L || denominator < 0L) {
                          return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
                      }
                      if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
                          return new Pair<>(IFD_FORMAT_URATIONAL, -1);
                      }
                      return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
                  } catch (NumberFormatException e)  {
                      // Ignored
                  }
              }
              return new Pair<>(IFD_FORMAT_STRING, -1);
          }
          try {
              Long longValue = Long.parseLong(entryValue);
              if (longValue >= 0 && longValue <= 65535) {
                  return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
              }
              if (longValue < 0) {
                  return new Pair<>(IFD_FORMAT_SLONG, -1);
              }
              return new Pair<>(IFD_FORMAT_ULONG, -1);
          } catch (NumberFormatException e) {
              // Ignored
          }
          try {
              Double.parseDouble(entryValue);
              return new Pair<>(IFD_FORMAT_DOUBLE, -1);
          } catch (NumberFormatException e) {
              // Ignored
          }
          return new Pair<>(IFD_FORMAT_STRING, -1);
      }
  
      // An input stream to parse EXIF data area, which can be written in either little or big endian
      // order.
      private static class ByteOrderedDataInputStream extends InputStream implements DataInput {
          private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
          private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
  
          private DataInputStream mDataInputStream;
          private InputStream mInputStream;
          private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
          private final int mLength;
          private int mPosition;
  
          public ByteOrderedDataInputStream(InputStream in) throws IOException {
              mInputStream = in;
              mDataInputStream = new DataInputStream(in);
              mLength = mDataInputStream.available();
              mPosition = 0;
              mDataInputStream.mark(mLength);
          }
  
          public ByteOrderedDataInputStream(byte[] bytes) throws IOException {
              this(new ByteArrayInputStream(bytes));
          }
  
          public void setByteOrder(ByteOrder byteOrder) {
              mByteOrder = byteOrder;
          }
  
          public void seek(long byteCount) throws IOException {
              if (mPosition > byteCount) {
                  mPosition = 0;
                  mDataInputStream.reset();
                  mDataInputStream.mark(mLength);
              } else {
                  byteCount -= mPosition;
              }
  
              if (skipBytes((int) byteCount) != (int) byteCount) {
                  throw new IOException("Couldn't seek up to the byteCount");
              }
          }
  
          public int peek() {
              return mPosition;
          }
  
          @Override
          public int available() throws IOException {
              return mDataInputStream.available();
          }
  
          @Override
          public int read() throws IOException {
              ++mPosition;
              return mDataInputStream.read();
          }
  
          @Override
          public int readUnsignedByte() throws IOException {
              ++mPosition;
              return mDataInputStream.readUnsignedByte();
          }
  
          @Override
          public String readLine() throws IOException {
              Log.d(TAG, "Currently unsupported");
              return null;
          }
  
          @Override
          public boolean readBoolean() throws IOException {
              ++mPosition;
              return mDataInputStream.readBoolean();
          }
  
          @Override
          public char readChar() throws IOException {
              mPosition += 2;
              return mDataInputStream.readChar();
          }
  
          @Override
          public String readUTF() throws IOException {
              mPosition += 2;
              return mDataInputStream.readUTF();
          }
  
          @Override
          public void readFully(byte[] buffer, int offset, int length) throws IOException {
              mPosition += length;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              if (mDataInputStream.read(buffer, offset, length) != length) {
                  throw new IOException("Couldn't read up to the length of buffer");
              }
          }
  
          @Override
          public void readFully(byte[] buffer) throws IOException {
              mPosition += buffer.length;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) {
                  throw new IOException("Couldn't read up to the length of buffer");
              }
          }
  
          @Override
          public byte readByte() throws IOException {
              ++mPosition;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              int ch = mDataInputStream.read();
              if (ch < 0) {
                  throw new EOFException();
              }
              return (byte) ch;
          }
  
          @Override
          public short readShort() throws IOException {
              mPosition += 2;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              int ch1 = mDataInputStream.read();
              int ch2 = mDataInputStream.read();
              if ((ch1 | ch2) < 0) {
                  throw new EOFException();
              }
              if (mByteOrder == LITTLE_ENDIAN) {
                  return (short) ((ch2 << 8) + (ch1));
              } else if (mByteOrder == BIG_ENDIAN) {
                  return (short) ((ch1 << 8) + (ch2));
              }
              throw new IOException("Invalid byte order: " + mByteOrder);
          }
  
          @Override
          public int readInt() throws IOException {
              mPosition += 4;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              int ch1 = mDataInputStream.read();
              int ch2 = mDataInputStream.read();
              int ch3 = mDataInputStream.read();
              int ch4 = mDataInputStream.read();
              if ((ch1 | ch2 | ch3 | ch4) < 0) {
                  throw new EOFException();
              }
              if (mByteOrder == LITTLE_ENDIAN) {
                  return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
              } else if (mByteOrder == BIG_ENDIAN) {
                  return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
              }
              throw new IOException("Invalid byte order: " + mByteOrder);
          }
  
          @Override
          public int skipBytes(int byteCount) throws IOException {
              int totalSkip = Math.min(byteCount, mLength - mPosition);
              int skipped = 0;
              while (skipped < totalSkip) {
                  skipped += mDataInputStream.skipBytes(totalSkip - skipped);
              }
              mPosition += skipped;
              return skipped;
          }
  
          public int readUnsignedShort() throws IOException {
              mPosition += 2;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              int ch1 = mDataInputStream.read();
              int ch2 = mDataInputStream.read();
              if ((ch1 | ch2) < 0) {
                  throw new EOFException();
              }
              if (mByteOrder == LITTLE_ENDIAN) {
                  return ((ch2 << 8) + (ch1));
              } else if (mByteOrder == BIG_ENDIAN) {
                  return ((ch1 << 8) + (ch2));
              }
              throw new IOException("Invalid byte order: " + mByteOrder);
          }
  
          public long readUnsignedInt() throws IOException {
              return readInt() & 0xffffffffL;
          }
  
          @Override
          public long readLong() throws IOException {
              mPosition += 8;
              if (mPosition > mLength) {
                  throw new EOFException();
              }
              int ch1 = mDataInputStream.read();
              int ch2 = mDataInputStream.read();
              int ch3 = mDataInputStream.read();
              int ch4 = mDataInputStream.read();
              int ch5 = mDataInputStream.read();
              int ch6 = mDataInputStream.read();
              int ch7 = mDataInputStream.read();
              int ch8 = mDataInputStream.read();
              if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
                  throw new EOFException();
              }
              if (mByteOrder == LITTLE_ENDIAN) {
                  return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
                          + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
                          + ((long) ch2 << 8) + (long) ch1);
              } else if (mByteOrder == BIG_ENDIAN) {
                  return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
                          + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
                          + ((long) ch7 << 8) + (long) ch8);
              }
              throw new IOException("Invalid byte order: " + mByteOrder);
          }
  
          @Override
          public float readFloat() throws IOException {
              return Float.intBitsToFloat(readInt());
          }
  
          @Override
          public double readDouble() throws IOException {
              return Double.longBitsToDouble(readLong());
          }
  
          public int getLength() {
              return mLength;
          }
      }
  
      // An output stream to write EXIF data area, which can be written in either little or big endian
      // order.
      private static class ByteOrderedDataOutputStream extends FilterOutputStream {
          private final OutputStream mOutputStream;
          private ByteOrder mByteOrder;
  
          public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
              super(out);
              mOutputStream = out;
              mByteOrder = byteOrder;
          }
  
          public void setByteOrder(ByteOrder byteOrder) {
              mByteOrder = byteOrder;
          }
  
          public void write(byte[] bytes) throws IOException {
              mOutputStream.write(bytes);
          }
  
          public void write(byte[] bytes, int offset, int length) throws IOException {
              mOutputStream.write(bytes, offset, length);
          }
  
          public void writeByte(int val) throws IOException {
              mOutputStream.write(val);
          }
  
          public void writeShort(short val) throws IOException {
              if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
                  mOutputStream.write((val >>> 0) & 0xFF);
                  mOutputStream.write((val >>> 8) & 0xFF);
              } else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
                  mOutputStream.write((val >>> 8) & 0xFF);
                  mOutputStream.write((val >>> 0) & 0xFF);
              }
          }
  
          public void writeInt(int val) throws IOException {
              if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
                  mOutputStream.write((val >>> 0) & 0xFF);
                  mOutputStream.write((val >>> 8) & 0xFF);
                  mOutputStream.write((val >>> 16) & 0xFF);
                  mOutputStream.write((val >>> 24) & 0xFF);
              } else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
                  mOutputStream.write((val >>> 24) & 0xFF);
                  mOutputStream.write((val >>> 16) & 0xFF);
                  mOutputStream.write((val >>> 8) & 0xFF);
                  mOutputStream.write((val >>> 0) & 0xFF);
              }
          }
  
          public void writeUnsignedShort(int val) throws IOException {
              writeShort((short) val);
          }
  
          public void writeUnsignedInt(long val) throws IOException {
              writeInt((int) val);
          }
      }
  
      // Swaps image data based on image size
      private void swapBasedOnImageSize(@IfdType int firstIfdType, @IfdType int secondIfdType)
              throws IOException {
          if (mAttributes[firstIfdType].isEmpty() || mAttributes[secondIfdType].isEmpty()) {
              if (DEBUG) {
                  Log.d(TAG, "Cannot perform swap since only one image data exists");
              }
              return;
          }
  
          ExifAttribute firstImageLengthAttribute =
                  (ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_LENGTH);
          ExifAttribute firstImageWidthAttribute =
                  (ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_WIDTH);
          ExifAttribute secondImageLengthAttribute =
                  (ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_LENGTH);
          ExifAttribute secondImageWidthAttribute =
                  (ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_WIDTH);
  
          if (firstImageLengthAttribute == null || firstImageWidthAttribute == null) {
              if (DEBUG) {
                  Log.d(TAG, "First image does not contain valid size information");
              }
          } else if (secondImageLengthAttribute == null || secondImageWidthAttribute == null) {
              if (DEBUG) {
                  Log.d(TAG, "Second image does not contain valid size information");
              }
          } else {
              int firstImageLengthValue = firstImageLengthAttribute.getIntValue(mExifByteOrder);
              int firstImageWidthValue = firstImageWidthAttribute.getIntValue(mExifByteOrder);
              int secondImageLengthValue = secondImageLengthAttribute.getIntValue(mExifByteOrder);
              int secondImageWidthValue = secondImageWidthAttribute.getIntValue(mExifByteOrder);
  
              if (firstImageLengthValue < secondImageLengthValue &&
                      firstImageWidthValue < secondImageWidthValue) {
                  HashMap tempMap = mAttributes[firstIfdType];
                  mAttributes[firstIfdType] = mAttributes[secondIfdType];
                  mAttributes[secondIfdType] = tempMap;
              }
          }
      }
  
      // Checks if there is a match
      private boolean containsMatch(byte[] mainBytes, byte[] findBytes) {
          for (int i = 0; i < mainBytes.length - findBytes.length; i++) {
              for (int j = 0; j < findBytes.length; j++) {
                  if (mainBytes[i + j] != findBytes[j]) {
                      break;
                  }
                  if (j == findBytes.length - 1) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      /**
       * Convert given int[] to long[]. If long[] is given, just return it.
       * Return null for other types of input.
       */
      private static long[] convertToLongArray(Object inputObj) {
          if (inputObj instanceof int[]) {
              int[] input = (int[]) inputObj;
              long[] result = new long[input.length];
              for (int i = 0; i < input.length; i++) {
                  result[i] = input[i];
              }
              return result;
          } else if (inputObj instanceof long[]) {
              return (long[]) inputObj;
          }
          return null;
      }
  }
  