guard against large tiff
This commit is contained in:
parent
8c5a600151
commit
0d946b5a43
8 changed files with 142 additions and 78 deletions
|
@ -52,6 +52,7 @@ import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
import deckers.thibault.aves.utils.MimeTypes.isImage
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isMultimedia
|
import deckers.thibault.aves.utils.MimeTypes.isMultimedia
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
|
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
|
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
|
||||||
|
@ -86,6 +87,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private fun getAllMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getAllMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
if (mimeType == null || uri == null) {
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getAllMetadata-args", "failed because of missing arguments", null)
|
result.error("getAllMetadata-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
|
@ -95,10 +97,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
var foundExif = false
|
var foundExif = false
|
||||||
var foundXmp = false
|
var foundXmp = false
|
||||||
|
|
||||||
if (isSupportedByMetadataExtractor(mimeType)) {
|
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||||
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
|
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
|
||||||
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
|
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
|
||||||
|
|
||||||
|
@ -138,7 +140,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundExif) {
|
if (!foundExif && isSupportedByExifInterface(mimeType, sizeBytes)) {
|
||||||
// fallback to read EXIF via ExifInterface
|
// fallback to read EXIF via ExifInterface
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
@ -192,12 +194,13 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val path = call.argument<String>("path")
|
val path = call.argument<String>("path")
|
||||||
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
if (mimeType == null || uri == null) {
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getCatalogMetadata-args", "failed because of missing arguments", null)
|
result.error("getCatalogMetadata-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, path))
|
val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, path, sizeBytes))
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri))
|
metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri))
|
||||||
}
|
}
|
||||||
|
@ -213,15 +216,15 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
// set `KEY_XMP_TITLE_DESCRIPTION` from these fields (by precedence):
|
// set `KEY_XMP_TITLE_DESCRIPTION` from these fields (by precedence):
|
||||||
// - XMP / dc:title
|
// - XMP / dc:title
|
||||||
// - XMP / dc:description
|
// - XMP / dc:description
|
||||||
private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?): Map<String, Any> {
|
private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?, sizeBytes: Long?): Map<String, Any> {
|
||||||
val metadataMap = HashMap<String, Any>()
|
val metadataMap = HashMap<String, Any>()
|
||||||
|
|
||||||
var foundExif = false
|
var foundExif = false
|
||||||
|
|
||||||
if (isSupportedByMetadataExtractor(mimeType)) {
|
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||||
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
|
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
|
||||||
|
|
||||||
// File type
|
// File type
|
||||||
|
@ -311,7 +314,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundExif) {
|
if (!foundExif && isSupportedByExifInterface(mimeType, sizeBytes)) {
|
||||||
// fallback to read EXIF via ExifInterface
|
// fallback to read EXIF via ExifInterface
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
@ -371,6 +374,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
if (mimeType == null || uri == null) {
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getOverlayMetadata-args", "failed because of missing arguments", null)
|
result.error("getOverlayMetadata-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
|
@ -396,10 +400,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
var foundExif = false
|
var foundExif = false
|
||||||
if (isSupportedByMetadataExtractor(mimeType)) {
|
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||||
foundExif = true
|
foundExif = true
|
||||||
dir.getSafeRational(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
|
dir.getSafeRational(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
|
||||||
|
@ -415,7 +419,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundExif) {
|
if (!foundExif && isSupportedByExifInterface(mimeType, sizeBytes)) {
|
||||||
// fallback to read EXIF via ExifInterface
|
// fallback to read EXIF via ExifInterface
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
@ -488,26 +492,31 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
if (uri == null) {
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null)
|
result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
val metadataMap = HashMap<String, String?>()
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
if (isSupportedByExifInterface(mimeType, sizeBytes, strict = false)) {
|
||||||
val exif = ExifInterface(input)
|
try {
|
||||||
val metadataMap = HashMap<String, String?>()
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
for (tag in ExifInterfaceHelper.allTags.keys.filter { exif.hasAttribute(it) }) {
|
val exif = ExifInterface(input)
|
||||||
metadataMap[tag] = exif.getAttribute(tag)
|
for (tag in ExifInterfaceHelper.allTags.keys.filter { exif.hasAttribute(it) }) {
|
||||||
|
metadataMap[tag] = exif.getAttribute(tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.success(metadataMap)
|
} catch (e: Exception) {
|
||||||
} ?: result.error("getExifInterfaceMetadata-noinput", "failed to get exif for uri=$uri", null)
|
// ExifInterface initialization can fail with a RuntimeException
|
||||||
} catch (e: Exception) {
|
// caused by an internal MediaMetadataRetriever failure
|
||||||
// ExifInterface initialization can fail with a RuntimeException
|
result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=$uri", e.message)
|
||||||
// caused by an internal MediaMetadataRetriever failure
|
return
|
||||||
result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=$uri", e.message)
|
}
|
||||||
}
|
}
|
||||||
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
@ -563,46 +572,45 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMetadataExtractorSummary(call: MethodCall, result: MethodChannel.Result) {
|
private fun getMetadataExtractorSummary(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
if (uri == null) {
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getMetadataExtractorSummary-args", "failed because of missing arguments", null)
|
result.error("getMetadataExtractorSummary-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadataMap = HashMap<String, String>()
|
val metadataMap = HashMap<String, String>()
|
||||||
try {
|
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
try {
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
metadataMap["mimeType"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
|
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||||
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) {
|
metadataMap["mimeType"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
|
||||||
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)
|
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) {
|
||||||
} else ""
|
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)
|
||||||
}
|
} else ""
|
||||||
metadataMap["typeName"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
|
}
|
||||||
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME)) {
|
metadataMap["typeName"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
|
||||||
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME)
|
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME)) {
|
||||||
} else ""
|
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME)
|
||||||
}
|
} else ""
|
||||||
for (dir in metadata.directories) {
|
}
|
||||||
val dirName = dir.name ?: ""
|
for (dir in metadata.directories) {
|
||||||
var index = 0
|
val dirName = dir.name ?: ""
|
||||||
while (metadataMap.containsKey("$dirName ($index)")) index++
|
var index = 0
|
||||||
var value = "${dir.tagCount} tags"
|
while (metadataMap.containsKey("$dirName ($index)")) index++
|
||||||
dir.parent?.let { value += ", parent: ${it.name}" }
|
var value = "${dir.tagCount} tags"
|
||||||
metadataMap["$dirName ($index)"] = value
|
dir.parent?.let { value += ", parent: ${it.name}" }
|
||||||
|
metadataMap["$dirName ($index)"] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
|
||||||
} catch (e: NoClassDefFoundError) {
|
|
||||||
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadataMap.isNotEmpty()) {
|
|
||||||
result.success(metadataMap)
|
|
||||||
} else {
|
|
||||||
result.error("getMetadataExtractorSummary-failure", "failed to get metadata for uri=$uri", null)
|
|
||||||
}
|
}
|
||||||
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
@ -628,26 +636,30 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
if (uri == null) {
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getExifThumbnails-args", "failed because of missing arguments", null)
|
result.error("getExifThumbnails-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val thumbnails = ArrayList<ByteArray>()
|
val thumbnails = ArrayList<ByteArray>()
|
||||||
try {
|
if (isSupportedByExifInterface(mimeType, sizeBytes)) {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
try {
|
||||||
val exif = ExifInterface(input)
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
val exif = ExifInterface(input)
|
||||||
exif.thumbnailBitmap?.let { bitmap ->
|
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||||
TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
|
exif.thumbnailBitmap?.let { bitmap ->
|
||||||
thumbnails.add(it.getBytes(canHaveAlpha = false, recycle = false))
|
TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
|
||||||
|
thumbnails.add(it.getBytes(canHaveAlpha = false, recycle = false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ExifInterface initialization can fail with a RuntimeException
|
||||||
|
// caused by an internal MediaMetadataRetriever failure
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
// ExifInterface initialization can fail with a RuntimeException
|
|
||||||
// caused by an internal MediaMetadataRetriever failure
|
|
||||||
}
|
}
|
||||||
result.success(thumbnails)
|
result.success(thumbnails)
|
||||||
}
|
}
|
||||||
|
@ -655,16 +667,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private fun getXmpThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
private fun getXmpThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
if (mimeType == null || uri == null) {
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getXmpThumbnails-args", "failed because of missing arguments", null)
|
result.error("getXmpThumbnails-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val thumbnails = ArrayList<ByteArray>()
|
val thumbnails = ArrayList<ByteArray>()
|
||||||
if (isSupportedByMetadataExtractor(mimeType)) {
|
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
val xmpMeta = dir.xmpMeta
|
val xmpMeta = dir.xmpMeta
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong
|
||||||
import deckers.thibault.aves.model.provider.FieldMap
|
import deckers.thibault.aves.model.provider.FieldMap
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class SourceImageEntry {
|
class SourceImageEntry {
|
||||||
|
@ -129,7 +130,10 @@ class SourceImageEntry {
|
||||||
fillByExifInterface(context)
|
fillByExifInterface(context)
|
||||||
}
|
}
|
||||||
if (!isSized) {
|
if (!isSized) {
|
||||||
fillByBitmapDecode(context)
|
when (sourceMimeType) {
|
||||||
|
MimeTypes.TIFF -> fillByTiffDecode(context)
|
||||||
|
else -> fillByBitmapDecode(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -155,11 +159,13 @@ class SourceImageEntry {
|
||||||
// finds: width, height, orientation, date, duration
|
// finds: width, height, orientation, date, duration
|
||||||
private fun fillByMetadataExtractor(context: Context) {
|
private fun fillByMetadataExtractor(context: Context) {
|
||||||
// skip raw images because `metadata-extractor` reports the decoded dimensions instead of the raw dimensions
|
// skip raw images because `metadata-extractor` reports the decoded dimensions instead of the raw dimensions
|
||||||
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType) || MimeTypes.isRaw(sourceMimeType)) return
|
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType, sizeBytes)
|
||||||
|
|| MimeTypes.isRaw(sourceMimeType)
|
||||||
|
) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -207,7 +213,7 @@ class SourceImageEntry {
|
||||||
|
|
||||||
// finds: width, height, orientation, date
|
// finds: width, height, orientation, date
|
||||||
private fun fillByExifInterface(context: Context) {
|
private fun fillByExifInterface(context: Context) {
|
||||||
if (!ExifInterface.isSupportedMimeType(sourceMimeType)) return;
|
if (!MimeTypes.isSupportedByExifInterface(sourceMimeType, sizeBytes)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
@ -240,6 +246,22 @@ class SourceImageEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fillByTiffDecode(context: Context) {
|
||||||
|
try {
|
||||||
|
context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||||
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options)
|
||||||
|
width = options.outWidth
|
||||||
|
height = options.outHeight
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// convenience method
|
// convenience method
|
||||||
private fun toLong(o: Any?): Long? = when (o) {
|
private fun toLong(o: Any?): Long? = when (o) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package deckers.thibault.aves.utils
|
package deckers.thibault.aves.utils
|
||||||
|
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
|
||||||
object MimeTypes {
|
object MimeTypes {
|
||||||
private const val IMAGE = "image"
|
private const val IMAGE = "image"
|
||||||
|
|
||||||
|
@ -65,12 +67,25 @@ object MimeTypes {
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
// as of metadata-extractor v2.14.0
|
// opening large TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
|
||||||
fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) {
|
// 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) {
|
||||||
WBMP, MP2T, WEBM -> false
|
WBMP, MP2T, WEBM -> false
|
||||||
|
TIFF -> sizeBytes != null && sizeBytes < tiffSizeBytesMax
|
||||||
else -> true
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Glide automatically applies EXIF orientation when decoding images of known formats
|
// Glide automatically applies EXIF orientation when decoding images of known formats
|
||||||
// but we need to rotate the decoded bitmap for the other formats
|
// but we need to rotate the decoded bitmap for the other formats
|
||||||
// maybe related to ExifInterface version used by Glide:
|
// maybe related to ExifInterface version used by Glide:
|
||||||
|
|
|
@ -173,7 +173,7 @@ class ImageEntry {
|
||||||
bool get isSvg => mimeType == MimeTypes.svg;
|
bool get isSvg => mimeType == MimeTypes.svg;
|
||||||
|
|
||||||
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
||||||
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg].contains(mimeType) || isRaw;
|
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
|
||||||
|
|
||||||
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
|
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
|
||||||
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
|
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
class BrandColors {
|
class BrandColors {
|
||||||
|
static const Color adobeAfterEffects = Color(0xFF9A9AFF);
|
||||||
static const Color adobeIllustrator = Color(0xFFFF9B00);
|
static const Color adobeIllustrator = Color(0xFFFF9B00);
|
||||||
static const Color adobePhotoshop = Color(0xFF2DAAFF);
|
static const Color adobePhotoshop = Color(0xFF2DAAFF);
|
||||||
static const Color android = Color(0xFF3DDC84);
|
static const Color android = Color(0xFF3DDC84);
|
||||||
|
@ -9,6 +10,8 @@ class BrandColors {
|
||||||
static Color get(String text) {
|
static Color get(String text) {
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
switch (text.toLowerCase()) {
|
switch (text.toLowerCase()) {
|
||||||
|
case 'after effects':
|
||||||
|
return adobeAfterEffects;
|
||||||
case 'illustrator':
|
case 'illustrator':
|
||||||
return adobeIllustrator;
|
return adobeIllustrator;
|
||||||
case 'photoshop':
|
case 'photoshop':
|
||||||
|
|
|
@ -7,6 +7,7 @@ class XMP {
|
||||||
'adsml-at': 'AdsML',
|
'adsml-at': 'AdsML',
|
||||||
'aux': 'Exif Aux',
|
'aux': 'Exif Aux',
|
||||||
'Camera': 'Camera',
|
'Camera': 'Camera',
|
||||||
|
'creatorAtom': 'After Effects',
|
||||||
'crs': 'Camera Raw Settings',
|
'crs': 'Camera Raw Settings',
|
||||||
'dc': 'Dublin Core',
|
'dc': 'Dublin Core',
|
||||||
'drone-dji': 'DJI Drone',
|
'drone-dji': 'DJI Drone',
|
||||||
|
|
|
@ -17,6 +17,7 @@ class MetadataService {
|
||||||
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
});
|
});
|
||||||
return result as Map;
|
return result as Map;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -44,6 +45,7 @@ class MetadataService {
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
'path': entry.path,
|
'path': entry.path,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
result['contentId'] = entry.contentId;
|
result['contentId'] = entry.contentId;
|
||||||
return CatalogMetadata.fromMap(result);
|
return CatalogMetadata.fromMap(result);
|
||||||
|
@ -69,6 +71,7 @@ class MetadataService {
|
||||||
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
return OverlayMetadata.fromMap(result);
|
return OverlayMetadata.fromMap(result);
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -108,7 +111,9 @@ class MetadataService {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from the `ExifInterface` library
|
// return map with all data available from the `ExifInterface` library
|
||||||
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
|
||||||
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
return result;
|
return result;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -134,7 +139,9 @@ class MetadataService {
|
||||||
try {
|
try {
|
||||||
// return map with the mime type and tag count for each directory found by `metadata-extractor`
|
// return map with the mime type and tag count for each directory found by `metadata-extractor`
|
||||||
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
|
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
|
||||||
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
return result;
|
return result;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -155,10 +162,12 @@ class MetadataService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getExifThumbnails(String uri) async {
|
static Future<List<Uint8List>> getExifThumbnails(ImageEntry entry) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{
|
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{
|
||||||
'uri': uri,
|
'mimeType': entry.mimeType,
|
||||||
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
});
|
});
|
||||||
return (result as List).cast<Uint8List>();
|
return (result as List).cast<Uint8List>();
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -172,6 +181,7 @@ class MetadataService {
|
||||||
final result = await platform.invokeMethod('getXmpThumbnails', <String, dynamic>{
|
final result = await platform.invokeMethod('getXmpThumbnails', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
});
|
});
|
||||||
return (result as List).cast<Uint8List>();
|
return (result as List).cast<Uint8List>();
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||||
_loader = MetadataService.getEmbeddedPictures(uri);
|
_loader = MetadataService.getEmbeddedPictures(uri);
|
||||||
break;
|
break;
|
||||||
case MetadataThumbnailSource.exif:
|
case MetadataThumbnailSource.exif:
|
||||||
_loader = MetadataService.getExifThumbnails(uri);
|
_loader = MetadataService.getExifThumbnails(entry);
|
||||||
break;
|
break;
|
||||||
case MetadataThumbnailSource.xmp:
|
case MetadataThumbnailSource.xmp:
|
||||||
_loader = MetadataService.getXmpThumbnails(entry);
|
_loader = MetadataService.getXmpThumbnails(entry);
|
||||||
|
|
Loading…
Reference in a new issue