diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c9fa9370a..fcdeaacd5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -121,6 +121,7 @@ android:label="@string/app_name" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" tools:targetApi="tiramisu"> { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleDeviceContainer.kt similarity index 82% rename from android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleDeviceContainer.kt index 60b74a507..41d27f9e0 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleDeviceContainer.kt @@ -1,10 +1,11 @@ -package deckers.thibault.aves.metadata +package deckers.thibault.aves.metadata.xmp import android.content.Context import android.net.Uri import com.adobe.internal.xmp.XMPMeta -import deckers.thibault.aves.metadata.XMP.countPropPathArrayItems -import deckers.thibault.aves.metadata.XMP.getSafeStructField +import deckers.thibault.aves.metadata.Metadata +import deckers.thibault.aves.metadata.xmp.XMP.countPropPathArrayItems +import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField import deckers.thibault.aves.utils.indexOfBytes import java.io.DataInputStream @@ -15,12 +16,12 @@ class GoogleDeviceContainer { private val offsets: MutableList = ArrayList() fun findItems(xmpMeta: XMPMeta) { - val containerDirectoryPath = listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME) + val containerDirectoryPath = listOf(GoogleXMP.GDEVICE_CONTAINER_PROP_NAME, GoogleXMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME) val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath) for (i in 1 until count + 1) { - val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value - val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull() - val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value + val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value + val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull() + val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value if (mimeType != null && length != null && dataUri != null) { items.add( GoogleDeviceContainerItem( diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleXMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleXMP.kt new file mode 100644 index 000000000..41550fc04 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleXMP.kt @@ -0,0 +1,199 @@ +package deckers.thibault.aves.metadata.xmp + +import android.util.Log +import com.adobe.internal.xmp.XMPError +import com.adobe.internal.xmp.XMPException +import com.adobe.internal.xmp.XMPMeta +import deckers.thibault.aves.metadata.xmp.XMP.countPropArrayItems +import deckers.thibault.aves.metadata.xmp.XMP.doesPropExist +import deckers.thibault.aves.metadata.xmp.XMP.doesPropPathExist +import deckers.thibault.aves.metadata.xmp.XMP.getSafeInt +import deckers.thibault.aves.metadata.xmp.XMP.getSafeLong +import deckers.thibault.aves.metadata.xmp.XMP.getSafeString +import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField +import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes + +object GoogleXMP { + private val LOG_TAG = LogUtils.createTag() + + // namespaces + private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/" + private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/" + private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/" + private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/" + private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/" + private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/" + private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/" + private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/" + private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/" + private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/" + + // embedded media data properties + // cf https://developers.google.com/depthmap-metadata + // cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format + private val knownDataProps = listOf( + XMPPropName(GAUDIO_NS_URI, "Data"), + XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"), + XMPPropName(GIMAGE_NS_URI, "Data"), + XMPPropName(GDEPTH_NS_URI, "Data"), + XMPPropName(GDEPTH_NS_URI, "Confidence"), + ) + + + fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it } + + // google portrait + + val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container") + val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory") + val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI") + val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length") + val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime") + + // container + + private val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset") + private val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory") + private val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item") + private val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length") + private val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime") + private val GCONTAINER_ITEM_SEMANTIC_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Semantic") + + private const val ITEM_SEMANTIC_GAIN_MAP = "GainMap" + + // panorama + // cf https://developers.google.com/streetview/spherical-metadata + + private val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels") + private val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels") + private val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels") + private val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels") + private val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels") + private val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels") + private val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType") + const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular" + + // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default + // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode) + private val gpanoRequiredProps = listOf( + GPANO_CROPPED_AREA_HEIGHT_PROP_NAME, + GPANO_CROPPED_AREA_WIDTH_PROP_NAME, + GPANO_CROPPED_AREA_LEFT_PROP_NAME, + GPANO_CROPPED_AREA_TOP_PROP_NAME, + GPANO_FULL_PANO_WIDTH_PROP_NAME, + ) + + fun isUltraHdPhoto(meta: XMPMeta): Boolean { + if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) { + val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME) + for (i in 1 until count + 1) { + val semantic = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_SEMANTIC_PROP_NAME))?.value + if (semantic == ITEM_SEMANTIC_GAIN_MAP) { + return true + } + } + } + return false + } + + fun isMotionPhoto(meta: XMPMeta): Boolean { + try { + // GCamera motion photo + if (meta.doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true + + // Container motion photo + if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) { + val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME) + var hasImage = false + var hasVideo = false + for (i in 1 until count + 1) { + val mime = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value + val length = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value + hasImage = hasImage || MimeTypes.isImage(mime) && length != null + hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null + } + if (hasImage && hasVideo) return true + } + + return false + } catch (e: XMPException) { + if (e.errorCode != XMPError.BADSCHEMA) { + // `BADSCHEMA` code is reported when we check a property + // from a non standard namespace, and that namespace is not declared in the XMP + Log.w(LOG_TAG, "failed to check Google motion photo props from XMP", e) + } + } + return false + } + + fun isPanorama(meta: XMPMeta): Boolean { + try { + if (gpanoRequiredProps.all { meta.doesPropExist(it) }) return true + } catch (e: XMPException) { + if (e.errorCode != XMPError.BADSCHEMA) { + // `BADSCHEMA` code is reported when we check a property + // from a non standard namespace, and that namespace is not declared in the XMP + Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e) + } + } + return false + } + + fun getPanoramaInfo(meta: XMPMeta): FieldMap { + val fields: FieldMap = hashMapOf() + try { + meta.getSafeInt(GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it } + meta.getSafeInt(GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it } + meta.getSafeInt(GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it } + meta.getSafeInt(GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it } + meta.getSafeInt(GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it } + meta.getSafeInt(GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it } + meta.getSafeString(GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it } + } catch (e: XMPException) { + Log.w(LOG_TAG, "failed to read XMP directory", e) + } + return fields + } + + fun getTrailingVideoOffsetFromEnd(meta: XMPMeta): Long? { + var offsetFromEnd: Long? = null + if (meta.doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) { + // `GCamera` motion photo + meta.getSafeLong(GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it } + } else if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) { + // `Container` motion photo + val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME) + for (i in 1 until count + 1) { + val mime = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value + val length = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value + if (MimeTypes.isVideo(mime) && length != null) { + offsetFromEnd = length.toLong() + } + } + } + return offsetFromEnd + } + + fun updateTrailingVideoOffset(xmp: String, oldOffset: Int, newOffset: Int): String { + return xmp.replace( + // GCamera motion photo + "${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$oldOffset\"", + "${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newOffset\"", + ).replace( + // Container motion photo + "${GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$oldOffset\"", + "${GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newOffset\"", + ) + } + + + fun getDeviceContainer(meta: XMPMeta): GoogleDeviceContainer? { + return if (meta.doesPropPathExist(listOf(GDEVICE_CONTAINER_PROP_NAME, GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) { + GoogleDeviceContainer().apply { findItems(meta) } + } else { + null + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt similarity index 67% rename from android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt index 2744a70aa..47a26ca7d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt @@ -1,4 +1,4 @@ -package deckers.thibault.aves.metadata +package deckers.thibault.aves.metadata.xmp import android.content.Context import android.net.Uri @@ -11,6 +11,7 @@ import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMetaFactory import com.adobe.internal.xmp.properties.XMPProperty import com.drew.metadata.Directory +import deckers.thibault.aves.metadata.Mp4ParserHelper import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler @@ -39,16 +40,6 @@ object XMP { private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/" // other namespaces - private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/" - private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/" - private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/" - private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/" - private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/" - private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/" - private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/" - private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/" - private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/" - private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/" private const val HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/" private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01" @@ -63,66 +54,16 @@ object XMP { private const val GENERIC_LANG = "" private const val SPECIFIC_LANG = "en-US" - // embedded media data properties - // cf https://developers.google.com/depthmap-metadata - // cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format - private val knownDataProps = listOf( - XMPPropName(GAUDIO_NS_URI, "Data"), - XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"), - XMPPropName(GIMAGE_NS_URI, "Data"), - XMPPropName(GDEPTH_NS_URI, "Data"), - XMPPropName(GDEPTH_NS_URI, "Confidence"), - ) - - fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it } - - // google portrait - - val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container") - val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory") - val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI") - val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length") - val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime") - - // container - - val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset") - val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory") - val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item") - val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length") - val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime") - private val GCONTAINER_ITEM_SEMANTIC_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Semantic") - - private const val ITEM_SEMANTIC_GAIN_MAP = "GainMap" + fun isDataPath(path: String) = GoogleXMP.isDataPath(path) // HDR gain map private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version") // panorama - // cf https://developers.google.com/streetview/spherical-metadata - - val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels") - val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels") - val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels") - val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels") - val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels") - val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels") - val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType") - const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular" private val PMTM_IS_PANO360_PROP_NAME = XMPPropName(PMTM_NS_URI, "IsPano360") - // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default - // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode) - private val gpanoRequiredProps = listOf( - GPANO_CROPPED_AREA_HEIGHT_PROP_NAME, - GPANO_CROPPED_AREA_WIDTH_PROP_NAME, - GPANO_CROPPED_AREA_LEFT_PROP_NAME, - GPANO_CROPPED_AREA_TOP_PROP_NAME, - GPANO_FULL_PANO_WIDTH_PROP_NAME, - ) - // as of `metadata-extractor` v2.18.0, XMP is not discovered in HEIC images, // so we fall back to the native content resolver, if possible fun checkHeic( @@ -191,20 +132,10 @@ object XMP { fun XMPMeta.hasHdrGainMap(): Boolean { try { // standard HDR gain map - if (doesPropExist(HDRGM_VERSION_PROP_NAME)) { - return true - } + if (doesPropExist(HDRGM_VERSION_PROP_NAME)) return true // `Ultra HDR` - if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) { - val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME) - for (i in 1 until count + 1) { - val semantic = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_SEMANTIC_PROP_NAME))?.value - if (semantic == ITEM_SEMANTIC_GAIN_MAP) { - return true - } - } - } + if (GoogleXMP.isUltraHdPhoto(this)) return true return false } catch (e: XMPException) { @@ -217,47 +148,11 @@ object XMP { return false } - fun XMPMeta.isMotionPhoto(): Boolean { - try { - // GCamera motion photo - if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true - - // Container motion photo - if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) { - val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME) - var hasImage = false - var hasVideo = false - for (i in 1 until count + 1) { - val mime = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value - val length = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value - hasImage = hasImage || MimeTypes.isImage(mime) && length != null - hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null - } - if (hasImage && hasVideo) return true - } - - return false - } catch (e: XMPException) { - if (e.errorCode != XMPError.BADSCHEMA) { - // `BADSCHEMA` code is reported when we check a property - // from a non standard namespace, and that namespace is not declared in the XMP - Log.w(LOG_TAG, "failed to check Google motion photo props from XMP", e) - } - } - return false - } + fun XMPMeta.isMotionPhoto() = GoogleXMP.isMotionPhoto(this) fun XMPMeta.isPanorama(): Boolean { // Google - try { - if (gpanoRequiredProps.all { doesPropExist(it) }) return true - } catch (e: XMPException) { - if (e.errorCode != XMPError.BADSCHEMA) { - // `BADSCHEMA` code is reported when we check a property - // from a non standard namespace, and that namespace is not declared in the XMP - Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e) - } - } + if (GoogleXMP.isPanorama(this)) return true // Photomatix try { 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 093f2c997..e3c654741 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 @@ -36,8 +36,8 @@ import deckers.thibault.aves.metadata.MultiPage import deckers.thibault.aves.metadata.PixyMetaHelper import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString -import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.metadataextractor.Helper +import deckers.thibault.aves.metadata.xmp.GoogleXMP import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.FieldMap @@ -982,15 +982,7 @@ abstract class ImageProvider { ) val newTrailerOffset = trailerOffset + diff return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp -> - xmp.replace( - // GCamera motion photo - "${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$trailerOffset\"", - "${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newTrailerOffset\"", - ).replace( - // Container motion photo - "${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"", - "${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"", - ) + GoogleXMP.updateTrailingVideoOffset(xmp, trailerOffset, newTrailerOffset) }) }