1165 lines
31 KiB
Kotlin
1165 lines
31 KiB
Kotlin
/*
|
|
* 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 it.sephiroth.android.library.exif2.utils.CountedDataInputStream
|
|
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.*
|
|
import kotlin.math.min
|
|
|
|
internal open class ExifParser
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
private constructor(
|
|
inputStream : InputStream,
|
|
private val mOptions : Int,
|
|
private val mInterface : ExifInterface
|
|
) {
|
|
|
|
// number of tags in the current IFD area.
|
|
private var tagCountInCurrentIfd = 0
|
|
|
|
/**
|
|
* the ID of current IFD.
|
|
*
|
|
* @see IfdData.TYPE_IFD_0
|
|
* @see IfdData.TYPE_IFD_1
|
|
* @see IfdData.TYPE_IFD_GPS
|
|
* @see IfdData.TYPE_IFD_INTEROPERABILITY
|
|
* @see IfdData.TYPE_IFD_EXIF
|
|
*/
|
|
var currentIfd : Int = 0
|
|
private set
|
|
|
|
/**
|
|
* If [.next] return [.EVENT_NEW_TAG] or
|
|
* [.EVENT_VALUE_OF_REGISTERED_TAG], call this function to get the
|
|
* corresponding tag.
|
|
*
|
|
*
|
|
* For [.EVENT_NEW_TAG], the tag may not contain the value if the size
|
|
* of the value is greater than 4 bytes. One should call
|
|
* [ExifTag.hasValue] to check if the tag contains value. If there
|
|
* is no value,call [.registerForTagValue] to have the parser
|
|
* emit [.EVENT_VALUE_OF_REGISTERED_TAG] when it reaches the area
|
|
* pointed by the offset.
|
|
*
|
|
*
|
|
* When [.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
|
|
* @see .read
|
|
* @see .read
|
|
* @see .readLong
|
|
* @see .readRational
|
|
* @see .readString
|
|
* @see .readString
|
|
*/
|
|
var tag : ExifTag? = null
|
|
private set
|
|
|
|
private var mImageEvent : ImageEvent? = null
|
|
private var mStripSizeTag : ExifTag? = null
|
|
private var mJpegSizeTag : ExifTag? = null
|
|
private var mNeedToParseOffsetsInCurrentIfd : Boolean = false
|
|
private var mDataAboveIfd0 : ByteArray? = null
|
|
private var mIfd0Position : Int = 0
|
|
|
|
var qualityGuess : Int = 0
|
|
private set
|
|
var imageWidth : Int = 0
|
|
private set
|
|
var imageLength : Int = 0
|
|
private set
|
|
var jpegProcess : Short = 0
|
|
private set
|
|
|
|
var uncompressedDataPosition = 0
|
|
private set
|
|
|
|
private val mByteArray = ByteArray(8)
|
|
private val mByteBuffer = ByteBuffer.wrap(mByteArray)
|
|
|
|
private val isThumbnailRequested : Boolean
|
|
get() = mOptions and ExifInterface.Options.OPTION_THUMBNAIL != 0
|
|
|
|
/**
|
|
* When receiving [.EVENT_UNCOMPRESSED_STRIP], call this function to
|
|
* get the index of this strip.
|
|
*/
|
|
val stripIndex : Int
|
|
get() = mImageEvent?.stripIndex ?: 0
|
|
|
|
/**
|
|
* When receiving [.EVENT_UNCOMPRESSED_STRIP], call this function to
|
|
* get the strip size.
|
|
*/
|
|
val stripSize : Int
|
|
get() = mStripSizeTag?.getValueAt(0)?.toInt() ?: 0
|
|
|
|
/**
|
|
* When receiving [.EVENT_COMPRESSED_IMAGE], call this function to get
|
|
* the image data size.
|
|
*/
|
|
val compressedImageSize : Int
|
|
get() = mJpegSizeTag?.getValueAt(0)?.toInt() ?: 0
|
|
|
|
/**
|
|
* Gets the byte order of the current InputStream.
|
|
*/
|
|
val byteOrder : ByteOrder
|
|
get() = mTiffStream.byteOrder
|
|
|
|
val sections : List<Section>
|
|
get() = mSections
|
|
|
|
private val mCorrespondingEvent = TreeMap<Int, Any>()
|
|
|
|
private val mSections = ArrayList<Section>(0)
|
|
|
|
private var mIfdStartOffset = 0
|
|
|
|
private val mTiffStream : CountedDataInputStream = seekTiffData(inputStream)
|
|
|
|
init {
|
|
|
|
// Log.d( TAG, "sections size: " + mSections.size() );
|
|
|
|
val tiffStream = mTiffStream
|
|
|
|
parseTiffHeader(tiffStream)
|
|
|
|
val offset = tiffStream.readUnsignedInt()
|
|
if(offset > Integer.MAX_VALUE) {
|
|
throw ExifInvalidFormatException("Invalid offset $offset")
|
|
}
|
|
mIfd0Position = offset.toInt()
|
|
currentIfd = IfdData.TYPE_IFD_0
|
|
|
|
if(isIfdRequested(IfdData.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
|
|
registerIfd(IfdData.TYPE_IFD_0, offset)
|
|
if(offset != DEFAULT_IFD0_OFFSET.toLong()) {
|
|
val ba = ByteArray(offset.toInt() - DEFAULT_IFD0_OFFSET)
|
|
mDataAboveIfd0 = ba
|
|
read(ba)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun readInt(b : ByteArray, @Suppress("SameParameterValue") offset : Int) : Int {
|
|
mByteBuffer.rewind()
|
|
mByteBuffer.put(b, offset, 4)
|
|
mByteBuffer.rewind()
|
|
return mByteBuffer.int
|
|
}
|
|
|
|
private fun readShort(b : ByteArray, @Suppress("SameParameterValue") offset : Int) : Short {
|
|
mByteBuffer.rewind()
|
|
mByteBuffer.put(b, offset, 2)
|
|
mByteBuffer.rewind()
|
|
return mByteBuffer.short
|
|
}
|
|
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
private fun seekTiffData(inputStream : InputStream) : CountedDataInputStream {
|
|
val dataStream =
|
|
CountedDataInputStream(inputStream)
|
|
var tiffStream : CountedDataInputStream? = null
|
|
|
|
var a = dataStream.readUnsignedByte()
|
|
val b = dataStream.readUnsignedByte()
|
|
|
|
|
|
if(a == 137 && b == 80) error("maybe PNG image")
|
|
|
|
if(a != 0xFF || b != JpegHeader.TAG_SOI) error("invalid jpeg header")
|
|
|
|
while(true) {
|
|
val itemlen : Int
|
|
var marker : Int
|
|
|
|
val got : Int
|
|
val data : ByteArray
|
|
|
|
var prev = 0
|
|
a = 0
|
|
while(true) {
|
|
marker = dataStream.readUnsignedByte()
|
|
if(marker != 0xff && prev == 0xff) break
|
|
prev = marker
|
|
a ++
|
|
}
|
|
|
|
if(a > 10) {
|
|
Log.w(TAG, "Extraneous ${a - 1} padding bytes before section $marker")
|
|
}
|
|
|
|
// Read the length of the section.
|
|
val lh = dataStream.readByte().toInt()
|
|
val ll = dataStream.readByte().toInt()
|
|
itemlen = lh and 0xff shl 8 or (ll and 0xff)
|
|
|
|
if(itemlen < 2) {
|
|
throw ExifInvalidFormatException("Invalid marker")
|
|
}
|
|
|
|
data = ByteArray(itemlen)
|
|
data[0] = lh.toByte()
|
|
data[1] = ll.toByte()
|
|
|
|
// 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 ExifInvalidFormatException("Premature end of file? Expecting " + (itemlen - 2) + ", received " + got)
|
|
}
|
|
|
|
val section = Section(type = marker, size = itemlen, data = data)
|
|
|
|
var ignore = false
|
|
|
|
when(marker) {
|
|
JpegHeader.TAG_M_SOS -> {
|
|
// stop before hitting compressed data
|
|
mSections.add(section)
|
|
uncompressedDataPosition = dataStream.readByteCount
|
|
return tiffStream ?: error("stop before hitting compressed data")
|
|
}
|
|
|
|
JpegHeader.TAG_M_DQT ->
|
|
// Use for jpeg quality guessing
|
|
process_M_DQT(data)
|
|
|
|
JpegHeader.TAG_M_DHT -> {
|
|
}
|
|
|
|
// in case it's a tables-only JPEG stream
|
|
JpegHeader.TAG_M_EOI -> {
|
|
error("\"No image in jpeg!\"")
|
|
}
|
|
|
|
JpegHeader.TAG_M_COM ->
|
|
// Comment section
|
|
ignore = true
|
|
|
|
JpegHeader.TAG_M_JFIF -> if(itemlen < 16) {
|
|
ignore = true
|
|
}
|
|
|
|
JpegHeader.TAG_M_IPTC -> {
|
|
}
|
|
|
|
JpegHeader.TAG_M_SOF0, JpegHeader.TAG_M_SOF1, JpegHeader.TAG_M_SOF2, JpegHeader.TAG_M_SOF3, JpegHeader.TAG_M_SOF5, JpegHeader.TAG_M_SOF6, JpegHeader.TAG_M_SOF7, JpegHeader.TAG_M_SOF9, JpegHeader.TAG_M_SOF10, JpegHeader.TAG_M_SOF11, JpegHeader.TAG_M_SOF13, JpegHeader.TAG_M_SOF14, JpegHeader.TAG_M_SOF15 -> process_M_SOFn(
|
|
data,
|
|
marker
|
|
)
|
|
|
|
JpegHeader.TAG_M_EXIF -> if(itemlen >= 8) {
|
|
val header = readInt(data, 2)
|
|
val headerTail = readShort(data, 6)
|
|
// header = Exif, headerTail=\0\0
|
|
if(header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
|
|
tiffStream =
|
|
CountedDataInputStream(
|
|
ByteArrayInputStream(data, 8, itemlen - 8)
|
|
)
|
|
tiffStream.end = itemlen - 6
|
|
ignore = false
|
|
} else {
|
|
Log.v(TAG, "Image cotains XMP section")
|
|
}
|
|
}
|
|
|
|
else -> Log.w(
|
|
TAG,
|
|
"Unknown marker: " + String.format("0x%2X", marker) + ", length: " + itemlen
|
|
)
|
|
}
|
|
|
|
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 [java.io.InputStream.read] because
|
|
* on remote input streams reading large amount of data can fail
|
|
*
|
|
* @param dataStream
|
|
* @param data
|
|
* @param offsetArg
|
|
* @param length
|
|
* @return
|
|
* @throws IOException
|
|
*/
|
|
@Throws(IOException::class)
|
|
private fun readBytes(
|
|
dataStream : InputStream,
|
|
data : ByteArray,
|
|
@Suppress("SameParameterValue") offsetArg : Int,
|
|
length : Int
|
|
) : Int {
|
|
var offset = offsetArg
|
|
var count = 0
|
|
var n : Int
|
|
var max_length = min(1024, length)
|
|
while(true) {
|
|
n = dataStream.read(data, offset, max_length)
|
|
if(n <= 0) break
|
|
count += n
|
|
offset += n
|
|
max_length = min(max_length, length - count)
|
|
}
|
|
return count
|
|
}
|
|
|
|
private fun process_M_SOFn(data : ByteArray, marker : Int) {
|
|
if(data.size > 7) {
|
|
//int data_precision = data[2] & 0xff;
|
|
//int num_components = data[7] & 0xff;
|
|
imageLength = Get16m(data, 3)
|
|
imageWidth = Get16m(data, 5)
|
|
}
|
|
jpegProcess = marker.toShort()
|
|
}
|
|
|
|
private fun process_M_DQT(data : ByteArray) {
|
|
var a = 2
|
|
var c : Int
|
|
var tableindex : Int
|
|
var coefindex : Int
|
|
var cumsf = 0.0
|
|
var reftable : IntArray? = null
|
|
var allones = 1
|
|
|
|
while(a < data.size) {
|
|
c = data[a ++].toInt()
|
|
tableindex = c and 0x0f
|
|
|
|
if(tableindex < 2) {
|
|
reftable = deftabs[tableindex]
|
|
}
|
|
|
|
// Read in the table, compute statistics relative to reference table
|
|
coefindex = 0
|
|
while(coefindex < 64) {
|
|
val `val` : Int
|
|
if(c shr 4 != 0) {
|
|
var temp : Int
|
|
temp = data[a ++].toInt()
|
|
temp *= 256
|
|
`val` = data[a ++].toInt() + temp
|
|
} else {
|
|
`val` = data[a ++].toInt()
|
|
}
|
|
if(reftable != null) {
|
|
val x : Double
|
|
// scaling factor in percent
|
|
x = 100.0 * `val`.toDouble() / reftable[coefindex].toDouble()
|
|
cumsf += x
|
|
// separate check for all-ones table (Q 100)
|
|
if(`val` != 1) allones = 0
|
|
}
|
|
coefindex ++
|
|
}
|
|
// Print summary stats
|
|
if(reftable != null) { // terse output includes quality
|
|
val qual : Double
|
|
cumsf /= 64.0 // mean scale factor
|
|
|
|
qual = when {
|
|
allones != 0 -> 100.0 // special case for all-ones table
|
|
cumsf <= 100.0 -> (200.0 - cumsf) / 2.0
|
|
else -> 5000.0 / cumsf
|
|
}
|
|
|
|
if(tableindex == 0) {
|
|
qualityGuess = (qual + 0.5).toInt()
|
|
// Log.v( TAG, "quality guess: " + mQualityGuess );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
private fun parseTiffHeader(stream : CountedDataInputStream) {
|
|
|
|
stream.byteOrder = when(stream.readShort()) {
|
|
LITTLE_ENDIAN_TAG -> ByteOrder.LITTLE_ENDIAN
|
|
BIG_ENDIAN_TAG -> ByteOrder.BIG_ENDIAN
|
|
else -> throw ExifInvalidFormatException("Invalid TIFF header")
|
|
}
|
|
|
|
if(stream.readShort() != TIFF_HEADER_TAIL) {
|
|
throw ExifInvalidFormatException("Invalid TIFF header")
|
|
}
|
|
}
|
|
|
|
private fun isIfdRequested(ifdType : Int) : Boolean {
|
|
when(ifdType) {
|
|
IfdData.TYPE_IFD_0 -> return mOptions and ExifInterface.Options.OPTION_IFD_0 != 0
|
|
IfdData.TYPE_IFD_1 -> return mOptions and ExifInterface.Options.OPTION_IFD_1 != 0
|
|
IfdData.TYPE_IFD_EXIF -> return mOptions and ExifInterface.Options.OPTION_IFD_EXIF != 0
|
|
IfdData.TYPE_IFD_GPS -> return mOptions and ExifInterface.Options.OPTION_IFD_GPS != 0
|
|
IfdData.TYPE_IFD_INTEROPERABILITY -> return mOptions and ExifInterface.Options.OPTION_IFD_INTEROPERABILITY != 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun needToParseOffsetsInCurrentIfd() : Boolean {
|
|
return when(currentIfd) {
|
|
|
|
IfdData.TYPE_IFD_0 ->
|
|
isIfdRequested(IfdData.TYPE_IFD_EXIF) ||
|
|
isIfdRequested(IfdData.TYPE_IFD_GPS) ||
|
|
isIfdRequested(IfdData.TYPE_IFD_INTEROPERABILITY) ||
|
|
isIfdRequested(IfdData.TYPE_IFD_1)
|
|
|
|
IfdData.TYPE_IFD_1 -> isThumbnailRequested
|
|
|
|
IfdData.TYPE_IFD_EXIF ->
|
|
// The offset to interoperability IFD is located in Exif IFD
|
|
isIfdRequested(IfdData.TYPE_IFD_INTEROPERABILITY)
|
|
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
private fun registerIfd(ifdType : Int, offset : Long) {
|
|
// Cast unsigned int to int since the offset is always smaller
|
|
// than the size of M_EXIF (65536)
|
|
mCorrespondingEvent[offset.toInt()] = IfdEvent(ifdType, isIfdRequested(ifdType))
|
|
}
|
|
|
|
/**
|
|
* Equivalent to read(buffer, 0, buffer.length).
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun read(buffer : ByteArray) : Int = mTiffStream.read(buffer)
|
|
|
|
//
|
|
// /**
|
|
// * 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
|
|
*/
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
operator fun next() : Int {
|
|
|
|
val offset = mTiffStream.readByteCount
|
|
val endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * tagCountInCurrentIfd
|
|
if(offset < endOfTags) {
|
|
val tag = readTag()
|
|
this.tag = tag
|
|
if(tag == null) {
|
|
return next()
|
|
} else if(mNeedToParseOffsetsInCurrentIfd) {
|
|
checkOffsetOrImageTag(tag)
|
|
}
|
|
return EVENT_NEW_TAG
|
|
} else if(offset == endOfTags) {
|
|
// There is a link to ifd1 at the end of ifd0
|
|
if(currentIfd == IfdData.TYPE_IFD_0) {
|
|
val ifdOffset = readUnsignedLong()
|
|
if(isIfdRequested(IfdData.TYPE_IFD_1) || isThumbnailRequested) {
|
|
if(ifdOffset != 0L) {
|
|
registerIfd(IfdData.TYPE_IFD_1, ifdOffset)
|
|
}
|
|
}
|
|
} else {
|
|
var offsetSize = 4
|
|
// Some camera models use invalid length of the offset
|
|
if(mCorrespondingEvent.size > 0) {
|
|
val firstEntry = mCorrespondingEvent.firstEntry() !!
|
|
offsetSize = firstEntry.key - mTiffStream.readByteCount
|
|
}
|
|
if(offsetSize < 4) {
|
|
Log.w(TAG, "Invalid size of link to next IFD: $offsetSize")
|
|
} else {
|
|
val ifdOffset = readUnsignedLong()
|
|
if(ifdOffset != 0L) {
|
|
Log.w(TAG, "Invalid link to next IFD: $ifdOffset")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while(mCorrespondingEvent.size != 0) {
|
|
val entry = mCorrespondingEvent.pollFirstEntry() !!
|
|
val event = entry.value
|
|
try {
|
|
// Log.v(TAG, "skipTo: " + entry.getKey());
|
|
skipTo(entry.key)
|
|
} catch(e : IOException) {
|
|
Log.w(
|
|
TAG,
|
|
"Failed to skip to data at: ${entry.key} for ${event.javaClass.name}, the file may be broken."
|
|
)
|
|
continue
|
|
}
|
|
|
|
if(event is IfdEvent) {
|
|
currentIfd = event.ifd
|
|
tagCountInCurrentIfd = mTiffStream.readUnsignedShort()
|
|
mIfdStartOffset = entry.key
|
|
|
|
if(tagCountInCurrentIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mTiffStream.end) {
|
|
Log.w(TAG, "Invalid size of IFD $currentIfd")
|
|
return EVENT_END
|
|
}
|
|
|
|
mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd()
|
|
if(event.isRequested) {
|
|
return EVENT_START_OF_IFD
|
|
} else {
|
|
skipRemainingTagsInCurrentIfd()
|
|
}
|
|
} else if(event is ImageEvent) {
|
|
mImageEvent = event
|
|
return event.type
|
|
} else {
|
|
val tagEvent = event as ExifTagEvent
|
|
val tag = tagEvent.tag
|
|
this.tag = tag
|
|
if(tag.dataType != ExifTag.TYPE_UNDEFINED) {
|
|
readFullTagValue(tag)
|
|
checkOffsetOrImageTag(tag)
|
|
}
|
|
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
|
|
*/
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
protected fun skipRemainingTagsInCurrentIfd() {
|
|
|
|
val endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * tagCountInCurrentIfd
|
|
var offset = mTiffStream.readByteCount
|
|
if(offset > endOfTags) return
|
|
|
|
if(mNeedToParseOffsetsInCurrentIfd) {
|
|
while(offset < endOfTags) {
|
|
val tag = readTag()
|
|
this.tag = tag
|
|
offset += TAG_SIZE
|
|
if(tag == null) {
|
|
continue
|
|
}
|
|
checkOffsetOrImageTag(tag)
|
|
}
|
|
} else {
|
|
skipTo(endOfTags)
|
|
}
|
|
val ifdOffset = readUnsignedLong()
|
|
// For ifd0, there is a link to ifd1 in the end of all tags
|
|
if(currentIfd == IfdData.TYPE_IFD_0 && (isIfdRequested(IfdData.TYPE_IFD_1) || isThumbnailRequested)) {
|
|
if(ifdOffset > 0) {
|
|
registerIfd(IfdData.TYPE_IFD_1, ifdOffset)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
private fun skipTo(offset : Int) {
|
|
mTiffStream.skipTo(offset.toLong())
|
|
|
|
// Log.v(TAG, "available: " + mTiffStream.available() );
|
|
while(! mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
|
|
mCorrespondingEvent.pollFirstEntry()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When getting [.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 [.EVENT_VALUE_OF_REGISTERED_TAG] when it reaches the area
|
|
* where the value is located.
|
|
*
|
|
* @see .EVENT_VALUE_OF_REGISTERED_TAG
|
|
*/
|
|
fun registerForTagValue(tag : ExifTag) {
|
|
if(tag.offset >= mTiffStream.readByteCount) {
|
|
mCorrespondingEvent[tag.offset] = ExifTagEvent(tag, true)
|
|
}
|
|
}
|
|
|
|
private fun registerCompressedImage(offset : Long) {
|
|
mCorrespondingEvent[offset.toInt()] = ImageEvent(EVENT_COMPRESSED_IMAGE)
|
|
}
|
|
|
|
private fun registerUncompressedStrip(stripIndex : Int, offset : Long) {
|
|
mCorrespondingEvent[offset.toInt()] = ImageEvent(EVENT_UNCOMPRESSED_STRIP, stripIndex)
|
|
}
|
|
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
private fun readTag() : ExifTag? {
|
|
|
|
val tagId = mTiffStream.readShort()
|
|
val dataFormat = mTiffStream.readShort()
|
|
val numOfComp = mTiffStream.readUnsignedInt()
|
|
if(numOfComp > Integer.MAX_VALUE) {
|
|
throw 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
|
|
val tag = ExifTag(
|
|
tagId,
|
|
dataFormat,
|
|
numOfComp.toInt(),
|
|
currentIfd,
|
|
numOfComp.toInt() != ExifTag.SIZE_UNDEFINED
|
|
)
|
|
val dataSize = tag.dataSize
|
|
if(dataSize > 4) {
|
|
val offset = mTiffStream.readUnsignedInt()
|
|
if(offset > Integer.MAX_VALUE) {
|
|
throw 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) {
|
|
val buf = ByteArray(numOfComp.toInt())
|
|
System.arraycopy(
|
|
mDataAboveIfd0 !!,
|
|
offset.toInt() - DEFAULT_IFD0_OFFSET,
|
|
buf,
|
|
0,
|
|
numOfComp.toInt()
|
|
)
|
|
tag.setValue(buf)
|
|
} else {
|
|
tag.offset = offset.toInt()
|
|
}
|
|
} else {
|
|
val defCount = tag.hasDefinedCount
|
|
// Set defined count to 0 so we can add \0 to non-terminated strings
|
|
tag.hasDefinedCount = false
|
|
// Read value
|
|
readFullTagValue(tag)
|
|
tag.hasDefinedCount = defCount
|
|
mTiffStream.skip((4 - dataSize).toLong())
|
|
// Set the offset to the position of value.
|
|
tag.offset = mTiffStream.readByteCount - 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 fun checkOffsetOrImageTag(tag : ExifTag) {
|
|
// Some invalid formattd image contains tag with 0 size.
|
|
if(tag.componentCount == 0) {
|
|
return
|
|
}
|
|
val tid = tag.tagId
|
|
val ifd = tag.ifd
|
|
if(tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
|
|
if(isIfdRequested(IfdData.TYPE_IFD_EXIF) || isIfdRequested(IfdData.TYPE_IFD_INTEROPERABILITY)) {
|
|
registerIfd(IfdData.TYPE_IFD_EXIF, tag.getValueAt(0))
|
|
}
|
|
} else if(tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
|
|
if(isIfdRequested(IfdData.TYPE_IFD_GPS)) {
|
|
registerIfd(IfdData.TYPE_IFD_GPS, tag.getValueAt(0))
|
|
}
|
|
} else if(tid == TAG_INTEROPERABILITY_IFD && checkAllowed(
|
|
ifd,
|
|
ExifInterface.TAG_INTEROPERABILITY_IFD
|
|
)) {
|
|
if(isIfdRequested(IfdData.TYPE_IFD_INTEROPERABILITY)) {
|
|
registerIfd(IfdData.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(i in 0 until tag.componentCount) {
|
|
if(tag.dataType == ExifTag.TYPE_UNSIGNED_SHORT) {
|
|
registerUncompressedStrip(i, tag.getValueAt(i))
|
|
} else {
|
|
registerUncompressedStrip(i, tag.getValueAt(i))
|
|
}
|
|
}
|
|
} else {
|
|
mCorrespondingEvent[tag.offset] = ExifTagEvent(tag, false)
|
|
}
|
|
}
|
|
} else if(tid == TAG_STRIP_BYTE_COUNTS && checkAllowed(
|
|
ifd,
|
|
ExifInterface.TAG_STRIP_BYTE_COUNTS
|
|
) && isThumbnailRequested && tag.hasValue) {
|
|
mStripSizeTag = tag
|
|
}
|
|
}
|
|
|
|
fun isDefinedTag(ifdId : Int, tagId : Short) : Boolean {
|
|
return mInterface.tagInfo.get(
|
|
ExifInterface.defineTag(
|
|
ifdId,
|
|
tagId
|
|
)
|
|
) != ExifInterface.DEFINITION_NULL
|
|
}
|
|
|
|
private fun checkAllowed(ifd : Int, tagId : Int) : Boolean {
|
|
val info = mInterface.tagInfo.get(tagId)
|
|
return if(info == ExifInterface.DEFINITION_NULL) {
|
|
false
|
|
} else ExifInterface.isIfdAllowed(info, ifd)
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
fun readFullTagValue(tag : ExifTag) {
|
|
|
|
// Some invalid images contains tags with wrong size, check it here
|
|
val type = tag.dataType
|
|
val componentCount = tag.componentCount
|
|
|
|
// sanity check
|
|
if(componentCount >= 0x66000000) throw IOException("size out of bounds")
|
|
|
|
if(type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED ||
|
|
type == ExifTag.TYPE_UNSIGNED_BYTE) {
|
|
var size = tag.componentCount
|
|
if(mCorrespondingEvent.size > 0) {
|
|
val firstEntry = mCorrespondingEvent.firstEntry() !!
|
|
if(firstEntry.key < mTiffStream.readByteCount + size) {
|
|
val event = firstEntry.value
|
|
if(event is ImageEvent) {
|
|
// Tag value overlaps thumbnail, ignore thumbnail.
|
|
Log.w(TAG, "Thumbnail overlaps value for tag: \n$tag")
|
|
val entry = mCorrespondingEvent.pollFirstEntry() !!
|
|
Log.w(TAG, "Invalid thumbnail offset: " + entry.key)
|
|
} else {
|
|
// Tag value overlaps another tag, shorten count
|
|
if(event is IfdEvent) {
|
|
Log.w(TAG, "Ifd ${event.ifd} overlaps value for tag: \n$tag")
|
|
} else if(event is ExifTagEvent) {
|
|
Log.w(
|
|
TAG,
|
|
"Tag value for tag: \n${event.tag} overlaps value for tag: \n$tag"
|
|
)
|
|
}
|
|
size = firstEntry.key - mTiffStream.readByteCount
|
|
Log.w(TAG, "Invalid size of tag: \n$tag setting count to: $size")
|
|
tag.forceSetComponentCount(size)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
when(tag.dataType) {
|
|
ExifTag.TYPE_UNSIGNED_BYTE, ExifTag.TYPE_UNDEFINED ->
|
|
tag.setValue(ByteArray(componentCount).also { read(it) })
|
|
|
|
ExifTag.TYPE_ASCII ->
|
|
tag.setValue(readString(componentCount))
|
|
|
|
ExifTag.TYPE_UNSIGNED_SHORT ->
|
|
tag.setValue(IntArray(componentCount) { readUnsignedShort() })
|
|
|
|
ExifTag.TYPE_LONG ->
|
|
tag.setValue(IntArray(componentCount) { readLong().toInt() })
|
|
ExifTag.TYPE_UNSIGNED_LONG ->
|
|
tag.setValue(LongArray(componentCount) { readUnsignedLong() })
|
|
|
|
ExifTag.TYPE_RATIONAL ->
|
|
tag.setValue(Array(componentCount) { readRational() })
|
|
ExifTag.TYPE_UNSIGNED_RATIONAL ->
|
|
tag.setValue(Array(componentCount) { readUnsignedRational() })
|
|
|
|
}
|
|
|
|
// Log.v( TAG, "\n" + tag.toString() );
|
|
}
|
|
|
|
/**
|
|
* Reads bytes from the InputStream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
protected fun read(buffer : ByteArray, offset : Int, length : Int) : Int =
|
|
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 [ExifTag.TYPE_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 [ExifTag.TYPE_ASCII].
|
|
*/
|
|
@Throws(IOException::class)
|
|
@JvmOverloads
|
|
protected fun readString(n : Int, charset : Charset = US_ASCII) : String =
|
|
when {
|
|
n <= 0 -> ""
|
|
else -> mTiffStream.readString(n, charset)
|
|
}
|
|
|
|
/**
|
|
* Reads value of type [ExifTag.TYPE_UNSIGNED_SHORT] from the
|
|
* InputStream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
protected fun readUnsignedShort() : Int =
|
|
mTiffStream.readShort().toInt() and 0xffff
|
|
|
|
/**
|
|
* Reads value of type [ExifTag.TYPE_UNSIGNED_LONG] from the
|
|
* InputStream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
protected fun readUnsignedLong() : Long {
|
|
return readLong() and 0xffffffffL
|
|
}
|
|
|
|
/**
|
|
* Reads value of type [ExifTag.TYPE_UNSIGNED_RATIONAL] from the
|
|
* InputStream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
protected fun readUnsignedRational() : Rational {
|
|
val nomi = readUnsignedLong()
|
|
val denomi = readUnsignedLong()
|
|
return Rational(nomi, denomi)
|
|
}
|
|
|
|
/**
|
|
* Reads value of type [ExifTag.TYPE_LONG] from the InputStream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
protected fun readLong() : Long =
|
|
mTiffStream.readInt().toLong()
|
|
|
|
/**
|
|
* Reads value of type [ExifTag.TYPE_RATIONAL] from the InputStream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
protected fun readRational() : Rational {
|
|
val nomi = readLong()
|
|
val denomi = readLong()
|
|
return Rational(nomi, denomi)
|
|
}
|
|
|
|
private class ImageEvent {
|
|
internal var stripIndex : Int = 0
|
|
internal var type : Int = 0
|
|
|
|
internal constructor(type : Int) {
|
|
this.stripIndex = 0
|
|
this.type = type
|
|
}
|
|
|
|
internal constructor(type : Int, stripIndex : Int) {
|
|
this.type = type
|
|
this.stripIndex = stripIndex
|
|
}
|
|
}
|
|
|
|
private class IfdEvent internal constructor(
|
|
internal var ifd : Int,
|
|
internal var isRequested : Boolean
|
|
)
|
|
|
|
private class ExifTagEvent internal constructor(
|
|
internal var tag : ExifTag,
|
|
internal var isRequested : Boolean
|
|
)
|
|
|
|
class Section(var size : Int, var type : Int, var data : ByteArray)
|
|
|
|
companion object {
|
|
private const val TAG = "ExifParser"
|
|
|
|
/**
|
|
* When the parser reaches a new IFD area. Call [.getCurrentIfd] to
|
|
* know which IFD we are in.
|
|
*/
|
|
const val EVENT_START_OF_IFD = 0
|
|
/**
|
|
* When the parser reaches a new tag. Call [.getTag]to get the
|
|
* corresponding tag.
|
|
*/
|
|
const val EVENT_NEW_TAG = 1
|
|
/**
|
|
* When the parser reaches the value area of tag that is registered by
|
|
* [.registerForTagValue] previously. Call [.getTag]
|
|
* to get the corresponding tag.
|
|
*/
|
|
const val EVENT_VALUE_OF_REGISTERED_TAG = 2
|
|
/**
|
|
* When the parser reaches the compressed image area.
|
|
*/
|
|
const val EVENT_COMPRESSED_IMAGE = 3
|
|
/**
|
|
* When the parser reaches the uncompressed image strip. Call
|
|
* [.getStripIndex] to get the index of the strip.
|
|
*
|
|
* @see .getStripIndex
|
|
*/
|
|
const val EVENT_UNCOMPRESSED_STRIP = 4
|
|
/**
|
|
* When there is nothing more to parse.
|
|
*/
|
|
const val EVENT_END = 5
|
|
|
|
protected const val EXIF_HEADER = 0x45786966 // EXIF header "Exif"
|
|
protected const val EXIF_HEADER_TAIL = 0x0000.toShort() // EXIF header in M_EXIF
|
|
// TIFF header
|
|
protected const val LITTLE_ENDIAN_TAG = 0x4949.toShort() // "II"
|
|
protected const val BIG_ENDIAN_TAG = 0x4d4d.toShort() // "MM"
|
|
protected const val TIFF_HEADER_TAIL : Short = 0x002A
|
|
protected const val TAG_SIZE = 12
|
|
protected const val OFFSET_SIZE = 2
|
|
protected const val DEFAULT_IFD0_OFFSET = 8
|
|
private val US_ASCII = Charset.forName("US-ASCII")
|
|
private val TAG_EXIF_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)
|
|
private val TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)
|
|
private val TAG_INTEROPERABILITY_IFD =
|
|
ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD)
|
|
private val TAG_JPEG_INTERCHANGE_FORMAT =
|
|
ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)
|
|
private val TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
|
|
ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)
|
|
private val TAG_STRIP_OFFSETS = ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)
|
|
private val TAG_STRIP_BYTE_COUNTS =
|
|
ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)
|
|
|
|
private val std_luminance_quant_tbl : IntArray
|
|
private val std_chrominance_quant_tbl : IntArray
|
|
val deftabs : Array<IntArray>
|
|
|
|
init {
|
|
std_luminance_quant_tbl = intArrayOf(
|
|
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 = intArrayOf(
|
|
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 = arrayOf(std_luminance_quant_tbl, std_chrominance_quant_tbl)
|
|
}
|
|
|
|
fun Get16m(data : ByteArray, position : Int) : Int {
|
|
val b1 = data[position].toInt() and 0xFF shl 8
|
|
val b2 = data[position + 1].toInt() and 0xFF
|
|
return b1 or b2
|
|
}
|
|
|
|
/**
|
|
* Parses the the given InputStream with the given options
|
|
*
|
|
* @throws java.io.IOException
|
|
* @throws ExifInvalidFormatException
|
|
*/
|
|
@Throws(IOException::class, ExifInvalidFormatException::class)
|
|
fun parse(inputStream : InputStream, options : Int, iRef : ExifInterface) : ExifParser =
|
|
ExifParser(inputStream, options, iRef)
|
|
}
|
|
} |