#894 google xmp refactor
This commit is contained in:
parent
29487a1303
commit
b4a5513fe1
10 changed files with 239 additions and 183 deletions
|
@ -121,6 +121,7 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|
|
@ -11,14 +11,13 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||||
import deckers.thibault.aves.metadata.GoogleDeviceContainer
|
|
||||||
import deckers.thibault.aves.metadata.Metadata
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
import deckers.thibault.aves.metadata.MultiPage
|
import deckers.thibault.aves.metadata.MultiPage
|
||||||
import deckers.thibault.aves.metadata.XMP
|
|
||||||
import deckers.thibault.aves.metadata.XMP.doesPropPathExist
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
|
||||||
import deckers.thibault.aves.metadata.XMPPropName
|
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||||
|
import deckers.thibault.aves.metadata.xmp.GoogleDeviceContainer
|
||||||
|
import deckers.thibault.aves.metadata.xmp.GoogleXMP
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMPPropName
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.model.provider.ImageProvider
|
import deckers.thibault.aves.model.provider.ImageProvider
|
||||||
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
|
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
|
||||||
|
@ -108,14 +107,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
// which is returned as a second XMP directory
|
// which is returned as a second XMP directory
|
||||||
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
||||||
try {
|
try {
|
||||||
container = xmpDirs.firstNotNullOfOrNull {
|
container = xmpDirs.firstNotNullOfOrNull { GoogleXMP.getDeviceContainer(it.xmpMeta) }
|
||||||
val xmpMeta = it.xmpMeta
|
|
||||||
if (xmpMeta.doesPropPathExist(listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) {
|
|
||||||
GoogleDeviceContainer().apply { findItems(xmpMeta) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
|
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
|
||||||
return
|
return
|
||||||
|
|
|
@ -50,16 +50,6 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper
|
||||||
import deckers.thibault.aves.metadata.MultiPage
|
import deckers.thibault.aves.metadata.MultiPage
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper
|
import deckers.thibault.aves.metadata.PixyMetaHelper
|
||||||
import deckers.thibault.aves.metadata.QuickTimeMetadata
|
import deckers.thibault.aves.metadata.QuickTimeMetadata
|
||||||
import deckers.thibault.aves.metadata.XMP
|
|
||||||
import deckers.thibault.aves.metadata.XMP.doesPropExist
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getPropArrayItemValues
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeString
|
|
||||||
import deckers.thibault.aves.metadata.XMP.hasHdrGainMap
|
|
||||||
import deckers.thibault.aves.metadata.XMP.isMotionPhoto
|
|
||||||
import deckers.thibault.aves.metadata.XMP.isPanorama
|
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME
|
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_LAST_MODIFICATION_TIME_FORMAT
|
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_LAST_MODIFICATION_TIME_FORMAT
|
||||||
|
@ -78,6 +68,16 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
|
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
|
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
||||||
|
import deckers.thibault.aves.metadata.xmp.GoogleXMP
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.doesPropExist
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getPropArrayItemValues
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getSafeDateMillis
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getSafeInt
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getSafeLocalizedText
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.hasHdrGainMap
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.isMotionPhoto
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.isPanorama
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
@ -1020,17 +1020,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) {
|
fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) {
|
||||||
if (foundXmp && !allowMultiple) return
|
if (foundXmp && !allowMultiple) return
|
||||||
foundXmp = true
|
foundXmp = true
|
||||||
try {
|
fields.putAll(GoogleXMP.getPanoramaInfo(xmpMeta))
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
|
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
|
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
|
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
|
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
|
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
|
|
||||||
xmpMeta.getSafeString(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
|
|
||||||
} catch (e: XMPException) {
|
|
||||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
|
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
|
||||||
|
@ -1062,7 +1052,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
if (fields.isEmpty()) {
|
if (fields.isEmpty()) {
|
||||||
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
|
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
|
||||||
} else {
|
} else {
|
||||||
fields["projectionType"] = fields["projectionType"] ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT
|
fields["projectionType"] = fields["projectionType"] ?: GoogleXMP.GPANO_PROJECTION_TYPE_DEFAULT
|
||||||
result.success(fields)
|
result.success(fields)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.metadata
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
|
|
@ -12,13 +12,11 @@ import android.util.Log
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
import com.drew.imaging.jpeg.JpegSegmentType
|
import com.drew.imaging.jpeg.JpegSegmentType
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.metadata.XMP.countPropArrayItems
|
|
||||||
import deckers.thibault.aves.metadata.XMP.doesPropExist
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLong
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
||||||
|
import deckers.thibault.aves.metadata.xmp.GoogleXMP
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
@ -276,20 +274,7 @@ object MultiPage {
|
||||||
var foundXmp = false
|
var foundXmp = false
|
||||||
|
|
||||||
fun processXmp(xmpMeta: XMPMeta) {
|
fun processXmp(xmpMeta: XMPMeta) {
|
||||||
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
|
offsetFromEnd = GoogleXMP.getTrailingVideoOffsetFromEnd(xmpMeta)
|
||||||
// `GCamera` motion photo
|
|
||||||
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
|
|
||||||
} else if (xmpMeta.doesPropExist(XMP.GCONTAINER_DIRECTORY_PROP_NAME)) {
|
|
||||||
// `Container` motion photo
|
|
||||||
val count = xmpMeta.countPropArrayItems(XMP.GCONTAINER_DIRECTORY_PROP_NAME)
|
|
||||||
for (i in 1 until count + 1) {
|
|
||||||
val mime = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_MIME_PROP_NAME))?.value
|
|
||||||
val length = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
|
||||||
if (MimeTypes.isVideo(mime) && length != null) {
|
|
||||||
offsetFromEnd = length.toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import com.drew.imaging.mp4.Mp4Handler
|
||||||
import com.drew.metadata.Metadata
|
import com.drew.metadata.Metadata
|
||||||
import com.drew.metadata.mp4.Mp4Context
|
import com.drew.metadata.mp4.Mp4Context
|
||||||
import com.drew.metadata.mp4.media.Mp4UuidBoxHandler
|
import com.drew.metadata.mp4.media.Mp4UuidBoxHandler
|
||||||
import deckers.thibault.aves.metadata.XMP
|
import deckers.thibault.aves.metadata.xmp.XMP
|
||||||
|
|
||||||
class SafeMp4UuidBoxHandler(metadata: Metadata) : Mp4UuidBoxHandler(metadata) {
|
class SafeMp4UuidBoxHandler(metadata: Metadata) : Mp4UuidBoxHandler(metadata) {
|
||||||
override fun processBox(type: String?, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*> {
|
override fun processBox(type: String?, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*> {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package deckers.thibault.aves.metadata
|
package deckers.thibault.aves.metadata.xmp
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
import deckers.thibault.aves.metadata.XMP.countPropPathArrayItems
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
import deckers.thibault.aves.metadata.xmp.XMP.countPropPathArrayItems
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
|
||||||
import deckers.thibault.aves.utils.indexOfBytes
|
import deckers.thibault.aves.utils.indexOfBytes
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
|
|
||||||
|
@ -15,12 +16,12 @@ class GoogleDeviceContainer {
|
||||||
private val offsets: MutableList<Int> = ArrayList()
|
private val offsets: MutableList<Int> = ArrayList()
|
||||||
|
|
||||||
fun findItems(xmpMeta: XMPMeta) {
|
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)
|
val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath)
|
||||||
for (i in 1 until count + 1) {
|
for (i in 1 until count + 1) {
|
||||||
val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_MIME_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, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull()
|
val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.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 dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
|
||||||
if (mimeType != null && length != null && dataUri != null) {
|
if (mimeType != null && length != null && dataUri != null) {
|
||||||
items.add(
|
items.add(
|
||||||
GoogleDeviceContainerItem(
|
GoogleDeviceContainerItem(
|
|
@ -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<GoogleXMP>()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package deckers.thibault.aves.metadata
|
package deckers.thibault.aves.metadata.xmp
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
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.XMPMetaFactory
|
||||||
import com.adobe.internal.xmp.properties.XMPProperty
|
import com.adobe.internal.xmp.properties.XMPProperty
|
||||||
import com.drew.metadata.Directory
|
import com.drew.metadata.Directory
|
||||||
|
import deckers.thibault.aves.metadata.Mp4ParserHelper
|
||||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
|
import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
|
||||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
|
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
|
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/"
|
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
|
||||||
|
|
||||||
// other namespaces
|
// 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 HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/"
|
||||||
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
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 GENERIC_LANG = ""
|
||||||
private const val SPECIFIC_LANG = "en-US"
|
private const val SPECIFIC_LANG = "en-US"
|
||||||
|
|
||||||
// embedded media data properties
|
fun isDataPath(path: String) = GoogleXMP.isDataPath(path)
|
||||||
// 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"
|
|
||||||
|
|
||||||
// HDR gain map
|
// HDR gain map
|
||||||
|
|
||||||
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
|
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
|
||||||
|
|
||||||
// panorama
|
// 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")
|
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,
|
// 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
|
// so we fall back to the native content resolver, if possible
|
||||||
fun checkHeic(
|
fun checkHeic(
|
||||||
|
@ -191,20 +132,10 @@ object XMP {
|
||||||
fun XMPMeta.hasHdrGainMap(): Boolean {
|
fun XMPMeta.hasHdrGainMap(): Boolean {
|
||||||
try {
|
try {
|
||||||
// standard HDR gain map
|
// standard HDR gain map
|
||||||
if (doesPropExist(HDRGM_VERSION_PROP_NAME)) {
|
if (doesPropExist(HDRGM_VERSION_PROP_NAME)) return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// `Ultra HDR`
|
// `Ultra HDR`
|
||||||
if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
|
if (GoogleXMP.isUltraHdPhoto(this)) return true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
|
@ -217,47 +148,11 @@ object XMP {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.isMotionPhoto(): Boolean {
|
fun XMPMeta.isMotionPhoto() = GoogleXMP.isMotionPhoto(this)
|
||||||
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.isPanorama(): Boolean {
|
fun XMPMeta.isPanorama(): Boolean {
|
||||||
// Google
|
// Google
|
||||||
try {
|
if (GoogleXMP.isPanorama(this)) return true
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Photomatix
|
// Photomatix
|
||||||
try {
|
try {
|
|
@ -36,8 +36,8 @@ import deckers.thibault.aves.metadata.MultiPage
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper
|
import deckers.thibault.aves.metadata.PixyMetaHelper
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
|
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.metadataextractor.Helper
|
||||||
|
import deckers.thibault.aves.metadata.xmp.GoogleXMP
|
||||||
import deckers.thibault.aves.model.AvesEntry
|
import deckers.thibault.aves.model.AvesEntry
|
||||||
import deckers.thibault.aves.model.ExifOrientationOp
|
import deckers.thibault.aves.model.ExifOrientationOp
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
|
@ -982,15 +982,7 @@ abstract class ImageProvider {
|
||||||
)
|
)
|
||||||
val newTrailerOffset = trailerOffset + diff
|
val newTrailerOffset = trailerOffset + diff
|
||||||
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
|
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
|
||||||
xmp.replace(
|
GoogleXMP.updateTrailingVideoOffset(xmp, trailerOffset, newTrailerOffset)
|
||||||
// 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\"",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue