improved handling of large TIFF files
This commit is contained in:
parent
6beb814ff8
commit
25ebc95d42
5 changed files with 78 additions and 41 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue