info: get all metadata
This commit is contained in:
parent
8759987dd7
commit
9c8df80a48
7 changed files with 248 additions and 73 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
32
lib/model/metadata_service.dart
Normal file
32
lib/model/metadata_service.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue