1100 lines
33 KiB
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;
|
|
}
|
|
}
|