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`