info: get all metadata

This commit is contained in:
Thibault Deckers 2019-07-28 20:09:10 +09:00
parent 8759987dd7
commit 9c8df80a48
7 changed files with 248 additions and 73 deletions

View file

@ -5,6 +5,7 @@ import android.os.Bundle;
import deckers.thibault.aves.channelhandlers.AppAdapterHandler; import deckers.thibault.aves.channelhandlers.AppAdapterHandler;
import deckers.thibault.aves.channelhandlers.ImageDecodeHandler; import deckers.thibault.aves.channelhandlers.ImageDecodeHandler;
import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler; import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler;
import deckers.thibault.aves.channelhandlers.MetadataHandler;
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
@ -22,6 +23,7 @@ public class MainActivity extends FlutterActivity {
FlutterView messenger = getFlutterView(); FlutterView messenger = getFlutterView();
new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this)); new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this));
new MethodChannel(messenger, ImageDecodeHandler.CHANNEL).setMethodCallHandler(new ImageDecodeHandler(this, mediaStoreStreamHandler)); new MethodChannel(messenger, ImageDecodeHandler.CHANNEL).setMethodCallHandler(new ImageDecodeHandler(this, mediaStoreStreamHandler));
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler());
new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler); new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler);
} }
} }

View file

@ -8,9 +8,6 @@ import android.net.Uri;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.karumi.dexter.Dexter; import com.karumi.dexter.Dexter;
import com.karumi.dexter.PermissionToken; import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionDeniedResponse; import com.karumi.dexter.listener.PermissionDeniedResponse;
@ -18,10 +15,6 @@ import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.PermissionRequest; import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.single.PermissionListener; import com.karumi.dexter.listener.single.PermissionListener;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.model.ImageEntry;
@ -65,10 +58,6 @@ public class ImageDecodeHandler implements MethodChannel.MethodCallHandler {
result.success(null); result.success(null);
break; break;
} }
case "getOverlayMetadata":
String path = call.argument("path");
getOverlayMetadata(result, path);
break;
default: default:
result.notImplemented(); result.notImplemented();
break; break;
@ -124,31 +113,4 @@ public class ImageDecodeHandler implements MethodChannel.MethodCallHandler {
} }
}).check(); }).check();
} }
private void getOverlayMetadata(MethodChannel.Result result, String 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 (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

@ -0,0 +1,112 @@
package deckers.thibault.aves.channelhandlers;
import android.support.annotation.NonNull;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPIterator;
import com.adobe.xmp.XMPMeta;
import com.adobe.xmp.properties.XMPPropertyInfo;
import com.drew.imaging.ImageMetadataReader;
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.xmp.XmpDirectory;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class MetadataHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/metadata";
@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;
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 (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)) {
Map<String, Map<String, String>> metadataMap = new HashMap<>();
Metadata metadata = ImageMetadataReader.readMetadata(is);
for (Directory dir : metadata.getDirectories()) {
if (dir.getTagCount() > 0) {
Map<String, String> dirMap = new HashMap<>();
// directory name
metadataMap.put(dir.getName(), dirMap);
// tags
for (Tag tag : dir.getTags()) {
dirMap.put(tag.getTagName(), tag.getDescription());
}
if (dir instanceof XmpDirectory) {
try {
XmpDirectory xmpDir = (XmpDirectory) dir;
XMPMeta xmpMeta = xmpDir.getXMPMeta();
xmpMeta.sort();
XMPIterator xmpIterator = xmpMeta.iterator();
while (xmpIterator.hasNext()) {
XMPPropertyInfo prop = (XMPPropertyInfo) xmpIterator.next();
String xmpPath = prop.getPath();
String xmpValue = prop.getValue();
if (xmpPath != null && !xmpPath.isEmpty() && xmpValue != null && !xmpValue.isEmpty()) {
dirMap.put(xmpPath, xmpValue);
}
}
} catch (XMPException e) {
e.printStackTrace();
}
}
}
}
result.success(metadataMap);
} catch (FileNotFoundException e) {
result.error("getAllMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
} catch (Exception e) {
result.error("getAllMetadata-exception", "failed to get metadata for path=" + path, e);
}
}
}

