#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.MetadataExtractorHelper
|
||||
import deckers.thibault.aves.metadata.MultiPage
|
||||
import deckers.thibault.aves.metadata.XMP
|
||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
||||
import deckers.thibault.aves.metadata.XMPPropName
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.provider.ContentImageProvider
|
||||
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 sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
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")
|
||||
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)
|
||||
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)) {
|
||||
try {
|
||||
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
|
||||
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
||||
try {
|
||||
val embedBytes: ByteArray = if (!dataPropPath.contains('/')) {
|
||||
val propNs = XMP.namespaceForPropPath(dataPropPath)
|
||||
xmpDirs.mapNotNull { it.xmpMeta.getPropertyBase64(propNs, dataPropPath) }.first()
|
||||
val embedBytes: ByteArray = if (props.size == 1) {
|
||||
val prop = props.first() as XMPPropName
|
||||
xmpDirs.mapNotNull { it.xmpMeta.getPropertyBase64(prop.nsUri, prop.toString()) }.first()
|
||||
} else {
|
||||
xmpDirs.mapNotNull { it.xmpMeta.getSafeStructField(dataPropPath) }.first().let {
|
||||
xmpDirs.mapNotNull { it.xmpMeta.getSafeStructField(props) }.first().let {
|
||||
XMPUtils.decodeBase64(it.value)
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +175,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
copyEmbeddedBytes(result, embedMimeType, displayName, embedBytes.inputStream())
|
||||
return
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +187,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
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) {
|
||||
|
|
|
@ -59,6 +59,8 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
|
|||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
||||
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.getSafeInt
|
||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
||||
|
@ -83,6 +85,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
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
|
||||
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) {
|
||||
|
@ -509,22 +515,21 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||
val xmpMeta = dir.xmpMeta
|
||||
try {
|
||||
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)) {
|
||||
val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, 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 }
|
||||
if (xmpMeta.doesPropExist(XMP.DC_SUBJECT_PROP_NAME)) {
|
||||
val values = xmpMeta.getPropArrayItemValues(XMP.DC_SUBJECT_PROP_NAME)
|
||||
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)) {
|
||||
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)) {
|
||||
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)) {
|
||||
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
|
||||
val standardRating = (percentRating / 25f).roundToInt() + 1
|
||||
metadataMap[KEY_RATING] = standardRating
|
||||
|
@ -834,13 +839,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
)
|
||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||
val xmpMeta = dir.xmpMeta
|
||||
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, 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_SCHEMA_NS, 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_SCHEMA_NS, 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.getSafeString(XMP.GPANO_SCHEMA_NS, XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
|
||||
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 }
|
||||
}
|
||||
result.success(fields)
|
||||
return
|
||||
|
@ -1071,8 +1076,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||
val xmpMeta = dir.xmpMeta
|
||||
try {
|
||||
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME)) {
|
||||
xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
|
||||
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
|
||||
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
|
||||
}
|
||||
} catch (e: XMPException) {
|
||||
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.util.Log
|
||||
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.model.FieldMap
|
||||
|
@ -146,17 +148,17 @@ object MultiPage {
|
|||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||
var offsetFromEnd: Long? = null
|
||||
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
|
||||
xmpMeta.getSafeLong(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
|
||||
} else if (xmpMeta.doesPropertyExist(XMP.CONTAINER_SCHEMA_NS, XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
|
||||
} else if (xmpMeta.doesPropExist(XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
// 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) {
|
||||
// expect the video to be the second item
|
||||
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 length = xmpMeta.getSafeStructField("${XMP.CONTAINER_DIRECTORY_PROP_NAME}[$i]/${XMP.CONTAINER_ITEM_PROP_NAME}/${XMP.CONTAINER_ITEM_LENGTH_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(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) {
|
||||
offsetFromEnd = length.toLong()
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.util.Log
|
|||
import com.adobe.internal.xmp.XMPError
|
||||
import com.adobe.internal.xmp.XMPException
|
||||
import com.adobe.internal.xmp.XMPMeta
|
||||
import com.adobe.internal.xmp.XMPMetaFactory
|
||||
import com.adobe.internal.xmp.properties.XMPProperty
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
|
@ -14,74 +15,65 @@ object XMP {
|
|||
|
||||
// standard namespaces
|
||||
// cf com.adobe.internal.xmp.XMPConst
|
||||
const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/"
|
||||
const val MICROSOFTPHOTO_SCHEMA_NS = "http://ns.microsoft.com/photo/1.0/"
|
||||
const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/"
|
||||
const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/"
|
||||
private const val XMP_GIMG_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/g/img/"
|
||||
private const val DC_NS_URI = "http://purl.org/dc/elements/1.1/"
|
||||
private const val MICROSOFTPHOTO_NS_URI = "http://ns.microsoft.com/photo/1.0/"
|
||||
private const val PHOTOSHOP_NS_URI = "http://ns.adobe.com/photoshop/1.0/"
|
||||
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
|
||||
|
||||
// other namespaces
|
||||
private const val GAUDIO_SCHEMA_NS = "http://ns.google.com/photos/1.0/audio/"
|
||||
const val GCAMERA_SCHEMA_NS = "http://ns.google.com/photos/1.0/camera/"
|
||||
private const val GDEPTH_SCHEMA_NS = "http://ns.google.com/photos/1.0/depthmap/"
|
||||
private const val GIMAGE_SCHEMA_NS = "http://ns.google.com/photos/1.0/image/"
|
||||
const val CONTAINER_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/"
|
||||
private const val CONTAINER_ITEM_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/item/"
|
||||
private const val CONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
|
||||
private const val CONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
|
||||
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 GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
|
||||
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"
|
||||
const val DC_DESCRIPTION_PROP_NAME = "dc:description"
|
||||
const val DC_TITLE_PROP_NAME = "dc:title"
|
||||
const val MS_RATING_PROP_NAME = "MicrosoftPhoto:Rating"
|
||||
const val PS_DATE_CREATED_PROP_NAME = "photoshop:DateCreated"
|
||||
const val XMP_CREATE_DATE_PROP_NAME = "xmp:CreateDate"
|
||||
const val XMP_RATING_PROP_NAME = "xmp:Rating"
|
||||
val DC_SUBJECT_PROP_NAME = XMPPropName(DC_NS_URI, "subject")
|
||||
val DC_DESCRIPTION_PROP_NAME = XMPPropName(DC_NS_URI, "description")
|
||||
val DC_TITLE_PROP_NAME = XMPPropName(DC_NS_URI, "title")
|
||||
val MS_RATING_PROP_NAME = XMPPropName(MICROSOFTPHOTO_NS_URI, "Rating")
|
||||
val PS_DATE_CREATED_PROP_NAME = XMPPropName(PHOTOSHOP_NS_URI, "DateCreated")
|
||||
val XMP_CREATE_DATE_PROP_NAME = XMPPropName(XMP_NS_URI, "CreateDate")
|
||||
val XMP_RATING_PROP_NAME = XMPPropName(XMP_NS_URI, "Rating")
|
||||
|
||||
private const val GENERIC_LANG = ""
|
||||
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
|
||||
// cf https://developers.google.com/depthmap-metadata
|
||||
// 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
|
||||
|
||||
const val GCAMERA_VIDEO_OFFSET_PROP_NAME = "GCamera:MicroVideoOffset"
|
||||
const val CONTAINER_DIRECTORY_PROP_NAME = "Container:Directory"
|
||||
const val CONTAINER_ITEM_PROP_NAME = "Container:Item"
|
||||
const val CONTAINER_ITEM_LENGTH_PROP_NAME = "Item:Length"
|
||||
const val CONTAINER_ITEM_MIME_PROP_NAME = "Item:Mime"
|
||||
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
|
||||
val CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Directory")
|
||||
val CONTAINER_ITEM_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Item")
|
||||
val CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Length")
|
||||
val CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Mime")
|
||||
|
||||
// panorama
|
||||
// cf https://developers.google.com/streetview/spherical-metadata
|
||||
|
||||
const val GPANO_SCHEMA_NS = "http://ns.google.com/photos/1.0/panorama/"
|
||||
private const val PMTM_SCHEMA_NS = "http://www.hdrsoft.com/photomatix_settings01"
|
||||
|
||||
const val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = "GPano:CroppedAreaImageHeightPixels"
|
||||
const val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = "GPano:CroppedAreaImageWidthPixels"
|
||||
const val GPANO_CROPPED_AREA_LEFT_PROP_NAME = "GPano:CroppedAreaLeftPixels"
|
||||
const val GPANO_CROPPED_AREA_TOP_PROP_NAME = "GPano:CroppedAreaTopPixels"
|
||||
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"
|
||||
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 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: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 {
|
||||
try {
|
||||
// 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
|
||||
if (doesPropertyExist(CONTAINER_SCHEMA_NS, CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
val count = countArrayItems(CONTAINER_SCHEMA_NS, CONTAINER_DIRECTORY_PROP_NAME)
|
||||
if (doesPropExist(CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
val count = countPropArrayItems(CONTAINER_DIRECTORY_PROP_NAME)
|
||||
if (count == 2) {
|
||||
var hasImage = false
|
||||
var hasVideo = false
|
||||
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 length = getSafeStructField("$CONTAINER_DIRECTORY_PROP_NAME[$i]/$CONTAINER_ITEM_PROP_NAME/$CONTAINER_ITEM_LENGTH_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(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||
hasImage = hasImage || MimeTypes.isImage(mime) && length != null
|
||||
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
|
||||
}
|
||||
|
@ -130,7 +122,7 @@ object XMP {
|
|||
fun XMPMeta.isPanorama(): Boolean {
|
||||
// Google
|
||||
try {
|
||||
if (gpanoRequiredProps.all { doesPropertyExist(GPANO_SCHEMA_NS, it) }) 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
|
||||
|
@ -141,7 +133,7 @@ object XMP {
|
|||
|
||||
// Photomatix
|
||||
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) {
|
||||
if (e.errorCode != XMPError.BADSCHEMA) {
|
||||
// `BADSCHEMA` code is reported when we check a property
|
||||
|
@ -153,7 +145,24 @@ object XMP {
|
|||
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 {
|
||||
if (doesPropertyExist(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 {
|
||||
if (doesPropertyExist(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 {
|
||||
if (doesPropertyExist(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 {
|
||||
if (doesPropertyExist(schema, propName)) {
|
||||
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 {
|
||||
if (doesPropertyExist(schema, propName)) {
|
||||
val item = getPropertyDate(schema, propName)
|
||||
|
@ -226,20 +243,38 @@ object XMP {
|
|||
}
|
||||
}
|
||||
|
||||
// e.g. 'Container:Directory[42]/Container:Item/Item:Mime'
|
||||
fun XMPMeta.getSafeStructField(path: String): XMPProperty? {
|
||||
val separator = path.lastIndexOf("/")
|
||||
if (separator != -1) {
|
||||
val structName = path.substring(0, separator)
|
||||
val structNs = namespaceForPropPath(structName)
|
||||
val fieldName = path.substring(separator + 1)
|
||||
val fieldNs = namespaceForPropPath(fieldName)
|
||||
try {
|
||||
return getStructField(structNs, structName, fieldNs, fieldName)
|
||||
} catch (e: XMPException) {
|
||||
Log.w(LOG_TAG, "failed to get XMP struct field for path=$path", e)
|
||||
// e.g. path 'Container:Directory[42]/Container:Item/Item:Mime' matches:
|
||||
// - structNs: "http://ns.google.com/photos/1.0/container/"
|
||||
// - structName: "Container:Directory[42]/Container:Item"
|
||||
// - fieldNs: "http://ns.google.com/photos/1.0/container/item/"
|
||||
// - fieldName: "Item:Mime"
|
||||
fun XMPMeta.getSafeStructField(props: List<Any>): XMPProperty? {
|
||||
if (props.size >= 2) {
|
||||
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 {
|
||||
return getStructField(structFirst.nsUri, structName, field.nsUri, fieldName)
|
||||
} catch (e: XMPException) {
|
||||
Log.w(LOG_TAG, "failed to get XMP struct field for props=$props", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
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> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType);
|
||||
Future<Map> extractXmpDataProp(AvesEntry entry, List<dynamic>? props, String? propMimeType);
|
||||
}
|
||||
|
||||
class PlatformEmbeddedDataService implements EmbeddedDataService {
|
||||
|
@ -61,14 +61,14 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Map> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async {
|
||||
Future<Map> extractXmpDataProp(AvesEntry entry, List<dynamic>? props, String? propMimeType) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
'displayName': '${entry.bestTitle} • $propPath',
|
||||
'propPath': propPath,
|
||||
'displayName': '${entry.bestTitle} • $props',
|
||||
'propPath': props,
|
||||
'propMimeType': propMimeType,
|
||||
});
|
||||
if (result != null) return result as Map;
|
||||
|
|
|
@ -2,14 +2,127 @@ import 'package:intl/intl.dart';
|
|||
import 'package:xml/xml.dart';
|
||||
|
||||
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 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 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 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 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 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 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 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 = {
|
||||
container: 'Container',
|
||||
|
@ -19,6 +132,7 @@ class Namespaces {
|
|||
rdf: 'rdf',
|
||||
x: 'x',
|
||||
xmp: 'xmp',
|
||||
xmpGImg: 'xmpGImg',
|
||||
xmpNote: 'xmpNote',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
|
|||
fields = await embeddedDataService.extractVideoEmbeddedPicture(entry);
|
||||
break;
|
||||
case EmbeddedDataSource.xmp:
|
||||
fields = await embeddedDataService.extractXmpDataProp(entry, notification.propPath, notification.mimeType);
|
||||
fields = await embeddedDataService.extractXmpDataProp(entry, notification.props, notification.mimeType);
|
||||
break;
|
||||
}
|
||||
if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) {
|
||||
|
|
|
@ -6,12 +6,12 @@ enum EmbeddedDataSource { motionPhotoVideo, videoCover, xmp }
|
|||
@immutable
|
||||
class OpenEmbeddedDataNotification extends Notification {
|
||||
final EmbeddedDataSource source;
|
||||
final String? propPath;
|
||||
final List<dynamic>? props;
|
||||
final String? mimeType;
|
||||
|
||||
const OpenEmbeddedDataNotification._private({
|
||||
required this.source,
|
||||
this.propPath,
|
||||
this.props,
|
||||
this.mimeType,
|
||||
});
|
||||
|
||||
|
@ -24,15 +24,15 @@ class OpenEmbeddedDataNotification extends Notification {
|
|||
);
|
||||
|
||||
factory OpenEmbeddedDataNotification.xmp({
|
||||
required String propPath,
|
||||
required List<dynamic> props,
|
||||
required String mimeType,
|
||||
}) =>
|
||||
OpenEmbeddedDataNotification._private(
|
||||
source: EmbeddedDataSource.xmp,
|
||||
propPath: propPath,
|
||||
props: props,
|
||||
mimeType: mimeType,
|
||||
);
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
var tags = dir.tags;
|
||||
if (tags.isEmpty) return const SizedBox.shrink();
|
||||
if (tags.isEmpty) return const SizedBox();
|
||||
|
||||
final dirName = dir.name;
|
||||
if (dirName == MetadataDirectory.xmpDirectory) {
|
||||
return XmpDirTile(
|
||||
entry: entry,
|
||||
title: title,
|
||||
allTags: dir.allTags,
|
||||
tags: tags,
|
||||
expandedNotifier: expandedDirectoryNotifier,
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
|
|
|
@ -26,110 +26,58 @@ import 'package:provider/provider.dart';
|
|||
|
||||
@immutable
|
||||
class XmpNamespace extends Equatable {
|
||||
final String namespace;
|
||||
final String nsUri, nsPrefix;
|
||||
final Map<String, String> rawProps;
|
||||
|
||||
@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) {
|
||||
switch (namespace) {
|
||||
case XmpBasicNamespace.ns:
|
||||
return XmpBasicNamespace(rawProps);
|
||||
case XmpContainer.ns:
|
||||
return XmpContainer(rawProps);
|
||||
case XmpCrsNamespace.ns:
|
||||
return XmpCrsNamespace(rawProps);
|
||||
case XmpDarktableNamespace.ns:
|
||||
return XmpDarktableNamespace(rawProps);
|
||||
case XmpDwcNamespace.ns:
|
||||
return XmpDwcNamespace(rawProps);
|
||||
case XmpExifNamespace.ns:
|
||||
return XmpExifNamespace(rawProps);
|
||||
case XmpGAudioNamespace.ns:
|
||||
return XmpGAudioNamespace(rawProps);
|
||||
case XmpGDepthNamespace.ns:
|
||||
return XmpGDepthNamespace(rawProps);
|
||||
case XmpGImageNamespace.ns:
|
||||
return XmpGImageNamespace(rawProps);
|
||||
case XmpIptcCoreNamespace.ns:
|
||||
return XmpIptcCoreNamespace(rawProps);
|
||||
case XmpIptc4xmpExtNamespace.ns:
|
||||
return XmpIptc4xmpExtNamespace(rawProps);
|
||||
case XmpMgwRegionsNamespace.ns:
|
||||
return XmpMgwRegionsNamespace(rawProps);
|
||||
case XmpMMNamespace.ns:
|
||||
return XmpMMNamespace(rawProps);
|
||||
case XmpMPNamespace.ns:
|
||||
return XmpMPNamespace(rawProps);
|
||||
case XmpNoteNamespace.ns:
|
||||
return XmpNoteNamespace(rawProps);
|
||||
case XmpPhotoshopNamespace.ns:
|
||||
return XmpPhotoshopNamespace(rawProps);
|
||||
case XmpPlusNamespace.ns:
|
||||
return XmpPlusNamespace(rawProps);
|
||||
case XmpTiffNamespace.ns:
|
||||
return XmpTiffNamespace(rawProps);
|
||||
factory XmpNamespace.create(String nsUri, String nsPrefix, Map<String, String> rawProps) {
|
||||
switch (nsUri) {
|
||||
case Namespaces.container:
|
||||
return XmpContainer(nsPrefix, rawProps);
|
||||
case Namespaces.crs:
|
||||
return XmpCrsNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.darktable:
|
||||
return XmpDarktableNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.dwc:
|
||||
return XmpDwcNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.exif:
|
||||
return XmpExifNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.gAudio:
|
||||
return XmpGAudioNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.gDepth:
|
||||
return XmpGDepthNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.gImage:
|
||||
return XmpGImageNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.iptc4xmpCore:
|
||||
return XmpIptcCoreNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.iptc4xmpExt:
|
||||
return XmpIptc4xmpExtNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.mwgrs:
|
||||
return XmpMgwRegionsNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.mp:
|
||||
return XmpMPNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.photoshop:
|
||||
return XmpPhotoshopNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.plus:
|
||||
return XmpPlusNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.tiff:
|
||||
return XmpTiffNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.xmp:
|
||||
return XmpBasicNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.xmpMM:
|
||||
return XmpMMNamespace(nsPrefix, rawProps);
|
||||
case Namespaces.xmpNote:
|
||||
return XmpNoteNamespace(nsPrefix, rawProps);
|
||||
default:
|
||||
return XmpNamespace(namespace, rawProps);
|
||||
return XmpNamespace(nsUri, nsPrefix, rawProps);
|
||||
}
|
||||
}
|
||||
|
||||
// cf https://exiftool.org/TagNames/XMP.html
|
||||
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;
|
||||
String get displayTitle => Namespaces.nsTitles[nsUri] ?? '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)';
|
||||
|
||||
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_structs.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class XmpCrsNamespace extends XmpNamespace {
|
||||
static const ns = 'crs';
|
||||
|
||||
static final cgbcPattern = RegExp(ns + r':CircularGradientBasedCorrections\[(\d+)\]/(.*)');
|
||||
static final gbcPattern = RegExp(ns + r':GradientBasedCorrections\[(\d+)\]/(.*)');
|
||||
static final mgbcPattern = RegExp(ns + r':MaskGroupBasedCorrections\[(\d+)\]/(.*)');
|
||||
static final pbcPattern = RegExp(ns + r':PaintBasedCorrections\[(\d+)\]/(.*)');
|
||||
static final retouchAreasPattern = RegExp(ns + r':RetouchAreas\[(\d+)\]/(.*)');
|
||||
static final lookPattern = RegExp(ns + r':Look/(.*)');
|
||||
static final rmmiPattern = RegExp(ns + r':RangeMaskMapInfo/' + ns + r':RangeMaskMapInfo/(.*)');
|
||||
late final cgbcPattern = RegExp(nsPrefix + r'CircularGradientBasedCorrections\[(\d+)\]/(.*)');
|
||||
late final gbcPattern = RegExp(nsPrefix + r'GradientBasedCorrections\[(\d+)\]/(.*)');
|
||||
late final mgbcPattern = RegExp(nsPrefix + r'MaskGroupBasedCorrections\[(\d+)\]/(.*)');
|
||||
late final pbcPattern = RegExp(nsPrefix + r'PaintBasedCorrections\[(\d+)\]/(.*)');
|
||||
late final retouchAreasPattern = RegExp(nsPrefix + r'RetouchAreas\[(\d+)\]/(.*)');
|
||||
late final lookPattern = RegExp(nsPrefix + r'Look/(.*)');
|
||||
late final rmmiPattern = RegExp(nsPrefix + r'RangeMaskMapInfo/' + nsPrefix + r'RangeMaskMapInfo/(.*)');
|
||||
|
||||
final cgbc = <int, Map<String, String>>{};
|
||||
final gbc = <int, Map<String, String>>{};
|
||||
|
@ -21,7 +20,7 @@ class XmpCrsNamespace extends XmpNamespace {
|
|||
final look = <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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class XmpDarktableNamespace extends XmpNamespace {
|
||||
static const ns = 'darktable';
|
||||
|
||||
static final historyPattern = RegExp(ns + r':history\[(\d+)\]/(.*)');
|
||||
late final historyPattern = RegExp(nsPrefix + r'history\[(\d+)\]/(.*)');
|
||||
|
||||
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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class XmpDwcNamespace extends XmpNamespace {
|
||||
static const ns = 'dwc';
|
||||
|
||||
static final dcTermsLocationPattern = RegExp(ns + r':dctermsLocation/(.*)');
|
||||
static final eventPattern = RegExp(ns + r':Event/(.*)');
|
||||
static final geologicalContextPattern = RegExp(ns + r':GeologicalContext/(.*)');
|
||||
static final identificationPattern = RegExp(ns + r':Identification/(.*)');
|
||||
static final measurementOrFactPattern = RegExp(ns + r':MeasurementOrFact/(.*)');
|
||||
static final occurrencePattern = RegExp(ns + r':Occurrence/(.*)');
|
||||
static final recordPattern = RegExp(ns + r':Record/(.*)');
|
||||
static final resourceRelationshipPattern = RegExp(ns + r':ResourceRelationship/(.*)');
|
||||
static final taxonPattern = RegExp(ns + r':Taxon/(.*)');
|
||||
late final dcTermsLocationPattern = RegExp(nsPrefix + r'dctermsLocation/(.*)');
|
||||
late final eventPattern = RegExp(nsPrefix + r'Event/(.*)');
|
||||
late final geologicalContextPattern = RegExp(nsPrefix + r'GeologicalContext/(.*)');
|
||||
late final identificationPattern = RegExp(nsPrefix + r'Identification/(.*)');
|
||||
late final measurementOrFactPattern = RegExp(nsPrefix + r'MeasurementOrFact/(.*)');
|
||||
late final occurrencePattern = RegExp(nsPrefix + r'Occurrence/(.*)');
|
||||
late final recordPattern = RegExp(nsPrefix + r'Record/(.*)');
|
||||
late final resourceRelationshipPattern = RegExp(nsPrefix + r'ResourceRelationship/(.*)');
|
||||
late final taxonPattern = RegExp(nsPrefix + r'Taxon/(.*)');
|
||||
|
||||
final dcTermsLocation = <String, String>{};
|
||||
final event = <String, String>{};
|
||||
|
@ -25,7 +24,7 @@ class XmpDwcNamespace extends XmpNamespace {
|
|||
final resourceRelationship = <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
|
||||
bool extractData(XmpProp prop) {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import 'package:aves/ref/exif.dart';
|
||||
import 'package:aves/utils/xmp_utils.dart';
|
||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||
|
||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md
|
||||
class XmpExifNamespace extends XmpNamespace {
|
||||
static const ns = 'exif';
|
||||
|
||||
const XmpExifNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
||||
const XmpExifNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.exif, nsPrefix, rawProps);
|
||||
|
||||
@override
|
||||
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/viewer/embedded/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
|
@ -8,7 +9,7 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:tuple/tuple.dart';
|
||||
|
||||
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;
|
||||
|
||||
|
@ -24,10 +25,29 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
|
|||
dataProp.displayKey,
|
||||
InfoRowGroup.linkSpanBuilder(
|
||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
||||
propPath: dataProp.path,
|
||||
mimeType: mimeProp.value,
|
||||
).dispatch(context),
|
||||
onTap: (context) {
|
||||
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,
|
||||
).dispatch(context);
|
||||
},
|
||||
))
|
||||
: null;
|
||||
}).whereNotNull());
|
||||
|
@ -35,43 +55,35 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
|
|||
}
|
||||
|
||||
class XmpGAudioNamespace extends XmpGoogleNamespace {
|
||||
static const ns = 'GAudio';
|
||||
|
||||
const XmpGAudioNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
||||
const XmpGAudioNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gAudio, nsPrefix, rawProps);
|
||||
|
||||
@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 {
|
||||
static const ns = 'GDepth';
|
||||
|
||||
const XmpGDepthNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
||||
const XmpGDepthNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gDepth, nsPrefix, rawProps);
|
||||
|
||||
@override
|
||||
List<Tuple2<String, String>> get dataProps => const [
|
||||
Tuple2('$ns:Data', '$ns:Mime'),
|
||||
Tuple2('$ns:Confidence', '$ns:ConfidenceMime'),
|
||||
List<Tuple2<String, String>> get dataProps => [
|
||||
Tuple2('${nsPrefix}Data', '${nsPrefix}Mime'),
|
||||
Tuple2('${nsPrefix}Confidence', '${nsPrefix}ConfidenceMime'),
|
||||
];
|
||||
}
|
||||
|
||||
class XmpGImageNamespace extends XmpGoogleNamespace {
|
||||
static const ns = 'GImage';
|
||||
|
||||
const XmpGImageNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
||||
const XmpGImageNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gImage, nsPrefix, rawProps);
|
||||
|
||||
@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 {
|
||||
static const ns = 'Container';
|
||||
|
||||
static final directoryPattern = RegExp('$ns:Directory\\[(\\d+)\\]/$ns:Item/(.*)');
|
||||
late final directoryPattern = RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)');
|
||||
|
||||
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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class XmpIptcCoreNamespace extends XmpNamespace {
|
||||
static const ns = 'Iptc4xmpCore';
|
||||
|
||||
static final creatorContactInfoPattern = RegExp(ns + r':CreatorContactInfo/(.*)');
|
||||
late final creatorContactInfoPattern = RegExp(nsPrefix + r'CreatorContactInfo/(.*)');
|
||||
|
||||
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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class XmpIptc4xmpExtNamespace extends XmpNamespace {
|
||||
static const ns = 'Iptc4xmpExt';
|
||||
|
||||
static final aooPattern = RegExp(ns + r':ArtworkOrObject\[(\d+)\]/(.*)');
|
||||
late final aooPattern = RegExp(nsPrefix + r'ArtworkOrObject\[(\d+)\]/(.*)');
|
||||
|
||||
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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class XmpMPNamespace extends XmpNamespace {
|
||||
static const ns = 'MP';
|
||||
|
||||
static final regionListPattern = RegExp(ns + r':RegionInfo/MPRI:Regions\[(\d+)\]/(.*)');
|
||||
late final regionListPattern = RegExp(nsPrefix + r'RegionInfo/MPRI:Regions\[(\d+)\]/(.*)');
|
||||
|
||||
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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15)
|
||||
class XmpMgwRegionsNamespace extends XmpNamespace {
|
||||
static const ns = 'mwg-rs';
|
||||
|
||||
static final dimensionsPattern = RegExp(ns + r':Regions/mwg-rs:AppliedToDimensions/(.*)');
|
||||
static final regionListPattern = RegExp(ns + r':Regions/mwg-rs:RegionList\[(\d+)\]/(.*)');
|
||||
late final dimensionsPattern = RegExp(nsPrefix + r'Regions/mwg-rs:AppliedToDimensions/(.*)');
|
||||
late final regionListPattern = RegExp(nsPrefix + r'Regions/mwg-rs:RegionList\[(\d+)\]/(.*)');
|
||||
|
||||
final dimensions = <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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
||||
class XmpPhotoshopNamespace extends XmpNamespace {
|
||||
static const ns = 'photoshop';
|
||||
|
||||
static final textLayersPattern = RegExp(ns + r':TextLayers\[(\d+)\]/(.*)');
|
||||
late final textLayersPattern = RegExp(nsPrefix + r'TextLayers\[(\d+)\]/(.*)');
|
||||
|
||||
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
|
||||
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_structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class XmpPlusNamespace extends XmpNamespace {
|
||||
static const ns = 'plus';
|
||||
|
||||
static final licensorPattern = RegExp(ns + r':Licensor\[(\d+)\]/(.*)');
|
||||
late final licensorPattern = RegExp(nsPrefix + r'Licensor\[(\d+)\]/(.*)');
|
||||
|
||||
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
|
||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, licensorPattern, licensor);
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import 'package:aves/ref/exif.dart';
|
||||
import 'package:aves/utils/xmp_utils.dart';
|
||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||
|
||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md
|
||||
class XmpTiffNamespace extends XmpNamespace {
|
||||
static const ns = 'tiff';
|
||||
|
||||
const XmpTiffNamespace(Map<String, String> rawProps) : super(ns, rawProps);
|
||||
const XmpTiffNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.tiff, nsPrefix, rawProps);
|
||||
|
||||
@override
|
||||
String formatValue(XmpProp prop) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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/viewer/embedded/notifications.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';
|
||||
|
||||
class XmpBasicNamespace extends XmpNamespace {
|
||||
static const ns = 'xmp';
|
||||
|
||||
static final thumbnailsPattern = RegExp(ns + r':Thumbnails\[(\d+)\]/(.*)');
|
||||
late final thumbnailsPattern = RegExp(nsPrefix + r'Thumbnails\[(\d+)\]/(.*)');
|
||||
static const thumbnailDataDisplayKey = 'Image';
|
||||
|
||||
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
|
||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails);
|
||||
|
@ -32,7 +31,11 @@ class XmpBasicNamespace extends XmpNamespace {
|
|||
thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder(
|
||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
||||
propPath: 'xmp:Thumbnails[$index]/xmpGImg:image',
|
||||
props: [
|
||||
const [Namespaces.xmp, 'Thumbnails'],
|
||||
index,
|
||||
const [Namespaces.xmpGImg, 'image'],
|
||||
],
|
||||
mimeType: MimeTypes.jpeg,
|
||||
).dispatch(context),
|
||||
),
|
||||
|
@ -43,22 +46,17 @@ class XmpBasicNamespace extends XmpNamespace {
|
|||
}
|
||||
|
||||
class XmpMMNamespace extends XmpNamespace {
|
||||
static const ns = 'xmpMM';
|
||||
|
||||
static const didPrefix = 'xmp.did:';
|
||||
static const iidPrefix = 'xmp.iid:';
|
||||
|
||||
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+)\]/(.*)');
|
||||
late final derivedFromPattern = RegExp(nsPrefix + r'DerivedFrom/(.*)');
|
||||
late final historyPattern = RegExp(nsPrefix + r'History\[(\d+)\]/(.*)');
|
||||
late final ingredientsPattern = RegExp(nsPrefix + r'Ingredients\[(\d+)\]/(.*)');
|
||||
late final pantryPattern = RegExp(nsPrefix + r'Pantry\[(\d+)\]/(.*)');
|
||||
|
||||
final derivedFrom = <String, String>{};
|
||||
final history = <int, Map<String, String>>{};
|
||||
final ingredients = <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
|
||||
bool extractData(XmpProp prop) {
|
||||
|
@ -92,23 +90,13 @@ class XmpMMNamespace extends XmpNamespace {
|
|||
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 {
|
||||
static const ns = 'xmpNote';
|
||||
|
||||
// `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
|
||||
bool extractData(XmpProp prop) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
@ -12,7 +13,7 @@ import 'package:provider/provider.dart';
|
|||
class XmpDirTile extends StatefulWidget {
|
||||
final AvesEntry entry;
|
||||
final String title;
|
||||
final SplayTreeMap<String, String> tags;
|
||||
final SplayTreeMap<String, String> allTags, tags;
|
||||
final ValueNotifier<String?>? expandedNotifier;
|
||||
final bool initiallyExpanded;
|
||||
|
||||
|
@ -20,6 +21,7 @@ class XmpDirTile extends StatefulWidget {
|
|||
super.key,
|
||||
required this.entry,
|
||||
required this.title,
|
||||
required this.allTags,
|
||||
required this.tags,
|
||||
required this.expandedNotifier,
|
||||
required this.initiallyExpanded,
|
||||
|
@ -30,16 +32,34 @@ class XmpDirTile extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _XmpDirTileState extends State<XmpDirTile> {
|
||||
late final Map<String, String> _schemaRegistryPrefixes, _tags;
|
||||
|
||||
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
|
||||
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 i = fullKey.indexOf(XMP.propNamespaceSeparator);
|
||||
final namespace = i == -1 ? '' : fullKey.substring(0, i);
|
||||
return namespace;
|
||||
}).entries.map((kv) => XmpNamespace.create(kv.key, Map.fromEntries(kv.value))).toList()
|
||||
final nsPrefix = i == -1 ? '' : fullKey.substring(0, i + 1);
|
||||
return nsPrefix;
|
||||
}).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));
|
||||
return AvesExpansionTile(
|
||||
// title may contain parent to distinguish multiple XMP directories
|
||||
|
|
Loading…
Reference in a new issue