OSDN Git Service

Exif parser modifications.
authorRuben Brunk <rubenbrunk@google.com>
Tue, 12 Mar 2013 02:00:12 +0000 (19:00 -0700)
committerRuben Brunk <rubenbrunk@google.com>
Thu, 14 Mar 2013 01:01:42 +0000 (18:01 -0700)
Bug: 8018327
Change-Id: I66a2ec309f9807ac255bbf29d8f5f26de60e89b8

30 files changed:
gallerycommon/src/com/android/gallery3d/exif/ExifData.java
gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java [new file with mode: 0644]
gallerycommon/src/com/android/gallery3d/exif/ExifModifier.java
gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java
gallerycommon/src/com/android/gallery3d/exif/ExifParser.java
gallerycommon/src/com/android/gallery3d/exif/ExifReader.java
gallerycommon/src/com/android/gallery3d/exif/ExifTag.java
gallerycommon/src/com/android/gallery3d/exif/IfdData.java
gallerycommon/src/com/android/gallery3d/exif/IfdId.java
gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java
gallerycommon/src/com/android/gallery3d/exif/Rational.java
src/com/android/camera/Exif.java
src/com/android/camera/PanoramaModule.java
src/com/android/gallery3d/data/Exif.java
src/com/android/gallery3d/data/LocalImage.java
src/com/android/gallery3d/data/MediaDetails.java
src/com/android/gallery3d/filtershow/cache/ImageLoader.java
src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
src_pd/com/android/gallery3d/picasasource/PicasaSource.java
tests/src/com/android/gallery3d/exif/ExifDataTest.java
tests/src/com/android/gallery3d/exif/ExifInterfaceTest.java [new file with mode: 0644]
tests/src/com/android/gallery3d/exif/ExifModifierTest.java
tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java
tests/src/com/android/gallery3d/exif/ExifParserTest.java
tests/src/com/android/gallery3d/exif/ExifReaderTest.java
tests/src/com/android/gallery3d/exif/ExifTagTest.java
tests/src/com/android/gallery3d/exif/ExifTestRunner.java
tests/src/com/android/gallery3d/exif/ExifXmlDataTestCase.java
tests/src/com/android/gallery3d/exif/ExifXmlReader.java
tests/src/com/android/gallery3d/exif/Util.java

index adaceb5..8422382 100644 (file)
@@ -19,91 +19,67 @@ package com.android.gallery3d.exif;
 import android.util.Log;
 
 import java.io.UnsupportedEncodingException;
-import java.lang.reflect.Array;
 import java.nio.ByteOrder;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Calendar;
-import java.util.TimeZone;
+import java.util.List;
 
 /**
- *  This class stores the EXIF header in IFDs according to the JPEG specification.
- *  It is the result produced by {@link ExifReader}.
- *  @see ExifReader
- *  @see IfdData
+ * This class stores the EXIF header in IFDs according to the JPEG
+ * specification. It is the result produced by {@link ExifReader}.
+ *
+ * @see ExifReader
+ * @see IfdData
  */
-public class ExifData {
+class ExifData {
     private static final String TAG = "ExifData";
-    private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
-    private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss";
-
     private static final byte[] USER_COMMENT_ASCII = {
-            0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00};
+            0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
+    };
     private static final byte[] USER_COMMENT_JIS = {
-            0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00};
+            0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
+    };
     private static final byte[] USER_COMMENT_UNICODE = {
-            0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00};
-    private static final byte[] USER_COMMENT_UNDEFINED = {
-            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-
-    private final DateFormat mDateTimeStampFormat =
-            new SimpleDateFormat(DATETIME_FORMAT_STR);
-    private final DateFormat mGPSDateStampFormat =
-            new SimpleDateFormat(GPS_DATE_FORMAT_STR);
-    private final Calendar mGPSTimeStampCalendar = Calendar.getInstance(
-            TimeZone.getTimeZone("UTC"));
+            0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00
+    };
 
     private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
     private byte[] mThumbnail;
     private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
     private final ByteOrder mByteOrder;
 
