From 7f9229a227df251ec8d28439266dce4992168582 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 26 Apr 2023 18:50:45 +0200 Subject: [PATCH] prevent editing item when Exif editing changes mime type --- CHANGELOG.md | 1 + .../deckers/thibault/aves/MainActivity.kt | 2 +- .../aves/model/provider/ImageProvider.kt | 34 +++++++++++++++++++ .../thibault/aves/utils/StorageUtils.kt | 9 ++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbf03f1a1..b9c24ab23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Viewer: multi-page context update when removing burst entries +- prevent editing item when Exif editing changes mime type ## [v1.8.5] - 2023-04-18 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 67cc1de06..b9b327b9a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -336,7 +336,7 @@ open class MainActivity : FlutterFragmentActivity() { private fun submitPickedItems(call: MethodCall) { val pickedUris = call.argument>("uris") - if (pickedUris != null && pickedUris.isNotEmpty()) { + if (!pickedUris.isNullOrEmpty()) { val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) } val intent = Intent().apply { val firstUri = toUri(pickedUris.first()) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index c84596bea..1c7da6e20 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -31,6 +31,7 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.updateRotation import deckers.thibault.aves.metadata.Mp4ParserHelper.updateXmp import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString +import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.model.* import deckers.thibault.aves.utils.* import deckers.thibault.aves.utils.FileUtils.transferFrom @@ -330,6 +331,7 @@ abstract class ImageProvider { @Suppress("deprecation") Bitmap.CompressFormat.WEBP } + else -> throw Exception("unsupported export MIME type=$exportMimeType") } bitmap.compress(format, quality, output) @@ -592,12 +594,14 @@ abstract class ImageProvider { } nameWithoutExtension } + NameConflictStrategy.REPLACE -> { if (targetFile.exists()) { deletePath(contextWrapper, targetFile.path, mimeType) } desiredNameWithoutExtension } + NameConflictStrategy.SKIP -> { if (targetFile.exists()) { null @@ -608,6 +612,25 @@ abstract class ImageProvider { } } + // cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check + private fun detectMimeType(context: Context, uri: Uri, mimeType: String): String? { + var detectedMimeType: String? = null + if (MimeTypes.canReadWithMetadataExtractor(mimeType)) { + try { + StorageUtils.openInputStream(context, uri)?.use { input -> + detectedMimeType = Helper.readMimeType(input) + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } + } + return detectedMimeType + } + private fun editExif( context: Context, path: String, @@ -657,6 +680,11 @@ abstract class ImageProvider { try { edit(ExifInterface(editableFile)) + val editedMimeType = detectMimeType(context, Uri.fromFile(editableFile), mimeType) + if (editedMimeType != mimeType) { + throw Exception("editing Exif changes mimeType=$mimeType -> $editedMimeType for uri=$uri path=$path") + } + if (videoBytes != null) { // append trailer video, if any editableFile.appendBytes(videoBytes!!) @@ -730,8 +758,10 @@ abstract class ImageProvider { when { iptc != null -> PixyMetaHelper.setIptc(input, output, iptc) + canRemoveMetadata(mimeType) -> PixyMetaHelper.removeMetadata(input, output, setOf(TYPE_IPTC)) + else -> { Log.w(LOG_TAG, "setting empty IPTC for mimeType=$mimeType") PixyMetaHelper.setIptc(input, output, null) @@ -787,6 +817,7 @@ abstract class ImageProvider { newFields["rotationDegrees"] = degrees } } + "xmp" -> isoFile.updateXmp(value) } } @@ -1039,6 +1070,7 @@ abstract class ImageProvider { exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, ExifInterfaceHelper.GPS_TIME_FORMAT.format(date)) } } + shiftMinutes != null -> { // shift val shiftMillis = shiftMinutes * 60000 @@ -1067,6 +1099,7 @@ abstract class ImageProvider { } } } + else -> { // clear if (fields.contains(ExifInterface.TAG_DATETIME)) { @@ -1135,6 +1168,7 @@ abstract class ImageProvider { ExifInterface.TAG_GPS_LONGITUDE_REF -> { setLocation = true } + else -> { if (value is String) { exif.setAttribute(tag, value) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index 3de3ca4c0..f1385bdd9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -25,6 +25,7 @@ import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath import deckers.thibault.aves.utils.UriUtils.tryParseId import java.io.File +import java.io.FileInputStream import java.io.InputStream import java.io.OutputStream import java.util.* @@ -588,6 +589,7 @@ object StorageUtils { // e.g. `content://media/external_primary/downloads/...` getMediaUriImageVideoUri(uri, mimeType)?.let { imageVideUri -> return imageVideUri } } + uriPath?.contains("/file/") == true -> { // e.g. `content://media/external/file/...` // create an ad-hoc temporary file for decoding only @@ -601,6 +603,7 @@ object StorageUtils { } } } + uri.userInfo != null -> return stripMediaUriUserInfo(uri) } } @@ -617,6 +620,7 @@ object StorageUtils { // e.g. `content://media/external_primary/downloads/...` getMediaUriImageVideoUri(uri, mimeType)?.let { imageVideUri -> return imageVideUri } } + uri.userInfo != null -> return stripMediaUriUserInfo(uri) } } @@ -643,7 +647,10 @@ object StorageUtils { fun openInputStream(context: Context, uri: Uri): InputStream? { val effectiveUri = getOriginalUri(context, uri) return try { - context.contentResolver.openInputStream(effectiveUri) + return when (uri.scheme) { + ContentResolver.SCHEME_FILE -> FileInputStream(uri.path) + else -> context.contentResolver.openInputStream(effectiveUri) + } } catch (e: Exception) { // among various other exceptions, // opening a file marked pending and owned by another package throws an `IllegalStateException`