SubwayTooter-Android-App/exif/src/main/java/it/sephiroth/android/library/exif2/ExifParser.java

1100 lines
33 KiB
Java

/*
* 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 it.sephiroth.android.library.exif2;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
class ExifParser {
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()
*/
public static final int EVENT_UNCOMPRESSED_STRIP = 4;
/**
* When there is nothing more to parse.
*/
public static final int EVENT_END = 5;
protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in M_EXIF
// 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;
protected static final int DEFAULT_IFD0_OFFSET = 8;
private static final Charset US_ASCII = Charset.forName( "US-ASCII" );
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 int mOptions;
private final ExifInterface mInterface;
private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>();
private final CountedDataInputStream mTiffStream;
private int mIfdStartOffset = 0;
private int mNumOfTagInIfd = 0;
private int mIfdType;
private ExifTag mTag;
private ImageEvent mImageEvent;
private ExifTag mStripSizeTag;
private ExifTag mJpegSizeTag;
private boolean mNeedToParseOffsetsInCurrentIfd;
private byte[] mDataAboveIfd0;
private int mIfd0Position;
private int mQualityGuess;
private int mImageWidth;
private int mImageLength;
private short mProcess = 0;
private List<Section> mSections;
private int mUncompressedDataPosition = 0;
static final int std_luminance_quant_tbl[];
static final int std_chrominance_quant_tbl[];
static final int deftabs[][];
static {
std_luminance_quant_tbl = new int[]{ 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, 57, 51, 56, 55, 64,
72, 92, 78, 64, 68, 87, 69, 55, 56, 80, 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, 100, 120, 92, 101, 103, 99 };
std_chrominance_quant_tbl = new int[]
{ 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99 };
deftabs = new int[][]{ std_luminance_quant_tbl, std_chrominance_quant_tbl };
}
private ExifParser( InputStream inputStream, int options, ExifInterface iRef ) throws IOException, ExifInvalidFormatException {
if( inputStream == null ) {
throw new IOException( "Null argument inputStream to ExifParser" );
}
Log.v( TAG, "Reading exif..." );
mSections = new ArrayList<Section>(0);
mInterface = iRef;
mTiffStream = seekTiffData( inputStream );
mOptions = options;
// Log.d( TAG, "sections size: " + mSections.size() );
if( mTiffStream == null ) {
return;
}
parseTiffHeader( mTiffStream );
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 );
}
}
}
private final byte mByteArray[] = new byte[8];
private final ByteBuffer mByteBuffer = ByteBuffer.wrap( mByteArray );
private int readInt( byte b[], int offset ) {
mByteBuffer.rewind();
mByteBuffer.put( b, offset, 4 );
mByteBuffer.rewind();
return mByteBuffer.getInt();
}
private short readShort( byte b[], int offset ) {
mByteBuffer.rewind();
mByteBuffer.put( b, offset, 2 );
mByteBuffer.rewind();
return mByteBuffer.getShort();
}
private CountedDataInputStream seekTiffData( InputStream inputStream ) throws IOException, ExifInvalidFormatException {
CountedDataInputStream dataStream = new CountedDataInputStream( inputStream );
CountedDataInputStream tiffStream = null;
int a = dataStream.readUnsignedByte();
int b = dataStream.readUnsignedByte();
if( a != 0xFF || b != JpegHeader.TAG_SOI ) {
Log.e( TAG, "invalid jpeg header" );
return null;
}
while( true ) {
int itemlen;
int prev;
int marker;
byte ll,lh;
int got;
byte data[];
prev = 0;
for( a = 0; ; a++ ) {
marker = dataStream.readUnsignedByte();
if( marker != 0xff && prev == 0xff ) break;
prev = marker;
}
if (a > 10){
Log.w( TAG, "Extraneous " + ( a - 1 ) + " padding bytes before section " + marker );
}
Section section = new Section();
section.type = marker;
// Read the length of the section.
lh = dataStream.readByte();
ll = dataStream.readByte();
itemlen = ( ( lh & 0xff ) << 8 ) | ( ll & 0xff );
if( itemlen < 2 ) {
throw new ExifInvalidFormatException( "Invalid marker" );
}
section.size = itemlen;
data = new byte[itemlen];
data[0] = lh;
data[1] = ll;
// Log.i( TAG, "marker: " + String.format( "0x%2X", marker ) + ": " + itemlen + ", position: " + dataStream.getReadByteCount() + ", available: " + dataStream.available() );
// got = dataStream.read( data, 2, itemlen-2 );
got = readBytes( dataStream, data, 2, itemlen - 2 );
if( got != itemlen - 2 ) {
throw new ExifInvalidFormatException( "Premature end of file? Expecting " + (itemlen-2) + ", received " + got );
}
section.data = data;
boolean ignore = false;
switch( marker ) {
case JpegHeader.TAG_M_SOS:
// stop before hitting compressed data
mSections.add( section );
mUncompressedDataPosition = dataStream.getReadByteCount();
return tiffStream;
case JpegHeader.TAG_M_DQT:
// Use for jpeg quality guessing
process_M_DQT( data, itemlen );
break;
case JpegHeader.TAG_M_DHT:
break;
case JpegHeader.TAG_M_EOI:
// in case it's a tables-only JPEG stream
Log.w( TAG, "No image in jpeg!" );
return null;
case JpegHeader.TAG_M_COM:
// Comment section
ignore = true;
break;
case JpegHeader.TAG_M_JFIF:
if( itemlen < 16 ) {
ignore = true;
}
break;
case JpegHeader.TAG_M_IPTC:
break;
case JpegHeader.TAG_M_SOF0:
case JpegHeader.TAG_M_SOF1:
case JpegHeader.TAG_M_SOF2:
case JpegHeader.TAG_M_SOF3:
case JpegHeader.TAG_M_SOF5:
case JpegHeader.TAG_M_SOF6:
case JpegHeader.TAG_M_SOF7:
case JpegHeader.TAG_M_SOF9:
case JpegHeader.TAG_M_SOF10:
case JpegHeader.TAG_M_SOF11:
case JpegHeader.TAG_M_SOF13:
case JpegHeader.TAG_M_SOF14:
case JpegHeader.TAG_M_SOF15:
process_M_SOFn( data, marker );
break;
case JpegHeader.TAG_M_EXIF:
if( itemlen >= 8 ) {
int header = readInt( data, 2 );
short headerTail = readShort( data, 6 );
// header = Exif, headerTail=\0\0
if( header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL ) {
tiffStream = new CountedDataInputStream( new ByteArrayInputStream( data, 8, itemlen - 8 ) );
tiffStream.setEnd( itemlen - 6 );
ignore = false;
} else {
Log.v( TAG, "Image cotains XMP section" );
}
}
break;
default:
Log.w( TAG, "Unknown marker: " + String.format( "0x%2X", marker ) + ", length: " + itemlen );
break;
}
if( !ignore ) {
// Log.d( TAG, "adding section with size: " + section.size );
mSections.add( section );
}
else {
Log.v( TAG, "ignoring marker: " + String.format( "0x%2X", marker ) + ", length: " + itemlen );
}
}
}
/**
* Using this instead of the default {@link java.io.InputStream#read(byte[], int, int)} because
* on remote input streams reading large amount of data can fail
*
* @param dataStream
* @param data
* @param offset
* @param length
* @return
* @throws IOException
*/
private int readBytes( final InputStream dataStream, final byte[] data, int offset, final int length ) throws IOException {
int count = 0;
int n;
int max_length = Math.min( 1024, length );
while( 0 < (n = dataStream.read(data, offset, max_length))) {
count += n;
offset += n;
max_length = Math.min( max_length, length-count );
}
return count;
}
static int Get16m( byte[] data, int position ) {
int b1 = ( data[position] & 0xFF ) << 8;
int b2 = data[position + 1] & 0xFF;
return b1 | b2;
}
private void process_M_SOFn( final byte[] data, final int marker ) {
if( data.length > 7 ) {
//int data_precision = data[2] & 0xff;
//int num_components = data[7] & 0xff;
mImageLength = Get16m( data, 3 );
mImageWidth = Get16m( data, 5 );
}
mProcess = (short) marker;
}
private void process_M_DQT( final byte[] data, int length ) {
int a = 2;
int c;
int tableindex, coefindex;
double cumsf = 0.0;
int[] reftable = null;
int allones = 1;
while( a < data.length ) {
c = data[a++];
tableindex = c & 0x0f;
if( tableindex < 2 ) {
reftable = deftabs[tableindex];
}
// Read in the table, compute statistics relative to reference table
for( coefindex = 0; coefindex < 64; coefindex++ ) {
int val;
if( ( c >> 4 ) != 0 ) {
int temp;
temp = (int) ( data[a++] );
temp *= 256;
val = (int) data[a++] + temp;
}
else {
val = (int) data[a++];
}
if( reftable != null ) {
double x;
// scaling factor in percent
x = 100.0 * (double) val / (double) reftable[coefindex];
cumsf += x;
// separate check for all-ones table (Q 100)
if( val != 1 ) allones = 0;
}
}
// Print summary stats
if( reftable != null ) { // terse output includes quality
double qual;
cumsf /= 64.0; // mean scale factor
if( allones != 0 ) { // special case for all-ones table
qual = 100.0;
}
else if( cumsf <= 100.0 ) {
qual = ( 200.0 - cumsf ) / 2.0;
}
else {
qual = 5000.0 / cumsf;
}
if( tableindex == 0 ) {
mQualityGuess = (int) ( qual + 0.5 );
// Log.v( TAG, "quality guess: " + mQualityGuess );
}
}
}
}
private void parseTiffHeader( final CountedDataInputStream stream ) throws IOException, ExifInvalidFormatException {
short byteOrder = stream.readShort();
if( LITTLE_ENDIAN_TAG == byteOrder ) {
stream.setByteOrder( ByteOrder.LITTLE_ENDIAN );
}
else if( BIG_ENDIAN_TAG == byteOrder ) {
stream.setByteOrder( ByteOrder.BIG_ENDIAN );
}
else {
throw new ExifInvalidFormatException( "Invalid TIFF header" );
}
if( stream.readShort() != TIFF_HEADER_TAIL ) {
throw new ExifInvalidFormatException( "Invalid TIFF header" );
}
}
private boolean isIfdRequested( int ifdType ) {
switch( ifdType ) {
case IfdId.TYPE_IFD_0:
return ( mOptions & ExifInterface.Options.OPTION_IFD_0 ) != 0;
case IfdId.TYPE_IFD_1:
return ( mOptions & ExifInterface.Options.OPTION_IFD_1 ) != 0;
case IfdId.TYPE_IFD_EXIF:
return ( mOptions & ExifInterface.Options.OPTION_IFD_EXIF ) != 0;
case IfdId.TYPE_IFD_GPS:
return ( mOptions & ExifInterface.Options.OPTION_IFD_GPS ) != 0;
case IfdId.TYPE_IFD_INTEROPERABILITY:
return ( mOptions & ExifInterface.Options.OPTION_IFD_INTEROPERABILITY ) != 0;
}
return false;
}
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;
}
}
private void registerIfd( int ifdType, long offset ) {
// Cast unsigned int to int since the offset is always smaller
// than the size of M_EXIF (65536)
mCorrespondingEvent.put( (int) offset, new IfdEvent( ifdType, isIfdRequested( ifdType ) ) );
}
/**
* Equivalent to read(buffer, 0, buffer.length).
*/
protected int read( byte[] buffer ) throws IOException {
return mTiffStream.read( buffer );
}
private boolean isThumbnailRequested() {
return ( mOptions & ExifInterface.Options.OPTION_THUMBNAIL ) != 0;
}
/**
* Parses the the given InputStream with the given options
*
* @throws java.io.IOException
* @throws 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.
// *
// * @throws java.io.IOException
// * @throws ExifInvalidFormatException
// */
// protected static ExifParser parse( InputStream inputStream, boolean requestThumbnail, ExifInterface iRef ) throws IOException, ExifInvalidFormatException {
// return new ExifParser( inputStream, OPTION_IFD_0 | OPTION_IFD_1 | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY | ( requestThumbnail ? OPTION_THUMBNAIL : 0 ), iRef );
// }
/**
* Moves the parser forward and returns the next parsing event
*
* @throws java.io.IOException
* @throws 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( null == mTiffStream ) {
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 {
// Log.v(TAG, "skipTo: " + entry.getKey());
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 > mTiffStream.getEnd() ) {
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 java.io.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 );
}
}
}
/**
* 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, java.nio.charset.Charset)
*/
protected ExifTag getTag() {
return mTag;
}
/**
* Gets number of tags in the current IFD area.
*/
public 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.
*/
protected int getStripIndex() {
return mImageEvent.stripIndex;
}
/**
* 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 );
// Log.v(TAG, "available: " + mTiffStream.available() );
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 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;
}
}
public boolean isDefinedTag(int ifdId, int tagId ) {
return mInterface.getTagInfo().get(ExifInterface.defineTag(ifdId, (short)tagId)) != ExifInterface.DEFINITION_NULL;
}
public 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( final ExifTag tag ) throws IOException {
// Some invalid images contains tags with wrong size, check it here
short type = tag.getDataType();
final int componentCount = tag.getComponentCount();
// sanity check
if (componentCount >= 0x66000000) throw new IOException("size out of bounds");
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[componentCount];
read( buf );
tag.setValue( buf );
}
break;
case ExifTag.TYPE_ASCII:
tag.setValue( readString(componentCount) );
break;
case ExifTag.TYPE_UNSIGNED_LONG: {
long value[] = new long[componentCount];
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[componentCount];
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[componentCount];
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[componentCount];
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[componentCount];
for( int i = 0, n = value.length; i < n; i++ ) {
value[i] = readRational();
}
tag.setValue( value );
}
break;
}
// Log.v( TAG, "\n" + tag.toString() );
}
/**
* Reads bytes from the InputStream.
*/
protected int read( byte[] buffer, int offset, int length ) throws IOException {
return mTiffStream.read( buffer, offset, length );
}
/**
* 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 );
}
/**
* Gets the byte order of the current InputStream.
*/
protected ByteOrder getByteOrder() {
if( null != mTiffStream ) return mTiffStream.getByteOrder();
return null;
}
public int getQualityGuess() {
return mQualityGuess;
}
public int getImageWidth() {
return mImageWidth;
}
public short getJpegProcess() {
return mProcess;
}
public int getImageLength() {
return mImageLength;
}
public List<Section> getSections() {
return mSections;
}
public int getUncompressedDataPosition() {
return mUncompressedDataPosition;
}
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;
}
}
public static class Section {
int size;
int type;
byte[] data;
}
}