debug: ExifInterface metadata list

This commit is contained in:
Thibault Deckers 2020-10-01 10:01:11 +09:00
parent b9ffc2d5f0
commit bbc9d69ba5
4 changed files with 238 additions and 28 deletions

View file

@ -12,6 +12,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import com.adobe.internal.xmp.XMPException;
import com.adobe.internal.xmp.XMPIterator;
@ -190,6 +191,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
case "getContentResolverMetadata":
new Thread(() -> getContentResolverMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getExifInterfaceMetadata":
new Thread(() -> getExifInterfaceMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getEmbeddedPictures":
new Thread(() -> getEmbeddedPictures(call, new MethodResultWrapper(result))).start();
break;
@ -533,6 +537,27 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
}
}
private void getExifInterfaceMetadata(MethodCall call, MethodChannel.Result result) {
String uriString = call.argument("uri");
if (uriString == null) {
result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null);
return;
}
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uriString))) {
ExifInterface exif = new ExifInterface(is);
Map<String, Object> metadataMap = new HashMap<>();
for (String tag : MetadataHelper.ExifInterfaceTags) {
if (exif.hasAttribute(tag)) {
metadataMap.put(tag, exif.getAttribute(tag));
}
}
result.success(metadataMap);
} catch (IOException e) {
result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=" + uriString, e.getMessage());
}
}
private void getEmbeddedPictures(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Uri uri = Uri.parse(call.argument("uri"));
List<byte[]> pictures = new ArrayList<>();
@ -544,7 +569,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
pictures.add(picture);
}
} catch (Exception e) {
result.error("getVideoEmbeddedPictures-failure", "failed to get embedded picture for uri=" + uri, e);
result.error("getVideoEmbeddedPictures-failure", "failed to get embedded picture for uri=" + uri, e.getMessage());
} finally {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release();

View file

@ -1,20 +1,183 @@
package deckers.thibault.aves.utils
import androidx.exifinterface.media.ExifInterface
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
object MetadataHelper {
// list of known tags from ExifInterface as of androidx.exifinterface:exifinterface:1.3.0
@JvmField
val ExifInterfaceTags = arrayOf(
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_ARTIST,
ExifInterface.TAG_BITS_PER_SAMPLE,
ExifInterface.TAG_BODY_SERIAL_NUMBER,
ExifInterface.TAG_BRIGHTNESS_VALUE,
ExifInterface.TAG_CAMERA_OWNER_NAME,
ExifInterface.TAG_CFA_PATTERN,
ExifInterface.TAG_COLOR_SPACE,
ExifInterface.TAG_COMPONENTS_CONFIGURATION,
ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
ExifInterface.TAG_COMPRESSION,
ExifInterface.TAG_CONTRAST,
ExifInterface.TAG_COPYRIGHT,
ExifInterface.TAG_CUSTOM_RENDERED,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_DEFAULT_CROP_SIZE,
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
ExifInterface.TAG_DNG_VERSION,
ExifInterface.TAG_EXIF_VERSION,
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
ExifInterface.TAG_EXPOSURE_INDEX,
ExifInterface.TAG_EXPOSURE_MODE,
ExifInterface.TAG_EXPOSURE_PROGRAM,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FILE_SOURCE,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FLASHPIX_VERSION,
ExifInterface.TAG_FLASH_ENERGY,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_GAIN_CONTROL,
ExifInterface.TAG_GAMMA,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_AREA_INFORMATION,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_DEST_BEARING,
ExifInterface.TAG_GPS_DEST_BEARING_REF,
ExifInterface.TAG_GPS_DEST_DISTANCE,
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
ExifInterface.TAG_GPS_DEST_LATITUDE,
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
ExifInterface.TAG_GPS_DEST_LONGITUDE,
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
ExifInterface.TAG_GPS_DIFFERENTIAL,
ExifInterface.TAG_GPS_DOP,
ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
ExifInterface.TAG_GPS_IMG_DIRECTION,
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_MAP_DATUM,
ExifInterface.TAG_GPS_MEASURE_MODE,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_SATELLITES,
ExifInterface.TAG_GPS_SPEED,
ExifInterface.TAG_GPS_SPEED_REF,
ExifInterface.TAG_GPS_STATUS,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_TRACK,
ExifInterface.TAG_GPS_TRACK_REF,
ExifInterface.TAG_GPS_VERSION_ID,
ExifInterface.TAG_IMAGE_DESCRIPTION,
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_UNIQUE_ID,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_INTEROPERABILITY_INDEX,
ExifInterface.TAG_ISO_SPEED,
ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
ExifInterface.TAG_LENS_MAKE,
ExifInterface.TAG_LENS_MODEL,
ExifInterface.TAG_LENS_SERIAL_NUMBER,
ExifInterface.TAG_LENS_SPECIFICATION,
ExifInterface.TAG_LIGHT_SOURCE,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MAKER_NOTE,
ExifInterface.TAG_MAX_APERTURE_VALUE,
ExifInterface.TAG_METERING_MODE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_NEW_SUBFILE_TYPE,
ExifInterface.TAG_OECF,
ExifInterface.TAG_OFFSET_TIME,
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
ExifInterface.TAG_ORF_ASPECT_FRAME,
ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH,
ExifInterface.TAG_ORF_PREVIEW_IMAGE_START,
ExifInterface.TAG_ORF_THUMBNAIL_IMAGE,
ExifInterface.TAG_ORIENTATION,
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
ExifInterface.TAG_PIXEL_X_DIMENSION,
ExifInterface.TAG_PIXEL_Y_DIMENSION,
ExifInterface.TAG_PLANAR_CONFIGURATION,
ExifInterface.TAG_PRIMARY_CHROMATICITIES,
ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
ExifInterface.TAG_REFERENCE_BLACK_WHITE,
ExifInterface.TAG_RELATED_SOUND_FILE,
ExifInterface.TAG_RESOLUTION_UNIT,
ExifInterface.TAG_ROWS_PER_STRIP,
ExifInterface.TAG_RW2_ISO,
ExifInterface.TAG_RW2_JPG_FROM_RAW,
ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER,
ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER,
ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER,
ExifInterface.TAG_RW2_SENSOR_TOP_BORDER,
ExifInterface.TAG_SAMPLES_PER_PIXEL,
ExifInterface.TAG_SATURATION,
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
ExifInterface.TAG_SCENE_TYPE,
ExifInterface.TAG_SENSING_METHOD,
ExifInterface.TAG_SENSITIVITY_TYPE,
ExifInterface.TAG_SHARPNESS,
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
ExifInterface.TAG_SOFTWARE,
ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
ExifInterface.TAG_SPECTRAL_SENSITIVITY,
ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
ExifInterface.TAG_STRIP_BYTE_COUNTS,
ExifInterface.TAG_STRIP_OFFSETS,
ExifInterface.TAG_SUBFILE_TYPE,
ExifInterface.TAG_SUBJECT_AREA,
ExifInterface.TAG_SUBJECT_DISTANCE,
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
ExifInterface.TAG_SUBJECT_LOCATION,
ExifInterface.TAG_SUBSEC_TIME,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
ExifInterface.TAG_TRANSFER_FUNCTION,
ExifInterface.TAG_USER_COMMENT,
ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.TAG_WHITE_POINT,
ExifInterface.TAG_XMP,
ExifInterface.TAG_X_RESOLUTION,
ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
ExifInterface.TAG_Y_CB_CR_POSITIONING,
ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
ExifInterface.TAG_Y_RESOLUTION,
)
// interpret EXIF code to angle (0, 90, 180 or 270 degrees)
@JvmStatic
fun getOrientationDegreesForExifCode(exifOrientation: Int): Int = when (exifOrientation) {
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_ROTATE_90 -> 90
ExifInterface.ORIENTATION_ROTATE_270 -> 270
else -> 0 // all other orientations (regular, flipped...) default to an angle of 0 degree
fun getRotationDegreesForExifCode(exifOrientation: Int): Int = when (exifOrientation) {
ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSVERSE -> 90
ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL -> 180
ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSPOSE -> 270
else -> 0
}
// interpret EXIF code to whether the image is flipped
@JvmStatic
fun isFlippedForExifCode(exifOrientation: Int): Boolean = when (exifOrientation) {
ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_TRANSPOSE -> true
else -> false
}
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?

View file

@ -89,6 +89,19 @@ class MetadataService {
return {};
}
static Future<Map> getExifInterfaceMetadata(ImageEntry entry) async {
try {
// return map with all data available from the ExifInterface library
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
'uri': entry.uri,
}) as Map;
return result;
} on PlatformException catch (e) {
debugPrint('getExifInterfaceMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
}
return {};
}
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
try {
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{

View file

@ -9,6 +9,7 @@ import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/settings/settings_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart';
@ -28,7 +29,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
Future<DateMetadata> _dbDateLoader;
Future<CatalogMetadata> _dbMetadataLoader;
Future<AddressDetails> _dbAddressLoader;
Future<Map> _contentResolverMetadataLoader;
Future<Map> _contentResolverMetadataLoader, _exifInterfaceMetadataLoader;
ImageEntry get entry => widget.entry;
@ -259,13 +260,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
static const millisecondTimestampKeys = ['datetaken', 'datetime'];
Widget _buildContentResolverTabView() {
return ListView(
padding: EdgeInsets.all(16),
children: [
Text('Content Resolver (Media Store):'),
FutureBuilder<Map>(
future: _contentResolverMetadataLoader,
builder: (context, snapshot) {
Widget builder(BuildContext context, AsyncSnapshot<Map> snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final data = SplayTreeMap.of(snapshot.data.map((k, v) {
@ -283,7 +278,20 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
return MapEntry(key, value);
}));
return InfoRowGroup(data);
},
}
return ListView(
padding: EdgeInsets.all(16),
children: [
SectionTitle('Content Resolver (Media Store)'),
FutureBuilder<Map>(
future: _contentResolverMetadataLoader,
builder: builder,
),
SectionTitle('Exif Interface'),
FutureBuilder<Map>(
future: _exifInterfaceMetadataLoader,
builder: builder,
),
],
);
@ -294,6 +302,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
_dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
_dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
setState(() {});
}
}