diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10ce00968..e60a72d20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- black screen launch when Firebase fails to initialize (Play version only)
+- crash when cataloguing JPEG with large extended XMP
## [v1.6.3] - 2022-03-28
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
index 6f555b18e..0d2061740 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
@@ -13,13 +13,9 @@ import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
-import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
-import deckers.thibault.aves.metadata.ExifInterfaceHelper
-import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
-import deckers.thibault.aves.metadata.Metadata
-import deckers.thibault.aves.metadata.PixyMetaHelper
+import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
@@ -288,7 +284,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
metadataMap["mimeType"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) {
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
index d34ef9eec..3b7d3a897 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
@@ -8,13 +8,11 @@ import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPUtils
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
-import com.drew.imaging.ImageMetadataReader
-import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.Metadata
-import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
+import deckers.thibault.aves.metadata.MetadataExtractorHelper
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.getSafeStructField
@@ -25,19 +23,21 @@ import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
-import deckers.thibault.aves.utils.MimeTypes.extensionFor
-import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
+import deckers.thibault.aves.utils.MimeTypes.extensionFor
+import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
-import java.util.*
class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -118,10 +118,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
retriever.embeddedPicture?.let { bytes ->
var embedMimeType: String? = null
bytes.inputStream().use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
- metadata.getFirstDirectoryOfType(FileTypeDirectory::class.java)?.let { dir ->
- dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { embedMimeType = it }
- }
+ MetadataExtractorHelper.readMimeType(input)?.let { embedMimeType = it }
}
embedMimeType?.let { mime ->
copyEmbeddedBytes(result, mime, displayName, bytes.inputStream())
@@ -153,7 +150,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
// data can be large and stored in "Extended XMP",
// which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
index 579641cf4..a6ebfdfae 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
@@ -14,7 +14,6 @@ import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMetaFactory
import com.adobe.internal.xmp.options.SerializeOptions
import com.adobe.internal.xmp.properties.XMPPropertyInfo
-import com.drew.imaging.ImageMetadataReader
import com.drew.lang.KeyValuePair
import com.drew.lang.Rational
import com.drew.metadata.Tag
@@ -127,12 +126,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
foundXmp = metadata.directories.any { it is XmpDirectory && it.tagCount > 0 }
val uuidDirCount = HashMap()
val dirByName = metadata.directories.filter {
- it.tagCount > 0
+ (it.tagCount > 0 || it.errorCount > 0)
&& it !is FileTypeDirectory
&& it !is AviDirectory
}.groupBy { dir -> dir.name }
@@ -320,6 +319,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
+ // include errors, if any
+ dir.errors.forEachIndexed { i, error ->
+ dirMap["Error[$i]"] = error
+ }
}
}
}
@@ -445,7 +449,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
// File type
@@ -710,7 +714,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
foundExif = true
dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
@@ -758,7 +762,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val fields = HashMap()
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
if (dir.containsGeoTiffTags()) {
@@ -819,7 +823,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val fields: FieldMap = hashMapOf(
"projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT,
)
@@ -881,7 +885,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val xmpStrings = metadata.getDirectoriesOfType(XmpDirectory::class.java).mapNotNull { XMPMetaFactory.serializeToString(it.xmpMeta, xmpSerializeOptions) }
result.success(xmpStrings.toMutableList())
return
@@ -983,7 +987,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val tag = when (field) {
ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
index 2286ee78c..bbd790fcf 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
@@ -1,6 +1,11 @@
package deckers.thibault.aves.metadata
import android.util.Log
+import com.drew.imaging.FileType
+import com.drew.imaging.FileTypeDetector
+import com.drew.imaging.ImageMetadataReader
+import com.drew.imaging.jpeg.JpegMetadataReader
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader
import com.drew.lang.ByteArrayReader
import com.drew.lang.Rational
import com.drew.lang.SequentialByteArrayReader
@@ -10,9 +15,13 @@ import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader
import com.drew.metadata.exif.ExifSubIFDDirectory
+import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory
+import com.drew.metadata.xmp.XmpReader
import deckers.thibault.aves.utils.LogUtils
+import java.io.BufferedInputStream
+import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.*
@@ -34,6 +43,40 @@ object MetadataExtractorHelper {
// e.g. "exif [...] 134 [...] 4578696600004949[...]"
private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL)
+ fun readMimeType(input: InputStream): String? {
+ val bufferedInputStream = if (input is BufferedInputStream) input else BufferedInputStream(input)
+ return FileTypeDetector.detectFileType(bufferedInputStream).mimeType
+ }
+
+ fun safeRead(input: InputStream, sizeBytes: Long?): com.drew.metadata.Metadata {
+ val streamLength = sizeBytes ?: -1
+ val bufferedInputStream = if (input is BufferedInputStream) input else BufferedInputStream(input)
+ val fileType = FileTypeDetector.detectFileType(bufferedInputStream)
+
+ val metadata = if (fileType == FileType.Jpeg) {
+ safeReadJpeg(bufferedInputStream)
+ } else {
+ ImageMetadataReader.readMetadata(bufferedInputStream, streamLength, fileType)
+ }
+
+ metadata.addDirectory(FileTypeDirectory(fileType))
+ return metadata
+ }
+
+ // Some JPEG (and other types?) contain XMP with a preposterous number of `DocumentAncestors`.
+ // This bloated XMP is unsafely loaded in memory by Adobe's `XMPMetaParser.parseInputSource`
+ // which easily yields OOM on Android, so we try to detect and strip extended XMP with a modified XMP reader.
+ private fun safeReadJpeg(input: InputStream): com.drew.metadata.Metadata {
+ val readers = ArrayList().apply {
+ addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader })
+ add(MetadataExtractorSafeXmpReader())
+ }
+
+ val metadata = com.drew.metadata.Metadata()
+ JpegMetadataReader.process(metadata, input, readers)
+ return metadata
+ }
+
// extensions
fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorSafeXmpReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorSafeXmpReader.kt
index fc0f96624..0edd03c85 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorSafeXmpReader.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorSafeXmpReader.kt
@@ -1,4 +1,155 @@
package deckers.thibault.aves.metadata
-class MetadataExtractorSafeXMPReader {
+import android.util.Log
+import com.adobe.internal.xmp.XMPException
+import com.adobe.internal.xmp.XMPMeta
+import com.adobe.internal.xmp.XMPMetaFactory
+import com.adobe.internal.xmp.impl.ByteBuffer
+import com.adobe.internal.xmp.options.ParseOptions
+import com.adobe.internal.xmp.properties.XMPPropertyInfo
+import com.drew.imaging.jpeg.JpegSegmentType
+import com.drew.lang.SequentialByteArrayReader
+import com.drew.lang.SequentialReader
+import com.drew.lang.annotations.NotNull
+import com.drew.lang.annotations.Nullable
+import com.drew.metadata.Directory
+import com.drew.metadata.Metadata
+import com.drew.metadata.xmp.XmpDirectory
+import com.drew.metadata.xmp.XmpReader
+import deckers.thibault.aves.utils.LogUtils
+import java.io.IOException
+
+class MetadataExtractorSafeXmpReader : XmpReader() {
+ // adapted from `XmpReader` to detect and skip large extended XMP
+ override fun readJpegSegments(segments: Iterable, metadata: Metadata, segmentType: JpegSegmentType) {
+ val preambleLength = XMP_JPEG_PREAMBLE.length
+ val extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length
+ var extendedXMPGUID: String? = null
+ var extendedXMPBuffer: ByteArray? = null
+
+ for (segmentBytes in segments) {
+ if (segmentBytes.size >= preambleLength) {
+ if (XMP_JPEG_PREAMBLE.equals(String(segmentBytes, 0, preambleLength), ignoreCase = true) ||
+ "XMP".equals(String(segmentBytes, 0, 3), ignoreCase = true)
+ ) {
+ val xmlBytes = ByteArray(segmentBytes.size - preambleLength)
+ System.arraycopy(segmentBytes, preambleLength, xmlBytes, 0, xmlBytes.size)
+ extract(xmlBytes, metadata)
+ extendedXMPGUID = getExtendedXMPGUID(metadata)
+ continue
+ }
+ }
+ if (extendedXMPGUID != null && segmentBytes.size >= extensionPreambleLength &&
+ XMP_EXTENSION_JPEG_PREAMBLE.equals(String(segmentBytes, 0, extensionPreambleLength), ignoreCase = true)
+ ) {
+ extendedXMPBuffer = processExtendedXMPChunk(metadata, segmentBytes, extendedXMPGUID, extendedXMPBuffer)
+ }
+ }
+
+ extendedXMPBuffer?.let { xmpBytes ->
+ val totalSize = xmpBytes.size
+ if (totalSize > segmentTypeSizeDangerThreshold) {
+ val error = "Extended XMP is too large, with a total size of $totalSize B"
+ Log.w(LOG_TAG, error)
+ metadata.addDirectory(XmpDirectory().apply {
+ addError(error)
+ })
+ } else {
+ extract(xmpBytes, metadata)
+ }
+ }
+ }
+
+ // adapted from `XmpReader` to provide different parsing options
+ override fun extract(@NotNull xmpBytes: ByteArray, offset: Int, length: Int, @NotNull metadata: Metadata, @Nullable parentDirectory: Directory?) {
+ val directory = XmpDirectory()
+ if (parentDirectory != null) directory.parent = parentDirectory
+
+ try {
+ val xmpMeta: XMPMeta = if (offset == 0 && length == xmpBytes.size) {
+ XMPMetaFactory.parseFromBuffer(xmpBytes, PARSE_OPTIONS)
+ } else {
+ val buffer = ByteBuffer(xmpBytes, offset, length)
+ XMPMetaFactory.parse(buffer.byteStream, PARSE_OPTIONS)
+ }
+ directory.xmpMeta = xmpMeta
+ } catch (e: XMPException) {
+ directory.addError("Error processing XMP data: " + e.message)
+ }
+ if (!directory.isEmpty) metadata.addDirectory(directory)
+ }
+
+ // adapted from `XmpReader` because original is private
+ private fun getExtendedXMPGUID(metadata: Metadata): String? {
+ val xmpDirectories = metadata.getDirectoriesOfType(XmpDirectory::class.java)
+ for (directory in xmpDirectories) {
+ val xmpMeta = directory.xmpMeta
+ try {
+ val itr = xmpMeta.iterator(SCHEMA_XMP_NOTES, null, null) ?: continue
+ while (itr.hasNext()) {
+ val pi = itr.next() as XMPPropertyInfo?
+ if (ATTRIBUTE_EXTENDED_XMP == pi!!.path) {
+ return pi.value
+ }
+ }
+ } catch (e: XMPException) {
+ // Fail silently here: we had a reading issue, not a decoding issue.
+ }
+ }
+ return null
+ }
+
+ // adapted from `XmpReader` because original is private
+ private fun processExtendedXMPChunk(metadata: Metadata, segmentBytes: ByteArray, extendedXMPGUID: String, extendedXMPBufferIn: ByteArray?): ByteArray? {
+ var extendedXMPBuffer: ByteArray? = extendedXMPBufferIn
+ val extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length
+ val segmentLength = segmentBytes.size
+ val totalOffset = extensionPreambleLength + EXTENDED_XMP_GUID_LENGTH + EXTENDED_XMP_INT_LENGTH + EXTENDED_XMP_INT_LENGTH
+ if (segmentLength >= totalOffset) {
+ try {
+ val reader: SequentialReader = SequentialByteArrayReader(segmentBytes)
+ reader.skip(extensionPreambleLength.toLong())
+ val segmentGUID = reader.getString(EXTENDED_XMP_GUID_LENGTH)
+ if (extendedXMPGUID == segmentGUID) {
+ val fullLength = reader.uInt32.toInt()
+ val chunkOffset = reader.uInt32.toInt()
+ if (extendedXMPBuffer == null) extendedXMPBuffer = ByteArray(fullLength)
+ if (extendedXMPBuffer.size == fullLength) {
+ System.arraycopy(segmentBytes, totalOffset, extendedXMPBuffer, chunkOffset, segmentLength - totalOffset)
+ } else {
+ val directory = XmpDirectory()
+ directory.addError(String.format("Inconsistent length for the Extended XMP buffer: %d instead of %d", fullLength, extendedXMPBuffer.size))
+ metadata.addDirectory(directory)
+ }
+ }
+ } catch (ex: IOException) {
+ val directory = XmpDirectory()
+ directory.addError(ex.message)
+ metadata.addDirectory(directory)
+ }
+ }
+ return extendedXMPBuffer
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+
+ // arbitrary size to detect extended XMP that may yield an OOM
+ private const val segmentTypeSizeDangerThreshold = 3 * (1 shl 20) // MB
+
+ // tighter node limits for faster loading
+ private val PARSE_OPTIONS = ParseOptions().setXMPNodesToLimit(
+ mapOf(
+ "photoshop:DocumentAncestors" to 200,
+ "xmpMM:History" to 200,
+ )
+ )
+
+ private const val XMP_JPEG_PREAMBLE = "http://ns.adobe.com/xap/1.0/\u0000"
+ private const val XMP_EXTENSION_JPEG_PREAMBLE = "http://ns.adobe.com/xmp/extension/\u0000"
+ private const val SCHEMA_XMP_NOTES = "http://ns.adobe.com/xmp/note/"
+ private const val ATTRIBUTE_EXTENDED_XMP = "xmpNote:HasExtendedXMP"
+ private const val EXTENDED_XMP_GUID_LENGTH = 32
+ private const val EXTENDED_XMP_INT_LENGTH = 4
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
index 1b6277be9..241e50f02 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
@@ -8,7 +8,6 @@ import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.util.Log
-import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.metadata.XMP.getSafeLong
import deckers.thibault.aves.metadata.XMP.getSafeStructField
@@ -16,7 +15,6 @@ import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
-import java.util.*
object MultiPage {
private val LOG_TAG = LogUtils.createTag()
@@ -142,7 +140,7 @@ object MultiPage {
fun getMotionPhotoOffset(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
var offsetFromEnd: Long? = null
val xmpMeta = dir.xmpMeta
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
index 6eb90a59f..a355a8fa9 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
@@ -8,7 +8,6 @@ import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import androidx.exifinterface.media.ExifInterface
-import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory
@@ -23,6 +22,7 @@ import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeLong
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeString
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode
+import deckers.thibault.aves.metadata.MetadataExtractorHelper
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong
@@ -161,7 +161,7 @@ class SourceEntry {
try {
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
+ val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
// do not switch on specific MIME types, as the reported MIME type could be wrong
// (e.g. PNG registered as JPG)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt
index f780e0c2a..b20fca239 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt
@@ -5,10 +5,8 @@ import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
-import com.drew.imaging.ImageMetadataReader
-import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.metadata.Metadata
-import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
+import deckers.thibault.aves.metadata.MetadataExtractorHelper
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
@@ -22,17 +20,12 @@ internal class ContentImageProvider : ImageProvider() {
try {
val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri))
StorageUtils.openInputStream(context, safeUri)?.use { input ->
- val metadata = ImageMetadataReader.readMetadata(input)
- for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) {
- // `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives)
- // cf https://github.com/drewnoakes/metadata-extractor/issues/296
- dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) {
- if (it != MimeTypes.TIFF) {
- extractorMimeType = it
- if (extractorMimeType != sourceMimeType) {
- Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri")
- }
- }
+ // `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives)
+ // cf https://github.com/drewnoakes/metadata-extractor/issues/296
+ MetadataExtractorHelper.readMimeType(input)?.takeIf { it != MimeTypes.TIFF }?.let {
+ extractorMimeType = it
+ if (extractorMimeType != sourceMimeType) {
+ Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri")
}
}
}