View file

@ -41,17 +41,4 @@ class ImageDecodeService {
debugPrint('cancelGetImageBytes failed with exception=${e.message}'); debugPrint('cancelGetImageBytes failed with exception=${e.message}');
} }
} }
// 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();
}
} }

View file

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
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>>
static Future<Map> getAllMetadata(String path) async {
try {
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
'path': path,
});
return result as Map;
} on PlatformException catch (e) {
debugPrint('getAllMetadata failed with exception=${e.message}');
}
return Map();
}
}

View file

@ -1,13 +1,39 @@
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/file_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class InfoPage extends StatelessWidget { class InfoPage extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
const InfoPage({this.entry}); const InfoPage({this.entry});
@override
State<StatefulWidget> createState() => InfoPageState();
}
class InfoPageState extends State<InfoPage> {
Future<Map> _metadataLoader;
ImageEntry get entry => widget.entry;
@override
void initState() {
super.initState();
initMetadataLoader();
}
@override
void didUpdateWidget(InfoPage oldWidget) {
super.didUpdateWidget(oldWidget);
initMetadataLoader();
}
initMetadataLoader() {
_metadataLoader = MetadataService.getAllMetadata(entry.path);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final date = entry.getBestDate(); final date = entry.getBestDate();
@ -15,18 +41,70 @@ class InfoPage extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text('Info'), title: Text('Info'),
), ),
body: Padding( body: ListView(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, SectionRow('File'),
children: [ InfoRow('Title', entry.title),
InfoRow('Title', entry.title), InfoRow('Date', '${DateFormat.yMMMd().format(date)} ${DateFormat.Hm().format(date)}'),
InfoRow('Date', '${DateFormat.yMMMd().format(date)} ${DateFormat.Hm().format(date)}'), InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'),
InfoRow('Size', formatFilesize(entry.sizeBytes)), InfoRow('Size', formatFilesize(entry.sizeBytes)),
InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'), InfoRow('Path', entry.path),
InfoRow('Path', entry.path), SectionRow('Location'),
], SectionRow('XMP Tags'),
), SectionRow('Metadata'),
FutureBuilder(
future: _metadataLoader,
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.connectionState != ConnectionState.done) {
return CircularProgressIndicator();
}
final metadataMap = snapshot.data.cast<String, Map>();
final directoryNames = metadataMap.keys.toList()..sort();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: directoryNames.expand(
(directoryName) {
final directory = metadataMap[directoryName];
final tagKeys = directory.keys.toList()..sort();
return [
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());
},
),
],
),
);
}
}
class SectionRow extends StatelessWidget {
final String title;
const SectionRow(this.title);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Expanded(child: Divider(color: Colors.white70)),
SizedBox(width: 8),
Text(title, style: TextStyle(fontSize: 18)),
SizedBox(width: 8),
Expanded(child: Divider(color: Colors.white70)),
],
), ),
); );
} }
@ -39,15 +117,17 @@ class InfoRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Wrap( return Padding(
children: [ padding: EdgeInsets.symmetric(vertical: 4.0),
Text( child: RichText(
label, text: TextSpan(
style: TextStyle(color: Colors.white70), style: DefaultTextStyle.of(context).style,
children: [
TextSpan(text: '$label ', style: TextStyle(color: Colors.white70)),
TextSpan(text: value),
],
), ),
SizedBox(width: 8), ),
Text(value),
],
); );
} }
} }

View file

@ -2,7 +2,7 @@ import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/android_app_service.dart'; import 'package:aves/model/android_app_service.dart';
import 'package:aves/model/image_decode_service.dart'; import 'package:aves/model/metadata_service.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -90,7 +90,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
} }
initDetailLoader() { initDetailLoader() {
_detailLoader = ImageDecodeService.getOverlayMetadata(entry.path); _detailLoader = MetadataService.getOverlayMetadata(entry.path);
} }
@override @override