From d831146135af216668dbe0f397040d80eae4d253 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 5 Aug 2019 00:17:02 +0900 Subject: [PATCH] info: google map & xmp tags --- android/app/src/main/AndroidManifest.xml | 2 + .../aves/channelhandlers/MetadataHandler.java | 126 +++++++++++++----- lib/model/metadata_service.dart | 45 +++++-- lib/widgets/fullscreen/info_page.dart | 99 +++++++++++--- pubspec.lock | 7 + pubspec.yaml | 1 + 6 files changed, 217 insertions(+), 63 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f2a7a0ff1..d1bd0a311 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ android:name="io.flutter.app.FlutterApplication" android:label="Aves" android:icon="@mipmap/ic_launcher"> + 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 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 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); + } + } } \ No newline at end of file diff --git a/lib/model/metadata_service.dart b/lib/model/metadata_service.dart index 9a37dfa2f..624faa928 100644 --- a/lib/model/metadata_service.dart +++ b/lib/model/metadata_service.dart @@ -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 getOverlayMetadata(String path) async { - try { - final result = await platform.invokeMethod('getOverlayMetadata', { - 'path': path, - }); - return result as Map; - } on PlatformException catch (e) { - debugPrint('getOverlayMetadata failed with exception=${e.message}'); - } - return Map(); - } - - // return Map> + // return Map> (map of directories, each directory being a map of metadata label and value description) static Future getAllMetadata(String path) async { try { final result = await platform.invokeMethod('getAllMetadata', { @@ -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 getCatalogMetadata(String path) async { + try { + final result = await platform.invokeMethod('getCatalogMetadata', { + '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 getOverlayMetadata(String path) async { + try { + final result = await platform.invokeMethod('getOverlayMetadata', { + 'path': path, + }); + return result as Map; + } on PlatformException catch (e) { + debugPrint('getOverlayMetadata failed with exception=${e.message}'); + } + return Map(); + } } diff --git a/lib/widgets/fullscreen/info_page.dart b/lib/widgets/fullscreen/info_page.dart index b762f2da6..ab269c9c4 100644 --- a/lib/widgets/fullscreen/info_page.dart +++ b/lib/widgets/fullscreen/info_page.dart @@ -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 { - Future _metadataLoader; + Future _catalogLoader, _metadataLoader; bool _scrollStartFromTop = false; ImageEntry get entry => widget.entry; @@ -32,6 +35,7 @@ class InfoPageState extends State { } initMetadataLoader() { + _catalogLoader = MetadataService.getCatalogMetadata(entry.path); _metadataLoader = MetadataService.getAllMetadata(entry.path); } @@ -72,41 +76,55 @@ class InfoPageState extends State { 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 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 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(); 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 { ), ); } + + List _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 _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 { diff --git a/pubspec.lock b/pubspec.lock index db0902693..befefe907 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 17be43d36..3d92c9bd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: chewie: collection: flutter_sticky_header: + google_maps_flutter: intl: photo_view: screen: