info: google map & xmp tags
This commit is contained in:
parent
53917de437
commit
d831146135
6 changed files with 217 additions and 63 deletions
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -22,6 +22,7 @@ dependencies:
|
|||
chewie:
|
||||
collection:
|
||||
flutter_sticky_header:
|
||||
google_maps_flutter:
|
||||
intl:
|
||||
photo_view:
|
||||
screen:
|
||||
|
|
Loading…
Reference in a new issue