Updating the ExifParsing code

> Using system implementation of ExifInterface to read orientation
  > For inputstream, creating a temporary file with just the header
    since the system API only supports file input

Change-Id: I19c94ff28e9d9bac14cd9b717de0ff165ba95595
This commit is contained in:
Sunny Goyal
2015-08-18 17:31:54 -07:00
parent c268148f06
commit 2563b3b956
20 changed files with 174 additions and 6133 deletions
@@ -21,8 +21,6 @@ import android.content.res.Resources;
import android.net.Uri;
import android.util.Log;
import com.android.gallery3d.exif.ExifInterface;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -42,38 +40,26 @@ public class BitmapUtils {
}
public static int getRotationFromExif(Context context, Uri uri) {
return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri);
return BitmapUtils.getRotationFromExifHelper(null, 0, uri, context);
}
public static int getRotationFromExif(Resources res, int resId) {
return BitmapUtils.getRotationFromExifHelper(res, resId, null, null);
public static int getRotationFromExif(Resources res, int resId, Context context) {
return BitmapUtils.getRotationFromExifHelper(res, resId, null, context);
}
private static int getRotationFromExifHelper(Resources res, int resId, Context context, Uri uri) {
ExifInterface ei = new ExifInterface();
private static int getRotationFromExifHelper(Resources res, int resId,
Uri uri, Context context) {
InputStream is = null;
BufferedInputStream bis = null;
try {
if (uri != null) {
is = context.getContentResolver().openInputStream(uri);
bis = new BufferedInputStream(is);
ei.readExif(bis);
} else {
is = res.openRawResource(resId);
bis = new BufferedInputStream(is);
ei.readExif(bis);
}
Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
if (ori != null) {
return ExifInterface.getRotationForOrientationValue(ori.shortValue());
}
} catch (IOException e) {
Log.w(TAG, "Getting exif data failed", e);
} catch (NullPointerException e) {
// Sometimes the ExifInterface has an internal NPE if Exif data isn't valid
return ExifOrientation.readRotation(new BufferedInputStream(is), context);
} catch (IOException | NullPointerException e) {
Log.w(TAG, "Getting exif data failed", e);
} finally {
Utils.closeSilently(bis);
Utils.closeSilently(is);
}
return 0;
@@ -0,0 +1,145 @@
/**
* Copyright (C) 2015 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.common;
import android.content.Context;
import android.media.ExifInterface;
import android.util.Log;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ExifOrientation {
private static final String TAG = "ExifOrientation";
private static final boolean DEBUG = false;
private static final short SOI = (short) 0xFFD8; // start of input
private static final short APP0 = (short) 0xFFE0;
private static final short APPF = (short) 0xFFEF;
private static final short APP1 = (short) 0xFFE1;
private static final short SOS = (short) 0xFFDA; // start of stream
private static final short EOI = (short) 0xFFD9; // end of input
// The header is available in first 64 bytes, so reading upto 128 bytes
// should be more than enough.
private static final int MAX_BYTES_TO_READ = 128 * 1024;
/**
* Parses the rotation of the JPEG image from the input stream.
*/
public static final int readRotation(InputStream in, Context context) {
// Since the platform implementation only takes file input, create a temporary file
// with just the image header.
File tempFile = null;
DataOutputStream tempOut = null;
try {
DataInputStream din = new DataInputStream(in);
int pos = 0;
if (din.readShort() == SOI) {
pos += 2;
short marker = din.readShort();
pos += 2;
while ((marker >= APP0 && marker <= APPF) && pos < MAX_BYTES_TO_READ) {
int length = din.readUnsignedShort();
if (length < 2) {
throw new IOException("Invalid header size");
}
// We only want APP1 headers
if (length > 2) {
if (marker == APP1) {
// Copy the header
if (tempFile == null) {
tempFile = File.createTempFile(TAG, ".jpg", context.getCacheDir());
tempOut = new DataOutputStream(new FileOutputStream(tempFile));
tempOut.writeShort(SOI);
}
tempOut.writeShort(marker);
tempOut.writeShort(length);
byte[] header = new byte[length - 2];
din.read(header);
tempOut.write(header);
} else {
din.skip(length - 2);
}
}
pos += length;
marker = din.readShort();
pos += 2;
}
if (tempOut != null) {
// Write empty image data.
tempOut.writeShort(SOS);
// Write the frame size as 2. Since this includes the size bytes as well
// (short = 2 bytes), it implies there is 0 byte of image data.
tempOut.writeShort(2);
// End of input
tempOut.writeShort(EOI);
tempOut.close();
return readRotation(tempFile.getAbsolutePath());
}
}
} catch (IOException e) {
if (DEBUG) {
Log.d(TAG, "Error parsing input stream", e);
}
} finally {
Utils.closeSilently(in);
Utils.closeSilently(tempOut);
if (tempFile != null) {
tempFile.delete();
}
}
return 0;
}
/**
* Parses the rotation of the JPEG image.
*/
public static final int readRotation(String filePath) {
try {
ExifInterface exif = new ExifInterface(filePath);
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
default:
return 0;
}
} catch (IOException e) {
if (DEBUG) {
Log.d(TAG, "Error reading file", e);
}
}
return 0;
}
}
@@ -1,48 +0,0 @@
/*
* 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.InputStream;
import java.nio.ByteBuffer;
class ByteBufferInputStream extends InputStream {
private ByteBuffer mBuf;
public ByteBufferInputStream(ByteBuffer buf) {
mBuf = buf;
}
@Override
public int read() {
if (!mBuf.hasRemaining()) {
return -1;
}
return mBuf.get() & 0xFF;
}
@Override
public int read(byte[] bytes, int off, int len) {
if (!mBuf.hasRemaining()) {
return -1;
}
len = Math.min(len, mBuf.remaining());
mBuf.get(bytes, off, len);
return len;
}
}
@@ -1,136 +0,0 @@
/*
* 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 CountedDataInputStream 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 CountedDataInputStream(InputStream in) {
super(in);
}
public int getReadByteCount() {
return mCount;
}
@Override
public int read(byte[] b) throws IOException {
int r = in.read(b);
mCount += (r >= 0) ? r : 0;
return r;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int r = in.read(b, off, len);
mCount += (r >= 0) ? r : 0;
return r;
}
@Override
public int read() throws IOException {
int r = in.read();
mCount += (r >= 0) ? 1 : 0;
return r;
}
@Override
public long skip(long length) throws IOException {
long skip = in.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);
}
}
@@ -1,348 +0,0 @@
/*
* 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 android.util.Log;
import java.io.UnsupportedEncodingException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
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
*/
class ExifData {
private static final String TAG = "ExifData";
private static final byte[] USER_COMMENT_ASCII = {
0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
};
private static final byte[] USER_COMMENT_JIS = {
0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final byte[] USER_COMMENT_UNICODE = {
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;
ExifData(ByteOrder order) {
mByteOrder = order;
}
/**
* Gets the compressed thumbnail. Returns null if there is no compressed
* thumbnail.
*
* @see #hasCompressedThumbnail()
*/
protected byte[] getCompressedThumbnail() {
return mThumbnail;
}
/**
* Sets the compressed thumbnail.
*/
protected void setCompressedThumbnail(byte[] thumbnail) {
mThumbnail = thumbnail;
}
/**
* Returns true it this header contains a compressed thumbnail.
*/
protected boolean hasCompressedThumbnail() {
return mThumbnail != null;
}
/**
* Adds an uncompressed strip.
*/
protected void setStripBytes(int index, byte[] strip) {
if (index < mStripBytes.size()) {
mStripBytes.set(index, strip);
} else {
for (int i = mStripBytes.size(); i < index; i++) {
mStripBytes.add(null);
}
mStripBytes.add(strip);
}
}
/**
* Gets the strip count.
*/
protected int getStripCount() {
return mStripBytes.size();
}
/**
* Gets the strip at the specified index.
*
* @exceptions #IndexOutOfBoundException
*/
protected byte[] getStrip(int index) {
return mStripBytes.get(index);
}
/**
* Returns true if this header contains uncompressed strip.
*/
protected boolean hasUncompressedStrip() {
return mStripBytes.size() != 0;
}
/**
* Gets the byte order.
*/
protected ByteOrder getByteOrder() {
return mByteOrder;
}
/**
* Returns the {@link IfdData} object corresponding to a given IFD if it
* exists or null.
*/
protected IfdData getIfdData(int ifdId) {
if (ExifTag.isValidIfd(ifdId)) {
return mIfdDatas[ifdId];
}
return null;
}
/**
* Adds IFD data. If IFD data of the same type already exists, it will be
* replaced by the new data.
*/
protected void addIfdData(IfdData data) {
mIfdDatas[data.getId()] = data;
}
/**
* Returns the {@link IfdData} object corresponding to a given IFD or
* generates one if none exist.
*/
protected IfdData getOrCreateIfdData(int ifdId) {
IfdData ifdData = mIfdDatas[ifdId];
if (ifdData == null) {
ifdData = new IfdData(ifdId);
mIfdDatas[ifdId] = ifdData;
}
return ifdData;
}
/**
* Returns the tag with a given TID in the given IFD if the tag exists.
* Otherwise returns null.
*/
protected ExifTag getTag(short tag, int ifd) {
IfdData ifdData = mIfdDatas[ifd];
return (ifdData == null) ? null : ifdData.getTag(tag);
}
/**
* Adds the given ExifTag to its default IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
protected ExifTag addTag(ExifTag tag) {
if (tag != null) {
int ifd = tag.getIfd();
return addTag(tag, ifd);
}
return null;
}
/**
* Adds the given ExifTag to the given IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
protected ExifTag addTag(ExifTag tag, int ifdId) {
if (tag != null && ExifTag.isValidIfd(ifdId)) {
IfdData ifdData = getOrCreateIfdData(ifdId);
return ifdData.setTag(tag);
}
return null;
}
protected void clearThumbnailAndStrips() {
mThumbnail = null;
mStripBytes.clear();
}
/**
* Removes the thumbnail and its related tags. IFD1 will be removed.
*/
protected void removeThumbnailData() {
clearThumbnailAndStrips();
mIfdDatas[IfdId.TYPE_IFD_1] = null;
}
/**
* Removes the tag with a given TID and IFD.
*/
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.
*/
protected String getUserComment() {
IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
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);
byte[] code = new byte[8];
System.arraycopy(buf, 0, code, 0, 8);
try {
if (Arrays.equals(code, USER_COMMENT_ASCII)) {
return new String(buf, 8, buf.length - 8, "US-ASCII");
} else if (Arrays.equals(code, USER_COMMENT_JIS)) {
return new String(buf, 8, buf.length - 8, "EUC-JP");
} else if (Arrays.equals(code, USER_COMMENT_UNICODE)) {
return new String(buf, 8, buf.length - 8, "UTF-16");
} else {
return null;
}
} catch (UnsupportedEncodingException e) {
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;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,23 +0,0 @@
/*
* 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);
}
}
@@ -1,195 +0,0 @@
/*
* 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 android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
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 {
final int mOffset;
final ExifTag mTag;
TagOffset(ExifTag tag, int offset) {
mTag = tag;
mOffset = offset;
}
}
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, mInterface);
mTagToModified = new ExifData(parser.getByteOrder());
mOffsetBase += parser.getTiffStartPosition();
mByteBuffer.position(0);
} finally {
ExifInterface.closeSilently(is);
}
}
protected ByteOrder getByteOrder() {
return mTagToModified.getByteOrder();
}
protected boolean commit() throws IOException, ExifInvalidFormatException {
InputStream is = null;
try {
is = new ByteBufferInputStream(mByteBuffer);
int flag = 0;
IfdData[] ifdDatas = new IfdData[] {
mTagToModified.getIfdData(IfdId.TYPE_IFD_0),
mTagToModified.getIfdData(IfdId.TYPE_IFD_1),
mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF),
mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY),
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_INTEROPERABILITY] != null) {
flag |= ExifParser.OPTION_IFD_INTEROPERABILITY;
}
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();
}
break;
case ExifParser.EVENT_NEW_TAG:
ExifTag oldTag = parser.getTag();
ExifTag newTag = currIfd.getTag(oldTag.getTagId());
if (newTag != null) {
if (newTag.getComponentCount() != oldTag.getComponentCount()
|| newTag.getDataType() != oldTag.getDataType()) {
return false;
} else {
mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset()));
currIfd.removeTag(oldTag.getTagId());
if (currIfd.getTagCount() == 0) {
parser.skipRemainingTagsInCurrentIfd();
}
}
}
break;
}
event = parser.next();
}
for (IfdData ifd : ifdDatas) {
if (ifd != null && ifd.getTagCount() > 0) {
return false;
}
}
modify();
} finally {
ExifInterface.closeSilently(is);
}
return true;
}
private void modify() {
mByteBuffer.order(getByteOrder());
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:
byte buf[] = tag.getStringByte();
if (buf.length == tag.getComponentCount()) {
buf[buf.length - 1] = 0;
mByteBuffer.put(buf);
} else {
mByteBuffer.put(buf);
mByteBuffer.put((byte) 0);
}
break;
case ExifTag.TYPE_LONG:
case ExifTag.TYPE_UNSIGNED_LONG:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
mByteBuffer.putInt((int) tag.getValueAt(i));
}
break;
case ExifTag.TYPE_RATIONAL:
case ExifTag.TYPE_UNSIGNED_RATIONAL:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
Rational v = tag.getRational(i);
mByteBuffer.putInt((int) v.getNumerator());
mByteBuffer.putInt((int) v.getDenominator());
}
break;
case ExifTag.TYPE_UNDEFINED:
case ExifTag.TYPE_UNSIGNED_BYTE:
buf = new byte[tag.getComponentCount()];
tag.getBytes(buf);
mByteBuffer.put(buf);
break;
case ExifTag.TYPE_UNSIGNED_SHORT:
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
mByteBuffer.putShort((short) tag.getValueAt(i));
}
break;
}
}
public void modifyTag(ExifTag tag) {
mTagToModified.addTag(tag);
}
}
@@ -1,518 +0,0 @@
/*
* 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 android.util.Log;
import java.io.BufferedOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
/**
* 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) {
* e.printStackTrace();
* } catch (IOException e) {
* e.printStackTrace();
* } finally {
* if (os != null) {
* try {
* os.close();
* } catch (IOException e) {
* e.printStackTrace();
* }
* }
* }
* }
* </pre>
*/
class ExifOutputStream extends FilterOutputStream {
private static final String TAG = "ExifOutputStream";
private static final boolean DEBUG = false;
private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
private static final int STATE_SOI = 0;
private static final int STATE_FRAME_HEADER = 1;
private static final int STATE_JPEG_DATA = 2;
private static final int EXIF_HEADER = 0x45786966;
private static final short TIFF_HEADER = 0x002A;
private static final short TIFF_BIG_ENDIAN = 0x4d4d;
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 byte[] mSingleByteArray = new byte[1];
private ByteBuffer mBuffer = ByteBuffer.allocate(4);
private final ExifInterface mInterface;
protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
mInterface = iRef;
}
/**
* Sets the ExifData to be written into the JPEG file. Should be called
* before writing image data.
*/
protected void setExifData(ExifData exifData) {
mExifData = exifData;
}
/**
* Gets the Exif header to be written into the JPEF file.
*/
protected ExifData getExifData() {
return mExifData;
}
private int requestByteToBuffer(int requestByteCount, byte[] buffer
, int offset, int length) {
int byteNeeded = requestByteCount - mBuffer.position();
int byteToRead = length > byteNeeded ? byteNeeded : length;
mBuffer.put(buffer, offset, byteToRead);
return byteToRead;
}
/**
* 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)
&& length > 0) {
if (mByteToSkip > 0) {
int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
length -= byteToProcess;
mByteToSkip -= byteToProcess;
offset += byteToProcess;
}
if (mByteToCopy > 0) {
int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
out.write(buffer, offset, byteToProcess);
length -= byteToProcess;
mByteToCopy -= byteToProcess;
offset += byteToProcess;
}
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;
}
mBuffer.rewind();
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.
byteRead = requestByteToBuffer(4, buffer, offset, length);
offset += byteRead;
length -= byteRead;
// Check if this image data doesn't contain SOF.
if (mBuffer.position() == 2) {
short tag = mBuffer.getShort();
if (tag == JpegHeader.EOI) {
out.write(mBuffer.array(), 0, 2);
mBuffer.rewind();
}
}
if (mBuffer.position() < 4) {
return;
}
mBuffer.rewind();
short marker = mBuffer.getShort();
if (marker == JpegHeader.APP1) {
mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
mState = STATE_JPEG_DATA;
} else if (!JpegHeader.isSofMarker(marker)) {
out.write(mBuffer.array(), 0, 4);
mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
} else {
out.write(mBuffer.array(), 0, 4);
mState = STATE_JPEG_DATA;
}
mBuffer.rewind();
}
}
if (length > 0) {
out.write(buffer, offset, length);
}
}
/**
* 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 {
mSingleByteArray[0] = (byte) (0xff & oneByte);
write(mSingleByteArray);
}
/**
* Equivalent to calling write(buffer, 0, buffer.length).
*/
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
private void writeExifData() throws IOException {
if (mExifData == null) {
return;
}
if (DEBUG) {
Log.v(TAG, "Writing exif data...");
}
ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
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);
dataOutputStream.writeShort((short) (exifSize + 8));
dataOutputStream.writeInt(EXIF_HEADER);
dataOutputStream.writeShort((short) 0x0000);
if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
} else {
dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
}
dataOutputStream.setByteOrder(mExifData.getByteOrder());
dataOutputStream.writeShort(TIFF_HEADER);
dataOutputStream.writeInt(8);
writeAllTags(dataOutputStream);
writeThumbnail(dataOutputStream);
for (ExifTag t : nullTags) {
mExifData.addTag(t);
}
}
private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
for(ExifTag t : data.getAllTags()) {
if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
data.removeTag(t.getTagId(), t.getIfd());
nullTags.add(t);
}
}
return nullTags;
}
private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
if (mExifData.hasCompressedThumbnail()) {
dataOutputStream.write(mExifData.getCompressedThumbnail());
} else if (mExifData.hasUncompressedStrip()) {
for (int i = 0; i < mExifData.getStripCount(); i++) {
dataOutputStream.write(mExifData.getStrip(i));
}
}
}
private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
if (interoperabilityIfd != null) {
writeIfd(interoperabilityIfd, dataOutputStream);
}
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
if (gpsIfd != null) {
writeIfd(gpsIfd, dataOutputStream);
}
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
if (ifd1 != null) {
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
}
}
private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
throws IOException {
ExifTag[] tags = ifd.getAllTags();
dataOutputStream.writeShort((short) tags.length);
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 {
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) {
if (tag.getDataSize() > 4) {
ExifOutputStream.writeTagValue(tag, dataOutputStream);
}
}
}
private int calculateOffsetOfIfd(IfdData ifd, int offset) {
offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
ExifTag[] tags = ifd.getAllTags();
for (ExifTag tag : tags) {
if (tag.getDataSize() > 4) {
tag.setOffset(offset);
offset += tag.getDataSize();
}
}
return offset;
}
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 = 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 files.
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
if (exifIfd == null) {
exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
mExifData.addIfdData(exifIfd);
}
// GPS IFD
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
if (gpsIfd != null) {
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 = 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);
}
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
// thumbnail
if (mExifData.hasCompressedThumbnail()) {
if (ifd1 == null) {
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
mExifData.addIfdData(ifd1);
}
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 = 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);
// 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 = 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;
}
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 if (ifd1 != null) {
// 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));
}
}
private int calculateAllOffset() {
int offset = TIFF_HEADER_SIZE;
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
offset = calculateOffsetOfIfd(ifd0, 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(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(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
offset = calculateOffsetOfIfd(gpsIfd, offset);
}
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
if (ifd1 != null) {
ifd0.setOffsetToNextIfd(offset);
offset = calculateOffsetOfIfd(ifd1, offset);
}
// thumbnail
if (mExifData.hasCompressedThumbnail()) {
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
.setValue(offset);
offset += mExifData.getCompressedThumbnail().length;
} 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(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
offsets);
}
return offset;
}
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;
}
}
}
@@ -1,916 +0,0 @@
/*
* 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 android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
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.
* <p>
* 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,
* ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
* int event = parser.next();
* while (event != ExifParser.EVENT_END) {
* switch (event) {
* case ExifParser.EVENT_START_OF_IFD:
* break;
* case ExifParser.EVENT_NEW_TAG:
* ExifTag tag = parser.getTag();
* if (!tag.hasValue()) {
* parser.registerForTagValue(tag);
* } else {
* processTag(tag);
* }
* break;
* case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
* tag = parser.getTag();
* if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
* processTag(tag);
* }
* break;
* }
* event = parser.next();
* }
* }
*
* void processTag(ExifTag tag) {
* // process the tag as you like.
* }
* </pre>
*/
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.
*/
public static final int EVENT_START_OF_IFD = 0;
/**
* When the parser reaches a new tag. Call {@link #getTag()}to get the
* corresponding tag.
*/
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.
*/
public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
/**
* When the parser reaches the compressed image area.
*/
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.
*
* @see #getStripIndex()
* @see #getStripCount()
*/
public static final int EVENT_UNCOMPRESSED_STRIP = 4;
/**
* When there is nothing more to parse.
*/
public static final int EVENT_END = 5;
/**
* Option bit to request to parse IFD0.
*/
public static final int OPTION_IFD_0 = 1 << 0;
/**
* Option bit to request to parse IFD1.
*/
public static final int OPTION_IFD_1 = 1 << 1;
/**
* Option bit to request to parse Exif-IFD.
*/
public static final int OPTION_IFD_EXIF = 1 << 2;
/**
* Option bit to request to parse GPS-IFD.
*/
public static final int OPTION_IFD_GPS = 1 << 3;
/**
* Option bit to request to parse Interoperability-IFD.
*/
public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
/**
* Option bit to request to parse thumbnail.
*/
public static final int OPTION_THUMBNAIL = 1 << 5;
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
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;
protected static final int TAG_SIZE = 12;
protected static final int OFFSET_SIZE = 2;
private static final Charset US_ASCII = Charset.forName("US-ASCII");
protected static final int DEFAULT_IFD0_OFFSET = 8;
private final CountedDataInputStream mTiffStream;
private final int mOptions;
private int mIfdStartOffset = 0;
private int mNumOfTagInIfd = 0;
private int mIfdType;
private ExifTag mTag;
private ImageEvent mImageEvent;
private int mStripCount;
private ExifTag mStripSizeTag;
private ExifTag mJpegSizeTag;
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>();
private boolean isIfdRequested(int ifdType) {
switch (ifdType) {
case IfdId.TYPE_IFD_0:
return (mOptions & OPTION_IFD_0) != 0;
case IfdId.TYPE_IFD_1:
return (mOptions & OPTION_IFD_1) != 0;
case IfdId.TYPE_IFD_EXIF:
return (mOptions & OPTION_IFD_EXIF) != 0;
case IfdId.TYPE_IFD_GPS:
return (mOptions & OPTION_IFD_GPS) != 0;
case IfdId.TYPE_IFD_INTEROPERABILITY:
return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
}
return false;
}
private boolean isThumbnailRequested() {
return (mOptions & OPTION_THUMBNAIL) != 0;
}
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;
}
parseTiffHeader();
long offset = mTiffStream.readUnsignedInt();
if (offset > Integer.MAX_VALUE) {
throw new ExifInvalidFormatException("Invalid offset " + offset);
}
mIfd0Position = (int) offset;
mIfdType = IfdId.TYPE_IFD_0;
if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
registerIfd(IfdId.TYPE_IFD_0, offset);
if (offset != DEFAULT_IFD0_OFFSET) {
mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
read(mDataAboveIfd0);
}
}
}
/**
* Parses the the given InputStream with the given options
*
* @exception IOException
* @exception ExifInvalidFormatException
*/
protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
throws IOException, ExifInvalidFormatException {
return new ExifParser(inputStream, options, iRef);
}
/**
* 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)
*/
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, iRef);
}
/**
* Moves the parser forward and returns the next parsing event
*
* @exception IOException
* @exception ExifInvalidFormatException
* @see #EVENT_START_OF_IFD
* @see #EVENT_NEW_TAG
* @see #EVENT_VALUE_OF_REGISTERED_TAG
* @see #EVENT_COMPRESSED_IMAGE
* @see #EVENT_UNCOMPRESSED_STRIP
* @see #EVENT_END
*/
protected int next() throws IOException, ExifInvalidFormatException {
if (!mContainExifData) {
return EVENT_END;
}
int offset = mTiffStream.getReadByteCount();
int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
if (offset < endOfTags) {
mTag = readTag();
if (mTag == null) {
return next();
}
if (mNeedToParseOffsetsInCurrentIfd) {
checkOffsetOrImageTag(mTag);
}
return EVENT_NEW_TAG;
} else if (offset == endOfTags) {
// There is a link to ifd1 at the end of ifd0
if (mIfdType == IfdId.TYPE_IFD_0) {
long ifdOffset = readUnsignedLong();
if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
if (ifdOffset != 0) {
registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
}
}
} else {
int offsetSize = 4;
// Some camera models use invalid length of the offset
if (mCorrespondingEvent.size() > 0) {
offsetSize = mCorrespondingEvent.firstEntry().getKey() -
mTiffStream.getReadByteCount();
}
if (offsetSize < 4) {
Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize);
} else {
long ifdOffset = readUnsignedLong();
if (ifdOffset != 0) {
Log.w(TAG, "Invalid link to next IFD: " + ifdOffset);
}
}
}
}
while (mCorrespondingEvent.size() != 0) {
Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
Object event = entry.getValue();
try {
skipTo(entry.getKey());
} catch (IOException e) {
Log.w(TAG, "Failed to skip to data at: " + entry.getKey() +
" for " + event.getClass().getName() + ", the file may be broken.");
continue;
}
if (event instanceof IfdEvent) {
mIfdType = ((IfdEvent) event).ifd;
mNumOfTagInIfd = mTiffStream.readUnsignedShort();
mIfdStartOffset = entry.getKey();
if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
Log.w(TAG, "Invalid size of IFD " + mIfdType);
return EVENT_END;
}
mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
if (((IfdEvent) event).isRequested) {
return EVENT_START_OF_IFD;
} else {
skipRemainingTagsInCurrentIfd();
}
} else if (event instanceof ImageEvent) {
mImageEvent = (ImageEvent) event;
return mImageEvent.type;
} else {
ExifTagEvent tagEvent = (ExifTagEvent) event;
mTag = tagEvent.tag;
if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
readFullTagValue(mTag);
checkOffsetOrImageTag(mTag);
}
if (tagEvent.isRequested) {
return EVENT_VALUE_OF_REGISTERED_TAG;
}
}
}
return EVENT_END;
}
/**
* Skips the tags area of current IFD, if the parser is not in the tag area,
* nothing will happen.
*
* @throws IOException
* @throws ExifInvalidFormatException
*/
protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
int offset = mTiffStream.getReadByteCount();
if (offset > endOfTags) {
return;
}
if (mNeedToParseOffsetsInCurrentIfd) {
while (offset < endOfTags) {
mTag = readTag();
offset += TAG_SIZE;
if (mTag == null) {
continue;
}
checkOffsetOrImageTag(mTag);
}
} else {
skipTo(endOfTags);
}
long ifdOffset = readUnsignedLong();
// For ifd0, there is a link to ifd1 in the end of all tags
if (mIfdType == IfdId.TYPE_IFD_0
&& (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
if (ifdOffset > 0) {
registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
}
}
}
private boolean needToParseOffsetsInCurrentIfd() {
switch (mIfdType) {
case IfdId.TYPE_IFD_0:
return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS)
|| isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
|| isIfdRequested(IfdId.TYPE_IFD_1);
case IfdId.TYPE_IFD_1:
return isThumbnailRequested();
case IfdId.TYPE_IFD_EXIF:
// The offset to interoperability IFD is located in Exif IFD
return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
default:
return false;
}
}
/**
* 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.
* <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.
*
* @see #registerForTagValue(ExifTag)
* @see #read(byte[])
* @see #read(byte[], int, int)
* @see #readLong()
* @see #readRational()
* @see #readString(int)
* @see #readString(int, Charset)
*/
protected ExifTag getTag() {
return mTag;
}
/**
* Gets number of tags in the current IFD area.
*/
protected int getTagCountInCurrentIfd() {
return mNumOfTagInIfd;
}
/**
* Gets the ID of current IFD.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
* @see IfdId#TYPE_IFD_EXIF
*/
protected int getCurrentIfd() {
return mIfdType;
}
/**
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
* get the index of this strip.
*
* @see #getStripCount()
*/
protected int getStripIndex() {
return mImageEvent.stripIndex;
}
/**
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
* get the number of strip data.
*
* @see #getStripIndex()
*/
protected int getStripCount() {
return mStripCount;
}
/**
* When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
* get the strip size.
*/
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.
*/
protected int getCompressedImageSize() {
if (mJpegSizeTag == null) {
return 0;
}
return (int) mJpegSizeTag.getValueAt(0);
}
private void skipTo(int offset) throws IOException {
mTiffStream.skipTo(offset);
while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
mCorrespondingEvent.pollFirstEntry();
}
}
/**
* 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
*/
protected void registerForTagValue(ExifTag tag) {
if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
}
}
private void registerIfd(int ifdType, long offset) {
// Cast unsigned int to int since the offset is always smaller
// than the size of APP1 (65536)
mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
}
private void registerCompressedImage(long offset) {
mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
}
private void registerUncompressedStrip(int stripIndex, long offset) {
mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP
, stripIndex));
}
private 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");
}
// Some invalid image file contains invalid data type. Ignore those tags
if (!ExifTag.isValidType(dataFormat)) {
Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat));
mTiffStream.skip(4);
return null;
}
// 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();
if (offset > Integer.MAX_VALUE) {
throw new ExifInvalidFormatException(
"offset is larger then Integer.MAX_VALUE");
}
// Some invalid images put some undefined data before IFD0.
// Read the data here.
if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
byte[] buf = new byte[(int) numOfComp];
System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET,
buf, 0, (int) numOfComp);
tag.setValue(buf);
} else {
tag.setOffset((int) offset);
}
} else {
boolean defCount = tag.hasDefinedCount();
// Set defined count to 0 so we can add \0 to non-terminated strings
tag.setHasDefinedCount(false);
// Read value
readFullTagValue(tag);
tag.setHasDefinedCount(defCount);
mTiffStream.skip(4 - dataSize);
// Set the offset to the position of value.
tag.setOffset(mTiffStream.getReadByteCount() - 4);
}
return tag;
}
/**
* 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;
}
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));
}
}
} 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);
}
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) {
Object event = mCorrespondingEvent.firstEntry().getValue();
if (event instanceof ImageEvent) {
// Tag value overlaps thumbnail, ignore thumbnail.
Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString());
Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey());
} else {
// Tag value overlaps another tag, shorten count
if (event instanceof IfdEvent) {
Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd
+ " overlaps value for tag: \n" + tag.toString());
} else if (event instanceof ExifTagEvent) {
Log.w(TAG, "Tag value for tag: \n"
+ ((ExifTagEvent) event).tag.toString()
+ " overlaps value for tag: \n" + tag.toString());
}
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()) {
case ExifTag.TYPE_UNSIGNED_BYTE:
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);
}
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);
}
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);
}
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);
}
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);
}
break;
}
if (LOGV) {
Log.v(TAG, "\n" + tag.toString());
}
}
private void parseTiffHeader() throws IOException,
ExifInvalidFormatException {
short byteOrder = mTiffStream.readShort();
if (LITTLE_ENDIAN_TAG == byteOrder) {
mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
} else if (BIG_ENDIAN_TAG == byteOrder) {
mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
} else {
throw new ExifInvalidFormatException("Invalid TIFF header");
}
if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
throw new ExifInvalidFormatException("Invalid TIFF header");
}
}
private boolean seekTiffData(InputStream inputStream) throws IOException,
ExifInvalidFormatException {
CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
if (dataStream.readShort() != JpegHeader.SOI) {
throw new ExifInvalidFormatException("Invalid JPEG format");
}
short marker = dataStream.readShort();
while (marker != JpegHeader.EOI
&& !JpegHeader.isSofMarker(marker)) {
int length = dataStream.readUnsignedShort();
// Some invalid formatted image contains multiple APP1,
// try to find the one with Exif data.
if (marker == JpegHeader.APP1) {
int header = 0;
short headerTail = 0;
if (length >= 8) {
header = dataStream.readInt();
headerTail = dataStream.readShort();
length -= 6;
if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
mTiffStartPosition = dataStream.getReadByteCount();
mApp1End = length;
mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End;
return true;
}
}
}
if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
Log.w(TAG, "Invalid JPEG format.");
return false;
}
marker = dataStream.readShort();
}
return false;
}
protected int getOffsetToExifEndFromSOF() {
return mOffsetToApp1EndFromSOF;
}
protected int getTiffStartPosition() {
return mTiffStartPosition;
}
/**
* Reads bytes from the InputStream.
*/
protected int read(byte[] buffer, int offset, int length) throws IOException {
return mTiffStream.read(buffer, offset, length);
}
/**
* Equivalent to read(buffer, 0, buffer.length).
*/
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}.
*/
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}.
*/
protected String readString(int n, Charset charset) throws IOException {
if (n > 0) {
return mTiffStream.readString(n, charset);
} else {
return "";
}
}
/**
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the
* InputStream.
*/
protected int readUnsignedShort() throws IOException {
return mTiffStream.readShort() & 0xffff;
}
/**
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the
* InputStream.
*/
protected long readUnsignedLong() throws IOException {
return readLong() & 0xffffffffL;
}
/**
* Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the
* InputStream.
*/
protected Rational readUnsignedRational() throws IOException {
long nomi = readUnsignedLong();
long denomi = readUnsignedLong();
return new Rational(nomi, denomi);
}
/**
* Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream.
*/
protected int readLong() throws IOException {
return mTiffStream.readInt();
}
/**
* Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream.
*/
protected Rational readRational() throws IOException {
int nomi = readLong();
int denomi = readLong();
return new Rational(nomi, denomi);
}
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;
}
}
private static class IfdEvent {
int ifd;
boolean isRequested;
IfdEvent(int ifd, boolean isInterestedIfd) {
this.ifd = ifd;
this.isRequested = isInterestedIfd;
}
}
private static class ExifTagEvent {
ExifTag tag;
boolean isRequested;
ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
this.tag = tag;
this.isRequested = isRequireByUser;
}
}
/**
* Gets the byte order of the current InputStream.
*/
protected ByteOrder getByteOrder() {
return mTiffStream.getByteOrder();
}
}
@@ -1,92 +0,0 @@
/*
* 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 android.util.Log;
import java.io.IOException;
import java.io.InputStream;
/**
* This class reads the EXIF header of a JPEG file and stores it in
* {@link ExifData}.
*/
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}.
*
* @throws ExifInvalidFormatException
* @throws IOException
*/
protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
IOException {
ExifParser parser = ExifParser.parse(inputStream, mInterface);
ExifData exifData = new ExifData(parser.getByteOrder());
ExifTag tag = null;
int event = parser.next();
while (event != ExifParser.EVENT_END) {
switch (event) {
case ExifParser.EVENT_START_OF_IFD:
exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
break;
case ExifParser.EVENT_NEW_TAG:
tag = parser.getTag();
if (!tag.hasValue()) {
parser.registerForTagValue(tag);
} else {
exifData.getIfdData(tag.getIfd()).setTag(tag);
}
break;
case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
tag = parser.getTag();
if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
parser.readFullTagValue(tag);
}
exifData.getIfdData(tag.getIfd()).setTag(tag);
break;
case ExifParser.EVENT_COMPRESSED_IMAGE:
byte buf[] = new byte[parser.getCompressedImageSize()];
if (buf.length == parser.read(buf)) {
exifData.setCompressedThumbnail(buf);
} else {
Log.w(TAG, "Failed to read the compressed thumbnail");
}
break;
case ExifParser.EVENT_UNCOMPRESSED_STRIP:
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;
}
event = parser.next();
}
return exifData;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,152 +0,0 @@
/*
* 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.util.HashMap;
import java.util.Map;
/**
* This class stores all the tags in an IFD.
*
* @see ExifData
* @see ExifTag
*/
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.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_EXIF
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
*/
IfdData(int ifdId) {
mIfdId = ifdId;
}
static protected int[] getIfds() {
return sIfds;
}
/**
* Get a array the contains all {@link ExifTag} in this IFD.
*/
protected ExifTag[] getAllTags() {
return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
}
/**
* Gets the ID of this IFD.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_EXIF
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
*/
protected int getId() {
return mIfdId;
}
/**
* Gets the {@link ExifTag} with given tag id. Return null if there is no
* such tag.
*/
protected ExifTag getTag(short tagId) {
return mExifTags.get(tagId);
}
/**
* Adds or replaces a {@link ExifTag}.
*/
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
*/
protected void removeTag(short tagId) {
mExifTags.remove(tagId);
}
/**
* Gets the tags count in the IFD.
*/
protected int getTagCount() {
return mExifTags.size();
}
/**
* Sets the offset of next IFD.
*/
protected void setOffsetToNextIfd(int offset) {
mOffsetToNextIfd = offset;
}
/**
* Gets the offset of next IFD.
*/
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.
*/
@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 (ExifInterface.isOffsetTag(tag.getTagId())) {
continue;
}
ExifTag tag2 = mExifTags.get(tag.getTagId());
if (!tag.equals(tag2)) {
return false;
}
}
return true;
}
}
return false;
}
}
@@ -1,31 +0,0 @@
/*
* 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;
/**
* The constants of the IFD ID defined in EXIF spec.
*/
public interface IfdId {
public static final int TYPE_IFD_0 = 0;
public static final int TYPE_IFD_1 = 1;
public static final int TYPE_IFD_EXIF = 2;
public static final int TYPE_IFD_INTEROPERABILITY = 3;
public static final int TYPE_IFD_GPS = 4;
/* This is used in ExifData to allocate enough IfdData */
static final int TYPE_IFD_COUNT = 5;
}
@@ -1,39 +0,0 @@
/*
* 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;
class JpegHeader {
public static final short SOI = (short) 0xFFD8;
public static final short APP1 = (short) 0xFFE1;
public static final short APP0 = (short) 0xFFE0;
public static final short EOI = (short) 0xFFD9;
/**
* SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG,
* and DAC marker.
*/
public static final short SOF0 = (short) 0xFFC0;
public static final short SOF15 = (short) 0xFFCF;
public static final short DHT = (short) 0xFFC4;
public static final short JPG = (short) 0xFFC8;
public static final short DAC = (short) 0xFFCC;
public static final boolean isSofMarker(short marker) {
return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
&& marker != DAC;
}
}
@@ -1,56 +0,0 @@
/*
* 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.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
class OrderedDataOutputStream extends FilterOutputStream {
private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4);
public OrderedDataOutputStream(OutputStream out) {
super(out);
}
public OrderedDataOutputStream setByteOrder(ByteOrder order) {
mByteBuffer.order(order);
return this;
}
public OrderedDataOutputStream writeShort(short value) throws IOException {
mByteBuffer.rewind();
mByteBuffer.putShort(value);
out.write(mByteBuffer.array(), 0, 2);
return this;
}
public OrderedDataOutputStream writeInt(int value) throws IOException {
mByteBuffer.rewind();
mByteBuffer.putInt(value);
out.write(mByteBuffer.array());
return this;
}
public OrderedDataOutputStream writeRational(Rational rational) throws IOException {
writeInt((int) rational.getNumerator());
writeInt((int) rational.getDenominator());
return this;
}
}
@@ -1,88 +0,0 @@
/*
* 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;
/**
* 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 mNumerator;
private final long mDenominator;
/**
* Create a Rational with a given numerator and denominator.
*
* @param nominator
* @param denominator
*/
public Rational(long nominator, long denominator) {
mNumerator = nominator;
mDenominator = denominator;
}
/**
* Create a copy of a Rational.
*/
public Rational(Rational r) {
mNumerator = r.mNumerator;
mDenominator = r.mDenominator;
}
/**
* Gets the numerator of the rational.
*/
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 mNumerator == data.mNumerator && mDenominator == data.mDenominator;
}
return false;
}
@Override
public String toString() {
return mNumerator + "/" + mDenominator;
}
}
@@ -318,7 +318,7 @@ public class WallpaperCropActivity extends BaseActivity implements Handler.Callb
Resources res, int resId, final boolean finishActivityWhenDone) {
// crop this image and scale it down to the default wallpaper size for
// this device
int rotation = BitmapUtils.getRotationFromExif(res, resId);
int rotation = BitmapUtils.getRotationFromExif(res, resId, this);
Point inSize = mCropView.getSourceDimensions();
Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
getWindowManager());
@@ -200,8 +200,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
@Override
public void onClick(final WallpaperPickerActivity a) {
a.setWallpaperButtonEnabled(false);
final BitmapRegionTileSource.UriBitmapSource bitmapSource =
new BitmapRegionTileSource.UriBitmapSource(a.getContext(), Uri.fromFile(mFile));
final BitmapRegionTileSource.FilePathBitmapSource bitmapSource =
new BitmapRegionTileSource.FilePathBitmapSource(mFile.getAbsolutePath());
a.setCropViewTileSource(bitmapSource, false, true, null, new Runnable() {
@Override
@@ -239,7 +239,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
public void onClick(final WallpaperPickerActivity a) {
a.setWallpaperButtonEnabled(false);
final BitmapRegionTileSource.ResourceBitmapSource bitmapSource =
new BitmapRegionTileSource.ResourceBitmapSource(mResources, mResId);
new BitmapRegionTileSource.ResourceBitmapSource(mResources, mResId, a);
a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() {
@Override
@@ -1030,7 +1030,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
} else {
Resources res = getResources();
Point defaultThumbSize = getDefaultThumbnailSize(res);
int rotation = BitmapUtils.getRotationFromExif(res, resId);
int rotation = BitmapUtils.getRotationFromExif(res, resId, this);
thumb = createThumbnail(
defaultThumbSize, getContext(), null, null, sysRes, resId, rotation, false);
if (thumb != null) {
@@ -25,14 +25,15 @@ import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.net.Uri;
import android.opengl.GLUtils;
import android.os.Build;
import android.util.Log;
import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.common.ExifOrientation;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.exif.ExifInterface;
import com.android.gallery3d.glrenderer.BasicTexture;
import com.android.gallery3d.glrenderer.BitmapTexture;
import com.android.photos.views.TiledImageRenderer;
@@ -160,13 +161,7 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
private State mState = State.NOT_LOADED;
public boolean loadInBackground(InBitmapProvider bitmapProvider) {
ExifInterface ei = new ExifInterface();
if (readExif(ei)) {
Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
if (ori != null) {
mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
}
}
mRotation = getExifRotation();
mDecoder = loadBitmapRegionDecoder();
if (mDecoder == null) {
mState = State.ERROR_LOADING;
@@ -232,7 +227,7 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
return mRotation;
}
public abstract boolean readExif(ExifInterface ei);
public abstract int getExifRotation();
public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
@@ -259,18 +254,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
return BitmapFactory.decodeFile(mPath, options);
}
@Override
public boolean readExif(ExifInterface ei) {
try {
ei.readExif(mPath);
return true;
} catch (NullPointerException e) {
Log.w("BitmapRegionTileSource", "reading exif failed", e);
return false;
} catch (IOException e) {
Log.w("BitmapRegionTileSource", "getting decoder failed", e);
return false;
}
public int getExifRotation() {
return ExifOrientation.readRotation(mPath);
}
}
@@ -315,35 +302,22 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
return null;
}
}
@Override
public boolean readExif(ExifInterface ei) {
InputStream is = null;
try {
is = regenerateInputStream();
ei.readExif(is);
Utils.closeSilently(is);
return true;
} catch (FileNotFoundException e) {
Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return false;
} catch (IOException e) {
Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return false;
} catch (NullPointerException e) {
Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
return false;
} finally {
Utils.closeSilently(is);
}
public int getExifRotation() {
return BitmapUtils.getRotationFromExif(mContext, mUri);
}
}
public static class ResourceBitmapSource extends BitmapSource {
private Resources mRes;
private int mResId;
public ResourceBitmapSource(Resources res, int resId) {
private Context mContext;
public ResourceBitmapSource(Resources res, int resId, Context context) {
mRes = res;
mResId = resId;
mContext = context;
}
private InputStream regenerateInputStream() {
InputStream is = mRes.openRawResource(mResId);
@@ -366,17 +340,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
return BitmapFactory.decodeResource(mRes, mResId, options);
}
@Override
public boolean readExif(ExifInterface ei) {
try {
InputStream is = regenerateInputStream();
ei.readExif(is);
Utils.closeSilently(is);
return true;
} catch (IOException e) {
Log.e("BitmapRegionTileSource", "Error reading resource", e);
return false;
}
public int getExifRotation() {
return BitmapUtils.getRotationFromExif(mRes, mResId, mContext);
}
}