/* * 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. *
* Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the * parser. * *
* 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. * } **/ 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
* 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. *
* 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