info: google map & xmp tags

This commit is contained in:
Thibault Deckers 2019-08-05 00:17:02 +09:00
parent 53917de437
commit d831146135
6 changed files with 217 additions and 63 deletions

View file

@ -7,6 +7,8 @@
android:name="io.flutter.app.FlutterApplication"
android:label="Aves"
android:icon="@mipmap/ic_launcher">
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyDf-1dN6JivrQGKSmxAdxERLM2egOvzGWs"/>
<activity
android:name=".MainActivity"
android:launchMode="singleTop"

View file

@ -9,13 +9,16 @@ import androidx.annotation.NonNull;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPIterator;
import com.adobe.xmp.XMPMeta;
import com.adobe.xmp.properties.XMPProperty;
import com.adobe.xmp.properties.XMPPropertyInfo;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.lang.GeoLocation;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory;
import com.drew.metadata.xmp.XmpDirectory;
import java.io.FileInputStream;
@ -23,6 +26,7 @@ import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import deckers.thibault.aves.utils.Constants;
import io.flutter.plugin.common.MethodCall;
@ -31,6 +35,9 @@ import io.flutter.plugin.common.MethodChannel;
public class MetadataHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/metadata";
private static final String XMP_DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/";
private static final String XMP_SUBJECT_PROP_NAME = "dc:subject";
private Context context;
public MetadataHandler(Context context) {
@ -40,48 +47,21 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "getOverlayMetadata":
getOverlayMetadata(call, result);
break;
case "getAllMetadata":
getAllMetadata(call, result);
break;
case "getCatalogMetadata":
getCatalogMetadata(call, result);
break;
case "getOverlayMetadata":
getOverlayMetadata(call, result);
break;
default:
result.notImplemented();
break;
}
}
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
try (InputStream is = new FileInputStream(path)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
Map<String, String> metadataMap = new HashMap<>();
if (directory != null) {
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
}
}
result.success(metadataMap);
} catch (ImageProcessingException e) {
result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
} catch (FileNotFoundException e) {
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
} catch (Exception e) {
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
}
}
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
try (InputStream is = new FileInputStream(path)) {
@ -159,4 +139,84 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
result.error("getAllVideoMetadataFallback-exception", "failed to get metadata for path=" + path, e);
}
}
private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
try (InputStream is = new FileInputStream(path)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
Map<String, Object> metadataMap = new HashMap<>();
// EXIF Sub-IFD
ExifSubIFDDirectory exifSubDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (exifSubDir != null) {
if (exifSubDir.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) {
metadataMap.put("dateMillis", exifSubDir.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL, null, TimeZone.getDefault()).getTime());
}
}
// GPS
GpsDirectory gpsDir = metadata.getFirstDirectoryOfType(GpsDirectory.class);
if (gpsDir != null) {
GeoLocation geoLocation = gpsDir.getGeoLocation();
if (geoLocation != null) {
metadataMap.put("latitude", geoLocation.getLatitude());
metadataMap.put("longitude", geoLocation.getLongitude());
}
}
// XMP
XmpDirectory xmpDir = metadata.getFirstDirectoryOfType(XmpDirectory.class);
if (xmpDir != null) {
XMPMeta xmpMeta = xmpDir.getXMPMeta();
try {
if (xmpMeta.doesPropertyExist(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME)) {
StringBuilder sb = new StringBuilder();
int count = xmpMeta.countArrayItems(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME);
for (int i = 1; i < count + 1; i++) {
XMPProperty item = xmpMeta.getArrayItem(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME, i);
sb.append(" ").append(item.getValue());
}
metadataMap.put("keywords", sb.toString());
}
} catch (XMPException e) {
e.printStackTrace();
}
}
result.success(metadataMap);
} catch (FileNotFoundException e) {
result.error("getCatalogMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
} catch (Exception e) {
result.error("getCatalogMetadata-exception", "failed to get metadata for path=" + path, e);
}
}
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
try (InputStream is = new FileInputStream(path)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
Map<String, String> metadataMap = new HashMap<>();
if (directory != null) {
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
}
}
result.success(metadataMap);
} catch (ImageProcessingException e) {
result.error("getOverlayMetadata-imageprocessing", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
} catch (FileNotFoundException e) {
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
} catch (Exception e) {
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
}
}
}

View file