-    public ExifData(ByteOrder order) {
+    ExifData(ByteOrder order) {
         mByteOrder = order;
-        mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    }
-
-    IfdData getIfdData(int ifdId) {
-        return mIfdDatas[ifdId];
     }
 
     /**
-     * Adds IFD data. If IFD data of the same type already exists,
-     * it will be replaced by the new data.
-     */
-    void addIfdData(IfdData data) {
-        mIfdDatas[data.getId()] = data;
-    }
-
-    /**
-     * Gets the compressed thumbnail. Returns null if there is no compressed thumbnail.
+     * Gets the compressed thumbnail. Returns null if there is no compressed
+     * thumbnail.
      *
      * @see #hasCompressedThumbnail()
      */
-    public byte[] getCompressedThumbnail() {
+    protected byte[] getCompressedThumbnail() {
         return mThumbnail;
     }
 
     /**
      * Sets the compressed thumbnail.
      */
-    public void setCompressedThumbnail(byte[] thumbnail) {
+    protected void setCompressedThumbnail(byte[] thumbnail) {
         mThumbnail = thumbnail;
     }
 
     /**
      * Returns true it this header contains a compressed thumbnail.
      */
-    public boolean hasCompressedThumbnail() {
+    protected boolean hasCompressedThumbnail() {
         return mThumbnail != null;
     }
 
     /**
      * Adds an uncompressed strip.
      */
-    public void setStripBytes(int index, byte[] strip) {
+    protected void setStripBytes(int index, byte[] strip) {
         if (index < mStripBytes.size()) {
             mStripBytes.set(index, strip);
         } else {
@@ -117,135 +93,57 @@ public class ExifData {
     /**
      * Gets the strip count.
      */
-    public int getStripCount() {
+    protected int getStripCount() {
         return mStripBytes.size();
     }
 
     /**
      * Gets the strip at the specified index.
+     *
      * @exceptions #IndexOutOfBoundException
      */
-    public byte[] getStrip(int index) {
+    protected byte[] getStrip(int index) {
         return mStripBytes.get(index);
     }
 
     /**
-     * Gets the byte order.
+     * Returns true if this header contains uncompressed strip.
      */
-    public ByteOrder getByteOrder() {
-        return mByteOrder;
+    protected boolean hasUncompressedStrip() {
+        return mStripBytes.size() != 0;
     }
 
     /**
-     * Returns true if this header contains uncompressed strip of thumbnail.
+     * Gets the byte order.
      */
-    public boolean hasUncompressedStrip() {
-        return mStripBytes.size() != 0;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj instanceof ExifData) {
-            ExifData data = (ExifData) obj;
-            if (data.mByteOrder != mByteOrder
-                    || !Arrays.equals(data.mThumbnail, mThumbnail)
-                    || data.mStripBytes.size() != mStripBytes.size()) return false;
-
-            for (int i = 0; i < mStripBytes.size(); i++) {
-                if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) return false;
-            }
-
-            for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
-                IfdData ifd1 = data.getIfdData(i);
-                IfdData ifd2 = getIfdData(i);
-                if ((ifd1 != ifd2) && (ifd1 != null && !ifd1.equals(ifd2))) return false;
-            }
-            return true;
-        }
-        return false;
+    protected ByteOrder getByteOrder() {
+        return mByteOrder;
     }
 
     /**
-     * A convenient method to adds tags {@link ExifTag#TAG_GPS_LATITUDE},
-     * {@link ExifTag#TAG_GPS_LONGITUDE}, {@link ExifTag#TAG_GPS_LATITUDE_REF} and
-     * {@link ExifTag#TAG_GPS_LONGITUDE_REF} at once with the
-     * given latitude and longitude.
+     * Returns the {@link IfdData} object corresponding to a given IFD if it
+     * exists or null.
      */
-    public void addGpsTags(double latitude, double longitude) {
-        IfdData gpsIfd = getIfdData(IfdId.TYPE_IFD_GPS);
-        if (gpsIfd == null) {
-            gpsIfd = new IfdData(IfdId.TYPE_IFD_GPS);
-            addIfdData(gpsIfd);
+    protected IfdData getIfdData(int ifdId) {
+        if (ExifTag.isValidIfd(ifdId)) {
+            return mIfdDatas[ifdId];
         }
-        ExifTag latTag = new ExifTag(ExifTag.TAG_GPS_LATITUDE, ExifTag.TYPE_RATIONAL,
-                3, IfdId.TYPE_IFD_GPS);
-        ExifTag longTag = new ExifTag(ExifTag.TAG_GPS_LONGITUDE, ExifTag.TYPE_RATIONAL,
-                3, IfdId.TYPE_IFD_GPS);
-        ExifTag latRefTag = new ExifTag(ExifTag.TAG_GPS_LATITUDE_REF,
-                ExifTag.TYPE_ASCII, 2, IfdId.TYPE_IFD_GPS);
-        ExifTag longRefTag = new ExifTag(ExifTag.TAG_GPS_LONGITUDE_REF,
-                ExifTag.TYPE_ASCII, 2, IfdId.TYPE_IFD_GPS);
-        latTag.setValue(toExifLatLong(latitude));
-        longTag.setValue(toExifLatLong(longitude));
-        latRefTag.setValue(latitude >= 0
-                ? ExifTag.GpsLatitudeRef.NORTH
-                : ExifTag.GpsLatitudeRef.SOUTH);
-        longRefTag.setValue(longitude >= 0
-                ? ExifTag.GpsLongitudeRef.EAST
-                : ExifTag.GpsLongitudeRef.WEST);
-        gpsIfd.setTag(latTag);
-        gpsIfd.setTag(longTag);
-        gpsIfd.setTag(latRefTag);
-        gpsIfd.setTag(longRefTag);
+        return null;
     }
 
     /**
-     * A convenient method to add date or time related tags (
-     * {@link ExifTag#TAG_DATE_TIME_DIGITIZED}, {@link ExifTag#TAG_DATE_TIME_ORIGINAL},
-     * and {@link ExifTag#TAG_DATE_TIME}) with the given time stamp value.
-     *
+     * Adds IFD data. If IFD data of the same type already exists, it will be
+     * replaced by the new data.
      */
-    public void addDateTimeStampTag(short tagId, long timestamp, TimeZone timezone) {
-        if (tagId == ExifTag.TAG_DATE_TIME ||
-                tagId == ExifTag.TAG_DATE_TIME_DIGITIZED ||
-                tagId == ExifTag.TAG_DATE_TIME_ORIGINAL) {
-            mDateTimeStampFormat.setTimeZone(timezone);
-            addTag(tagId).setValue(mDateTimeStampFormat.format(timestamp));
-        } else {
-            throw new IllegalArgumentException(
-                    String.format("Tag %04x is not a supported date or time stamp tag", tagId));
-        }
+    protected void addIfdData(IfdData data) {
+        mIfdDatas[data.getId()] = data;
     }
 
     /**
-     * A convenient method to add both {@link ExifTag#TAG_GPS_DATE_STAMP}
-     * and {@link ExifTag#TAG_GPS_TIME_STAMP}).
-     * Note that UTC timezone will be used as specified in the EXIF standard.
+     * Returns the {@link IfdData} object corresponding to a given IFD or
+     * generates one if none exist.
      */
-    public void addGpsDateTimeStampTag(long timestamp) {
-        addTag(ExifTag.TAG_GPS_DATE_STAMP).setValue(mGPSDateStampFormat.format(timestamp));
-
-        mGPSTimeStampCalendar.setTimeInMillis(timestamp);
-        addTag(ExifTag.TAG_GPS_TIME_STAMP).
-                setValue(new Rational[] {
-                        new Rational(mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1),
-                        new Rational(mGPSTimeStampCalendar.get(Calendar.MINUTE), 1),
-                        new Rational(mGPSTimeStampCalendar.get(Calendar.SECOND), 1)});
-    }
-
-    private static Rational[] toExifLatLong(double value) {
-        // convert to the format dd/1 mm/1 ssss/100
-        value = Math.abs(value);
-        int degrees = (int) value;
-        value = (value - degrees) * 60;
-        int minutes = (int) value;
-        value = (value - minutes) * 6000;
-        int seconds = (int) value;
-        return new Rational[] {
-                new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100)};
-    }
-
-    private IfdData getOrCreateIfdData(int ifdId) {
+    protected IfdData getOrCreateIfdData(int ifdId) {
         IfdData ifdData = mIfdDatas[ifdId];
         if (ifdData == null) {
             ifdData = new IfdData(ifdId);
@@ -255,98 +153,78 @@ public class ExifData {
     }
 
     /**
-     * Gets the tag with the given tag ID. Returns null if the tag does not exist. For tags
-     * related to interoperability or thumbnail, call {@link #getInteroperabilityTag(short)} and
-     * {@link #getThumbnailTag(short)} respectively.
-     */
-    public ExifTag getTag(short tagId) {
-        int ifdId = ExifTag.getIfdIdFromTagId(tagId);
-        IfdData ifdData = mIfdDatas[ifdId];
-        return (ifdData == null) ? null : ifdData.getTag(tagId);
-    }
-
-    /**
-     * Gets the thumbnail-related tag with the given tag ID.
-     */
-    public ExifTag getThumbnailTag(short tagId) {
-        IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_1];
-        return (ifdData == null) ? null : ifdData.getTag(tagId);
-    }
-
-    /**
-     * Gets the interoperability-related tag with the given tag ID.
+     * Returns the tag with a given TID in the given IFD if the tag exists.
+     * Otherwise returns null.
      */
-    public ExifTag getInteroperabilityTag(short tagId) {
-        IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_INTEROPERABILITY];
-        return (ifdData == null) ? null : ifdData.getTag(tagId);
+    protected ExifTag getTag(short tag, int ifd) {
+        IfdData ifdData = mIfdDatas[ifd];
+        return (ifdData == null) ? null : ifdData.getTag(tag);
     }
 
     /**
-     * Adds a tag with the given tag ID. If the tag of the given ID already exists,
-     * the original tag will be replaced. For tags
-     * related to interoperability or thumbnail, call {@link #addInteroperabilityTag(short)} or
-     * {@link #addThumbnailTag(short)} respectively.
-     * @exception IllegalArgumentException if the tag ID is invalid.
+     * Adds the given ExifTag to its default IFD and returns an existing ExifTag
+     * with the same TID or null if none exist.
      */
-    public ExifTag addTag(short tagId) {
-        int ifdId = ExifTag.getIfdIdFromTagId(tagId);
-        IfdData ifdData = getOrCreateIfdData(ifdId);
-        ExifTag tag = ExifTag.buildTag(tagId);
-        ifdData.setTag(tag);
-        return tag;
+    protected ExifTag addTag(ExifTag tag) {
+        if (tag != null) {
+            int ifd = tag.getIfd();
+            return addTag(tag, ifd);
+        }
+        return null;
     }
 
     /**
-     * Adds the given ExifTag to its corresponding IFD.
+     * Adds the given ExifTag to the given IFD and returns an existing ExifTag
+     * with the same TID or null if none exist.
      */
-    public void addTag(ExifTag tag) {
-        IfdData ifdData = getOrCreateIfdData(tag.getIfd());
-        ifdData.setTag(tag);
+    protected ExifTag addTag(ExifTag tag, int ifdId) {
+        if (tag != null && ExifTag.isValidIfd(ifdId)) {
+            IfdData ifdData = getOrCreateIfdData(ifdId);
+            return ifdData.setTag(tag);
+        }
+        return null;
     }
 
-    /**
-     * Adds a thumbnail-related tag with the given tag ID. If the tag of the given ID
-     * already exists, the original tag will be replaced.
-     * @exception IllegalArgumentException if the tag ID is invalid.
-     */
-    public ExifTag addThumbnailTag(short tagId) {
-        IfdData ifdData = getOrCreateIfdData(IfdId.TYPE_IFD_1);
-        ExifTag tag = ExifTag.buildThumbnailTag(tagId);
-        ifdData.setTag(tag);
-        return tag;
+    protected void clearThumbnailAndStrips() {
+        mThumbnail = null;
+        mStripBytes.clear();
     }
 
     /**
-     * Adds an interoperability-related tag with the given tag ID. If the tag of the given ID
-     * already exists, the original tag will be replaced.
-     * @exception IllegalArgumentException if the tag ID is invalid.
+     * Removes the thumbnail and its related tags. IFD1 will be removed.
      */
-    public ExifTag addInteroperabilityTag(short tagId) {
-        IfdData ifdData = getOrCreateIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
-        ExifTag tag = ExifTag.buildInteroperabilityTag(tagId);
-        ifdData.setTag(tag);
-        return tag;
+    protected void removeThumbnailData() {
+        clearThumbnailAndStrips();
+        mIfdDatas[IfdId.TYPE_IFD_1] = null;
     }
 
     /**
-     * Removes the thumbnail and its related tags. IFD1 will be removed.
+     * Removes the tag with a given TID and IFD.
      */
-    public void removeThumbnailData() {
-        mThumbnail = null;
-        mStripBytes.clear();
-        mIfdDatas[IfdId.TYPE_IFD_1] = null;
+    protected void removeTag(short tagId, int ifdId) {
+        IfdData ifdData = mIfdDatas[ifdId];
+        if (ifdData == null) {
+            return;
+        }
+        ifdData.removeTag(tagId);
     }
 
     /**
-     * Decodes the user comment tag into string as specified in the EXIF standard.
-     * Returns null if decoding failed.
+     * Decodes the user comment tag into string as specified in the EXIF
+     * standard. Returns null if decoding failed.
      */
-    public String decodeUserComment() {
+    protected String getUserComment() {
         IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
-        if (ifdData == null) return null;
-        ExifTag tag = ifdData.getTag(ExifTag.TAG_USER_COMMENT);
-        if (tag == null) return null;
-        if (tag.getComponentCount() < 8) return null;
+        if (ifdData == null) {
+            return null;
+        }
+        ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT));
+        if (tag == null) {
+            return null;
+        }
+        if (tag.getComponentCount() < 8) {
+            return null;
+        }
 
         byte[] buf = new byte[tag.getComponentCount()];
         tag.getBytes(buf);
@@ -368,6 +246,103 @@ public class ExifData {
             Log.w(TAG, "Failed to decode the user comment");
             return null;
         }
+    }
 
+    /**
+     * Returns a list of all {@link ExifTag}s in the ExifData or null if there
+     * are none.
+     */
+    protected List<ExifTag> getAllTags() {
+        ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
+        for (IfdData d : mIfdDatas) {
+            if (d != null) {
+                ExifTag[] tags = d.getAllTags();
+                if (tags != null) {
+                    for (ExifTag t : tags) {
+                        ret.add(t);
+                    }
+                }
+            }
+        }
+        if (ret.size() == 0) {
+            return null;
+        }
+        return ret;
     }
+
+    /**
+     * Returns a list of all {@link ExifTag}s in a given IFD or null if there
+     * are none.
+     */
+    protected List<ExifTag> getAllTagsForIfd(int ifd) {
+        IfdData d = mIfdDatas[ifd];
+        if (d == null) {
+            return null;
+        }
+        ExifTag[] tags = d.getAllTags();
+        if (tags == null) {
+            return null;
+        }
+        ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length);
+        for (ExifTag t : tags) {
+            ret.add(t);
+        }
+        if (ret.size() == 0) {
+            return null;
+        }
+        return ret;
+    }
+
+    /**
+     * Returns a list of all {@link ExifTag}s with a given TID or null if there
+     * are none.
+     */
+    protected List<ExifTag> getAllTagsForTagId(short tag) {
+        ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
+        for (IfdData d : mIfdDatas) {
+            if (d != null) {
+                ExifTag t = d.getTag(tag);
+                if (t != null) {
+                    ret.add(t);
+                }
+            }
+        }
+        if (ret.size() == 0) {
+            return null;
+        }
+        return ret;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof ExifData) {
+            ExifData data = (ExifData) obj;
+            if (data.mByteOrder != mByteOrder ||
+                    data.mStripBytes.size() != mStripBytes.size() ||
+                    !Arrays.equals(data.mThumbnail, mThumbnail)) {
+                return false;
+            }
+            for (int i = 0; i < mStripBytes.size(); i++) {
+                if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) {
+                    return false;
+                }
+            }
+            for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+                IfdData ifd1 = data.getIfdData(i);
+                IfdData ifd2 = getIfdData(i);
+                if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
 }
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java b/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java
new file mode 100644 (file)
index 0000000..2fef9ed
--- /dev/null
@@ -0,0 +1,2403 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.exif;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.SparseIntArray;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel.MapMode;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * This class provides methods and constants for reading and writing jpeg file
+ * metadata. It contains a collection of ExifTags, and a collection of
+ * definitions for creating valid ExifTags. The collection of ExifTags can be
+ * updated by: reading new ones from a file, deleting or adding existing ones,
+ * or building new ExifTags from a tag definition. These ExifTags can be written
+ * to a valid jpeg image as exif metadata.
+ * <p>
+ * Each ExifTag has a tag ID (TID) and is stored in a specific image file
+ * directory (IFD) as specified by the exif standard. A tag definition can be
+ * looked up with a constant that is a combination of TID and IFD. This
+ * definition has information about the type, number of components, and valid
+ * IFDs for a tag.
+ *
+ * @see ExifTag
+ */
+public class ExifInterface {
+    public static final int TAG_NULL = -1;
+    public static final int IFD_NULL = -1;
+    public static final int DEFINITION_NULL = 0;
+
+    /**
+     * Tag constants for Jeita EXIF 2.2
+     */
+
+    // IFD 0
+    public static final int TAG_IMAGE_WIDTH =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0100);
+    public static final int TAG_IMAGE_LENGTH =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0101); // Image height
+    public static final int TAG_BITS_PER_SAMPLE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0102);
+    public static final int TAG_COMPRESSION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0103);
+    public static final int TAG_PHOTOMETRIC_INTERPRETATION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0106);
+    public static final int TAG_IMAGE_DESCRIPTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x010E);
+    public static final int TAG_MAKE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x010F);
+    public static final int TAG_MODEL =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0110);
+    public static final int TAG_STRIP_OFFSETS =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0111);
+    public static final int TAG_ORIENTATION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0112);
+    public static final int TAG_SAMPLES_PER_PIXEL =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0115);
+    public static final int TAG_ROWS_PER_STRIP =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0116);
+    public static final int TAG_STRIP_BYTE_COUNTS =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0117);
+    public static final int TAG_X_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x011A);
+    public static final int TAG_Y_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x011B);
+    public static final int TAG_PLANAR_CONFIGURATION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x011C);
+    public static final int TAG_RESOLUTION_UNIT =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0128);
+    public static final int TAG_TRANSFER_FUNCTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x012D);
+    public static final int TAG_SOFTWARE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0131);
+    public static final int TAG_DATE_TIME =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0132);
+    public static final int TAG_ARTIST =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x013B);
+    public static final int TAG_WHITE_POINT =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x013E);
+    public static final int TAG_PRIMARY_CHROMATICITIES =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x013F);
+    public static final int TAG_Y_CB_CR_COEFFICIENTS =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0211);
+    public static final int TAG_Y_CB_CR_SUB_SAMPLING =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0212);
+    public static final int TAG_Y_CB_CR_POSITIONING =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0213);
+    public static final int TAG_REFERENCE_BLACK_WHITE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0214);
+    public static final int TAG_COPYRIGHT =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x8298);
+    public static final int TAG_EXIF_IFD =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x8769);
+    public static final int TAG_GPS_IFD =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x8825);
+    // IFD 1
+    public static final int TAG_JPEG_INTERCHANGE_FORMAT =
+        defineTag(IfdId.TYPE_IFD_1, (short) 0x0201);
+    public static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
+        defineTag(IfdId.TYPE_IFD_1, (short) 0x0202);
+    // IFD Exif Tags
+    public static final int TAG_EXPOSURE_TIME =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829A);
+    public static final int TAG_F_NUMBER =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829D);
+    public static final int TAG_EXPOSURE_PROGRAM =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8822);
+    public static final int TAG_SPECTRAL_SENSITIVITY =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8824);
+    public static final int TAG_ISO_SPEED_RATINGS =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8827);
+    public static final int TAG_OECF =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8828);
+    public static final int TAG_EXIF_VERSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9000);
+    public static final int TAG_DATE_TIME_ORIGINAL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9003);
+    public static final int TAG_DATE_TIME_DIGITIZED =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9004);
+    public static final int TAG_COMPONENTS_CONFIGURATION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9101);
+    public static final int TAG_COMPRESSED_BITS_PER_PIXEL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9102);
+    public static final int TAG_SHUTTER_SPEED_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9201);
+    public static final int TAG_APERTURE_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9202);
+    public static final int TAG_BRIGHTNESS_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9203);
+    public static final int TAG_EXPOSURE_BIAS_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9204);
+    public static final int TAG_MAX_APERTURE_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9205);
+    public static final int TAG_SUBJECT_DISTANCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9206);
+    public static final int TAG_METERING_MODE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9207);
+    public static final int TAG_LIGHT_SOURCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9208);
+    public static final int TAG_FLASH =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9209);
+    public static final int TAG_FOCAL_LENGTH =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x920A);
+    public static final int TAG_SUBJECT_AREA =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9214);
+    public static final int TAG_MAKER_NOTE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x927C);
+    public static final int TAG_USER_COMMENT =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9286);
+    public static final int TAG_SUB_SEC_TIME =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9290);
+    public static final int TAG_SUB_SEC_TIME_ORIGINAL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9291);
+    public static final int TAG_SUB_SEC_TIME_DIGITIZED =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9292);
+    public static final int TAG_FLASHPIX_VERSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA000);
+    public static final int TAG_COLOR_SPACE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA001);
+    public static final int TAG_PIXEL_X_DIMENSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA002);
+    public static final int TAG_PIXEL_Y_DIMENSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA003);
+    public static final int TAG_RELATED_SOUND_FILE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA004);
+    public static final int TAG_INTEROPERABILITY_IFD =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005);
+    public static final int TAG_FLASH_ENERGY =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20B);
+    public static final int TAG_SPATIAL_FREQUENCY_RESPONSE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20C);
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20E);
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20F);
+    public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA210);
+    public static final int TAG_SUBJECT_LOCATION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA214);
+    public static final int TAG_EXPOSURE_INDEX =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA215);
+    public static final int TAG_SENSING_METHOD =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA217);
+    public static final int TAG_FILE_SOURCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA300);
+    public static final int TAG_SCENE_TYPE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA301);
+    public static final int TAG_CFA_PATTERN =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA302);
+    public static final int TAG_CUSTOM_RENDERED =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA401);
+    public static final int TAG_EXPOSURE_MODE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA402);
+    public static final int TAG_WHITE_BALANCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA403);
+    public static final int TAG_DIGITAL_ZOOM_RATIO =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA404);
+    public static final int TAG_FOCAL_LENGTH_IN_35_MM_FILE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA405);
+    public static final int TAG_SCENE_CAPTURE_TYPE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA406);
+    public static final int TAG_GAIN_CONTROL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA407);
+    public static final int TAG_CONTRAST =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA408);
+    public static final int TAG_SATURATION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA409);
+    public static final int TAG_SHARPNESS =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40A);
+    public static final int TAG_DEVICE_SETTING_DESCRIPTION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40B);
+    public static final int TAG_SUBJECT_DISTANCE_RANGE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40C);
+    public static final int TAG_IMAGE_UNIQUE_ID =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA420);
+    // IFD GPS tags
+    public static final int TAG_GPS_VERSION_ID =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 0);
+    public static final int TAG_GPS_LATITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 1);
+    public static final int TAG_GPS_LATITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 2);
+    public static final int TAG_GPS_LONGITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 3);
+    public static final int TAG_GPS_LONGITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 4);
+    public static final int TAG_GPS_ALTITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 5);
+    public static final int TAG_GPS_ALTITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 6);
+    public static final int TAG_GPS_TIME_STAMP =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 7);
+    public static final int TAG_GPS_SATTELLITES =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 8);
+    public static final int TAG_GPS_STATUS =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 9);
+    public static final int TAG_GPS_MEASURE_MODE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 10);
+    public static final int TAG_GPS_DOP =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 11);
+    public static final int TAG_GPS_SPEED_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 12);
+    public static final int TAG_GPS_SPEED =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 13);
+    public static final int TAG_GPS_TRACK_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 14);
+    public static final int TAG_GPS_TRACK =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 15);
+    public static final int TAG_GPS_IMG_DIRECTION_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 16);
+    public static final int TAG_GPS_IMG_DIRECTION =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 17);
+    public static final int TAG_GPS_MAP_DATUM =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 18);
+    public static final int TAG_GPS_DEST_LATITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 19);
+    public static final int TAG_GPS_DEST_LATITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 20);
+    public static final int TAG_GPS_DEST_LONGITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 21);
+    public static final int TAG_GPS_DEST_LONGITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 22);
+    public static final int TAG_GPS_DEST_BEARING_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 23);
+    public static final int TAG_GPS_DEST_BEARING =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 24);
+    public static final int TAG_GPS_DEST_DISTANCE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 25);
+    public static final int TAG_GPS_DEST_DISTANCE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 26);
+    public static final int TAG_GPS_PROCESSING_METHOD =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 27);
+    public static final int TAG_GPS_AREA_INFORMATION =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 28);
+    public static final int TAG_GPS_DATE_STAMP =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 29);
+    public static final int TAG_GPS_DIFFERENTIAL =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 30);
+    // IFD Interoperability tags
+    public static final int TAG_INTEROPERABILITY_INDEX =
+        defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 1);
+
+    /**
+     * Tags that contain offset markers. These are included in the banned
+     * defines.
+     */
+    private static HashSet<Short> sOffsetTags = new HashSet<Short>();
+    static {
+        sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD));
+        sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD));
+        sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT));
+        sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD));
+        sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS));
+    }
+
+    /**
+     * Tags with definitions that cannot be overridden (banned defines).
+     */
+    protected static HashSet<Short> sBannedDefines = new HashSet<Short>(sOffsetTags);
+    static {
+        sBannedDefines.add(getTrueTagKey(TAG_NULL));
+        sBannedDefines.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+        sBannedDefines.add(getTrueTagKey(TAG_STRIP_BYTE_COUNTS));
+    }
+
+    /**
+     * Returns the constant representing a tag with a given TID and default IFD.
+     */
+    public static int defineTag(int ifdId, short tagId) {
+        return (tagId & 0x0000ffff) | (ifdId << 16);
+    }
+
+    /**
+     * Returns the TID for a tag constant.
+     */
+    public static short getTrueTagKey(int tag) {
+        // Truncate
+        return (short) tag;
+    }
+
+    /**
+     * Returns the default IFD for a tag constant.
+     */
+    public static int getTrueIfd(int tag) {
+        return tag >>> 16;
+    }
+
+    /**
+     * Constants for {@link TAG_ORIENTATION}. They can be interpreted as
+     * follows:
+     * <ul>
+     * <li>TOP_LEFT is the normal orientation.</li>
+     * <li>TOP_RIGHT is a left-right mirror.</li>
+     * <li>BOTTOM_LEFT is a 180 degree rotation.</li>
+     * <li>BOTTOM_RIGHT is a top-bottom mirror.</li>
+     * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.</li>
+     * <li>RIGHT_TOP is a 90 degree clockwise rotation.</li>
+     * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.</li>
+     * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.</li>
+     * </ul>
+     */
+    public static interface Orientation {
+        public static final short TOP_LEFT = 1;
+        public static final short TOP_RIGHT = 2;
+        public static final short BOTTOM_LEFT = 3;
+        public static final short BOTTOM_RIGHT = 4;
+        public static final short LEFT_TOP = 5;
+        public static final short RIGHT_TOP = 6;
+        public static final short LEFT_BOTTOM = 7;
+        public static final short RIGHT_BOTTOM = 8;
+    }
+
+    /**
+     * Constants for {@link TAG_Y_CB_CR_POSITIONING}
+     */
+    public static interface YCbCrPositioning {
+        public static final short CENTERED = 1;
+        public static final short CO_SITED = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_COMPRESSION}
+     */
+    public static interface Compression {
+        public static final short UNCOMPRESSION = 1;
+        public static final short JPEG = 6;
+    }
+
+    /**
+     * Constants for {@link TAG_RESOLUTION_UNIT}
+     */
+    public static interface ResolutionUnit {
+        public static final short INCHES = 2;
+        public static final short CENTIMETERS = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_PHOTOMETRIC_INTERPRETATION}
+     */
+    public static interface PhotometricInterpretation {
+        public static final short RGB = 2;
+        public static final short YCBCR = 6;
+    }
+
+    /**
+     * Constants for {@link TAG_PLANAR_CONFIGURATION}
+     */
+    public static interface PlanarConfiguration {
+        public static final short CHUNKY = 1;
+        public static final short PLANAR = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_EXPOSURE_PROGRAM}
+     */
+    public static interface ExposureProgram {
+        public static final short NOT_DEFINED = 0;
+        public static final short MANUAL = 1;
+        public static final short NORMAL_PROGRAM = 2;
+        public static final short APERTURE_PRIORITY = 3;
+        public static final short SHUTTER_PRIORITY = 4;
+        public static final short CREATIVE_PROGRAM = 5;
+        public static final short ACTION_PROGRAM = 6;
+        public static final short PROTRAIT_MODE = 7;
+        public static final short LANDSCAPE_MODE = 8;
+    }
+
+    /**
+     * Constants for {@link TAG_METERING_MODE}
+     */
+    public static interface MeteringMode {
+        public static final short UNKNOWN = 0;
+        public static final short AVERAGE = 1;
+        public static final short CENTER_WEIGHTED_AVERAGE = 2;
+        public static final short SPOT = 3;
+        public static final short MULTISPOT = 4;
+        public static final short PATTERN = 5;
+        public static final short PARTAIL = 6;
+        public static final short OTHER = 255;
+    }
+
+    /**
+     * Constants for {@link TAG_FLASH} As the definition in Jeita EXIF 2.2
+     * standard, we can treat this constant as bitwise flag.
+     * <p>
+     * e.g.
+     * <p>
+     * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED |
+     * MODE_AUTO_MODE
+     */
+    public static interface Flash {
+        // LSB
+        public static final short DID_NOT_FIRED = 0;
+        public static final short FIRED = 1;
+        // 1st~2nd bits
+        public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1;
+        public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1;
+        public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1;
+        // 3rd~4th bits
+        public static final short MODE_UNKNOWN = 0 << 3;
+        public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3;
+        public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3;
+        public static final short MODE_AUTO_MODE = 3 << 3;
+        // 5th bit
+        public static final short FUNCTION_PRESENT = 0 << 5;
+        public static final short FUNCTION_NO_FUNCTION = 1 << 5;
+        // 6th bit
+        public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6;
+        public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6;
+    }
+
+    /**
+     * Constants for {@link TAG_COLOR_SPACE}
+     */
+    public static interface ColorSpace {
+        public static final short SRGB = 1;
+        public static final short UNCALIBRATED = (short) 0xFFFF;
+    }
+
+    /**
+     * Constants for {@link TAG_EXPOSURE_MODE}
+     */
+    public static interface ExposureMode {
+        public static final short AUTO_EXPOSURE = 0;
+        public static final short MANUAL_EXPOSURE = 1;
+        public static final short AUTO_BRACKET = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_WHITE_BALANCE}
+     */
+    public static interface WhiteBalance {
+        public static final short AUTO = 0;
+        public static final short MANUAL = 1;
+    }
+
+    /**
+     * Constants for {@link TAG_SCENE_CAPTURE_TYPE}
+     */
+    public static interface SceneCapture {
+        public static final short STANDARD = 0;
+        public static final short LANDSCAPE = 1;
+        public static final short PROTRAIT = 2;
+        public static final short NIGHT_SCENE = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_COMPONENTS_CONFIGURATION}
+     */
+    public static interface ComponentsConfiguration {
+        public static final short NOT_EXIST = 0;
+        public static final short Y = 1;
+        public static final short CB = 2;
+        public static final short CR = 3;
+        public static final short R = 4;
+        public static final short G = 5;
+        public static final short B = 6;
+    }
+
+    /**
+     * Constants for {@link TAG_LIGHT_SOURCE}
+     */
+    public static interface LightSource {
+        public static final short UNKNOWN = 0;
+        public static final short DAYLIGHT = 1;
+        public static final short FLUORESCENT = 2;
+        public static final short TUNGSTEN = 3;
+        public static final short FLASH = 4;
+        public static final short FINE_WEATHER = 9;
+        public static final short CLOUDY_WEATHER = 10;
+        public static final short SHADE = 11;
+        public static final short DAYLIGHT_FLUORESCENT = 12;
+        public static final short DAY_WHITE_FLUORESCENT = 13;
+        public static final short COOL_WHITE_FLUORESCENT = 14;
+        public static final short WHITE_FLUORESCENT = 15;
+        public static final short STANDARD_LIGHT_A = 17;
+        public static final short STANDARD_LIGHT_B = 18;
+        public static final short STANDARD_LIGHT_C = 19;
+        public static final short D55 = 20;
+        public static final short D65 = 21;
+        public static final short D75 = 22;
+        public static final short D50 = 23;
+        public static final short ISO_STUDIO_TUNGSTEN = 24;
+        public static final short OTHER = 255;
+    }
+
+    /**
+     * Constants for {@link TAG_SENSING_METHOD}
+     */
+    public static interface SensingMethod {
+        public static final short NOT_DEFINED = 1;
+        public static final short ONE_CHIP_COLOR = 2;
+        public static final short TWO_CHIP_COLOR = 3;
+        public static final short THREE_CHIP_COLOR = 4;
+        public static final short COLOR_SEQUENTIAL_AREA = 5;
+        public static final short TRILINEAR = 7;
+        public static final short COLOR_SEQUENTIAL_LINEAR = 8;
+    }
+
+    /**
+     * Constants for {@link TAG_FILE_SOURCE}
+     */
+    public static interface FileSource {
+        public static final short DSC = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_SCENE_TYPE}
+     */
+    public static interface SceneType {
+        public static final short DIRECT_PHOTOGRAPHED = 1;
+    }
+
+    /**
+     * Constants for {@link TAG_GAIN_CONTROL}
+     */
+    public static interface GainControl {
+        public static final short NONE = 0;
+        public static final short LOW_UP = 1;
+        public static final short HIGH_UP = 2;
+        public static final short LOW_DOWN = 3;
+        public static final short HIGH_DOWN = 4;
+    }
+
+    /**
+     * Constants for {@link TAG_CONTRAST}
+     */
+    public static interface Contrast {
+        public static final short NORMAL = 0;
+        public static final short SOFT = 1;
+        public static final short HARD = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_SATURATION}
+     */
+    public static interface Saturation {
+        public static final short NORMAL = 0;
+        public static final short LOW = 1;
+        public static final short HIGH = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_SHARPNESS}
+     */
+    public static interface Sharpness {
+        public static final short NORMAL = 0;
+        public static final short SOFT = 1;
+        public static final short HARD = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_SUBJECT_DISTANCE}
+     */
+    public static interface SubjectDistance {
+        public static final short UNKNOWN = 0;
+        public static final short MACRO = 1;
+        public static final short CLOSE_VIEW = 2;
+        public static final short DISTANT_VIEW = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_LATITUDE_REF},
+     * {@link TAG_GPS_DEST_LATITUDE_REF}
+     */
+    public static interface GpsLatitudeRef {
+        public static final String NORTH = "N";
+        public static final String SOUTH = "S";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_LONGITUDE_REF},
+     * {@link TAG_GPS_DEST_LONGITUDE_REF}
+     */
+    public static interface GpsLongitudeRef {
+        public static final String EAST = "E";
+        public static final String WEST = "W";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_ALTITUDE_REF}
+     */
+    public static interface GpsAltitudeRef {
+        public static final short SEA_LEVEL = 0;
+        public static final short SEA_LEVEL_NEGATIVE = 1;
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_STATUS}
+     */
+    public static interface GpsStatus {
+        public static final String IN_PROGRESS = "A";
+        public static final String INTEROPERABILITY = "V";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_MEASURE_MODE}
+     */
+    public static interface GpsMeasureMode {
+        public static final String MODE_2_DIMENSIONAL = "2";
+        public static final String MODE_3_DIMENSIONAL = "3";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_SPEED_REF},
+     * {@link TAG_GPS_DEST_DISTANCE_REF}
+     */
+    public static interface GpsSpeedRef {
+        public static final String KILOMETERS = "K";
+        public static final String MILES = "M";
+        public static final String KNOTS = "N";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_TRACK_REF},
+     * {@link TAG_GPS_IMG_DIRECTION_REF}, {@link TAG_GPS_DEST_BEARING_REF}
+     */
+    public static interface GpsTrackRef {
+        public static final String TRUE_DIRECTION = "T";
+        public static final String MAGNETIC_DIRECTION = "M";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_DIFFERENTIAL}
+     */
+    public static interface GpsDifferential {
+        public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0;
+        public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1;
+    }
+
+    private static final String NULL_ARGUMENT_STRING = "Argument is null";
+    private ExifData mData = new ExifData(DEFAULT_BYTE_ORDER);
+    public static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
+
+    public ExifInterface() {
+        mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+    /**
+     * Reads the exif tags from a byte array, clearing this ExifInterface
+     * object's existing exif tags.
+     *
+     * @param jpeg a byte array containing a jpeg compressed image.
+     * @throws IOException
+     */
+    public void readExif(byte[] jpeg) throws IOException {
+        readExif(new ByteArrayInputStream(jpeg));
+    }
+
+    /**
+     * Reads the exif tags from an InputStream, clearing this ExifInterface
+     * object's existing exif tags.
+     *
+     * @param inStream an InputStream containing a jpeg compressed image.
+     * @throws IOException
+     */
+    public void readExif(InputStream inStream) throws IOException {
+        if (inStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        ExifData d = null;
+        try {
+            d = new ExifReader(this).read(inStream);
+        } catch (ExifInvalidFormatException e) {
+            throw new IOException("Invalid exif format : " + e);
+        }
+        mData = d;
+    }
+
+    /**
+     * Reads the exif tags from a file, clearing this ExifInterface object's
+     * existing exif tags.
+     *
+     * @param inFileName a string representing the filepath to jpeg file.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void readExif(String inFileName) throws FileNotFoundException, IOException {
+        if (inFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        InputStream is = null;
+        try {
+            is = (InputStream) new FileInputStream(inFileName);
+            readExif(is);
+        } catch (IOException e) {
+            closeSilently(is);
+            throw e;
+        }
+        is.close();
+    }
+
+    /**
+     * Sets the exif tags, clearing this ExifInterface object's existing exif
+     * tags.
+     *
+     * @param tags a collection of exif tags to set.
+     */
+    public void setExif(Collection<ExifTag> tags) {
+        clearExif();
+        setTags(tags);
+    }
+
+    /**
+     * Clears this ExifInterface object's existing exif tags.
+     */
+    public void clearExif() {
+        mData = new ExifData(DEFAULT_BYTE_ORDER);
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg image,
+     * removing prior exif tags.
+     *
+     * @param jpeg a byte array containing a jpeg compressed image.
+     * @param exifOutStream an OutputStream to which the jpeg image with added
+     *            exif tags will be written.
+     * @throws IOException
+     */
+    public void writeExif(byte[] jpeg, OutputStream exifOutStream) throws IOException {
+        if (jpeg == null || exifOutStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = getExifWriterStream(exifOutStream);
+        s.write(jpeg, 0, jpeg.length);
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg compressed
+     * bitmap, removing prior exif tags.
+     *
+     * @param bmap a bitmap to compress and write exif into.
+     * @param exifOutStream the OutputStream to which the jpeg image with added
+     *            exif tags will be written.
+     * @throws IOException
+     */
+    public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException {
+        if (bmap == null || exifOutStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = getExifWriterStream(exifOutStream);
+        bmap.compress(Bitmap.CompressFormat.JPEG, 90, s);
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg stream,
+     * removing prior exif tags.
+     *
+     * @param jpegStream an InputStream containing a jpeg compressed image.
+     * @param exifOutStream an OutputStream to which the jpeg image with added
+     *            exif tags will be written.
+     * @throws IOException
+     */
+    public void writeExif(InputStream jpegStream, OutputStream exifOutStream) throws IOException {
+        if (jpegStream == null || exifOutStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = getExifWriterStream(exifOutStream);
+        doExifStreamIO(jpegStream, s);
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg image,
+     * removing prior exif tags.
+     *
+     * @param jpeg a byte array containing a jpeg compressed image.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(byte[] jpeg, String exifOutFileName) throws FileNotFoundException,
+            IOException {
+        if (jpeg == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = null;
+        try {
+            s = getExifWriterStream(exifOutFileName);
+            s.write(jpeg, 0, jpeg.length);
+            s.flush();
+        } catch (IOException e) {
+            closeSilently(s);
+            throw e;
+        }
+        s.close();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg compressed
+     * bitmap, removing prior exif tags.
+     *
+     * @param bmap a bitmap to compress and write exif into.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(Bitmap bmap, String exifOutFileName) throws FileNotFoundException,
+            IOException {
+        if (bmap == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = null;
+        try {
+            s = getExifWriterStream(exifOutFileName);
+            bmap.compress(Bitmap.CompressFormat.JPEG, 90, s);
+            s.flush();
+        } catch (IOException e) {
+            closeSilently(s);
+            throw e;
+        }
+        s.close();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg stream,
+     * removing prior exif tags.
+     *
+     * @param jpegStream an InputStream containing a jpeg compressed image.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(InputStream jpegStream, String exifOutFileName)
+            throws FileNotFoundException, IOException {
+        if (jpegStream == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = null;
+        try {
+            s = getExifWriterStream(exifOutFileName);
+            doExifStreamIO(jpegStream, s);
+            s.flush();
+        } catch (IOException e) {
+            closeSilently(s);
+            throw e;
+        }
+        s.close();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg file, removing
+     * prior exif tags.
+     *
+     * @param jpegFileName a String containing the filepath for a jpeg file.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(String jpegFileName, String exifOutFileName)
+            throws FileNotFoundException, IOException {
+        if (jpegFileName == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        InputStream is = null;
+        try {
+            is = new FileInputStream(jpegFileName);
+            writeExif(is, exifOutFileName);
+        } catch (IOException e) {
+            closeSilently(is);
+            throw e;
+        }
+        is.close();
+    }
+
+    /**
+     * Wraps an OutputStream object with an ExifOutputStream. Exif tags in this
+     * ExifInterface object will be added to a jpeg image written to this
+     * stream, removing prior exif tags. Other methods of this ExifInterface
+     * object should not be called until the returned OutputStream has been
+     * closed.
+     *
+     * @param outStream an OutputStream to wrap.
+     * @return an OutputStream that wraps the outStream parameter, and adds exif
+     *         metadata. A jpeg image should be written to this stream.
+     */
+    public OutputStream getExifWriterStream(OutputStream outStream) {
+        if (outStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        ExifOutputStream eos = new ExifOutputStream(outStream, this);
+        eos.setExifData(mData);
+        return eos;
+    }
+
+    /**
+     * Returns an OutputStream object that writes to a file. Exif tags in this
+     * ExifInterface object will be added to a jpeg image written to this
+     * stream, removing prior exif tags. Other methods of this ExifInterface
+     * object should not be called until the returned OutputStream has been
+     * closed.
+     *
+     * @param exifOutFileName an String containing a filepath for a jpeg file.
+     * @return an OutputStream that writes to the exifOutFileName file, and adds
+     *         exif metadata. A jpeg image should be written to this stream.
+     * @throws FileNotFoundException
+     */
+    public OutputStream getExifWriterStream(String exifOutFileName) throws FileNotFoundException {
+        if (exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream out = null;
+        try {
+            out = (OutputStream) new FileOutputStream(exifOutFileName);
+        } catch (FileNotFoundException e) {
+            closeSilently(out);
+            throw e;
+        }
+        return getExifWriterStream(out);
+    }
+
+    /**
+     * Attempts to do an in-place rewrite the exif metadata in a file for the
+     * given tags. If tags do not exist or do not have the same size as the
+     * existing exif tags, this method will fail.
+     *
+     * @param filename a String containing a filepath for a jpeg file with exif
+     *            tags to rewrite.
+     * @param tags tags that will be written into the jpeg file over existing
+     *            tags if possible.
+     * @return true if success, false if could not overwrite. If false, no
+     *         changes are made to the file.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public boolean rewriteExif(String filename, Collection<ExifTag> tags)
+            throws FileNotFoundException, IOException {
+        RandomAccessFile file = null;
+        InputStream is = null;
+        boolean ret;
+        try {
+            File temp = new File(filename);
+            is = new FileInputStream(temp);
+
+            // Parse beginning of APP1 in exif to find size of exif header.
+            ExifParser parser = null;
+            try {
+                parser = ExifParser.parse(is, this);
+            } catch (ExifInvalidFormatException e) {
+                throw new IOException("Invalid exif format : ", e);
+            }
+            long exifSize = parser.getOffsetToExifEndFromSOF();
+
+            // Free up resources
+            is.close();
+            is = null;
+
+            // Open file for memory mapping.
+            file = new RandomAccessFile(temp, "rw");
+            long fileLength = file.length();
+            if (fileLength < exifSize) {
+                throw new IOException("Filesize changed during operation");
+            }
+
+            // Map only exif header into memory.
+            ByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, exifSize);
+
+            // Attempt to overwrite tag values without changing lengths (avoids
+            // file copy).
+            ret = rewriteExif(buf, tags);
+        } catch (IOException e) {
+            closeSilently(file);
+            throw e;
+        } finally {
+            closeSilently(is);
+        }
+        file.close();
+        return ret;
+    }
+
+    /**
+     * Attempts to do an in-place rewrite the exif metadata in a ByteBuffer for
+     * the given tags. If tags do not exist or do not have the same size as the
+     * existing exif tags, this method will fail.
+     *
+     * @param buf a ByteBuffer containing a jpeg file with existing exif tags to
+     *            rewrite.
+     * @param tags tags that will be written into the jpeg ByteBuffer over
+     *            existing tags if possible.
+     * @return true if success, false if could not overwrite. If false, no
+     *         changes are made to the ByteBuffer.
+     * @throws IOException
+     */
+    public boolean rewriteExif(ByteBuffer buf, Collection<ExifTag> tags) throws IOException {
+        ExifModifier mod = null;
+        try {
+            mod = new ExifModifier(buf, this);
+            for (ExifTag t : tags) {
+                mod.modifyTag(t);
+            }
+            return mod.commit();
+        } catch (ExifInvalidFormatException e) {
+            throw new IOException("Invalid exif format : " + e);
+        }
+    }
+
+    /**
+     * Attempts to do an in-place rewrite of the exif metadata. If this fails,
+     * fall back to overwriting file. This preserves tags that are not being
+     * rewritten.
+     *
+     * @param filename a String containing a filepath for a jpeg file.
+     * @param tags tags that will be written into the jpeg file over existing
+     *            tags if possible.
+     * @throws FileNotFoundException
+     * @throws IOException
+     * @see #rewriteExif
+     */
+    public void forceRewriteExif(String filename, Collection<ExifTag> tags)
+            throws FileNotFoundException,
+            IOException {
+        // Attempt in-place write
+        if (!rewriteExif(filename, tags)) {
+            // Fall back to doing a copy
+            ExifData tempData = mData;
+            mData = new ExifData(DEFAULT_BYTE_ORDER);
+            FileInputStream is = null;
+            ByteArrayOutputStream bytes = null;
+            try {
+                is = new FileInputStream(filename);
+                bytes = new ByteArrayOutputStream();
+                doExifStreamIO(is, bytes);
+                byte[] imageBytes = bytes.toByteArray();
+                readExif(imageBytes);
+                setTags(tags);
+                writeExif(imageBytes, filename);
+            } catch (IOException e) {
+                closeSilently(is);
+                throw e;
+            } finally {
+                is.close();
+                // Prevent clobbering of mData
+                mData = tempData;
+            }
+        }
+    }
+
+    /**
+     * Attempts to do an in-place rewrite of the exif metadata using the tags in
+     * this ExifInterface object. If this fails, fall back to overwriting file.
+     * This preserves tags that are not being rewritten.
+     *
+     * @param filename a String containing a filepath for a jpeg file.
+     * @throws FileNotFoundException
+     * @throws IOException
+     * @see #rewriteExif
+     */
+    public void forceRewriteExif(String filename) throws FileNotFoundException, IOException {
+        forceRewriteExif(filename, getAllTags());
+    }
+
+    /**
+     * Get the exif tags in this ExifInterface object or null if none exist.
+     *
+     * @return a List of {@link ExifTag}s.
+     */
+    public List<ExifTag> getAllTags() {
+        return mData.getAllTags();
+    }
+
+    /**
+     * Returns a list of ExifTags that share a TID (which can be obtained by
+     * calling {@link #getTrueTagKey} on a defined tag constant) or null if none
+     * exist.
+     *
+     * @param tagId a TID as defined in the exif standard (or with
+     *            {@link #defineTag}).
+     * @return a List of {@link ExifTag}s.
+     */
+    public List<ExifTag> getTagsForTagId(short tagId) {
+        return mData.getAllTagsForTagId(tagId);
+    }
+
+    /**
+     * Returns a list of ExifTags that share an IFD (which can be obtained by
+     * calling {@link #getTrueIFD} on a defined tag constant) or null if none
+     * exist.
+     *
+     * @param ifdId an IFD as defined in the exif standard (or with
+     *            {@link #defineTag}).
+     * @return a List of {@link ExifTag}s.
+     */
+    public List<ExifTag> getTagsForIfdId(int ifdId) {
+        return mData.getAllTagsForIfd(ifdId);
+    }
+
+    /**
+     * Gets an ExifTag for an IFD other than the tag's default.
+     *
+     * @see #getTag
+     */
+    public ExifTag getTag(int tagId, int ifdId) {
+        if (!ExifTag.isValidIfd(ifdId)) {
+            return null;
+        }
+        return mData.getTag(getTrueTagKey(tagId), ifdId);
+    }
+
+    /**
+     * Returns the ExifTag in that tag's default IFD for a defined tag constant
+     * or null if none exists.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return an {@link ExifTag} or null if none exists.
+     */
+    public ExifTag getTag(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTag(tagId, ifdId);
+    }
+
+    /**
+     * Gets a tag value for an IFD other than the tag's default.
+     *
+     * @see #getTagValue
+     */
+    public Object getTagValue(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        return (t == null) ? null : t.getValue();
+    }
+
+    /**
+     * Returns the value of the ExifTag in that tag's default IFD for a defined
+     * tag constant or null if none exists or the value could not be cast into
+     * the return type.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the value of the ExifTag or null if none exists.
+     */
+    public Object getTagValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagValue(tagId, ifdId);
+    }
+
+    /*
+     * Getter methods that are similar to getTagValue. Null is returned if the
+     * tag value cannot be cast into the return type.
+     */
+
+    /**
+     * @see #getTagValue
+     */
+    public String getTagStringValue(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsString();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public String getTagStringValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagStringValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Long getTagLongValue(int tagId, int ifdId) {
+        long[] l = getTagLongValues(tagId, ifdId);
+        if (l == null || l.length <= 0) {
+            return null;
+        }
+        return new Long(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Long getTagLongValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagLongValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Integer getTagIntValue(int tagId, int ifdId) {
+        int[] l = getTagIntValues(tagId, ifdId);
+        if (l == null || l.length <= 0) {
+            return null;
+        }
+        return new Integer(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Integer getTagIntValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagIntValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Byte getTagByteValue(int tagId, int ifdId) {
+        byte[] l = getTagByteValues(tagId, ifdId);
+        if (l == null || l.length <= 0) {
+            return null;
+        }
+        return new Byte(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Byte getTagByteValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagByteValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational getTagRationalValue(int tagId, int ifdId) {
+        Rational[] l = getTagRationalValues(tagId, ifdId);
+        if (l == null || l.length == 0) {
+            return null;
+        }
+        return new Rational(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational getTagRationalValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagRationalValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public long[] getTagLongValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsLongs();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public long[] getTagLongValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagLongValues(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public int[] getTagIntValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsInts();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public int[] getTagIntValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagIntValues(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public byte[] getTagByteValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsBytes();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public byte[] getTagByteValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagByteValues(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational[] getTagRationalValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsRationals();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational[] getTagRationalValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagRationalValues(tagId, ifdId);
+    }
+
+    /**
+     * Checks whether a tag has a defined number of elements.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return true if the tag has a defined number of elements.
+     */
+    public boolean isTagCountDefined(int tagId) {
+        int info = getTagInfo().get(tagId);
+        // No value in info can be zero, as all tags have a non-zero type
+        if (info == 0) {
+            return false;
+        }
+        return getComponentCountFromInfo(info) != ExifTag.SIZE_UNDEFINED;
+    }
+
+    /**
+     * Gets the defined number of elements for a tag.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the number of elements or {@link ExifTag#SIZE_UNDEFINED} if the
+     *         tag or the number of elements is not defined.
+     */
+    public int getDefinedTagCount(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0) {
+            return ExifTag.SIZE_UNDEFINED;
+        }
+        return getComponentCountFromInfo(info);
+    }
+
+    /**
+     * Gets the number of elements for an ExifTag in a given IFD.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD containing the ExifTag to check.
+     * @return the number of elements in the ExifTag, if the tag's size is
+     *         undefined this will return the actual number of elements that is
+     *         in the ExifTag's value.
+     */
+    public int getActualTagCount(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return 0;
+        }
+        return t.getComponentCount();
+    }
+
+    /**
+     * Gets the default IFD for a tag.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the default IFD for a tag definition or {@link #IFD_NULL} if no
+     *         definition exists.
+     */
+    public int getDefinedTagDefaultIfd(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == DEFINITION_NULL) {
+            return IFD_NULL;
+        }
+        return getTrueIfd(tagId);
+    }
+
+    /**
+     * Gets the defined type for a tag.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the type.
+     * @see ExifTag#getDataType()
+     */
+    public short getDefinedTagType(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0) {
+            return -1;
+        }
+        return getTypeFromInfo(info);
+    }
+
+    /**
+     * Returns true if tag TID is one of the following: {@link TAG_EXIF_IFD},
+     * {@link TAG_GPS_IFD}, {@link TAG_JPEG_INTERCHANGE_FORMAT},
+     * {@link TAG_STRIP_OFFSETS}, {@link TAG_INTEROPERABILITY_IFD}
+     * <p>
+     * Note: defining tags with these TID's is disallowed.
+     *
+     * @param tag a tag's TID (can be obtained from a defined tag constant with
+     *            {@link #getTrueTagKey}).
+     * @return true if the TID is that of an offset tag.
+     */
+    protected static boolean isOffsetTag(short tag) {
+        return sOffsetTags.contains(tag);
+    }
+
+    /**
+     * Creates a tag for a defined tag constant in a given IFD if that IFD is
+     * allowed for the tag.  This method will fail anytime the appropriate
+     * {@link ExifTag#setValue} for this tag's datatype would fail.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD that the tag should be in.
+     * @param val the value of the tag to set.
+     * @return an ExifTag object or null if one could not be constructed.
+     * @see #buildTag
+     */
+    public ExifTag buildTag(int tagId, int ifdId, Object val) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0 || val == null) {
+            return null;
+        }
+        short type = getTypeFromInfo(info);
+        int definedCount = getComponentCountFromInfo(info);
+        boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED);
+        if (!ExifInterface.isIfdAllowed(info, ifdId)) {
+            return null;
+        }
+        ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount);
+        if (!t.setValue(val)) {
+            return null;
+        }
+        return t;
+    }
+
+    /**
+     * Creates a tag for a defined tag constant in the tag's default IFD.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param val the tag's value.
+     * @return an ExifTag object.
+     */
+    public ExifTag buildTag(int tagId, Object val) {
+        int ifdId = getTrueIfd(tagId);
+        return buildTag(tagId, ifdId, val);
+    }
+
+    protected ExifTag buildUninitializedTag(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0) {
+            return null;
+        }
+        short type = getTypeFromInfo(info);
+        int definedCount = getComponentCountFromInfo(info);
+        boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED);
+        int ifdId = getTrueIfd(tagId);
+        ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount);
+        return t;
+    }
+
+    /**
+     * Sets the value of an ExifTag if it exists in the given IFD. The value
+     * must be the correct type and length for that ExifTag.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD that the ExifTag is in.
+     * @param val the value to set.
+     * @return true if success, false if the ExifTag doesn't exist or the value
+     *         is the wrong type/length.
+     * @see #setTagValue
+     */
+    public boolean setTagValue(int tagId, int ifdId, Object val) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return false;
+        }
+        return t.setValue(val);
+    }
+
+    /**
+     * Sets the value of an ExifTag if it exists it's default IFD. The value
+     * must be the correct type and length for that ExifTag.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param val the value to set.
+     * @return true if success, false if the ExifTag doesn't exist or the value
+     *         is the wrong type/length.
+     */
+    public boolean setTagValue(int tagId, Object val) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return setTagValue(tagId, ifdId, val);
+    }
+
+    /**
+     * Puts an ExifTag into this ExifInterface object's tags, removing a
+     * previous ExifTag with the same TID and IFD. The IFD it is put into will
+     * be the one the tag was created with in {@link #buildTag}.
+     *
+     * @param tag an ExifTag to put into this ExifInterface's tags.
+     * @return the previous ExifTag with the same TID and IFD or null if none
+     *         exists.
+     */
+    public ExifTag setTag(ExifTag tag) {
+        return mData.addTag(tag);
+    }
+
+    /**
+     * Puts a collection of ExifTags into this ExifInterface objects's tags. Any
+     * previous ExifTags with the same TID and IFDs will be removed.
+     *
+     * @param tags a Collection of ExifTags.
+     * @see #setTag
+     */
+    public void setTags(Collection<ExifTag> tags) {
+        for (ExifTag t : tags) {
+            setTag(t);
+        }
+    }
+
+    /**
+     * Removes the ExifTag for a tag constant from the given IFD.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD of the ExifTag to remove.
+     */
+    public void deleteTag(int tagId, int ifdId) {
+        mData.removeTag(getTrueTagKey(tagId), ifdId);
+    }
+
+    /**
+     * Removes the ExifTag for a tag constant from that tag's default IFD.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     */
+    public void deleteTag(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        deleteTag(tagId, ifdId);
+    }
+
+    /**
+     * Creates a new tag definition in this ExifInterface object for a given TID
+     * and default IFD. Creating a definition with the same TID and default IFD
+     * as a previous definition will override it.
+     *
+     * @param tagId the TID for the tag.
+     * @param defaultIfd the default IFD for the tag.
+     * @param tagType the type of the tag (see {@link ExifTag#getDataType()}).
+     * @param defaultComponentCount the number of elements of this tag's type in
+     *            the tags value.
+     * @param allowedIfds the IFD's this tag is allowed to be put in.
+     * @return the defined tag constant (e.g. {@link #TAG_IMAGE_WIDTH}) or
+     *         {@link #TAG_NULL} if the definition could not be made.
+     */
+    public int setTagDefinition(short tagId, int defaultIfd, short tagType,
+            short defaultComponentCount, int[] allowedIfds) {
+        if (sBannedDefines.contains(tagId)) {
+            return TAG_NULL;
+        }
+        if (ExifTag.isValidType(tagType) && ExifTag.isValidIfd(defaultIfd)) {
+            int tagDef = defineTag(defaultIfd, tagId);
+            if (tagDef == TAG_NULL) {
+                return TAG_NULL;
+            }
+            int[] otherDefs = getTagDefinitionsForTagId(tagId);
+            SparseIntArray infos = getTagInfo();
+            // Make sure defaultIfd is in allowedIfds
+            boolean defaultCheck = false;
+            for (int i : allowedIfds) {
+                if (defaultIfd == i) {
+                    defaultCheck = true;
+                }
+                if (!ExifTag.isValidIfd(i)) {
+                    return TAG_NULL;
+                }
+            }
+            if (!defaultCheck) {
+                return TAG_NULL;
+            }
+
+            int ifdFlags = getFlagsFromAllowedIfds(allowedIfds);
+            // Make sure no identical tags can exist in allowedIfds
+            if (otherDefs != null) {
+                for (int def : otherDefs) {
+                    int tagInfo = infos.get(def);
+                    int allowedFlags = getAllowedIfdFlagsFromInfo(tagInfo);
+                    if ((ifdFlags & allowedFlags) != 0) {
+                        return TAG_NULL;
+                    }
+                }
+            }
+            getTagInfo().put(tagDef, ifdFlags << 24 | (tagType << 16) | defaultComponentCount);
+            return tagDef;
+        }
+        return TAG_NULL;
+    }
+
+    protected int getTagDefinition(short tagId, int defaultIfd) {
+        return getTagInfo().get(defineTag(defaultIfd, tagId));
+    }
+
+    protected int[] getTagDefinitionsForTagId(short tagId) {
+        int[] ifds = IfdData.getIfds();
+        int[] defs = new int[ifds.length];
+        int counter = 0;
+        SparseIntArray infos = getTagInfo();
+        for (int i : ifds) {
+            int def = defineTag(i, tagId);
+            if (infos.get(def) != DEFINITION_NULL) {
+                defs[counter++] = def;
+            }
+        }
+        if (counter == 0) {
+            return null;
+        }
+
+        return Arrays.copyOfRange(defs, 0, counter);
+    }
+
+    protected int getTagDefinitionForTag(ExifTag tag) {
+        short type = tag.getDataType();
+        int count = tag.getComponentCount();
+        int ifd = tag.getIfd();
+        return getTagDefinitionForTag(tag.getTagId(), type, count, ifd);
+    }
+
+    protected int getTagDefinitionForTag(short tagId, short type, int count, int ifd) {
+        int[] defs = getTagDefinitionsForTagId(tagId);
+        if (defs == null) {
+            return TAG_NULL;
+        }
+        SparseIntArray infos = getTagInfo();
+        int ret = TAG_NULL;
+        for (int i : defs) {
+            int info = infos.get(i);
+            short def_type = getTypeFromInfo(info);
+            int def_count = getComponentCountFromInfo(info);
+            int[] def_ifds = getAllowedIfdsFromInfo(info);
+            boolean valid_ifd = false;
+            for (int j : def_ifds) {
+                if (j == ifd) {
+                    valid_ifd = true;
+                    break;
+                }
+            }
+            if (valid_ifd && type == def_type
+                    && (count == def_count || def_count == ExifTag.SIZE_UNDEFINED)) {
+                ret = i;
+                break;
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Removes a tag definition for given defined tag constant.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     */
+    public void removeTagDefinition(int tagId) {
+        getTagInfo().delete(tagId);
+    }
+
+    /**
+     * Resets tag definitions to the default ones.
+     */
+    public void resetTagDefinitions() {
+        mTagInfo = null;
+    }
+
+    /**
+     * Returns the thumbnail from IFD1 as a bitmap, or null if none exists.
+     *
+     * @return the thumbnail as a bitmap.
+     */
+    public Bitmap getThumbnailBitmap() {
+        if (mData.hasCompressedThumbnail()) {
+            byte[] thumb = mData.getCompressedThumbnail();
+            return BitmapFactory.decodeByteArray(thumb, 0, thumb.length);
+        } else if (mData.hasUncompressedStrip()) {
+            // TODO: implement uncompressed
+        }
+        return null;
+    }
+
+    /**
+     * Returns the thumbnail from IFD1 as a byte array, or null if none exists.
+     * The bytes may either be an uncompressed strip as specified in the exif
+     * standard or a jpeg compressed image.
+     *
+     * @return the thumbnail as a byte array.
+     */
+    public byte[] getThumbnailBytes() {
+        if (mData.hasCompressedThumbnail()) {
+            return mData.getCompressedThumbnail();
+        } else if (mData.hasUncompressedStrip()) {
+            // TODO: implement this
+        }
+        return null;
+    }
+
+    /**
+     * Returns the thumbnail if it is jpeg compressed, or null if none exists.
+     *
+     * @return the thumbnail as a byte array.
+     */
+    public byte[] getThumbnail() {
+        return mData.getCompressedThumbnail();
+    }
+
+    /**
+     * Check if thumbnail is compressed.
+     *
+     * @return true if the thumbnail is compressed.
+     */
+    public boolean isThumbnailCompressed() {
+        return mData.hasCompressedThumbnail();
+    }
+
+    /**
+     * Check if thumbnail exists.
+     *
+     * @return true if a compressed thumbnail exists.
+     */
+    public boolean hasThumbnail() {
+        // TODO: add back in uncompressed strip
+        return mData.hasCompressedThumbnail();
+    }
+
+    // TODO: uncompressed thumbnail setters
+
+    /**
+     * Sets the thumbnail to be a jpeg compressed image. Clears any prior
+     * thumbnail.
+     *
+     * @param thumb a byte array containing a jpeg compressed image.
+     * @return true if the thumbnail was set.
+     */
+    public boolean setCompressedThumbnail(byte[] thumb) {
+        mData.clearThumbnailAndStrips();
+        mData.setCompressedThumbnail(thumb);
+        return true;
+    }
+
+    /**
+     * Sets the thumbnail to be a jpeg compressed bitmap. Clears any prior
+     * thumbnail.
+     *
+     * @param thumb a bitmap to compress to a jpeg thumbnail.
+     * @return true if the thumbnail was set.
+     */
+    public boolean setCompressedThumbnail(Bitmap thumb) {
+        ByteArrayOutputStream thumbnail = new ByteArrayOutputStream();
+        if (!thumb.compress(Bitmap.CompressFormat.JPEG, 90, thumbnail)) {
+            return false;
+        }
+        return setCompressedThumbnail(thumbnail.toByteArray());
+    }
+
+    /**
+     * Clears the compressed thumbnail if it exists.
+     */
+    public void removeCompressedThumbnail() {
+        mData.setCompressedThumbnail(null);
+    }
+
+    // Convenience methods:
+
+    /**
+     * Decodes the user comment tag into string as specified in the EXIF
+     * standard. Returns null if decoding failed.
+     */
+    public String getUserComment() {
+        return mData.getUserComment();
+    }
+
+    /**
+     * Returns the Orientation ExifTag value for a given number of degrees.
+     *
+     * @param degrees the amount an image is rotated in degrees.
+     */
+    public static short getOrientationValueForRotation(int degrees) {
+        degrees %= 360;
+        if (degrees < 0) {
+            degrees += 360;
+        }
+        if (degrees < 90) {
+            return Orientation.TOP_LEFT; // 0 degrees
+        } else if (degrees < 180) {
+            return Orientation.RIGHT_TOP; // 90 degrees cw
+        } else if (degrees < 270) {
+            return Orientation.BOTTOM_LEFT; // 180 degrees
+        } else {
+            return Orientation.RIGHT_BOTTOM; // 270 degrees cw
+        }
+    }
+
+    /**
+     * Returns the rotation degrees corresponding to an ExifTag Orientation
+     * value.
+     *
+     * @param orientation the ExifTag Orientation value.
+     */
+    public static int getRotationForOrientationValue(short orientation) {
+        switch (orientation) {
+            case Orientation.TOP_LEFT:
+                return 0;
+            case Orientation.RIGHT_TOP:
+                return 90;
+            case Orientation.BOTTOM_LEFT:
+                return 180;
+            case Orientation.RIGHT_BOTTOM:
+                return 270;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Gets the double representation of the GPS latitude or longitude
+     * coordinate.
+     *
+     * @param coordinate an array of 3 Rationals representing the degrees,
+     *            minutes, and seconds of the GPS location as defined in the
+     *            exif specification.
+     * @param reference a GPS reference reperesented by a String containing "N",
+     *            "S", "E", or "W".
+     * @return the GPS coordinate represented as degrees + minutes/60 +
+     *         seconds/3600
+     */
+    public static double convertLatOrLongToDouble(Rational[] coordinate, String reference) {
+        try {
+            double degrees = coordinate[0].toDouble();
+            double minutes = coordinate[1].toDouble();
+            double seconds = coordinate[2].toDouble();
+            double result = degrees + minutes / 60.0 + seconds / 3600.0;
+            if ((reference.equals("S") || reference.equals("W"))) {
+                return -result;
+            }
+            return result;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Gets the GPS latitude and longitude as a pair of doubles from this
+     * ExifInterface object's tags, or null if the necessary tags do not exist.
+     *
+     * @return an array of 2 doubles containing the latitude, and longitude
+     *         respectively.
+     * @see #convertLatOrLongToDouble
+     */
+    public double[] getLatLongAsDoubles() {
+        Rational[] latitude = getTagRationalValues(TAG_GPS_LATITUDE);
+        String latitudeRef = getTagStringValue(TAG_GPS_LATITUDE_REF);
+        Rational[] longitude = getTagRationalValues(TAG_GPS_LONGITUDE);
+        String longitudeRef = getTagStringValue(TAG_GPS_LONGITUDE_REF);
+        if (latitude == null || longitude == null || latitudeRef == null || longitudeRef == null
+                || latitude.length < 3 || longitude.length < 3) {
+            return null;
+        }
+        double[] latLon = new double[2];
+        latLon[0] = convertLatOrLongToDouble(latitude, latitudeRef);
+        latLon[1] = convertLatOrLongToDouble(longitude, longitudeRef);
+        return latLon;
+    }
+
+    private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
+    private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss";
+    private final DateFormat mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR);
+    private final DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
+    private final Calendar mGPSTimeStampCalendar = Calendar
+            .getInstance(TimeZone.getTimeZone("UTC"));
+
+    /**
+     * Creates, formats, and sets the DateTimeStamp tag for one of:
+     * {@link #TAG_DATE_TIME}, {@link #TAG_DATE_TIME_DIGITIZED},
+     * {@link #TAG_DATE_TIME_ORIGINAL}.
+     *
+     * @param tagId one of the DateTimeStamp tags.
+     * @param timestamp a timestamp to format.
+     * @param timezone a TimeZone object.
+     * @return true if success, false if the tag could not be set.
+     */
+    public boolean addDateTimeStampTag(int tagId, long timestamp, TimeZone timezone) {
+        if (tagId == TAG_DATE_TIME || tagId == TAG_DATE_TIME_DIGITIZED
+                || tagId == TAG_DATE_TIME_ORIGINAL) {
+            mDateTimeStampFormat.setTimeZone(timezone);
+            ExifTag t = buildTag(tagId, mDateTimeStampFormat.format(timestamp));
+            if (t == null) {
+                return false;
+            }
+            setTag(t);
+        } else {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Creates and sets all to the GPS tags for a give latitude and longitude.
+     *
+     * @param latitude a GPS latitude coordinate.
+     * @param longitude a GPS longitude coordinate.
+     * @return true if success, false if they could not be created or set.
+     */
+    public boolean addGpsTags(double latitude, double longitude) {
+        ExifTag latTag = buildTag(TAG_GPS_LATITUDE, toExifLatLong(latitude));
+        ExifTag longTag = buildTag(TAG_GPS_LONGITUDE, toExifLatLong(longitude));
+        ExifTag latRefTag = buildTag(TAG_GPS_LATITUDE_REF,
+                latitude >= 0 ? ExifInterface.GpsLatitudeRef.NORTH
+                        : ExifInterface.GpsLatitudeRef.SOUTH);
+        ExifTag longRefTag = buildTag(TAG_GPS_LONGITUDE_REF,
+                longitude >= 0 ? ExifInterface.GpsLongitudeRef.EAST
+                        : ExifInterface.GpsLongitudeRef.WEST);
+        if (latTag == null || longTag == null || latRefTag == null || longRefTag == null) {
+            return false;
+        }
+        setTag(latTag);
+        setTag(longTag);
+        setTag(latRefTag);
+        setTag(longRefTag);
+        return true;
+    }
+
+    /**
+     * Creates and sets the GPS timestamp tag.
+     *
+     * @param timestamp a GPS timestamp.
+     * @return true if success, false if could not be created or set.
+     */
+    public boolean addGpsDateTimeStampTag(long timestamp) {
+        ExifTag t = buildTag(TAG_GPS_DATE_STAMP, mGPSDateStampFormat.format(timestamp));
+        if (t == null) {
+            return false;
+        }
+        setTag(t);
+        mGPSTimeStampCalendar.setTimeInMillis(timestamp);
+        t = buildTag(TAG_GPS_TIME_STAMP, new Rational[] {
+                new Rational(mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1),
+                new Rational(mGPSTimeStampCalendar.get(Calendar.MINUTE), 1),
+                new Rational(mGPSTimeStampCalendar.get(Calendar.SECOND), 1)
+        });
+        if (t == null) {
+            return false;
+        }
+        setTag(t);
+        return true;
+    }
+
+    private static Rational[] toExifLatLong(double value) {
+        // convert to the format dd/1 mm/1 ssss/100
+        value = Math.abs(value);
+        int degrees = (int) value;
+        value = (value - degrees) * 60;
+        int minutes = (int) value;
+        value = (value - minutes) * 6000;
+        int seconds = (int) value;
+        return new Rational[] {
+                new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100)
+        };
+    }
+
+    private void doExifStreamIO(InputStream is, OutputStream os) throws IOException {
+        byte[] buf = new byte[1024];
+        int ret = is.read(buf, 0, 1024);
+        while (ret != -1) {
+            os.write(buf, 0, ret);
+            ret = is.read(buf, 0, 1024);
+        }
+    }
+
+    protected static void closeSilently(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (Throwable e) {
+                // ignored
+            }
+        }
+    }
+
+    private SparseIntArray mTagInfo = null;
+
+    protected SparseIntArray getTagInfo() {
+        if (mTagInfo == null) {
+            mTagInfo = new SparseIntArray();
+            initTagInfo();
+        }
+        return mTagInfo;
+    }
+
+    private void initTagInfo() {
+        /**
+         * We put tag information in a 4-bytes integer. The first byte a bitmask
+         * representing the allowed IFDs of the tag, the second byte is the data
+         * type, and the last two byte are a short value indicating the default
+         * component count of this tag.
+         */
+        // IFD0 tags
+        int[] ifdAllowedIfds = {
+                IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1
+        };
+        int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_MAKE,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_WIDTH,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_LENGTH,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_BITS_PER_SAMPLE,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_COMPRESSION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16
+                | 1);
+        mTagInfo.put(ExifInterface.TAG_SAMPLES_PER_PIXEL,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PLANAR_CONFIGURATION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_Y_CB_CR_POSITIONING,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_X_RESOLUTION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_Y_RESOLUTION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_RESOLUTION_UNIT,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_ROWS_PER_STRIP,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_TRANSFER_FUNCTION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3 * 256);
+        mTagInfo.put(ExifInterface.TAG_WHITE_POINT,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_PRIMARY_CHROMATICITIES,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6);
+        mTagInfo.put(ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_REFERENCE_BLACK_WHITE,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6);
+        mTagInfo.put(ExifInterface.TAG_DATE_TIME,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | 20);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_DESCRIPTION,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_MAKE,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_MODEL,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SOFTWARE,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_ARTIST,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_COPYRIGHT,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_EXIF_IFD,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_IFD,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        // IFD1 tags
+        int[] ifd1AllowedIfds = {
+            IfdId.TYPE_IFD_1
+        };
+        int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+                ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+                ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        // Exif tags
+        int[] exifAllowedIfds = {
+            IfdId.TYPE_IFD_EXIF
+        };
+        int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_EXIF_VERSION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_FLASHPIX_VERSION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_COLOR_SPACE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_COMPONENTS_CONFIGURATION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PIXEL_X_DIMENSION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PIXEL_Y_DIMENSION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_MAKER_NOTE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_USER_COMMENT,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_RELATED_SOUND_FILE,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 13);
+        mTagInfo.put(ExifInterface.TAG_DATE_TIME_ORIGINAL,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 20);
+        mTagInfo.put(ExifInterface.TAG_DATE_TIME_DIGITIZED,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 20);
+        mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_ORIGINAL,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_DIGITIZED,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_UNIQUE_ID,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 33);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_TIME,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_F_NUMBER,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_PROGRAM,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SPECTRAL_SENSITIVITY,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_ISO_SPEED_RATINGS,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_OECF,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE,
+                exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_APERTURE_VALUE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_BRIGHTNESS_VALUE,
+                exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
+                exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_MAX_APERTURE_VALUE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_METERING_MODE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_LIGHT_SOURCE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FLASH,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_AREA,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_FLASH_ENERGY,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_LOCATION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_INDEX,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SENSING_METHOD,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FILE_SOURCE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SCENE_TYPE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_CFA_PATTERN,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_CUSTOM_RENDERED,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_MODE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_WHITE_BALANCE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH_IN_35_MM_FILE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SCENE_CAPTURE_TYPE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GAIN_CONTROL,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_CONTRAST,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SATURATION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SHARPNESS,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags
+                | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        // GPS tag
+        int[] gpsAllowedIfds = {
+            IfdId.TYPE_IFD_GPS
+        };
+        int gpsFlags = getFlagsFromAllowedIfds(gpsAllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_GPS_VERSION_ID,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE,
+                gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE,
+                gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE_REF,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_TIME_STAMP,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_GPS_SATTELLITES,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_STATUS,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_MEASURE_MODE,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DOP,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_SPEED_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_SPEED,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_TRACK_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_TRACK,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_MAP_DATUM,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_PROCESSING_METHOD,
+                gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_AREA_INFORMATION,
+                gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_DATE_STAMP,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 11);
+        mTagInfo.put(ExifInterface.TAG_GPS_DIFFERENTIAL,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 11);
+        // Interoperability tag
+        int[] interopAllowedIfds = {
+            IfdId.TYPE_IFD_INTEROPERABILITY
+        };
+        int interopFlags = getFlagsFromAllowedIfds(interopAllowedIfds) << 24;
+        mTagInfo.put(TAG_INTEROPERABILITY_INDEX, interopFlags | ExifTag.TYPE_ASCII << 16
+                | ExifTag.SIZE_UNDEFINED);
+    }
+
+    protected static int getAllowedIfdFlagsFromInfo(int info) {
+        return info >>> 24;
+    }
+
+    protected static int[] getAllowedIfdsFromInfo(int info) {
+        int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+        int[] ifds = IfdData.getIfds();
+        ArrayList<Integer> l = new ArrayList<Integer>();
+        for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+            int flag = (ifdFlags >> i) & 1;
+            if (flag == 1) {
+                l.add(ifds[i]);
+            }
+        }
+        if (l.size() <= 0) {
+            return null;
+        }
+        int[] ret = new int[l.size()];
+        int j = 0;
+        for (int i : l) {
+            ret[j++] = i;
+        }
+        return ret;
+    }
+
+    protected static boolean isIfdAllowed(int info, int ifd) {
+        int[] ifds = IfdData.getIfds();
+        int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+        for (int i = 0; i < ifds.length; i++) {
+            if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected static int getFlagsFromAllowedIfds(int[] allowedIfds) {
+        if (allowedIfds == null || allowedIfds.length == 0) {
+            return 0;
+        }
+        int flags = 0;
+        int[] ifds = IfdData.getIfds();
+        for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+            for (int j : allowedIfds) {
+                if (ifds[i] == j) {
+                    flags |= 1 << i;
+                    break;
+                }
+            }
+        }
+        return flags;
+    }
+
+    protected static short getTypeFromInfo(int info) {
+        return (short) ((info >> 16) & 0x0ff);
+    }
+
+    protected static int getComponentCountFromInfo(int info) {
+        return info & 0x0ffff;
+    }
+
+}
index da31a29..f00362b 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.gallery3d.exif;
 
+import android.util.Log;
+
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
@@ -24,10 +26,13 @@ import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.List;
 
-public class ExifModifier {
+class ExifModifier {
+    public static final String TAG = "ExifModifier";
+    public static final boolean DEBUG = false;
     private final ByteBuffer mByteBuffer;
     private final ExifData mTagToModified;
     private final List<TagOffset> mTagOffsets = new ArrayList<TagOffset>();
+    private final ExifInterface mInterface;
     private int mOffsetBase;
 
     private static class TagOffset {
@@ -38,33 +43,31 @@ public class ExifModifier {
             mTag = tag;
             mOffset = offset;
         }
-
-        public ExifTag getTag() {
-            return mTag;
-        }
     }
 
-    public ExifModifier(ByteBuffer byteBuffer) throws IOException, ExifInvalidFormatException {
+    protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException,
+            ExifInvalidFormatException {
         mByteBuffer = byteBuffer;
         mOffsetBase = byteBuffer.position();
+        mInterface = iRef;
         InputStream is = null;
         try {
             is = new ByteBufferInputStream(byteBuffer);
             // Do not require any IFD;
-            ExifParser parser = ExifParser.parse(is, 0);
+            ExifParser parser = ExifParser.parse(is, mInterface);
             mTagToModified = new ExifData(parser.getByteOrder());
             mOffsetBase += parser.getTiffStartPosition();
             mByteBuffer.position(0);
         } finally {
-            closeSilently(is);
+            ExifInterface.closeSilently(is);
         }
     }
 
-    public ByteOrder getByteOrder() {
+    protected ByteOrder getByteOrder() {
         return mTagToModified.getByteOrder();
     }
 
-    public boolean commit() throws IOException, ExifInvalidFormatException {
+    protected boolean commit() throws IOException, ExifInvalidFormatException {
         InputStream is = null;
         try {
             is = new ByteBufferInputStream(mByteBuffer);
@@ -77,22 +80,32 @@ public class ExifModifier {
                     mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS)
             };
 
-            if (ifdDatas[IfdId.TYPE_IFD_0] != null) flag |= ExifParser.OPTION_IFD_0;
-            if (ifdDatas[IfdId.TYPE_IFD_1] != null) flag |= ExifParser.OPTION_IFD_1;
-            if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) flag |= ExifParser.OPTION_IFD_EXIF;
-            if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) flag |= ExifParser.OPTION_IFD_GPS;
+            if (ifdDatas[IfdId.TYPE_IFD_0] != null) {
+                flag |= ExifParser.OPTION_IFD_0;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_1] != null) {
+                flag |= ExifParser.OPTION_IFD_1;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) {
+                flag |= ExifParser.OPTION_IFD_EXIF;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) {
+                flag |= ExifParser.OPTION_IFD_GPS;
+            }
             if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) {
                 flag |= ExifParser.OPTION_IFD_INTEROPERABILITY;
             }
 
-            ExifParser parser = ExifParser.parse(is, flag);
+            ExifParser parser = ExifParser.parse(is, flag, mInterface);
             int event = parser.next();
             IfdData currIfd = null;
             while (event != ExifParser.EVENT_END) {
                 switch (event) {
                     case ExifParser.EVENT_START_OF_IFD:
                         currIfd = ifdDatas[parser.getCurrentIfd()];
-                        if (currIfd == null) parser.skipRemainingTagsInCurrentIfd();
+                        if (currIfd == null) {
+                            parser.skipRemainingTagsInCurrentIfd();
+                        }
                         break;
                     case ExifParser.EVENT_NEW_TAG:
                         ExifTag oldTag = parser.getTag();
@@ -113,24 +126,30 @@ public class ExifModifier {
                 }
                 event = parser.next();
             }
-            for (IfdData ifd: ifdDatas) {
-                if (ifd != null && ifd.getTagCount() > 0) return false;
+            for (IfdData ifd : ifdDatas) {
+                if (ifd != null && ifd.getTagCount() > 0) {
+                    return false;
+                }
             }
             modify();
         } finally {
-            closeSilently(is);
+            ExifInterface.closeSilently(is);
         }
         return true;
     }
 
     private void modify() {
         mByteBuffer.order(getByteOrder());
-        for (TagOffset tagOffset: mTagOffsets) {
+        for (TagOffset tagOffset : mTagOffsets) {
             writeTagValue(tagOffset.mTag, tagOffset.mOffset);
         }
     }
 
     private void writeTagValue(ExifTag tag, int offset) {
+        if (DEBUG) {
+            Log.v(TAG, "modifying tag to: \n" + tag.toString());
+            Log.v(TAG, "at offset: " + offset);
+        }
         mByteBuffer.position(offset + mOffsetBase);
         switch (tag.getDataType()) {
             case ExifTag.TYPE_ASCII:
@@ -153,7 +172,7 @@ public class ExifModifier {
             case ExifTag.TYPE_UNSIGNED_RATIONAL:
                 for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
                     Rational v = tag.getRational(i);
-                    mByteBuffer.putInt((int) v.getNominator());
+                    mByteBuffer.putInt((int) v.getNumerator());
                     mByteBuffer.putInt((int) v.getDenominator());
                 }
                 break;
@@ -174,13 +193,4 @@ public class ExifModifier {
     public void modifyTag(ExifTag tag) {
         mTagToModified.addTag(tag);
     }
-
-    private static void closeSilently(Closeable c) {
-        if (c == null) return;
-        try {
-            c.close();
-        } catch (Throwable t) {
-            // do nothing
-        }
-    }
 }
index f34321b..6ea6554 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.gallery3d.exif;
 
+import android.util.Log;
+
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -26,16 +28,15 @@ import java.nio.ByteOrder;
  * This class provides a way to replace the Exif header of a JPEG image.
  * <p>
  * Below is an example of writing EXIF data into a file
+ *
  * <pre>
  * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
  *     OutputStream os = null;
  *     try {
  *         os = new FileOutputStream(path);
  *         ExifOutputStream eos = new ExifOutputStream(os);
- *
  *         // Set the exif header
  *         eos.setExifData(exif);
- *
  *         // Write the original jpeg out, the header will be add into the file.
  *         eos.write(jpeg);
  *     } catch (FileNotFoundException e) {
@@ -54,8 +55,9 @@ import java.nio.ByteOrder;
  * }
  * </pre>
  */
-public class ExifOutputStream extends FilterOutputStream {
+class ExifOutputStream extends FilterOutputStream {
     private static final String TAG = "ExifOutputStream";
+    private static final boolean DEBUG = false;
 
     private static final int STATE_SOI = 0;
     private static final int STATE_FRAME_HEADER = 1;
@@ -67,29 +69,32 @@ public class ExifOutputStream extends FilterOutputStream {
     private static final short TIFF_LITTLE_ENDIAN = 0x4949;
     private static final short TAG_SIZE = 12;
     private static final short TIFF_HEADER_SIZE = 8;
+    private static final int MAX_EXIF_SIZE = 65535;
 
     private ExifData mExifData;
     private int mState = STATE_SOI;
     private int mByteToSkip;
     private int mByteToCopy;
     private ByteBuffer mBuffer = ByteBuffer.allocate(4);
+    private final ExifInterface mInterface;
 
-    public ExifOutputStream(OutputStream ou) {
+    protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
         super(ou);
+        mInterface = iRef;
     }
 
     /**
-     * Sets the ExifData to be written into the JPEG file. Should be called before writing image
-     * data.
+     * Sets the ExifData to be written into the JPEG file. Should be called
+     * before writing image data.
      */
-    public void setExifData(ExifData exifData) {
+    protected void setExifData(ExifData exifData) {
         mExifData = exifData;
     }
 
     /**
      * Gets the Exif header to be written into the JPEF file.
      */
-    public ExifData getExifData() {
+    protected ExifData getExifData() {
         return mExifData;
     }
 
@@ -102,12 +107,12 @@ public class ExifOutputStream extends FilterOutputStream {
     }
 
     /**
-     * Writes the image out. The input data should be a valid JPEG format. After writing, it's
-     * Exif header will be replaced by the given header.
+     * Writes the image out. The input data should be a valid JPEG format. After
+     * writing, it's Exif header will be replaced by the given header.
      */
     @Override
     public void write(byte[] buffer, int offset, int length) throws IOException {
-        while((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
+        while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
                 && length > 0) {
             if (mByteToSkip > 0) {
                 int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
@@ -122,22 +127,29 @@ public class ExifOutputStream extends FilterOutputStream {
                 mByteToCopy -= byteToProcess;
                 offset += byteToProcess;
             }
-            if (length == 0) return;
+            if (length == 0) {
+                return;
+            }
             switch (mState) {
                 case STATE_SOI:
                     int byteRead = requestByteToBuffer(2, buffer, offset, length);
                     offset += byteRead;
                     length -= byteRead;
-                    if (mBuffer.position() < 2) return;
+                    if (mBuffer.position() < 2) {
+                        return;
+                    }
                     mBuffer.rewind();
-                    assert(mBuffer.getShort() == JpegHeader.SOI);
-                    out.write(mBuffer.array(), 0 ,2);
+                    if (mBuffer.getShort() != JpegHeader.SOI) {
+                        throw new IOException("Not a valid jpeg image, cannot write exif");
+                    }
+                    out.write(mBuffer.array(), 0, 2);
                     mState = STATE_FRAME_HEADER;
                     mBuffer.rewind();
                     writeExifData();
                     break;
                 case STATE_FRAME_HEADER:
-                    // We ignore the APP1 segment and copy all other segments until SOF tag.
+                    // We ignore the APP1 segment and copy all other segments
+                    // until SOF tag.
                     byteRead = requestByteToBuffer(4, buffer, offset, length);
                     offset += byteRead;
                     length -= byteRead;
@@ -149,15 +161,17 @@ public class ExifOutputStream extends FilterOutputStream {
                             mBuffer.rewind();
                         }
                     }
-                    if (mBuffer.position() < 4) return;
+                    if (mBuffer.position() < 4) {
+                        return;
+                    }
                     mBuffer.rewind();
                     short marker = mBuffer.getShort();
                     if (marker == JpegHeader.APP1) {
-                        mByteToSkip = (mBuffer.getShort() & 0xff) - 2;
+                        mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
                         mState = STATE_JPEG_DATA;
                     } else if (!JpegHeader.isSofMarker(marker)) {
                         out.write(mBuffer.array(), 0, 4);
-                        mByteToCopy = (mBuffer.getShort() & 0xff) - 2;
+                        mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
                     } else {
                         out.write(mBuffer.array(), 0, 4);
                         mState = STATE_JPEG_DATA;
@@ -171,12 +185,14 @@ public class ExifOutputStream extends FilterOutputStream {
     }
 
     /**
-     * Writes the one bytes out. The input data should be a valid JPEG format. After writing, it's
-     * Exif header will be replaced by the given header.
+     * Writes the one bytes out. The input data should be a valid JPEG format.
+     * After writing, it's Exif header will be replaced by the given header.
      */
     @Override
     public void write(int oneByte) throws IOException {
-        byte[] buf = new byte[] {(byte) (0xff & oneByte)};
+        byte[] buf = new byte[] {
+            (byte) (0xff & oneByte)
+        };
         write(buf);
     }
 
@@ -189,9 +205,17 @@ public class ExifOutputStream extends FilterOutputStream {
     }
 
     private void writeExifData() throws IOException {
-        if (mExifData == null) return;
+        if (mExifData == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "Writing exif data...");
+        }
         createRequiredIfdAndTag();
         int exifSize = calculateAllOffset();
+        if (exifSize + 8 > MAX_EXIF_SIZE) {
+            throw new IOException("Exif header is too large (>64Kb)");
+        }
         OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
         dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
         dataOutputStream.writeShort(JpegHeader.APP1);
@@ -241,70 +265,34 @@ public class ExifOutputStream extends FilterOutputStream {
             throws IOException {
         ExifTag[] tags = ifd.getAllTags();
         dataOutputStream.writeShort((short) tags.length);
-        for (ExifTag tag: tags) {
+        for (ExifTag tag : tags) {
             dataOutputStream.writeShort(tag.getTagId());
             dataOutputStream.writeShort(tag.getDataType());
             dataOutputStream.writeInt(tag.getComponentCount());
+            if (DEBUG) {
+                Log.v(TAG, "\n" + tag.toString());
+            }
             if (tag.getDataSize() > 4) {
                 dataOutputStream.writeInt(tag.getOffset());
             } else {
-                writeTagValue(tag, dataOutputStream);
+                ExifOutputStream.writeTagValue(tag, dataOutputStream);
                 for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
                     dataOutputStream.write(0);
                 }
             }
         }
         dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
-        for (ExifTag tag: tags) {
+        for (ExifTag tag : tags) {
             if (tag.getDataSize() > 4) {
-                writeTagValue(tag, dataOutputStream);
+                ExifOutputStream.writeTagValue(tag, dataOutputStream);
             }
         }
     }
 
-    private void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
-            throws IOException {
-        switch (tag.getDataType()) {
-            case ExifTag.TYPE_ASCII:
-                byte buf[] = tag.getStringByte();
-                if (buf.length == tag.getComponentCount()) {
-                    buf[buf.length - 1] = 0;
-                    dataOutputStream.write(buf);
-                } else {
-                    dataOutputStream.write(buf);
-                    dataOutputStream.write(0);
-                }
-                break;
-            case ExifTag.TYPE_LONG:
-            case ExifTag.TYPE_UNSIGNED_LONG:
-                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    dataOutputStream.writeInt((int) tag.getValueAt(i));
-                }
-                break;
-            case ExifTag.TYPE_RATIONAL:
-            case ExifTag.TYPE_UNSIGNED_RATIONAL:
-                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    dataOutputStream.writeRational(tag.getRational(i));
-                }
-                break;
-            case ExifTag.TYPE_UNDEFINED:
-            case ExifTag.TYPE_UNSIGNED_BYTE:
-                buf = new byte[tag.getComponentCount()];
-                tag.getBytes(buf);
-                dataOutputStream.write(buf);
-                break;
-            case ExifTag.TYPE_UNSIGNED_SHORT:
-                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    dataOutputStream.writeShort((short) tag.getValueAt(i));
-                }
-                break;
-        }
-    }
-
     private int calculateOffsetOfIfd(IfdData ifd, int offset) {
         offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
         ExifTag[] tags = ifd.getAllTags();
-        for(ExifTag tag: tags) {
+        for (ExifTag tag : tags) {
             if (tag.getDataSize() > 4) {
                 tag.setOffset(offset);
                 offset += tag.getDataSize();
@@ -313,18 +301,21 @@ public class ExifOutputStream extends FilterOutputStream {
         return offset;
     }
 
-    private void createRequiredIfdAndTag() {
+    private void createRequiredIfdAndTag() throws IOException {
         // IFD0 is required for all file
         IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
         if (ifd0 == null) {
             ifd0 = new IfdData(IfdId.TYPE_IFD_0);
             mExifData.addIfdData(ifd0);
         }
-        ExifTag exifOffsetTag = new ExifTag(ExifTag.TAG_EXIF_IFD,
-                ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_0);
+        ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
+        if (exifOffsetTag == null) {
+            throw new IOException("No definition for crucial exif tag: "
+                    + ExifInterface.TAG_EXIF_IFD);
+        }
         ifd0.setTag(exifOffsetTag);
 
-        // Exif IFD is required for all file.
+        // Exif IFD is required for all files.
         IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
         if (exifIfd == null) {
             exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
@@ -334,16 +325,23 @@ public class ExifOutputStream extends FilterOutputStream {
         // GPS IFD
         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
         if (gpsIfd != null) {
-            ExifTag gpsOffsetTag = new ExifTag(ExifTag.TAG_GPS_IFD,
-                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_0);
+            ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
+            if (gpsOffsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_GPS_IFD);
+            }
             ifd0.setTag(gpsOffsetTag);
         }
 
         // Interoperability IFD
         IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
         if (interIfd != null) {
-            ExifTag interOffsetTag = new ExifTag(ExifTag.TAG_INTEROPERABILITY_IFD,
-                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_EXIF);
+            ExifTag interOffsetTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
+            if (interOffsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_INTEROPERABILITY_IFD);
+            }
             exifIfd.setTag(interOffsetTag);
         }
 
@@ -351,27 +349,50 @@ public class ExifOutputStream extends FilterOutputStream {
 
         // thumbnail
         if (mExifData.hasCompressedThumbnail()) {
+
             if (ifd1 == null) {
                 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
                 mExifData.addIfdData(ifd1);
             }
-            ExifTag offsetTag = new ExifTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT,
-                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_1);
+
+            ExifTag offsetTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+            if (offsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+            }
+
             ifd1.setTag(offsetTag);
-            ExifTag lengthTag = new ExifTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
-                    ExifTag.TYPE_UNSIGNED_LONG, 1, IfdId.TYPE_IFD_1);
+            ExifTag lengthTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+            if (lengthTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+            }
+
             lengthTag.setValue(mExifData.getCompressedThumbnail().length);
             ifd1.setTag(lengthTag);
-        } else if (mExifData.hasUncompressedStrip()){
+
+            // Get rid of tags for uncompressed if they exist.
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+        } else if (mExifData.hasUncompressedStrip()) {
             if (ifd1 == null) {
                 ifd1 = new IfdData(IfdId.TYPE_IFD_1);
                 mExifData.addIfdData(ifd1);
             }
             int stripCount = mExifData.getStripCount();
-            ExifTag offsetTag = new ExifTag(ExifTag.TAG_STRIP_OFFSETS,
-                    ExifTag.TYPE_UNSIGNED_LONG, stripCount, IfdId.TYPE_IFD_1);
-            ExifTag lengthTag = new ExifTag(ExifTag.TAG_STRIP_BYTE_COUNTS,
-                    ExifTag.TYPE_UNSIGNED_LONG, stripCount, IfdId.TYPE_IFD_1);
+            ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
+            if (offsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_STRIP_OFFSETS);
+            }
+            ExifTag lengthTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+            if (lengthTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_STRIP_BYTE_COUNTS);
+            }
             long[] lengths = new long[stripCount];
             for (int i = 0; i < mExifData.getStripCount(); i++) {
                 lengths[i] = mExifData.getStrip(i).length;
@@ -379,6 +400,17 @@ public class ExifOutputStream extends FilterOutputStream {
             lengthTag.setValue(lengths);
             ifd1.setTag(offsetTag);
             ifd1.setTag(lengthTag);
+            // Get rid of tags for compressed if they exist.
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
+            ifd1.removeTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+        } else {
+            // Get rid of offset and length tags if there is no thumbnail.
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
+            ifd1.removeTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
         }
     }
 
@@ -386,20 +418,21 @@ public class ExifOutputStream extends FilterOutputStream {
         int offset = TIFF_HEADER_SIZE;
         IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
         offset = calculateOffsetOfIfd(ifd0, offset);
-        ifd0.getTag(ExifTag.TAG_EXIF_IFD).setValue(offset);
+        ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
 
         IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
         offset = calculateOffsetOfIfd(exifIfd, offset);
 
         IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
         if (interIfd != null) {
-            exifIfd.getTag(ExifTag.TAG_INTEROPERABILITY_IFD).setValue(offset);
+            exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
+                    .setValue(offset);
             offset = calculateOffsetOfIfd(interIfd, offset);
         }
 
         IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
         if (gpsIfd != null) {
-            ifd0.getTag(ExifTag.TAG_GPS_IFD).setValue(offset);
+            ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
             offset = calculateOffsetOfIfd(gpsIfd, offset);
         }
 
@@ -411,17 +444,58 @@ public class ExifOutputStream extends FilterOutputStream {
 
         // thumbnail
         if (mExifData.hasCompressedThumbnail()) {
-            ifd1.getTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT).setValue(offset);
+            ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
+                    .setValue(offset);
             offset += mExifData.getCompressedThumbnail().length;
-        } else if (mExifData.hasUncompressedStrip()){
+        } else if (mExifData.hasUncompressedStrip()) {
             int stripCount = mExifData.getStripCount();
             long[] offsets = new long[stripCount];
             for (int i = 0; i < mExifData.getStripCount(); i++) {
                 offsets[i] = offset;
                 offset += mExifData.getStrip(i).length;
             }
-            ifd1.getTag(ExifTag.TAG_STRIP_OFFSETS).setValue(offsets);
+            ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
+                    offsets);
         }
         return offset;
     }
-}
\ No newline at end of file
+
+    static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
+            throws IOException {
+        switch (tag.getDataType()) {
+            case ExifTag.TYPE_ASCII:
+                byte buf[] = tag.getStringByte();
+                if (buf.length == tag.getComponentCount()) {
+                    buf[buf.length - 1] = 0;
+                    dataOutputStream.write(buf);
+                } else {
+                    dataOutputStream.write(buf);
+                    dataOutputStream.write(0);
+                }
+                break;
+            case ExifTag.TYPE_LONG:
+            case ExifTag.TYPE_UNSIGNED_LONG:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    dataOutputStream.writeInt((int) tag.getValueAt(i));
+                }
+                break;
+            case ExifTag.TYPE_RATIONAL:
+            case ExifTag.TYPE_UNSIGNED_RATIONAL:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    dataOutputStream.writeRational(tag.getRational(i));
+                }
+                break;
+            case ExifTag.TYPE_UNDEFINED:
+            case ExifTag.TYPE_UNSIGNED_BYTE:
+                buf = new byte[tag.getComponentCount()];
+                tag.getBytes(buf);
+                dataOutputStream.write(buf);
+                break;
+            case ExifTag.TYPE_UNSIGNED_SHORT:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    dataOutputStream.writeShort((short) tag.getValueAt(i));
+                }
+                break;
+        }
+    }
+}
index 1e4803f..b6d7e61 100644 (file)
@@ -26,10 +26,13 @@ import java.util.Map.Entry;
 import java.util.TreeMap;
 
 /**
- * This class provides a low-level EXIF parsing API. Given a JPEG format InputStream, the caller
- * can request which IFD's to read via {@link #parse(InputStream, int)} with given options.
+ * This class provides a low-level EXIF parsing API. Given a JPEG format
+ * InputStream, the caller can request which IFD's to read via
+ * {@link #parse(InputStream, int)} with given options.
  * <p>
- * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the parser.
+ * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the
+ * parser.
+ *
  * <pre>
  * void parse() {
  *     ExifParser parser = ExifParser.parse(mImageInputStream,
@@ -63,11 +66,12 @@ import java.util.TreeMap;
  * }
  * </pre>
  */
-public class ExifParser {
+class ExifParser {
+    private static final boolean LOGV = false;
     private static final String TAG = "ExifParser";
     /**
-     * When the parser reaches a new IFD area. Call
-     *  {@link #getCurrentIfd()} to know which IFD we are in.
+     * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to
+     * know which IFD we are in.
      */
     public static final int EVENT_START_OF_IFD = 0;
     /**
@@ -77,8 +81,8 @@ public class ExifParser {
     public static final int EVENT_NEW_TAG = 1;
     /**
      * When the parser reaches the value area of tag that is registered by
-     *  {@link #registerForTagValue(ExifTag)} previously. Call
-     *  {@link #getTag()} to get the corresponding tag.
+     * {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()}
+     * to get the corresponding tag.
      */
     public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
 
@@ -87,8 +91,9 @@ public class ExifParser {
      */
     public static final int EVENT_COMPRESSED_IMAGE = 3;
     /**
-     * When the parser reaches the uncompressed image strip.
-     *  Call {@link #getStripIndex()} to get the index of the strip.
+     * When the parser reaches the uncompressed image strip. Call
+     * {@link #getStripIndex()} to get the index of the strip.
+     *
      * @see #getStripIndex()
      * @see #getStripCount()
      */
@@ -123,20 +128,20 @@ public class ExifParser {
      */
     public static final int OPTION_THUMBNAIL = 1 << 5;
 
-    private static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
-    private static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
+    protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+    protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
 
     // TIFF header
-    private static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
-    private static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
-    private static final short TIFF_HEADER_TAIL = 0x002A;
+    protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
+    protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
+    protected static final short TIFF_HEADER_TAIL = 0x002A;
 
-    private static final int TAG_SIZE = 12;
-    private static final int OFFSET_SIZE = 2;
+    protected static final int TAG_SIZE = 12;
+    protected static final int OFFSET_SIZE = 2;
 
     private static final Charset US_ASCII = Charset.forName("US-ASCII");
 
-    private static final int DEFAULT_IFD0_OFFSET = 8;
+    protected static final int DEFAULT_IFD0_OFFSET = 8;
 
     private final CountedDataInputStream mTiffStream;
     private final int mOptions;
@@ -151,9 +156,25 @@ public class ExifParser {
     private boolean mNeedToParseOffsetsInCurrentIfd;
     private boolean mContainExifData = false;
     private int mApp1End;
+    private int mOffsetToApp1EndFromSOF = 0;
     private byte[] mDataAboveIfd0;
     private int mIfd0Position;
     private int mTiffStartPosition;
+    private final ExifInterface mInterface;
+
+    private static final short TAG_EXIF_IFD = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
+    private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
+    private static final short TAG_INTEROPERABILITY_IFD = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
+    private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+    private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+    private static final short TAG_STRIP_OFFSETS = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
+    private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
 
     private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>();
 
@@ -177,15 +198,21 @@ public class ExifParser {
         return (mOptions & OPTION_THUMBNAIL) != 0;
     }
 
-    private ExifParser(InputStream inputStream, int options)
+    private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
             throws IOException, ExifInvalidFormatException {
         if (inputStream == null) {
             throw new IOException("Null argument inputStream to ExifParser");
         }
+        if (LOGV) {
+            Log.v(TAG, "Reading exif...");
+        }
+        mInterface = iRef;
         mContainExifData = seekTiffData(inputStream);
         mTiffStream = new CountedDataInputStream(inputStream);
         mOptions = options;
-        if (!mContainExifData) return;
+        if (!mContainExifData) {
+            return;
+        }
 
         parseTiffHeader();
         long offset = mTiffStream.readUnsignedInt();
@@ -205,26 +232,28 @@ public class ExifParser {
 
     /**
      * Parses the the given InputStream with the given options
+     *
      * @exception IOException
      * @exception ExifInvalidFormatException
      */
-    public static ExifParser parse(InputStream inputStream, int options)
+    protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
             throws IOException, ExifInvalidFormatException {
-        return new ExifParser(inputStream, options);
+        return new ExifParser(inputStream, options, iRef);
     }
 
     /**
-     * Parses the the given InputStream with default options; that is, every IFD and thumbnaill
-     * will be parsed.
+     * Parses the the given InputStream with default options; that is, every IFD
+     * and thumbnaill will be parsed.
+     *
      * @exception IOException
      * @exception ExifInvalidFormatException
      * @see #parse(InputStream, int)
      */
-    public static ExifParser parse(InputStream inputStream)
+    protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
             throws IOException, ExifInvalidFormatException {
         return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1
                 | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY
-                | OPTION_THUMBNAIL);
+                | OPTION_THUMBNAIL, iRef);
     }
 
     /**
@@ -239,7 +268,7 @@ public class ExifParser {
      * @see #EVENT_UNCOMPRESSED_STRIP
      * @see #EVENT_END
      */
-    public int next() throws IOException, ExifInvalidFormatException {
+    protected int next() throws IOException, ExifInvalidFormatException {
         if (!mContainExifData) {
             return EVENT_END;
         }
@@ -280,7 +309,7 @@ public class ExifParser {
                 }
             }
         }
-        while(mCorrespondingEvent.size() != 0) {
+        while (mCorrespondingEvent.size() != 0) {
             Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
             Object event = entry.getValue();
             try {
@@ -325,21 +354,25 @@ public class ExifParser {
     }
 
     /**
-     * Skips the tags area of current IFD, if the parser is not in the tag area, nothing will
-     * happen.
+     * Skips the tags area of current IFD, if the parser is not in the tag area,
+     * nothing will happen.
      *
      * @throws IOException
      * @throws ExifInvalidFormatException
      */
-    public void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
+    protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
         int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
         int offset = mTiffStream.getReadByteCount();
-        if (offset > endOfTags) return;
+        if (offset > endOfTags) {
+            return;
+        }
         if (mNeedToParseOffsetsInCurrentIfd) {
             while (offset < endOfTags) {
                 mTag = readTag();
                 offset += TAG_SIZE;
-                if (mTag == null) continue;
+                if (mTag == null) {
+                    continue;
+                }
                 checkOffsetOrImageTag(mTag);
             }
         } else {
@@ -372,20 +405,20 @@ public class ExifParser {
     }
 
     /**
-     * If {@link #next()} return {@link #EVENT_NEW_TAG} or {@link #EVENT_VALUE_OF_REGISTERED_TAG},
-     * call this function to get the corresponding tag.
+     * If {@link #next()} return {@link #EVENT_NEW_TAG} or
+     * {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the
+     * corresponding tag.
      * <p>
-     *
-     * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size of the value is
-     * greater than 4 bytes. One should call {@link ExifTag#hasValue()} to check if the tag
-     * contains value.
-     * If there is no value,call {@link #registerForTagValue(ExifTag)} to have the parser emit
-     * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area pointed by the offset.
-     *
+     * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size
+     * of the value is greater than 4 bytes. One should call
+     * {@link ExifTag#hasValue()} to check if the tag contains value. If there
+     * is no value,call {@link #registerForTagValue(ExifTag)} to have the parser
+     * emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
+     * pointed by the offset.
      * <p>
-     * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the tag will have
-     * already been read except for tags of undefined type. For tags of undefined type, call
-     * one of the read methods to get the value.
+     * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the
+     * tag will have already been read except for tags of undefined type. For
+     * tags of undefined type, call one of the read methods to get the value.
      *
      * @see #registerForTagValue(ExifTag)
      * @see #read(byte[])
@@ -395,14 +428,14 @@ public class ExifParser {
      * @see #readString(int)
      * @see #readString(int, Charset)
      */
-    public ExifTag getTag() {
+    protected ExifTag getTag() {
         return mTag;
     }
 
     /**
      * Gets number of tags in the current IFD area.
      */
-    public int getTagCountInCurrentIfd() {
+    protected int getTagCountInCurrentIfd() {
         return mNumOfTagInIfd;
     }
 
@@ -415,42 +448,48 @@ public class ExifParser {
      * @see IfdId#TYPE_IFD_INTEROPERABILITY
      * @see IfdId#TYPE_IFD_EXIF
      */
-    public int getCurrentIfd() {
+    protected int getCurrentIfd() {
         return mIfdType;
     }
 
     /**
-     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP},
-     * call this function to get the index of this strip.
+     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+     * get the index of this strip.
+     *
      * @see #getStripCount()
      */
-    public int getStripIndex() {
+    protected int getStripIndex() {
         return mImageEvent.stripIndex;
     }
 
     /**
-     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the number
-     * of strip data.
+     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+     * get the number of strip data.
+     *
      * @see #getStripIndex()
      */
-    public int getStripCount() {
+    protected int getStripCount() {
         return mStripCount;
     }
 
     /**
-     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the strip size.
+     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+     * get the strip size.
      */
-    public int getStripSize() {
-        if (mStripSizeTag == null) return 0;
+    protected int getStripSize() {
+        if (mStripSizeTag == null)
+            return 0;
         return (int) mStripSizeTag.getValueAt(0);
     }
 
     /**
-     * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get the image data
-     * size.
+     * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get
+     * the image data size.
      */
-    public int getCompressedImageSize() {
-        if (mJpegSizeTag == null) return 0;
+    protected int getCompressedImageSize() {
+        if (mJpegSizeTag == null) {
+            return 0;
+        }
         return (int) mJpegSizeTag.getValueAt(0);
     }
 
@@ -462,13 +501,15 @@ public class ExifParser {
     }
 
     /**
-     * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD,
-     * the tag may not contain the value if the size of the value is greater than 4 bytes.
-     * When the value is not available here, call this method so that the parser will emit
-     * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area where the value is located.
+     * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may
+     * not contain the value if the size of the value is greater than 4 bytes.
+     * When the value is not available here, call this method so that the parser
+     * will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
+     * where the value is located.
+     *
      * @see #EVENT_VALUE_OF_REGISTERED_TAG
      */
-    public void registerForTagValue(ExifTag tag) {
+    protected void registerForTagValue(ExifTag tag) {
         mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
     }
 
@@ -501,7 +542,9 @@ public class ExifParser {
             mTiffStream.skip(4);
             return null;
         }
-        ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType);
+        // TODO: handle numOfComp overflow
+        ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType,
+                ((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
         int dataSize = tag.getDataSize();
         if (dataSize > 4) {
             long offset = mTiffStream.readUnsignedInt();
@@ -529,146 +572,154 @@ public class ExifParser {
     }
 
     /**
-     * Check the tag, if the tag is one of the offset tag that points to the IFD or image the
-     * caller is interested in, register the IFD or image.
+     * Check the tag, if the tag is one of the offset tag that points to the IFD
+     * or image the caller is interested in, register the IFD or image.
      */
     private void checkOffsetOrImageTag(ExifTag tag) {
         // Some invalid formattd image contains tag with 0 size.
         if (tag.getComponentCount() == 0) {
             return;
         }
-        switch (tag.getTagId()) {
-            case ExifTag.TAG_EXIF_IFD:
-                if (isIfdRequested(IfdId.TYPE_IFD_EXIF)
-                        || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
-                    registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
-                }
-                break;
-            case ExifTag.TAG_GPS_IFD:
-                if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
-                    registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
-                }
-                break;
-            case ExifTag.TAG_INTEROPERABILITY_IFD:
-                if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
-                    registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
-                }
-                break;
-            case ExifTag.TAG_JPEG_INTERCHANGE_FORMAT:
-                if (isThumbnailRequested()) {
-                    registerCompressedImage(tag.getValueAt(0));
-                }
-                break;
-            case ExifTag.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH:
-                if (isThumbnailRequested()) {
-                    mJpegSizeTag = tag;
-                }
-                break;
-            case ExifTag.TAG_STRIP_OFFSETS:
-                if (isThumbnailRequested()) {
-                    if (tag.hasValue()) {
-                        for (int i = 0; i < tag.getComponentCount(); i++) {
-                            if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
-                                registerUncompressedStrip(i, tag.getValueAt(i));
-                            } else {
-                                registerUncompressedStrip(i, tag.getValueAt(i));
-                            }
+        short tid = tag.getTagId();
+        int ifd = tag.getIfd();
+        if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
+            if (isIfdRequested(IfdId.TYPE_IFD_EXIF)
+                    || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+                registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
+            }
+        } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
+            if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
+                registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
+            }
+        } else if (tid == TAG_INTEROPERABILITY_IFD
+                && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
+            if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+                registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
+            }
+        } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
+                && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
+            if (isThumbnailRequested()) {
+                registerCompressedImage(tag.getValueAt(0));
+            }
+        } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
+                && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
+            if (isThumbnailRequested()) {
+                mJpegSizeTag = tag;
+            }
+        } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
+            if (isThumbnailRequested()) {
+                if (tag.hasValue()) {
+                    for (int i = 0; i < tag.getComponentCount(); i++) {
+                        if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
+                            registerUncompressedStrip(i, tag.getValueAt(i));
+                        } else {
+                            registerUncompressedStrip(i, tag.getValueAt(i));
                         }
-                    } else {
-                        mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
-                    }
-                }
-                break;
-            case ExifTag.TAG_STRIP_BYTE_COUNTS:
-                if (isThumbnailRequested()) {
-                    if (tag.hasValue()) {
-                        mStripSizeTag = tag;
                     }
+                } else {
+                    mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
                 }
-                break;
+            }
+        } else if (tid == TAG_STRIP_BYTE_COUNTS
+                && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
+                &&isThumbnailRequested() && tag.hasValue()) {
+            mStripSizeTag = tag;
+        }
+    }
+
+    private boolean checkAllowed(int ifd, int tagId) {
+        int info = mInterface.getTagInfo().get(tagId);
+        if (info == ExifInterface.DEFINITION_NULL) {
+            return false;
         }
+        return ExifInterface.isIfdAllowed(info, ifd);
     }
 
-    void readFullTagValue(ExifTag tag) throws IOException {
+    protected void readFullTagValue(ExifTag tag) throws IOException {
         // Some invalid images contains tags with wrong size, check it here
         short type = tag.getDataType();
         if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED ||
                 type == ExifTag.TYPE_UNSIGNED_BYTE) {
             int size = tag.getComponentCount();
             if (mCorrespondingEvent.size() > 0) {
-                if (mCorrespondingEvent.firstEntry().getKey() <
-                        mTiffStream.getReadByteCount() + size) {
-                    Log.w(TAG, "Invalid size of tag.");
-                    size = mCorrespondingEvent.firstEntry().getKey()
-                            - mTiffStream.getReadByteCount();
-                    tag.setComponentCount(size);
+                if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount()
+                        + size) {
+                    if (mCorrespondingEvent.firstEntry().getValue() instanceof ImageEvent) {
+                        // Invalid thumbnail offset: tag metadata overlaps with
+                        // strip.
+                        Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+                        // Ignore thumbnail.
+                        Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey());
+                    } else {
+                        size = mCorrespondingEvent.firstEntry().getKey()
+                                - mTiffStream.getReadByteCount();
+                        Log.w(TAG, "Invalid size of tag: \n" + tag.toString()
+                                + " setting count to: " + size);
+                        tag.forceSetComponentCount(size);
+                    }
                 }
             }
         }
-        switch(tag.getDataType()) {
+        switch (tag.getDataType()) {
             case ExifTag.TYPE_UNSIGNED_BYTE:
-            case ExifTag.TYPE_UNDEFINED:
-                {
-                    byte buf[] = new byte[tag.getComponentCount()];
-                    read(buf);
-                    tag.setValue(buf);
-                }
+            case ExifTag.TYPE_UNDEFINED: {
+                byte buf[] = new byte[tag.getComponentCount()];
+                read(buf);
+                tag.setValue(buf);
+            }
                 break;
             case ExifTag.TYPE_ASCII:
                 tag.setValue(readString(tag.getComponentCount()));
                 break;
-            case ExifTag.TYPE_UNSIGNED_LONG:
-                {
-                    long value[] = new long[tag.getComponentCount()];
-                    for (int i = 0, n = value.length; i < n; i++) {
-                        value[i] = readUnsignedLong();
-                    }
-                    tag.setValue(value);
+            case ExifTag.TYPE_UNSIGNED_LONG: {
+                long value[] = new long[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readUnsignedLong();
                 }
+                tag.setValue(value);
+            }
                 break;
-            case ExifTag.TYPE_UNSIGNED_RATIONAL:
-                {
-                    Rational value[] = new Rational[tag.getComponentCount()];
-                    for (int i = 0, n = value.length; i < n; i++) {
-                        value[i] = readUnsignedRational();
-                    }
-                    tag.setValue(value);
+            case ExifTag.TYPE_UNSIGNED_RATIONAL: {
+                Rational value[] = new Rational[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readUnsignedRational();
                 }
+                tag.setValue(value);
+            }
                 break;
-            case ExifTag.TYPE_UNSIGNED_SHORT:
-                {
-                    int value[] = new int[tag.getComponentCount()];
-                    for (int i = 0, n = value.length; i < n; i++) {
-                        value[i] = readUnsignedShort();
-                    }
-                    tag.setValue(value);
+            case ExifTag.TYPE_UNSIGNED_SHORT: {
+                int value[] = new int[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readUnsignedShort();
                 }
+                tag.setValue(value);
+            }
                 break;
-            case ExifTag.TYPE_LONG:
-                {
-                    int value[] = new int[tag.getComponentCount()];
-                    for (int i = 0, n = value.length; i < n; i++) {
-                        value[i] = readLong();
-                    }
-                    tag.setValue(value);
+            case ExifTag.TYPE_LONG: {
+                int value[] = new int[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readLong();
                 }
+                tag.setValue(value);
+            }
                 break;
-            case ExifTag.TYPE_RATIONAL:
-                {
-                    Rational value[] = new Rational[tag.getComponentCount()];
-                    for (int i = 0, n = value.length; i < n; i++) {
-                        value[i] = readRational();
-                    }
-                    tag.setValue(value);
+            case ExifTag.TYPE_RATIONAL: {
+                Rational value[] = new Rational[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readRational();
                 }
+                tag.setValue(value);
+            }
                 break;
         }
+        if (LOGV) {
+            Log.v(TAG, "\n" + tag.toString());
+        }
     }
 
     private void parseTiffHeader() throws IOException,
             ExifInvalidFormatException {
         short byteOrder = mTiffStream.readShort();
-        ByteOrder order;
         if (LITTLE_ENDIAN_TAG == byteOrder) {
             mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
         } else if (BIG_ENDIAN_TAG == byteOrder) {
@@ -690,7 +741,7 @@ public class ExifParser {
         }
 
         short marker = dataStream.readShort();
-        while(marker != JpegHeader.EOI
+        while (marker != JpegHeader.EOI
                 && !JpegHeader.isSofMarker(marker)) {
             int length = dataStream.readUnsignedShort();
             // Some invalid formatted image contains multiple APP1,
@@ -705,6 +756,7 @@ public class ExifParser {
                     if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
                         mTiffStartPosition = dataStream.getReadByteCount();
                         mApp1End = length;
+                        mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End;
                         return true;
                     }
                 }
@@ -718,41 +770,44 @@ public class ExifParser {
         return false;
     }
 
-    int getTiffStartPosition() {
+    protected int getOffsetToExifEndFromSOF() {
+        return mOffsetToApp1EndFromSOF;
+    }
+
+    protected int getTiffStartPosition() {
         return mTiffStartPosition;
     }
 
     /**
      * Reads bytes from the InputStream.
      */
-    public int read(byte[] buffer, int offset, int length) throws IOException {
+    protected int read(byte[] buffer, int offset, int length) throws IOException {
         return mTiffStream.read(buffer, offset, length);
     }
 
     /**
      * Equivalent to read(buffer, 0, buffer.length).
      */
-    public int read(byte[] buffer) throws IOException {
+    protected int read(byte[] buffer) throws IOException {
         return mTiffStream.read(buffer);
     }
 
     /**
-     * Reads a String from the InputStream with US-ASCII charset.
-     * The parser will read n bytes and convert it to ascii string.
-     * This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+     * Reads a String from the InputStream with US-ASCII charset. The parser
+     * will read n bytes and convert it to ascii string. This is used for
+     * reading values of type {@link ExifTag#TYPE_ASCII}.
      */
-    public String readString(int n) throws IOException {
+    protected String readString(int n) throws IOException {
         return readString(n, US_ASCII);
     }
 
     /**
-     * Reads a String from the InputStream with the given charset.
-     * The parser will read n bytes and convert it to string.
-     * This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+     * Reads a String from the InputStream with the given charset. The parser
+     * will read n bytes and convert it to string. This is used for reading
+     * values of type {@link ExifTag#TYPE_ASCII}.
      */
-    public String readString(int n, Charset charset) throws IOException {
+    protected String readString(int n, Charset charset) throws IOException {
         if (n > 0) {
-            byte[] buf = new byte[n];
             return mTiffStream.readString(n, charset);
         } else {
             return "";
@@ -760,23 +815,26 @@ public class ExifParser {
     }
 
     /**
-     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the InputStream.
+     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the
+     * InputStream.
      */
-    public int readUnsignedShort() throws IOException {
+    protected int readUnsignedShort() throws IOException {
         return mTiffStream.readShort() & 0xffff;
     }
 
     /**
-     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the InputStream.
+     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the
+     * InputStream.
      */
-    public long readUnsignedLong() throws IOException {
+    protected long readUnsignedLong() throws IOException {
         return readLong() & 0xffffffffL;
     }
 
     /**
-     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the InputStream.
+     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the
+     * InputStream.
      */
-    public Rational readUnsignedRational() throws IOException {
+    protected Rational readUnsignedRational() throws IOException {
         long nomi = readUnsignedLong();
         long denomi = readUnsignedLong();
         return new Rational(nomi, denomi);
@@ -785,14 +843,14 @@ public class ExifParser {
     /**
      * Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream.
      */
-    public int readLong() throws IOException {
+    protected int readLong() throws IOException {
         return mTiffStream.readInt();
     }
 
     /**
      * Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream.
      */
-    public Rational readRational() throws IOException {
+    protected Rational readRational() throws IOException {
         int nomi = readLong();
         int denomi = readLong();
         return new Rational(nomi, denomi);
@@ -801,10 +859,12 @@ public class ExifParser {
     private static class ImageEvent {
         int stripIndex;
         int type;
+
         ImageEvent(int type) {
             this.stripIndex = 0;
             this.type = type;
         }
+
         ImageEvent(int type, int stripIndex) {
             this.type = type;
             this.stripIndex = stripIndex;
@@ -814,6 +874,7 @@ public class ExifParser {
     private static class IfdEvent {
         int ifd;
         boolean isRequested;
+
         IfdEvent(int ifd, boolean isInterestedIfd) {
             this.ifd = ifd;
             this.isRequested = isInterestedIfd;
@@ -823,6 +884,7 @@ public class ExifParser {
     private static class ExifTagEvent {
         ExifTag tag;
         boolean isRequested;
+
         ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
             this.tag = tag;
             this.isRequested = isRequireByUser;
@@ -832,7 +894,7 @@ public class ExifParser {
     /**
      * Gets the byte order of the current InputStream.
      */
-    public ByteOrder getByteOrder() {
+    protected ByteOrder getByteOrder() {
         return mTiffStream.getByteOrder();
     }
 }
index 5bce9c4..68e972f 100644 (file)
@@ -22,19 +22,30 @@ import java.io.IOException;
 import java.io.InputStream;
 
 /**
- * This class reads the EXIF header of a JPEG file and stores it in {@link ExifData}.
+ * This class reads the EXIF header of a JPEG file and stores it in
+ * {@link ExifData}.
  */
-public class ExifReader {
+class ExifReader {
     private static final String TAG = "ExifReader";
+
+    private final ExifInterface mInterface;
+
+    ExifReader(ExifInterface iRef) {
+        mInterface = iRef;
+    }
+
     /**
-     * Parses the inputStream and  and returns the EXIF data in an {@link ExifData}.
+     * Parses the inputStream and and returns the EXIF data in an
+     * {@link ExifData}.
+     *
      * @throws ExifInvalidFormatException
      * @throws IOException
      */
-    public ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
+    protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
             IOException {
-        ExifParser parser = ExifParser.parse(inputStream);
+        ExifParser parser = ExifParser.parse(inputStream, mInterface);
         ExifData exifData = new ExifData(parser.getByteOrder());
+        ExifTag tag = null;
 
         int event = parser.next();
         while (event != ExifParser.EVENT_END) {
@@ -43,7 +54,7 @@ public class ExifReader {
                     exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
                     break;
                 case ExifParser.EVENT_NEW_TAG:
-                    ExifTag tag = parser.getTag();
+                    tag = parser.getTag();
                     if (!tag.hasValue()) {
                         parser.registerForTagValue(tag);
                     } else {
@@ -69,6 +80,7 @@ public class ExifReader {
                     buf = new byte[parser.getStripSize()];
                     if (buf.length == parser.read(buf)) {
                         exifData.setStripBytes(parser.getStripIndex(), buf);
+                    } else {
                         Log.w(TAG, "Failed to read the strip bytes");
                     }
                     break;
index 753b18c..0b31c4d 100644 (file)
 
 package com.android.gallery3d.exif;
 
-import android.util.SparseArray;
-
 import java.nio.charset.Charset;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Date;
 
 /**
- * This class stores information of an EXIF tag.
- * @see ExifParser
- * @see ExifReader
- * @see IfdData
- * @see ExifData
+ * This class stores information of an EXIF tag. For more information about
+ * defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be
+ * instantiated using {@link ExifInterface#buildTag}.
+ *
+ * @see ExifInterface
  */
 public class ExifTag {
-    // Tiff Tags
-    public static final short TAG_IMAGE_WIDTH = 0x100;
-    /*
-     * The height of the image.
-     */
-    public static final short TAG_IMAGE_LENGTH = 0x101;
-    public static final short TAG_BITS_PER_SAMPLE = 0x102;
-    public static final short TAG_COMPRESSION = 0x103;
-    public static final short TAG_PHOTOMETRIC_INTERPRETATION = 0x106;
-    public static final short TAG_IMAGE_DESCRIPTION = 0x10E;
-    public static final short TAG_MAKE = 0x10F;
-    public static final short TAG_MODEL = 0x110;
-    public static final short TAG_STRIP_OFFSETS = 0x111;
-    public static final short TAG_ORIENTATION = 0x112;
-    public static final short TAG_SAMPLES_PER_PIXEL = 0x115;
-    public static final short TAG_ROWS_PER_STRIP = 0x116;
-    public static final short TAG_STRIP_BYTE_COUNTS = 0x117;
-    public static final short TAG_X_RESOLUTION = 0x11A;
-    public static final short TAG_Y_RESOLUTION = 0x11B;
-    public static final short TAG_PLANAR_CONFIGURATION = 0x11C;
-    public static final short TAG_RESOLUTION_UNIT = 0x128;
-    public static final short TAG_TRANSFER_FUNCTION = 0x12D;
-    public static final short TAG_SOFTWARE = 0x131;
-    public static final short TAG_DATE_TIME = 0x132;
-    public static final short TAG_ARTIST = 0x13B;
-    public static final short TAG_WHITE_POINT = 0x13E;
-    public static final short TAG_PRIMARY_CHROMATICITIES = 0x13F;
-    public static final short TAG_JPEG_INTERCHANGE_FORMAT = 0x201;
-    public static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 0x202;
-    public static final short TAG_Y_CB_CR_COEFFICIENTS = 0x211;
-    public static final short TAG_Y_CB_CR_SUB_SAMPLING = 0x212;
-    public static final short TAG_Y_CB_CR_POSITIONING = 0x213;
-    public static final short TAG_REFERENCE_BLACK_WHITE = 0x214;
-    public static final short TAG_COPYRIGHT = (short) 0x8298;
-    public static final short TAG_EXIF_IFD = (short) 0x8769;
-    public static final short TAG_GPS_IFD = (short) 0x8825;
-
-    // Exif Tags
-    public static final short TAG_EXPOSURE_TIME = (short) 0x829A;
-    public static final short TAG_F_NUMBER = (short) 0x829D;
-    public static final short TAG_EXPOSURE_PROGRAM = (short) 0x8822;
-    public static final short TAG_SPECTRAL_SENSITIVITY = (short) 0x8824;
-    public static final short TAG_ISO_SPEED_RATINGS = (short) 0x8827;
-    public static final short TAG_OECF = (short) 0x8828;
-    public static final short TAG_EXIF_VERSION = (short) 0x9000;
-    public static final short TAG_DATE_TIME_ORIGINAL = (short) 0x9003;
-    public static final short TAG_DATE_TIME_DIGITIZED = (short) 0x9004;
-    public static final short TAG_COMPONENTS_CONFIGURATION = (short) 0x9101;
-    public static final short TAG_COMPRESSED_BITS_PER_PIXEL = (short) 0x9102;
-    public static final short TAG_SHUTTER_SPEED_VALUE = (short) 0x9201;
-    public static final short TAG_APERTURE_VALUE = (short) 0x9202;
-    public static final short TAG_BRIGHTNESS_VALUE = (short) 0x9203;
-    public static final short TAG_EXPOSURE_BIAS_VALUE = (short) 0x9204;
-    public static final short TAG_MAX_APERTURE_VALUE = (short) 0x9205;
-    public static final short TAG_SUBJECT_DISTANCE = (short) 0x9206;
-    public static final short TAG_METERING_MODE = (short) 0x9207;
-    public static final short TAG_LIGHT_SOURCE = (short) 0x9208;
-    public static final short TAG_FLASH = (short) 0x9209;
-    public static final short TAG_FOCAL_LENGTH = (short) 0x920A;
-    public static final short TAG_SUBJECT_AREA = (short) 0x9214;
-    public static final short TAG_MAKER_NOTE = (short) 0x927C;
-    public static final short TAG_USER_COMMENT = (short) 0x9286;
-    public static final short TAG_SUB_SEC_TIME = (short) 0x9290;
-    public static final short TAG_SUB_SEC_TIME_ORIGINAL = (short) 0x9291;
-    public static final short TAG_SUB_SEC_TIME_DIGITIZED = (short) 0x9292;
-    public static final short TAG_FLASHPIX_VERSION = (short) 0xA000;
-    public static final short TAG_COLOR_SPACE = (short) 0xA001;
-    public static final short TAG_PIXEL_X_DIMENSION = (short) 0xA002;
-    public static final short TAG_PIXEL_Y_DIMENSION = (short) 0xA003;
-    public static final short TAG_RELATED_SOUND_FILE = (short) 0xA004;
-    public static final short TAG_INTEROPERABILITY_IFD = (short) 0xA005;
-    public static final short TAG_FLASH_ENERGY = (short) 0xA20B;
-    public static final short TAG_SPATIAL_FREQUENCY_RESPONSE = (short) 0xA20C;
-    public static final short TAG_FOCAL_PLANE_X_RESOLUTION = (short) 0xA20E;
-    public static final short TAG_FOCAL_PLANE_Y_RESOLUTION = (short) 0xA20F;
-    public static final short TAG_FOCAL_PLANE_RESOLUTION_UNIT = (short) 0xA210;
-    public static final short TAG_SUBJECT_LOCATION = (short) 0xA214;
-    public static final short TAG_EXPOSURE_INDEX = (short) 0xA215;
-    public static final short TAG_SENSING_METHOD = (short) 0xA217;
-    public static final short TAG_FILE_SOURCE = (short) 0xA300;
-    public static final short TAG_SCENE_TYPE = (short) 0xA301;
-    public static final short TAG_CFA_PATTERN = (short) 0xA302;
-    public static final short TAG_CUSTOM_RENDERED = (short) 0xA401;
-    public static final short TAG_EXPOSURE_MODE = (short) 0xA402;
-    public static final short TAG_WHITE_BALANCE = (short) 0xA403;
-    public static final short TAG_DIGITAL_ZOOM_RATIO = (short) 0xA404;
-    public static final short TAG_FOCAL_LENGTH_IN_35_MM_FILE = (short) 0xA405;
-    public static final short TAG_SCENE_CAPTURE_TYPE = (short) 0xA406;
-    public static final short TAG_GAIN_CONTROL = (short) 0xA407;
-    public static final short TAG_CONTRAST = (short) 0xA408;
-    public static final short TAG_SATURATION = (short) 0xA409;
-    public static final short TAG_SHARPNESS = (short) 0xA40A;
-    public static final short TAG_DEVICE_SETTING_DESCRIPTION = (short) 0xA40B;
-    public static final short TAG_SUBJECT_DISTANCE_RANGE = (short) 0xA40C;
-    public static final short TAG_IMAGE_UNIQUE_ID = (short) 0xA420;
-
-    // GPS tags
-    public static final short TAG_GPS_VERSION_ID = 0;
-    public static final short TAG_GPS_LATITUDE_REF = 1;
-    public static final short TAG_GPS_LATITUDE = 2;
-    public static final short TAG_GPS_LONGITUDE_REF = 3;
-    public static final short TAG_GPS_LONGITUDE = 4;
-    public static final short TAG_GPS_ALTITUDE_REF = 5;
-    public static final short TAG_GPS_ALTITUDE = 6;
-    public static final short TAG_GPS_TIME_STAMP = 7;
-    public static final short TAG_GPS_SATTELLITES = 8;
-    public static final short TAG_GPS_STATUS = 9;
-    public static final short TAG_GPS_MEASURE_MODE = 10;
-    public static final short TAG_GPS_DOP = 11;
-    public static final short TAG_GPS_SPEED_REF = 12;
-    public static final short TAG_GPS_SPEED = 13;
-    public static final short TAG_GPS_TRACK_REF = 14;
-    public static final short TAG_GPS_TRACK = 15;
-    public static final short TAG_GPS_IMG_DIRECTION_REF = 16;
-    public static final short TAG_GPS_IMG_DIRECTION = 17;
-    public static final short TAG_GPS_MAP_DATUM = 18;
-    public static final short TAG_GPS_DEST_LATITUDE_REF = 19;
-    public static final short TAG_GPS_DEST_LATITUDE = 20;
-    public static final short TAG_GPS_DEST_LONGITUDE_REF = 21;
-    public static final short TAG_GPS_DEST_LONGITUDE = 22;
-    public static final short TAG_GPS_DEST_BEARING_REF = 23;
-    public static final short TAG_GPS_DEST_BEARING = 24;
-    public static final short TAG_GPS_DEST_DISTANCE_REF = 25;
-    public static final short TAG_GPS_DEST_DISTANCE = 26;
-    public static final short TAG_GPS_PROCESSING_METHOD = 27;
-    public static final short TAG_GPS_AREA_INFORMATION = 28;
-    public static final short TAG_GPS_DATE_STAMP = 29;
-    public static final short TAG_GPS_DIFFERENTIAL = 30;
-
-    // Interoperability tag
-    public static final short TAG_INTEROPERABILITY_INDEX = 1;
-
-    /**
-     * Constants for {@link #TAG_ORIENTATION}
-     */
-    public static interface Orientation {
-        public static final short TOP_LEFT = 1;
-        public static final short TOP_RIGHT = 2;
-        public static final short BOTTOM_LEFT = 3;
-        public static final short BOTTOM_RIGHT = 4;
-        public static final short LEFT_TOP = 5;
-        public static final short RIGHT_TOP = 6;
-        public static final short LEFT_BOTTOM = 7;
-        public static final short RIGHT_BOTTOM = 8;
-    }
-
-    /**
-     * Constants for {@link #TAG_Y_CB_CR_POSITIONING}
-     */
-    public static interface YCbCrPositioning {
-        public static final short CENTERED = 1;
-        public static final short CO_SITED = 2;
-    }
-
-    /**
-     * Constants for {@link #TAG_COMPRESSION}
-     */
-    public static interface Compression {
-        public static final short UNCOMPRESSION = 1;
-        public static final short JPEG = 6;
-    }
-
-    /**
-     * Constants for {@link #TAG_RESOLUTION_UNIT}
-     */
-    public static interface ResolutionUnit {
-        public static final short INCHES = 2;
-        public static final short CENTIMETERS = 3;
-    }
-
-    /**
-     * Constants for {@link #TAG_PHOTOMETRIC_INTERPRETATION}
-     */
-    public static interface PhotometricInterpretation {
-        public static final short RGB = 2;
-        public static final short YCBCR = 6;
-    }
-
-    /**
-     * Constants for {@link #TAG_PLANAR_CONFIGURATION}
-     */
-    public static interface PlanarConfiguration {
-        public static final short CHUNKY = 1;
-        public static final short PLANAR = 2;
-    }
-
-    /**
-     * Constants for {@link #TAG_EXPOSURE_PROGRAM}
-     */
-    public static interface ExposureProgram {
-        public static final short NOT_DEFINED = 0;
-        public static final short MANUAL = 1;
-        public static final short NORMAL_PROGRAM = 2;
-        public static final short APERTURE_PRIORITY = 3;
-        public static final short SHUTTER_PRIORITY = 4;
-        public static final short CREATIVE_PROGRAM = 5;
-        public static final short ACTION_PROGRAM = 6;
-        public static final short PROTRAIT_MODE = 7;
-        public static final short LANDSCAPE_MODE = 8;
-    }
-
-    /**
-     * Constants for {@link #TAG_METERING_MODE}
-     */
-    public static interface MeteringMode {
-        public static final short UNKNOWN = 0;
-        public static final short AVERAGE = 1;
-        public static final short CENTER_WEIGHTED_AVERAGE = 2;
-        public static final short SPOT = 3;
-        public static final short MULTISPOT = 4;
-        public static final short PATTERN = 5;
-        public static final short PARTAIL = 6;
-        public static final short OTHER = 255;
-    }
-
-    /**
-     * Constants for {@link #TAG_FLASH} As the definition in Jeita EXIF 2.2 standard, we can
-     * treat this constant as bitwise flag.
-     * <p>
-     * e.g.
-     * <p>
-     * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED | MODE_AUTO_MODE
-     */
-    public static interface Flash {
-        // LSB
-        public static final short DID_NOT_FIRED = 0;
-        public static final short FIRED = 1;
-        // 1st~2nd bits
-        public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1;
-        public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1;
-        public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1;
-        // 3rd~4th bits
-        public static final short MODE_UNKNOWN = 0 << 3;
-        public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3;
-        public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3;
-        public static final short MODE_AUTO_MODE = 3 << 3;
-        // 5th bit
-        public static final short FUNCTION_PRESENT = 0 << 5;
-        public static final short FUNCTION_NO_FUNCTION = 1 << 5;
-        // 6th bit
-        public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6;
-        public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6;
-    }
-
-    /**
-     * Constants for {@link #TAG_COLOR_SPACE}
-     */
-    public static interface ColorSpace {
-        public static final short SRGB = 1;
-        public static final short UNCALIBRATED = (short) 0xFFFF;
-    }
-
-    /**
-     * Constants for {@link #TAG_EXPOSURE_MODE}
-     */
-    public static interface ExposureMode {
-        public static final short AUTO_EXPOSURE = 0;
-        public static final short MANUAL_EXPOSURE = 1;
-        public static final short AUTO_BRACKET = 2;
-    }
-
-    /**
-     * Constants for {@link #TAG_WHITE_BALANCE}
-     */
-    public static interface WhiteBalance {
-        public static final short AUTO = 0;
-        public static final short MANUAL = 1;
-    }
-
-    /**
-     * Constants for {@link #TAG_SCENE_CAPTURE_TYPE}
-     */
-    public static interface SceneCapture {
-        public static final short STANDARD = 0;
-        public static final short LANDSCAPE = 1;
-        public static final short PROTRAIT = 2;
-        public static final short NIGHT_SCENE = 3;
-    }
-
-    /**
-     * Constants for {@link #TAG_COMPONENTS_CONFIGURATION}
-     */
-    public static interface ComponentsConfiguration {
-        public static final short NOT_EXIST = 0;
-        public static final short Y = 1;
-        public static final short CB = 2;
-        public static final short CR = 3;
-        public static final short R = 4;
-        public static final short G = 5;
-        public static final short B = 6;
-    }
-
-    /**
-     * Constants for {@link #TAG_LIGHT_SOURCE}
-     */
-    public static interface LightSource {
-        public static final short UNKNOWN = 0;
-        public static final short DAYLIGHT = 1;
-        public static final short FLUORESCENT = 2;
-        public static final short TUNGSTEN = 3;
-        public static final short FLASH = 4;
-        public static final short FINE_WEATHER = 9;
-        public static final short CLOUDY_WEATHER = 10;
-        public static final short SHADE = 11;
-        public static final short DAYLIGHT_FLUORESCENT = 12;
-        public static final short DAY_WHITE_FLUORESCENT = 13;
-        public static final short COOL_WHITE_FLUORESCENT = 14;
-        public static final short WHITE_FLUORESCENT = 15;
-        public static final short STANDARD_LIGHT_A = 17;
-        public static final short STANDARD_LIGHT_B = 18;
-        public static final short STANDARD_LIGHT_C = 19;
-        public static final short D55 = 20;
-        public static final short D65 = 21;
-        public static final short D75 = 22;
-        public static final short D50 = 23;
-        public static final short ISO_STUDIO_TUNGSTEN = 24;
-        public static final short OTHER = 255;
-    }
-
-    /**
-     * Constants for {@link #TAG_SENSING_METHOD}
-     */
-    public static interface SensingMethod {
-        public static final short NOT_DEFINED = 1;
-        public static final short ONE_CHIP_COLOR = 2;
-        public static final short TWO_CHIP_COLOR = 3;
-        public static final short THREE_CHIP_COLOR = 4;
-        public static final short COLOR_SEQUENTIAL_AREA = 5;
-        public static final short TRILINEAR = 7;
-        public static final short COLOR_SEQUENTIAL_LINEAR = 8;
-    }
-
-    /**
-     * Constants for {@link #TAG_FILE_SOURCE}
-     */
-    public static interface FileSource {
-        public static final short DSC = 3;
-    }
-
-    /**
-     * Constants for {@link #TAG_SCENE_TYPE}
-     */
-    public static interface SceneType {
-        public static final short DIRECT_PHOTOGRAPHED = 1;
-    }
-
-    /**
-     * Constants for {@link #TAG_GAIN_CONTROL}
-     */
-    public static interface GainControl {
-        public static final short NONE = 0;
-        public static final short LOW_UP = 1;
-        public static final short HIGH_UP = 2;
-        public static final short LOW_DOWN = 3;
-        public static final short HIGH_DOWN = 4;
-    }
-
-    /**
-     * Constants for {@link #TAG_CONTRAST}
-     */
-    public static interface Contrast {
-        public static final short NORMAL = 0;
-        public static final short SOFT = 1;
-        public static final short HARD = 2;
-    }
-
-    /**
-     * Constants for {@link #TAG_SATURATION}
-     */
-    public static interface Saturation {
-        public static final short NORMAL = 0;
-        public static final short LOW = 1;
-        public static final short HIGH = 2;
-    }
-
-    /**
-     * Constants for {@link #TAG_SHARPNESS}
-     */
-    public static interface Sharpness {
-        public static final short NORMAL = 0;
-        public static final short SOFT = 1;
-        public static final short HARD = 2;
-    }
-
-    /**
-     * Constants for {@link #TAG_SUBJECT_DISTANCE}
-     */
-    public static interface SubjectDistance {
-        public static final short UNKNOWN = 0;
-        public static final short MACRO = 1;
-        public static final short CLOSE_VIEW = 2;
-        public static final short DISTANT_VIEW = 3;
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_LATITUDE_REF}, {@link #TAG_GPS_DEST_LATITUDE_REF}
-     */
-    public static interface GpsLatitudeRef {
-        public static final String NORTH = "N";
-        public static final String SOUTH = "S";
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_LONGITUDE_REF}, {@link #TAG_GPS_DEST_LONGITUDE_REF}
-     */
-    public static interface GpsLongitudeRef {
-        public static final String EAST = "E";
-        public static final String WEST = "W";
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_ALTITUDE_REF}
-     */
-    public static interface GpsAltitudeRef {
-        public static final short SEA_LEVEL = 0;
-        public static final short SEA_LEVEL_NEGATIVE = 1;
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_STATUS}
-     */
-    public static interface GpsStatus {
-        public static final String IN_PROGRESS = "A";
-        public static final String INTEROPERABILITY = "V";
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_MEASURE_MODE}
-     */
-    public static interface GpsMeasureMode {
-        public static final String MODE_2_DIMENSIONAL = "2";
-        public static final String MODE_3_DIMENSIONAL = "3";
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_SPEED_REF}, {@link #TAG_GPS_DEST_DISTANCE_REF}
-     */
-    public static interface GpsSpeedRef {
-        public static final String KILOMETERS = "K";
-        public static final String MILES = "M";
-        public static final String KNOTS = "N";
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_TRACK_REF}, {@link #TAG_GPS_IMG_DIRECTION_REF},
-     * {@link #TAG_GPS_DEST_BEARING_REF}
-     */
-    public static interface GpsTrackRef {
-        public static final String TRUE_DIRECTION = "T";
-        public static final String MAGNETIC_DIRECTION = "M";
-    }
-
-    /**
-     * Constants for {@link #TAG_GPS_DIFFERENTIAL}
-     */
-    public static interface GpsDifferential {
-        public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0;
-        public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1;
-    }
-
     /**
      * The BYTE type in the EXIF standard. An 8-bit unsigned integer.
      */
     public static final short TYPE_UNSIGNED_BYTE = 1;
     /**
-     * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit ASCII code.
-     * The final byte is terminated with NULL.
+     * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit
+     * ASCII code. The final byte is terminated with NULL.
      */
     public static final short TYPE_ASCII = 2;
     /**
@@ -509,13 +47,13 @@ public class ExifTag {
      */
     public static final short TYPE_UNSIGNED_LONG = 4;
     /**
-     * The RATIONAL type of EXIF standard. It consists of two LONGs. The first one is the numerator
-     * and the second one expresses the denominator.
+     * The RATIONAL type of EXIF standard. It consists of two LONGs. The first
+     * one is the numerator and the second one expresses the denominator.
      */
     public static final short TYPE_UNSIGNED_RATIONAL = 5;
     /**
-     * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any value
-     * depending on the field definition.
+     * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any
+     * value depending on the field definition.
      */
     public static final short TYPE_UNDEFINED = 7;
     /**
@@ -524,12 +62,18 @@ public class ExifTag {
      */
     public static final short TYPE_LONG = 9;
     /**
-     * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first one is the
-     * numerator and the second one is the denominator.
+     * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first
+     * one is the numerator and the second one is the denominator.
      */
     public static final short TYPE_RATIONAL = 10;
 
+    private static Charset US_ASCII = Charset.forName("US-ASCII");
     private static final int TYPE_TO_SIZE_MAP[] = new int[11];
+    private static final int UNSIGNED_SHORT_MAX = 65535;
+    private static final long UNSIGNED_LONG_MAX = 4294967295L;
+    private static final long LONG_MAX = Integer.MAX_VALUE;
+    private static final long LONG_MIN = Integer.MIN_VALUE;
+
     static {
         TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1;
         TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
@@ -541,391 +85,69 @@ public class ExifTag {
         TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
     }
 
-    /**
-     * Gets the element size of the given data type.
-     *
-     * @see #TYPE_ASCII
-     * @see #TYPE_LONG
-     * @see #TYPE_RATIONAL
-     * @see #TYPE_UNDEFINED
-     * @see #TYPE_UNSIGNED_BYTE
-     * @see #TYPE_UNSIGNED_LONG
-     * @see #TYPE_UNSIGNED_RATIONAL
-     * @see #TYPE_UNSIGNED_SHORT
-     */
-    public static int getElementSize(short type) {
-        return TYPE_TO_SIZE_MAP[type];
-    }
-
-    private static volatile SparseArray<Integer> sTagInfo = null;
-    private static volatile SparseArray<Integer> sInteroperTagInfo = null;
-    private static final int SIZE_UNDEFINED = 0;
-
-    private static SparseArray<Integer> getTagInfo() {
-        if (sTagInfo == null) {
-            synchronized(ExifTag.class) {
-                if (sTagInfo == null) {
-                    sTagInfo = new SparseArray<Integer>();
-                    initTagInfo();
-                }
-            }
-        }
-        return sTagInfo;
-    }
-
-    private static SparseArray<Integer> getInteroperTagInfo() {
-        if (sInteroperTagInfo == null) {
-            synchronized(ExifTag.class) {
-                if (sInteroperTagInfo == null) {
-                    sInteroperTagInfo = new SparseArray<Integer>();
-                    sInteroperTagInfo.put(TAG_INTEROPERABILITY_INDEX,
-                            (IfdId.TYPE_IFD_INTEROPERABILITY << 24)
-                            | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-                }
-            }
-        }
-        return sInteroperTagInfo;
-    }
-
-    private static void initTagInfo() {
-        /**
-         * We put tag information in a 4-bytes integer. The first byte is the
-         * IFD of the tag, and the second byte is the default data type. The
-         * last two byte are a short value indicating the component count of this
-         * tag.
-         */
-        sTagInfo.put(TAG_MAKE,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_IMAGE_WIDTH,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_IMAGE_LENGTH,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_BITS_PER_SAMPLE,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 3);
-        sTagInfo.put(TAG_COMPRESSION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_PHOTOMETRIC_INTERPRETATION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_ORIENTATION, (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_SAMPLES_PER_PIXEL,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_PLANAR_CONFIGURATION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_Y_CB_CR_SUB_SAMPLING,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 2);
-        sTagInfo.put(TAG_Y_CB_CR_POSITIONING,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_X_RESOLUTION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_Y_RESOLUTION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_RESOLUTION_UNIT,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_STRIP_OFFSETS,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_ROWS_PER_STRIP,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_STRIP_BYTE_COUNTS,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_JPEG_INTERCHANGE_FORMAT,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_TRANSFER_FUNCTION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_SHORT << 16 | 3 * 256);
-        sTagInfo.put(TAG_WHITE_POINT,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 2);
-        sTagInfo.put(TAG_PRIMARY_CHROMATICITIES,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 6);
-        sTagInfo.put(TAG_Y_CB_CR_COEFFICIENTS,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 3);
-        sTagInfo.put(TAG_REFERENCE_BLACK_WHITE,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 6);
-        sTagInfo.put(TAG_DATE_TIME,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | 20);
-        sTagInfo.put(TAG_IMAGE_DESCRIPTION,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_MAKE,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_MODEL,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_SOFTWARE,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_ARTIST,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_COPYRIGHT,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_EXIF_IFD,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_GPS_IFD,
-                (IfdId.TYPE_IFD_0 << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-
-        // EXIF TAG
-        sTagInfo.put(TAG_EXIF_VERSION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 4);
-        sTagInfo.put(TAG_FLASHPIX_VERSION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 4);
-        sTagInfo.put(TAG_COLOR_SPACE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_COMPONENTS_CONFIGURATION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 4);
-        sTagInfo.put(TAG_COMPRESSED_BITS_PER_PIXEL,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_PIXEL_X_DIMENSION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_PIXEL_Y_DIMENSION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_LONG << 16 | 1);
-        sTagInfo.put(TAG_MAKER_NOTE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_USER_COMMENT,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_RELATED_SOUND_FILE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 13);
-        sTagInfo.put(TAG_DATE_TIME_ORIGINAL,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 20);
-        sTagInfo.put(TAG_DATE_TIME_DIGITIZED,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 20);
-        sTagInfo.put(TAG_SUB_SEC_TIME,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_SUB_SEC_TIME_ORIGINAL,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_SUB_SEC_TIME_DIGITIZED,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_IMAGE_UNIQUE_ID,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | 33);
-        sTagInfo.put(TAG_EXPOSURE_TIME,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_F_NUMBER,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_EXPOSURE_PROGRAM,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_SPECTRAL_SENSITIVITY,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_ISO_SPEED_RATINGS,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_OECF,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_SHUTTER_SPEED_VALUE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_APERTURE_VALUE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_BRIGHTNESS_VALUE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_EXPOSURE_BIAS_VALUE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_MAX_APERTURE_VALUE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_SUBJECT_DISTANCE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_METERING_MODE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_LIGHT_SOURCE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_FLASH,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_FOCAL_LENGTH,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_SUBJECT_AREA,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_FLASH_ENERGY,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_SPATIAL_FREQUENCY_RESPONSE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_FOCAL_PLANE_X_RESOLUTION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_FOCAL_PLANE_Y_RESOLUTION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_SUBJECT_LOCATION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 2);
-        sTagInfo.put(TAG_EXPOSURE_INDEX,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_SENSING_METHOD,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_FILE_SOURCE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 1);
-        sTagInfo.put(TAG_SCENE_TYPE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | 1);
-        sTagInfo.put(TAG_CFA_PATTERN,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_CUSTOM_RENDERED,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_EXPOSURE_MODE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_WHITE_BALANCE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_DIGITAL_ZOOM_RATIO,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_FOCAL_LENGTH_IN_35_MM_FILE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_SCENE_CAPTURE_TYPE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_GAIN_CONTROL,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_CONTRAST,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_SATURATION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_SHARPNESS,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        sTagInfo.put(TAG_DEVICE_SETTING_DESCRIPTION,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_SUBJECT_DISTANCE_RANGE,
-                (IfdId.TYPE_IFD_EXIF << 24) | TYPE_UNSIGNED_SHORT << 16 | 1);
-        // GPS tag
-        sTagInfo.put(TAG_GPS_VERSION_ID,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_BYTE << 16 | 4);
-        sTagInfo.put(TAG_GPS_LATITUDE_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_LONGITUDE_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_LATITUDE,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_RATIONAL << 16 | 3);
-        sTagInfo.put(TAG_GPS_LONGITUDE,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_RATIONAL << 16 | 3);
-        sTagInfo.put(TAG_GPS_ALTITUDE_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_BYTE << 16 | 1);
-        sTagInfo.put(TAG_GPS_ALTITUDE,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_TIME_STAMP,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 3);
-        sTagInfo.put(TAG_GPS_SATTELLITES,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_GPS_STATUS,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_MEASURE_MODE,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_DOP,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_SPEED_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_SPEED,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_TRACK_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_TRACK,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_IMG_DIRECTION_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_IMG_DIRECTION,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_MAP_DATUM,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_GPS_DEST_LATITUDE_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_DEST_LATITUDE,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_DEST_BEARING_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_DEST_BEARING,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_DEST_DISTANCE_REF,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 2);
-        sTagInfo.put(TAG_GPS_DEST_DISTANCE,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_RATIONAL << 16 | 1);
-        sTagInfo.put(TAG_GPS_PROCESSING_METHOD,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_GPS_AREA_INFORMATION,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNDEFINED << 16 | SIZE_UNDEFINED);
-        sTagInfo.put(TAG_GPS_DATE_STAMP,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_ASCII << 16 | 11);
-        sTagInfo.put(TAG_GPS_DIFFERENTIAL,
-                (IfdId.TYPE_IFD_GPS << 24) | TYPE_UNSIGNED_SHORT << 16 | 11);
-    }
+    static final int SIZE_UNDEFINED = 0;
 
-    private static Charset US_ASCII = Charset.forName("US-ASCII");
+    // Exif TagId
     private final short mTagId;
+    // Exif Tag Type
     private final short mDataType;
-    private final int mIfd;
-    private final boolean mComponentCountDefined;
-    private int mComponentCount;
+    // If tag has defined count
+    private boolean mHasDefinedDefaultComponentCount;
+    // Actual data count in tag (should be number of elements in value array)
+    private int mComponentCountActual;
+    // The ifd that this tag should be put in
+    private int mIfd;
+    // The value (array of elements of type Tag Type)
     private Object mValue;
+    // Value offset in exif header.
     private int mOffset;
 
-    static private short getTypeFromInfo(int info) {
-        return (short) ((info >> 16) & 0xff);
-    }
-
-    static private int getComponentCountFromInfo(int info) {
-        return info & 0xffff;
-    }
-
-    static private int getIfdIdFromInfo(int info) {
-        return (info >> 24) & 0xff;
-    }
-
-    static private boolean getComponentCountDefined(short tagId, int ifd) {
-        Integer info = (ifd == IfdId.TYPE_IFD_INTEROPERABILITY) ?
-                getInteroperTagInfo().get(tagId) : getTagInfo().get(tagId);
-        if (info == null) return false;
-        return getComponentCountFromInfo(info) != SIZE_UNDEFINED;
-    }
-
-    static int getIfdIdFromTagId(short tagId) {
-        Integer info = getTagInfo().get(tagId);
-        if (info == null) {
-            throw new IllegalArgumentException("Unknown Tag ID: " + tagId);
-        }
-        return getIfdIdFromInfo(info);
-    }
-
-    /**
-     * Create a tag with given ID. For tags related to interoperability and thumbnail, call
-     * {@link #buildInteroperabilityTag(short)} and {@link #buildThumbnailTag(short)} respectively.
-     * @exception IllegalArgumentException If the ID is invalid.
-     */
-    static public ExifTag buildTag(short tagId) {
-        Integer info = getTagInfo().get(tagId);
-        if (info == null) {
-            throw new IllegalArgumentException("Unknown Tag ID: " + tagId);
-        }
-        return new ExifTag(tagId, getTypeFromInfo(info),
-                getComponentCountFromInfo(info),
-                getIfdIdFromInfo(info));
-    }
+    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss");
 
     /**
-     * Create a tag related to thumbnail with given ID.
-     * @exception IllegalArgumentException If the ID is invalid.
+     * Returns true if the given IFD is a valid IFD.
      */
-    static public ExifTag buildThumbnailTag(short tagId) {
-        Integer info = getTagInfo().get(tagId);
-        if (info == null || getIfdIdFromInfo(info) != IfdId.TYPE_IFD_0) {
-            throw new IllegalArgumentException("Unknown Thumnail Tag ID: " + tagId);
-        }
-        return new ExifTag(tagId, getTypeFromInfo(info),
-                getComponentCountFromInfo(info),
-                IfdId.TYPE_IFD_1);
+    public static boolean isValidIfd(int ifdId) {
+        return ifdId == IfdId.TYPE_IFD_0 || ifdId == IfdId.TYPE_IFD_1
+                || ifdId == IfdId.TYPE_IFD_EXIF || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY
+                || ifdId == IfdId.TYPE_IFD_GPS;
     }
 
     /**
-     * Create a tag related to interoperability with given ID.
-     * @exception IllegalArgumentException If the ID is invalid.
+     * Returns true if a given type is a valid tag type.
      */
-    static public ExifTag buildInteroperabilityTag(short tagId) {
-        Integer info = getInteroperTagInfo().get(tagId);
-        if (info == null || getIfdIdFromInfo(info) != IfdId.TYPE_IFD_INTEROPERABILITY) {
-            throw new RuntimeException("Unknown Interoperability Tag ID: " + tagId);
-        }
-        return new ExifTag(tagId, getTypeFromInfo(info),
-                getComponentCountFromInfo(info),
-                IfdId.TYPE_IFD_INTEROPERABILITY);
-    }
-
-    static boolean isValidType(short type) {
+    public static boolean isValidType(short type) {
         return type == TYPE_UNSIGNED_BYTE || type == TYPE_ASCII ||
                 type == TYPE_UNSIGNED_SHORT || type == TYPE_UNSIGNED_LONG ||
                 type == TYPE_UNSIGNED_RATIONAL || type == TYPE_UNDEFINED ||
                 type == TYPE_LONG || type == TYPE_RATIONAL;
     }
 
-    ExifTag(short tagId, short type, int componentCount, int ifd) {
+    // Use builtTag in ExifInterface instead of constructor.
+    ExifTag(short tagId, short type, int componentCount, int ifd,
+            boolean hasDefinedComponentCount) {
         mTagId = tagId;
         mDataType = type;
-        mComponentCount = componentCount;
-        mComponentCountDefined = getComponentCountDefined(tagId, ifd);
+        mComponentCountActual = componentCount;
+        mHasDefinedDefaultComponentCount = hasDefinedComponentCount;
         mIfd = ifd;
+        mValue = null;
+    }
+
+    /**
+     * Gets the element size of the given data type in bytes.
+     *
+     * @see #TYPE_ASCII
+     * @see #TYPE_LONG
+     * @see #TYPE_RATIONAL
+     * @see #TYPE_UNDEFINED
+     * @see #TYPE_UNSIGNED_BYTE
+     * @see #TYPE_UNSIGNED_LONG
+     * @see #TYPE_UNSIGNED_RATIONAL
+     * @see #TYPE_UNSIGNED_SHORT
+     */
+    public static int getElementSize(short type) {
+        return TYPE_TO_SIZE_MAP[type];
     }
 
     /**
@@ -941,8 +163,12 @@ public class ExifTag {
         return mIfd;
     }
 
+    protected void setIfd(int ifdId) {
+        mIfd = ifdId;
+    }
+
     /**
-     * Gets the ID of this tag.
+     * Gets the TID of this tag.
      */
     public short getTagId() {
         return mTagId;
@@ -974,349 +200,589 @@ public class ExifTag {
     /**
      * Gets the component count of this tag.
      */
+
+    // TODO: fix integer overflows with this
     public int getComponentCount() {
-        return mComponentCount;
+        return mComponentCountActual;
     }
 
     /**
-     * Sets the component count of this tag.
-     * Call this function before setValue() if the length of value does not
-     * match the component count.
+     * Sets the component count of this tag. Call this function before
+     * setValue() if the length of value does not match the component count.
      */
-    public void setComponentCount(int count) {
-        mComponentCount = count;
+    protected void forceSetComponentCount(int count) {
+        mComponentCountActual = count;
     }
 
     /**
-     * Returns true if this ExifTag contains value; otherwise, this tag will contain an offset value
-     * that links to the area where the actual value is located.
-     *
-     * @see #getOffset()
+     * Returns true if this ExifTag contains value; otherwise, this tag will
+     * contain an offset value that is determined when the tag is written.
      */
     public boolean hasValue() {
         return mValue != null;
     }
 
     /**
-     * Gets the offset of this tag. This is only valid if this data size > 4 and contains an offset
-     * to the location of the actual value.
+     * Sets integer values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_SHORT}. This method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
+     * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The value.length does NOT match the component count in the definition
+     * for this tag.</li>
+     * </ul>
      */
-    public int getOffset() {
-        return mOffset;
+    public boolean setValue(int[] value) {
+        if (checkBadComponentCount(value.length)) {
+            return false;
+        }
+        if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG &&
+                mDataType != TYPE_UNSIGNED_LONG) {
+            return false;
+        }
+        if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
+            return false;
+        } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
+            return false;
+        }
+
+        long[] data = new long[value.length];
+        System.arraycopy(value, 0, data, 0, value.length);
+        mValue = data;
+        mComponentCountActual = value.length;
+        return true;
     }
 
     /**
-     * Sets the offset of this tag.
+     * Sets integer value into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_SHORT}, or {@link #TYPE_LONG}. This method
+     * will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
+     * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The component count in the definition of this tag is not 1.</li>
+     * </ul>
      */
-    void setOffset(int offset) {
-        mOffset = offset;
+    public boolean setValue(int value) {
+        return setValue(new int[] {
+                value
+        });
     }
 
-    private void checkComponentCountOrThrow(int count)
-            throws IllegalArgumentException {
-        if (mComponentCountDefined && (mComponentCount != count)) {
-            throw new IllegalArgumentException("Tag " + mTagId + ": Required "
-                    + mComponentCount + " components but was given " + count
-                    + " component(s)");
+    /**
+     * Sets long values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The value.length does NOT match the component count in the definition
+     * for this tag.</li>
+     * </ul>
+     */
+    public boolean setValue(long[] value) {
+        if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) {
+            return false;
+        }
+        if (checkOverflowForUnsignedLong(value)) {
+            return false;
         }
+        mValue = value;
+        mComponentCountActual = value.length;
+        return true;
     }
 
-    private void throwTypeNotMatchedException(String className)
-            throws IllegalArgumentException {
-        throw new IllegalArgumentException("Tag " + mTagId + ": expect type " +
-                convertTypeToString(mDataType) + " but got " + className);
+    /**
+     * Sets long values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The component count in the definition for this tag is not 1.</li>
+     * </ul>
+     */
+    public boolean setValue(long value) {
+        return setValue(new long[] {
+                value
+        });
     }
 
-    private static String convertTypeToString(short type) {
-        switch (type) {
-            case TYPE_UNSIGNED_BYTE:
-                return "UNSIGNED_BYTE";
-            case TYPE_ASCII:
-                return "ASCII";
-            case TYPE_UNSIGNED_SHORT:
-                return "UNSIGNED_SHORT";
-            case TYPE_UNSIGNED_LONG:
-                return "UNSIGNED_LONG";
-            case TYPE_UNSIGNED_RATIONAL:
-                return "UNSIGNED_RATIONAL";
-            case TYPE_UNDEFINED:
-                return "UNDEFINED";
-            case TYPE_LONG:
-                return "LONG";
-            case TYPE_RATIONAL:
-                return "RATIONAL";
-            default:
-                return "";
+    /**
+     * Sets a string value into this tag. This method should be used for tags of
+     * type {@link #TYPE_ASCII}. The string is converted to an ASCII string.
+     * Characters that cannot be converted are replaced with '?'. The length of
+     * the string must be equal to either (component count -1) or (component
+     * count). The final byte will be set to the string null terminator '\0',
+     * overwriting the last character in the string if the value.length is equal
+     * to the component count. This method will fail if:
+     * <ul>
+     * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.</li>
+     * <li>The length of the string is not equal to (component count -1) or
+     * (component count) in the definition for this tag.</li>
+     * </ul>
+     */
+    public boolean setValue(String value) {
+        if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) {
+            return false;
         }
-    }
 
-    private static final int UNSIGNED_SHORT_MAX = 65535;
-    private static final long UNSIGNED_LONG_MAX = 4294967295L;
-    private static final long LONG_MAX = Integer.MAX_VALUE;
-    private static final long LONG_MIN = Integer.MIN_VALUE;
-
-    private void checkOverflowForUnsignedShort(int[] value) {
-        for (int v : value) {
-            if (v > UNSIGNED_SHORT_MAX || v < 0) {
-                throw new IllegalArgumentException(
-                        "Tag " + mTagId+ ": Value" + v +
-                        " is illegal for type UNSIGNED_SHORT");
-            }
+        byte[] buf = value.getBytes(US_ASCII);
+        byte[] finalBuf = (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED) ? buf : Arrays
+                .copyOf(buf, buf.length + 1);
+        int count = finalBuf.length;
+        if (checkBadComponentCount(count)) {
+            return false;
         }
+        mComponentCountActual = count;
+        mValue = finalBuf;
+        return true;
     }
 
-    private void checkOverflowForUnsignedLong(long[] value) {
-        for (long v: value) {
-            if (v < 0 || v > UNSIGNED_LONG_MAX) {
-                throw new IllegalArgumentException(
-                        "Tag " + mTagId+ ": Value" + v +
-                        " is illegal for type UNSIGNED_LONG");
-            }
+    /**
+     * Sets Rational values into this tag. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This
+     * method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL}
+     * or {@link #TYPE_RATIONAL}.</li>
+     * <li>The value overflows.</li>
+     * <li>The value.length does NOT match the component count in the definition
+     * for this tag.</li>
+     * </ul>
+     *
+     * @see Rational
+     */
+    public boolean setValue(Rational[] value) {
+        if (checkBadComponentCount(value.length)) {
+            return false;
         }
-    }
-
-    private void checkOverflowForUnsignedLong(int[] value) {
-        for (int v: value) {
-            if (v < 0) {
-                throw new IllegalArgumentException(
-                        "Tag " + mTagId+ ": Value" + v +
-                        " is illegal for type UNSIGNED_LONG");
-            }
+        if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) {
+            return false;
         }
-    }
-
-    private void checkOverflowForUnsignedRational(Rational[] value) {
-        for (Rational v: value) {
-            if (v.getNominator() < 0 || v.getDenominator() < 0
-                    || v.getNominator() > UNSIGNED_LONG_MAX
-                    || v.getDenominator() > UNSIGNED_LONG_MAX) {
-                throw new IllegalArgumentException(
-                        "Tag " + mTagId+ ": Value" + v +
-                        " is illegal for type UNSIGNED_RATIONAL");
-            }
+        if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
+            return false;
+        } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
+            return false;
         }
+
+        mValue = value;
+        mComponentCountActual = value.length;
+        return true;
     }
 
-    private void checkOverflowForRational(Rational[] value) {
-        for (Rational v: value) {
-            if (v.getNominator() < LONG_MIN || v.getDenominator() < LONG_MIN
-                    || v.getNominator() > LONG_MAX
-                    || v.getDenominator() > LONG_MAX) {
-                throw new IllegalArgumentException(
-                        "Tag " + mTagId+ ": Value" + v +
-                        " is illegal for type RATIONAL");
-            }
-        }
+    /**
+     * Sets a Rational value into this tag. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This
+     * method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL}
+     * or {@link #TYPE_RATIONAL}.</li>
+     * <li>The value overflows.</li>
+     * <li>The component count in the definition for this tag is not 1.</li>
+     * </ul>
+     *
+     * @see Rational
+     */
+    public boolean setValue(Rational value) {
+        return setValue(new Rational[] {
+                value
+        });
     }
 
     /**
-     * Sets integer values into this tag.
-     * @exception IllegalArgumentException for the following situation:
+     * Sets byte values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method
+     * will fail if:
      * <ul>
-     *     <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
-     *      {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
-     *     <li>The value overflows. </li>
-     *     <li>The value.length does NOT match the definition of component count in
-     *      EXIF standard.</li>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
+     * {@link #TYPE_UNDEFINED} .</li>
+     * <li>The length does NOT match the component count in the definition for
+     * this tag.</li>
      * </ul>
      */
-    public void setValue(int[] value) {
-        checkComponentCountOrThrow(value.length);
-        if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG &&
-                mDataType != TYPE_UNSIGNED_LONG) {
-            throwTypeNotMatchedException("int");
+    public boolean setValue(byte[] value, int offset, int length) {
+        if (checkBadComponentCount(length)) {
+            return false;
         }
-        if (mDataType == TYPE_UNSIGNED_SHORT) {
-            checkOverflowForUnsignedShort(value);
-        } else if (mDataType == TYPE_UNSIGNED_LONG) {
-            checkOverflowForUnsignedLong(value);
+        if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
+            return false;
         }
+        mValue = new byte[length];
+        System.arraycopy(value, offset, mValue, 0, length);
+        mComponentCountActual = length;
+        return true;
+    }
 
-        long[] data = new long[value.length];
-        for (int i = 0; i < value.length; i++) {
-            data[i] = value[i];
-        }
-        mValue = data;
-        mComponentCount = value.length;
+    /**
+     * Equivalent to setValue(value, 0, value.length).
+     */
+    public boolean setValue(byte[] value) {
+        return setValue(value, 0, value.length);
     }
 
     /**
-     * Sets integer values into this tag.
-     * @exception IllegalArgumentException For the following situation:
+     * Sets byte value into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method
+     * will fail if:
      * <ul>
-     *     <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
-     *      {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
-     *     <li>The value overflows.</li>
-     *     <li>The component count in the definition of EXIF standard is not 1.</li>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
+     * {@link #TYPE_UNDEFINED} .</li>
+     * <li>The component count in the definition for this tag is not 1.</li>
      * </ul>
      */
-    public void setValue(int value) {
-        checkComponentCountOrThrow(1);
-        setValue(new int[] {value});
+    public boolean setValue(byte value) {
+        return setValue(new byte[] {
+                value
+        });
     }
 
     /**
-     * Sets long values into this tag.
-     * @exception IllegalArgumentException For the following situation:
+     * Sets the value for this tag using an appropriate setValue method for the
+     * given object. This method will fail if:
      * <ul>
-     *      <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
-     *      <li>The value overflows. </li>
-     *      <li>The value.length does NOT match the definition of component count in
-     *       EXIF standard.</li>
+     * <li>The corresponding setValue method for the class of the object passed
+     * in would fail.</li>
+     * <li>There is no obvious way to cast the object passed in into an EXIF tag
+     * type.</li>
      * </ul>
      */
-    public void setValue(long[] value) {
-        checkComponentCountOrThrow(value.length);
-        if (mDataType != TYPE_UNSIGNED_LONG) {
-            throwTypeNotMatchedException("long");
+    public boolean setValue(Object obj) {
+        if (obj == null) {
+            return false;
+        } else if (obj instanceof Short) {
+            return setValue(((Short) obj).shortValue() & 0x0ffff);
+        } else if (obj instanceof String) {
+            return setValue((String) obj);
+        } else if (obj instanceof int[]) {
+            return setValue((int[]) obj);
+        } else if (obj instanceof long[]) {
+            return setValue((long[]) obj);
+        } else if (obj instanceof Rational) {
+            return setValue((Rational) obj);
+        } else if (obj instanceof Rational[]) {
+            return setValue((Rational[]) obj);
+        } else if (obj instanceof byte[]) {
+            return setValue((byte[]) obj);
+        } else if (obj instanceof Integer) {
+            return setValue(((Integer) obj).intValue());
+        } else if (obj instanceof Long) {
+            return setValue(((Long) obj).longValue());
+        } else if (obj instanceof Byte) {
+            return setValue(((Byte) obj).byteValue());
+        } else if (obj instanceof Short[]) {
+            // Nulls in this array are treated as zeroes.
+            Short[] arr = (Short[]) obj;
+            int[] fin = new int[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].shortValue() & 0x0ffff;
+            }
+            return setValue(fin);
+        } else if (obj instanceof Integer[]) {
+            // Nulls in this array are treated as zeroes.
+            Integer[] arr = (Integer[]) obj;
+            int[] fin = new int[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].intValue();
+            }
+            return setValue(fin);
+        } else if (obj instanceof Long[]) {
+            // Nulls in this array are treated as zeroes.
+            Long[] arr = (Long[]) obj;
+            long[] fin = new long[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].longValue();
+            }
+            return setValue(fin);
+        } else if (obj instanceof Byte[]) {
+            // Nulls in this array are treated as zeroes.
+            Byte[] arr = (Byte[]) obj;
+            byte[] fin = new byte[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].byteValue();
+            }
+            return setValue(fin);
+        } else {
+            return false;
         }
-        checkOverflowForUnsignedLong(value);
-        mValue = value;
-        mComponentCount = value.length;
     }
 
     /**
-     * Sets long values into this tag.
-     * @exception IllegalArgumentException For the following situation:
-     * <ul>
-     *     <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
-     *     <li>The value overflows. </li>
-     *     <li>The component count in the definition of EXIF standard is not 1.</li>
-     * </ul>
+     * Sets a timestamp to this tag. The method converts the timestamp with the
+     * format of "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}. This
+     * method will fail if the data type is not {@link #TYPE_ASCII} or the
+     * component count of this tag is not 20 or undefined.
+     *
+     * @param time the number of milliseconds since Jan. 1, 1970 GMT
+     * @return true on success
      */
-    public void setValue(long value) {
-        setValue(new long[] {value});
+    public boolean setTimeValue(long time) {
+        // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe
+        synchronized (TIME_FORMAT) {
+            return setValue(TIME_FORMAT.format(new Date(time)));
+        }
     }
 
     /**
-     * Sets a string value into this tag. The value is treated as an ASCII string where we only
-     * preserve the lower byte of each character. The length of the string should be equal
-     * to either (component count -1) or (component count). A "0" byte will be appeneded while
-     * written to the EXIF file. If the length equals (component count), the final byte will be
-     * replaced by a "0" byte.
+     * Gets the value as a String. This method should be used for tags of type
+     * {@link #TYPE_ASCII}.
      *
-     * @exception IllegalArgumentException If the data type is not {@link #TYPE_ASCII}
-     * or the length of the string is not equal to (component count -1) and (component count)
-     */
-    public void setValue(String value) {
-        if (mDataType != TYPE_ASCII) {
-            throwTypeNotMatchedException("String");
+     * @return the value as a String, or null if the tag's value does not exist
+     *         or cannot be converted to a String.
+     */
+    public String getValueAsString() {
+        if (mValue == null) {
+            return null;
+        } else if (mValue instanceof String) {
+            return (String) mValue;
+        } else if (mValue instanceof byte[]) {
+            return new String((byte[]) mValue, US_ASCII);
         }
+        return null;
+    }
 
-        byte[] buf = new byte[value.length()];
-        for (int i = 0, n = value.length(); i < n; i++) {
-            buf[i] = (byte) value.charAt(i);
+    /**
+     * Gets the value as a String. This method should be used for tags of type
+     * {@link #TYPE_ASCII}.
+     *
+     * @param defaultValue the String to return if the tag's value does not
+     *            exist or cannot be converted to a String.
+     * @return the tag's value as a String, or the defaultValue.
+     */
+    public String getValueAsString(String defaultValue) {
+        String s = getValueAsString();
+        if (s == null) {
+            return defaultValue;
         }
+        return s;
+    }
 
-        int count = buf.length;
-        if (mComponentCountDefined) {
-            if (mComponentCount != count && mComponentCount != count + 1) {
-                throw new IllegalArgumentException("Tag " + mTagId + ": Required "
-                        + mComponentCount + " or " + (mComponentCount + 1)
-                        + " components but was given " + count
-                        + " component(s)");
-            }
-        } else {
-            mComponentCount = buf[count - 1] == 0 ? count : count + 1;
+    /**
+     * Gets the value as a byte array. This method should be used for tags of
+     * type {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+     *
+     * @return the value as a byte array, or null if the tag's value does not
+     *         exist or cannot be converted to a byte array.
+     */
+    public byte[] getValueAsBytes() {
+        if (mValue instanceof byte[]) {
+            return (byte[]) mValue;
         }
-        mValue = buf;
+        return null;
     }
 
     /**
-     * Sets Rational values into this tag.
-     * @exception IllegalArgumentException For the following situation:
-     * <ul>
-     *      <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or
-     *       {@link #TYPE_RATIONAL} .</li>
-     *      <li>The value overflows. </li>
-     *      <li>The value.length does NOT match the definition of component count in
-     *       EXIF standard.</li>
-     * </ul>
+     * Gets the value as a byte. If there are more than 1 bytes in this value,
+     * gets the first byte. This method should be used for tags of type
+     * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+     *
+     * @param defaultValue the byte to return if tag's value does not exist or
+     *            cannot be converted to a byte.
+     * @return the tag's value as a byte, or the defaultValue.
+     */
+    public byte getValueAsByte(byte defaultValue) {
+        byte[] b = getValueAsBytes();
+        if (b == null || b.length < 1) {
+            return defaultValue;
+        }
+        return b[0];
+    }
+
+    /**
+     * Gets the value as an array of Rationals. This method should be used for
+     * tags of type {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @return the value as as an array of Rationals, or null if the tag's value
+     *         does not exist or cannot be converted to an array of Rationals.
      */
-    public void setValue(Rational[] value) {
-        if (mDataType == TYPE_UNSIGNED_RATIONAL) {
-            checkOverflowForUnsignedRational(value);
-        } else if (mDataType == TYPE_RATIONAL) {
-            checkOverflowForRational(value);
-        } else {
-            throwTypeNotMatchedException("Rational");
+    public Rational[] getValueAsRationals() {
+        if (mValue instanceof Rational[]) {
+            return (Rational[]) mValue;
         }
-        checkComponentCountOrThrow(value.length);
-        mValue = value;
-        mComponentCount = value.length;
+        return null;
     }
 
     /**
-     * Sets Rational values into this tag.
-     * @exception IllegalArgumentException For the following situation:
-     * <ul>
-     *      <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or
-     *       {@link #TYPE_RATIONAL} .</li>
-     *      <li>The value overflows. </li>
-     *      <li>The component count in the definition of EXIF standard is not 1.</li>
-     * </ul>
-     * */
-    public void setValue(Rational value) {
-        setValue(new Rational[] {value});
+     * Gets the value as a Rational. If there are more than 1 Rationals in this
+     * value, gets the first one. This method should be used for tags of type
+     * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @param defaultValue the Rational to return if tag's value does not exist
+     *            or cannot be converted to a Rational.
+     * @return the tag's value as a Rational, or the defaultValue.
+     */
+    public Rational getValueAsRational(Rational defaultValue) {
+        Rational[] r = getValueAsRationals();
+        if (r == null || r.length < 1) {
+            return defaultValue;
+        }
+        return r[0];
     }
 
     /**
-     * Sets byte values into this tag.
-     * @exception IllegalArgumentException For the following situation:
-     * <ul>
-     *      <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
-     *       {@link #TYPE_UNDEFINED} .</li>
-     *      <li>The length does NOT match the definition of component count in EXIF standard.</li>
-     * </ul>
-     * */
-    public void setValue(byte[] value, int offset, int length) {
-        checkComponentCountOrThrow(length);
-        if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
-            throwTypeNotMatchedException("byte");
+     * Gets the value as a Rational. If there are more than 1 Rationals in this
+     * value, gets the first one. This method should be used for tags of type
+     * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @param defaultValue the numerator of the Rational to return if tag's
+     *            value does not exist or cannot be converted to a Rational (the
+     *            denominator will be 1).
+     * @return the tag's value as a Rational, or the defaultValue.
+     */
+    public Rational getValueAsRational(long defaultValue) {
+        Rational defaultVal = new Rational(defaultValue, 1);
+        return getValueAsRational(defaultVal);
+    }
+
+    /**
+     * Gets the value as an array of ints. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @return the value as as an array of ints, or null if the tag's value does
+     *         not exist or cannot be converted to an array of ints.
+     */
+    public int[] getValueAsInts() {
+        if (mValue == null) {
+            return null;
+        } else if (mValue instanceof long[]) {
+            long[] val = (long[]) mValue;
+            int[] arr = new int[val.length];
+            for (int i = 0; i < val.length; i++) {
+                arr[i] = (int) val[i]; // Truncates
+            }
+            return arr;
         }
-        mValue = new byte[length];
-        System.arraycopy(value, offset, mValue, 0, length);
-        mComponentCount = length;
+        return null;
     }
 
     /**
-     * Equivalent to setValue(value, 0, value.length).
+     * Gets the value as an int. If there are more than 1 ints in this value,
+     * gets the first one. This method should be used for tags of type
+     * {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @param defaultValue the int to return if tag's value does not exist or
+     *            cannot be converted to an int.
+     * @return the tag's value as a int, or the defaultValue.
+     */
+    public int getValueAsInt(int defaultValue) {
+        int[] i = getValueAsInts();
+        if (i == null || i.length < 1) {
+            return defaultValue;
+        }
+        return i[0];
+    }
+
+    /**
+     * Gets the value as an array of longs. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @return the value as as an array of longs, or null if the tag's value
+     *         does not exist or cannot be converted to an array of longs.
      */
-    public void setValue(byte[] value) {
-        setValue(value, 0, value.length);
+    public long[] getValueAsLongs() {
+        if (mValue instanceof long[]) {
+            return (long[]) mValue;
+        }
+        return null;
     }
 
-    private static final SimpleDateFormat TIME_FORMAT =
-            new SimpleDateFormat("yyyy:MM:dd kk:mm:ss");
+    /**
+     * Gets the value or null if none exists. If there are more than 1 longs in
+     * this value, gets the first one. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @param defaultValue the long to return if tag's value does not exist or
+     *            cannot be converted to a long.
+     * @return the tag's value as a long, or the defaultValue.
+     */
+    public long getValueAsLong(long defaultValue) {
+        long[] l = getValueAsLongs();
+        if (l == null || l.length < 1) {
+            return defaultValue;
+        }
+        return l[0];
+    }
 
     /**
-     * Sets a timestamp to this tag. The method converts the timestamp with the format of
-     * "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}.
+     * Gets the tag's value or null if none exists.
+     */
+    public Object getValue() {
+        return mValue;
+    }
+
+    /**
+     * Gets a long representation of the value.
      *
-     * @param time the number of milliseconds since Jan. 1, 1970 GMT
-     * @exception IllegalArgumentException If the data type is not {@link #TYPE_ASCII}
-     * or the component count of this tag is not 20 or undefined
+     * @param defaultValue value to return if there is no value or value is a
+     *            rational with a denominator of 0.
+     * @return the tag's value as a long, or defaultValue if no representation
+     *         exists.
+     */
+    public long forceGetValueAsLong(long defaultValue) {
+        long[] l = getValueAsLongs();
+        if (l != null && l.length >= 1) {
+            return l[0];
+        }
+        byte[] b = getValueAsBytes();
+        if (b != null && b.length >= 1) {
+            return b[0];
+        }
+        Rational[] r = getValueAsRationals();
+        if (r != null && r.length >= 1 && r[0].getDenominator() != 0) {
+            return (long) r[0].toDouble();
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Gets a string representation of the value.
      */
-    public void setTimeValue(long time) {
-        // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe
-        synchronized (TIME_FORMAT) {
-            setValue(TIME_FORMAT.format(new Date(time)));
+    public String forceGetValueAsString() {
+        if (mValue == null) {
+            return "";
+        } else if (mValue instanceof byte[]) {
+            if (mDataType == TYPE_ASCII) {
+                return new String((byte[]) mValue, US_ASCII);
+            } else {
+                return Arrays.toString((byte[]) mValue);
+            }
+        } else if (mValue instanceof long[]) {
+            if (((long[]) mValue).length == 1) {
+                return String.valueOf(((long[]) mValue)[0]);
+            } else {
+                return Arrays.toString((long[]) mValue);
+            }
+        } else if (mValue instanceof Object[]) {
+            if (((Object[]) mValue).length == 1) {
+                Object val = ((Object[]) mValue)[0];
+                if (val == null) {
+                    return "";
+                } else {
+                    return val.toString();
+                }
+            } else {
+                return Arrays.toString((Object[]) mValue);
+            }
+        } else {
+            return mValue.toString();
         }
     }
+
     /**
      * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG},
-     * {@link #TYPE_UNDEFINED}, {@link #TYPE_UNSIGNED_BYTE}, {@link #TYPE_UNSIGNED_LONG}, or
-     * {@link #TYPE_UNSIGNED_SHORT}. For {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL},
-     * call {@link #getRational(int)} instead.
+     * {@link #TYPE_UNDEFINED}, {@link #TYPE_UNSIGNED_BYTE},
+     * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}. For
+     * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}, call
+     * {@link #getRational(int)} instead.
      *
-     * @exception IllegalArgumentException if the data type is {@link #TYPE_RATIONAL} or
-     * {@link #TYPE_UNSIGNED_RATIONAL}.
+     * @exception IllegalArgumentException if the data type is
+     *                {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
      */
-    public long getValueAt(int index) {
+    protected long getValueAt(int index) {
         if (mValue instanceof long[]) {
-            return ((long[]) mValue) [index];
+            return ((long[]) mValue)[index];
         } else if (mValue instanceof byte[]) {
-            return ((byte[]) mValue) [index];
+            return ((byte[]) mValue)[index];
         }
         throw new IllegalArgumentException("Cannot get integer value from "
                 + convertTypeToString(mDataType));
@@ -1324,9 +790,11 @@ public class ExifTag {
 
     /**
      * Gets the {@link #TYPE_ASCII} data.
-     * @exception IllegalArgumentException If the type is NOT {@link #TYPE_ASCII}.
+     *
+     * @exception IllegalArgumentException If the type is NOT
+     *                {@link #TYPE_ASCII}.
      */
-    public String getString() {
+    protected String getString() {
         if (mDataType != TYPE_ASCII) {
             throw new IllegalArgumentException("Cannot get ASCII value from "
                     + convertTypeToString(mDataType));
@@ -1337,27 +805,28 @@ public class ExifTag {
     /*
      * Get the converted ascii byte. Used by ExifOutputStream.
      */
-    byte[] getStringByte() {
+    protected byte[] getStringByte() {
         return (byte[]) mValue;
     }
 
     /**
      * Gets the {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL} data.
-     * @exception IllegalArgumentException If the type is NOT {@link #TYPE_RATIONAL} or
-     * {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @exception IllegalArgumentException If the type is NOT
+     *                {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
      */
-    public Rational getRational(int index) {
+    protected Rational getRational(int index) {
         if ((mDataType != TYPE_RATIONAL) && (mDataType != TYPE_UNSIGNED_RATIONAL)) {
             throw new IllegalArgumentException("Cannot get RATIONAL value from "
                     + convertTypeToString(mDataType));
         }
-        return ((Rational[]) mValue) [index];
+        return ((Rational[]) mValue)[index];
     }
 
     /**
      * Equivalent to getBytes(buffer, 0, buffer.length).
      */
-    public void getBytes(byte[] buf) {
+    protected void getBytes(byte[] buf) {
         getBytes(buf, 0, buf.length);
     }
 
@@ -1366,56 +835,147 @@ public class ExifTag {
      *
      * @param buf the byte array in which to store the bytes read.
      * @param offset the initial position in buffer to store the bytes.
-     * @param length the maximum number of bytes to store in buffer. If length > component count,
-     * only the valid bytes will be stored.
-     *
-     * @exception IllegalArgumentException If the type is NOT {@link #TYPE_UNDEFINED} or
-     * {@link #TYPE_UNSIGNED_BYTE}.
+     * @param length the maximum number of bytes to store in buffer. If length >
+     *            component count, only the valid bytes will be stored.
+     * @exception IllegalArgumentException If the type is NOT
+     *                {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
      */
-    public void getBytes(byte[] buf, int offset, int length) {
+    protected void getBytes(byte[] buf, int offset, int length) {
         if ((mDataType != TYPE_UNDEFINED) && (mDataType != TYPE_UNSIGNED_BYTE)) {
             throw new IllegalArgumentException("Cannot get BYTE value from "
                     + convertTypeToString(mDataType));
         }
         System.arraycopy(mValue, 0, buf, offset,
-                (length > mComponentCount) ? mComponentCount : length);
+                (length > mComponentCountActual) ? mComponentCountActual : length);
     }
 
     /**
-     * Returns true if the ID is one of the following: {@link #TAG_EXIF_IFD},
-     * {@link #TAG_GPS_IFD}, {@link #TAG_JPEG_INTERCHANGE_FORMAT},
-     * {@link #TAG_STRIP_OFFSETS}, {@link #TAG_INTEROPERABILITY_IFD}
+     * Gets the offset of this tag. This is only valid if this data size > 4 and
+     * contains an offset to the location of the actual value.
      */
-    static boolean isOffsetTag(short tagId) {
-        return tagId == TAG_EXIF_IFD
-                || tagId == TAG_GPS_IFD
-                || tagId == TAG_JPEG_INTERCHANGE_FORMAT
-                || tagId == TAG_STRIP_OFFSETS
-                || tagId == TAG_INTEROPERABILITY_IFD;
+    protected int getOffset() {
+        return mOffset;
     }
 
     /**
-     * Returns true if the ID is one of the following: {@link #TAG_EXIF_IFD},
-     * {@link #TAG_GPS_IFD}, {@link #TAG_INTEROPERABILITY_IFD}
+     * Sets the offset of this tag.
      */
-    static boolean isSubIfdOffsetTag(short tagId) {
-        return tagId == TAG_EXIF_IFD
-                || tagId == TAG_GPS_IFD
-                || tagId == TAG_INTEROPERABILITY_IFD;
+    protected void setOffset(int offset) {
+        mOffset = offset;
+    }
+
+    protected void setHasDefinedCount(boolean d) {
+        mHasDefinedDefaultComponentCount = d;
+    }
+
+    private boolean checkBadComponentCount(int count) {
+        if (mHasDefinedDefaultComponentCount && (mComponentCountActual != count)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static String convertTypeToString(short type) {
+        switch (type) {
+            case TYPE_UNSIGNED_BYTE:
+                return "UNSIGNED_BYTE";
+            case TYPE_ASCII:
+                return "ASCII";
+            case TYPE_UNSIGNED_SHORT:
+                return "UNSIGNED_SHORT";
+            case TYPE_UNSIGNED_LONG:
+                return "UNSIGNED_LONG";
+            case TYPE_UNSIGNED_RATIONAL:
+                return "UNSIGNED_RATIONAL";
+            case TYPE_UNDEFINED:
+                return "UNDEFINED";
+            case TYPE_LONG:
+                return "LONG";
+            case TYPE_RATIONAL:
+                return "RATIONAL";
+            default:
+                return "";
+        }
+    }
+
+    private boolean checkOverflowForUnsignedShort(int[] value) {
+        for (int v : value) {
+            if (v > UNSIGNED_SHORT_MAX || v < 0) {
+                return true;
+            }
+        }
+        return false;
     }
+
+    private boolean checkOverflowForUnsignedLong(long[] value) {
+        for (long v : value) {
+            if (v < 0 || v > UNSIGNED_LONG_MAX) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForUnsignedLong(int[] value) {
+        for (int v : value) {
+            if (v < 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForUnsignedRational(Rational[] value) {
+        for (Rational v : value) {
+            if (v.getNumerator() < 0 || v.getDenominator() < 0
+                    || v.getNumerator() > UNSIGNED_LONG_MAX
+                    || v.getDenominator() > UNSIGNED_LONG_MAX) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForRational(Rational[] value) {
+        for (Rational v : value) {
+            if (v.getNumerator() < LONG_MIN || v.getDenominator() < LONG_MIN
+                    || v.getNumerator() > LONG_MAX
+                    || v.getDenominator() > LONG_MAX) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
         if (obj instanceof ExifTag) {
             ExifTag tag = (ExifTag) obj;
+            if (tag.mTagId != this.mTagId
+                    || tag.mComponentCountActual != this.mComponentCountActual
+                    || tag.mDataType != this.mDataType) {
+                return false;
+            }
             if (mValue != null) {
-                if (mValue instanceof long[]) {
-                    if (!(tag.mValue instanceof long[])) return false;
+                if (tag.mValue == null) {
+                    return false;
+                } else if (mValue instanceof long[]) {
+                    if (!(tag.mValue instanceof long[])) {
+                        return false;
+                    }
                     return Arrays.equals((long[]) mValue, (long[]) tag.mValue);
                 } else if (mValue instanceof Rational[]) {
-                    if (!(tag.mValue instanceof Rational[])) return false;
+                    if (!(tag.mValue instanceof Rational[])) {
+                        return false;
+                    }
                     return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue);
                 } else if (mValue instanceof byte[]) {
-                    if (!(tag.mValue instanceof byte[])) return false;
+                    if (!(tag.mValue instanceof byte[])) {
+                        return false;
+                    }
                     return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue);
                 } else {
                     return mValue.equals(tag.mValue);
@@ -1426,4 +986,12 @@ public class ExifTag {
         }
         return false;
     }
+
+    @Override
+    public String toString() {
+        return String.format("tag id: %04X\n", mTagId) + "ifd id: " + mIfd + "\ntype: "
+                + convertTypeToString(mDataType) + "\ncount: " + mComponentCountActual
+                + "\noffset: " + mOffset + "\nvalue: " + forceGetValueAsString() + "\n";
+    }
+
 }
index 6336049..093944a 100644 (file)
@@ -30,7 +30,10 @@ class IfdData {
     private final int mIfdId;
     private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>();
     private int mOffsetToNextIfd = 0;
-
+    private static final int[] sIfds = {
+            IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF,
+            IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS
+    };
     /**
      * Creates an IfdData with given IFD ID.
      *
@@ -40,14 +43,18 @@ class IfdData {
      * @see IfdId#TYPE_IFD_GPS
      * @see IfdId#TYPE_IFD_INTEROPERABILITY
      */
-    public IfdData(int ifdId) {
+    IfdData(int ifdId) {
         mIfdId = ifdId;
     }
 
+    static protected int[] getIfds() {
+        return sIfds;
+    }
+
     /**
      * Get a array the contains all {@link ExifTag} in this IFD.
      */
-    public ExifTag[] getAllTags() {
+    protected ExifTag[] getAllTags() {
         return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
     }
 
@@ -60,70 +67,86 @@ class IfdData {
      * @see IfdId#TYPE_IFD_GPS
      * @see IfdId#TYPE_IFD_INTEROPERABILITY
      */
-    public int getId() {
+    protected int getId() {
         return mIfdId;
     }
 
     /**
-     * Gets the {@link ExifTag} with given tag id. Return null if there is no such tag.
+     * Gets the {@link ExifTag} with given tag id. Return null if there is no
+     * such tag.
      */
-    public ExifTag getTag(short tagId) {
+    protected ExifTag getTag(short tagId) {
         return mExifTags.get(tagId);
     }
 
     /**
      * Adds or replaces a {@link ExifTag}.
      */
-    public void setTag(ExifTag tag) {
-        mExifTags.put(tag.getTagId(), tag);
+    protected ExifTag setTag(ExifTag tag) {
+        tag.setIfd(mIfdId);
+        return mExifTags.put(tag.getTagId(), tag);
+    }
+
+    protected boolean checkCollision(short tagId) {
+        return mExifTags.get(tagId) != null;
     }
 
     /**
      * Removes the tag of the given ID
      */
-    public void removeTag(short tagId) {
+    protected void removeTag(short tagId) {
         mExifTags.remove(tagId);
     }
 
     /**
      * Gets the tags count in the IFD.
      */
-    public int getTagCount() {
+    protected int getTagCount() {
         return mExifTags.size();
     }
 
     /**
      * Sets the offset of next IFD.
      */
-    void setOffsetToNextIfd(int offset) {
+    protected void setOffsetToNextIfd(int offset) {
         mOffsetToNextIfd = offset;
     }
 
     /**
      * Gets the offset of next IFD.
      */
-    int getOffsetToNextIfd() {
+    protected int getOffsetToNextIfd() {
         return mOffsetToNextIfd;
     }
 
     /**
-     * Returns true if all tags in this two IFDs are equal. Note that tags of IFDs offset or
-     * thumbnail offset will be ignored.
+     * Returns true if all tags in this two IFDs are equal. Note that tags of
+     * IFDs offset or thumbnail offset will be ignored.
      */
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
         if (obj instanceof IfdData) {
             IfdData data = (IfdData) obj;
             if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
                 ExifTag[] tags = data.getAllTags();
-                for (ExifTag tag: tags) {
-                    if (ExifTag.isOffsetTag(tag.getTagId())) continue;
+                for (ExifTag tag : tags) {
+                    if (ExifInterface.isOffsetTag(tag.getTagId())) {
+                        continue;
+                    }
                     ExifTag tag2 = mExifTags.get(tag.getTagId());
-                    if (!tag.equals(tag2)) return false;
+                    if (!tag.equals(tag2)) {
+                        return false;
+                    }
                 }
                 return true;
             }
         }
         return false;
     }
-}
\ No newline at end of file
+}
index c05fb02..7842edb 100644 (file)
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.gallery3d.exif;
 
 /**
@@ -26,4 +27,5 @@ public interface IfdId {
     public static final int TYPE_IFD_GPS = 4;
     /* This is used in ExifData to allocate enough IfdData */
     static final int TYPE_IFD_COUNT = 5;
+
 }
index 4f785a8..428e6b9 100644 (file)
@@ -29,24 +29,28 @@ class OrderedDataOutputStream extends FilterOutputStream {
         super(out);
     }
 
-    public void setByteOrder(ByteOrder order) {
+    public OrderedDataOutputStream setByteOrder(ByteOrder order) {
         mByteBuffer.order(order);
+        return this;
     }
 
-    public void writeShort(short value) throws IOException {
+    public OrderedDataOutputStream writeShort(short value) throws IOException {
         mByteBuffer.rewind();
         mByteBuffer.putShort(value);
         out.write(mByteBuffer.array(), 0, 2);
-     }
+        return this;
+    }
 
-    public void writeInt(int value) throws IOException {
+    public OrderedDataOutputStream writeInt(int value) throws IOException {
         mByteBuffer.rewind();
         mByteBuffer.putInt(value);
         out.write(mByteBuffer.array());
+        return this;
     }
 
-    public void writeRational(Rational rational) throws IOException {
-        writeInt((int) rational.getNominator());
+    public OrderedDataOutputStream writeRational(Rational rational) throws IOException {
+        writeInt((int) rational.getNumerator());
         writeInt((int) rational.getDenominator());
+        return this;
     }
 }
index 3f8c18a..591d63f 100644 (file)
 package com.android.gallery3d.exif;
 
 /**
- * The rational data type of EXIF tag.
+ * The rational data type of EXIF tag. Contains a pair of longs representing the
+ * numerator and denominator of a Rational number.
  */
 public class Rational {
 
-    private final long mNominator;
+    private final long mNumerator;
     private final long mDenominator;
 
+    /**
+     * Create a Rational with a given numerator and denominator.
+     *
+     * @param nominator
+     * @param denominator
+     */
     public Rational(long nominator, long denominator) {
-        mNominator = nominator;
+        mNumerator = nominator;
         mDenominator = denominator;
     }
 
-    /*
-     * Gets the nominator of the rational.
+    /**
+     * Create a copy of a Rational.
+     */
+    public Rational(Rational r) {
+        mNumerator = r.mNumerator;
+        mDenominator = r.mDenominator;
+    }
+
+    /**
+     * Gets the numerator of the rational.
      */
-    public long getNominator() {
-        return mNominator;
+    public long getNumerator() {
+        return mNumerator;
     }
 
-    /*
+    /**
      * Gets the denominator of the rational
      */
     public long getDenominator() {
         return mDenominator;
     }
 
+    /**
+     * Gets the rational value as type double. Will cause a divide-by-zero error
+     * if the denominator is 0.
+     */
+    public double toDouble() {
+        return mNumerator / (double) mDenominator;
+    }
+
     @Override
     public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof Rational) {
             Rational data = (Rational) obj;
-            return mNominator == data.mNominator && mDenominator == data.mDenominator;
+            return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
         }
         return false;
     }
 
     @Override
     public String toString() {
-        return mNominator + "/" + mDenominator;
-    }
-
-    /*
-     * Gets the rational value as type double.
-     */
-    public double toDouble() {
-        return mNominator / (double) mDenominator;
+        return mNumerator + "/" + mDenominator;
     }
-}
\ No newline at end of file
+}
index 6055565..ee39d67 100644 (file)
@@ -18,9 +18,7 @@ package com.android.camera;
 
 import android.util.Log;
 
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifParser;
-import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.ExifInterface;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -31,44 +29,23 @@ public class Exif {
 
     // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
     public static int getOrientation(byte[] jpeg) {
-        if (jpeg == null) return 0;
+        if (jpeg == null) {
+            return 0;
+        }
 
+        ExifInterface exif = new ExifInterface();
         InputStream is = new ByteArrayInputStream(jpeg);
-
         try {
-            ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
-            int event = parser.next();
-            while(event != ExifParser.EVENT_END) {
-                if (event == ExifParser.EVENT_NEW_TAG) {
-                    ExifTag tag = parser.getTag();
-                    if (tag.getTagId() == ExifTag.TAG_ORIENTATION &&
-                            tag.hasValue()) {
-                        int orient = (int) tag.getValueAt(0);
-                        switch (orient) {
-                            case ExifTag.Orientation.TOP_LEFT:
-                                return 0;
-                            case ExifTag.Orientation.BOTTOM_LEFT:
-                                return 180;
-                            case ExifTag.Orientation.RIGHT_TOP:
-                                return 90;
-                            case ExifTag.Orientation.RIGHT_BOTTOM:
-                                return 270;
-                            default:
-                                Log.i(TAG, "Unsupported orientation");
-                                return 0;
-                        }
-                    }
-                }
-                event = parser.next();
+            exif.readExif(is);
+            Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+            if (val == null) {
+                return 0;
+            } else {
+                return ExifInterface.getRotationForOrientationValue(val.shortValue());
             }
-            Log.i(TAG, "Orientation not found");
-            return 0;
         } catch (IOException e) {
             Log.w(TAG, "Failed to read EXIF orientation", e);
             return 0;
-        } catch (ExifInvalidFormatException e) {
-            Log.w(TAG, "Failed to read EXIF orientation", e);
-            return 0;
         }
     }
 }
index 623d96d..dc27f56 100644 (file)
@@ -60,10 +60,7 @@ import com.android.camera.ui.PopupManager;
 import com.android.camera.ui.Rotatable;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
-import com.android.gallery3d.exif.ExifData;
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifOutputStream;
-import com.android.gallery3d.exif.ExifReader;
+import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.exif.ExifTag;
 import com.android.gallery3d.ui.GLRootView;
 import com.android.gallery3d.util.UsageStatistics;
@@ -913,31 +910,18 @@ public class PanoramaModule implements CameraModule,
                     mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
             String filepath = Storage.generateFilepath(filename);
 
-            ExifOutputStream out = null;
-            InputStream is = null;
+            ExifInterface exif = new ExifInterface();
             try {
-                is = new ByteArrayInputStream(jpegData);
-                ExifReader reader = new ExifReader();
-                ExifData data = reader.read(is);
-
-                // Add Exif tags.
-                data.addGpsDateTimeStampTag(mTimeTaken);
-                data.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, mTimeTaken, TimeZone.getDefault());
-                data.addTag(ExifTag.TAG_ORIENTATION).
-                        setValue(getExifOrientation(orientation));
-
-                out = new ExifOutputStream(new FileOutputStream(filepath));
-                out.setExifData(data);
-                out.write(jpegData);
+                exif.readExif(jpegData);
+                exif.addGpsDateTimeStampTag(mTimeTaken);
+                exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken,
+                        TimeZone.getDefault());
+                exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
+                        ExifInterface.getOrientationValueForRotation(orientation)));
+                exif.writeExif(jpegData, filepath);
             } catch (IOException e) {
-                Log.e(TAG, "Cannot set EXIF for " + filepath, e);
+                Log.e(TAG, "Cannot set exif for " + filepath, e);
                 Storage.writeFile(filepath, jpegData);
-            } catch (ExifInvalidFormatException e) {
-                Log.e(TAG, "Cannot set EXIF for " + filepath, e);
-                Storage.writeFile(filepath, jpegData);
-            } finally {
-                Util.closeSilently(out);
-                Util.closeSilently(is);
             }
 
             int jpegLength = (int) (new File(filepath).length());
@@ -947,21 +931,6 @@ public class PanoramaModule implements CameraModule,
         return null;
     }
 
-    private static int getExifOrientation(int orientation) {
-        switch (orientation) {
-            case 0:
-                return ExifTag.Orientation.TOP_LEFT;
-            case 90:
-                return ExifTag.Orientation.RIGHT_TOP;
-            case 180:
-                return ExifTag.Orientation.BOTTOM_LEFT;
-            case 270:
-                return ExifTag.Orientation.RIGHT_BOTTOM;
-            default:
-                throw new AssertionError("invalid: " + orientation);
-        }
-    }
-
     private void clearMosaicFrameProcessorIfNeeded() {
         if (!mPaused || mThreadRunning) return;
         // Only clear the processor if it is initialized by this activity
index 30aba7e..950e7de 100644 (file)
@@ -18,55 +18,31 @@ package com.android.gallery3d.data;
 
 import android.util.Log;
 
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifParser;
-import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.ExifInterface;
 
 import java.io.IOException;
 import java.io.InputStream;
 
 public class Exif {
-    private static final String TAG = "GalleryExif";
+    private static final String TAG = "CameraExif";
 
+    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
     public static int getOrientation(InputStream is) {
         if (is == null) {
             return 0;
         }
-
+        ExifInterface exif = new ExifInterface();
         try {
-            ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
-            int event = parser.next();
-            while (event != ExifParser.EVENT_END) {
-                if (event == ExifParser.EVENT_NEW_TAG) {
-                    ExifTag tag = parser.getTag();
-                    if (tag.getTagId() == ExifTag.TAG_ORIENTATION &&
-                            tag.hasValue()) {
-                        int orient = (int) tag.getValueAt(0);
-                        switch (orient) {
-                            case ExifTag.Orientation.TOP_LEFT:
-                                return 0;
-                            case ExifTag.Orientation.BOTTOM_LEFT:
-                                return 180;
-                            case ExifTag.Orientation.RIGHT_TOP:
-                                return 90;
-                            case ExifTag.Orientation.RIGHT_BOTTOM:
-                                return 270;
-                            default:
-                                Log.i(TAG, "Unsupported orientation");
-                                return 0;
-                        }
-                    }
-                }
-                event = parser.next();
+            exif.readExif(is);
+            Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+            if (val == null) {
+                return 0;
+            } else {
+                return ExifInterface.getRotationForOrientationValue(val.shortValue());
             }
-            Log.i(TAG, "Orientation not found");
-            return 0;
         } catch (IOException e) {
             Log.w(TAG, "Failed to read EXIF orientation", e);
             return 0;
-        } catch (ExifInvalidFormatException e) {
-            Log.w(TAG, "Failed to read EXIF orientation", e);
-            return 0;
         }
     }
 }
index d5fad54..1ed67ec 100644 (file)
@@ -23,7 +23,6 @@ import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
-import android.media.ExifInterface;
 import android.net.Uri;
 import android.os.Build;
 import android.provider.MediaStore.Images;
@@ -37,8 +36,7 @@ import com.android.gallery3d.app.StitchingProgressManager;
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.BitmapUtils;
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifModifier;
+import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.exif.ExifTag;
 import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.ThreadPool.Job;
@@ -46,6 +44,7 @@ import com.android.gallery3d.util.ThreadPool.JobContext;
 import com.android.gallery3d.util.UpdateHelper;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel.MapMode;
@@ -196,15 +195,15 @@ public class LocalImage extends LocalMediaItem {
 
             // try to decode from JPEG EXIF
             if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
-                ExifInterface exif = null;
-                byte [] thumbData = null;
+                ExifInterface exif = new ExifInterface();
+                byte[] thumbData = null;
                 try {
-                    exif = new ExifInterface(mLocalFilePath);
-                    if (exif != null) {
-                        thumbData = exif.getThumbnail();
-                    }
-                } catch (Throwable t) {
-                    Log.w(TAG, "fail to get exif thumb", t);
+                    exif.readExif(mLocalFilePath);
+                    thumbData = exif.getThumbnail();
+                } catch (FileNotFoundException e) {
+                    Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath);
+                } catch (IOException e) {
+                    Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath);
                 }
                 if (thumbData != null) {
                     Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
@@ -276,21 +275,6 @@ public class LocalImage extends LocalMediaItem {
                 new String[]{String.valueOf(id)});
     }
 
-    private static int getExifOrientation(int orientation) {
-        switch (orientation) {
-            case 0:
-                return ExifInterface.ORIENTATION_NORMAL;
-            case 90:
-                return ExifInterface.ORIENTATION_ROTATE_90;
-            case 180:
-                return ExifInterface.ORIENTATION_ROTATE_180;
-            case 270:
-                return ExifInterface.ORIENTATION_ROTATE_270;
-            default:
-                throw new AssertionError("invalid: " + orientation);
-        }
-    }
-
     @Override
     public void rotate(int degrees) {
         GalleryUtils.assertNotInRenderThread();
@@ -300,34 +284,22 @@ public class LocalImage extends LocalMediaItem {
         if (rotation < 0) rotation += 360;
 
         if (mimeType.equalsIgnoreCase("image/jpeg")) {
-            RandomAccessFile file = null;
-            try {
-                // Because most of the images contain the orientation tag, we
-                // use ExifModifier to modify the tag for better efficiency.
-                // If the tag doesn't exist, ExifInterface will be used to replace the entire
-                // header.
-                file = new RandomAccessFile(filePath, "rw");
-                ExifModifier modifier = new ExifModifier(
-                                file.getChannel().map(MapMode.READ_WRITE, 0, file.length()));
-                ExifTag tag = ExifTag.buildTag(ExifTag.TAG_ORIENTATION);
-                tag.setValue(getExifOrientation(rotation));
-                modifier.modifyTag(tag);
-                if (!modifier.commit()) {
-                    // Need to change the file size, use ExifInterface instead.
-                    ExifInterface exif = new ExifInterface(filePath);
-                    exif.setAttribute(ExifInterface.TAG_ORIENTATION,
-                            String.valueOf(getExifOrientation(rotation)));
-                    exif.saveAttributes();
-                    // We need to update the filesize as well
+            ExifInterface exifInterface = new ExifInterface();
+            ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.getOrientationValueForRotation(rotation));
+            if(tag != null) {
+                exifInterface.setTag(tag);
+                try {
+                    exifInterface.forceRewriteExif(filePath);
                     fileSize = new File(filePath).length();
                     values.put(Images.Media.SIZE, fileSize);
+                } catch (FileNotFoundException e) {
+                    Log.w(TAG, "cannot find file to set exif: " + filePath);
+                } catch (IOException e) {
+                    Log.w(TAG, "cannot set exif data: " + filePath);
                 }
-            } catch (IOException e) {
-                Log.w(TAG, "cannot set exif data: " + filePath);
-            } catch (ExifInvalidFormatException e) {
-                Log.w(TAG, "cannot set exif data: " + filePath);
-            } finally {
-                Utils.closeSilently(file);
+            } else {
+                Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION);
             }
         }
 
index 662bd14..cac524b 100644 (file)
@@ -18,12 +18,12 @@ package com.android.gallery3d.data;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.exif.ExifData;
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifReader;
+import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
 
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
@@ -115,11 +115,11 @@ public class MediaDetails implements Iterable<Entry<Integer, Object>> {
             String value = null;
             int type = tag.getDataType();
             if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) {
-                value = String.valueOf(tag.getRational(0).toDouble());
+                value = String.valueOf(tag.getValueAsRational(0).toDouble());
             } else if (type == ExifTag.TYPE_ASCII) {
-                value = tag.getString();
+                value = tag.getValueAsString();
             } else {
-                value = String.valueOf(tag.getValueAt(0));
+                value = String.valueOf(tag.forceGetValueAsLong(0));
             }
             if (key == MediaDetails.INDEX_FLASH) {
                 MediaDetails.FlashState state = new MediaDetails.FlashState(
@@ -132,37 +132,39 @@ public class MediaDetails implements Iterable<Entry<Integer, Object>> {
     }
 
     public static void extractExifInfo(MediaDetails details, String filePath) {
-        InputStream is = null;
+
+        ExifInterface exif = new ExifInterface();
         try {
-            is = new FileInputStream(filePath);
-            ExifData data = new ExifReader().read(is);
-            setExifData(details, data.getTag(ExifTag.TAG_FLASH), MediaDetails.INDEX_FLASH);
-            setExifData(details, data.getTag(ExifTag.TAG_IMAGE_WIDTH), MediaDetails.INDEX_WIDTH);
-            setExifData(details, data.getTag(ExifTag.TAG_IMAGE_LENGTH), MediaDetails.INDEX_HEIGHT);
-            setExifData(details, data.getTag(ExifTag.TAG_MAKE), MediaDetails.INDEX_MAKE);
-            setExifData(details, data.getTag(ExifTag.TAG_MODEL),MediaDetails.INDEX_MODEL);
-            setExifData(details, data.getTag(ExifTag.TAG_APERTURE_VALUE),
-                    MediaDetails.INDEX_APERTURE);
-            setExifData(details, data.getTag(ExifTag.TAG_ISO_SPEED_RATINGS),
-                    MediaDetails.INDEX_ISO);
-            setExifData(details, data.getTag(ExifTag.TAG_WHITE_BALANCE),
-                    MediaDetails.INDEX_WHITE_BALANCE);
-            setExifData(details, data.getTag(ExifTag.TAG_EXPOSURE_TIME),
-                    MediaDetails.INDEX_EXPOSURE_TIME);
-            ExifTag focalTag = data.getTag(ExifTag.TAG_FOCAL_LENGTH);
-            if (focalTag != null) {
-                details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH,
-                        focalTag.getRational(0).toDouble());
-                details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
-            }
-        } catch (IOException ex) {
-            // ignore it.
-            Log.w(TAG, "", ex);
-        } catch (ExifInvalidFormatException ex) {
-         // ignore it.
-            Log.w(TAG, "", ex);
-        } finally {
-            Utils.closeSilently(is);
+            exif.readExif(filePath);
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Could not find file to read exif: " + filePath, e);
+        } catch (IOException e) {
+            Log.w(TAG, "Could not read exif from file: " + filePath, e);
+        }
+
+        setExifData(details, exif.getTag(ExifInterface.TAG_FLASH),
+                MediaDetails.INDEX_FLASH);
+        setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH),
+                MediaDetails.INDEX_WIDTH);
+        setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH),
+                MediaDetails.INDEX_HEIGHT);
+        setExifData(details, exif.getTag(ExifInterface.TAG_MAKE),
+                MediaDetails.INDEX_MAKE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_MODEL),
+                MediaDetails.INDEX_MODEL);
+        setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE),
+                MediaDetails.INDEX_APERTURE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS),
+                MediaDetails.INDEX_ISO);
+        setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE),
+                MediaDetails.INDEX_WHITE_BALANCE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME),
+                MediaDetails.INDEX_EXPOSURE_TIME);
+        ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH);
+        if (focalTag != null) {
+            details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH,
+                    focalTag.getValueAsRational(0).toDouble());
+            details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
         }
     }
 }
index 6cf4622..2c1a847 100644 (file)
@@ -27,7 +27,7 @@ import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
 import android.graphics.Matrix;
 import android.graphics.Rect;
-import android.media.ExifInterface;
+import android.graphics.Bitmap.CompressFormat;
 import android.net.Uri;
 import android.provider.MediaStore;
 import android.util.Log;
@@ -36,9 +36,8 @@ import com.adobe.xmp.XMPException;
 import com.adobe.xmp.XMPMeta;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifParser;
 import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.HistoryAdapter;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
@@ -48,6 +47,8 @@ import com.android.gallery3d.filtershow.tools.SaveCopyTask;
 import com.android.gallery3d.util.InterruptableOutputStream;
 import com.android.gallery3d.util.XmpUtilHelper;
 
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -79,14 +80,14 @@ public class ImageLoader {
     public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
     public static final int DEFAULT_COMPRESS_QUALITY = 95;
 
-    public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL;
-    public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90;
-    public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
-    public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
-    public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
-    public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL;
-    public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE;
-    public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
+    public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
+    public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
+    public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
+    public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
+    public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
+    public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
+    public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
+    public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
 
     private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
     private Context mContext = null;
@@ -147,26 +148,13 @@ public class ImageLoader {
             String path = uri.getPath();
             int orientation = -1;
             InputStream is = null;
+            ExifInterface exif = new ExifInterface();
             try {
-                is = new FileInputStream(path);
-                ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
-                int event = parser.next();
-                while (event != ExifParser.EVENT_END) {
-                    if (event == ExifParser.EVENT_NEW_TAG) {
-                        ExifTag tag = parser.getTag();
-                        if (tag.getTagId() == ExifTag.TAG_ORIENTATION) {
-                            orientation = (int) tag.getValueAt(0);
-                            break;
-                        }
-                    }
-                    event = parser.next();
-                }
+                exif.readExif(path);
+                orientation = ExifInterface.getRotationForOrientationValue(
+                        exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue());
             } catch (IOException e) {
-                e.printStackTrace();
-            } catch (ExifInvalidFormatException e) {
-                e.printStackTrace();
-            } finally {
-                Utils.closeSilently(is);
+                Log.w(LOGTAG, "Failed to read EXIF orientation", e);
             }
             return orientation;
         }
@@ -196,9 +184,9 @@ public class ImageLoader {
                 return -1;
             }
         } catch (SQLiteException e) {
-            return ExifInterface.ORIENTATION_UNDEFINED;
+            return -1;
         } catch (IllegalArgumentException e) {
-            return ExifInterface.ORIENTATION_UNDEFINED;
+            return -1;
         } finally {
             Utils.closeSilently(cursor);
         }
index b88dbbc..aa7e700 100644 (file)
@@ -21,7 +21,6 @@ import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -31,21 +30,15 @@ import android.provider.MediaStore.Images.ImageColumns;
 import android.util.Log;
 
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.exif.ExifData;
-import com.android.gallery3d.exif.ExifInvalidFormatException;
-import com.android.gallery3d.exif.ExifOutputStream;
-import com.android.gallery3d.exif.ExifReader;
-import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.util.XmpUtilHelper;
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.sql.Date;
 import java.text.SimpleDateFormat;
 import java.util.TimeZone;
@@ -55,35 +48,7 @@ import java.util.TimeZone;
  */
 public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
 
-
     private static final String LOGTAG = "SaveCopyTask";
-    /**
-     * Saves the bitmap in the final destination
-     */
-    public static void saveBitmap(Bitmap bitmap, File destination, Object xmp) {
-        saveBitmap(bitmap, destination, xmp, null);
-    }
-
-    private static void saveBitmap(Bitmap bitmap, File destination, Object xmp, ExifData exif) {
-        OutputStream os = null;
-        try {
-            os = new FileOutputStream(destination);
-            if (exif != null) {
-                ExifOutputStream eos = new ExifOutputStream(os);
-                eos.setExifData(exif);
-                bitmap.compress(CompressFormat.JPEG, ImageLoader.DEFAULT_COMPRESS_QUALITY, eos);
-            } else {
-                bitmap.compress(CompressFormat.JPEG, ImageLoader.DEFAULT_COMPRESS_QUALITY, os);
-            }
-        } catch (FileNotFoundException e) {
-            Log.v(LOGTAG,"Error in writing "+destination.getAbsolutePath());
-        } finally {
-            Utils.closeSilently(os);;
-        }
-        if (xmp != null) {
-            XmpUtilHelper.writeXMPMeta(destination.getAbsolutePath(), xmp);
-        }
-    }
 
     /**
      * Callback for the completed asynchronous task.
@@ -128,7 +93,8 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
                     ImageLoader.DEFAULT_SAVE_DIRECTORY);
         }
         // Create the directory if it doesn't exist
-        if (!saveDirectory.exists()) saveDirectory.mkdirs();
+        if (!saveDirectory.exists())
+            saveDirectory.mkdirs();
         return saveDirectory;
     }
 
@@ -139,28 +105,59 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
         return new File(saveDirectory, filename + ".JPG");
     }
 
-    private ExifData getExifData(Uri sourceUri) {
+    public Object getPanoramaXMPData(Uri source, ImagePreset preset) {
+        Object xmp = null;
+        if (preset.isPanoramaSafe()) {
+            InputStream is = null;
+            try {
+                is = context.getContentResolver().openInputStream(source);
+                xmp = XmpUtilHelper.extractXMPMeta(is);
+            } catch (FileNotFoundException e) {
+                Log.w(LOGTAG, "Failed to get XMP data from image: ", e);
+            } finally {
+                Utils.closeSilently(is);
+            }
+        }
+        return xmp;
+    }
+
+    public boolean putPanoramaXMPData(File file, Object xmp) {
+        if (xmp != null) {
+            return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp);
+        }
+        return false;
+    }
+
+    public ExifInterface getExifData(Uri source) {
+        ExifInterface exif = new ExifInterface();
         String mimeType = context.getContentResolver().getType(sourceUri);
-        if (mimeType != ImageLoader.JPEG_MIME_TYPE) {
-            return null;
+        if (mimeType == ImageLoader.JPEG_MIME_TYPE) {
+            InputStream inStream = null;
+            try {
+                inStream = context.getContentResolver().openInputStream(source);
+                exif.readExif(inStream);
+            } catch (FileNotFoundException e) {
+                Log.w(LOGTAG, "Cannot find file: " + source, e);
+            } catch (IOException e) {
+                Log.w(LOGTAG, "Cannot read exif for: " + source, e);
+            } finally {
+                Utils.closeSilently(inStream);
+            }
         }
-        InputStream is = null;
+        return exif;
+    }
+
+    public boolean putExifData(File file, ExifInterface exif, Bitmap image) {
+        boolean ret = false;
         try {
-            is = context.getContentResolver().openInputStream(sourceUri);
-            ExifReader reader = new ExifReader();
-            return reader.read(is);
+            exif.writeExif(image, file.getAbsolutePath());
+            ret = true;
         } catch (FileNotFoundException e) {
-            Log.w(LOGTAG, "Failed to find file", e);
-            return null;
-        } catch (ExifInvalidFormatException e) {
-            Log.w(LOGTAG, "Invalid EXIF data", e);
-            return null;
+            Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e);
         } catch (IOException e) {
-            Log.w(LOGTAG, "Failed to read original file", e);
-            return null;
-        } finally {
-            Utils.closeSilently(is);
+            Log.w(LOGTAG, "Could not write exif: ", e);
         }
+        return ret;
     }
 
     /**
@@ -173,12 +170,12 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
             return null;
         }
         ImagePreset preset = params[0];
-        InputStream is = null;
         BitmapFactory.Options options = new BitmapFactory.Options();
+        Uri uri = null;
         boolean noBitmap = true;
         int num_tries = 0;
         // Stopgap fix for low-memory devices.
-        while(noBitmap) {
+        while (noBitmap) {
             try {
                 // Try to do bitmap operations, downsample if low-memory
                 Bitmap bitmap = ImageLoader.loadMutableBitmap(context, sourceUri, options);
@@ -189,24 +186,23 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
                 bitmap = preset.applyGeometry(bitmap);
                 bitmap = preset.apply(bitmap);
 
-                Object xmp = null;
-                if (preset.isPanoramaSafe()) {
-                    is = context.getContentResolver().openInputStream(sourceUri);
-                    xmp =  XmpUtilHelper.extractXMPMeta(is);
-                }
-                ExifData exif = getExifData(sourceUri);
-                if (exif != null) {
-                    exif.addDateTimeStampTag(ExifTag.TAG_DATE_TIME, System.currentTimeMillis(),
-                            TimeZone.getDefault());
-                    // Since the image has been modified, set the orientation to normal.
-                    exif.addTag(ExifTag.TAG_ORIENTATION).setValue(ExifTag.Orientation.TOP_LEFT);
+                Object xmp = getPanoramaXMPData(sourceUri, preset);
+                ExifInterface exif = getExifData(sourceUri);
+
+                // Set tags
+                long time = System.currentTimeMillis();
+                exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time,
+                        TimeZone.getDefault());
+                exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
+                        ExifInterface.Orientation.TOP_LEFT));
+
+                // If we succeed in writing the bitmap as a jpeg, return a uri.
+                if (putExifData(this.destinationFile, exif, bitmap)) {
+                    putPanoramaXMPData(this.destinationFile, xmp);
+                    uri = insertContent(context, sourceUri, this.destinationFile, saveFileName,
+                            time);
                 }
-                saveBitmap(bitmap, this.destinationFile, xmp, exif);
-                bitmap.recycle();
                 noBitmap = false;
-            } catch (FileNotFoundException ex) {
-                Log.w(LOGTAG, "Failed to save image!", ex);
-                return null;
             } catch (java.lang.OutOfMemoryError e) {
                 // Try 5 times before failing for good.
                 if (++num_tries >= 5) {
@@ -214,13 +210,9 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
                 }
                 System.gc();
                 options.inSampleSize *= 2;
-            } finally {
-                Utils.closeSilently(is);
             }
         }
-        Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName);
         return uri;
-
     }
 
     @Override
@@ -267,16 +259,17 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
     /**
      * Insert the content (saved file) with proper source photo properties.
      */
-    public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName) {
-        long now = System.currentTimeMillis() / 1000;
+    public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName,
+            long time) {
+        time /= 1000;
 
         final ContentValues values = new ContentValues();
         values.put(Images.Media.TITLE, saveFileName);
         values.put(Images.Media.DISPLAY_NAME, file.getName());
         values.put(Images.Media.MIME_TYPE, "image/jpeg");
-        values.put(Images.Media.DATE_TAKEN, now);
-        values.put(Images.Media.DATE_MODIFIED, now);
-        values.put(Images.Media.DATE_ADDED, now);
+        values.put(Images.Media.DATE_TAKEN, time);
+        values.put(Images.Media.DATE_MODIFIED, time);
+        values.put(Images.Media.DATE_ADDED, time);
         values.put(Images.Media.ORIENTATION, 0);
         values.put(Images.Media.DATA, file.getAbsolutePath());
         values.put(Images.Media.SIZE, file.length());
@@ -288,20 +281,20 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
         querySource(context, sourceUri, projection,
                 new ContentResolverQueryCallback() {
 
-            @Override
-            public void onCursorResult(Cursor cursor) {
-                values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
-
-                double latitude = cursor.getDouble(1);
-                double longitude = cursor.getDouble(2);
-                // TODO: Change || to && after the default location issue is
-                // fixed.
-                if ((latitude != 0f) || (longitude != 0f)) {
-                    values.put(Images.Media.LATITUDE, latitude);
-                    values.put(Images.Media.LONGITUDE, longitude);
-                }
-            }
-        });
+                    @Override
+                    public void onCursorResult(Cursor cursor) {
+                        values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
+
+                        double latitude = cursor.getDouble(1);
+                        double longitude = cursor.getDouble(2);
+                        // TODO: Change || to && after the default location
+                        // issue is fixed.
+                        if ((latitude != 0f) || (longitude != 0f)) {
+                            values.put(Images.Media.LATITUDE, latitude);
+                            values.put(Images.Media.LONGITUDE, longitude);
+                        }
+                    }
+                });
 
         return context.getContentResolver().insert(
                 Images.Media.EXTERNAL_CONTENT_URI, values);
index 702b3b5..de457bf 100644 (file)
@@ -28,7 +28,6 @@ import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.MediaSource;
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.data.PathMatcher;
-import com.android.gallery3d.exif.ExifData;
 
 import java.io.FileNotFoundException;
 
@@ -147,8 +146,6 @@ public class PicasaSource extends MediaSource {
 
     public static void onPackageChanged(Context context, String packageName) {/*do nothing*/}
 
-    public static void extractExifValues(MediaObject item, ExifData exif) {/*do nothing*/}
-
     public static Dialog getVersionCheckDialog(Activity activity){
         return null;
     }
index fed8e1e..949f22c 100644 (file)
@@ -19,36 +19,135 @@ package com.android.gallery3d.exif;
 import junit.framework.TestCase;
 
 import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 public class ExifDataTest extends TestCase {
+    Map<Integer, ExifTag> mTestTags;
+    ExifInterface mInterface;
+    private ExifTag mVersionTag;
+    private ExifTag mGpsVersionTag;
+    private ExifTag mModelTag;
+    private ExifTag mDateTimeTag;
+    private ExifTag mCompressionTag;
+    private ExifTag mThumbnailFormatTag;
+    private ExifTag mLongitudeTag;
+    private ExifTag mShutterTag;
+    private ExifTag mInteropIndex;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mInterface = new ExifInterface();
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                5, 4, 3, 2
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                6, 7, 8, 9
+        });
+        // TYPE ASCII with arbitrary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "helloworld");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2013:02:11 20:20:20");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(2, 2), new Rational(11, 11),
+                new Rational(102, 102)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(4, 6));
+        // TYPE_ASCII with arbitrary length
+        mInteropIndex = mInterface.buildTag(ExifInterface.TAG_INTEROPERABILITY_INDEX, "foo");
+
+        mTestTags = new HashMap<Integer, ExifTag>();
+
+        mTestTags.put(ExifInterface.TAG_EXIF_VERSION, mVersionTag);
+        mTestTags.put(ExifInterface.TAG_GPS_VERSION_ID, mGpsVersionTag);
+        mTestTags.put(ExifInterface.TAG_MODEL, mModelTag);
+        mTestTags.put(ExifInterface.TAG_DATE_TIME, mDateTimeTag);
+        mTestTags.put(ExifInterface.TAG_COMPRESSION, mCompressionTag);
+        mTestTags.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, mThumbnailFormatTag);
+        mTestTags.put(ExifInterface.TAG_GPS_LONGITUDE, mLongitudeTag);
+        mTestTags.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, mShutterTag);
+        mTestTags.put(ExifInterface.TAG_INTEROPERABILITY_INDEX, mInteropIndex);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mInterface = null;
+        mTestTags = null;
+    }
+
     public void testAddTag() {
         ExifData exifData = new ExifData(ByteOrder.BIG_ENDIAN);
-        // IFD0 tag
-        exifData.addTag(ExifTag.TAG_MAKE).setValue("test");
-        exifData.addTag(ExifTag.TAG_IMAGE_WIDTH).setValue(1000);
-
-        // EXIF tag
-        exifData.addTag(ExifTag.TAG_ISO_SPEED_RATINGS).setValue(1);
-
-        // GPS tag
-        exifData.addTag(ExifTag.TAG_GPS_ALTITUDE).setValue(new Rational(10, 100));
-
-        // Interoperability tag
-        exifData.addInteroperabilityTag(ExifTag.TAG_INTEROPERABILITY_INDEX).setValue("inter_test");
-
-        // IFD1 tag
-        exifData.addThumbnailTag(ExifTag.TAG_MAKE).setValue("test_thumb");
-        exifData.addThumbnailTag(ExifTag.TAG_IMAGE_WIDTH).setValue(100);
-
-        // check data
-        assertEquals("test", exifData.getTag(ExifTag.TAG_MAKE).getString());
-        assertEquals(1000, (int) exifData.getTag(ExifTag.TAG_IMAGE_WIDTH).getValueAt(0));
-        assertEquals(1, (int) exifData.getTag(ExifTag.TAG_ISO_SPEED_RATINGS).getValueAt(0));
-        assertEquals(new Rational(10, 100),
-                exifData.getTag(ExifTag.TAG_GPS_ALTITUDE).getRational(0));
-        assertEquals("inter_test",
-                exifData.getInteroperabilityTag(ExifTag.TAG_INTEROPERABILITY_INDEX).getString());
-        assertEquals("test_thumb", exifData.getThumbnailTag(ExifTag.TAG_MAKE).getString());
-        assertEquals(100, (int) exifData.getThumbnailTag(ExifTag.TAG_IMAGE_WIDTH).getValueAt(0));
+
+        // Add all test tags
+        for (ExifTag t : mTestTags.values()) {
+            assertTrue(exifData.addTag(t) == null);
+        }
+
+        // Make sure no initial thumbnails
+        assertFalse(exifData.hasCompressedThumbnail());
+        assertFalse(exifData.hasUncompressedStrip());
+
+        // Check that we can set thumbnails
+        exifData.setStripBytes(3, new byte[] {
+                1, 2, 3, 4, 5
+        });
+        assertTrue(exifData.hasUncompressedStrip());
+        exifData.setCompressedThumbnail(new byte[] {
+            1
+        });
+        assertTrue(exifData.hasCompressedThumbnail());
+
+        // Check that we can clear thumbnails
+        exifData.clearThumbnailAndStrips();
+        assertFalse(exifData.hasCompressedThumbnail());
+        assertFalse(exifData.hasUncompressedStrip());
+
+        // Make sure ifds exist
+        for (int i : IfdData.getIfds()) {
+            assertTrue(exifData.getIfdData(i) != null);
+        }
+
+        // Get all test tags
+        List<ExifTag> allTags = exifData.getAllTags();
+        assertTrue(allTags != null);
+
+        // Make sure all test tags are in data
+        for (ExifTag t : mTestTags.values()) {
+            boolean check = false;
+            for (ExifTag i : allTags) {
+                if (t.equals(i)) {
+                    check = true;
+                    break;
+                }
+            }
+            assertTrue(check);
+        }
+
+        // Check if getting tags for a tid works
+        List<ExifTag> tidTags = exifData.getAllTagsForTagId(ExifInterface
+                .getTrueTagKey(ExifInterface.TAG_SHUTTER_SPEED_VALUE));
+        assertTrue(tidTags.size() == 1);
+        assertTrue(tidTags.get(0).equals(mShutterTag));
+
+        // Check if getting tags for an ifd works
+        List<ExifTag> ifdTags = exifData.getAllTagsForIfd(IfdId.TYPE_IFD_INTEROPERABILITY);
+        assertTrue(ifdTags.size() == 1);
+        assertTrue(ifdTags.get(0).equals(mInteropIndex));
+
     }
 }
diff --git a/tests/src/com/android/gallery3d/exif/ExifInterfaceTest.java b/tests/src/com/android/gallery3d/exif/ExifInterfaceTest.java
new file mode 100644 (file)
index 0000000..af1ccfb
--- /dev/null
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.exif;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import java.io.ByteArrayInputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ExifInterfaceTest extends ExifXmlDataTestCase {
+
+    private File mTmpFile;
+    private List<Map<Short, List<String>>> mGroundTruth;
+    private ExifInterface mInterface;
+    private ExifTag mVersionTag;
+    private ExifTag mGpsVersionTag;
+    private ExifTag mModelTag;
+    private ExifTag mDateTimeTag;
+    private ExifTag mCompressionTag;
+    private ExifTag mThumbnailFormatTag;
+    private ExifTag mLongitudeTag;
+    private ExifTag mShutterTag;
+    Map<Integer, ExifTag> mTestTags;
+    Map<Integer, Integer> mTagDefinitions;
+
+    public ExifInterfaceTest(int imageRes, int xmlRes) {
+        super(imageRes, xmlRes);
+    }
+
+    public ExifInterfaceTest(String imagePath, String xmlPath) {
+        super(imagePath, xmlPath);
+    }
+
+    public void testInterface() throws Exception {
+
+        InputStream imageInputStream = null;
+        try {
+            // Basic checks
+
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // Check defines
+            int tag = ExifInterface.defineTag(1, (short) 0x0100);
+            assertTrue(getImageTitle(), tag == 0x00010100);
+            int tagDef = mInterface.getTagDefinition((short) 0x0100, IfdId.TYPE_IFD_0);
+            assertTrue(getImageTitle(), tagDef == 0x03040001);
+            int[] allowed = ExifInterface.getAllowedIfdsFromInfo(mInterface.getTagInfo().get(
+                    ExifInterface.TAG_IMAGE_WIDTH));
+            assertTrue(getImageTitle(), allowed.length == 2 && allowed[0] == IfdId.TYPE_IFD_0
+                    && allowed[1] == IfdId.TYPE_IFD_1);
+
+            // Check if there are any initial tags
+            assertTrue(getImageTitle(), mInterface.getAllTags() == null);
+
+            // ///////// Basic read/write testing
+
+            // Make sure we can read
+            imageInputStream = new ByteArrayInputStream(imgData);
+            mInterface.readExif(imageInputStream);
+
+            // Check tags against ground truth
+            checkTagsAgainstXml(mInterface.getAllTags());
+
+            // Make sure clearing Exif works
+            mInterface.clearExif();
+            assertTrue(getImageTitle(), mInterface.getAllTags() == null);
+
+            // Make sure setting tags works
+            mInterface.setTags(mTestTags.values());
+            checkTagsAgainstHash(mInterface.getAllTags(), mTestTags);
+
+            // Try writing over bitmap exif
+            ByteArrayOutputStream imgModified = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData, imgModified);
+
+            // Check if bitmap is valid
+            byte[] imgData2 = imgModified.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+            // Make sure we get the same tags out
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            mInterface.readExif(imageInputStream);
+            checkTagsAgainstHash(mInterface.getAllTags(), mTestTags);
+
+            // Reread original image
+            imageInputStream = new ByteArrayInputStream(imgData);
+            mInterface.readExif(imageInputStream);
+
+            // Write out with original exif
+            imgModified = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData2, imgModified);
+
+            // Read back in exif and check tags
+            imgData2 = imgModified.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            mInterface.readExif(imageInputStream);
+            checkTagsAgainstXml(mInterface.getAllTags());
+
+            // Check if bitmap is valid
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    public void testInterfaceModify() throws Exception {
+
+        // TODO: This test is dependent on galaxy_nexus jpeg/xml file.
+        InputStream imageInputStream = null;
+        try {
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // ///////// Exif modifier testing.
+
+            // Read exif and write to temp file
+            imageInputStream = new ByteArrayInputStream(imgData);
+            mInterface.readExif(imageInputStream);
+            mInterface.writeExif(imgData, mTmpFile.getPath());
+
+            // Check if bitmap is valid
+            imageInputStream = new FileInputStream(mTmpFile);
+            checkBitmap(imageInputStream);
+
+            // Create some tags to overwrite with
+            ArrayList<ExifTag> tags = new ArrayList<ExifTag>();
+            tags.add(mInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.Orientation.RIGHT_TOP));
+            tags.add(mInterface.buildTag(ExifInterface.TAG_USER_COMMENT, "goooooooooooooooooogle"));
+
+            // Attempt to rewrite tags
+            assertTrue(getImageTitle(), mInterface.rewriteExif(mTmpFile.getPath(), tags));
+
+            imageInputStream.close();
+            // Check if bitmap is valid
+            imageInputStream = new FileInputStream(mTmpFile);
+            checkBitmap(imageInputStream);
+
+            // Read tags and check against xml
+            mInterface.readExif(mTmpFile.getPath());
+            for (ExifTag t : mInterface.getAllTags()) {
+                short tid = t.getTagId();
+                if (tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_ORIENTATION)
+                        && tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT)) {
+                    checkTagAgainstXml(t);
+                }
+            }
+            assertTrue(getImageTitle(), mInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION)
+                    .shortValue() == ExifInterface.Orientation.RIGHT_TOP);
+            String valString = mInterface.getTagStringValue(ExifInterface.TAG_USER_COMMENT);
+            assertTrue(getImageTitle(), valString.equals("goooooooooooooooooogle"));
+
+            // Test forced modify
+
+            // Create some tags to overwrite with
+            tags = new ArrayList<ExifTag>();
+            tags.add(mInterface.buildTag(ExifInterface.TAG_SOFTWARE, "magic super photomaker pro"));
+            tags.add(mInterface.buildTag(ExifInterface.TAG_USER_COMMENT, "noodles"));
+            tags.add(mInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.Orientation.TOP_LEFT));
+
+            // Force rewrite tags
+            mInterface.forceRewriteExif(mTmpFile.getPath(), tags);
+
+            imageInputStream.close();
+            // Check if bitmap is valid
+            imageInputStream = new FileInputStream(mTmpFile);
+            checkBitmap(imageInputStream);
+
+            // Read tags and check against xml
+            mInterface.readExif(mTmpFile.getPath());
+            for (ExifTag t : mInterface.getAllTags()) {
+                short tid = t.getTagId();
+                if (!ExifInterface.isOffsetTag(tid)
+                        && tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_SOFTWARE)
+                        && tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT)) {
+                    checkTagAgainstXml(t);
+                }
+            }
+            valString = mInterface.getTagStringValue(ExifInterface.TAG_SOFTWARE);
+            String compareString = "magic super photomaker pro\0";
+            assertTrue(getImageTitle(), valString.equals(compareString));
+            valString = mInterface.getTagStringValue(ExifInterface.TAG_USER_COMMENT);
+            assertTrue(getImageTitle(), valString.equals("noodles"));
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    public void testInterfaceDefines() throws Exception {
+
+        InputStream imageInputStream = null;
+        try {
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // Set some tags.
+            mInterface.setTags(mTestTags.values());
+
+            // Check tag definitions against default
+            for (Integer i : mTestTags.keySet()) {
+                int check = mTagDefinitions.get(i).intValue();
+                int actual = mInterface.getTagInfo().get(i);
+                assertTrue(check == actual);
+            }
+
+            // Check defines
+            int tag1 = ExifInterface.defineTag(IfdId.TYPE_IFD_1, (short) 42);
+            int tag2 = ExifInterface.defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 43);
+            assertTrue(tag1 == 0x0001002a);
+            assertTrue(tag2 == 0x0003002b);
+
+            // Define some non-standard tags
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1,
+                    ExifTag.TYPE_UNSIGNED_BYTE, (short) 16, new int[] {
+                        IfdId.TYPE_IFD_1
+                    }) == tag1);
+            assertTrue(mInterface.getTagInfo().get(tag1) == 0x02010010);
+            assertTrue(mInterface.setTagDefinition((short) 43, IfdId.TYPE_IFD_INTEROPERABILITY,
+                    ExifTag.TYPE_ASCII, (short) 5, new int[] {
+                            IfdId.TYPE_IFD_GPS, IfdId.TYPE_IFD_INTEROPERABILITY
+                    }) == tag2);
+            assertTrue(mInterface.getTagInfo().get(tag2) == 0x18020005);
+
+            // Make sure these don't work
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1,
+                    ExifTag.TYPE_UNSIGNED_BYTE, (short) 16, new int[] {
+                        IfdId.TYPE_IFD_0
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1, (short) 0,
+                    (short) 16, new int[] {
+                        IfdId.TYPE_IFD_1
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 42, 5, ExifTag.TYPE_UNSIGNED_BYTE,
+                    (short) 16, new int[] {
+                        5
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1,
+                    ExifTag.TYPE_UNSIGNED_BYTE, (short) 16, new int[] {
+                        -1
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 43, IfdId.TYPE_IFD_GPS,
+                    ExifTag.TYPE_ASCII, (short) 5, new int[] {
+                        IfdId.TYPE_IFD_GPS
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 43, IfdId.TYPE_IFD_0,
+                    ExifTag.TYPE_ASCII, (short) 5, new int[] {
+                            IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_GPS
+                    }) == ExifInterface.TAG_NULL);
+
+            // Set some tags
+            mInterface.setTags(mTestTags.values());
+            checkTagsAgainstHash(mInterface.getAllTags(), mTestTags);
+
+            // Make some tags using new defines
+            ExifTag defTag0 = mInterface.buildTag(tag1, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            });
+            assertTrue(defTag0 != null);
+            ExifTag defTag1 = mInterface.buildTag(tag2, "hihi");
+            assertTrue(defTag1 != null);
+            ExifTag defTag2 = mInterface.buildTag(tag2, IfdId.TYPE_IFD_GPS, "byte");
+            assertTrue(defTag2 != null);
+
+            // Make sure these don't work
+            ExifTag badTag = mInterface.buildTag(tag1, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
+            });
+            assertTrue(badTag == null);
+            badTag = mInterface.buildTag(tag1, IfdId.TYPE_IFD_0, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            });
+            assertTrue(badTag == null);
+            badTag = mInterface.buildTag(0x0002002a, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            });
+            assertTrue(badTag == null);
+            badTag = mInterface.buildTag(tag2, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17
+            });
+            assertTrue(badTag == null);
+
+            // Set the tags
+            assertTrue(mInterface.setTag(defTag0) == null);
+            assertTrue(mInterface.setTag(defTag1) == null);
+            assertTrue(mInterface.setTag(defTag2) == null);
+            assertTrue(mInterface.setTag(defTag0).equals(defTag0));
+            assertTrue(mInterface.setTag(null) == null);
+            assertTrue(mInterface.setTagValue(tag2, "yoyo") == true);
+            assertTrue(mInterface.setTagValue(tag2, "yaaarggg") == false);
+            assertTrue(mInterface.getTagStringValue(tag2).equals("yoyo\0"));
+
+            // Try writing over bitmap exif
+            ByteArrayOutputStream imgModified = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData, imgModified);
+
+            // Check if bitmap is valid
+            byte[] imgData2 = imgModified.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+            // Read back in the tags
+            mInterface.readExif(imgData2);
+
+            // Check tags
+            for (ExifTag t : mInterface.getAllTags()) {
+                int tid = t.getTagId();
+                if (tid != ExifInterface.getTrueTagKey(tag1)
+                        && tid != ExifInterface.getTrueTagKey(tag2)) {
+                    checkTagAgainstHash(t, mTestTags);
+                }
+            }
+            assertTrue(Arrays.equals(mInterface.getTagByteValues(tag1), new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            }));
+            assertTrue(mInterface.getTagStringValue(tag2).equals("yoyo\0"));
+            assertTrue(mInterface.getTagStringValue(tag2, IfdId.TYPE_IFD_GPS).equals("byte\0"));
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    public void testInterfaceThumbnails() throws Exception {
+
+        InputStream imageInputStream = null;
+        try {
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // Check thumbnails
+            mInterface.readExif(imgData);
+            Bitmap bmap = mInterface.getThumbnailBitmap();
+            assertTrue(getImageTitle(), bmap != null);
+
+            // Make a new thumbnail and set it
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inSampleSize = 16;
+            Bitmap thumb = BitmapFactory.decodeByteArray(imgData, 0, imgData.length, opts);
+            assertTrue(getImageTitle(), thumb != null);
+            assertTrue(getImageTitle(), mInterface.setCompressedThumbnail(thumb) == true);
+
+            // Write out image
+            ByteArrayOutputStream outData = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData, outData);
+
+            // Make sure bitmap is still valid
+            byte[] imgData2 = outData.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+            // Read in bitmap and make sure thumbnail is still valid
+            mInterface.readExif(imgData2);
+            bmap = mInterface.getThumbnailBitmap();
+            assertTrue(getImageTitle(), bmap != null);
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mTmpFile = File.createTempFile("exif_test", ".jpg");
+        mGroundTruth = ExifXmlReader.readXml(getXmlParser());
+
+        mInterface = new ExifInterface();
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                5, 4, 3, 2
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                6, 7, 8, 9
+        });
+        // TYPE ASCII with arbitary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "helloworld");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2013:02:11 20:20:20");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(2, 2), new Rational(11, 11),
+                new Rational(102, 102)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(4, 6));
+
+        mTestTags = new HashMap<Integer, ExifTag>();
+
+        mTestTags.put(ExifInterface.TAG_EXIF_VERSION, mVersionTag);
+        mTestTags.put(ExifInterface.TAG_GPS_VERSION_ID, mGpsVersionTag);
+        mTestTags.put(ExifInterface.TAG_MODEL, mModelTag);
+        mTestTags.put(ExifInterface.TAG_DATE_TIME, mDateTimeTag);
+        mTestTags.put(ExifInterface.TAG_COMPRESSION, mCompressionTag);
+        mTestTags.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, mThumbnailFormatTag);
+        mTestTags.put(ExifInterface.TAG_GPS_LONGITUDE, mLongitudeTag);
+        mTestTags.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, mShutterTag);
+
+        mTagDefinitions = new HashMap<Integer, Integer>();
+        mTagDefinitions.put(ExifInterface.TAG_EXIF_VERSION, 0x04070004);
+        mTagDefinitions.put(ExifInterface.TAG_GPS_VERSION_ID, 0x10010004);
+        mTagDefinitions.put(ExifInterface.TAG_MODEL, 0x03020000);
+        mTagDefinitions.put(ExifInterface.TAG_DATE_TIME, 0x03020014);
+        mTagDefinitions.put(ExifInterface.TAG_COMPRESSION, 0x03030001);
+        mTagDefinitions.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 0x02040001);
+        mTagDefinitions.put(ExifInterface.TAG_GPS_LONGITUDE, 0x100a0003);
+        mTagDefinitions.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, 0x040a0001);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mTmpFile.delete();
+    }
+
+    // Helper functions
+
+    private void checkTagAgainstXml(ExifTag tag) {
+        List<String> truth = mGroundTruth.get(tag.getIfd()).get(tag.getTagId());
+
+        if (truth == null) {
+            fail(String.format("Unknown Tag %02x", tag.getTagId()) + ", " + getImageTitle());
+        }
+
+        // No value from exiftool.
+        if (truth.contains(null))
+            return;
+
+        String dataString = Util.tagValueToString(tag).trim();
+        assertTrue(String.format("Tag %02x", tag.getTagId()) + ", " + getImageTitle()
+                + ": " + dataString,
+                truth.contains(dataString));
+    }
+
+    private void checkTagsAgainstXml(List<ExifTag> tags) {
+        for (ExifTag t : tags) {
+            checkTagAgainstXml(t);
+        }
+    }
+
+    private void checkTagAgainstHash(ExifTag tag, Map<Integer, ExifTag> testTags) {
+        int tagdef = mInterface.getTagDefinitionForTag(tag);
+        assertTrue(getImageTitle(), tagdef != ExifInterface.TAG_NULL);
+        ExifTag t = testTags.get(tagdef);
+        // Ignore offset tags & other special tags
+        if (!ExifInterface.sBannedDefines.contains(tag.getTagId())) {
+            assertTrue(getImageTitle(), t != null);
+        } else {
+            return;
+        }
+        if (t == tag)
+            return;
+        assertTrue(getImageTitle(), tag.equals(t));
+        assertTrue(getImageTitle(), tag.getDataType() == t.getDataType());
+        assertTrue(getImageTitle(), tag.getTagId() == t.getTagId());
+        assertTrue(getImageTitle(), tag.getIfd() == t.getIfd());
+        assertTrue(getImageTitle(), tag.getComponentCount() == t.getComponentCount());
+    }
+
+    private void checkTagsAgainstHash(List<ExifTag> tags, Map<Integer, ExifTag> testTags) {
+        for (ExifTag t : tags) {
+            checkTagAgainstHash(t, testTags);
+        }
+    }
+
+    private void checkBitmap(InputStream inputStream) throws IOException {
+        Bitmap bmp = BitmapFactory.decodeStream(inputStream);
+        assertTrue(getImageTitle(), bmp != null);
+    }
+
+}
index 14e956f..713a9d9 100644 (file)
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.gallery3d.exif;
 
 import java.io.File;
@@ -30,47 +31,16 @@ public class ExifModifierTest extends ExifXmlDataTestCase {
 
     private File mTmpFile;
     private List<Map<Short, List<String>>> mGroundTruth;
-
-    // TYPE_UNDEFINED with 4 components
-    private static final ExifTag sVersionTag = ExifTag.buildTag(ExifTag.TAG_EXIF_VERSION);
-    // TYPE_UNSIGNED_BYTE with 4 components
-    private static final ExifTag sGpsVersionTag = ExifTag.buildTag(ExifTag.TAG_GPS_VERSION_ID);
-    // TYPE ASCII with arbitary length
-    private static final ExifTag sModelTag = ExifTag.buildTag(ExifTag.TAG_MODEL);
-    // TYPE_ASCII with 20 components
-    private static final ExifTag sDateTimeTag = ExifTag.buildTag(ExifTag.TAG_DATE_TIME);
-    // TYPE_UNSIGNED_SHORT with 1 components
-    private static final ExifTag sCompressionTag = ExifTag.buildTag(ExifTag.TAG_COMPRESSION);
-    // TYPE_UNSIGNED_LONG with 1 components
-    private static final ExifTag sThumbnailFormatTag =
-            ExifTag.buildTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT);
-    // TYPE_UNSIGNED_RATIONAL with 3 components
-    private static final ExifTag sLongitudeTag = ExifTag.buildTag(ExifTag.TAG_GPS_LONGITUDE);
-    // TYPE_RATIONAL with 1 components
-    private static final ExifTag sShutterTag = ExifTag.buildTag(ExifTag.TAG_SHUTTER_SPEED_VALUE);
-
-    private static final Map<Short, ExifTag> sTestTags = new HashMap<Short, ExifTag>();
-
-    static {
-        sVersionTag.setValue(new byte[] {1, 2, 3, 4});
-        sGpsVersionTag.setValue(new byte[] {4, 3, 2, 1});
-        sModelTag.setValue("end-of-the-world");
-        sDateTimeTag.setValue("2012:12:31 23:59:59");
-        sCompressionTag.setValue(100);
-        sThumbnailFormatTag.setValue(100);
-        sLongitudeTag.setValue(new Rational[] {new Rational(1, 1), new Rational(10, 10),
-                new Rational(100, 100)});
-        sShutterTag.setValue(new Rational(1, 1));
-
-        sTestTags.put(sVersionTag.getTagId(), sVersionTag);
-        sTestTags.put(sGpsVersionTag.getTagId(), sGpsVersionTag);
-        sTestTags.put(sModelTag.getTagId(), sModelTag);
-        sTestTags.put(sDateTimeTag.getTagId(), sDateTimeTag);
-        sTestTags.put(sCompressionTag.getTagId(), sCompressionTag);
-        sTestTags.put(sThumbnailFormatTag.getTagId(), sThumbnailFormatTag);
-        sTestTags.put(sLongitudeTag.getTagId(), sLongitudeTag);
-        sTestTags.put(sShutterTag.getTagId(), sShutterTag);
-    }
+    private ExifInterface mInterface;
+    private Map<Short, ExifTag> mTestTags;
+    ExifTag mVersionTag;
+    ExifTag mGpsVersionTag;
+    ExifTag mModelTag;
+    ExifTag mDateTimeTag;
+    ExifTag mCompressionTag;
+    ExifTag mThumbnailFormatTag;
+    ExifTag mLongitudeTag;
+    ExifTag mShutterTag;
 
     @Override
     public void setUp() throws Exception {
@@ -89,14 +59,53 @@ public class ExifModifierTest extends ExifXmlDataTestCase {
         } finally {
             Util.closeSilently(os);
         }
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                1, 2, 3, 4
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                4, 3, 2, 1
+        });
+        // TYPE ASCII with arbitary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "end-of-the-world");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2012:12:31 23:59:59");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(1, 1), new Rational(10, 10),
+                new Rational(100, 100)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(1, 1));
+
+        mTestTags = new HashMap<Short, ExifTag>();
+
+        mTestTags.put(mVersionTag.getTagId(), mVersionTag);
+        mTestTags.put(mGpsVersionTag.getTagId(), mGpsVersionTag);
+        mTestTags.put(mModelTag.getTagId(), mModelTag);
+        mTestTags.put(mDateTimeTag.getTagId(), mDateTimeTag);
+        mTestTags.put(mCompressionTag.getTagId(), mCompressionTag);
+        mTestTags.put(mThumbnailFormatTag.getTagId(), mThumbnailFormatTag);
+        mTestTags.put(mLongitudeTag.getTagId(), mLongitudeTag);
+        mTestTags.put(mShutterTag.getTagId(), mShutterTag);
     }
 
     public ExifModifierTest(int imageRes, int xmlRes) {
         super(imageRes, xmlRes);
+        mInterface = new ExifInterface();
     }
 
     public ExifModifierTest(String imagePath, String xmlPath) {
         super(imagePath, xmlPath);
+        mInterface = new ExifInterface();
     }
 
     public void testModify() throws Exception {
@@ -106,8 +115,8 @@ public class ExifModifierTest extends ExifXmlDataTestCase {
         try {
             file = new RandomAccessFile(mTmpFile, "rw");
             MappedByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, file.length());
-            for (ExifTag tag: sTestTags.values()) {
-                ExifModifier modifier = new ExifModifier(buf);
+            for (ExifTag tag : mTestTags.values()) {
+                ExifModifier modifier = new ExifModifier(buf, mInterface);
                 modifier.modifyTag(tag);
                 boolean result = modifier.commit();
                 results.put(tag.getTagId(), result);
@@ -116,8 +125,8 @@ public class ExifModifierTest extends ExifXmlDataTestCase {
 
                 if (!result) {
                     List<String> value = mGroundTruth.get(tag.getIfd()).get(tag.getTagId());
-                    assertTrue (String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
-                            value == null || tag.getTagId() == ExifTag.TAG_MODEL);
+                    assertTrue(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
+                            value == null || tag.getTagId() == ExifInterface.TAG_MODEL);
                 }
             }
         } finally {
@@ -128,7 +137,7 @@ public class ExifModifierTest extends ExifXmlDataTestCase {
         InputStream is = null;
         try {
             is = new FileInputStream(mTmpFile);
-            ExifData data = new ExifReader().read(is);
+            ExifData data = new ExifReader(mInterface).read(is);
             for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
                 checkIfd(data.getIfdData(i), mGroundTruth.get(i), results);
             }
@@ -138,20 +147,21 @@ public class ExifModifierTest extends ExifXmlDataTestCase {
 
     }
 
-
     private void checkIfd(IfdData ifd, Map<Short, List<String>> ifdValue,
             Map<Short, Boolean> results) {
         if (ifd == null) {
-            assertEquals(getImageTitle(), 0 ,ifdValue.size());
+            assertEquals(getImageTitle(), 0ifdValue.size());
             return;
         }
         ExifTag[] tags = ifd.getAllTags();
         for (ExifTag tag : tags) {
             List<String> truth = ifdValue.get(tag.getTagId());
             assertNotNull(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(), truth);
-            if (truth.contains(null)) continue;
+            if (truth.contains(null)) {
+                continue;
+            }
 
-            ExifTag newTag = sTestTags.get(tag.getTagId());
+            ExifTag newTag = mTestTags.get(tag.getTagId());
             if (newTag != null
                     && results.get(tag.getTagId())) {
                 assertEquals(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
index 789fd35..8c4fc3d 100644 (file)
@@ -20,17 +20,18 @@ import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 
 public class ExifOutputStreamTest extends ExifXmlDataTestCase {
 
     private File mTmpFile;
 
+    private ExifInterface mInterface;
+
     @Override
     public void setUp() throws Exception {
         super.setUp();
@@ -39,10 +40,12 @@ public class ExifOutputStreamTest extends ExifXmlDataTestCase {
 
     public ExifOutputStreamTest(int imgRes, int xmlRes) {
         super(imgRes, xmlRes);
+        mInterface = new ExifInterface();
     }
 
     public ExifOutputStreamTest(String imgPath, String xmlPath) {
         super(imgPath, xmlPath);
+        mInterface = new ExifInterface();
     }
 
     public void testExifOutputStream() throws Exception {
@@ -50,58 +53,84 @@ public class ExifOutputStreamTest extends ExifXmlDataTestCase {
         InputStream exifInputStream = null;
         FileInputStream reDecodeInputStream = null;
         FileInputStream reParseInputStream = null;
+
+        InputStream dangerInputStream = null;
+        OutputStream dangerOutputStream = null;
         try {
             try {
-                byte[] imgData = readToByteArray(getImageInputStream());
+                byte[] imgData = Util.readToByteArray(getImageInputStream());
                 imageInputStream = new ByteArrayInputStream(imgData);
                 exifInputStream = new ByteArrayInputStream(imgData);
 
                 // Read the image data
                 Bitmap bmp = BitmapFactory.decodeStream(imageInputStream);
                 // The image is invalid
-                if (bmp == null) return;
+                if (bmp == null) {
+                    return;
+                }
 
                 // Read exif data
-                ExifData exifData = new ExifReader().read(exifInputStream);
+                ExifData exifData = new ExifReader(mInterface).read(exifInputStream);
 
                 // Encode the image with the exif data
                 FileOutputStream outputStream = new FileOutputStream(mTmpFile);
-                ExifOutputStream exifOutputStream = new ExifOutputStream(outputStream);
+                ExifOutputStream exifOutputStream = new ExifOutputStream(outputStream, mInterface);
                 exifOutputStream.setExifData(exifData);
-                bmp.compress(Bitmap.CompressFormat.JPEG, 100, exifOutputStream);
+                bmp.compress(Bitmap.CompressFormat.JPEG, 90, exifOutputStream);
                 exifOutputStream.close();
+                exifOutputStream = null;
 
                 // Re-decode the temp file and check the data.
                 reDecodeInputStream = new FileInputStream(mTmpFile);
                 Bitmap decodedBmp = BitmapFactory.decodeStream(reDecodeInputStream);
                 assertNotNull(getImageTitle(), decodedBmp);
+                reDecodeInputStream.close();
 
                 // Re-parse the temp file the check EXIF tag
                 reParseInputStream = new FileInputStream(mTmpFile);
-                ExifData reExifData = new ExifReader().read(reParseInputStream);
+                ExifData reExifData = new ExifReader(mInterface).read(reParseInputStream);
+                assertEquals(getImageTitle(), exifData, reExifData);
+                reParseInputStream.close();
+
+                // Try writing exif to file with existing exif.
+                dangerOutputStream = (OutputStream) new FileOutputStream(mTmpFile);
+                exifOutputStream = new ExifOutputStream(dangerOutputStream, mInterface);
+                exifOutputStream.setExifData(exifData);
+                exifOutputStream.write(imgData);
+                // exifOutputStream.write(strippedImgData);
+                exifOutputStream.close();
+                exifOutputStream = null;
+
+                // Make sure it still can be parsed into a bitmap.
+                dangerInputStream = (InputStream) new FileInputStream(mTmpFile);
+                decodedBmp = null;
+                decodedBmp = BitmapFactory.decodeStream(dangerInputStream);
+                assertNotNull(getImageTitle(), decodedBmp);
+                dangerInputStream.close();
+                dangerInputStream = null;
+
+                // Make sure exif is still well-formatted.
+                dangerInputStream = (InputStream) new FileInputStream(mTmpFile);
+                reExifData = null;
+                reExifData = new ExifReader(mInterface).read(dangerInputStream);
                 assertEquals(getImageTitle(), exifData, reExifData);
+                dangerInputStream.close();
+                dangerInputStream = null;
+
             } finally {
                 Util.closeSilently(imageInputStream);
                 Util.closeSilently(exifInputStream);
                 Util.closeSilently(reDecodeInputStream);
                 Util.closeSilently(reParseInputStream);
+
+                Util.closeSilently(dangerInputStream);
+                Util.closeSilently(dangerOutputStream);
             }
         } catch (Exception e) {
             throw new Exception(getImageTitle(), e);
         }
     }
 
-    private byte[] readToByteArray(InputStream is) throws IOException {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        int len;
-        byte[] buf = new byte[1024];
-        while ((len = is.read(buf)) > -1) {
-            bos.write(buf, 0, len);
-        }
-        bos.flush();
-        return bos.toByteArray();
-    }
-
     @Override
     public void tearDown() throws Exception {
         super.tearDown();
index 7a9d6e6..5c3fd24 100644 (file)
@@ -25,12 +25,16 @@ import java.util.Map;
 public class ExifParserTest extends ExifXmlDataTestCase {
     private static final String TAG = "ExifParserTest";
 
+    private ExifInterface mInterface;
+
     public ExifParserTest(int imgRes, int xmlRes) {
         super(imgRes, xmlRes);
+        mInterface = new ExifInterface();
     }
 
     public ExifParserTest(String imgPath, String xmlPath) {
         super(imgPath, xmlPath);
+        mInterface = new ExifInterface();
     }
 
     private List<Map<Short, List<String>>> mGroundTruth;
@@ -43,7 +47,7 @@ public class ExifParserTest extends ExifXmlDataTestCase {
 
     public void testParse() throws Exception {
         try {
-            ExifParser parser = ExifParser.parse(getImageInputStream());
+            ExifParser parser = ExifParser.parse(getImageInputStream(), mInterface);
             int event = parser.next();
             while (event != ExifParser.EVENT_END) {
                 switch (event) {
@@ -62,7 +66,7 @@ public class ExifParserTest extends ExifXmlDataTestCase {
                         if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
                             byte[] buf = new byte[tag.getComponentCount()];
                             parser.read(buf);
-                            tag.setValue(buf);
+                            assertTrue(TAG, tag.setValue(buf));
                         }
                         checkTag(tag);
                         break;
@@ -82,7 +86,9 @@ public class ExifParserTest extends ExifXmlDataTestCase {
         }
 
         // No value from exiftool.
-        if (truth.contains(null)) return;
+        if (truth.contains(null)) {
+            return;
+        }
 
         String dataString = Util.tagValueToString(tag).trim();
         assertTrue(String.format("Tag %02x", tag.getTagId()) + ", " + getImageTitle()
@@ -94,9 +100,9 @@ public class ExifParserTest extends ExifXmlDataTestCase {
         try {
             Map<Short, List<String>> expectedResult = mGroundTruth.get(ifd);
             int numOfTag = 0;
-            ExifParser parser = ExifParser.parse(getImageInputStream(), options);
+            ExifParser parser = ExifParser.parse(getImageInputStream(), options, mInterface);
             int event = parser.next();
-            while(event != ExifParser.EVENT_END) {
+            while (event != ExifParser.EVENT_END) {
                 switch (event) {
                     case ExifParser.EVENT_START_OF_IFD:
                         assertEquals(getImageTitle(), ifd, parser.getCurrentIfd());
@@ -150,10 +156,13 @@ public class ExifParserTest extends ExifXmlDataTestCase {
 
     public void testOnlyReadSomeTag() throws Exception {
         // Do not do this test if there is no model tag.
-        if (mGroundTruth.get(IfdId.TYPE_IFD_0).get(ExifTag.TAG_MODEL) == null) return;
+        if (mGroundTruth.get(IfdId.TYPE_IFD_0).get(ExifInterface.TAG_MODEL) == null) {
+            return;
+        }
 
         try {
-            ExifParser parser = ExifParser.parse(getImageInputStream(), ExifParser.OPTION_IFD_0);
+            ExifParser parser = ExifParser.parse(getImageInputStream(), ExifParser.OPTION_IFD_0,
+                    mInterface);
             int event = parser.next();
             boolean isTagFound = false;
             while (event != ExifParser.EVENT_END) {
@@ -163,7 +172,7 @@ public class ExifParserTest extends ExifXmlDataTestCase {
                         break;
                     case ExifParser.EVENT_NEW_TAG:
                         ExifTag tag = parser.getTag();
-                        if (tag.getTagId() == ExifTag.TAG_MODEL) {
+                        if (tag.getTagId() == ExifInterface.TAG_MODEL) {
                             if (tag.hasValue()) {
                                 isTagFound = true;
                                 checkTag(tag);
@@ -175,7 +184,7 @@ public class ExifParserTest extends ExifXmlDataTestCase {
                         break;
                     case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
                         tag = parser.getTag();
-                        assertEquals(getImageTitle(), ExifTag.TAG_MODEL, tag.getTagId());
+                        assertEquals(getImageTitle(), ExifInterface.TAG_MODEL, tag.getTagId());
                         checkTag(tag);
                         isTagFound = true;
                         break;
@@ -191,7 +200,7 @@ public class ExifParserTest extends ExifXmlDataTestCase {
     public void testReadThumbnail() throws Exception {
         try {
             ExifParser parser = ExifParser.parse(getImageInputStream(),
-                    ExifParser.OPTION_IFD_1 | ExifParser.OPTION_THUMBNAIL);
+                    ExifParser.OPTION_IFD_1 | ExifParser.OPTION_THUMBNAIL, mInterface);
 
             int event = parser.next();
             Bitmap bmp = null;
@@ -200,8 +209,8 @@ public class ExifParserTest extends ExifXmlDataTestCase {
                 switch (event) {
                     case ExifParser.EVENT_NEW_TAG:
                         ExifTag tag = parser.getTag();
-                        if (tag.getTagId() == ExifTag.TAG_COMPRESSION) {
-                            if (tag.getValueAt(0) == ExifTag.Compression.JPEG) {
+                        if (tag.getTagId() == ExifInterface.TAG_COMPRESSION) {
+                            if (tag.getValueAt(0) == ExifInterface.Compression.JPEG) {
                                 mIsContainCompressedImage = true;
                             }
                         }
index 30f34c0..a057186 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Map;
 public class ExifReaderTest extends ExifXmlDataTestCase {
     private static final String TAG = "ExifReaderTest";
 
+    private ExifInterface mInterface;
     private List<Map<Short, List<String>>> mGroundTruth;
 
     @Override
@@ -34,15 +35,17 @@ public class ExifReaderTest extends ExifXmlDataTestCase {
 
     public ExifReaderTest(int imgRes, int xmlRes) {
         super(imgRes, xmlRes);
+        mInterface = new ExifInterface();
     }
 
     public ExifReaderTest(String imgPath, String xmlPath) {
         super(imgPath, xmlPath);
+        mInterface = new ExifInterface();
     }
 
     public void testRead() throws Exception {
         try {
-            ExifReader reader = new ExifReader();
+            ExifReader reader = new ExifReader(mInterface);
             ExifData exifData = reader.read(getImageInputStream());
             for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
                 checkIfd(exifData.getIfdData(i), mGroundTruth.get(i));
@@ -56,59 +59,76 @@ public class ExifReaderTest extends ExifXmlDataTestCase {
     private void checkThumbnail(ExifData exifData) {
         Map<Short, List<String>> ifd1Truth = mGroundTruth.get(IfdId.TYPE_IFD_1);
 
-        List<String> typeTagValue = ifd1Truth.get(ExifTag.TAG_COMPRESSION);
-        if (typeTagValue == null) return;
+        List<String> typeTagValue = ifd1Truth.get(ExifInterface.TAG_COMPRESSION);
+        if (typeTagValue == null)
+            return;
 
         IfdData ifd1 = exifData.getIfdData(IfdId.TYPE_IFD_1);
-        if (ifd1 == null) fail(getImageTitle() + ": failed to find IFD1");
+        if (ifd1 == null)
+            fail(getImageTitle() + ": failed to find IFD1");
 
         String typeTagTruth = typeTagValue.get(0);
 
-        int type = (int) ifd1.getTag(ExifTag.TAG_COMPRESSION).getValueAt(0);
+        int type = (int) ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_COMPRESSION))
+                .getValueAt(0);
 
-        if (String.valueOf(ExifTag.Compression.JPEG).equals(typeTagTruth)) {
-            assertTrue(getImageTitle(), type == ExifTag.Compression.JPEG);
+        if (String.valueOf(ExifInterface.Compression.JPEG).equals(typeTagTruth)) {
+            assertTrue(getImageTitle(), type == ExifInterface.Compression.JPEG);
             assertTrue(getImageTitle(), exifData.hasCompressedThumbnail());
             byte[] thumbnail = exifData.getCompressedThumbnail();
             assertTrue(getImageTitle(),
                     BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length) != null);
-        } else if (String.valueOf(ExifTag.Compression.UNCOMPRESSION).equals(typeTagTruth)) {
-            assertTrue(getImageTitle(), type == ExifTag.Compression.UNCOMPRESSION);
+        } else if (String.valueOf(ExifInterface.Compression.UNCOMPRESSION).equals(typeTagTruth)) {
+            assertTrue(getImageTitle(), type == ExifInterface.Compression.UNCOMPRESSION);
             // Try to check the strip count with the formula provided by EXIF spec.
-            int planarType = ExifTag.PlanarConfiguration.CHUNKY;
-            ExifTag planarTag = ifd1.getTag(ExifTag.TAG_PLANAR_CONFIGURATION);
+            int planarType = ExifInterface.PlanarConfiguration.CHUNKY;
+            ExifTag planarTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_PLANAR_CONFIGURATION));
             if (planarTag != null) {
                 planarType = (int) planarTag.getValueAt(0);
             }
 
-            if (!ifd1Truth.containsKey(ExifTag.TAG_IMAGE_LENGTH) ||
-                    !ifd1Truth.containsKey(ExifTag.TAG_ROWS_PER_STRIP)) return;
+            if (!ifd1Truth.containsKey(ExifInterface.TAG_IMAGE_LENGTH) ||
+                    !ifd1Truth.containsKey(ExifInterface.TAG_ROWS_PER_STRIP)) {
+                return;
+            }
 
-            ExifTag heightTag = ifd1.getTag(ExifTag.TAG_IMAGE_LENGTH);
-            ExifTag rowPerStripTag = ifd1.getTag(ExifTag.TAG_ROWS_PER_STRIP);
+            ExifTag heightTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_IMAGE_LENGTH));
+            ExifTag rowPerStripTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_ROWS_PER_STRIP));
 
             // Fail the test if required tags are missing
-            if (heightTag == null || rowPerStripTag == null) fail(getImageTitle());
+            if (heightTag == null || rowPerStripTag == null) {
+                fail(getImageTitle());
+            }
 
             int imageLength = (int) heightTag.getValueAt(0);
             int rowsPerStrip = (int) rowPerStripTag.getValueAt(0);
             int stripCount = ifd1.getTag(
-                    ExifTag.TAG_STRIP_OFFSETS).getComponentCount();
+                    ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
+                    .getComponentCount();
 
-            if (planarType == ExifTag.PlanarConfiguration.CHUNKY) {
+            if (planarType == ExifInterface.PlanarConfiguration.CHUNKY) {
                 assertTrue(getImageTitle(),
                         stripCount == (imageLength + rowsPerStrip - 1) / rowsPerStrip);
             } else {
-                if (!ifd1Truth.containsKey(ExifTag.TAG_SAMPLES_PER_PIXEL)) return;
-                ExifTag samplePerPixelTag = ifd1.getTag(ExifTag.TAG_SAMPLES_PER_PIXEL);
+                if (!ifd1Truth.containsKey(ExifInterface.TAG_SAMPLES_PER_PIXEL)) {
+                    return;
+                }
+                ExifTag samplePerPixelTag = ifd1.getTag(ExifInterface
+                        .getTrueTagKey(ExifInterface.TAG_SAMPLES_PER_PIXEL));
                 int samplePerPixel = (int) samplePerPixelTag.getValueAt(0);
                 assertTrue(getImageTitle(),
                         stripCount ==
                         (imageLength + rowsPerStrip - 1) / rowsPerStrip * samplePerPixel);
             }
 
-            if (!ifd1Truth.containsKey(ExifTag.TAG_STRIP_BYTE_COUNTS)) return;
-            ExifTag byteCountTag = ifd1.getTag(ExifTag.TAG_STRIP_BYTE_COUNTS);
+            if (!ifd1Truth.containsKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)) {
+                return;
+            }
+            ExifTag byteCountTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
             short byteCountDataType = byteCountTag.getDataType();
             for (int i = 0; i < stripCount; i++) {
                 if (byteCountDataType == ExifTag.TYPE_UNSIGNED_SHORT) {
@@ -124,14 +144,16 @@ public class ExifReaderTest extends ExifXmlDataTestCase {
 
     private void checkIfd(IfdData ifd, Map<Short, List<String>> ifdValue) {
         if (ifd == null) {
-            assertEquals(getImageTitle(), 0 ,ifdValue.size());
+            assertEquals(getImageTitle(), 0ifdValue.size());
             return;
         }
         ExifTag[] tags = ifd.getAllTags();
         for (ExifTag tag : tags) {
             List<String> truth = ifdValue.get(tag.getTagId());
             assertNotNull(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(), truth);
-            if (truth.contains(null)) continue;
+            if (truth.contains(null)) {
+                continue;
+            }
             assertTrue(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
                     truth.contains(Util.tagValueToString(tag).trim()));
         }
index 128956d..59067c3 100644 (file)
@@ -18,27 +18,83 @@ package com.android.gallery3d.exif;
 
 import junit.framework.TestCase;
 
+import java.util.HashMap;
+import java.util.Map;
+
 public class ExifTagTest extends TestCase {
 
     private static long MAX_UNSIGNED_LONG = (1L << 32) - 1;
     private static int MAX_LONG = Integer.MAX_VALUE;
     private static int MIN_LONG = Integer.MIN_VALUE;
 
-    private static final ExifTag sTestTags[] = {
-        ExifTag.buildTag(ExifTag.TAG_EXIF_VERSION), // TYPE_UNDEFINED with 4 components
-        ExifTag.buildTag(ExifTag.TAG_GPS_VERSION_ID), // TYPE_UNSIGNED_BYTE with 4 components
-        ExifTag.buildTag(ExifTag.TAG_DATE_TIME), // TYPE_ASCII with 20 components
-        ExifTag.buildTag(ExifTag.TAG_COMPRESSION), // TYPE_UNSIGNED_SHORT with 1 components
+    Map<Integer, ExifTag> mTestTags;
+    ExifInterface mInterface;
+    private ExifTag mVersionTag;
+    private ExifTag mGpsVersionTag;
+    private ExifTag mModelTag;
+    private ExifTag mDateTimeTag;
+    private ExifTag mCompressionTag;
+    private ExifTag mThumbnailFormatTag;
+    private ExifTag mLongitudeTag;
+    private ExifTag mShutterTag;
+    private ExifTag mInteropIndex;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mInterface = new ExifInterface();
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                5, 4, 3, 2
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                6, 7, 8, 9
+        });
+        // TYPE ASCII with arbitrary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "helloworld");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2013:02:11 20:20:20");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
         // TYPE_UNSIGNED_LONG with 1 components
-        ExifTag.buildTag(ExifTag.TAG_JPEG_INTERCHANGE_FORMAT),
-        ExifTag.buildTag(ExifTag.TAG_GPS_LONGITUDE), // TYPE_UNSIGNED_RATIONAL with 3 components
-        ExifTag.buildTag(ExifTag.TAG_SHUTTER_SPEED_VALUE), // TYPE_RATIONAL with 1 components
-        // There is no tag defined with TYPE_LONG. Create a dummy one for testing.
-        new ExifTag((short) 0, ExifTag.TYPE_LONG, 1, 0)
-    };
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(2, 2), new Rational(11, 11),
+                new Rational(102, 102)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(4, 6));
+        // TYPE_ASCII with arbitrary length
+        mInteropIndex = mInterface.buildTag(ExifInterface.TAG_INTEROPERABILITY_INDEX, "foo");
+
+        mTestTags = new HashMap<Integer, ExifTag>();
+
+        mTestTags.put(ExifInterface.TAG_EXIF_VERSION, mVersionTag);
+        mTestTags.put(ExifInterface.TAG_GPS_VERSION_ID, mGpsVersionTag);
+        mTestTags.put(ExifInterface.TAG_MODEL, mModelTag);
+        mTestTags.put(ExifInterface.TAG_DATE_TIME, mDateTimeTag);
+        mTestTags.put(ExifInterface.TAG_COMPRESSION, mCompressionTag);
+        mTestTags.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, mThumbnailFormatTag);
+        mTestTags.put(ExifInterface.TAG_GPS_LONGITUDE, mLongitudeTag);
+        mTestTags.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, mShutterTag);
+        mTestTags.put(ExifInterface.TAG_INTEROPERABILITY_INDEX, mInteropIndex);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mInterface = null;
+        mTestTags = null;
+    }
 
     public void testValueType() {
-        for (ExifTag tag: sTestTags) {
+        for (ExifTag tag : mTestTags.values()) {
+            assertTrue(tag != null);
             int count = tag.getComponentCount();
             int intBuf[] = new int[count];
             long longBuf[] = new long[count];
@@ -51,7 +107,11 @@ public class ExifTagTest extends TestCase {
                 byteBuf[i] = 0;
                 rationalBuf[i] = new Rational(0, 0);
                 // The string size should equal to component count - 1
-                if (i != 0) sb.append("*");
+                if (i != count - 1) {
+                    sb.append("*");
+                } else {
+                    sb.append("\0");
+                }
             }
             String strBuf = sb.toString();
 
@@ -66,141 +126,90 @@ public class ExifTagTest extends TestCase {
     }
 
     private void checkTypeByte(ExifTag tag, byte[] buf) {
-        boolean excepThrow = false;
         short type = tag.getDataType();
-        try {
-            tag.setValue(buf);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
-        assertTrue("Tag ID: " + tag.getTagId(),
-                (type == ExifTag.TYPE_UNDEFINED || type == ExifTag.TYPE_UNSIGNED_BYTE)
-                ^ excepThrow);
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(buf)
+                ^ (type == ExifTag.TYPE_UNDEFINED || type == ExifTag.TYPE_UNSIGNED_BYTE));
     }
 
     private void checkTypeAscii(ExifTag tag, String str) {
-        boolean excepThrow = false;
-        try {
-            tag.setValue(str);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
-        assertTrue("Tag ID: " + tag.getTagId(),
-                tag.getDataType() == ExifTag.TYPE_ASCII ^ excepThrow);
+        short type = tag.getDataType();
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(str)
+                ^ (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED));
     }
 
     private void checkTypeUnsignedShort(ExifTag tag, int[] intBuf) {
-        boolean excepThrow = false;
         short type = tag.getDataType();
-        try {
-            tag.setValue(intBuf);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
-        assertTrue("Tag ID: " + tag.getTagId(),
-                (type == ExifTag.TYPE_UNSIGNED_SHORT
-                || type == ExifTag.TYPE_UNSIGNED_LONG || type == ExifTag.TYPE_LONG) ^ excepThrow);
+        assertFalse("\nTag: " + tag.toString(),
+                tag.setValue(intBuf)
+                        ^ (type == ExifTag.TYPE_UNSIGNED_SHORT
+                                || type == ExifTag.TYPE_UNSIGNED_LONG
+                                || type == ExifTag.TYPE_LONG));
     }
 
     private void checkTypeUnsignedLong(ExifTag tag, int[] intBuf, long[] longBuf) {
 
         // Test value only for unsigned long.
-        boolean excepThrow = false;
         int count = intBuf.length;
-        try {
-            intBuf[count - 1] = MAX_LONG;
-            tag.setValue(intBuf);
-            longBuf[count - 1] = MAX_UNSIGNED_LONG;
-            tag.setValue(longBuf);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
-        intBuf[count - 1] = 0;
-        assertTrue("Tag ID: " + tag.getTagId(),
-                tag.getDataType() == ExifTag.TYPE_UNSIGNED_LONG ^ excepThrow);
+        intBuf[count - 1] = MAX_LONG;
+        tag.setValue(intBuf);
+        longBuf[count - 1] = MAX_UNSIGNED_LONG;
 
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(longBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_UNSIGNED_LONG));
 
+        intBuf[count - 1] = 0;
         // Test invalid value for all type.
-        try {
-            longBuf[count - 1] = MAX_UNSIGNED_LONG + 1;
-            tag.setValue(longBuf);
-            fail();
-        } catch (IllegalArgumentException expected) {}
+        longBuf[count - 1] = MAX_UNSIGNED_LONG + 1;
+        assertFalse(tag.setValue(longBuf));
         longBuf[count - 1] = 0;
     }
 
     private void checkTypeLong(ExifTag tag, int[] intBuf) {
-        boolean excepThrow = false;
         int count = intBuf.length;
-        try {
-            intBuf[count - 1] = MAX_LONG;
-            tag.setValue(intBuf);
-            intBuf[count - 1] = MIN_LONG;
-            tag.setValue(intBuf);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
+        intBuf[count - 1] = MAX_LONG;
+        tag.setValue(intBuf);
+        intBuf[count - 1] = MIN_LONG;
+
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(intBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_LONG));
         intBuf[count - 1] = 0;
-        assertTrue("Tag ID: " + tag.getTagId(),
-                tag.getDataType() == ExifTag.TYPE_LONG ^ excepThrow);
     }
 
     private void checkTypeRational(ExifTag tag, Rational rationalBuf[]) {
-        boolean excepThrow = false;
         int count = rationalBuf.length;
         Rational r = rationalBuf[count - 1];
-        try {
-            rationalBuf[count - 1] = new Rational(MAX_LONG, MIN_LONG);
-            tag.setValue(rationalBuf);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
-        assertTrue("Tag ID: " + tag.getTagId(),
-                tag.getDataType() == ExifTag.TYPE_RATIONAL ^ excepThrow);
+        rationalBuf[count - 1] = new Rational(MAX_LONG, MIN_LONG);
+
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(rationalBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_RATIONAL));
 
-        if(tag.getDataType() == ExifTag.TYPE_RATIONAL) {
+        if (tag.getDataType() == ExifTag.TYPE_RATIONAL) {
             // check overflow
-            try {
-                rationalBuf[count - 1] = new Rational(MAX_LONG + 1L, MIN_LONG);
-                tag.setValue(rationalBuf);
-                fail();
-            } catch (IllegalArgumentException expected) {}
-
-            try {
-                rationalBuf[count - 1] = new Rational(MAX_LONG, MIN_LONG - 1L);
-                tag.setValue(rationalBuf);
-                fail();
-            } catch (IllegalArgumentException expected) {}
+
+            rationalBuf[count - 1] = new Rational(MAX_LONG + 1L, MIN_LONG);
+            assertFalse(tag.setValue(rationalBuf));
+
+            rationalBuf[count - 1] = new Rational(MAX_LONG, MIN_LONG - 1L);
+            assertFalse(tag.setValue(rationalBuf));
         }
         rationalBuf[count - 1] = r;
     }
 
     private void checkTypeUnsignedRational(ExifTag tag, Rational rationalBuf[]) {
-        boolean excepThrow = false;
         int count = rationalBuf.length;
         Rational r = rationalBuf[count - 1];
-        try {
-            rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG, MAX_UNSIGNED_LONG);
-            tag.setValue(rationalBuf);
-        } catch (IllegalArgumentException e) {
-            excepThrow = true;
-        }
-        assertTrue("Tag ID: " + tag.getTagId(),
-                tag.getDataType() == ExifTag.TYPE_UNSIGNED_RATIONAL ^ excepThrow);
+        rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG, MAX_UNSIGNED_LONG);
 
-        if(tag.getDataType() == ExifTag.TYPE_UNSIGNED_RATIONAL) {
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(rationalBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_UNSIGNED_RATIONAL));
+
+        if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_RATIONAL) {
             // check overflow
-            try {
-                rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG + 1, 0);
-                tag.setValue(rationalBuf);
-                fail();
-            } catch (IllegalArgumentException expected) {}
-
-            try {
-                rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG, -1);
-                tag.setValue(rationalBuf);
-                fail();
-            } catch (IllegalArgumentException expected) {}
+            rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG + 1, 0);
+            assertFalse(tag.setValue(rationalBuf));
+
+            rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG, -1);
+            assertFalse(tag.setValue(rationalBuf));
         }
         rationalBuf[count - 1] = r;
     }
index b094263..162baea 100644 (file)
@@ -37,11 +37,11 @@ public class ExifTestRunner extends InstrumentationTestRunner {
     private static final String TAG = "ExifTestRunner";
 
     private static final int[] IMG_RESOURCE = {
-        R.raw.galaxy_nexus
+            R.raw.galaxy_nexus
     };
 
     private static final int[] EXIF_DATA_RESOURCE = {
-        R.xml.galaxy_nexus
+            R.xml.galaxy_nexus
     };
 
     private static List<String> mTestImgPath = new ArrayList<String>();
@@ -57,6 +57,7 @@ public class ExifTestRunner extends InstrumentationTestRunner {
         addAllTestsFromExifTestCase(ExifReaderTest.class, suite);
         addAllTestsFromExifTestCase(ExifOutputStreamTest.class, suite);
         addAllTestsFromExifTestCase(ExifModifierTest.class, suite);
+        addAllTestsFromExifTestCase(ExifInterfaceTest.class, suite);
         return suite;
     }
 
@@ -67,8 +68,10 @@ public class ExifTestRunner extends InstrumentationTestRunner {
 
         if (imgDir != null && xmlDir != null) {
             String[] imgs = imgDir.list();
-            if (imgs == null) return;
-            for (String imgName: imgs) {
+            if (imgs == null) {
+                return;
+            }
+            for (String imgName : imgs) {
                 String xmlName = imgName.substring(0, imgName.lastIndexOf('.')) + ".xml";
                 File xmlFile = new File(xmlDir, xmlName);
                 if (xmlFile.exists()) {
index 811b0a6..5f200ea 100644 (file)
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-
 package com.android.gallery3d.exif;
 
 import android.content.res.Resources;
index bb08ccd..12e9cf7 100644 (file)
@@ -110,14 +110,14 @@ public class ExifXmlReader {
         } else if (INTEROP_IFD.equals(prefix)) {
             return IfdId.TYPE_IFD_INTEROPERABILITY;
         } else {
-            assert(false);
+            assert (false);
             return -1;
         }
     }
 
     static public int getTrueTagNumber(Map<Short, List<String>> ifdData) {
         int size = 0;
-        for (List<String> tag: ifdData.values()) {
+        for (List<String> tag : ifdData.values()) {
             size += tag.size();
         }
         return size;
index 0e51fd7..15de007 100644 (file)
 
 package com.android.gallery3d.exif;
 
+import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
 
 class Util {
     public static boolean equals(Object a, Object b) {
@@ -24,7 +28,8 @@ class Util {
     }
 
     public static void closeSilently(Closeable c) {
-        if (c == null) return;
+        if (c == null)
+            return;
         try {
             c.close();
         } catch (Throwable t) {
@@ -32,6 +37,17 @@ class Util {
         }
     }
 
+    public static byte[] readToByteArray(InputStream is) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        int len;
+        byte[] buf = new byte[1024];
+        while ((len = is.read(buf)) > -1) {
+            bos.write(buf, 0, len);
+        }
+        bos.flush();
+        return bos.toByteArray();
+    }
+
     /**
      * Tags that are not defined in the spec.
      */
@@ -45,27 +61,30 @@ class Util {
         StringBuilder sbuilder = new StringBuilder();
         byte[] buf = new byte[tag.getComponentCount()];
         tag.getBytes(buf);
-        switch (tag.getTagId()) {
-            case ExifTag.TAG_COMPONENTS_CONFIGURATION:
-                for(int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    if(i != 0) sbuilder.append(" ");
-                    sbuilder.append(buf[i]);
+        short tagId = tag.getTagId();
+        if (tagId == ExifInterface.getTrueTagKey(ExifInterface.TAG_COMPONENTS_CONFIGURATION)) {
+            for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                if (i != 0) {
+                    sbuilder.append(" ");
                 }
-                break;
-            default:
-                if (buf.length == 1) {
-                    sbuilder.append(buf[0]);
-                } else {
-                    for (int i = 0, n = buf.length; i < n; i++) {
-                        byte code = buf[i];
-                        if (code == 0) continue;
-                        if (code > 31 && code < 127) {
-                            sbuilder.append((char) code);
-                        } else {
-                            sbuilder.append('.');
-                        }
+                sbuilder.append(buf[i]);
+            }
+        } else {
+            if (buf.length == 1) {
+                sbuilder.append(buf[0]);
+            } else {
+                for (int i = 0, n = buf.length; i < n; i++) {
+                    byte code = buf[i];
+                    if (code == 0) {
+                        continue;
+                    }
+                    if (code > 31 && code < 127) {
+                        sbuilder.append((char) code);
+                    } else {
+                        sbuilder.append('.');
                     }
                 }
+            }
         }
         return sbuilder.toString();
     }
@@ -81,15 +100,16 @@ class Util {
                 sbuilder.append(tagUndefinedTypeValueToString(tag));
                 break;
             case ExifTag.TYPE_UNSIGNED_BYTE:
-                if (id == ExifTag.TAG_MAKER_NOTE || id == TAG_XP_TITLE ||
+                if (id == ExifInterface.TAG_MAKER_NOTE || id == TAG_XP_TITLE ||
                         id == TAG_XP_COMMENT || id == TAG_XP_AUTHOR ||
                         id == TAG_XP_KEYWORDS || id == TAG_XP_SUBJECT) {
                     sbuilder.append(tagUndefinedTypeValueToString(tag));
                 } else {
                     byte[] buf = new byte[tag.getComponentCount()];
                     tag.getBytes(buf);
-                    for(int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                        if(i != 0) sbuilder.append(" ");
+                    for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                        if (i != 0)
+                            sbuilder.append(" ");
                         sbuilder.append(buf[i]);
                     }
                 }
@@ -100,7 +120,8 @@ class Util {
                     byte code = buf[i];
                     if (code == 0) {
                         // Treat some tag as undefined type data.
-                        if (id == ExifTag.TAG_COPYRIGHT || id == ExifTag.TAG_GPS_DATE_STAMP) {
+                        if (id == ExifInterface.TAG_COPYRIGHT
+                                || id == ExifInterface.TAG_GPS_DATE_STAMP) {
                             continue;
                         } else {
                             break;
@@ -114,32 +135,61 @@ class Util {
                 }
                 break;
             case ExifTag.TYPE_UNSIGNED_LONG:
-                for(int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    if(i != 0) sbuilder.append(" ");
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
                     sbuilder.append(tag.getValueAt(i));
                 }
                 break;
             case ExifTag.TYPE_RATIONAL:
             case ExifTag.TYPE_UNSIGNED_RATIONAL:
-                for(int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
                     Rational r = tag.getRational(i);
-                    if(i != 0) sbuilder.append(" ");
-                    sbuilder.append(r.getNominator()).append("/").append(r.getDenominator());
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
+                    sbuilder.append(r.getNumerator()).append("/").append(r.getDenominator());
                 }
                 break;
             case ExifTag.TYPE_UNSIGNED_SHORT:
-                for(int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    if(i != 0) sbuilder.append(" ");
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
                     sbuilder.append((int) tag.getValueAt(i));
                 }
                 break;
             case ExifTag.TYPE_LONG:
-                for(int i = 0, n = tag.getComponentCount(); i < n; i++) {
-                    if(i != 0) sbuilder.append(" ");
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
                     sbuilder.append((int) tag.getValueAt(i));
                 }
                 break;
         }
         return sbuilder.toString();
     }
+
+    public static String valueToString(Object obj) {
+        if (obj instanceof int[]) {
+            return Arrays.toString((int[]) obj);
+        } else if (obj instanceof Integer[]) {
+            return Arrays.toString((Integer[]) obj);
+        } else if (obj instanceof long[]) {
+            return Arrays.toString((long[]) obj);
+        } else if (obj instanceof Long[]) {
+            return Arrays.toString((Long[]) obj);
+        } else if (obj instanceof Rational) {
+            return ((Rational) obj).toString();
+        } else if (obj instanceof Rational[]) {
+            return Arrays.toString((Rational[]) obj);
+        } else if (obj instanceof byte[]) {
+            return Arrays.toString((byte[]) obj);
+        } else if (obj != null) {
+            return obj.toString();
+        }
+        return "";
+    }
 }