diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index ce6ea20ab..f152348e0 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -52,6 +52,7 @@ import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage 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.isVideo 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) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { result.error("getAllMetadata-args", "failed because of missing arguments", null) return @@ -95,10 +97,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { var foundExif = false var foundXmp = false - if (isSupportedByMetadataExtractor(mimeType)) { + if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { try { StorageUtils.openInputStream(context, uri)?.use { input -> - val metadata = ImageMetadataReader.readMetadata(input) + val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1) foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::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 try { StorageUtils.openInputStream(context, uri)?.use { input -> @@ -192,12 +194,13 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } val path = call.argument("path") + val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { result.error("getCatalogMetadata-args", "failed because of missing arguments", null) return } - val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, path)) + val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, path, sizeBytes)) if (isVideo(mimeType)) { 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): // - XMP / dc:title // - XMP / dc:description - private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?): Map { + private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?, sizeBytes: Long?): Map { val metadataMap = HashMap() var foundExif = false - if (isSupportedByMetadataExtractor(mimeType)) { + if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { try { StorageUtils.openInputStream(context, uri)?.use { input -> - val metadata = ImageMetadataReader.readMetadata(input) + val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1) foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java) // 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 try { 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) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { result.error("getOverlayMetadata-args", "failed because of missing arguments", null) return @@ -396,10 +400,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } var foundExif = false - if (isSupportedByMetadataExtractor(mimeType)) { + if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { try { 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)) { foundExif = true 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 try { 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) { + val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } - if (uri == null) { + val sizeBytes = call.argument("sizeBytes")?.toLong() + if (mimeType == null || uri == null) { result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null) return } - try { - StorageUtils.openInputStream(context, uri)?.use { input -> - val exif = ExifInterface(input) - val metadataMap = HashMap() - for (tag in ExifInterfaceHelper.allTags.keys.filter { exif.hasAttribute(it) }) { - metadataMap[tag] = exif.getAttribute(tag) + val metadataMap = HashMap() + if (isSupportedByExifInterface(mimeType, sizeBytes, strict = false)) { + try { + StorageUtils.openInputStream(context, uri)?.use { input -> + val exif = ExifInterface(input) + for (tag in ExifInterfaceHelper.allTags.keys.filter { exif.hasAttribute(it) }) { + metadataMap[tag] = exif.getAttribute(tag) + } } - result.success(metadataMap) - } ?: result.error("getExifInterfaceMetadata-noinput", "failed to get exif for uri=$uri", null) - } catch (e: Exception) { - // ExifInterface initialization can fail with a RuntimeException - // caused by an internal MediaMetadataRetriever failure - result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=$uri", e.message) + } catch (e: Exception) { + // ExifInterface initialization can fail with a RuntimeException + // caused by an internal MediaMetadataRetriever failure + result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=$uri", e.message) + return + } } + result.success(metadataMap) } 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) { + val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } - if (uri == null) { + val sizeBytes = call.argument("sizeBytes")?.toLong() + if (mimeType == null || uri == null) { result.error("getMetadataExtractorSummary-args", "failed because of missing arguments", null) return } val metadataMap = HashMap() - try { - StorageUtils.openInputStream(context, uri)?.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) - } else "" - } - metadataMap["typeName"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir -> - if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME)) { - dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME) - } else "" - } - for (dir in metadata.directories) { - val dirName = dir.name ?: "" - var index = 0 - while (metadataMap.containsKey("$dirName ($index)")) index++ - var value = "${dir.tagCount} tags" - dir.parent?.let { value += ", parent: ${it.name}" } - metadataMap["$dirName ($index)"] = value + if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { + try { + StorageUtils.openInputStream(context, uri)?.use { input -> + val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1) + 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) + } else "" + } + metadataMap["typeName"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir -> + if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME)) { + dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_TYPE_NAME) + } else "" + } + for (dir in metadata.directories) { + val dirName = dir.name ?: "" + var index = 0 + while (metadataMap.containsKey("$dirName ($index)")) index++ + var value = "${dir.tagCount} tags" + 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) { @@ -628,26 +636,30 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } - if (uri == null) { + val sizeBytes = call.argument("sizeBytes")?.toLong() + if (mimeType == null || uri == null) { result.error("getExifThumbnails-args", "failed because of missing arguments", null) return } val thumbnails = ArrayList() - try { - StorageUtils.openInputStream(context, uri)?.use { input -> - val exif = ExifInterface(input) - val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) - exif.thumbnailBitmap?.let { bitmap -> - TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let { - thumbnails.add(it.getBytes(canHaveAlpha = false, recycle = false)) + if (isSupportedByExifInterface(mimeType, sizeBytes)) { + try { + StorageUtils.openInputStream(context, uri)?.use { input -> + val exif = ExifInterface(input) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + exif.thumbnailBitmap?.let { bitmap -> + 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) } @@ -655,16 +667,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private fun getXmpThumbnails(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { result.error("getXmpThumbnails-args", "failed because of missing arguments", null) return } val thumbnails = ArrayList() - if (isSupportedByMetadataExtractor(mimeType)) { + if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { try { 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)) { val xmpMeta = dir.xmpMeta try { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt index aaf961622..409966f68 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt @@ -27,6 +27,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils +import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException class SourceImageEntry { @@ -129,7 +130,10 @@ class SourceImageEntry { fillByExifInterface(context) } if (!isSized) { - fillByBitmapDecode(context) + when (sourceMimeType) { + MimeTypes.TIFF -> fillByTiffDecode(context) + else -> fillByBitmapDecode(context) + } } return this } @@ -155,11 +159,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) || MimeTypes.isRaw(sourceMimeType)) return + if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType, sizeBytes) + || MimeTypes.isRaw(sourceMimeType) + ) return try { 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 // (e.g. PNG registered as JPG) @@ -207,7 +213,7 @@ class SourceImageEntry { // finds: width, height, orientation, date private fun fillByExifInterface(context: Context) { - if (!ExifInterface.isSupportedMimeType(sourceMimeType)) return; + if (!MimeTypes.isSupportedByExifInterface(sourceMimeType, sizeBytes)) return; try { 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 { // convenience method private fun toLong(o: Any?): Long? = when (o) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index be9117260..d2d535349 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -1,5 +1,7 @@ package deckers.thibault.aves.utils +import androidx.exifinterface.media.ExifInterface + object MimeTypes { private const val IMAGE = "image" @@ -65,12 +67,25 @@ object MimeTypes { else -> false } - // as of metadata-extractor v2.14.0 - fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) { + // 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) { 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 + } + // Glide automatically applies EXIF orientation when decoding images of known formats // but we need to rotate the decoded bitmap for the other formats // maybe related to ExifInterface version used by Glide: diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index d00498597..e253bc434 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -173,7 +173,7 @@ class ImageEntry { 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) - 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" // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below, diff --git a/lib/ref/brand_colors.dart b/lib/ref/brand_colors.dart index 1e5c99a91..17db1afd0 100644 --- a/lib/ref/brand_colors.dart +++ b/lib/ref/brand_colors.dart @@ -1,6 +1,7 @@ import 'package:flutter/painting.dart'; class BrandColors { + static const Color adobeAfterEffects = Color(0xFF9A9AFF); static const Color adobeIllustrator = Color(0xFFFF9B00); static const Color adobePhotoshop = Color(0xFF2DAAFF); static const Color android = Color(0xFF3DDC84); @@ -9,6 +10,8 @@ class BrandColors { static Color get(String text) { if (text != null) { switch (text.toLowerCase()) { + case 'after effects': + return adobeAfterEffects; case 'illustrator': return adobeIllustrator; case 'photoshop': diff --git a/lib/ref/xmp.dart b/lib/ref/xmp.dart index 932faa4c8..725728c75 100644 --- a/lib/ref/xmp.dart +++ b/lib/ref/xmp.dart @@ -7,6 +7,7 @@ class XMP { 'adsml-at': 'AdsML', 'aux': 'Exif Aux', 'Camera': 'Camera', + 'creatorAtom': 'After Effects', 'crs': 'Camera Raw Settings', 'dc': 'Dublin Core', 'drone-dji': 'DJI Drone', diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 43a4998b1..f3e7bfb54 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -17,6 +17,7 @@ class MetadataService { final result = await platform.invokeMethod('getAllMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, }); return result as Map; } on PlatformException catch (e) { @@ -44,6 +45,7 @@ class MetadataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'path': entry.path, + 'sizeBytes': entry.sizeBytes, }) as Map; result['contentId'] = entry.contentId; return CatalogMetadata.fromMap(result); @@ -69,6 +71,7 @@ class MetadataService { final result = await platform.invokeMethod('getOverlayMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, }) as Map; return OverlayMetadata.fromMap(result); } on PlatformException catch (e) { @@ -108,7 +111,9 @@ class MetadataService { try { // return map with all data available from the `ExifInterface` library final result = await platform.invokeMethod('getExifInterfaceMetadata', { + 'mimeType': entry.mimeType, 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, }) as Map; return result; } on PlatformException catch (e) { @@ -134,7 +139,9 @@ class MetadataService { try { // return map with the mime type and tag count for each directory found by `metadata-extractor` final result = await platform.invokeMethod('getMetadataExtractorSummary', { + 'mimeType': entry.mimeType, 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, }) as Map; return result; } on PlatformException catch (e) { @@ -155,10 +162,12 @@ class MetadataService { return []; } - static Future> getExifThumbnails(String uri) async { + static Future> getExifThumbnails(ImageEntry entry) async { try { final result = await platform.invokeMethod('getExifThumbnails', { - 'uri': uri, + 'mimeType': entry.mimeType, + 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, }); return (result as List).cast(); } on PlatformException catch (e) { @@ -172,6 +181,7 @@ class MetadataService { final result = await platform.invokeMethod('getXmpThumbnails', { 'mimeType': entry.mimeType, 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, }); return (result as List).cast(); } on PlatformException catch (e) { diff --git a/lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart b/lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart index 5a13d63dd..f47024f2a 100644 --- a/lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart @@ -36,7 +36,7 @@ class _MetadataThumbnailsState extends State { _loader = MetadataService.getEmbeddedPictures(uri); break; case MetadataThumbnailSource.exif: - _loader = MetadataService.getExifThumbnails(uri); + _loader = MetadataService.getExifThumbnails(entry); break; case MetadataThumbnailSource.xmp: _loader = MetadataService.getXmpThumbnails(entry);