fixed crash when cataloguing JPEG with large extended XMP
This commit is contained in:
parent
c372fca915
commit
c594911146
9 changed files with 230 additions and 47 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue