XMP: reviewed data prop linking, open thumbnails like other data prop
This commit is contained in:
parent
b297fd5fe0
commit
690d257375
16 changed files with 237 additions and 190 deletions
|
@ -74,7 +74,6 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
|||
"getOverlayMetadata" -> GlobalScope.launch { getOverlayMetadata(call, Coresult(result)) }
|
||||
"getEmbeddedPictures" -> GlobalScope.launch { getEmbeddedPictures(call, Coresult(result)) }
|
||||
"getExifThumbnails" -> GlobalScope.launch { getExifThumbnails(call, Coresult(result)) }
|
||||
"getXmpThumbnails" -> GlobalScope.launch { getXmpThumbnails(call, Coresult(result)) }
|
||||
"extractXmpDataProp" -> GlobalScope.launch { extractXmpDataProp(call, Coresult(result)) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
|
@ -539,53 +538,13 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(thumbnails)
|
||||
}
|
||||
|
||||
private fun getXmpThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getXmpThumbnails-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val thumbnails = ArrayList<ByteArray>()
|
||||
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
|
||||
try {
|
||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||
val metadata = ImageMetadataReader.readMetadata(input, sizeBytes ?: -1)
|
||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||
val xmpMeta = dir.xmpMeta
|
||||
try {
|
||||
if (xmpMeta.doesPropertyExist(XMP.XMP_SCHEMA_NS, XMP.THUMBNAIL_PROP_NAME)) {
|
||||
val count = xmpMeta.countArrayItems(XMP.XMP_SCHEMA_NS, XMP.THUMBNAIL_PROP_NAME)
|
||||
for (i in 1 until count + 1) {
|
||||
val structName = "${XMP.THUMBNAIL_PROP_NAME}[$i]"
|
||||
val image = xmpMeta.getStructField(XMP.XMP_SCHEMA_NS, structName, XMP.IMG_SCHEMA_NS, XMP.THUMBNAIL_IMAGE_PROP_NAME)
|
||||
if (image != null) {
|
||||
thumbnails.add(XMPUtils.decodeBase64(image.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: XMPException) {
|
||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e)
|
||||
}
|
||||
}
|
||||
result.success(thumbnails)
|
||||
}
|
||||
|
||||
private fun extractXmpDataProp(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
val dataPropPath = call.argument<String>("propPath")
|
||||
if (mimeType == null || uri == null || dataPropPath == null) {
|
||||
val embedMimeType = call.argument<String>("propMimeType")
|
||||
if (mimeType == null || uri == null || dataPropPath == null || embedMimeType == null) {
|
||||
result.error("extractXmpDataProp-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
@ -598,10 +557,22 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
|||
// which is returned as a second XMP directory
|
||||
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
||||
try {
|
||||
val ns = XMP.namespaceForDataPath(dataPropPath)
|
||||
val mimePropPath = XMP.mimeTypePathForDataPath(dataPropPath)
|
||||
val embedMimeType = xmpDirs.map { it.xmpMeta.getPropertyString(ns, mimePropPath) }.first { it != null }
|
||||
val embedBytes = xmpDirs.map { it.xmpMeta.getPropertyBase64(ns, dataPropPath) }.first { it != null }
|
||||
val pathParts = dataPropPath.split('/')
|
||||
|
||||
val embedBytes: ByteArray = if (pathParts.size == 1) {
|
||||
val propName = pathParts[0]
|
||||
val propNs = XMP.namespaceForPropPath(propName)
|
||||
xmpDirs.map { it.xmpMeta.getPropertyBase64(propNs, propName) }.first { it != null }
|
||||
} else {
|
||||
val structName = pathParts[0]
|
||||
val structNs = XMP.namespaceForPropPath(structName)
|
||||
val fieldName = pathParts[1]
|
||||
val fieldNs = XMP.namespaceForPropPath(fieldName)
|
||||
xmpDirs.map { it.xmpMeta.getStructField(structNs, structName, fieldNs, fieldName) }.first { it != null }.let {
|
||||
XMPUtils.decodeBase64(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
val embedFile = File.createTempFile("aves", null, context.cacheDir).apply {
|
||||
deleteOnExit()
|
||||
outputStream().use { outputStream ->
|
||||
|
|
|
@ -12,19 +12,33 @@ object XMP {
|
|||
const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/"
|
||||
const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/"
|
||||
const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/"
|
||||
const val IMG_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/g/img/"
|
||||
|
||||
const val SUBJECT_PROP_NAME = "dc:subject"
|
||||
const val TITLE_PROP_NAME = "dc:title"
|
||||
const val DESCRIPTION_PROP_NAME = "dc:description"
|
||||
const val PS_DATE_CREATED_PROP_NAME = "photoshop:DateCreated";
|
||||
const val PS_DATE_CREATED_PROP_NAME = "photoshop:DateCreated"
|
||||
const val CREATE_DATE_PROP_NAME = "xmp:CreateDate"
|
||||
const val THUMBNAIL_PROP_NAME = "xmp:Thumbnails"
|
||||
const val THUMBNAIL_IMAGE_PROP_NAME = "xmpGImg:image"
|
||||
|
||||
private const val GENERIC_LANG = ""
|
||||
private const val SPECIFIC_LANG = "en-US"
|
||||
|
||||
private val schemas = hashMapOf(
|
||||
"GAudio" to "http://ns.google.com/photos/1.0/audio/",
|
||||
"GDepth" to "http://ns.google.com/photos/1.0/depthmap/",
|
||||
"GImage" to "http://ns.google.com/photos/1.0/image/",
|
||||
"xmp" to XMP_SCHEMA_NS,
|
||||
"xmpGImg" to "http://ns.adobe.com/xap/1.0/g/img/",
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
fun isDataPath(path: String) = knownDataPaths.contains(path)
|
||||
|
||||
// panorama
|
||||
// cf https://developers.google.com/streetview/spherical-metadata
|
||||
|
||||
|
@ -48,44 +62,6 @@ object XMP {
|
|||
GPANO_PROJECTION_TYPE_PROP_NAME,
|
||||
)
|
||||
|
||||
// embedded media data properties
|
||||
// cf https://developers.google.com/depthmap-metadata
|
||||
// cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
|
||||
|
||||
private const val GAUDIO_SCHEMA_NS = "http://ns.google.com/photos/1.0/audio/"
|
||||
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/"
|
||||
|
||||
private const val GAUDIO_DATA_PROP_NAME = "GAudio:Data"
|
||||
private const val GIMAGE_DATA_PROP_NAME = "GImage:Data"
|
||||
private const val GDEPTH_DATA_PROP_NAME = "GDepth:Data"
|
||||
private const val GDEPTH_CONFIDENCE_PROP_NAME = "GDepth:Confidence"
|
||||
|
||||
private const val GAUDIO_MIME_PROP_NAME = "GAudio:Mime"
|
||||
private const val GIMAGE_MIME_PROP_NAME = "GImage:Mime"
|
||||
private const val GDEPTH_MIME_PROP_NAME = "GDepth:Mime"
|
||||
private const val GDEPTH_CONFIDENCE_MIME_PROP_NAME = "GDepth:ConfidenceMime"
|
||||
|
||||
private val dataPropNamespaces = hashMapOf(
|
||||
GAUDIO_DATA_PROP_NAME to GAUDIO_SCHEMA_NS,
|
||||
GIMAGE_DATA_PROP_NAME to GIMAGE_SCHEMA_NS,
|
||||
GDEPTH_DATA_PROP_NAME to GDEPTH_SCHEMA_NS,
|
||||
GDEPTH_CONFIDENCE_PROP_NAME to GDEPTH_SCHEMA_NS,
|
||||
)
|
||||
|
||||
private val dataPropMimeProps = hashMapOf(
|
||||
GAUDIO_DATA_PROP_NAME to GAUDIO_MIME_PROP_NAME,
|
||||
GIMAGE_DATA_PROP_NAME to GIMAGE_MIME_PROP_NAME,
|
||||
GDEPTH_DATA_PROP_NAME to GDEPTH_MIME_PROP_NAME,
|
||||
GDEPTH_CONFIDENCE_PROP_NAME to GDEPTH_CONFIDENCE_MIME_PROP_NAME,
|
||||
)
|
||||
|
||||
fun isDataPath(path: String) = dataPropNamespaces.containsKey(path)
|
||||
|
||||
fun namespaceForDataPath(dataPropPath: String) = dataPropNamespaces[dataPropPath]
|
||||
|
||||
fun mimeTypePathForDataPath(dataPropPath: String) = dataPropMimeProps[dataPropPath]
|
||||
|
||||
// extensions
|
||||
|
||||
fun XMPMeta.getSafeLocalizedText(schema: String, propName: String, save: (value: String) -> Unit) {
|
||||
|
|
|
@ -11,17 +11,12 @@ class XMP {
|
|||
'crs': 'Camera Raw Settings',
|
||||
'dc': 'Dublin Core',
|
||||
'drone-dji': 'DJI Drone',
|
||||
'exif': 'Exif',
|
||||
'exifEX': 'Exif Ex',
|
||||
'GettyImagesGIFT': 'Getty Images',
|
||||
'GIMP': 'GIMP',
|
||||
'GAudio': 'Google Audio',
|
||||
'GDepth': 'Google Depth',
|
||||
'GFocus': 'Google Focus',
|
||||
'GImage': 'Google Image',
|
||||
'GPano': 'Google Panorama',
|
||||
'illustrator': 'Illustrator',
|
||||
'Iptc4xmpCore': 'IPTC Core',
|
||||
'lr': 'Lightroom',
|
||||
'MicrosoftPhoto': 'Microsoft Photo',
|
||||
'panorama': 'Panorama',
|
||||
|
@ -29,21 +24,10 @@ class XMP {
|
|||
'pdfx': 'PDF/X',
|
||||
'PanoStudioXMP': 'PanoramaStudio',
|
||||
'photomechanic': 'Photo Mechanic',
|
||||
'photoshop': 'Photoshop',
|
||||
'plus': 'PLUS',
|
||||
'tiff': 'TIFF',
|
||||
'xmp': 'Basic',
|
||||
'xmpBJ': 'Basic Job Ticket',
|
||||
'xmpDM': 'Dynamic Media',
|
||||
'xmpMM': 'Media Management',
|
||||
'xmpRights': 'Rights Management',
|
||||
'xmpTPg': 'Paged-Text',
|
||||
};
|
||||
|
||||
// TODO TLAD 'xmp:Thumbnails[\d]/Image'
|
||||
static const dataProps = [
|
||||
'GAudio:Data',
|
||||
'GDepth:Data',
|
||||
'GImage:Data',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -106,27 +106,14 @@ class MetadataService {
|
|||
return [];
|
||||
}
|
||||
|
||||
static Future<List<Uint8List>> getXmpThumbnails(ImageEntry entry) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getXmpThumbnails', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
});
|
||||
return (result as List).cast<Uint8List>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getXmpThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<Map> extractXmpDataProp(ImageEntry entry, String propPath) async {
|
||||
static Future<Map> extractXmpDataProp(ImageEntry entry, String propPath, String propMimeType) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
'propPath': propPath,
|
||||
'propMimeType': propMimeType,
|
||||
});
|
||||
return result;
|
||||
} on PlatformException catch (e) {
|
||||
|
|
|
@ -33,17 +33,23 @@ class BasicSection extends StatelessWidget {
|
|||
final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0;
|
||||
final resolutionText = '${entry.resolutionText}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
||||
|
||||
// TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081
|
||||
// inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue)
|
||||
final title = entry.bestTitle ?? Constants.infoUnknown;
|
||||
final uri = entry.uri ?? Constants.infoUnknown;
|
||||
final path = entry.path;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoRowGroup({
|
||||
'Title': entry.bestTitle ?? Constants.infoUnknown,
|
||||
'Title': title,
|
||||
'Date': dateText,
|
||||
if (entry.isVideo) ..._buildVideoRows(),
|
||||
if (!entry.isSvg) 'Resolution': resolutionText,
|
||||
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown,
|
||||
'URI': entry.uri ?? Constants.infoUnknown,
|
||||
if (entry.path != null) 'Path': entry.path,
|
||||
'URI': uri,
|
||||
if (path != null) 'Path': path,
|
||||
}),
|
||||
_buildChips(),
|
||||
],
|
||||
|
|
|
@ -99,7 +99,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
|||
final handler = linkHandlers[key];
|
||||
value = handler.linkText;
|
||||
// open link on tap
|
||||
recognizer = TapGestureRecognizer()..onTap = handler.onTap;
|
||||
recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context);
|
||||
style = linkStyle;
|
||||
} else {
|
||||
value = kv.value;
|
||||
|
@ -149,7 +149,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
|||
|
||||
class InfoLinkHandler {
|
||||
final String linkText;
|
||||
final VoidCallback onTap;
|
||||
final void Function(BuildContext context) onTap;
|
||||
|
||||
const InfoLinkHandler({
|
||||
@required this.linkText,
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/model/image_entry.dart';
|
|||
import 'package:aves/services/metadata_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum MetadataThumbnailSource { embedded, exif, xmp }
|
||||
enum MetadataThumbnailSource { embedded, exif }
|
||||
|
||||
class MetadataThumbnails extends StatefulWidget {
|
||||
final MetadataThumbnailSource source;
|
||||
|
@ -38,9 +38,6 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
|||
case MetadataThumbnailSource.exif:
|
||||
_loader = MetadataService.getExifThumbnails(entry);
|
||||
break;
|
||||
case MetadataThumbnailSource.xmp:
|
||||
_loader = MetadataService.getXmpThumbnails(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,38 +7,6 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class XmpProp {
|
||||
final String path, value;
|
||||
final String displayKey;
|
||||
|
||||
static final sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
|
||||
static final sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])');
|
||||
|
||||
XmpProp(this.path, this.value) : displayKey = formatKey(path);
|
||||
|
||||
bool get isOpenable => XMP.dataProps.contains(path);
|
||||
|
||||
static String formatKey(String propPath) {
|
||||
return propPath.splitMapJoin(XMP.structFieldSeparator,
|
||||
onMatch: (match) => ' ${match.group(0)} ',
|
||||
onNonMatch: (s) {
|
||||
// strip namespace
|
||||
var key = s.split(XMP.propNamespaceSeparator).last;
|
||||
|
||||
// uppercase first letter
|
||||
key = key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
|
||||
|
||||
// sentence case
|
||||
return key.replaceAllMapped(sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType#${shortHash(this)}{path=$path, value=$value}';
|
||||
}
|
||||
}
|
||||
|
||||
class XmpNamespace {
|
||||
final String namespace;
|
||||
|
||||
|
@ -48,22 +16,11 @@ class XmpNamespace {
|
|||
|
||||
List<Widget> buildNamespaceSection({
|
||||
@required List<MapEntry<String, String>> rawProps,
|
||||
@required void Function(String propPath) openEmbeddedData,
|
||||
}) {
|
||||
final linkHandlers = <String, InfoLinkHandler>{};
|
||||
|
||||
final props = rawProps
|
||||
.map((kv) {
|
||||
final prop = XmpProp(kv.key, kv.value);
|
||||
if (extractData(prop)) return null;
|
||||
|
||||
if (prop.isOpenable) {
|
||||
linkHandlers.putIfAbsent(
|
||||
prop.displayKey,
|
||||
() => InfoLinkHandler(linkText: 'Open', onTap: () => openEmbeddedData(prop.path)),
|
||||
);
|
||||
}
|
||||
return prop;
|
||||
return extractData(prop) ? null : prop;
|
||||
})
|
||||
.where((e) => e != null)
|
||||
.toList()
|
||||
|
@ -74,7 +31,7 @@ class XmpNamespace {
|
|||
InfoRowGroup(
|
||||
Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))),
|
||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||
linkHandlers: linkHandlers,
|
||||
linkHandlers: linkifyValues(props),
|
||||
),
|
||||
...buildFromExtractedData(),
|
||||
];
|
||||
|
@ -123,6 +80,8 @@ class XmpNamespace {
|
|||
|
||||
String formatValue(XmpProp prop) => prop.value;
|
||||
|
||||
Map<String, InfoLinkHandler> linkifyValues(List<XmpProp> props) => null;
|
||||
|
||||
// identity
|
||||
|
||||
@override
|
||||
|
@ -139,3 +98,48 @@ class XmpNamespace {
|
|||
return '$runtimeType#${shortHash(this)}{namespace=$namespace}';
|
||||
}
|
||||
}
|
||||
|
||||
class XmpProp {
|
||||
final String path, value;
|
||||
final String displayKey;
|
||||
|
||||
static final sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
|
||||
static final sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])');
|
||||
|
||||
XmpProp(this.path, this.value) : displayKey = formatKey(path);
|
||||
|
||||
static String formatKey(String propPath) {
|
||||
return propPath.splitMapJoin(XMP.structFieldSeparator,
|
||||
onMatch: (match) => ' ${match.group(0)} ',
|
||||
onNonMatch: (s) {
|
||||
// strip namespace
|
||||
var key = s.split(XMP.propNamespaceSeparator).last;
|
||||
|
||||
// uppercase first letter
|
||||
key = key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
|
||||
|
||||
// sentence case
|
||||
return key.replaceAllMapped(sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType#${shortHash(this)}{path=$path, value=$value}';
|
||||
}
|
||||
}
|
||||
|
||||
class OpenEmbeddedDataNotification extends Notification {
|
||||
final String propPath;
|
||||
final String mimeType;
|
||||
|
||||
const OpenEmbeddedDataNotification({
|
||||
@required this.propPath,
|
||||
@required this.mimeType,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType#${shortHash(this)}{propPath=$propPath, mimeType=$mimeType}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ class XmpExifNamespace extends XmpNamespace {
|
|||
|
||||
XmpExifNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
String get displayTitle => 'Exif';
|
||||
|
||||
@override
|
||||
String formatValue(XmpProp prop) {
|
||||
final v = prop.value;
|
||||
|
|
69
lib/widgets/fullscreen/info/metadata/xmp_ns/google.dart
Normal file
69
lib/widgets/fullscreen/info/metadata/xmp_ns/google.dart
Normal file
|
@ -0,0 +1,69 @@
|
|||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||
XmpGoogleNamespace(String ns) : super(ns);
|
||||
|
||||
List<Tuple2<String, String>> get dataProps;
|
||||
|
||||
@override
|
||||
Map<String, InfoLinkHandler> linkifyValues(List<XmpProp> props) {
|
||||
return Map.fromEntries(dataProps.map((t) {
|
||||
final dataPropPath = t.item1;
|
||||
final mimePropPath = t.item2;
|
||||
final dataProp = props.firstWhere((prop) => prop.path == dataPropPath, orElse: () => null);
|
||||
final mimeProp = props.firstWhere((prop) => prop.path == mimePropPath, orElse: () => null);
|
||||
return (dataProp != null && mimeProp != null)
|
||||
? MapEntry(
|
||||
dataProp.displayKey,
|
||||
InfoLinkHandler(
|
||||
linkText: 'Open',
|
||||
onTap: (context) => OpenEmbeddedDataNotification(
|
||||
propPath: dataProp.path,
|
||||
mimeType: mimeProp.value,
|
||||
).dispatch(context),
|
||||
))
|
||||
: null;
|
||||
}).where((e) => e != null));
|
||||
}
|
||||
}
|
||||
|
||||
class XmpGAudioNamespace extends XmpGoogleNamespace {
|
||||
static const ns = 'GAudio';
|
||||
|
||||
XmpGAudioNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
List<Tuple2<String, String>> get dataProps => [Tuple2('$ns:Data', '$ns:Mime')];
|
||||
|
||||
@override
|
||||
String get displayTitle => 'Google Audio';
|
||||
}
|
||||
|
||||
class XmpGDepthNamespace extends XmpGoogleNamespace {
|
||||
static const ns = 'GDepth';
|
||||
|
||||
XmpGDepthNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
List<Tuple2<String, String>> get dataProps => [
|
||||
Tuple2('$ns:Data', '$ns:Mime'),
|
||||
Tuple2('$ns:Confidence', '$ns:ConfidenceMime'),
|
||||
];
|
||||
|
||||
@override
|
||||
String get displayTitle => 'Google Depth';
|
||||
}
|
||||
|
||||
class XmpGImageNamespace extends XmpGoogleNamespace {
|
||||
static const ns = 'GImage';
|
||||
|
||||
XmpGImageNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
List<Tuple2<String, String>> get dataProps => [Tuple2('$ns:Data', '$ns:Mime')];
|
||||
|
||||
@override
|
||||
String get displayTitle => 'Google Image';
|
||||
}
|
|
@ -11,6 +11,9 @@ class XmpIptcCoreNamespace extends XmpNamespace {
|
|||
|
||||
XmpIptcCoreNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
String get displayTitle => 'IPTC Core';
|
||||
|
||||
@override
|
||||
bool extractData(XmpProp prop) => extractStruct(prop, creatorContactInfoPattern, creatorContactInfo);
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ class XmpPhotoshopNamespace extends XmpNamespace {
|
|||
|
||||
XmpPhotoshopNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
String get displayTitle => 'Photoshop';
|
||||
|
||||
@override
|
||||
String formatValue(XmpProp prop) {
|
||||
final value = prop.value;
|
||||
|
|
|
@ -5,6 +5,9 @@ import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
|
|||
class XmpTiffNamespace extends XmpNamespace {
|
||||
static const ns = 'tiff';
|
||||
|
||||
@override
|
||||
String get displayTitle => 'TIFF';
|
||||
|
||||
XmpTiffNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -6,11 +8,15 @@ class XmpBasicNamespace extends XmpNamespace {
|
|||
static const ns = 'xmp';
|
||||
|
||||
static final thumbnailsPattern = RegExp(r'xmp:Thumbnails\[(\d+)\]/(.*)');
|
||||
static const thumbnailDataDisplayKey = 'Image';
|
||||
|
||||
final thumbnails = <int, Map<String, String>>{};
|
||||
|
||||
XmpBasicNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
String get displayTitle => 'Basic';
|
||||
|
||||
@override
|
||||
bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails);
|
||||
|
||||
|
@ -20,6 +26,19 @@ class XmpBasicNamespace extends XmpNamespace {
|
|||
XmpStructArrayCard(
|
||||
title: 'Thumbnail',
|
||||
structByIndex: thumbnails,
|
||||
linkifier: (index) {
|
||||
final struct = thumbnails[index];
|
||||
return {
|
||||
if (struct.containsKey(thumbnailDataDisplayKey))
|
||||
thumbnailDataDisplayKey: InfoLinkHandler(
|
||||
linkText: 'Open',
|
||||
onTap: (context) => OpenEmbeddedDataNotification(
|
||||
propPath: 'xmp:Thumbnails[$index]/xmpGImg:image',
|
||||
mimeType: MimeTypes.jpeg,
|
||||
).dispatch(context),
|
||||
),
|
||||
};
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
|
@ -39,7 +58,14 @@ class XmpMMNamespace extends XmpNamespace {
|
|||
XmpMMNamespace() : super(ns);
|
||||
|
||||
@override
|
||||
bool extractData(XmpProp prop) => extractStruct(prop, derivedFromPattern, derivedFrom) || extractIndexedStruct(prop, historyPattern, history);
|
||||
String get displayTitle => 'Media Management';
|
||||
|
||||
@override
|
||||
bool extractData(XmpProp prop) {
|
||||
final hasStructs = extractStruct(prop, derivedFromPattern, derivedFrom);
|
||||
final hasIndexedStructs = extractIndexedStruct(prop, historyPattern, history);
|
||||
return hasStructs || hasIndexedStructs;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildFromExtractedData() => [
|
||||
|
|
|
@ -11,10 +11,12 @@ import 'package:flutter/material.dart';
|
|||
class XmpStructArrayCard extends StatefulWidget {
|
||||
final String title;
|
||||
final List<Map<String, String>> structs = [];
|
||||
final Map<String, InfoLinkHandler> Function(int index) linkifier;
|
||||
|
||||
XmpStructArrayCard({
|
||||
@required this.title,
|
||||
@required Map<int, Map<String, String>> structByIndex,
|
||||
this.linkifier,
|
||||
}) {
|
||||
structs.length = structByIndex.keys.fold(0, max);
|
||||
structByIndex.keys.forEach((index) => structs[index - 1] = structByIndex[index]);
|
||||
|
@ -89,6 +91,7 @@ class _XmpStructArrayCardState extends State<XmpStructArrayCard> {
|
|||
child: InfoRowGroup(
|
||||
structs[_index],
|
||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||
linkHandlers: widget.linkifier?.call(_index + 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -101,12 +104,14 @@ class _XmpStructArrayCardState extends State<XmpStructArrayCard> {
|
|||
class XmpStructCard extends StatelessWidget {
|
||||
final String title;
|
||||
final Map<String, String> struct;
|
||||
final Map<String, InfoLinkHandler> Function() linkifier;
|
||||
|
||||
static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0);
|
||||
|
||||
const XmpStructCard({
|
||||
@required this.title,
|
||||
@required this.struct,
|
||||
this.linkifier,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -126,6 +131,7 @@ class XmpStructCard extends StatelessWidget {
|
|||
InfoRowGroup(
|
||||
struct,
|
||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||
linkHandlers: linkifier?.call(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -10,9 +10,9 @@ import 'package:aves/widgets/common/behaviour/routes.dart';
|
|||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/exif.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/google.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/iptc.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/photoshop.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/tiff.dart';
|
||||
|
@ -41,7 +41,6 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry);
|
||||
final sections = SplayTreeMap<XmpNamespace, List<MapEntry<String, String>>>.of(
|
||||
groupBy(widget.tags.entries, (kv) {
|
||||
final fullKey = kv.key;
|
||||
|
@ -52,6 +51,12 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
|
|||
return XmpBasicNamespace();
|
||||
case XmpExifNamespace.ns:
|
||||
return XmpExifNamespace();
|
||||
case XmpGAudioNamespace.ns:
|
||||
return XmpGAudioNamespace();
|
||||
case XmpGDepthNamespace.ns:
|
||||
return XmpGDepthNamespace();
|
||||
case XmpGImageNamespace.ns:
|
||||
return XmpGImageNamespace();
|
||||
case XmpIptcCoreNamespace.ns:
|
||||
return XmpIptcCoreNamespace();
|
||||
case XmpMMNamespace.ns:
|
||||
|
@ -72,25 +77,29 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
|
|||
title: 'XMP',
|
||||
expandedNotifier: widget.expandedNotifier,
|
||||
children: [
|
||||
if (thumbnail != null) thumbnail,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: sections.entries
|
||||
.expand((kv) => kv.key.buildNamespaceSection(
|
||||
rawProps: kv.value,
|
||||
openEmbeddedData: _openEmbeddedData,
|
||||
))
|
||||
.toList(),
|
||||
NotificationListener<OpenEmbeddedDataNotification>(
|
||||
onNotification: (notification) {
|
||||
_openEmbeddedData(notification.propPath, notification.mimeType);
|
||||
return true;
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: sections.entries
|
||||
.expand((kv) => kv.key.buildNamespaceSection(
|
||||
rawProps: kv.value,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openEmbeddedData(String propPath) async {
|
||||
final fields = await MetadataService.extractXmpDataProp(entry, propPath);
|
||||
Future<void> _openEmbeddedData(String propPath, String propMimeType) async {
|
||||
final fields = await MetadataService.extractXmpDataProp(entry, propPath, propMimeType);
|
||||
if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) {
|
||||
showFeedback(context, 'Failed');
|
||||
return;
|
||||
|
|
Loading…
Reference in a new issue