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") } } }