@ -4,20 +4,7 @@ import 'package:flutter/services.dart';
class MetadataService {
static const platform = const MethodChannel('deckers.thibault/aves/metadata');
// return map with: 'aperture' 'exposureTime' 'focalLength' 'iso'
static Future<Map> getOverlayMetadata(String path) async {
try {
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
'path': path,
});
return result as Map;
} on PlatformException catch (e) {
debugPrint('getOverlayMetadata failed with exception=${e.message}');
}
return Map();
}
// return Map<Map<Key, Value>>
// return Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
static Future<Map> getAllMetadata(String path) async {
try {
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
@ -29,4 +16,34 @@ class MetadataService {
}
return Map();
}
// return map with:
// 'dateMillis': date taken in milliseconds since Epoch (long)
// 'latitude': latitude (double)
// 'longitude': longitude (double)
// 'keywords': space separated XMP subjects (string)
static Future<Map> getCatalogMetadata(String path) async {
try {
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
'path': path,
});
return result as Map;
} on PlatformException catch (e) {
debugPrint('getCatalogMetadata failed with exception=${e.message}');
}
return Map();
}
// return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso'
static Future<Map> getOverlayMetadata(String path) async {
try {
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
'path': path,
});
return result as Map;
} on PlatformException catch (e) {
debugPrint('getOverlayMetadata failed with exception=${e.message}');
}
return Map();
}
}

View file

@ -1,7 +1,10 @@
import 'dart:async';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intl/intl.dart';
class InfoPage extends StatefulWidget {
@ -14,7 +17,7 @@ class InfoPage extends StatefulWidget {
}
class InfoPageState extends State<InfoPage> {
Future<Map> _metadataLoader;
Future<Map> _catalogLoader, _metadataLoader;
bool _scrollStartFromTop = false;
ImageEntry get entry => widget.entry;
@ -32,6 +35,7 @@ class InfoPageState extends State<InfoPage> {
}
initMetadataLoader() {
_catalogLoader = MetadataService.getCatalogMetadata(entry.path);
_metadataLoader = MetadataService.getAllMetadata(entry.path);
}
@ -72,41 +76,55 @@ class InfoPageState extends State<InfoPage> {
child: ListView(
padding: EdgeInsets.all(8.0),
children: [
SectionRow('File'),
InfoRow('Title', entry.title),
InfoRow('Date', dateText),
if (entry.isVideo) InfoRow('Duration', entry.durationText),
InfoRow('Resolution', resolutionText),
InfoRow('Size', formatFilesize(entry.sizeBytes)),
InfoRow('Path', entry.path),
SectionRow('Metadata'),
FutureBuilder(
future: _catalogLoader,
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
if (snapshot.hasError) return Text(snapshot.error);
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final metadata = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildLocationSection(metadata['latitude'], metadata['longitude']),
..._buildTagSection(metadata['keywords']),
],
);
},
),
FutureBuilder(
future: _metadataLoader,
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.connectionState != ConnectionState.done) {
return SizedBox.shrink();
}
if (snapshot.hasError) return Text(snapshot.error);
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final metadataMap = snapshot.data.cast<String, Map>();
final directoryNames = metadataMap.keys.toList()..sort();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: directoryNames.expand(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionRow('Metadata'),
...directoryNames.expand(
(directoryName) {
final directory = metadataMap[directoryName];
final tagKeys = directory.keys.toList()..sort();
return [
if (directoryName.isNotEmpty) Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text(directoryName, style: TextStyle(fontSize: 18)),
),
if (directoryName.isNotEmpty)
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text(directoryName, style: TextStyle(fontSize: 18)),
),
...tagKeys.map((tagKey) => InfoRow(tagKey, directory[tagKey])),
SizedBox(height: 16),
];
},
).toList());
)
],
);
},
),
],
@ -114,6 +132,55 @@ class InfoPageState extends State<InfoPage> {
),
);
}
List<Widget> _buildLocationSection(double latitude, double longitude) {
if (latitude == null || longitude == null) return [];
final latLng = LatLng(latitude, longitude);
return [
SectionRow('Location'),
SizedBox(
height: 200,
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(16),
),
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: latLng,
zoom: 12,
),
markers: [
Marker(
markerId: MarkerId(entry.path),
icon: BitmapDescriptor.defaultMarker,
position: latLng,
)
].toSet(),
),
),
),
];
}
List<Widget> _buildTagSection(String keywords) {
if (keywords == null) return [];
return [
SectionRow('XMP Tags'),
Wrap(
children: keywords
.split(' ')
.where((word) => word.isNotEmpty)
.map((word) => Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: Chip(
backgroundColor: Colors.indigo,
label: Text(word),
),
))
.toList(),
),
];
}
}
class SectionRow extends StatelessWidget {

View file

@ -60,6 +60,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
google_maps_flutter:
dependency: "direct main"
description:
name: google_maps_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.20+1"
intl:
dependency: "direct main"
description:

View file

@ -22,6 +22,7 @@ dependencies:
chewie:
collection:
flutter_sticky_header:
google_maps_flutter:
intl:
photo_view:
screen: