#304 use xmp namespace URIs instead of prefixes
This commit is contained in:
parent
5b717d69d4
commit
cf5711e0f6
24 changed files with 445 additions and 323 deletions
|
@ -14,8 +14,8 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||||
import deckers.thibault.aves.metadata.Metadata
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper
|
||||||
import deckers.thibault.aves.metadata.MultiPage
|
import deckers.thibault.aves.metadata.MultiPage
|
||||||
import deckers.thibault.aves.metadata.XMP
|
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
||||||
|
import deckers.thibault.aves.metadata.XMPPropName
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.model.provider.ContentImageProvider
|
import deckers.thibault.aves.model.provider.ContentImageProvider
|
||||||
import deckers.thibault.aves.model.provider.ImageProvider
|
import deckers.thibault.aves.model.provider.ImageProvider
|
||||||
|
@ -140,13 +140,21 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
val displayName = call.argument<String>("displayName")
|
val displayName = call.argument<String>("displayName")
|
||||||
val dataPropPath = call.argument<String>("propPath")
|
val dataProp = call.argument<List<Any>>("propPath")
|
||||||
val embedMimeType = call.argument<String>("propMimeType")
|
val embedMimeType = call.argument<String>("propMimeType")
|
||||||
if (mimeType == null || uri == null || dataPropPath == null || embedMimeType == null) {
|
if (mimeType == null || uri == null || dataProp == null || embedMimeType == null) {
|
||||||
result.error("extractXmpDataProp-args", "missing arguments", null)
|
result.error("extractXmpDataProp-args", "missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val props = dataProp.mapNotNull {
|
||||||
|
when (it) {
|
||||||
|
is List<*> -> XMPPropName(it.first() as String, it.last() as String)
|
||||||
|
is Int -> it
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (canReadWithMetadataExtractor(mimeType)) {
|
if (canReadWithMetadataExtractor(mimeType)) {
|
||||||
try {
|
try {
|
||||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
|
@ -155,11 +163,11 @@ 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 {
|
||||||
val embedBytes: ByteArray = if (!dataPropPath.contains('/')) {
|
val embedBytes: ByteArray = if (props.size == 1) {
|
||||||
val propNs = XMP.namespaceForPropPath(dataPropPath)
|
val prop = props.first() as XMPPropName
|
||||||
xmpDirs.mapNotNull { it.xmpMeta.getPropertyBase64(propNs, dataPropPath) }.first()
|
xmpDirs.mapNotNull { it.xmpMeta.getPropertyBase64(prop.nsUri, prop.toString()) }.first()
|
||||||
} else {
|
} else {
|
||||||
xmpDirs.mapNotNull { it.xmpMeta.getSafeStructField(dataPropPath) }.first().let {
|
xmpDirs.mapNotNull { it.xmpMeta.getSafeStructField(props) }.first().let {
|
||||||
XMPUtils.decodeBase64(it.value)
|
XMPUtils.decodeBase64(it.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,7 +175,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
copyEmbeddedBytes(result, embedMimeType, displayName, embedBytes.inputStream())
|
copyEmbeddedBytes(result, embedMimeType, displayName, embedBytes.inputStream())
|
||||||
return
|
return
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
result.error("extractXmpDataProp-xmp", "failed to read XMP directory for uri=$uri prop=$dataPropPath", e.message)
|
result.error("extractXmpDataProp-xmp", "failed to read XMP directory for uri=$uri prop=$dataProp", e.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +187,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
Log.w(LOG_TAG, "failed to extract file from XMP", e)
|
Log.w(LOG_TAG, "failed to extract file from XMP", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
|
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataProp", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyEmbeddedBytes(result: MethodChannel.Result, mimeType: String, displayName: String?, embeddedByteStream: InputStream) {
|
private fun copyEmbeddedBytes(result: MethodChannel.Result, mimeType: String, displayName: String?, embeddedByteStream: InputStream) {
|
||||||
|
|
|
@ -59,6 +59,8 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isPngTextDir
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isPngTextDir
|
||||||
|
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.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
||||||
|
@ -83,6 +85,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONObject
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
@ -280,6 +283,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
// remove this stat as it is not actual XMP data
|
// remove this stat as it is not actual XMP data
|
||||||
dirMap.remove(dir.getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT))
|
dirMap.remove(dir.getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT))
|
||||||
|
// add schema prefixes for namespace resolution
|
||||||
|
val prefixes = XMPMetaFactory.getSchemaRegistry().prefixes
|
||||||
|
dirMap["schemaRegistryPrefixes"] = JSONObject(prefixes).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir is Mp4UuidBoxDirectory) {
|
if (dir is Mp4UuidBoxDirectory) {
|
||||||
|
@ -509,22 +515,21 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
val xmpMeta = dir.xmpMeta
|
val xmpMeta = dir.xmpMeta
|
||||||
try {
|
try {
|
||||||
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)) {
|
if (xmpMeta.doesPropExist(XMP.DC_SUBJECT_PROP_NAME)) {
|
||||||
val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)
|
val values = xmpMeta.getPropArrayItemValues(XMP.DC_SUBJECT_PROP_NAME)
|
||||||
val values = (1 until count + 1).map { xmpMeta.getArrayItem(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME, it).value }
|
|
||||||
metadataMap[KEY_XMP_SUBJECTS] = values.joinToString(XMP_SUBJECTS_SEPARATOR)
|
metadataMap[KEY_XMP_SUBJECTS] = values.joinToString(XMP_SUBJECTS_SEPARATOR)
|
||||||
}
|
}
|
||||||
xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE] = it }
|
xmpMeta.getSafeLocalizedText(XMP.DC_TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE] = it }
|
||||||
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||||
xmpMeta.getSafeDateMillis(XMP.XMP_SCHEMA_NS, XMP.XMP_CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
|
xmpMeta.getSafeDateMillis(XMP.XMP_CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
|
||||||
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||||
xmpMeta.getSafeDateMillis(XMP.PHOTOSHOP_SCHEMA_NS, XMP.PS_DATE_CREATED_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
|
xmpMeta.getSafeDateMillis(XMP.PS_DATE_CREATED_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xmpMeta.getSafeInt(XMP.XMP_SCHEMA_NS, XMP.XMP_RATING_PROP_NAME) { metadataMap[KEY_RATING] = it }
|
xmpMeta.getSafeInt(XMP.XMP_RATING_PROP_NAME) { metadataMap[KEY_RATING] = it }
|
||||||
if (!metadataMap.containsKey(KEY_RATING)) {
|
if (!metadataMap.containsKey(KEY_RATING)) {
|
||||||
xmpMeta.getSafeInt(XMP.MICROSOFTPHOTO_SCHEMA_NS, XMP.MS_RATING_PROP_NAME) { percentRating ->
|
xmpMeta.getSafeInt(XMP.MS_RATING_PROP_NAME) { percentRating ->
|
||||||
// values of 1,25,50,75,99% correspond to 1,2,3,4,5 stars
|
// values of 1,25,50,75,99% correspond to 1,2,3,4,5 stars
|
||||||
val standardRating = (percentRating / 25f).roundToInt() + 1
|
val standardRating = (percentRating / 25f).roundToInt() + 1
|
||||||
metadataMap[KEY_RATING] = standardRating
|
metadataMap[KEY_RATING] = standardRating
|
||||||
|
@ -834,13 +839,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
)
|
)
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
val xmpMeta = dir.xmpMeta
|
val xmpMeta = dir.xmpMeta
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
|
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
|
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
|
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
|
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
|
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
|
||||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
|
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
|
||||||
xmpMeta.getSafeString(XMP.GPANO_SCHEMA_NS, XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
|
xmpMeta.getSafeString(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
|
||||||
}
|
}
|
||||||
result.success(fields)
|
result.success(fields)
|
||||||
return
|
return
|
||||||
|
@ -1071,8 +1076,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
val xmpMeta = dir.xmpMeta
|
val xmpMeta = dir.xmpMeta
|
||||||
try {
|
try {
|
||||||
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME)) {
|
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
|
||||||
xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
|
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
|
||||||
}
|
}
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
||||||
|
|
|
@ -9,6 +9,8 @@ import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.getSafeLong
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
|
@ -146,17 +148,17 @@ object MultiPage {
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
var offsetFromEnd: Long? = null
|
var offsetFromEnd: Long? = null
|
||||||
val xmpMeta = dir.xmpMeta
|
val xmpMeta = dir.xmpMeta
|
||||||
if (xmpMeta.doesPropertyExist(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
|
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
|
||||||
// GCamera motion photo
|
// GCamera motion photo
|
||||||
xmpMeta.getSafeLong(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
|
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
|
||||||
} else if (xmpMeta.doesPropertyExist(XMP.CONTAINER_SCHEMA_NS, XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
|
} else if (xmpMeta.doesPropExist(XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||||
// Container motion photo
|
// Container motion photo
|
||||||
val count = xmpMeta.countArrayItems(XMP.CONTAINER_SCHEMA_NS, XMP.CONTAINER_DIRECTORY_PROP_NAME)
|
val count = xmpMeta.countPropArrayItems(XMP.CONTAINER_DIRECTORY_PROP_NAME)
|
||||||
if (count == 2) {
|
if (count == 2) {
|
||||||
// expect the video to be the second item
|
// expect the video to be the second item
|
||||||
val i = 2
|
val i = 2
|
||||||
val mime = xmpMeta.getSafeStructField("${XMP.CONTAINER_DIRECTORY_PROP_NAME}[$i]/${XMP.CONTAINER_ITEM_PROP_NAME}/${XMP.CONTAINER_ITEM_MIME_PROP_NAME}")?.value
|
val mime = xmpMeta.getSafeStructField(listOf(XMP.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_MIME_PROP_NAME))?.value
|
||||||
val length = xmpMeta.getSafeStructField("${XMP.CONTAINER_DIRECTORY_PROP_NAME}[$i]/${XMP.CONTAINER_ITEM_PROP_NAME}/${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}")?.value
|
val length = xmpMeta.getSafeStructField(listOf(XMP.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||||
if (MimeTypes.isVideo(mime) && length != null) {
|
if (MimeTypes.isVideo(mime) && length != null) {
|
||||||
offsetFromEnd = length.toLong()
|
offsetFromEnd = length.toLong()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.util.Log
|
||||||
import com.adobe.internal.xmp.XMPError
|
import com.adobe.internal.xmp.XMPError
|
||||||
import com.adobe.internal.xmp.XMPException
|
import com.adobe.internal.xmp.XMPException
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
|
import com.adobe.internal.xmp.XMPMetaFactory
|
||||||
import com.adobe.internal.xmp.properties.XMPProperty
|
import com.adobe.internal.xmp.properties.XMPProperty
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
@ -14,74 +15,65 @@ object XMP {
|
||||||
|
|
||||||
// standard namespaces
|
// standard namespaces
|
||||||
// cf com.adobe.internal.xmp.XMPConst
|
// cf com.adobe.internal.xmp.XMPConst
|
||||||
const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/"
|
private const val DC_NS_URI = "http://purl.org/dc/elements/1.1/"
|
||||||
const val MICROSOFTPHOTO_SCHEMA_NS = "http://ns.microsoft.com/photo/1.0/"
|
private const val MICROSOFTPHOTO_NS_URI = "http://ns.microsoft.com/photo/1.0/"
|
||||||
const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/"
|
private const val PHOTOSHOP_NS_URI = "http://ns.adobe.com/photoshop/1.0/"
|
||||||
const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/"
|
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
|
||||||
private const val XMP_GIMG_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/g/img/"
|
|
||||||
|
|
||||||
// other namespaces
|
// other namespaces
|
||||||
private const val GAUDIO_SCHEMA_NS = "http://ns.google.com/photos/1.0/audio/"
|
private const val CONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
|
||||||
const val GCAMERA_SCHEMA_NS = "http://ns.google.com/photos/1.0/camera/"
|
private const val CONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
|
||||||
private const val GDEPTH_SCHEMA_NS = "http://ns.google.com/photos/1.0/depthmap/"
|
private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/"
|
||||||
private const val GIMAGE_SCHEMA_NS = "http://ns.google.com/photos/1.0/image/"
|
private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/"
|
||||||
const val CONTAINER_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/"
|
private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
|
||||||
private const val CONTAINER_ITEM_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/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 PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
||||||
|
|
||||||
const val DC_SUBJECT_PROP_NAME = "dc:subject"
|
val DC_SUBJECT_PROP_NAME = XMPPropName(DC_NS_URI, "subject")
|
||||||
const val DC_DESCRIPTION_PROP_NAME = "dc:description"
|
val DC_DESCRIPTION_PROP_NAME = XMPPropName(DC_NS_URI, "description")
|
||||||
const val DC_TITLE_PROP_NAME = "dc:title"
|
val DC_TITLE_PROP_NAME = XMPPropName(DC_NS_URI, "title")
|
||||||
const val MS_RATING_PROP_NAME = "MicrosoftPhoto:Rating"
|
val MS_RATING_PROP_NAME = XMPPropName(MICROSOFTPHOTO_NS_URI, "Rating")
|
||||||
const val PS_DATE_CREATED_PROP_NAME = "photoshop:DateCreated"
|
val PS_DATE_CREATED_PROP_NAME = XMPPropName(PHOTOSHOP_NS_URI, "DateCreated")
|
||||||
const val XMP_CREATE_DATE_PROP_NAME = "xmp:CreateDate"
|
val XMP_CREATE_DATE_PROP_NAME = XMPPropName(XMP_NS_URI, "CreateDate")
|
||||||
const val XMP_RATING_PROP_NAME = "xmp:Rating"
|
val XMP_RATING_PROP_NAME = XMPPropName(XMP_NS_URI, "Rating")
|
||||||
|
|
||||||
private const val GENERIC_LANG = ""
|
private const val GENERIC_LANG = ""
|
||||||
private const val SPECIFIC_LANG = "en-US"
|
private const val SPECIFIC_LANG = "en-US"
|
||||||
|
|
||||||
private val schemas = hashMapOf(
|
|
||||||
"Container" to CONTAINER_SCHEMA_NS,
|
|
||||||
"GAudio" to GAUDIO_SCHEMA_NS,
|
|
||||||
"GDepth" to GDEPTH_SCHEMA_NS,
|
|
||||||
"GImage" to GIMAGE_SCHEMA_NS,
|
|
||||||
"Item" to CONTAINER_ITEM_SCHEMA_NS,
|
|
||||||
"xmp" to XMP_SCHEMA_NS,
|
|
||||||
"xmpGImg" to XMP_GIMG_SCHEMA_NS,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun namespaceForPropPath(propPath: String) = schemas[propPath.split(":")[0]]
|
|
||||||
|
|
||||||
// embedded media data properties
|
// embedded media data properties
|
||||||
// cf https://developers.google.com/depthmap-metadata
|
// cf https://developers.google.com/depthmap-metadata
|
||||||
// cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
|
// cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
|
||||||
private val knownDataPaths = listOf("GAudio:Data", "GImage:Data", "GDepth:Data", "GDepth:Confidence")
|
private val knownDataProps = listOf(
|
||||||
|
XMPPropName(GAUDIO_NS_URI, "Data"),
|
||||||
|
XMPPropName(GIMAGE_NS_URI, "Data"),
|
||||||
|
XMPPropName(GDEPTH_NS_URI, "Data"),
|
||||||
|
XMPPropName(GDEPTH_NS_URI, "Confidence"),
|
||||||
|
)
|
||||||
|
|
||||||
fun isDataPath(path: String) = knownDataPaths.contains(path)
|
fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it }
|
||||||
|
|
||||||
// motion photo
|
// motion photo
|
||||||
|
|
||||||
const val GCAMERA_VIDEO_OFFSET_PROP_NAME = "GCamera:MicroVideoOffset"
|
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
|
||||||
const val CONTAINER_DIRECTORY_PROP_NAME = "Container:Directory"
|
val CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Directory")
|
||||||
const val CONTAINER_ITEM_PROP_NAME = "Container:Item"
|
val CONTAINER_ITEM_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Item")
|
||||||
const val CONTAINER_ITEM_LENGTH_PROP_NAME = "Item:Length"
|
val CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Length")
|
||||||
const val CONTAINER_ITEM_MIME_PROP_NAME = "Item:Mime"
|
val CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Mime")
|
||||||
|
|
||||||
// panorama
|
// panorama
|
||||||
// cf https://developers.google.com/streetview/spherical-metadata
|
// cf https://developers.google.com/streetview/spherical-metadata
|
||||||
|
|
||||||
const val GPANO_SCHEMA_NS = "http://ns.google.com/photos/1.0/panorama/"
|
val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels")
|
||||||
private const val PMTM_SCHEMA_NS = "http://www.hdrsoft.com/photomatix_settings01"
|
val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels")
|
||||||
|
val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels")
|
||||||
const val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = "GPano:CroppedAreaImageHeightPixels"
|
val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels")
|
||||||
const val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = "GPano:CroppedAreaImageWidthPixels"
|
val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels")
|
||||||
const val GPANO_CROPPED_AREA_LEFT_PROP_NAME = "GPano:CroppedAreaLeftPixels"
|
val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels")
|
||||||
const val GPANO_CROPPED_AREA_TOP_PROP_NAME = "GPano:CroppedAreaTopPixels"
|
val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType")
|
||||||
const val GPANO_FULL_PANO_HEIGHT_PROP_NAME = "GPano:FullPanoHeightPixels"
|
|
||||||
const val GPANO_FULL_PANO_WIDTH_PROP_NAME = "GPano:FullPanoWidthPixels"
|
|
||||||
const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType"
|
|
||||||
const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular"
|
const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular"
|
||||||
|
|
||||||
private const val PMTM_IS_PANO360 = "pmtm: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: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)
|
// `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode)
|
||||||
|
@ -98,17 +90,17 @@ object XMP {
|
||||||
fun XMPMeta.isMotionPhoto(): Boolean {
|
fun XMPMeta.isMotionPhoto(): Boolean {
|
||||||
try {
|
try {
|
||||||
// GCamera motion photo
|
// GCamera motion photo
|
||||||
if (doesPropertyExist(GCAMERA_SCHEMA_NS, GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
|
if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
|
||||||
|
|
||||||
// Container motion photo
|
// Container motion photo
|
||||||
if (doesPropertyExist(CONTAINER_SCHEMA_NS, CONTAINER_DIRECTORY_PROP_NAME)) {
|
if (doesPropExist(CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||||
val count = countArrayItems(CONTAINER_SCHEMA_NS, CONTAINER_DIRECTORY_PROP_NAME)
|
val count = countPropArrayItems(CONTAINER_DIRECTORY_PROP_NAME)
|
||||||
if (count == 2) {
|
if (count == 2) {
|
||||||
var hasImage = false
|
var hasImage = false
|
||||||
var hasVideo = false
|
var hasVideo = false
|
||||||
for (i in 1 until count + 1) {
|
for (i in 1 until count + 1) {
|
||||||
val mime = getSafeStructField("$CONTAINER_DIRECTORY_PROP_NAME[$i]/$CONTAINER_ITEM_PROP_NAME/$CONTAINER_ITEM_MIME_PROP_NAME")?.value
|
val mime = getSafeStructField(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_MIME_PROP_NAME))?.value
|
||||||
val length = getSafeStructField("$CONTAINER_DIRECTORY_PROP_NAME[$i]/$CONTAINER_ITEM_PROP_NAME/$CONTAINER_ITEM_LENGTH_PROP_NAME")?.value
|
val length = getSafeStructField(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||||
hasImage = hasImage || MimeTypes.isImage(mime) && length != null
|
hasImage = hasImage || MimeTypes.isImage(mime) && length != null
|
||||||
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
|
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
|
||||||
}
|
}
|
||||||
|
@ -130,7 +122,7 @@ object XMP {
|
||||||
fun XMPMeta.isPanorama(): Boolean {
|
fun XMPMeta.isPanorama(): Boolean {
|
||||||
// Google
|
// Google
|
||||||
try {
|
try {
|
||||||
if (gpanoRequiredProps.all { doesPropertyExist(GPANO_SCHEMA_NS, it) }) return true
|
if (gpanoRequiredProps.all { doesPropExist(it) }) return true
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
if (e.errorCode != XMPError.BADSCHEMA) {
|
if (e.errorCode != XMPError.BADSCHEMA) {
|
||||||
// `BADSCHEMA` code is reported when we check a property
|
// `BADSCHEMA` code is reported when we check a property
|
||||||
|
@ -141,7 +133,7 @@ object XMP {
|
||||||
|
|
||||||
// Photomatix
|
// Photomatix
|
||||||
try {
|
try {
|
||||||
if (getPropertyString(PMTM_SCHEMA_NS, PMTM_IS_PANO360) == "Yes") return true
|
if (getPropertyString(PMTM_IS_PANO360_PROP_NAME.nsUri, PMTM_IS_PANO360_PROP_NAME.toString()) == "Yes") return true
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
if (e.errorCode != XMPError.BADSCHEMA) {
|
if (e.errorCode != XMPError.BADSCHEMA) {
|
||||||
// `BADSCHEMA` code is reported when we check a property
|
// `BADSCHEMA` code is reported when we check a property
|
||||||
|
@ -153,7 +145,24 @@ object XMP {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.getSafeInt(schema: String, propName: String, save: (value: Int) -> Unit) {
|
fun XMPMeta.doesPropExist(prop: XMPPropName): Boolean {
|
||||||
|
return doesPropertyExist(prop.nsUri, prop.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun XMPMeta.countPropArrayItems(prop: XMPPropName): Int {
|
||||||
|
return countArrayItems(prop.nsUri, prop.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun XMPMeta.getPropArrayItemValues(prop: XMPPropName): List<String> {
|
||||||
|
val schema = prop.nsUri
|
||||||
|
val propName = prop.toString()
|
||||||
|
val count = countArrayItems(schema, propName)
|
||||||
|
return (1 until count + 1).map { getArrayItem(schema, propName, it).value }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun XMPMeta.getSafeInt(prop: XMPPropName, save: (value: Int) -> Unit) {
|
||||||
|
val schema = prop.nsUri
|
||||||
|
val propName = prop.toString()
|
||||||
try {
|
try {
|
||||||
if (doesPropertyExist(schema, propName)) {
|
if (doesPropertyExist(schema, propName)) {
|
||||||
val item = getPropertyInteger(schema, propName)
|
val item = getPropertyInteger(schema, propName)
|
||||||
|
@ -167,7 +176,9 @@ object XMP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.getSafeLong(schema: String, propName: String, save: (value: Long) -> Unit) {
|
fun XMPMeta.getSafeLong(prop: XMPPropName, save: (value: Long) -> Unit) {
|
||||||
|
val schema = prop.nsUri
|
||||||
|
val propName = prop.toString()
|
||||||
try {
|
try {
|
||||||
if (doesPropertyExist(schema, propName)) {
|
if (doesPropertyExist(schema, propName)) {
|
||||||
val item = getPropertyLong(schema, propName)
|
val item = getPropertyLong(schema, propName)
|
||||||
|
@ -181,7 +192,9 @@ object XMP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.getSafeString(schema: String, propName: String, save: (value: String) -> Unit) {
|
fun XMPMeta.getSafeString(prop: XMPPropName, save: (value: String) -> Unit) {
|
||||||
|
val schema = prop.nsUri
|
||||||
|
val propName = prop.toString()
|
||||||
try {
|
try {
|
||||||
if (doesPropertyExist(schema, propName)) {
|
if (doesPropertyExist(schema, propName)) {
|
||||||
val item = getPropertyString(schema, propName)
|
val item = getPropertyString(schema, propName)
|
||||||
|
@ -195,7 +208,9 @@ object XMP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.getSafeLocalizedText(schema: String, propName: String, acceptBlank: Boolean = true, save: (value: String) -> Unit) {
|
fun XMPMeta.getSafeLocalizedText(prop: XMPPropName, acceptBlank: Boolean = true, save: (value: String) -> Unit) {
|
||||||
|
val schema = prop.nsUri
|
||||||
|
val propName = prop.toString()
|
||||||
try {
|
try {
|
||||||
if (doesPropertyExist(schema, propName)) {
|
if (doesPropertyExist(schema, propName)) {
|
||||||
val item = getLocalizedText(schema, propName, GENERIC_LANG, SPECIFIC_LANG)
|
val item = getLocalizedText(schema, propName, GENERIC_LANG, SPECIFIC_LANG)
|
||||||
|
@ -209,7 +224,9 @@ object XMP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.getSafeDateMillis(schema: String, propName: String, save: (value: Long) -> Unit) {
|
fun XMPMeta.getSafeDateMillis(prop: XMPPropName, save: (value: Long) -> Unit) {
|
||||||
|
val schema = prop.nsUri
|
||||||
|
val propName = prop.toString()
|
||||||
try {
|
try {
|
||||||
if (doesPropertyExist(schema, propName)) {
|
if (doesPropertyExist(schema, propName)) {
|
||||||
val item = getPropertyDate(schema, propName)
|
val item = getPropertyDate(schema, propName)
|
||||||
|
@ -226,20 +243,38 @@ object XMP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// e.g. 'Container:Directory[42]/Container:Item/Item:Mime'
|
// e.g. path 'Container:Directory[42]/Container:Item/Item:Mime' matches:
|
||||||
fun XMPMeta.getSafeStructField(path: String): XMPProperty? {
|
// - structNs: "http://ns.google.com/photos/1.0/container/"
|
||||||
val separator = path.lastIndexOf("/")
|
// - structName: "Container:Directory[42]/Container:Item"
|
||||||
if (separator != -1) {
|
// - fieldNs: "http://ns.google.com/photos/1.0/container/item/"
|
||||||
val structName = path.substring(0, separator)
|
// - fieldName: "Item:Mime"
|
||||||
val structNs = namespaceForPropPath(structName)
|
fun XMPMeta.getSafeStructField(props: List<Any>): XMPProperty? {
|
||||||
val fieldName = path.substring(separator + 1)
|
if (props.size >= 2) {
|
||||||
val fieldNs = namespaceForPropPath(fieldName)
|
val structFirst = props.first()
|
||||||
|
val field = props.last()
|
||||||
|
if (structFirst is XMPPropName && field is XMPPropName) {
|
||||||
|
val structName = props.take(props.size - 1).mapIndexed { index, prop ->
|
||||||
|
when (prop) {
|
||||||
|
is XMPPropName -> "${if (index == 0) "" else "/"}$prop"
|
||||||
|
is Int -> "[$prop]"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.filterNotNull().joinToString("")
|
||||||
|
val fieldName = field.toString()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return getStructField(structNs, structName, fieldNs, fieldName)
|
return getStructField(structFirst.nsUri, structName, field.nsUri, fieldName)
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
Log.w(LOG_TAG, "failed to get XMP struct field for path=$path", e)
|
Log.w(LOG_TAG, "failed to get XMP struct field for props=$props", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class XMPPropName(val nsUri: String, private val prop: String) {
|
||||||
|
private fun resolve(): String = "${XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(nsUri)}$prop"
|
||||||
|
|
||||||
|
override fun toString(): String = resolve()
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ abstract class EmbeddedDataService {
|
||||||
|
|
||||||
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
|
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
|
||||||
|
|
||||||
Future<Map> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType);
|
Future<Map> extractXmpDataProp(AvesEntry entry, List<dynamic>? props, String? propMimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformEmbeddedDataService implements EmbeddedDataService {
|
class PlatformEmbeddedDataService implements EmbeddedDataService {
|
||||||
|
@ -61,14 +61,14 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async {
|
Future<Map> extractXmpDataProp(AvesEntry entry, List<dynamic>? props, String? propMimeType) async {
|
||||||
try {
|
try {
|
||||||
final result = await _platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
|
final result = await _platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
'sizeBytes': entry.sizeBytes,
|
'sizeBytes': entry.sizeBytes,
|
||||||
'displayName': '${entry.bestTitle} • $propPath',
|
'displayName': '${entry.bestTitle} • $props',
|
||||||
'propPath': propPath,
|
'propPath': props,
|
||||||
'propMimeType': propMimeType,
|
'propMimeType': propMimeType,
|
||||||
});
|
});
|
||||||
if (result != null) return result as Map;
|
if (result != null) return result as Map;
|
||||||
|
|
|
@ -2,14 +2,127 @@ import 'package:intl/intl.dart';
|
||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
class Namespaces {
|
class Namespaces {
|
||||||
|
static const acdsee = 'http://ns.acdsee.com/iptc/1.0/';
|
||||||
|
static const adsmlat = 'http://adsml.org/xmlns/';
|
||||||
|
static const avm = 'http://www.communicatingastronomy.org/avm/1.0/';
|
||||||
|
static const cc = 'http://creativecommons.org/ns#';
|
||||||
static const container = 'http://ns.google.com/photos/1.0/container/';
|
static const container = 'http://ns.google.com/photos/1.0/container/';
|
||||||
|
static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/';
|
||||||
|
static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/';
|
||||||
|
static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/';
|
||||||
|
static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/';
|
||||||
|
static const darktable = 'http://darktable.sf.net/';
|
||||||
static const dc = 'http://purl.org/dc/elements/1.1/';
|
static const dc = 'http://purl.org/dc/elements/1.1/';
|
||||||
|
static const dcterms = 'http://purl.org/dc/terms/';
|
||||||
|
static const dicom = 'http://ns.adobe.com/DICOM/';
|
||||||
|
static const digiKam = 'http://www.digikam.org/ns/1.0/';
|
||||||
|
static const droneDji = 'http://www.dji.com/drone-dji/1.0/';
|
||||||
|
static const dwc = 'http://rs.tdwg.org/dwc/index.htm';
|
||||||
|
static const dwciri = 'http://rs.tdwg.org/dwc/iri/';
|
||||||
|
static const exif = 'http://ns.adobe.com/exif/1.0/';
|
||||||
|
static const exifAux = 'http://ns.adobe.com/exif/1.0/aux/';
|
||||||
|
static const exifEx = 'http://cipa.jp/exif/1.0/';
|
||||||
|
static const gAudio = 'http://ns.google.com/photos/1.0/audio/';
|
||||||
static const gCamera = 'http://ns.google.com/photos/1.0/camera/';
|
static const gCamera = 'http://ns.google.com/photos/1.0/camera/';
|
||||||
|
static const gCreations = 'http://ns.google.com/photos/1.0/creations/';
|
||||||
|
static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/';
|
||||||
|
static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/';
|
||||||
|
static const gFocus = 'http://ns.google.com/photos/1.0/focus/';
|
||||||
|
static const gImage = 'http://ns.google.com/photos/1.0/image/';
|
||||||
|
static const gimp = 'http://www.gimp.org/ns/2.10/';
|
||||||
|
static const gPano = 'http://ns.google.com/photos/1.0/panorama/';
|
||||||
|
static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/';
|
||||||
|
static const illustrator = 'http://ns.adobe.com/illustrator/1.0/';
|
||||||
|
static const iptc4xmpCore = 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/';
|
||||||
|
static const iptc4xmpExt = 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/';
|
||||||
|
static const lr = 'http://ns.adobe.com/lightroom/1.0/';
|
||||||
|
static const mediapro = 'http://ns.iview-multimedia.com/mediapro/1.0/';
|
||||||
|
|
||||||
|
// also seen in the wild for prefix `MicrosoftPhoto`: 'http://ns.microsoft.com/photo/1.0'
|
||||||
static const microsoftPhoto = 'http://ns.microsoft.com/photo/1.0/';
|
static const microsoftPhoto = 'http://ns.microsoft.com/photo/1.0/';
|
||||||
|
static const mp1 = 'http://ns.microsoft.com/photo/1.1';
|
||||||
|
static const mp = 'http://ns.microsoft.com/photo/1.2/';
|
||||||
|
static const mpri = 'http://ns.microsoft.com/photo/1.2/t/RegionInfo#';
|
||||||
|
static const mpreg = 'http://ns.microsoft.com/photo/1.2/t/Region#';
|
||||||
|
static const mwgrs = 'http://www.metadataworkinggroup.com/schemas/regions/';
|
||||||
|
static const nga = 'https://standards.nga.gov/metadata/media/image/artobject/1.0';
|
||||||
|
static const panorama = 'http://ns.adobe.com/photoshop/1.0/panorama-profile';
|
||||||
|
static const panoStudio = 'http://www.tshsoft.com/xmlns';
|
||||||
|
static const pdf = 'http://ns.adobe.com/pdf/1.3/';
|
||||||
|
static const pdfX = 'http://ns.adobe.com/pdfx/1.3/';
|
||||||
|
static const photoMechanic = 'http://ns.camerabits.com/photomechanic/1.0/';
|
||||||
|
static const photoshop = 'http://ns.adobe.com/photoshop/1.0/';
|
||||||
|
static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/';
|
||||||
|
static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01';
|
||||||
static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||||
|
static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#';
|
||||||
|
static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#';
|
||||||
|
static const tiff = 'http://ns.adobe.com/tiff/1.0/';
|
||||||
static const x = 'adobe:ns:meta/';
|
static const x = 'adobe:ns:meta/';
|
||||||
static const xmp = 'http://ns.adobe.com/xap/1.0/';
|
static const xmp = 'http://ns.adobe.com/xap/1.0/';
|
||||||
|
static const xmpBJ = 'http://ns.adobe.com/xap/1.0/bj/';
|
||||||
|
static const xmpDM = 'http://ns.adobe.com/xmp/1.0/DynamicMedia/';
|
||||||
|
static const xmpGImg = 'http://ns.adobe.com/xap/1.0/g/img/';
|
||||||
|
static const xmpMM = 'http://ns.adobe.com/xap/1.0/mm/';
|
||||||
static const xmpNote = 'http://ns.adobe.com/xmp/note/';
|
static const xmpNote = 'http://ns.adobe.com/xmp/note/';
|
||||||
|
static const xmpRights = 'http://ns.adobe.com/xap/1.0/rights/';
|
||||||
|
static const xmpTPg = 'http://ns.adobe.com/xap/1.0/t/pg/';
|
||||||
|
|
||||||
|
// cf https://exiftool.org/TagNames/XMP.html
|
||||||
|
static const Map<String, String> nsTitles = {
|
||||||
|
acdsee: 'ACDSee',
|
||||||
|
adsmlat: 'AdsML',
|
||||||
|
exifAux: 'Exif Aux',
|
||||||
|
avm: 'Astronomy Visualization',
|
||||||
|
cc: 'Creative Commons',
|
||||||
|
container: 'Container',
|
||||||
|
crd: 'Camera Raw Defaults',
|
||||||
|
creatorAtom: 'After Effects',
|
||||||
|
crs: 'Camera Raw Settings',
|
||||||
|
crss: 'Camera Raw Saved Settings',
|
||||||
|
darktable: 'darktable',
|
||||||
|
dc: 'Dublin Core',
|
||||||
|
digiKam: 'digiKam',
|
||||||
|
droneDji: 'DJI Drone',
|
||||||
|
dwc: 'Darwin Core',
|
||||||
|
exif: 'Exif',
|
||||||
|
exifEx: 'Exif Ex',
|
||||||
|
gettyImagesGift: 'Getty Images',
|
||||||
|
gAudio: 'Google Audio',
|
||||||
|
gCamera: 'Google Camera',
|
||||||
|
gCreations: 'Google Creations',
|
||||||
|
gDepth: 'Google Depth',
|
||||||
|
gFocus: 'Google Focus',
|
||||||
|
gImage: 'Google Image',
|
||||||
|
gimp: 'GIMP',
|
||||||
|
gPano: 'Google Panorama',
|
||||||
|
gSpherical: 'Google Spherical',
|
||||||
|
illustrator: 'Illustrator',
|
||||||
|
iptc4xmpCore: 'IPTC Core',
|
||||||
|
iptc4xmpExt: 'IPTC Extension',
|
||||||
|
lr: 'Lightroom',
|
||||||
|
mediapro: 'MediaPro',
|
||||||
|
microsoftPhoto: 'Microsoft Photo 1.0',
|
||||||
|
mp1: 'Microsoft Photo 1.1',
|
||||||
|
mp: 'Microsoft Photo 1.2',
|
||||||
|
mwgrs: 'Regions',
|
||||||
|
nga: 'National Gallery of Art',
|
||||||
|
panorama: 'Panorama',
|
||||||
|
panoStudio: 'PanoramaStudio',
|
||||||
|
pdf: 'PDF',
|
||||||
|
pdfX: 'PDF/X',
|
||||||
|
photoMechanic: 'Photo Mechanic',
|
||||||
|
photoshop: 'Photoshop',
|
||||||
|
plus: 'PLUS',
|
||||||
|
pmtm: 'Photomatix',
|
||||||
|
tiff: 'TIFF',
|
||||||
|
xmp: 'Basic',
|
||||||
|
xmpBJ: 'Basic Job Ticket',
|
||||||
|
xmpDM: 'Dynamic Media',
|
||||||
|
xmpMM: 'Media Management',
|
||||||
|
xmpRights: 'Rights Management',
|
||||||
|
xmpTPg: 'Paged-Text',
|
||||||
|
};
|
||||||
|
|
||||||
static final defaultPrefixes = {
|
static final defaultPrefixes = {
|
||||||
container: 'Container',
|
container: 'Container',
|
||||||
|
@ -19,6 +132,7 @@ class Namespaces {
|
||||||
rdf: 'rdf',
|
rdf: 'rdf',
|
||||||
x: 'x',
|
x: 'x',
|
||||||
xmp: 'xmp',
|
xmp: 'xmp',
|
||||||
|
xmpGImg: 'xmpGImg',
|
||||||
xmpNote: 'xmpNote',
|
xmpNote: 'xmpNote',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
|
||||||
fields = await embeddedDataService.extractVideoEmbeddedPicture(entry);
|
fields = await embeddedDataService.extractVideoEmbeddedPicture(entry);
|
||||||
break;
|
break;
|
||||||
case EmbeddedDataSource.xmp:
|
case EmbeddedDataSource.xmp:
|
||||||
fields = await embeddedDataService.extractXmpDataProp(entry, notification.propPath, notification.mimeType);
|
fields = await embeddedDataService.extractXmpDataProp(entry, notification.props, notification.mimeType);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) {
|
if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) {
|
||||||
|
|
|
@ -6,12 +6,12 @@ enum EmbeddedDataSource { motionPhotoVideo, videoCover, xmp }
|
||||||
@immutable
|
@immutable
|
||||||
class OpenEmbeddedDataNotification extends Notification {
|
class OpenEmbeddedDataNotification extends Notification {
|
||||||
final EmbeddedDataSource source;
|
final EmbeddedDataSource source;
|
||||||
final String? propPath;
|
final List<dynamic>? props;
|
||||||
final String? mimeType;
|
final String? mimeType;
|
||||||
|
|
||||||
const OpenEmbeddedDataNotification._private({
|
const OpenEmbeddedDataNotification._private({
|
||||||
required this.source,
|
required this.source,
|
||||||
this.propPath,
|
this.props,
|
||||||
this.mimeType,
|
this.mimeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,15 +24,15 @@ class OpenEmbeddedDataNotification extends Notification {
|
||||||
);
|
);
|
||||||
|
|
||||||
factory OpenEmbeddedDataNotification.xmp({
|
factory OpenEmbeddedDataNotification.xmp({
|
||||||
required String propPath,
|
required List<dynamic> props,
|
||||||
required String mimeType,
|
required String mimeType,
|
||||||
}) =>
|
}) =>
|
||||||
OpenEmbeddedDataNotification._private(
|
OpenEmbeddedDataNotification._private(
|
||||||
source: EmbeddedDataSource.xmp,
|
source: EmbeddedDataSource.xmp,
|
||||||
propPath: propPath,
|
props: props,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{source=$source, propPath=$propPath, mimeType=$mimeType}';
|
String toString() => '$runtimeType#${shortHash(this)}{source=$source, props=$props, mimeType=$mimeType}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,13 +38,14 @@ class MetadataDirTile extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var tags = dir.tags;
|
var tags = dir.tags;
|
||||||
if (tags.isEmpty) return const SizedBox.shrink();
|
if (tags.isEmpty) return const SizedBox();
|
||||||
|
|
||||||
final dirName = dir.name;
|
final dirName = dir.name;
|
||||||
if (dirName == MetadataDirectory.xmpDirectory) {
|
if (dirName == MetadataDirectory.xmpDirectory) {
|
||||||
return XmpDirTile(
|
return XmpDirTile(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
title: title,
|
title: title,
|
||||||
|
allTags: dir.allTags,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
expandedNotifier: expandedDirectoryNotifier,
|
expandedNotifier: expandedDirectoryNotifier,
|
||||||
initiallyExpanded: initiallyExpanded,
|
initiallyExpanded: initiallyExpanded,
|
||||||
|
|
|
@ -26,110 +26,58 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class XmpNamespace extends Equatable {
|
class XmpNamespace extends Equatable {
|
||||||
final String namespace;
|
final String nsUri, nsPrefix;
|
||||||
final Map<String, String> rawProps;
|
final Map<String, String> rawProps;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [namespace];
|
List<Object?> get props => [nsUri, nsPrefix];
|
||||||
|
|
||||||
const XmpNamespace(this.namespace, this.rawProps);
|
const XmpNamespace(this.nsUri, this.nsPrefix, this.rawProps);
|
||||||
|
|
||||||
factory XmpNamespace.create(String namespace, Map<String, String> rawProps) {
|
factory XmpNamespace.create(String nsUri, String nsPrefix, Map<String, String> rawProps) {
|
||||||
switch (namespace) {
|
switch (nsUri) {
|
||||||
case XmpBasicNamespace.ns:
|
case Namespaces.container:
|
||||||
return XmpBasicNamespace(rawProps);
|
return XmpContainer(nsPrefix, rawProps);
|
||||||
case XmpContainer.ns:
|
case Namespaces.crs:
|
||||||
return XmpContainer(rawProps);
|
return XmpCrsNamespace(nsPrefix, rawProps);
|
||||||
case XmpCrsNamespace.ns:
|
case Namespaces.darktable:
|
||||||
return XmpCrsNamespace(rawProps);
|
return XmpDarktableNamespace(nsPrefix, rawProps);
|
||||||
case XmpDarktableNamespace.ns:
|
case Namespaces.dwc:
|
||||||
return XmpDarktableNamespace(rawProps);
|
return XmpDwcNamespace(nsPrefix, rawProps);
|
||||||
case XmpDwcNamespace.ns:
|
case Namespaces.exif:
|
||||||
return XmpDwcNamespace(rawProps);
|
return XmpExifNamespace(nsPrefix, rawProps);
|
||||||
case XmpExifNamespace.ns:
|
case Namespaces.gAudio:
|
||||||
return XmpExifNamespace(rawProps);
|
return XmpGAudioNamespace(nsPrefix, rawProps);
|
||||||
case XmpGAudioNamespace.ns:
|
case Namespaces.gDepth:
|
||||||
return XmpGAudioNamespace(rawProps);
|
return XmpGDepthNamespace(nsPrefix, rawProps);
|
||||||
case XmpGDepthNamespace.ns:
|
case Namespaces.gImage:
|
||||||
return XmpGDepthNamespace(rawProps);
|
return XmpGImageNamespace(nsPrefix, rawProps);
|
||||||
case XmpGImageNamespace.ns:
|
case Namespaces.iptc4xmpCore:
|
||||||
return XmpGImageNamespace(rawProps);
|
return XmpIptcCoreNamespace(nsPrefix, rawProps);
|
||||||
case XmpIptcCoreNamespace.ns:
|
case Namespaces.iptc4xmpExt:
|
||||||
return XmpIptcCoreNamespace(rawProps);
|
return XmpIptc4xmpExtNamespace(nsPrefix, rawProps);
|
||||||
case XmpIptc4xmpExtNamespace.ns:
|
case Namespaces.mwgrs:
|
||||||
return XmpIptc4xmpExtNamespace(rawProps);
|
return XmpMgwRegionsNamespace(nsPrefix, rawProps);
|
||||||
case XmpMgwRegionsNamespace.ns:
|
case Namespaces.mp:
|
||||||
return XmpMgwRegionsNamespace(rawProps);
|
return XmpMPNamespace(nsPrefix, rawProps);
|
||||||
case XmpMMNamespace.ns:
|
case Namespaces.photoshop:
|
||||||
return XmpMMNamespace(rawProps);
|
return XmpPhotoshopNamespace(nsPrefix, rawProps);
|
||||||
case XmpMPNamespace.ns:
|
case Namespaces.plus:
|
||||||
return XmpMPNamespace(rawProps);
|
return XmpPlusNamespace(nsPrefix, rawProps);
|
||||||
case XmpNoteNamespace.ns:
|
case Namespaces.tiff:
|
||||||
return XmpNoteNamespace(rawProps);
|
return XmpTiffNamespace(nsPrefix, rawProps);
|
||||||
case XmpPhotoshopNamespace.ns:
|
case Namespaces.xmp:
|
||||||
return XmpPhotoshopNamespace(rawProps);
|
return XmpBasicNamespace(nsPrefix, rawProps);
|
||||||
case XmpPlusNamespace.ns:
|
case Namespaces.xmpMM:
|
||||||
return XmpPlusNamespace(rawProps);
|
return XmpMMNamespace(nsPrefix, rawProps);
|
||||||
case XmpTiffNamespace.ns:
|
case Namespaces.xmpNote:
|
||||||
return XmpTiffNamespace(rawProps);
|
return XmpNoteNamespace(nsPrefix, rawProps);
|
||||||
default:
|
default:
|
||||||
return XmpNamespace(namespace, rawProps);
|
return XmpNamespace(nsUri, nsPrefix, rawProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cf https://exiftool.org/TagNames/XMP.html
|
String get displayTitle => Namespaces.nsTitles[nsUri] ?? '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)';
|
||||||
static const Map<String, String> nsTitles = {
|
|
||||||
'acdsee': 'ACDSee',
|
|
||||||
'adsml-at': 'AdsML',
|
|
||||||
'aux': 'Exif Aux',
|
|
||||||
'avm': 'Astronomy Visualization',
|
|
||||||
'Camera': 'Camera',
|
|
||||||
'cc': 'Creative Commons',
|
|
||||||
'crd': 'Camera Raw Defaults',
|
|
||||||
'creatorAtom': 'After Effects',
|
|
||||||
'crs': 'Camera Raw Settings',
|
|
||||||
'dc': 'Dublin Core',
|
|
||||||
'drone-dji': 'DJI Drone',
|
|
||||||
'dwc': 'Darwin Core',
|
|
||||||
'exif': 'Exif',
|
|
||||||
'exifEX': 'Exif Ex',
|
|
||||||
'GettyImagesGIFT': 'Getty Images',
|
|
||||||
'GAudio': 'Google Audio',
|
|
||||||
'GDepth': 'Google Depth',
|
|
||||||
'GImage': 'Google Image',
|
|
||||||
'GIMP': 'GIMP',
|
|
||||||
'GCamera': 'Google Camera',
|
|
||||||
'GCreations': 'Google Creations',
|
|
||||||
'GFocus': 'Google Focus',
|
|
||||||
'GPano': 'Google Panorama',
|
|
||||||
'illustrator': 'Illustrator',
|
|
||||||
'Iptc4xmpCore': 'IPTC Core',
|
|
||||||
'Iptc4xmpExt': 'IPTC Extension',
|
|
||||||
'lr': 'Lightroom',
|
|
||||||
'mediapro': 'MediaPro',
|
|
||||||
'MicrosoftPhoto': 'Microsoft Photo 1.0',
|
|
||||||
'MP1': 'Microsoft Photo 1.1',
|
|
||||||
'MP': 'Microsoft Photo 1.2',
|
|
||||||
'mwg-rs': 'Regions',
|
|
||||||
'nga': 'National Gallery of Art',
|
|
||||||
'panorama': 'Panorama',
|
|
||||||
'PanoStudioXMP': 'PanoramaStudio',
|
|
||||||
'pdf': 'PDF',
|
|
||||||
'pdfx': 'PDF/X',
|
|
||||||
'photomechanic': 'Photo Mechanic',
|
|
||||||
'photoshop': 'Photoshop',
|
|
||||||
'plus': 'PLUS',
|
|
||||||
'pmtm': 'Photomatix',
|
|
||||||
'tiff': 'TIFF',
|
|
||||||
'xmp': 'Basic',
|
|
||||||
'xmpBJ': 'Basic Job Ticket',
|
|
||||||
'xmpDM': 'Dynamic Media',
|
|
||||||
'xmpMM': 'Media Management',
|
|
||||||
'xmpRights': 'Rights Management',
|
|
||||||
'xmpTPg': 'Paged-Text',
|
|
||||||
};
|
|
||||||
|
|
||||||
String get displayTitle => nsTitles[namespace] ?? namespace;
|
|
||||||
|
|
||||||
Map<String, String> get buildProps => rawProps;
|
Map<String, String> get buildProps => rawProps;
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class XmpCrsNamespace extends XmpNamespace {
|
class XmpCrsNamespace extends XmpNamespace {
|
||||||
static const ns = 'crs';
|
late final cgbcPattern = RegExp(nsPrefix + r'CircularGradientBasedCorrections\[(\d+)\]/(.*)');
|
||||||
|
late final gbcPattern = RegExp(nsPrefix + r'GradientBasedCorrections\[(\d+)\]/(.*)');
|
||||||
static final cgbcPattern = RegExp(ns + r':CircularGradientBasedCorrections\[(\d+)\]/(.*)');
|
late final mgbcPattern = RegExp(nsPrefix + r'MaskGroupBasedCorrections\[(\d+)\]/(.*)');
|
||||||
static final gbcPattern = RegExp(ns + r':GradientBasedCorrections\[(\d+)\]/(.*)');
|
late final pbcPattern = RegExp(nsPrefix + r'PaintBasedCorrections\[(\d+)\]/(.*)');
|
||||||
static final mgbcPattern = RegExp(ns + r':MaskGroupBasedCorrections\[(\d+)\]/(.*)');
|
late final retouchAreasPattern = RegExp(nsPrefix + r'RetouchAreas\[(\d+)\]/(.*)');
|
||||||
static final pbcPattern = RegExp(ns + r':PaintBasedCorrections\[(\d+)\]/(.*)');
|
late final lookPattern = RegExp(nsPrefix + r'Look/(.*)');
|
||||||
static final retouchAreasPattern = RegExp(ns + r':RetouchAreas\[(\d+)\]/(.*)');
|
late final rmmiPattern = RegExp(nsPrefix + r'RangeMaskMapInfo/' + nsPrefix + r'RangeMaskMapInfo/(.*)');
|
||||||
static final lookPattern = RegExp(ns + r':Look/(.*)');
|
|
||||||
static final rmmiPattern = RegExp(ns + r':RangeMaskMapInfo/' + ns + r':RangeMaskMapInfo/(.*)');
|
|
||||||
|
|
||||||
final cgbc = <int, Map<String, String>>{};
|
final cgbc = <int, Map<String, String>>{};
|
||||||
final gbc = <int, Map<String, String>>{};
|
final gbc = <int, Map<String, String>>{};
|
||||||
|
@ -21,7 +20,7 @@ class XmpCrsNamespace extends XmpNamespace {
|
||||||
final look = <String, String>{};
|
final look = <String, String>{};
|
||||||
final rmmi = <String, String>{};
|
final rmmi = <String, String>{};
|
||||||
|
|
||||||
XmpCrsNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpCrsNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.crs, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
bool extractData(XmpProp prop) {
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class XmpDarktableNamespace extends XmpNamespace {
|
class XmpDarktableNamespace extends XmpNamespace {
|
||||||
static const ns = 'darktable';
|
late final historyPattern = RegExp(nsPrefix + r'history\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final historyPattern = RegExp(ns + r':history\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final history = <int, Map<String, String>>{};
|
final history = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpDarktableNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpDarktableNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.darktable, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, historyPattern, history);
|
bool extractData(XmpProp prop) => extractIndexedStruct(prop, historyPattern, history);
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class XmpDwcNamespace extends XmpNamespace {
|
class XmpDwcNamespace extends XmpNamespace {
|
||||||
static const ns = 'dwc';
|
late final dcTermsLocationPattern = RegExp(nsPrefix + r'dctermsLocation/(.*)');
|
||||||
|
late final eventPattern = RegExp(nsPrefix + r'Event/(.*)');
|
||||||
static final dcTermsLocationPattern = RegExp(ns + r':dctermsLocation/(.*)');
|
late final geologicalContextPattern = RegExp(nsPrefix + r'GeologicalContext/(.*)');
|
||||||
static final eventPattern = RegExp(ns + r':Event/(.*)');
|
late final identificationPattern = RegExp(nsPrefix + r'Identification/(.*)');
|
||||||
static final geologicalContextPattern = RegExp(ns + r':GeologicalContext/(.*)');
|
late final measurementOrFactPattern = RegExp(nsPrefix + r'MeasurementOrFact/(.*)');
|
||||||
static final identificationPattern = RegExp(ns + r':Identification/(.*)');
|
late final occurrencePattern = RegExp(nsPrefix + r'Occurrence/(.*)');
|
||||||
static final measurementOrFactPattern = RegExp(ns + r':MeasurementOrFact/(.*)');
|
late final recordPattern = RegExp(nsPrefix + r'Record/(.*)');
|
||||||
static final occurrencePattern = RegExp(ns + r':Occurrence/(.*)');
|
late final resourceRelationshipPattern = RegExp(nsPrefix + r'ResourceRelationship/(.*)');
|
||||||
static final recordPattern = RegExp(ns + r':Record/(.*)');
|
late final taxonPattern = RegExp(nsPrefix + r'Taxon/(.*)');
|
||||||
static final resourceRelationshipPattern = RegExp(ns + r':ResourceRelationship/(.*)');
|
|
||||||
static final taxonPattern = RegExp(ns + r':Taxon/(.*)');
|
|
||||||
|
|
||||||
final dcTermsLocation = <String, String>{};
|
final dcTermsLocation = <String, String>{};
|
||||||
final event = <String, String>{};
|
final event = <String, String>{};
|
||||||
|
@ -25,7 +24,7 @@ class XmpDwcNamespace extends XmpNamespace {
|
||||||
final resourceRelationship = <String, String>{};
|
final resourceRelationship = <String, String>{};
|
||||||
final taxon = <String, String>{};
|
final taxon = <String, String>{};
|
||||||
|
|
||||||
XmpDwcNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpDwcNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.dwc, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
bool extractData(XmpProp prop) {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import 'package:aves/ref/exif.dart';
|
import 'package:aves/ref/exif.dart';
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
|
|
||||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md
|
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md
|
||||||
class XmpExifNamespace extends XmpNamespace {
|
class XmpExifNamespace extends XmpNamespace {
|
||||||
static const ns = 'exif';
|
const XmpExifNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.exif, nsPrefix, rawProps);
|
||||||
|
|
||||||
const XmpExifNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String formatValue(XmpProp prop) {
|
String formatValue(XmpProp prop) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
|
@ -8,7 +9,7 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
abstract class XmpGoogleNamespace extends XmpNamespace {
|
abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||||
const XmpGoogleNamespace(String ns, Map<String, String> rawProps) : super(ns, rawProps);
|
const XmpGoogleNamespace(String nsUri, String nsPrefix, Map<String, String> rawProps) : super(nsUri, nsPrefix, rawProps);
|
||||||
|
|
||||||
List<Tuple2<String, String>> get dataProps;
|
List<Tuple2<String, String>> get dataProps;
|
||||||
|
|
||||||
|
@ -24,10 +25,29 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||||
dataProp.displayKey,
|
dataProp.displayKey,
|
||||||
InfoRowGroup.linkSpanBuilder(
|
InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
onTap: (context) {
|
||||||
propPath: dataProp.path,
|
final pattern = RegExp(r'(.+):(.+)([(\d)])?');
|
||||||
|
final props = dataProp.path.split('/').expand((part) {
|
||||||
|
var match = pattern.firstMatch(part);
|
||||||
|
if (match == null) return [];
|
||||||
|
|
||||||
|
// ignore namespace prefix
|
||||||
|
final propName = match.group(2);
|
||||||
|
final prop = [nsUri, propName];
|
||||||
|
|
||||||
|
final indexString = match.groupCount >= 4 ? match.group(4) : null;
|
||||||
|
final index = indexString != null ? int.tryParse(indexString) : null;
|
||||||
|
if (index != null) {
|
||||||
|
return [prop, index];
|
||||||
|
} else {
|
||||||
|
return [prop];
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
return OpenEmbeddedDataNotification.xmp(
|
||||||
|
props: props,
|
||||||
mimeType: mimeProp.value,
|
mimeType: mimeProp.value,
|
||||||
).dispatch(context),
|
).dispatch(context);
|
||||||
|
},
|
||||||
))
|
))
|
||||||
: null;
|
: null;
|
||||||
}).whereNotNull());
|
}).whereNotNull());
|
||||||
|
@ -35,43 +55,35 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpGAudioNamespace extends XmpGoogleNamespace {
|
class XmpGAudioNamespace extends XmpGoogleNamespace {
|
||||||
static const ns = 'GAudio';
|
const XmpGAudioNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gAudio, nsPrefix, rawProps);
|
||||||
|
|
||||||
const XmpGAudioNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Tuple2<String, String>> get dataProps => const [Tuple2('$ns:Data', '$ns:Mime')];
|
List<Tuple2<String, String>> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')];
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpGDepthNamespace extends XmpGoogleNamespace {
|
class XmpGDepthNamespace extends XmpGoogleNamespace {
|
||||||
static const ns = 'GDepth';
|
const XmpGDepthNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gDepth, nsPrefix, rawProps);
|
||||||
|
|
||||||
const XmpGDepthNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Tuple2<String, String>> get dataProps => const [
|
List<Tuple2<String, String>> get dataProps => [
|
||||||
Tuple2('$ns:Data', '$ns:Mime'),
|
Tuple2('${nsPrefix}Data', '${nsPrefix}Mime'),
|
||||||
Tuple2('$ns:Confidence', '$ns:ConfidenceMime'),
|
Tuple2('${nsPrefix}Confidence', '${nsPrefix}ConfidenceMime'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpGImageNamespace extends XmpGoogleNamespace {
|
class XmpGImageNamespace extends XmpGoogleNamespace {
|
||||||
static const ns = 'GImage';
|
const XmpGImageNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gImage, nsPrefix, rawProps);
|
||||||
|
|
||||||
const XmpGImageNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Tuple2<String, String>> get dataProps => const [Tuple2('$ns:Data', '$ns:Mime')];
|
List<Tuple2<String, String>> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')];
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpContainer extends XmpNamespace {
|
class XmpContainer extends XmpNamespace {
|
||||||
static const ns = 'Container';
|
late final directoryPattern = RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)');
|
||||||
|
|
||||||
static final directoryPattern = RegExp('$ns:Directory\\[(\\d+)\\]/$ns:Item/(.*)');
|
|
||||||
|
|
||||||
final directories = <int, Map<String, String>>{};
|
final directories = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpContainer(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpContainer(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.container, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, directoryPattern, directories);
|
bool extractData(XmpProp prop) => extractIndexedStruct(prop, directoryPattern, directories);
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class XmpIptcCoreNamespace extends XmpNamespace {
|
class XmpIptcCoreNamespace extends XmpNamespace {
|
||||||
static const ns = 'Iptc4xmpCore';
|
late final creatorContactInfoPattern = RegExp(nsPrefix + r'CreatorContactInfo/(.*)');
|
||||||
|
|
||||||
static final creatorContactInfoPattern = RegExp(ns + r':CreatorContactInfo/(.*)');
|
|
||||||
|
|
||||||
final creatorContactInfo = <String, String>{};
|
final creatorContactInfo = <String, String>{};
|
||||||
|
|
||||||
XmpIptcCoreNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpIptcCoreNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractStruct(prop, creatorContactInfoPattern, creatorContactInfo);
|
bool extractData(XmpProp prop) => extractStruct(prop, creatorContactInfoPattern, creatorContactInfo);
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class XmpIptc4xmpExtNamespace extends XmpNamespace {
|
class XmpIptc4xmpExtNamespace extends XmpNamespace {
|
||||||
static const ns = 'Iptc4xmpExt';
|
late final aooPattern = RegExp(nsPrefix + r'ArtworkOrObject\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final aooPattern = RegExp(ns + r':ArtworkOrObject\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final aoo = <int, Map<String, String>>{};
|
final aoo = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpIptc4xmpExtNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpIptc4xmpExtNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, aooPattern, aoo);
|
bool extractData(XmpProp prop) => extractIndexedStruct(prop, aooPattern, aoo);
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class XmpMPNamespace extends XmpNamespace {
|
class XmpMPNamespace extends XmpNamespace {
|
||||||
static const ns = 'MP';
|
late final regionListPattern = RegExp(nsPrefix + r'RegionInfo/MPRI:Regions\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final regionListPattern = RegExp(ns + r':RegionInfo/MPRI:Regions\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final regionList = <int, Map<String, String>>{};
|
final regionList = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpMPNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpMPNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mp, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, regionListPattern, regionList);
|
bool extractData(XmpProp prop) => extractIndexedStruct(prop, regionListPattern, regionList);
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15)
|
// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15)
|
||||||
class XmpMgwRegionsNamespace extends XmpNamespace {
|
class XmpMgwRegionsNamespace extends XmpNamespace {
|
||||||
static const ns = 'mwg-rs';
|
late final dimensionsPattern = RegExp(nsPrefix + r'Regions/mwg-rs:AppliedToDimensions/(.*)');
|
||||||
|
late final regionListPattern = RegExp(nsPrefix + r'Regions/mwg-rs:RegionList\[(\d+)\]/(.*)');
|
||||||
static final dimensionsPattern = RegExp(ns + r':Regions/mwg-rs:AppliedToDimensions/(.*)');
|
|
||||||
static final regionListPattern = RegExp(ns + r':Regions/mwg-rs:RegionList\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final dimensions = <String, String>{};
|
final dimensions = <String, String>{};
|
||||||
final regionList = <int, Map<String, String>>{};
|
final regionList = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpMgwRegionsNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpMgwRegionsNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
bool extractData(XmpProp prop) {
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
||||||
class XmpPhotoshopNamespace extends XmpNamespace {
|
class XmpPhotoshopNamespace extends XmpNamespace {
|
||||||
static const ns = 'photoshop';
|
late final textLayersPattern = RegExp(nsPrefix + r'TextLayers\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final textLayersPattern = RegExp(ns + r':TextLayers\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final textLayers = <int, Map<String, String>>{};
|
final textLayers = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpPhotoshopNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpPhotoshopNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.photoshop, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
bool extractData(XmpProp prop) {
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class XmpPlusNamespace extends XmpNamespace {
|
class XmpPlusNamespace extends XmpNamespace {
|
||||||
static const ns = 'plus';
|
late final licensorPattern = RegExp(nsPrefix + r'Licensor\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final licensorPattern = RegExp(ns + r':Licensor\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final licensor = <int, Map<String, String>>{};
|
final licensor = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpPlusNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpPlusNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.plus, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, licensorPattern, licensor);
|
bool extractData(XmpProp prop) => extractIndexedStruct(prop, licensorPattern, licensor);
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import 'package:aves/ref/exif.dart';
|
import 'package:aves/ref/exif.dart';
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||||
|
|
||||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md
|
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md
|
||||||
class XmpTiffNamespace extends XmpNamespace {
|
class XmpTiffNamespace extends XmpNamespace {
|
||||||
static const ns = 'tiff';
|
const XmpTiffNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.tiff, nsPrefix, rawProps);
|
||||||
|
|
||||||
const XmpTiffNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String formatValue(XmpProp prop) {
|
String formatValue(XmpProp prop) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
import 'package:aves/widgets/viewer/embedded/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
|
@ -7,14 +8,12 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class XmpBasicNamespace extends XmpNamespace {
|
class XmpBasicNamespace extends XmpNamespace {
|
||||||
static const ns = 'xmp';
|
late final thumbnailsPattern = RegExp(nsPrefix + r'Thumbnails\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final thumbnailsPattern = RegExp(ns + r':Thumbnails\[(\d+)\]/(.*)');
|
|
||||||
static const thumbnailDataDisplayKey = 'Image';
|
static const thumbnailDataDisplayKey = 'Image';
|
||||||
|
|
||||||
final thumbnails = <int, Map<String, String>>{};
|
final thumbnails = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpBasicNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpBasicNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmp, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails);
|
bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails);
|
||||||
|
@ -32,7 +31,11 @@ class XmpBasicNamespace extends XmpNamespace {
|
||||||
thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder(
|
thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
||||||
propPath: 'xmp:Thumbnails[$index]/xmpGImg:image',
|
props: [
|
||||||
|
const [Namespaces.xmp, 'Thumbnails'],
|
||||||
|
index,
|
||||||
|
const [Namespaces.xmpGImg, 'image'],
|
||||||
|
],
|
||||||
mimeType: MimeTypes.jpeg,
|
mimeType: MimeTypes.jpeg,
|
||||||
).dispatch(context),
|
).dispatch(context),
|
||||||
),
|
),
|
||||||
|
@ -43,22 +46,17 @@ class XmpBasicNamespace extends XmpNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpMMNamespace extends XmpNamespace {
|
class XmpMMNamespace extends XmpNamespace {
|
||||||
static const ns = 'xmpMM';
|
late final derivedFromPattern = RegExp(nsPrefix + r'DerivedFrom/(.*)');
|
||||||
|
late final historyPattern = RegExp(nsPrefix + r'History\[(\d+)\]/(.*)');
|
||||||
static const didPrefix = 'xmp.did:';
|
late final ingredientsPattern = RegExp(nsPrefix + r'Ingredients\[(\d+)\]/(.*)');
|
||||||
static const iidPrefix = 'xmp.iid:';
|
late final pantryPattern = RegExp(nsPrefix + r'Pantry\[(\d+)\]/(.*)');
|
||||||
|
|
||||||
static final derivedFromPattern = RegExp(ns + r':DerivedFrom/(.*)');
|
|
||||||
static final historyPattern = RegExp(ns + r':History\[(\d+)\]/(.*)');
|
|
||||||
static final ingredientsPattern = RegExp(ns + r':Ingredients\[(\d+)\]/(.*)');
|
|
||||||
static final pantryPattern = RegExp(ns + r':Pantry\[(\d+)\]/(.*)');
|
|
||||||
|
|
||||||
final derivedFrom = <String, String>{};
|
final derivedFrom = <String, String>{};
|
||||||
final history = <int, Map<String, String>>{};
|
final history = <int, Map<String, String>>{};
|
||||||
final ingredients = <int, Map<String, String>>{};
|
final ingredients = <int, Map<String, String>>{};
|
||||||
final pantry = <int, Map<String, String>>{};
|
final pantry = <int, Map<String, String>>{};
|
||||||
|
|
||||||
XmpMMNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpMMNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
bool extractData(XmpProp prop) {
|
||||||
|
@ -92,23 +90,13 @@ class XmpMMNamespace extends XmpNamespace {
|
||||||
structByIndex: pantry,
|
structByIndex: pantry,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
|
||||||
String formatValue(XmpProp prop) {
|
|
||||||
final value = prop.value;
|
|
||||||
if (value.startsWith(didPrefix)) return value.replaceFirst(didPrefix, '');
|
|
||||||
if (value.startsWith(iidPrefix)) return value.replaceFirst(iidPrefix, '');
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpNoteNamespace extends XmpNamespace {
|
class XmpNoteNamespace extends XmpNamespace {
|
||||||
static const ns = 'xmpNote';
|
|
||||||
|
|
||||||
// `xmpNote:HasExtendedXMP` is structural and should not be displayed to users
|
// `xmpNote:HasExtendedXMP` is structural and should not be displayed to users
|
||||||
static const hasExtendedXmp = '$ns:HasExtendedXMP';
|
late final hasExtendedXmp = '${nsPrefix}HasExtendedXMP';
|
||||||
|
|
||||||
const XmpNoteNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
XmpNoteNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmpNote, nsPrefix, rawProps);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool extractData(XmpProp prop) {
|
bool extractData(XmpProp prop) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
|
@ -12,7 +13,7 @@ import 'package:provider/provider.dart';
|
||||||
class XmpDirTile extends StatefulWidget {
|
class XmpDirTile extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final String title;
|
final String title;
|
||||||
final SplayTreeMap<String, String> tags;
|
final SplayTreeMap<String, String> allTags, tags;
|
||||||
final ValueNotifier<String?>? expandedNotifier;
|
final ValueNotifier<String?>? expandedNotifier;
|
||||||
final bool initiallyExpanded;
|
final bool initiallyExpanded;
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ class XmpDirTile extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.title,
|
required this.title,
|
||||||
|
required this.allTags,
|
||||||
required this.tags,
|
required this.tags,
|
||||||
required this.expandedNotifier,
|
required this.expandedNotifier,
|
||||||
required this.initiallyExpanded,
|
required this.initiallyExpanded,
|
||||||
|
@ -30,16 +32,34 @@ class XmpDirTile extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _XmpDirTileState extends State<XmpDirTile> {
|
class _XmpDirTileState extends State<XmpDirTile> {
|
||||||
|
late final Map<String, String> _schemaRegistryPrefixes, _tags;
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
static const schemaRegistryPrefixesKey = 'schemaRegistryPrefixes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tags = Map.from(widget.tags)..remove(schemaRegistryPrefixesKey);
|
||||||
|
final prefixesJson = widget.allTags[schemaRegistryPrefixesKey];
|
||||||
|
final Map<String, dynamic> prefixesDecoded = prefixesJson != null ? json.decode(prefixesJson) : {};
|
||||||
|
_schemaRegistryPrefixes = Map.fromEntries(prefixesDecoded.entries.map((kv) => MapEntry(kv.key, kv.value as String)));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sections = groupBy<MapEntry<String, String>, String>(widget.tags.entries, (kv) {
|
final sections = groupBy<MapEntry<String, String>, String>(_tags.entries, (kv) {
|
||||||
final fullKey = kv.key;
|
final fullKey = kv.key;
|
||||||
final i = fullKey.indexOf(XMP.propNamespaceSeparator);
|
final i = fullKey.indexOf(XMP.propNamespaceSeparator);
|
||||||
final namespace = i == -1 ? '' : fullKey.substring(0, i);
|
final nsPrefix = i == -1 ? '' : fullKey.substring(0, i + 1);
|
||||||
return namespace;
|
return nsPrefix;
|
||||||
}).entries.map((kv) => XmpNamespace.create(kv.key, Map.fromEntries(kv.value))).toList()
|
}).entries.map((kv) {
|
||||||
|
final nsPrefix = kv.key;
|
||||||
|
final nsUri = _schemaRegistryPrefixes[nsPrefix] ?? '';
|
||||||
|
final rawProps = Map.fromEntries(kv.value);
|
||||||
|
return XmpNamespace.create(nsUri, nsPrefix, rawProps);
|
||||||
|
}).toList()
|
||||||
..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle));
|
..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle));
|
||||||
return AvesExpansionTile(
|
return AvesExpansionTile(
|
||||||
// title may contain parent to distinguish multiple XMP directories
|
// title may contain parent to distinguish multiple XMP directories
|
||||||
|
|
Loading…
Reference in a new issue