improved handling of large TIFF files

This commit is contained in:
Thibault Deckers 2020-12-11 13:01:21 +09:00
parent 6beb814ff8
commit 25ebc95d42
5 changed files with 78 additions and 41 deletions

View file

@ -14,6 +14,7 @@ import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.isImage
@ -145,9 +146,9 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
val metadataMap = HashMap<String, String?>()
if (isSupportedByExifInterface(mimeType, sizeBytes, strict = false)) {
if (isSupportedByExifInterface(mimeType, strict = false)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
for (tag in ExifInterfaceHelper.allTags.keys.filter { exif.hasAttribute(it) }) {
metadataMap[tag] = exif.getAttribute(tag)
@ -197,10 +198,10 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
val metadataMap = HashMap<String, String>()
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
if (isSupportedByMetadataExtractor(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
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)

View file

@ -92,10 +92,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
var foundExif = false
var foundXmp = false
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
if (isSupportedByMetadataExtractor(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
@ -149,10 +149,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
if (!foundExif && isSupportedByExifInterface(mimeType, sizeBytes)) {
if (!foundExif && isSupportedByExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
val allTags = describeAll(exif).toMutableMap()
if (foundXmp) {
@ -238,10 +238,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
var flags = 0
var foundExif = false
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
if (isSupportedByMetadataExtractor(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
// File type
@ -358,10 +358,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
if (!foundExif && isSupportedByExifInterface(mimeType, sizeBytes)) {
if (!foundExif && isSupportedByExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
@ -448,10 +448,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
var foundExif = false
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
if (isSupportedByMetadataExtractor(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
foundExif = true
dir.getSafeRational(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
@ -467,10 +467,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
if (!foundExif && isSupportedByExifInterface(mimeType, sizeBytes)) {
if (!foundExif && isSupportedByExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
@ -519,9 +519,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
val thumbnails = ArrayList<ByteArray>()
if (isSupportedByExifInterface(mimeType, sizeBytes)) {
if (isSupportedByExifInterface(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
exif.thumbnailBitmap?.let { bitmap ->
@ -549,10 +549,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
return
}
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
if (isSupportedByMetadataExtractor(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
// data can be large and stored in "Extended XMP",
// which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)

View file

@ -1,6 +1,12 @@
package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import java.io.File
import java.io.InputStream
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
@ -88,4 +94,42 @@ object Metadata {
}
return dateMillis
}
// opening large TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
// so we define an arbitrary threshold to avoid a crash on launch.
// It is not clear whether it is because of the file itself or its metadata.
const val tiffSizeBytesMax = 100 * (1 shl 20) // MB
// we try and read metadata from large files by copying an arbitrary amount from its beginning
// to a temporary file, and reusing that preview file for all metadata reading purposes
private const val previewSize = 5 * (1 shl 20) // MB
private val previewFiles = HashMap<Uri, File>()
private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri {
if (mimeType != MimeTypes.TIFF) return uri
if (sizeBytes != null && sizeBytes < tiffSizeBytesMax) return uri
var previewFile = previewFiles[uri]
if (previewFile == null) {
previewFile = File.createTempFile("aves", null, context.cacheDir).apply {
deleteOnExit()
outputStream().use { outputStream ->
StorageUtils.openInputStream(context, uri)?.use { inputStream ->
val b = ByteArray(previewSize)
inputStream.read(b, 0, previewSize)
outputStream.write(b)
}
}
}
previewFiles[uri] = previewFile
}
return Uri.fromFile(previewFile)
}
fun openSafeInputStream(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): InputStream? {
val safeUri = getSafeUri(context, uri, mimeType, sizeBytes)
return StorageUtils.openInputStream(context, safeUri)
}
}

View file

@ -20,6 +20,7 @@ import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMi
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt
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.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
@ -159,13 +160,13 @@ class SourceImageEntry {
// finds: width, height, orientation, date, duration
private fun fillByMetadataExtractor(context: Context) {
// skip raw images because `metadata-extractor` reports the decoded dimensions instead of the raw dimensions
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType, sizeBytes)
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)
|| MimeTypes.isRaw(sourceMimeType)
) return
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
// do not switch on specific mime types, as the reported mime type could be wrong
// (e.g. PNG registered as JPG)
@ -213,10 +214,10 @@ class SourceImageEntry {
// finds: width, height, orientation, date
private fun fillByExifInterface(context: Context) {
if (!MimeTypes.isSupportedByExifInterface(sourceMimeType, sizeBytes)) return
if (!MimeTypes.isSupportedByExifInterface(sourceMimeType)) return
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
foundExif = true
exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it }

View file

@ -67,24 +67,15 @@ object MimeTypes {
else -> false
}
// opening large TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
// so we define an arbitrary threshold to avoid a crash on launch.
// It is not clear whether it is because of the file itself or its metadata.
private const val tiffSizeBytesMax = 128 * (1 shl 20) // MB
// as of `metadata-extractor` v2.14.0
fun isSupportedByMetadataExtractor(mimeType: String, sizeBytes: Long?) = when (mimeType) {
fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) {
WBMP, MP2T, WEBM -> false
TIFF -> sizeBytes != null && sizeBytes < tiffSizeBytesMax
else -> true
}
// as of `ExifInterface` v1.3.1, `isSupportedMimeType` reports
// no support for TIFF images, but it can actually open them (maybe other formats too)
fun isSupportedByExifInterface(mimeType: String, sizeBytes: Long?, strict: Boolean = true) = when (mimeType) {
TIFF -> sizeBytes != null && sizeBytes < tiffSizeBytesMax
else -> ExifInterface.isSupportedMimeType(mimeType) || !strict
}
fun isSupportedByExifInterface(mimeType: String, strict: Boolean = true) = ExifInterface.isSupportedMimeType(mimeType) || !strict
// Glide automatically applies EXIF orientation when decoding images of known formats
// but we need to rotate the decoded bitmap for the other formats