fixed crash when cataloguing JPEG with large extended XMP

This commit is contained in:
Thibault Deckers 2022-04-16 11:39:44 +09:00
parent c372fca915
commit c594911146
9 changed files with 230 additions and 47 deletions

View file

@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- black screen launch when Firebase fails to initialize (Play version only) - black screen launch when Firebase fails to initialize (Play version only)
- crash when cataloguing JPEG with large extended XMP
## <a id="v1.6.3"></a>[v1.6.3] - 2022-03-28 ## <a id="v1.6.3"></a>[v1.6.3] - 2022-03-28

View file

@ -13,13 +13,9 @@ import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.file.FileTypeDirectory import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
@ -288,7 +284,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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 -> metadataMap["mimeType"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) { if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) {
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)

View file

@ -8,13 +8,11 @@ import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPUtils import com.adobe.internal.xmp.XMPUtils
import com.bumptech.glide.load.resource.bitmap.TransformationUtils 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 com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.Metadata 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.MultiPage
import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.getSafeStructField 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.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes 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.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor 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.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler 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.File
import java.io.InputStream import java.io.InputStream
import java.util.*
class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -118,10 +118,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
retriever.embeddedPicture?.let { bytes -> retriever.embeddedPicture?.let { bytes ->
var embedMimeType: String? = null var embedMimeType: String? = null
bytes.inputStream().use { input -> bytes.inputStream().use { input ->
val metadata = ImageMetadataReader.readMetadata(input) MetadataExtractorHelper.readMimeType(input)?.let { embedMimeType = it }
metadata.getFirstDirectoryOfType(FileTypeDirectory::class.java)?.let { dir ->
dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { embedMimeType = it }
}
} }
embedMimeType?.let { mime -> embedMimeType?.let { mime ->
copyEmbeddedBytes(result, mime, displayName, bytes.inputStream()) copyEmbeddedBytes(result, mime, displayName, bytes.inputStream())
@ -153,7 +150,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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", // data can be large and stored in "Extended XMP",
// which is returned as a second XMP directory // which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java) val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)

View file

@ -14,7 +14,6 @@ import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMetaFactory import com.adobe.internal.xmp.XMPMetaFactory
import com.adobe.internal.xmp.options.SerializeOptions import com.adobe.internal.xmp.options.SerializeOptions
import com.adobe.internal.xmp.properties.XMPPropertyInfo import com.adobe.internal.xmp.properties.XMPPropertyInfo
import com.drew.imaging.ImageMetadataReader
import com.drew.lang.KeyValuePair import com.drew.lang.KeyValuePair
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.metadata.Tag import com.drew.metadata.Tag
@ -127,12 +126,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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 } foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
foundXmp = metadata.directories.any { it is XmpDirectory && it.tagCount > 0 } foundXmp = metadata.directories.any { it is XmpDirectory && it.tagCount > 0 }
val uuidDirCount = HashMap<String, Int>() val uuidDirCount = HashMap<String, Int>()
val dirByName = metadata.directories.filter { val dirByName = metadata.directories.filter {
it.tagCount > 0 (it.tagCount > 0 || it.errorCount > 0)
&& it !is FileTypeDirectory && it !is FileTypeDirectory
&& it !is AviDirectory && it !is AviDirectory
}.groupBy { dir -> dir.name } }.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)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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 } foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
// File type // File type
@ -710,7 +714,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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)) { for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
foundExif = true foundExif = true
dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator } 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)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val fields = HashMap<Int, Any?>() val fields = HashMap<Int, Any?>()
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
if (dir.containsGeoTiffTags()) { if (dir.containsGeoTiffTags()) {
@ -819,7 +823,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val fields: FieldMap = hashMapOf( val fields: FieldMap = hashMapOf(
"projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT, "projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT,
) )
@ -881,7 +885,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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) } val xmpStrings = metadata.getDirectoriesOfType(XmpDirectory::class.java).mapNotNull { XMPMetaFactory.serializeToString(it.xmpMeta, xmpSerializeOptions) }
result.success(xmpStrings.toMutableList()) result.success(xmpStrings.toMutableList())
return return
@ -983,7 +987,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = MetadataExtractorHelper.safeRead(input, sizeBytes)
val tag = when (field) { val tag = when (field) {
ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED

View file

@ -1,6 +1,11 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.util.Log 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.ByteArrayReader
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.lang.SequentialByteArrayReader 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.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader import com.drew.metadata.exif.ExifReader
import com.drew.metadata.exif.ExifSubIFDDirectory import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.iptc.IptcReader import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory import com.drew.metadata.png.PngDirectory
import com.drew.metadata.xmp.XmpReader
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import java.io.BufferedInputStream
import java.io.InputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -34,6 +43,40 @@ object MetadataExtractorHelper {
// e.g. "exif [...] 134 [...] 4578696600004949[...]" // e.g. "exif [...] 134 [...] 4578696600004949[...]"
private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL) 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<JpegSegmentMetadataReader>().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 // extensions
fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) { fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) {

View file

@ -1,4 +1,155 @@
package deckers.thibault.aves.metadata 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<ByteArray>, 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<MetadataExtractorSafeXmpReader>()
// 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
}
} }

View file

@ -8,7 +8,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.xmp.XmpDirectory import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.metadata.XMP.getSafeLong import deckers.thibault.aves.metadata.XMP.getSafeLong
import deckers.thibault.aves.metadata.XMP.getSafeStructField 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.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.util.*
object MultiPage { object MultiPage {
private val LOG_TAG = LogUtils.createTag<MultiPage>() private val LOG_TAG = LogUtils.createTag<MultiPage>()
@ -142,7 +140,7 @@ object MultiPage {
fun getMotionPhotoOffset(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? { fun getMotionPhotoOffset(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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)) { for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
var offsetFromEnd: Long? = null var offsetFromEnd: Long? = null
val xmpMeta = dir.xmpMeta val xmpMeta = dir.xmpMeta

View file

@ -8,7 +8,6 @@ import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.avi.AviDirectory import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory 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.MediaMetadataRetrieverHelper.getSafeString
import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode 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.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong
@ -161,7 +161,7 @@ class SourceEntry {
try { try {
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input -> 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 // do not switch on specific MIME types, as the reported MIME type could be wrong
// (e.g. PNG registered as JPG) // (e.g. PNG registered as JPG)

View file

@ -5,10 +5,8 @@ import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log 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.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.FieldMap
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
@ -22,20 +20,15 @@ internal class ContentImageProvider : ImageProvider() {
try { try {
val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri)) val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri))
StorageUtils.openInputStream(context, safeUri)?.use { input -> 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) // `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives)
// cf https://github.com/drewnoakes/metadata-extractor/issues/296 // cf https://github.com/drewnoakes/metadata-extractor/issues/296
dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { MetadataExtractorHelper.readMimeType(input)?.takeIf { it != MimeTypes.TIFF }?.let {
if (it != MimeTypes.TIFF) {
extractorMimeType = it extractorMimeType = it
if (extractorMimeType != sourceMimeType) { if (extractorMimeType != sourceMimeType) {
Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri") Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri")
} }
} }
} }
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {