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.ImageDecodeHandler;
import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler;
import deckers.thibault.aves.channelhandlers.MetadataHandler;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
@ -22,6 +23,7 @@ public class MainActivity extends FlutterActivity {
FlutterView messenger = getFlutterView();
new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this));
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);
}
}

View file

@ -8,9 +8,6 @@ import android.net.Uri;
import android.provider.Settings;
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.PermissionToken;
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.single.PermissionListener;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import deckers.thibault.aves.model.ImageEntry;
@ -65,10 +58,6 @@ public class ImageDecodeHandler implements MethodChannel.MethodCallHandler {
result.success(null);
break;
}
case "getOverlayMetadata":
String path = call.argument("path");
getOverlayMetadata(result, path);
break;
default:
result.notImplemented();
break;
@ -124,31 +113,4 @@ public class ImageDecodeHandler implements MethodChannel.MethodCallHandler {
}
}).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}');
}
}
// 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/metadata_service.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class InfoPage extends StatelessWidget {
class InfoPage extends StatefulWidget {
final ImageEntry 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
Widget build(BuildContext context) {
final date = entry.getBestDate();
@ -15,18 +41,70 @@ class InfoPage extends StatelessWidget {
appBar: AppBar(
title: Text('Info'),
),
body: Padding(
body: ListView(
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoRow('Title', entry.title),
InfoRow('Date', '${DateFormat.yMMMd().format(date)} ${DateFormat.Hm().format(date)}'),
InfoRow('Size', formatFilesize(entry.sizeBytes)),
InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'),
InfoRow('Path', entry.path),
],
),
children: [
SectionRow('File'),
InfoRow('Title', entry.title),
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('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
Widget build(BuildContext context) {
return Wrap(
children: [
Text(
label,
style: TextStyle(color: Colors.white70),
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: RichText(
text: TextSpan(
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 '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:flutter/material.dart';
import 'package:intl/intl.dart';
@ -90,7 +90,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
}
initDetailLoader() {
_detailLoader = ImageDecodeService.getOverlayMetadata(entry.path);
_detailLoader = MetadataService.getOverlayMetadata(entry.path);
}
@override