remove unused module exif. define compile_sdk_version build script variable.
|
@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion target_sdk_version
|
compileSdkVersion compile_sdk_version
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
|
@ -6,7 +6,7 @@ apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
compileSdkVersion target_sdk_version
|
compileSdkVersion compile_sdk_version
|
||||||
|
|
||||||
// exoPlayer 2.9.0 以降は Java 8 compiler support を要求する
|
// exoPlayer 2.9.0 以降は Java 8 compiler support を要求する
|
||||||
// https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
// https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||||
|
|
|
@ -2,6 +2,7 @@ buildscript {
|
||||||
|
|
||||||
ext.min_sdk_version = 21
|
ext.min_sdk_version = 21
|
||||||
ext.target_sdk_version = 30
|
ext.target_sdk_version = 30
|
||||||
|
ext.compile_sdk_version = 30
|
||||||
ext.appcompat_version='1.2.0'
|
ext.appcompat_version='1.2.0'
|
||||||
|
|
||||||
ext.kotlin_version = '1.4.30'
|
ext.kotlin_version = '1.4.30'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion target_sdk_version
|
compileSdkVersion compile_sdk_version
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
|
@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion target_sdk_version
|
compileSdkVersion compile_sdk_version
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
|
@ -1,52 +0,0 @@
|
||||||
apply plugin: 'com.android.library'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'de.mobilej.unmock'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion target_sdk_version
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
targetSdkVersion target_sdk_version
|
|
||||||
minSdkVersion min_sdk_version
|
|
||||||
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
unMock {
|
|
||||||
keepStartingWith "android.util."
|
|
||||||
keepStartingWith "com.android.internal.util."
|
|
||||||
|
|
||||||
keep "android.graphics.Bitmap"
|
|
||||||
keep "android.graphics.BitmapFactory"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
testImplementation "junit:junit:$junit_version"
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0-rc03'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-rc03'
|
|
||||||
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
implementation 'commons-io:commons-io:2.6'
|
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
|
||||||
implementation "androidx.core:core-ktx:1.3.1"
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
|
|
||||||
unmock 'org.robolectric:android-all:5.0.0_r2-robolectric-0'
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in C:\android\sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
-dontobfuscate
|
|
|
@ -1,3 +0,0 @@
|
||||||
<manifest package="it.sephiroth.android.library.exif2">
|
|
||||||
<application/>
|
|
||||||
</manifest>
|
|
|
@ -1,298 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.util.Log
|
|
||||||
import it.sephiroth.android.library.exif2.utils.notEmpty
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.Arrays
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class stores the EXIF header in IFDs according to the JPEG
|
|
||||||
* specification. It is the result produced by [ExifReader].
|
|
||||||
*
|
|
||||||
* @see ExifReader
|
|
||||||
*
|
|
||||||
* @see IfdData
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
internal class ExifData(
|
|
||||||
val byteOrder : ByteOrder = ExifInterface.DEFAULT_BYTE_ORDER,
|
|
||||||
val sections : List<ExifParser.Section> = ArrayList(),
|
|
||||||
val mUncompressedDataPosition : Int = 0,
|
|
||||||
val qualityGuess : Int = 0,
|
|
||||||
val jpegProcess : Short = 0
|
|
||||||
) {
|
|
||||||
|
|
||||||
private var imageLength = - 1
|
|
||||||
private var imageWidth = - 1
|
|
||||||
|
|
||||||
private val mIfdDatas = arrayOfNulls<IfdData>(IfdData.TYPE_IFD_COUNT)
|
|
||||||
|
|
||||||
// the compressed thumbnail.
|
|
||||||
// null if there is no compressed thumbnail.
|
|
||||||
var compressedThumbnail : ByteArray? = null
|
|
||||||
|
|
||||||
private val mStripBytes = ArrayList<ByteArray?>()
|
|
||||||
|
|
||||||
val stripList : List<ByteArray>?
|
|
||||||
get() = mStripBytes.filterNotNull().notEmpty()
|
|
||||||
|
|
||||||
// Decodes the user comment tag into string as specified in the EXIF standard.
|
|
||||||
// Returns null if decoding failed.
|
|
||||||
val userComment : String?
|
|
||||||
get() {
|
|
||||||
|
|
||||||
val ifdData = mIfdDatas[IfdData.TYPE_IFD_0]
|
|
||||||
?: return null
|
|
||||||
|
|
||||||
val tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT))
|
|
||||||
?: return null
|
|
||||||
|
|
||||||
if(tag.componentCount < 8)
|
|
||||||
return null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
|
|
||||||
val buf = ByteArray(tag.componentCount)
|
|
||||||
tag.getBytes(buf)
|
|
||||||
|
|
||||||
val code = ByteArray(8)
|
|
||||||
System.arraycopy(buf, 0, code, 0, 8)
|
|
||||||
|
|
||||||
val charset = when {
|
|
||||||
code.contentEquals(USER_COMMENT_ASCII) -> Charsets.US_ASCII
|
|
||||||
code.contentEquals(USER_COMMENT_JIS) -> eucJp
|
|
||||||
code.contentEquals(USER_COMMENT_UNICODE) -> Charsets.UTF_16
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if(charset == null) null else String(buf, 8, buf.size - 8, charset)
|
|
||||||
} catch(e : UnsupportedEncodingException) {
|
|
||||||
Log.w(TAG, "Failed to decode the user comment")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// list of all [ExifTag]s in the ExifData
|
|
||||||
// or null if there are none.
|
|
||||||
val allTags : List<ExifTag>
|
|
||||||
get() = ArrayList<ExifTag>()
|
|
||||||
.apply { mIfdDatas.forEach { if(it != null) addAll(it.allTagsCollection) } }
|
|
||||||
|
|
||||||
val imageSize : IntArray
|
|
||||||
get() = intArrayOf(imageWidth, imageLength)
|
|
||||||
|
|
||||||
val thumbnailBytes : ByteArray?
|
|
||||||
get() = when {
|
|
||||||
compressedThumbnail != null -> compressedThumbnail
|
|
||||||
stripList != null -> null // TODO: implement this
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val thumbnailBitmap : Bitmap?
|
|
||||||
get() {
|
|
||||||
val compressedThumbnail = this.compressedThumbnail
|
|
||||||
if(compressedThumbnail != null) {
|
|
||||||
return BitmapFactory
|
|
||||||
.decodeByteArray(compressedThumbnail, 0, compressedThumbnail.size)
|
|
||||||
}
|
|
||||||
val stripList = this.stripList
|
|
||||||
if(stripList != null) {
|
|
||||||
// TODO: decoding uncompressed thumbnail is not implemented.
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an uncompressed strip.
|
|
||||||
*/
|
|
||||||
fun setStripBytes(index : Int, strip : ByteArray) {
|
|
||||||
if(index in mStripBytes.indices) {
|
|
||||||
mStripBytes[index] = strip
|
|
||||||
} else {
|
|
||||||
for(i in mStripBytes.size until index) {
|
|
||||||
mStripBytes.add(null)
|
|
||||||
}
|
|
||||||
mStripBytes.add(strip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the [IfdData] object corresponding to a given IFD or
|
|
||||||
* generates one if none exist.
|
|
||||||
*/
|
|
||||||
private fun prepareIfdData(ifdId : Int) : IfdData {
|
|
||||||
var ifdData = mIfdDatas[ifdId]
|
|
||||||
if(ifdData == null) {
|
|
||||||
ifdData = IfdData(ifdId)
|
|
||||||
mIfdDatas[ifdId] = ifdData
|
|
||||||
}
|
|
||||||
return ifdData
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds IFD data. If IFD data of the same type already exists, it will be
|
|
||||||
* replaced by the new data.
|
|
||||||
*/
|
|
||||||
fun addIfdData(data : IfdData) {
|
|
||||||
mIfdDatas[data.id] = data
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the tag with a given TID in the given IFD if the tag exists.
|
|
||||||
* Otherwise returns null.
|
|
||||||
*/
|
|
||||||
fun getTag(tag : Short, ifd : Int) : ExifTag? =
|
|
||||||
mIfdDatas[ifd]?.getTag(tag)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given ExifTag to its default IFD and returns an existing ExifTag
|
|
||||||
* with the same TID or null if none exist.
|
|
||||||
*/
|
|
||||||
fun addTag(tag : ExifTag?) : ExifTag? =
|
|
||||||
when(tag) {
|
|
||||||
null -> null
|
|
||||||
else -> addTag(tag, tag.ifd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given ExifTag to the given IFD and returns an existing ExifTag
|
|
||||||
* with the same TID or null if none exist.
|
|
||||||
*/
|
|
||||||
private fun addTag(tag : ExifTag?, ifdId : Int) : ExifTag? =
|
|
||||||
when {
|
|
||||||
tag == null -> null
|
|
||||||
! ExifTag.isValidIfd(ifdId) -> null
|
|
||||||
else -> prepareIfdData(ifdId).setTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the tag with a given TID and IFD.
|
|
||||||
*/
|
|
||||||
fun removeTag(tagId : Short, ifdId : Int) {
|
|
||||||
mIfdDatas[ifdId]?.removeTag(tagId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the thumbnail and its related tags. IFD1 will be removed.
|
|
||||||
*/
|
|
||||||
fun removeThumbnailData() {
|
|
||||||
clearThumbnailAndStrips()
|
|
||||||
mIfdDatas[IfdData.TYPE_IFD_1] = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearThumbnailAndStrips() {
|
|
||||||
compressedThumbnail = null
|
|
||||||
mStripBytes.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of all [ExifTag]s in a given IFD or null if there
|
|
||||||
* are none.
|
|
||||||
*/
|
|
||||||
fun getAllTagsForIfd(ifd : Int) : List<ExifTag>? =
|
|
||||||
mIfdDatas[ifd]?.allTagsCollection?.notEmpty()?.toList()
|
|
||||||
|
|
||||||
// Returns a list of all [ExifTag]s with a given TID
|
|
||||||
// or null if there are none.
|
|
||||||
fun getAllTagsForTagId(tag : Short) : List<ExifTag>? =
|
|
||||||
ArrayList<ExifTag>()
|
|
||||||
.apply { mIfdDatas.forEach { it?.getTag(tag)?.let { t -> add(t) } } }
|
|
||||||
.notEmpty()
|
|
||||||
|
|
||||||
override fun equals(other : Any?) : Boolean {
|
|
||||||
if(this === other) return true
|
|
||||||
if(other is ExifData) {
|
|
||||||
if(other.byteOrder != byteOrder
|
|
||||||
|| other.mStripBytes.size != mStripBytes.size
|
|
||||||
|| ! Arrays.equals(other.compressedThumbnail, compressedThumbnail)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for(i in mStripBytes.indices) {
|
|
||||||
val a = mStripBytes[i]
|
|
||||||
val b = other.mStripBytes[i]
|
|
||||||
|
|
||||||
if(a != null && b != null) {
|
|
||||||
if(! a.contentEquals(b)) return false // 内容が異なる
|
|
||||||
} else if((a == null) xor (b == null)) {
|
|
||||||
return false // 片方だけnull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(i in 0 until IfdData.TYPE_IFD_COUNT) {
|
|
||||||
val ifd1 = other.getIfdData(i)
|
|
||||||
val ifd2 = getIfdData(i)
|
|
||||||
if(ifd1 != ifd2) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the [IfdData] object corresponding to a given IFD if it
|
|
||||||
* exists or null.
|
|
||||||
*/
|
|
||||||
fun getIfdData(ifdId : Int) = when {
|
|
||||||
! ExifTag.isValidIfd(ifdId) -> null
|
|
||||||
else -> mIfdDatas[ifdId]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setImageSize(imageWidth : Int, imageLength : Int) {
|
|
||||||
this.imageWidth = imageWidth
|
|
||||||
this.imageLength = imageLength
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() : Int {
|
|
||||||
var result = byteOrder.hashCode()
|
|
||||||
result = 31 * result + (sections.hashCode())
|
|
||||||
result = 31 * result + mIfdDatas.contentHashCode()
|
|
||||||
result = 31 * result + (compressedThumbnail?.contentHashCode() ?: 0)
|
|
||||||
result = 31 * result + mStripBytes.hashCode()
|
|
||||||
result = 31 * result + qualityGuess
|
|
||||||
result = 31 * result + imageLength
|
|
||||||
result = 31 * result + imageWidth
|
|
||||||
result = 31 * result + jpegProcess
|
|
||||||
result = 31 * result + mUncompressedDataPosition
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "ExifData"
|
|
||||||
|
|
||||||
private val USER_COMMENT_ASCII =
|
|
||||||
byteArrayOf(0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00)
|
|
||||||
|
|
||||||
private val USER_COMMENT_JIS =
|
|
||||||
byteArrayOf(0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00)
|
|
||||||
|
|
||||||
private val USER_COMMENT_UNICODE =
|
|
||||||
byteArrayOf(0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00)
|
|
||||||
|
|
||||||
private val eucJp = Charset.forName("EUC-JP")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
class ExifInvalidFormatException(meg : String) : Exception(meg)
|
|
|
@ -1,378 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import it.sephiroth.android.library.exif2.utils.OrderedDataOutputStream
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
internal class ExifOutputStream(
|
|
||||||
|
|
||||||
private val mInterface : ExifInterface,
|
|
||||||
|
|
||||||
// the Exif header to be written into the JPEG file.
|
|
||||||
private val exifData : ExifData
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val mBuffer = ByteBuffer.allocate(4)
|
|
||||||
|
|
||||||
private fun requestByteToBuffer(
|
|
||||||
requestByteCount : Int, buffer : ByteArray, offset : Int, length : Int
|
|
||||||
) : Int {
|
|
||||||
val byteNeeded = requestByteCount - mBuffer.position()
|
|
||||||
val byteToRead = if(length > byteNeeded) byteNeeded else length
|
|
||||||
mBuffer.put(buffer, offset, byteToRead)
|
|
||||||
return byteToRead
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeExifData(out : OutputStream) {
|
|
||||||
Log.v(TAG, "Writing exif data...")
|
|
||||||
|
|
||||||
val nullTags = stripNullValueTags(exifData)
|
|
||||||
createRequiredIfdAndTag()
|
|
||||||
val exifSize = calculateAllOffset()
|
|
||||||
// Log.i(TAG, "exifSize: " + (exifSize + 8));
|
|
||||||
if(exifSize + 8 > MAX_EXIF_SIZE) {
|
|
||||||
throw IOException("Exif header is too large (>64Kb)")
|
|
||||||
}
|
|
||||||
|
|
||||||
val outputStream = BufferedOutputStream(out, STREAMBUFFER_SIZE)
|
|
||||||
val dataOutputStream =
|
|
||||||
OrderedDataOutputStream(outputStream)
|
|
||||||
|
|
||||||
dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
dataOutputStream.write(0xFF)
|
|
||||||
dataOutputStream.write(JpegHeader.TAG_M_EXIF)
|
|
||||||
dataOutputStream.writeShort((exifSize + 8).toShort())
|
|
||||||
dataOutputStream.writeInt(EXIF_HEADER)
|
|
||||||
dataOutputStream.writeShort(0x0000.toShort())
|
|
||||||
if(exifData.byteOrder == ByteOrder.BIG_ENDIAN) {
|
|
||||||
dataOutputStream.writeShort(TIFF_BIG_ENDIAN)
|
|
||||||
} else {
|
|
||||||
dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN)
|
|
||||||
}
|
|
||||||
dataOutputStream.setByteOrder(exifData.byteOrder)
|
|
||||||
dataOutputStream.writeShort(TIFF_HEADER)
|
|
||||||
dataOutputStream.writeInt(8)
|
|
||||||
writeAllTags(dataOutputStream)
|
|
||||||
|
|
||||||
writeThumbnail(dataOutputStream)
|
|
||||||
|
|
||||||
for(t in nullTags) {
|
|
||||||
exifData.addTag(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataOutputStream.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip tags that has null value
|
|
||||||
// return list of removed tags
|
|
||||||
private fun stripNullValueTags(data : ExifData) =
|
|
||||||
ArrayList<ExifTag>()
|
|
||||||
.apply {
|
|
||||||
for(t in data.allTags) {
|
|
||||||
if(t.getValue() == null && ! ExifInterface.isOffsetTag(t.tagId)) {
|
|
||||||
data.removeTag(t.tagId, t.ifd)
|
|
||||||
add(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeThumbnail(dataOutputStream : OrderedDataOutputStream) {
|
|
||||||
val compressedThumbnail = exifData.compressedThumbnail
|
|
||||||
if(compressedThumbnail != null) {
|
|
||||||
Log.d(TAG, "writing thumbnail..")
|
|
||||||
dataOutputStream.write(compressedThumbnail)
|
|
||||||
} else {
|
|
||||||
val stripList = exifData.stripList
|
|
||||||
if(stripList != null) {
|
|
||||||
Log.d(TAG, "writing uncompressed strip..")
|
|
||||||
stripList.forEach {
|
|
||||||
dataOutputStream.write(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeAllTags(dataOutputStream : OrderedDataOutputStream) {
|
|
||||||
writeIfd(exifData.getIfdData(IfdData.TYPE_IFD_0), dataOutputStream)
|
|
||||||
writeIfd(exifData.getIfdData(IfdData.TYPE_IFD_EXIF), dataOutputStream)
|
|
||||||
writeIfd(exifData.getIfdData(IfdData.TYPE_IFD_INTEROPERABILITY), dataOutputStream)
|
|
||||||
writeIfd(exifData.getIfdData(IfdData.TYPE_IFD_GPS), dataOutputStream)
|
|
||||||
writeIfd(exifData.getIfdData(IfdData.TYPE_IFD_1), dataOutputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeIfd(ifd : IfdData?, dataOutputStream : OrderedDataOutputStream) {
|
|
||||||
ifd ?: return
|
|
||||||
val tags = ifd.allTagsCollection
|
|
||||||
dataOutputStream.writeShort(tags.size.toShort())
|
|
||||||
for(tag in tags) {
|
|
||||||
dataOutputStream.writeShort(tag.tagId)
|
|
||||||
dataOutputStream.writeShort(tag.dataType)
|
|
||||||
dataOutputStream.writeInt(tag.componentCount)
|
|
||||||
// Log.v( TAG, "\n" + tag.toString() );
|
|
||||||
if(tag.dataSize > 4) {
|
|
||||||
dataOutputStream.writeInt(tag.offset)
|
|
||||||
} else {
|
|
||||||
writeTagValue(tag, dataOutputStream)
|
|
||||||
var i = 0
|
|
||||||
val n = 4 - tag.dataSize
|
|
||||||
while(i < n) {
|
|
||||||
dataOutputStream.write(0)
|
|
||||||
i ++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataOutputStream.writeInt(ifd.offsetToNextIfd)
|
|
||||||
for(tag in tags) {
|
|
||||||
if(tag.dataSize > 4) {
|
|
||||||
writeTagValue(tag, dataOutputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateOffsetOfIfd(ifd : IfdData, offsetArg : Int) : Int {
|
|
||||||
var offset = offsetArg
|
|
||||||
offset += 2 + ifd.tagCount * TAG_SIZE + 4
|
|
||||||
|
|
||||||
for(tag in ifd.allTagsCollection) {
|
|
||||||
if(tag.dataSize > 4) {
|
|
||||||
tag.offset = offset
|
|
||||||
offset += tag.dataSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return offset
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun createRequiredIfdAndTag() {
|
|
||||||
|
|
||||||
// IFD0 is required for all file
|
|
||||||
var ifd0 = exifData.getIfdData(IfdData.TYPE_IFD_0)
|
|
||||||
if(ifd0 == null) {
|
|
||||||
ifd0 = IfdData(IfdData.TYPE_IFD_0)
|
|
||||||
exifData.addIfdData(ifd0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: " + ExifInterface.TAG_EXIF_IFD)
|
|
||||||
ifd0.setTag(exifOffsetTag)
|
|
||||||
|
|
||||||
// Exif IFD is required for all files.
|
|
||||||
var exifIfd = exifData.getIfdData(IfdData.TYPE_IFD_EXIF)
|
|
||||||
if(exifIfd == null) {
|
|
||||||
exifIfd = IfdData(IfdData.TYPE_IFD_EXIF)
|
|
||||||
exifData.addIfdData(exifIfd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GPS IFD
|
|
||||||
val gpsIfd = exifData.getIfdData(IfdData.TYPE_IFD_GPS)
|
|
||||||
if(gpsIfd != null) {
|
|
||||||
val gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_GPS_IFD}")
|
|
||||||
ifd0.setTag(gpsOffsetTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interoperability IFD
|
|
||||||
val interIfd = exifData.getIfdData(IfdData.TYPE_IFD_INTEROPERABILITY)
|
|
||||||
if(interIfd != null) {
|
|
||||||
val interOffsetTag =
|
|
||||||
mInterface.buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_INTEROPERABILITY_IFD}")
|
|
||||||
exifIfd.setTag(interOffsetTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ifd1 = exifData.getIfdData(IfdData.TYPE_IFD_1)
|
|
||||||
|
|
||||||
// thumbnail
|
|
||||||
val compressedThumbnail = exifData.compressedThumbnail
|
|
||||||
val stripList = exifData.stripList
|
|
||||||
when {
|
|
||||||
|
|
||||||
compressedThumbnail != null -> {
|
|
||||||
if(ifd1 == null) {
|
|
||||||
ifd1 = IfdData(IfdData.TYPE_IFD_1)
|
|
||||||
exifData.addIfdData(ifd1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val offsetTag =
|
|
||||||
mInterface.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT}")
|
|
||||||
|
|
||||||
ifd1.setTag(offsetTag)
|
|
||||||
val lengthTag =
|
|
||||||
mInterface.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH}")
|
|
||||||
|
|
||||||
lengthTag.setValue(compressedThumbnail.size)
|
|
||||||
ifd1.setTag(lengthTag)
|
|
||||||
|
|
||||||
// Get rid of tags for uncompressed if they exist.
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS))
|
|
||||||
}
|
|
||||||
|
|
||||||
stripList != null -> {
|
|
||||||
if(ifd1 == null) {
|
|
||||||
ifd1 = IfdData(IfdData.TYPE_IFD_1)
|
|
||||||
exifData.addIfdData(ifd1)
|
|
||||||
}
|
|
||||||
val offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_STRIP_OFFSETS}")
|
|
||||||
val lengthTag =
|
|
||||||
mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS)
|
|
||||||
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_STRIP_BYTE_COUNTS}")
|
|
||||||
|
|
||||||
val bytesList = LongArray(stripList.size)
|
|
||||||
stripList.forEachIndexed { index, bytes -> bytesList[index] = bytes.size.toLong() }
|
|
||||||
lengthTag.setValue(bytesList)
|
|
||||||
ifd1.setTag(offsetTag)
|
|
||||||
ifd1.setTag(lengthTag)
|
|
||||||
// Get rid of tags for compressed if they exist.
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH))
|
|
||||||
}
|
|
||||||
|
|
||||||
ifd1 != null -> {
|
|
||||||
// Get rid of offset and length tags if there is no thumbnail.
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS))
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
|
|
||||||
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateAllOffset() : Int {
|
|
||||||
var offset = TIFF_HEADER_SIZE.toInt()
|
|
||||||
|
|
||||||
val ifd0 = exifData.getIfdData(IfdData.TYPE_IFD_0)?.also {
|
|
||||||
offset = calculateOffsetOfIfd(it, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exifIfd = exifData.getIfdData(IfdData.TYPE_IFD_EXIF)?.also { it ->
|
|
||||||
ifd0?.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD))
|
|
||||||
?.setValue(offset)
|
|
||||||
offset = calculateOffsetOfIfd(it, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
exifData.getIfdData(IfdData.TYPE_IFD_INTEROPERABILITY)?.also { it ->
|
|
||||||
exifIfd?.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
|
|
||||||
?.setValue(offset)
|
|
||||||
offset = calculateOffsetOfIfd(it, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
exifData.getIfdData(IfdData.TYPE_IFD_GPS)?.also {
|
|
||||||
ifd0?.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD))
|
|
||||||
?.setValue(offset)
|
|
||||||
offset = calculateOffsetOfIfd(it, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ifd1 = exifData.getIfdData(IfdData.TYPE_IFD_1)?.also {
|
|
||||||
ifd0?.offsetToNextIfd = offset
|
|
||||||
offset = calculateOffsetOfIfd(it, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
val compressedThumbnail = exifData.compressedThumbnail
|
|
||||||
if(compressedThumbnail != null) {
|
|
||||||
ifd1
|
|
||||||
?.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
|
|
||||||
?.setValue(offset)
|
|
||||||
offset += compressedThumbnail.size
|
|
||||||
} else {
|
|
||||||
// uncompressed thumbnail
|
|
||||||
val stripList = exifData.stripList
|
|
||||||
if(stripList != null) {
|
|
||||||
val offsets = LongArray(stripList.size)
|
|
||||||
stripList.forEachIndexed { index, bytes ->
|
|
||||||
offsets[index] = offset.toLong()
|
|
||||||
offset += bytes.size
|
|
||||||
}
|
|
||||||
ifd1
|
|
||||||
?.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
|
|
||||||
?.setValue(offsets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return offset
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "ExifOutputStream"
|
|
||||||
private const val STREAMBUFFER_SIZE = 0x00010000 // 64Kb
|
|
||||||
|
|
||||||
private const val STATE_SOI = 0
|
|
||||||
private const val EXIF_HEADER = 0x45786966
|
|
||||||
private const val TIFF_HEADER : Short = 0x002A
|
|
||||||
private const val TIFF_BIG_ENDIAN : Short = 0x4d4d
|
|
||||||
private const val TIFF_LITTLE_ENDIAN : Short = 0x4949
|
|
||||||
private const val TAG_SIZE : Short = 12
|
|
||||||
private const val TIFF_HEADER_SIZE : Short = 8
|
|
||||||
private const val MAX_EXIF_SIZE = 65535
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeTagValue(tag : ExifTag, dataOutputStream : OrderedDataOutputStream) {
|
|
||||||
when(tag.dataType) {
|
|
||||||
ExifTag.TYPE_ASCII -> {
|
|
||||||
val buf = tag.stringByte !!
|
|
||||||
if(buf.size == tag.componentCount) {
|
|
||||||
buf[buf.size - 1] = 0
|
|
||||||
dataOutputStream.write(buf)
|
|
||||||
} else {
|
|
||||||
dataOutputStream.write(buf)
|
|
||||||
dataOutputStream.write(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifTag.TYPE_LONG, ExifTag.TYPE_UNSIGNED_LONG -> run {
|
|
||||||
for(i in 0 until tag.componentCount) {
|
|
||||||
dataOutputStream.writeInt(tag.getValueAt(i).toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifTag.TYPE_RATIONAL, ExifTag.TYPE_UNSIGNED_RATIONAL -> run {
|
|
||||||
for(i in 0 until tag.componentCount) {
|
|
||||||
dataOutputStream.writeRational(tag.getRational(i) !!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifTag.TYPE_UNDEFINED, ExifTag.TYPE_UNSIGNED_BYTE -> {
|
|
||||||
val buf = ByteArray(tag.componentCount)
|
|
||||||
tag.getBytes(buf)
|
|
||||||
dataOutputStream.write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifTag.TYPE_UNSIGNED_SHORT -> {
|
|
||||||
for(i in 0 until tag.componentCount) {
|
|
||||||
dataOutputStream.writeShort(tag.getValueAt(i).toShort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class reads the EXIF header of a JPEG file and stores it in
|
|
||||||
* [ExifData].
|
|
||||||
*/
|
|
||||||
internal class ExifReader(private val mInterface : ExifInterface) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the inputStream and and returns the EXIF data in an
|
|
||||||
* [ExifData].
|
|
||||||
*
|
|
||||||
* @throws ExifInvalidFormatException
|
|
||||||
* @throws java.io.IOException
|
|
||||||
*/
|
|
||||||
@Throws(ExifInvalidFormatException::class, IOException::class)
|
|
||||||
fun read(inputStream : InputStream, options : Int) : ExifData {
|
|
||||||
val parser = ExifParser.parse(inputStream, options, mInterface)
|
|
||||||
val exifData = ExifData(
|
|
||||||
byteOrder = parser.byteOrder,
|
|
||||||
sections = parser.sections,
|
|
||||||
mUncompressedDataPosition = parser.uncompressedDataPosition,
|
|
||||||
qualityGuess = parser.qualityGuess,
|
|
||||||
jpegProcess = parser.jpegProcess
|
|
||||||
)
|
|
||||||
|
|
||||||
val w = parser.imageWidth
|
|
||||||
val h = parser.imageLength
|
|
||||||
|
|
||||||
if(w > 0 && h > 0) {
|
|
||||||
exifData.setImageSize(w, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
var event = parser.next()
|
|
||||||
while(event != ExifParser.EVENT_END) {
|
|
||||||
when(event) {
|
|
||||||
|
|
||||||
ExifParser.EVENT_START_OF_IFD ->
|
|
||||||
exifData.addIfdData(IfdData(parser.currentIfd))
|
|
||||||
|
|
||||||
ExifParser.EVENT_NEW_TAG -> {
|
|
||||||
val tag = parser.tag
|
|
||||||
when {
|
|
||||||
tag == null ->
|
|
||||||
Log.w(TAG, "parser.tag is null")
|
|
||||||
|
|
||||||
! tag.hasValue ->
|
|
||||||
parser.registerForTagValue(tag)
|
|
||||||
|
|
||||||
! parser.isDefinedTag(tag.ifd, tag.tagId) ->
|
|
||||||
Log.w(TAG, "skip tag because not registered in the tag table:$tag")
|
|
||||||
|
|
||||||
else ->
|
|
||||||
exifData.getIfdData(tag.ifd)?.setTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifParser.EVENT_VALUE_OF_REGISTERED_TAG -> {
|
|
||||||
val tag = parser.tag !!
|
|
||||||
if(tag.dataType == ExifTag.TYPE_UNDEFINED) {
|
|
||||||
parser.readFullTagValue(tag)
|
|
||||||
}
|
|
||||||
exifData.getIfdData(tag.ifd) !!.setTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifParser.EVENT_COMPRESSED_IMAGE -> {
|
|
||||||
val buf = ByteArray(parser.compressedImageSize)
|
|
||||||
if(buf.size == parser.read(buf)) {
|
|
||||||
exifData.compressedThumbnail = buf
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Failed to read the compressed thumbnail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExifParser.EVENT_UNCOMPRESSED_STRIP -> {
|
|
||||||
val buf = ByteArray(parser.stripSize)
|
|
||||||
if(buf.size == parser.read(buf)) {
|
|
||||||
exifData.setStripBytes(parser.stripIndex, buf)
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Failed to read the strip bytes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event = parser.next()
|
|
||||||
}
|
|
||||||
return exifData
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "ExifReader"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,842 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class stores information of an EXIF tag. For more information about
|
|
||||||
* defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be
|
|
||||||
* instantiated using [ExifInterface.buildTag].
|
|
||||||
*
|
|
||||||
* @see ExifInterface
|
|
||||||
*/
|
|
||||||
// Use builtTag in ExifInterface instead of constructor.
|
|
||||||
@Suppress("unused")
|
|
||||||
open class ExifTag internal constructor(
|
|
||||||
|
|
||||||
// Exif TagId. the TID of this tag.
|
|
||||||
val tagId : Short,
|
|
||||||
|
|
||||||
// Exif Tag Type. the data type of this tag
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @see .TYPE_ASCII
|
|
||||||
* @see .TYPE_LONG
|
|
||||||
* @see .TYPE_RATIONAL
|
|
||||||
* @see .TYPE_UNDEFINED
|
|
||||||
* @see .TYPE_UNSIGNED_BYTE
|
|
||||||
* @see .TYPE_UNSIGNED_LONG
|
|
||||||
* @see .TYPE_UNSIGNED_RATIONAL
|
|
||||||
* @see .TYPE_UNSIGNED_SHORT
|
|
||||||
*/
|
|
||||||
val dataType : Short,
|
|
||||||
|
|
||||||
componentCount : Int,
|
|
||||||
|
|
||||||
// The ifd that this tag should be put in. the ID of the IFD this tag belongs to.
|
|
||||||
/*
|
|
||||||
* @see IfdData.TYPE_IFD_0
|
|
||||||
* @see IfdData.TYPE_IFD_1
|
|
||||||
* @see IfdData.TYPE_IFD_EXIF
|
|
||||||
* @see IfdData.TYPE_IFD_GPS
|
|
||||||
* @see IfdData.TYPE_IFD_INTEROPERABILITY
|
|
||||||
*/
|
|
||||||
var ifd : Int,
|
|
||||||
|
|
||||||
// If tag has defined count
|
|
||||||
private var mHasDefinedDefaultComponentCount : Boolean
|
|
||||||
) {
|
|
||||||
// Actual data count in tag (should be number of elements in value array)
|
|
||||||
/**
|
|
||||||
* Gets the component count of this tag.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: fix integer overflows with this
|
|
||||||
var componentCount : Int = componentCount
|
|
||||||
private set
|
|
||||||
|
|
||||||
// The value (array of elements of type Tag Type)
|
|
||||||
private var mValue : Any? = null
|
|
||||||
|
|
||||||
// Value offset in exif header. the offset of this tag.
|
|
||||||
// This is only valid if this data size > 4 and contains an offset to the location of the actual value.
|
|
||||||
var offset : Int = 0
|
|
||||||
|
|
||||||
// the total data size in bytes of the value of this tag.
|
|
||||||
val dataSize : Int
|
|
||||||
get() = componentCount * getElementSize(dataType)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this ExifTag contains value; otherwise, this tag will
|
|
||||||
* contain an offset value that is determined when the tag is written.
|
|
||||||
*/
|
|
||||||
val hasValue :Boolean
|
|
||||||
get() = mValue != null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as a byte array. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE].
|
|
||||||
*
|
|
||||||
* @return the value as a byte array, or null if the tag's value does not
|
|
||||||
* exist or cannot be converted to a byte array.
|
|
||||||
*/
|
|
||||||
val valueAsBytes : ByteArray?
|
|
||||||
get() = mValue as? ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as an array of longs. This method should be used for tags
|
|
||||||
* of type [.TYPE_UNSIGNED_LONG].
|
|
||||||
*
|
|
||||||
* @return the value as as an array of longs, or null if the tag's value
|
|
||||||
* does not exist or cannot be converted to an array of longs.
|
|
||||||
*/
|
|
||||||
val valueAsLongs : LongArray?
|
|
||||||
get() = mValue as? LongArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as an array of Rationals. This method should be used for
|
|
||||||
* tags of type [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
|
|
||||||
*
|
|
||||||
* @return the value as as an array of Rationals, or null if the tag's value
|
|
||||||
* does not exist or cannot be converted to an array of Rationals.
|
|
||||||
*/
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val valueAsRationals : Array<Rational>?
|
|
||||||
get() = mValue as? Array<Rational>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as an array of ints. This method should be used for tags
|
|
||||||
* of type [.TYPE_UNSIGNED_SHORT], [.TYPE_UNSIGNED_LONG].
|
|
||||||
*
|
|
||||||
* @return the value as as an array of ints, or null if the tag's value does
|
|
||||||
* not exist or cannot be converted to an array of ints.
|
|
||||||
*/
|
|
||||||
// Truncates
|
|
||||||
val valueAsInts : IntArray?
|
|
||||||
get() = when(val v = mValue) {
|
|
||||||
is LongArray -> IntArray(v.size) { v[it].toInt() }
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as a String. This method should be used for tags of type
|
|
||||||
* [.TYPE_ASCII].
|
|
||||||
*
|
|
||||||
* @return the value as a String, or null if the tag's value does not exist
|
|
||||||
* or cannot be converted to a String.
|
|
||||||
*/
|
|
||||||
val valueAsString : String?
|
|
||||||
get() = when(val v = mValue) {
|
|
||||||
is String -> v
|
|
||||||
is ByteArray -> String(v, US_ASCII)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the [.TYPE_ASCII] data.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException If the type is NOT
|
|
||||||
* [.TYPE_ASCII].
|
|
||||||
*/
|
|
||||||
protected val string : String
|
|
||||||
get() = valueAsString !!
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Get the converted ascii byte. Used by ExifOutputStream.
|
|
||||||
*/
|
|
||||||
val stringByte : ByteArray?
|
|
||||||
get() = mValue as? ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the component count of this tag. Call this function before
|
|
||||||
* setValue() if the length of value does not match the component count.
|
|
||||||
*/
|
|
||||||
fun forceSetComponentCount(count : Int) {
|
|
||||||
componentCount = count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets integer values into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_SHORT]. This method will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_SHORT],
|
|
||||||
* [.TYPE_UNSIGNED_LONG], or [.TYPE_LONG].
|
|
||||||
* * The value overflows.
|
|
||||||
* * The value.length does NOT match the component count in the definition
|
|
||||||
* for this tag.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun setValue(value : IntArray) : Boolean {
|
|
||||||
if(checkBadComponentCount(value.size)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(dataType != TYPE_UNSIGNED_SHORT && dataType != TYPE_LONG &&
|
|
||||||
dataType != TYPE_UNSIGNED_LONG) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(dataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
|
|
||||||
return false
|
|
||||||
} else if(dataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val data = LongArray(value.size)
|
|
||||||
for(i in value.indices) {
|
|
||||||
data[i] = value[i].toLong()
|
|
||||||
}
|
|
||||||
mValue = data
|
|
||||||
componentCount = value.size
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets integer value into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_SHORT], or [.TYPE_LONG]. This method
|
|
||||||
* will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_SHORT],
|
|
||||||
* [.TYPE_UNSIGNED_LONG], or [.TYPE_LONG].
|
|
||||||
* * The value overflows.
|
|
||||||
* * The component count in the definition of this tag is not 1.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun setValue(value : Int) = setValue(intArrayOf(value))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets long values into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_LONG]. This method will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_LONG].
|
|
||||||
* * The value overflows.
|
|
||||||
* * The value.length does NOT match the component count in the definition
|
|
||||||
* for this tag.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun setValue(value : LongArray) : Boolean {
|
|
||||||
if(checkBadComponentCount(value.size) || dataType != TYPE_UNSIGNED_LONG) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(checkOverflowForUnsignedLong(value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mValue = value
|
|
||||||
componentCount = value.size
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets long values into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_LONG]. This method will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_LONG].
|
|
||||||
* * The value overflows.
|
|
||||||
* * The component count in the definition for this tag is not 1.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun setValue(value : Long) = setValue(longArrayOf(value))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets Rational values into this tag. This method should be used for tags
|
|
||||||
* of type [.TYPE_UNSIGNED_RATIONAL], or [.TYPE_RATIONAL]. This
|
|
||||||
* method will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_RATIONAL]
|
|
||||||
* or [.TYPE_RATIONAL].
|
|
||||||
* * The value overflows.
|
|
||||||
* * The value.length does NOT match the component count in the definition
|
|
||||||
* for this tag.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @see Rational
|
|
||||||
*/
|
|
||||||
fun setValue(value : Array<Rational>) : Boolean {
|
|
||||||
if(checkBadComponentCount(value.size)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(dataType != TYPE_UNSIGNED_RATIONAL && dataType != TYPE_RATIONAL) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(dataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
|
|
||||||
return false
|
|
||||||
} else if(dataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
mValue = value
|
|
||||||
componentCount = value.size
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a Rational value into this tag. This method should be used for tags
|
|
||||||
* of type [.TYPE_UNSIGNED_RATIONAL], or [.TYPE_RATIONAL]. This
|
|
||||||
* method will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_RATIONAL]
|
|
||||||
* or [.TYPE_RATIONAL].
|
|
||||||
* * The value overflows.
|
|
||||||
* * The component count in the definition for this tag is not 1.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @see Rational
|
|
||||||
*/
|
|
||||||
fun setValue(value : Rational) =setValue(arrayOf(value))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets byte values into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_BYTE] or [.TYPE_UNDEFINED]. This method
|
|
||||||
* will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_BYTE] or
|
|
||||||
* [.TYPE_UNDEFINED] .
|
|
||||||
* * The length does NOT match the component count in the definition for
|
|
||||||
* this tag.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@JvmOverloads
|
|
||||||
fun setValue(value : ByteArray, offset : Int = 0, length : Int = value.size) : Boolean {
|
|
||||||
if(checkBadComponentCount(length)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if(dataType != TYPE_UNSIGNED_BYTE && dataType != TYPE_UNDEFINED) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
componentCount = length
|
|
||||||
mValue = ByteArray(length).also {
|
|
||||||
System.arraycopy(value, offset, it, 0, length)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets byte value into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_BYTE] or [.TYPE_UNDEFINED]. This method
|
|
||||||
* will fail if:
|
|
||||||
*
|
|
||||||
* * The component type of this tag is not [.TYPE_UNSIGNED_BYTE] or
|
|
||||||
* [.TYPE_UNDEFINED] .
|
|
||||||
* * The component count in the definition for this tag is not 1.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun setValue(value : Byte) = setValue(byteArrayOf(value))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the value for this tag using an appropriate setValue method for the
|
|
||||||
* given object. This method will fail if:
|
|
||||||
*
|
|
||||||
* * The corresponding setValue method for the class of the object passed
|
|
||||||
* in would fail.
|
|
||||||
* * There is no obvious way to cast the object passed in into an EXIF tag
|
|
||||||
* type.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Any> setValueAny(obj : T) : Boolean {
|
|
||||||
when(obj) {
|
|
||||||
// null -> return false
|
|
||||||
|
|
||||||
is String -> return setValue(obj)
|
|
||||||
is ByteArray -> return setValue(obj)
|
|
||||||
is IntArray -> return setValue(obj)
|
|
||||||
is LongArray -> return setValue(obj)
|
|
||||||
is Rational -> return setValue(obj)
|
|
||||||
is Byte -> return setValue(obj.toByte())
|
|
||||||
is Short -> return setValue(obj.toInt() and 0x0ffff)
|
|
||||||
is Int -> return setValue(obj.toInt())
|
|
||||||
is Long -> return setValue(obj.toLong())
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val ra = obj as? Array<Rational>
|
|
||||||
if(ra != null) return setValue(ra)
|
|
||||||
|
|
||||||
// Nulls in this array are treated as zeroes.
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val sa = obj as? Array<Short?>
|
|
||||||
if(sa != null) return setValue(IntArray(sa.size) {
|
|
||||||
(sa[it]?.toInt() ?: 0) and 0xffff
|
|
||||||
})
|
|
||||||
|
|
||||||
// Nulls in this array are treated as zeroes.
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val ia = obj as? Array<Int?>
|
|
||||||
if(ia != null) return setValue(IntArray(ia.size) { ia[it] ?: 0 })
|
|
||||||
|
|
||||||
// Nulls in this array are treated as zeroes.
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val la = obj as? Array<Long?>
|
|
||||||
if(la != null) return setValue(LongArray(la.size) { la[it] ?: 0L })
|
|
||||||
|
|
||||||
// Nulls in this array are treated as zeroes.
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val ba = obj as? Array<Byte?>
|
|
||||||
if(ba != null) return setValue(ByteArray(ba.size) { ba[it] ?: 0 })
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a timestamp to this tag. The method converts the timestamp with the
|
|
||||||
* format of "yyyy:MM:dd kk:mm:ss" and calls [.setValue]. This
|
|
||||||
* method will fail if the data type is not [.TYPE_ASCII] or the
|
|
||||||
* component count of this tag is not 20 or undefined.
|
|
||||||
*
|
|
||||||
* @param time the number of milliseconds since Jan. 1, 1970 GMT
|
|
||||||
* @return true on success
|
|
||||||
*/
|
|
||||||
fun setValueTime(time : Long) : Boolean {
|
|
||||||
// synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe
|
|
||||||
synchronized(TIME_FORMAT) {
|
|
||||||
return setValue(TIME_FORMAT.format(Date(time)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a string value into this tag. This method should be used for tags of
|
|
||||||
* type [.TYPE_ASCII]. The string is converted to an ASCII string.
|
|
||||||
* Characters that cannot be converted are replaced with '?'. The length of
|
|
||||||
* the string must be equal to either (component count -1) or (component
|
|
||||||
* count). The final byte will be set to the string null terminator '\0',
|
|
||||||
* overwriting the last character in the string if the value.length is equal
|
|
||||||
* to the component count. This method will fail if:
|
|
||||||
*
|
|
||||||
* * The data type is not [.TYPE_ASCII] or [.TYPE_UNDEFINED].
|
|
||||||
* * The length of the string is not equal to (component count -1) or
|
|
||||||
* (component count) in the definition for this tag.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun setValue(value : String) : Boolean {
|
|
||||||
if(dataType != TYPE_ASCII && dataType != TYPE_UNDEFINED) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val buf = value.toByteArray(US_ASCII)
|
|
||||||
|
|
||||||
val finalBuf = when {
|
|
||||||
buf.isNotEmpty() -> when {
|
|
||||||
buf[buf.size - 1].toInt() == 0 || dataType == TYPE_UNDEFINED -> buf
|
|
||||||
else -> buf.copyOf(buf.size + 1)
|
|
||||||
}
|
|
||||||
dataType == TYPE_ASCII && componentCount == 1 -> byteArrayOf(0)
|
|
||||||
else -> buf
|
|
||||||
}
|
|
||||||
val count = finalBuf.size
|
|
||||||
if(checkBadComponentCount(count)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
componentCount = count
|
|
||||||
mValue = finalBuf
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkBadComponentCount(count : Int) : Boolean {
|
|
||||||
return mHasDefinedDefaultComponentCount && componentCount != count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as a String. This method should be used for tags of type
|
|
||||||
* [.TYPE_ASCII].
|
|
||||||
*
|
|
||||||
* @param defaultValue the String to return if the tag's value does not
|
|
||||||
* exist or cannot be converted to a String.
|
|
||||||
* @return the tag's value as a String, or the defaultValue.
|
|
||||||
*/
|
|
||||||
fun getValueAsString(defaultValue : String) : String {
|
|
||||||
return valueAsString ?: defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as a byte. If there are more than 1 bytes in this value,
|
|
||||||
* gets the first byte. This method should be used for tags of type
|
|
||||||
* [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE].
|
|
||||||
*
|
|
||||||
* @param defaultValue the byte to return if tag's value does not exist or
|
|
||||||
* cannot be converted to a byte.
|
|
||||||
* @return the tag's value as a byte, or the defaultValue.
|
|
||||||
*/
|
|
||||||
fun getValueAsByte(defaultValue : Byte) : Byte {
|
|
||||||
val array = valueAsBytes
|
|
||||||
return if(array?.isNotEmpty() == true) array[0] else defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as a Rational. If there are more than 1 Rationals in this
|
|
||||||
* value, gets the first one. This method should be used for tags of type
|
|
||||||
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
|
|
||||||
*
|
|
||||||
* @param defaultValue the numerator of the Rational to return if tag's
|
|
||||||
* value does not exist or cannot be converted to a Rational (the
|
|
||||||
* denominator will be 1).
|
|
||||||
* @return the tag's value as a Rational, or the defaultValue.
|
|
||||||
*/
|
|
||||||
fun getValueAsRational(defaultValue : Long) : Rational =
|
|
||||||
getValueAsRational(Rational(defaultValue, 1))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as a Rational. If there are more than 1 Rationals in this
|
|
||||||
* value, gets the first one. This method should be used for tags of type
|
|
||||||
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
|
|
||||||
*
|
|
||||||
* @param defaultValue the Rational to return if tag's value does not exist
|
|
||||||
* or cannot be converted to a Rational.
|
|
||||||
* @return the tag's value as a Rational, or the defaultValue.
|
|
||||||
*/
|
|
||||||
private fun getValueAsRational(defaultValue : Rational) : Rational {
|
|
||||||
val array = valueAsRationals
|
|
||||||
return if(array?.isNotEmpty() == true) array[0] else defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value as an int. If there are more than 1 ints in this value,
|
|
||||||
* gets the first one. This method should be used for tags of type
|
|
||||||
* [.TYPE_UNSIGNED_SHORT], [.TYPE_UNSIGNED_LONG].
|
|
||||||
*
|
|
||||||
* @param defaultValue the int to return if tag's value does not exist or
|
|
||||||
* cannot be converted to an int.
|
|
||||||
* @return the tag's value as a int, or the defaultValue.
|
|
||||||
*/
|
|
||||||
fun getValueAsInt(defaultValue : Int) : Int {
|
|
||||||
val array = valueAsInts
|
|
||||||
return if(array?.isNotEmpty() == true) array[0] else defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value or null if none exists. If there are more than 1 longs in
|
|
||||||
* this value, gets the first one. This method should be used for tags of
|
|
||||||
* type [.TYPE_UNSIGNED_LONG].
|
|
||||||
*
|
|
||||||
* @param defaultValue the long to return if tag's value does not exist or
|
|
||||||
* cannot be converted to a long.
|
|
||||||
* @return the tag's value as a long, or the defaultValue.
|
|
||||||
*/
|
|
||||||
fun getValueAsLong(defaultValue : Long) : Long {
|
|
||||||
val array = valueAsLongs
|
|
||||||
return if(array?.isNotEmpty() == true) array[0] else defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the tag's value or null if none exists.
|
|
||||||
*/
|
|
||||||
fun getValue() : Any? {
|
|
||||||
return mValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a long representation of the value.
|
|
||||||
*
|
|
||||||
* @param defaultValue value to return if there is no value or value is a
|
|
||||||
* rational with a denominator of 0.
|
|
||||||
* @return the tag's value as a long, or defaultValue if no representation
|
|
||||||
* exists.
|
|
||||||
*/
|
|
||||||
fun forceGetValueAsLong(defaultValue : Long) : Long {
|
|
||||||
when(val v = mValue) {
|
|
||||||
is LongArray -> if(v.isNotEmpty()) return v[0]
|
|
||||||
is ByteArray -> if(v.isNotEmpty()) return v[0].toLong()
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val r = valueAsRationals
|
|
||||||
if(r?.isNotEmpty() == true && r[0].denominator != 0L) {
|
|
||||||
return r[0].toDouble().toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value for type [.TYPE_ASCII], [.TYPE_LONG],
|
|
||||||
* [.TYPE_UNDEFINED], [.TYPE_UNSIGNED_BYTE],
|
|
||||||
* [.TYPE_UNSIGNED_LONG], or [.TYPE_UNSIGNED_SHORT]. For
|
|
||||||
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL], call
|
|
||||||
* [.getRational] instead.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException if the data type is
|
|
||||||
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
|
|
||||||
*/
|
|
||||||
fun getValueAt(index : Int) : Long {
|
|
||||||
return when(val v = mValue) {
|
|
||||||
is LongArray -> v[index]
|
|
||||||
is ByteArray -> v[index].toLong()
|
|
||||||
else -> error(
|
|
||||||
"Cannot get integer value from ${convertTypeToString(dataType)}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL] data.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException If the type is NOT
|
|
||||||
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
|
|
||||||
*/
|
|
||||||
fun getRational(index : Int) : Rational? {
|
|
||||||
require(! (dataType != TYPE_RATIONAL && dataType != TYPE_UNSIGNED_RATIONAL)) {
|
|
||||||
"Cannot get RATIONAL value from " + convertTypeToString(dataType)
|
|
||||||
}
|
|
||||||
return valueAsRationals?.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE] data.
|
|
||||||
*
|
|
||||||
* @param buf the byte array in which to store the bytes read.
|
|
||||||
* @param offset the initial position in buffer to store the bytes.
|
|
||||||
* @param length the maximum number of bytes to store in buffer. If length >
|
|
||||||
* component count, only the valid bytes will be stored.
|
|
||||||
* @throws IllegalArgumentException If the type is NOT
|
|
||||||
* [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE].
|
|
||||||
*/
|
|
||||||
@JvmOverloads
|
|
||||||
fun getBytes(buf : ByteArray, offset : Int = 0, length : Int = buf.size) {
|
|
||||||
require(! (dataType != TYPE_UNDEFINED && dataType != TYPE_UNSIGNED_BYTE)) {
|
|
||||||
"Cannot get BYTE value from " + convertTypeToString(
|
|
||||||
dataType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
System.arraycopy(
|
|
||||||
mValue !!,
|
|
||||||
0,
|
|
||||||
buf,
|
|
||||||
offset,
|
|
||||||
if(length > componentCount) componentCount else length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasDefinedCount : Boolean
|
|
||||||
get() = mHasDefinedDefaultComponentCount
|
|
||||||
set(value) {
|
|
||||||
mHasDefinedDefaultComponentCount = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkOverflowForUnsignedShort(value : IntArray) : Boolean =
|
|
||||||
null != value.find { it !in 0 .. UNSIGNED_SHORT_MAX }
|
|
||||||
|
|
||||||
private fun checkOverflowForUnsignedLong(value : LongArray) : Boolean =
|
|
||||||
null != value.find { it !in 0 .. UNSIGNED_LONG_MAX }
|
|
||||||
|
|
||||||
private fun checkOverflowForUnsignedLong(value : IntArray) : Boolean =
|
|
||||||
null != value.find { it < 0 }
|
|
||||||
|
|
||||||
private fun checkOverflowForUnsignedRational(value : Array<Rational>) : Boolean =
|
|
||||||
null != value.find { it.numerator !in 0 .. UNSIGNED_LONG_MAX || it.denominator !in 0 .. UNSIGNED_LONG_MAX }
|
|
||||||
|
|
||||||
private fun checkOverflowForRational(value : Array<Rational>) : Boolean =
|
|
||||||
null != value.find { it.numerator !in LONG_MIN .. LONG_MAX || it.denominator !in LONG_MIN .. LONG_MAX }
|
|
||||||
|
|
||||||
override fun hashCode() : Int {
|
|
||||||
var result = tagId.toInt()
|
|
||||||
result = 31 * result + dataType.toInt()
|
|
||||||
result = 31 * result + ifd
|
|
||||||
result = 31 * result + componentCount
|
|
||||||
result = 31 * result + offset
|
|
||||||
result = 31 * result + mHasDefinedDefaultComponentCount.hashCode()
|
|
||||||
result = 31 * result + (mValue?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other : Any?) : Boolean {
|
|
||||||
if(other !is ExifTag) return false
|
|
||||||
|
|
||||||
if(other.tagId != this.tagId
|
|
||||||
|| other.componentCount != this.componentCount
|
|
||||||
|| other.dataType != this.dataType
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val va = this.mValue
|
|
||||||
val vb = other.mValue
|
|
||||||
|
|
||||||
return when {
|
|
||||||
|
|
||||||
va == null -> vb == null
|
|
||||||
|
|
||||||
vb == null -> false
|
|
||||||
|
|
||||||
va is LongArray -> when(vb) {
|
|
||||||
is LongArray -> Arrays.equals(va, vb)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
va is ByteArray -> when(vb) {
|
|
||||||
is ByteArray -> Arrays.equals(va, vb)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
va is Array<*> && va.isArrayOf<Rational>() -> when {
|
|
||||||
vb is Array<*> && vb.isArrayOf<Rational>() -> Arrays.equals(va, vb)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> va == vb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() : String {
|
|
||||||
val strTagId = String.format("%04X", tagId)
|
|
||||||
return "tag id: $strTagId\nifd id: $ifd\ntype: ${convertTypeToString(dataType)}\ncount: $componentCount\noffset: $offset\nvalue: ${forceGetValueAsString()}\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a string representation of the value.
|
|
||||||
*/
|
|
||||||
private fun forceGetValueAsString() : String {
|
|
||||||
when(val v = mValue) {
|
|
||||||
|
|
||||||
null -> return ""
|
|
||||||
|
|
||||||
is ByteArray -> return when(dataType) {
|
|
||||||
TYPE_ASCII -> String(v, US_ASCII)
|
|
||||||
else -> Arrays.toString(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
is LongArray -> return when {
|
|
||||||
v.size == 1 -> v[0].toString()
|
|
||||||
else -> Arrays.toString(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Array<*> -> return when {
|
|
||||||
v.size == 1 -> v[0]?.toString() ?: ""
|
|
||||||
else -> Arrays.toString(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> return v.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* The BYTE type in the EXIF standard. An 8-bit unsigned integer.
|
|
||||||
*/
|
|
||||||
const val TYPE_UNSIGNED_BYTE : Short = 1
|
|
||||||
/**
|
|
||||||
* The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit
|
|
||||||
* ASCII code. The final byte is terminated with NULL.
|
|
||||||
*/
|
|
||||||
const val TYPE_ASCII : Short = 2
|
|
||||||
/**
|
|
||||||
* The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer
|
|
||||||
*/
|
|
||||||
const val TYPE_UNSIGNED_SHORT : Short = 3
|
|
||||||
/**
|
|
||||||
* The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer
|
|
||||||
*/
|
|
||||||
const val TYPE_UNSIGNED_LONG : Short = 4
|
|
||||||
/**
|
|
||||||
* The RATIONAL type of EXIF standard. It consists of two LONGs. The first
|
|
||||||
* one is the numerator and the second one expresses the denominator.
|
|
||||||
*/
|
|
||||||
const val TYPE_UNSIGNED_RATIONAL : Short = 5
|
|
||||||
/**
|
|
||||||
* The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any
|
|
||||||
* value depending on the field definition.
|
|
||||||
*/
|
|
||||||
const val TYPE_UNDEFINED : Short = 7
|
|
||||||
/**
|
|
||||||
* The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer
|
|
||||||
* (2's complement notation).
|
|
||||||
*/
|
|
||||||
const val TYPE_LONG : Short = 9
|
|
||||||
/**
|
|
||||||
* The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first
|
|
||||||
* one is the numerator and the second one is the denominator.
|
|
||||||
*/
|
|
||||||
const val TYPE_RATIONAL : Short = 10
|
|
||||||
internal const val SIZE_UNDEFINED = 0
|
|
||||||
private val TYPE_TO_SIZE_MAP = IntArray(11)
|
|
||||||
private const val UNSIGNED_SHORT_MAX = 65535
|
|
||||||
private const val UNSIGNED_LONG_MAX = 4294967295L
|
|
||||||
private const val LONG_MAX = Integer.MAX_VALUE.toLong()
|
|
||||||
private const val LONG_MIN = Integer.MIN_VALUE.toLong()
|
|
||||||
|
|
||||||
init {
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE.toInt()] = 1
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_ASCII.toInt()] = 1
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT.toInt()] = 2
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG.toInt()] = 4
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL.toInt()] = 8
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_UNDEFINED.toInt()] = 1
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_LONG.toInt()] = 4
|
|
||||||
TYPE_TO_SIZE_MAP[TYPE_RATIONAL.toInt()] = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
private val TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd kk:mm:ss", Locale.ENGLISH)
|
|
||||||
private val US_ASCII = Charset.forName("US-ASCII")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given IFD is a valid IFD.
|
|
||||||
*/
|
|
||||||
fun isValidIfd(ifdId : Int) : Boolean =
|
|
||||||
when(ifdId) {
|
|
||||||
IfdData.TYPE_IFD_0,
|
|
||||||
IfdData.TYPE_IFD_1,
|
|
||||||
IfdData.TYPE_IFD_EXIF,
|
|
||||||
IfdData.TYPE_IFD_INTEROPERABILITY,
|
|
||||||
IfdData.TYPE_IFD_GPS -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if a given type is a valid tag type.
|
|
||||||
*/
|
|
||||||
fun isValidType(type : Short) : Boolean =
|
|
||||||
when(type) {
|
|
||||||
TYPE_UNSIGNED_BYTE,
|
|
||||||
TYPE_ASCII,
|
|
||||||
TYPE_UNSIGNED_SHORT,
|
|
||||||
TYPE_UNSIGNED_LONG,
|
|
||||||
TYPE_UNSIGNED_RATIONAL,
|
|
||||||
TYPE_UNDEFINED,
|
|
||||||
TYPE_LONG,
|
|
||||||
TYPE_RATIONAL -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the element size of the given data type in bytes.
|
|
||||||
*
|
|
||||||
* @see .TYPE_ASCII
|
|
||||||
* @see .TYPE_LONG
|
|
||||||
* @see .TYPE_RATIONAL
|
|
||||||
* @see .TYPE_UNDEFINED
|
|
||||||
* @see .TYPE_UNSIGNED_BYTE
|
|
||||||
* @see .TYPE_UNSIGNED_LONG
|
|
||||||
* @see .TYPE_UNSIGNED_RATIONAL
|
|
||||||
* @see .TYPE_UNSIGNED_SHORT
|
|
||||||
*/
|
|
||||||
fun getElementSize(type : Short) : Int = TYPE_TO_SIZE_MAP[type.toInt()]
|
|
||||||
|
|
||||||
private fun convertTypeToString(type : Short) : String =
|
|
||||||
when(type) {
|
|
||||||
TYPE_UNSIGNED_BYTE -> "UNSIGNED_BYTE"
|
|
||||||
TYPE_ASCII -> "ASCII"
|
|
||||||
TYPE_UNSIGNED_SHORT -> "UNSIGNED_SHORT"
|
|
||||||
TYPE_UNSIGNED_LONG -> "UNSIGNED_LONG"
|
|
||||||
TYPE_UNSIGNED_RATIONAL -> "UNSIGNED_RATIONAL"
|
|
||||||
TYPE_UNDEFINED -> "UNDEFINED"
|
|
||||||
TYPE_LONG -> "LONG"
|
|
||||||
TYPE_RATIONAL -> "RATIONAL"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by alessandro on 20/04/14.
|
|
||||||
*/
|
|
||||||
object ExifUtil {
|
|
||||||
|
|
||||||
private val formatter = DecimalFormat.getInstance()
|
|
||||||
|
|
||||||
fun processLensSpecifications(values : Array<Rational>) : String {
|
|
||||||
val min_focal = values[0]
|
|
||||||
val max_focal = values[1]
|
|
||||||
val min_f = values[2]
|
|
||||||
val max_f = values[3]
|
|
||||||
|
|
||||||
formatter.maximumFractionDigits = 1
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
sb.append(formatter.format(min_focal.toDouble()))
|
|
||||||
sb.append("-")
|
|
||||||
sb.append(formatter.format(max_focal.toDouble()))
|
|
||||||
sb.append("mm f/")
|
|
||||||
sb.append(formatter.format(min_f.toDouble()))
|
|
||||||
sb.append("-")
|
|
||||||
sb.append(formatter.format(max_f.toDouble()))
|
|
||||||
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import java.util.HashMap
|
|
||||||
|
|
||||||
// This class stores all the tags in an IFD.
|
|
||||||
// an IfdData with given IFD ID.
|
|
||||||
internal class IfdData(
|
|
||||||
val id : Int // the ID of this IFD.
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* The constants of the IFD ID defined in EXIF spec.
|
|
||||||
*/
|
|
||||||
const val TYPE_IFD_0 = 0
|
|
||||||
const val TYPE_IFD_1 = 1
|
|
||||||
const val TYPE_IFD_EXIF = 2
|
|
||||||
const val TYPE_IFD_INTEROPERABILITY = 3
|
|
||||||
const val TYPE_IFD_GPS = 4
|
|
||||||
/* This is used in ExifData to allocate enough IfdData */
|
|
||||||
const val TYPE_IFD_COUNT = 5
|
|
||||||
|
|
||||||
val list = intArrayOf(
|
|
||||||
TYPE_IFD_0,
|
|
||||||
TYPE_IFD_1,
|
|
||||||
TYPE_IFD_EXIF,
|
|
||||||
TYPE_IFD_INTEROPERABILITY,
|
|
||||||
TYPE_IFD_GPS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mExifTags = HashMap<Short, ExifTag>()
|
|
||||||
|
|
||||||
// the offset of next IFD.
|
|
||||||
var offsetToNextIfd = 0
|
|
||||||
|
|
||||||
// the tags count in the IFD.
|
|
||||||
val tagCount : Int
|
|
||||||
get() = mExifTags.size
|
|
||||||
|
|
||||||
// Collection the contains all [ExifTag] in this IFD.
|
|
||||||
val allTagsCollection : Collection<ExifTag>
|
|
||||||
get() = mExifTags.values
|
|
||||||
|
|
||||||
// checkCollision
|
|
||||||
fun contains(tagId : Short) : Boolean {
|
|
||||||
return mExifTags[tagId] != null
|
|
||||||
}
|
|
||||||
|
|
||||||
// the [ExifTag] with given tag id.
|
|
||||||
// null if there is no such tag.
|
|
||||||
fun getTag(tagId : Short) : ExifTag? {
|
|
||||||
return mExifTags[tagId]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds or replaces a [ExifTag].
|
|
||||||
fun setTag(tag : ExifTag) : ExifTag? {
|
|
||||||
tag.ifd = id
|
|
||||||
return mExifTags.put(tag.tagId, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes the tag of the given ID
|
|
||||||
fun removeTag(tagId : Short) {
|
|
||||||
mExifTags.remove(tagId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if all tags in this two IFDs are equal. Note that tags of
|
|
||||||
* IFDs offset or thumbnail offset will be ignored.
|
|
||||||
*/
|
|
||||||
override fun equals(other : Any?) : Boolean {
|
|
||||||
if(other is IfdData) {
|
|
||||||
if(other === this) return true
|
|
||||||
if(other.id == id && other.tagCount == tagCount) {
|
|
||||||
for(tag in other.allTagsCollection) {
|
|
||||||
if(ExifInterface.isOffsetTag(tag.tagId)) continue
|
|
||||||
if(tag != mExifTags[tag.tagId]) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() : Int {
|
|
||||||
var result = id
|
|
||||||
result = 31 * result + mExifTags.hashCode()
|
|
||||||
result = 31 * result + offsetToNextIfd
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
|
||||||
object JpegHeader {
|
|
||||||
/** Start Of Image */
|
|
||||||
const val TAG_SOI = 0xD8
|
|
||||||
|
|
||||||
/** JFIF (JPEG File Interchange Format) */
|
|
||||||
const val TAG_M_JFIF = 0xE0
|
|
||||||
|
|
||||||
/** EXIF table */
|
|
||||||
const val TAG_M_EXIF = 0xE1
|
|
||||||
|
|
||||||
/** Product Information Comment */
|
|
||||||
const val TAG_M_COM = 0xFE
|
|
||||||
|
|
||||||
/** Quantization Table */
|
|
||||||
const val TAG_M_DQT = 0xDB
|
|
||||||
|
|
||||||
/** Start of frame */
|
|
||||||
const val TAG_M_SOF0 = 0xC0
|
|
||||||
const val TAG_M_SOF1 = 0xC1
|
|
||||||
const val TAG_M_SOF2 = 0xC2
|
|
||||||
const val TAG_M_SOF3 = 0xC3
|
|
||||||
const val TAG_M_DHT = 0xC4
|
|
||||||
const val TAG_M_SOF5 = 0xC5
|
|
||||||
const val TAG_M_SOF6 = 0xC6
|
|
||||||
const val TAG_M_SOF7 = 0xC7
|
|
||||||
const val TAG_M_SOF9 = 0xC9
|
|
||||||
const val TAG_M_SOF10 = 0xCA
|
|
||||||
const val TAG_M_SOF11 = 0xCB
|
|
||||||
const val TAG_M_SOF13 = 0xCD
|
|
||||||
const val TAG_M_SOF14 = 0xCE
|
|
||||||
const val TAG_M_SOF15 = 0xCF
|
|
||||||
|
|
||||||
/** Start Of Scan */
|
|
||||||
const val TAG_M_SOS = 0xDA
|
|
||||||
|
|
||||||
/** End of Image */
|
|
||||||
const val TAG_M_EOI = 0xD9
|
|
||||||
|
|
||||||
const val TAG_M_IPTC = 0xED
|
|
||||||
|
|
||||||
/** default JFIF Header bytes */
|
|
||||||
val JFIF_HEADER = byteArrayOf(
|
|
||||||
0xff.toByte(),
|
|
||||||
TAG_M_JFIF.toByte(),
|
|
||||||
0x00,
|
|
||||||
0x10,
|
|
||||||
'J'.toByte(),
|
|
||||||
'F'.toByte(),
|
|
||||||
'I'.toByte(),
|
|
||||||
'F'.toByte(),
|
|
||||||
0x00,
|
|
||||||
0x01,
|
|
||||||
0x01,
|
|
||||||
0x01,
|
|
||||||
0x01,
|
|
||||||
0x2C,
|
|
||||||
0x01,
|
|
||||||
0x2C,
|
|
||||||
0x00,
|
|
||||||
0x00
|
|
||||||
)
|
|
||||||
|
|
||||||
const val SOI = 0xFFD8.toShort()
|
|
||||||
const val M_EXIF = 0xFFE1.toShort()
|
|
||||||
const val M_JFIF = 0xFFE0.toShort()
|
|
||||||
const val M_EOI = 0xFFD9.toShort()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SOF (start of frame). All value between M_SOF0 and SOF15 is SOF marker except for M_DHT, JPG,
|
|
||||||
* and DAC marker.
|
|
||||||
*/
|
|
||||||
const val M_SOF0 = 0xFFC0.toShort()
|
|
||||||
const val M_SOF1 = 0xFFC1.toShort()
|
|
||||||
const val M_SOF2 = 0xFFC2.toShort()
|
|
||||||
const val M_SOF3 = 0xFFC3.toShort()
|
|
||||||
const val M_SOF5 = 0xFFC5.toShort()
|
|
||||||
const val M_SOF6 = 0xFFC6.toShort()
|
|
||||||
const val M_SOF7 = 0xFFC7.toShort()
|
|
||||||
const val M_SOF9 = 0xFFC9.toShort()
|
|
||||||
const val M_SOF10 = 0xFFCA.toShort()
|
|
||||||
const val M_SOF11 = 0xFFCB.toShort()
|
|
||||||
const val M_SOF13 = 0xFFCD.toShort()
|
|
||||||
const val M_SOF14 = 0xFFCE.toShort()
|
|
||||||
const val M_SOF15 = 0xFFCF.toShort()
|
|
||||||
const val M_DHT = 0xFFC4.toShort()
|
|
||||||
const val JPG = 0xFFC8.toShort()
|
|
||||||
const val DAC = 0xFFCC.toShort()
|
|
||||||
|
|
||||||
/** Define quantization table */
|
|
||||||
const val M_DQT = 0xFFDB.toShort()
|
|
||||||
|
|
||||||
/** IPTC marker */
|
|
||||||
const val M_IPTC = 0xFFED.toShort()
|
|
||||||
|
|
||||||
/** Start of scan (begins compressed data */
|
|
||||||
const val M_SOS = 0xFFDA.toShort()
|
|
||||||
|
|
||||||
/** Comment section * */
|
|
||||||
const val M_COM = 0xFFFE.toShort() // Comment section
|
|
||||||
|
|
||||||
fun isSofMarker(marker : Short) : Boolean {
|
|
||||||
return marker >= M_SOF0 && marker <= M_SOF15 && marker != M_DHT && marker != JPG && marker != DAC
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The rational data type of EXIF tag. Contains a pair of longs representing the
|
|
||||||
* numerator and denominator of a Rational number.
|
|
||||||
*/
|
|
||||||
class Rational(
|
|
||||||
|
|
||||||
//the numerator of the rational.
|
|
||||||
val numerator : Long = 0,
|
|
||||||
|
|
||||||
//the denominator of the rational
|
|
||||||
val denominator : Long = 1
|
|
||||||
) {
|
|
||||||
|
|
||||||
// copy from a Rational.
|
|
||||||
@Suppress("unused")
|
|
||||||
constructor(r : Rational) : this(
|
|
||||||
numerator = r.numerator,
|
|
||||||
denominator = r.denominator
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun equals(other : Any?) : Boolean {
|
|
||||||
return when {
|
|
||||||
other === null -> false
|
|
||||||
other === this -> true
|
|
||||||
other is Rational -> numerator == other.numerator && denominator == other.denominator
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() : Int =
|
|
||||||
31 * numerator.hashCode() + denominator.hashCode()
|
|
||||||
|
|
||||||
override fun toString() : String = "$numerator/$denominator"
|
|
||||||
|
|
||||||
// Gets the rational value as type double.
|
|
||||||
// Will cause a divide-by-zero error if the denominator is 0.
|
|
||||||
fun toDouble() : Double = numerator.toDouble() / denominator.toDouble()
|
|
||||||
}
|
|
|
@ -1,149 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2.utils
|
|
||||||
|
|
||||||
import java.io.EOFException
|
|
||||||
import java.io.FilterInputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
internal class CountedDataInputStream constructor(`in` : InputStream) :
|
|
||||||
FilterInputStream(`in`) {
|
|
||||||
|
|
||||||
// allocate a byte buffer for a long value;
|
|
||||||
private val mByteArray = ByteArray(8)
|
|
||||||
private val mByteBuffer = ByteBuffer.wrap(mByteArray)
|
|
||||||
var readByteCount = 0
|
|
||||||
private set
|
|
||||||
var end = 0
|
|
||||||
|
|
||||||
var byteOrder : ByteOrder
|
|
||||||
get() = mByteBuffer.order()
|
|
||||||
set(order) {
|
|
||||||
mByteBuffer.order(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun read(b : ByteArray) : Int {
|
|
||||||
val r = `in`.read(b)
|
|
||||||
readByteCount += if(r >= 0) r else 0
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun read() : Int {
|
|
||||||
val r = `in`.read()
|
|
||||||
readByteCount += if(r >= 0) 1 else 0
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun read(b : ByteArray, off : Int, len : Int) : Int {
|
|
||||||
val r = `in`.read(b, off, len)
|
|
||||||
readByteCount += if(r >= 0) r else 0
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun skip(length : Long) : Long {
|
|
||||||
val skip = `in`.skip(length)
|
|
||||||
readByteCount += skip.toInt()
|
|
||||||
return skip
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun skipTo(target : Long) {
|
|
||||||
val cur = readByteCount.toLong()
|
|
||||||
val diff = target - cur
|
|
||||||
if(diff < 0) throw IndexOutOfBoundsException("skipTo: negative move")
|
|
||||||
skipOrThrow(diff)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun skipOrThrow(length : Long) {
|
|
||||||
if(skip(length) != length) throw EOFException()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readUnsignedShort() : Int = readShort().toInt() and 0xffff
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readShort() : Short {
|
|
||||||
readOrThrow(mByteArray, 0, 2)
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
return mByteBuffer.short
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readByte() : Byte {
|
|
||||||
readOrThrow(mByteArray, 0, 1)
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
return mByteBuffer.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readUnsignedByte() : Int {
|
|
||||||
readOrThrow(mByteArray, 0, 1)
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
return mByteBuffer.get().toInt() and 0xff
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
@JvmOverloads
|
|
||||||
fun readOrThrow(b : ByteArray, off : Int = 0, len : Int = b.size) {
|
|
||||||
val r = read(b, off, len)
|
|
||||||
if(r != len) throw EOFException()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readUnsignedInt() : Long {
|
|
||||||
return readInt().toLong() and 0xffffffffL
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readInt() : Int {
|
|
||||||
readOrThrow(mByteArray, 0, 4)
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
return mByteBuffer.int
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readLong() : Long {
|
|
||||||
readOrThrow(mByteArray, 0, 8)
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
return mByteBuffer.long
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readString(n : Int) : String {
|
|
||||||
val buf = ByteArray(n)
|
|
||||||
readOrThrow(buf)
|
|
||||||
return String(buf, StandardCharsets.UTF_8)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readString(n : Int, charset : Charset) : String {
|
|
||||||
val buf = ByteArray(n)
|
|
||||||
readOrThrow(buf)
|
|
||||||
return String(buf, charset)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package it.sephiroth.android.library.exif2.utils
|
|
||||||
|
|
||||||
import it.sephiroth.android.library.exif2.Rational
|
|
||||||
import java.io.FilterOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
|
|
||||||
internal class OrderedDataOutputStream(out : OutputStream) : FilterOutputStream(out) {
|
|
||||||
private val mByteBuffer = ByteBuffer.allocate(4)
|
|
||||||
|
|
||||||
fun setByteOrder(order : ByteOrder) : OrderedDataOutputStream {
|
|
||||||
mByteBuffer.order(order)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeShort(value : Short) : OrderedDataOutputStream {
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
mByteBuffer.putShort(value)
|
|
||||||
out.write(mByteBuffer.array(), 0, 2)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeRational(rational : Rational) : OrderedDataOutputStream {
|
|
||||||
writeInt(rational.numerator.toInt())
|
|
||||||
writeInt(rational.denominator.toInt())
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun writeInt(value : Int) : OrderedDataOutputStream {
|
|
||||||
mByteBuffer.rewind()
|
|
||||||
mByteBuffer.putInt(value)
|
|
||||||
out.write(mByteBuffer.array())
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package it.sephiroth.android.library.exif2.utils
|
|
||||||
|
|
||||||
internal fun <E> Collection<E>?.notEmpty() : Collection<E>? =
|
|
||||||
if(this?.isNotEmpty() == true) this else null
|
|
||||||
|
|
||||||
internal fun <E> List<E>?.notEmpty() : List<E>? =
|
|
||||||
if(this?.isNotEmpty() == true) this else null
|
|
|
@ -1,115 +0,0 @@
|
||||||
package it.sephiroth.android.library.exif2
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.SparseIntArray
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Test
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
|
|
||||||
class Test1 {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testLog() {
|
|
||||||
Log.v("TEST", "test")
|
|
||||||
assertTrue("using android.util.Log", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSparseIntArray() {
|
|
||||||
val a = SparseIntArray()
|
|
||||||
a.put(1, 2)
|
|
||||||
assertTrue("get existing value", a[1] == 2)
|
|
||||||
assertTrue("fallback to default value ", a.get(0, - 1) == - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get File object from files in src/test/resources/
|
|
||||||
private fun getFile(fileName : String) : File {
|
|
||||||
return when(val resource = this.javaClass.classLoader !!.getResource(fileName)) {
|
|
||||||
null -> error("missing file $fileName")
|
|
||||||
else -> File(resource.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOrientation(fileName : String) : Pair<Int?, Throwable?> =
|
|
||||||
try {
|
|
||||||
val o = FileInputStream(getFile(fileName)).use { inStream ->
|
|
||||||
ExifInterface()
|
|
||||||
.readExif(
|
|
||||||
inStream,
|
|
||||||
ExifInterface.Options.OPTION_IFD_0
|
|
||||||
or ExifInterface.Options.OPTION_IFD_1
|
|
||||||
or ExifInterface.Options.OPTION_IFD_EXIF
|
|
||||||
)
|
|
||||||
.getTagIntValue(ExifInterface.TAG_ORIENTATION)
|
|
||||||
}
|
|
||||||
Pair(o, null)
|
|
||||||
} catch(ex : Throwable) {
|
|
||||||
Pair(null, ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getThumbnailBytes(fileName : String) : Pair<ByteArray?, Throwable?> =
|
|
||||||
try {
|
|
||||||
val o = FileInputStream(getFile(fileName)).use { inStream ->
|
|
||||||
ExifInterface()
|
|
||||||
.readExif(
|
|
||||||
inStream,
|
|
||||||
ExifInterface.Options.OPTION_IFD_0
|
|
||||||
or ExifInterface.Options.OPTION_IFD_1
|
|
||||||
or ExifInterface.Options.OPTION_IFD_EXIF
|
|
||||||
)
|
|
||||||
.thumbnailBytes
|
|
||||||
}
|
|
||||||
Pair(o, null)
|
|
||||||
} catch(ex : Throwable) {
|
|
||||||
Pair(null, ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNotJpeg() {
|
|
||||||
fun testNotJpegSub(fileName : String) {
|
|
||||||
val (o, ex) = getOrientation(fileName)
|
|
||||||
assertTrue("testNotJpegSub", o == null && ex != null)
|
|
||||||
}
|
|
||||||
testNotJpegSub("test.gif")
|
|
||||||
testNotJpegSub("test.png")
|
|
||||||
testNotJpegSub("test.webp")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testJpeg() {
|
|
||||||
var fileName : String
|
|
||||||
var rvO : Pair<Int?, Throwable?>
|
|
||||||
var rvT : Pair<ByteArray?, Throwable?>
|
|
||||||
|
|
||||||
// this file has orientation 6.
|
|
||||||
fileName = "test3.jpg"
|
|
||||||
rvO = getOrientation(fileName)
|
|
||||||
assertEquals(fileName, 6, rvO.first)
|
|
||||||
rvT = getThumbnailBytes(fileName)
|
|
||||||
assertNull(fileName, rvT.first)
|
|
||||||
|
|
||||||
// this file has orientation 1
|
|
||||||
fileName = "test1.jpg"
|
|
||||||
rvO = getOrientation(fileName)
|
|
||||||
assertEquals(fileName, 1, rvO.first)
|
|
||||||
rvT = getThumbnailBytes(fileName)
|
|
||||||
assertNull(fileName, rvT.first)
|
|
||||||
|
|
||||||
// this file has no orientation, it raises exception.
|
|
||||||
fileName = "test2.jpg"
|
|
||||||
rvO = getOrientation(fileName)
|
|
||||||
assertNotNull(
|
|
||||||
fileName,
|
|
||||||
rvO.second
|
|
||||||
) // <java.lang.IllegalStateException: stop before hitting compressed data>
|
|
||||||
|
|
||||||
rvT = getThumbnailBytes(fileName)
|
|
||||||
assertNotNull(
|
|
||||||
fileName,
|
|
||||||
rvT.second
|
|
||||||
) // <java.lang.IllegalStateException: stop before hitting compressed data>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.3 KiB |
|
@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion target_sdk_version
|
compileSdkVersion compile_sdk_version
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|