Merge "Low-level exif parser" into gb-ub-photos-arches
diff --git a/src/com/android/gallery3d/exif/ExifInvalidFormatException.java b/src/com/android/gallery3d/exif/ExifInvalidFormatException.java
new file mode 100644
index 0000000..bf923ec
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifInvalidFormatException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class ExifInvalidFormatException extends Exception {
+    public ExifInvalidFormatException(String meg) {
+        super(meg);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/ExifParser.java b/src/com/android/gallery3d/exif/ExifParser.java
new file mode 100644
index 0000000..534f2f6
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifParser.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 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 java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+
+public class ExifParser {
+
+    private static final String TAG = "ExifParser";
+
+    private static final short SOI =  (short) 0xFFD8; // SOI marker of JPEG
+    private static final short APP1 = (short) 0xFFE1; // APP1 marker of JPEG
+
+    private static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+    private 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;
+
+    public IfdParser parse(InputStream inputStream) throws ExifInvalidFormatException, IOException{
+        if (!seekTiffData(inputStream)) {
+            return null;
+        }
+        TiffInputStream tiffStream = new TiffInputStream(inputStream);
+        parseTiffHeader(tiffStream);
+        long offset = tiffStream.readUnsignedInt();
+        if (offset > Integer.MAX_VALUE) {
+            throw new ExifInvalidFormatException("Offset value is larger than Integer.MAX_VALUE");
+        }
+        return new IfdParser(tiffStream, (int)offset);
+    }
+
+    private void parseTiffHeader(TiffInputStream tiffStream) throws IOException,
+            ExifInvalidFormatException {
+        short byteOrder = tiffStream.readShort();
+        ByteOrder order;
+        if (LITTLE_ENDIAN_TAG == byteOrder) {
+            tiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+        } else if (BIG_ENDIAN_TAG == byteOrder) {
+            tiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+        } else {
+            throw new ExifInvalidFormatException("Invalid TIFF header");
+        }
+
+        if (tiffStream.readShort() != TIFF_HEADER_TAIL) {
+            throw new ExifInvalidFormatException("Invalid TIFF header");
+        }
+    }
+
+    /**
+     * Try to seek the tiff data. If there is no tiff data, return false, else return true and
+     * the inputstream will be at the start of tiff data
+     */
+    private boolean seekTiffData(InputStream inputStream) throws IOException,
+            ExifInvalidFormatException {
+        DataInputStream dataStream = new DataInputStream(inputStream);
+
+        // SOI and APP1
+        if (dataStream.readShort() != SOI) {
+            throw new ExifInvalidFormatException("Invalid JPEG format");
+        }
+
+        if (dataStream.readShort() != APP1) {
+            return false;
+        }
+
+        // APP1 length, it's not used for us
+        dataStream.readShort();
+
+        // Exif header
+        if (dataStream.readInt() != EXIF_HEADER
+                || dataStream.readShort() != EXIF_HEADER_TAIL) {
+            // There is no EXIF data;
+            return false;
+        }
+
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/ExifTag.java b/src/com/android/gallery3d/exif/ExifTag.java
new file mode 100644
index 0000000..87fbe62
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifTag.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class ExifTag {
+
+    public static final short TYPE_BYTE = 1;
+    public static final short TYPE_ASCII = 2;
+    public static final short TYPE_SHORT = 3;
+    public static final short TYPE_INT = 4;
+    public static final short TYPE_RATIONAL = 5;
+    public static final short TYPE_UNDEFINED = 7;
+    public static final short TYPE_SINT = 9;
+    public static final short TYPE_SRATIONAL = 10;
+
+    private static final int TYPE_TO_SIZE_MAP[] = new int[11];
+    static {
+        TYPE_TO_SIZE_MAP[TYPE_BYTE] = 1;
+        TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
+        TYPE_TO_SIZE_MAP[TYPE_SHORT] = 2;
+        TYPE_TO_SIZE_MAP[TYPE_INT] = 4;
+        TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
+        TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1;
+        TYPE_TO_SIZE_MAP[TYPE_SINT] = 4;
+        TYPE_TO_SIZE_MAP[TYPE_SRATIONAL] = 8;
+    }
+
+    public static int getElementSize(short type) {
+        return TYPE_TO_SIZE_MAP[type];
+    }
+
+    private final short mTagId;
+    private final short mDataType;
+    private final int mDataCount;
+    private final int mOffset;
+
+    ExifTag(short tagId, short type, int dataCount) {
+        mTagId = tagId;
+        mDataType = type;
+        mDataCount = dataCount;
+        mOffset = -1;
+    }
+
+    ExifTag(short tagId, short type, int dataCount, int offset) {
+        mTagId = tagId;
+        mDataType = type;
+        mDataCount = dataCount;
+        mOffset = offset;
+    }
+
+    public int getOffset() {
+        return mOffset;
+    }
+
+    public short getTagId() {
+        return mTagId;
+    }
+
+    public short getDataType() {
+        return mDataType;
+    }
+
+    public int getComponentCount() {
+        return mDataCount;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/IfdParser.java b/src/com/android/gallery3d/exif/IfdParser.java
new file mode 100644
index 0000000..6af10c7
--- /dev/null
+++ b/src/com/android/gallery3d/exif/IfdParser.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2012 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 java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+public class IfdParser {
+
+    // special sub IDF tags
+    private static final short EXIF_IDF = (short) 0x8769;
+    private static final short GPS_IDF = (short) 0x8825;
+    private static final short INTEROPERABILITY_IDF = (short) 0xA005;
+
+    private static final int TAG_SIZE = 12;
+
+    private final TiffInputStream mTiffStream;
+    private final int mEndOfTagOffset;
+    private final int mNumOfTag;
+    private int mNextOffset;
+    private int mOffsetToNextIfd = 0;
+
+    private TreeSet<ExifTag> mCorrespondingTag = new TreeSet<ExifTag>(
+            new Comparator<ExifTag>() {
+        @Override
+        public int compare(ExifTag lhs, ExifTag rhs) {
+            return lhs.getOffset() - rhs.getOffset();
+        }
+    });
+    private ExifTag mCurrTag;
+
+    public static final int TYPE_NEW_TAG = 0;
+    public static final int TYPE_VALUE_OF_PREV_TAG = 1;
+    public static final int TYPE_NEXT_IFD = 2;
+    public static final int TYPE_END = 3;
+    public static final int TYPE_SUB_IFD = 4;
+
+    IfdParser(TiffInputStream tiffStream, int offset) throws IOException {
+        mTiffStream = tiffStream;
+        mTiffStream.skipTo(offset);
+        mNumOfTag = mTiffStream.readUnsignedShort();
+        mEndOfTagOffset = offset + mNumOfTag * TAG_SIZE + 2;
+        mNextOffset = offset + 2;
+    }
+
+    public int next() throws IOException {
+        int offset = mTiffStream.getReadByteCount();
+
+        if (offset < mEndOfTagOffset) {
+            skipTo(mNextOffset);
+            mNextOffset += TAG_SIZE;
+            return TYPE_NEW_TAG;
+        }
+
+        if (offset == mEndOfTagOffset) {
+            mOffsetToNextIfd = mTiffStream.readInt();
+        }
+
+        if (!mCorrespondingTag.isEmpty()) {
+            mCurrTag = mCorrespondingTag.pollFirst();
+            skipTo(mCurrTag.getOffset());
+            if (isSubIfdTag(mCurrTag.getTagId())) {
+                return TYPE_SUB_IFD;
+            } else {
+                return TYPE_VALUE_OF_PREV_TAG;
+            }
+        } else {
+            if (offset <= mOffsetToNextIfd) {
+                skipTo(mOffsetToNextIfd);
+                return TYPE_NEXT_IFD;
+            } else {
+                return TYPE_END;
+            }
+        }
+    }
+
+    public ExifTag readTag() throws IOException, ExifInvalidFormatException {
+        short tagId = mTiffStream.readShort();
+        short dataFormat = mTiffStream.readShort();
+        long numOfComp = mTiffStream.readUnsignedInt();
+        if (numOfComp > Integer.MAX_VALUE) {
+            throw new ExifInvalidFormatException(
+                    "Number of component is larger then Integer.MAX_VALUE");
+        }
+
+        if (ExifTag.getElementSize(dataFormat) * numOfComp > 4
+                || isSubIfdTag(tagId)) {
+            int offset = mTiffStream.readInt();
+            return new ExifTag(tagId, dataFormat, (int) numOfComp, offset);
+        } else {
+            return new ExifTag(tagId, dataFormat, (int) numOfComp);
+        }
+    }
+
+    public ExifTag getCorrespodingExifTag() {
+        return mCurrTag.getOffset() != mTiffStream.getReadByteCount() ? null : mCurrTag;
+    }
+
+    public void waitValueOfTag(ExifTag tag) {
+        mCorrespondingTag.add(tag);
+    }
+
+    public void skipTo(int offset) throws IOException {
+        mTiffStream.skipTo(offset);
+        while (!mCorrespondingTag.isEmpty() && mCorrespondingTag.first().getOffset() < offset) {
+            mCorrespondingTag.pollFirst();
+        }
+    }
+
+    public IfdParser parseIfdBlock() throws IOException {
+        return new IfdParser(mTiffStream, mTiffStream.getReadByteCount());
+    }
+
+    public int read(byte[] buffer, int offset, int length) throws IOException {
+        return mTiffStream.read(buffer, offset, length);
+    }
+
+    public int read(byte[] buffer) throws IOException {
+        return mTiffStream.read(buffer);
+    }
+
+    public String readString(int n) throws IOException {
+        byte[] buf = new byte[n];
+        mTiffStream.readOrThrow(buf);
+        return new String(buf, 0, n - 1, "UTF8");
+    }
+
+    public String readString(int n, Charset charset) throws IOException {
+        byte[] buf = new byte[n];
+        mTiffStream.readOrThrow(buf);
+        return new String(buf, 0, n - 1, charset);
+    }
+
+    public int readUnsignedShort() throws IOException {
+        return readShort() & 0xffff;
+    }
+
+    public long readUnsignedInt() throws IOException {
+        return readInt() & 0xffffffffL;
+    }
+
+    public Rational readUnsignedRational() throws IOException {
+        long nomi = readUnsignedInt();
+        long denomi = readUnsignedInt();
+        return new Rational(nomi, denomi);
+    }
+
+    public int readInt() throws IOException {
+        return mTiffStream.readInt();
+    }
+
+    public short readShort() throws IOException {
+        return mTiffStream.readShort();
+    }
+
+    public Rational readRational() throws IOException {
+        int nomi = readInt();
+        int denomi = readInt();
+        return new Rational(nomi, denomi);
+    }
+
+    private static boolean isSubIfdTag(short tagId) {
+        return tagId == EXIF_IDF || tagId == GPS_IDF || tagId == INTEROPERABILITY_IDF;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/Rational.java b/src/com/android/gallery3d/exif/Rational.java
new file mode 100644
index 0000000..cef6c91
--- /dev/null
+++ b/src/com/android/gallery3d/exif/Rational.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class Rational {
+
+    private final long mNominator;
+    private final long mDenominator;
+
+    public Rational(long nominator, long denominator) {
+        mNominator = nominator;
+        mDenominator = denominator;
+    }
+
+    public long getNominator() {
+        return mNominator;
+    }
+
+    public long getDenominator() {
+        return mDenominator;
+    }
+}
diff --git a/src/com/android/gallery3d/exif/TiffInputStream.java b/src/com/android/gallery3d/exif/TiffInputStream.java
new file mode 100644
index 0000000..2b0054c
--- /dev/null
+++ b/src/com/android/gallery3d/exif/TiffInputStream.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2012 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 java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+class TiffInputStream extends FilterInputStream {
+
+    private int mCount = 0;
+
+    // allocate a byte buffer for a long value;
+    private final byte mByteArray[] = new byte[8];
+    private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
+
+    protected TiffInputStream(InputStream in) {
+        super(in);
+    }
+
+    public int getReadByteCount() {
+        return mCount;
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return read(b, 0, b.length);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int r = super.read(b, off, len);
+        mCount += (r >= 0) ? r : 0;
+        return r;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int r = super.read();
+        mCount += (r >= 0) ? 1 : 0;
+        return r;
+    }
+
+    @Override
+    public long skip(long length) throws IOException {
+        long skip = super.skip(length);
+        mCount += skip;
+        return skip;
+    }
+
+    public void skipOrThrow(long length) throws IOException {
+        if (skip(length) != length) throw new EOFException();
+    }
+
+    public void skipTo(long target) throws IOException {
+        long cur = mCount;
+        long diff = target - cur;
+        assert(diff >= 0);
+        skipOrThrow(diff);
+    }
+
+    public void readOrThrow(byte[] b, int off, int len) throws IOException {
+        int r = read(b, off, len);
+        if (r != len) throw new EOFException();
+    }
+
+    public void readOrThrow(byte[] b) throws IOException {
+        readOrThrow(b, 0, b.length);
+    }
+
+    public void setByteOrder(ByteOrder order) {
+        mByteBuffer.order(order);
+    }
+
+    public ByteOrder getByteOrder() {
+        return mByteBuffer.order();
+    }
+
+    public short readShort() throws IOException {
+        readOrThrow(mByteArray, 0 ,2);
+        mByteBuffer.rewind();
+        return mByteBuffer.getShort();
+    }
+
+    public int readUnsignedShort() throws IOException {
+        return readShort() & 0xffff;
+    }
+
+    public int readInt() throws IOException {
+        readOrThrow(mByteArray, 0 , 4);
+        mByteBuffer.rewind();
+        return mByteBuffer.getInt();
+    }
+
+    public long readUnsignedInt() throws IOException {
+        return readInt() & 0xffffffffL;
+    }
+
+    public long readLong() throws IOException {
+        readOrThrow(mByteArray, 0 , 8);
+        mByteBuffer.rewind();
+        return mByteBuffer.getLong();
+    }
+
+    public String readString(int n) throws IOException {
+        byte buf[] = new byte[n];
+        readOrThrow(buf);
+        return new String(buf, "UTF8");
+    }
+
+    public String readString(int n, Charset charset) throws IOException {
+        byte buf[] = new byte[n];
+        readOrThrow(buf);
+        return new String(buf, charset);
+    }
+}
\ No newline at end of file