rotate/flip improvements (WIP)
This commit is contained in:
parent
ff58b64773
commit
ae413dd82c
18 changed files with 477 additions and 380 deletions
|
@ -131,7 +131,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
ContentResolver resolver = activity.getContentResolver();
|
ContentResolver resolver = activity.getContentResolver();
|
||||||
Bitmap bitmap = resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
Bitmap bitmap = resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||||
String mimeType = entry.mimeType;
|
String mimeType = entry.mimeType;
|
||||||
if (MimeTypes.DNG.equals(mimeType)) {
|
if (MimeTypes.needRotationAfterContentResolverThumbnail(mimeType)) {
|
||||||
bitmap = rotateBitmap(bitmap, entry.rotationDegrees);
|
bitmap = rotateBitmap(bitmap, entry.rotationDegrees);
|
||||||
}
|
}
|
||||||
return bitmap;
|
return bitmap;
|
||||||
|
@ -186,7 +186,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
try {
|
try {
|
||||||
Bitmap bitmap = target.get();
|
Bitmap bitmap = target.get();
|
||||||
String mimeType = entry.mimeType;
|
String mimeType = entry.mimeType;
|
||||||
if (MimeTypes.DNG.equals(mimeType) || MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType)) {
|
if (MimeTypes.needRotationAfterGlide(mimeType)) {
|
||||||
bitmap = rotateBitmap(bitmap, entry.rotationDegrees);
|
bitmap = rotateBitmap(bitmap, entry.rotationDegrees);
|
||||||
}
|
}
|
||||||
return bitmap;
|
return bitmap;
|
||||||
|
|
|
@ -11,7 +11,6 @@ import android.text.format.Formatter;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.exifinterface.media.ExifInterface;
|
import androidx.exifinterface.media.ExifInterface;
|
||||||
|
|
||||||
import com.adobe.internal.xmp.XMPException;
|
import com.adobe.internal.xmp.XMPException;
|
||||||
|
@ -64,11 +63,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
// catalog metadata
|
// catalog metadata
|
||||||
private static final String KEY_MIME_TYPE = "mimeType";
|
private static final String KEY_MIME_TYPE = "mimeType";
|
||||||
private static final String KEY_DATE_MILLIS = "dateMillis";
|
private static final String KEY_DATE_MILLIS = "dateMillis";
|
||||||
private static final String KEY_IS_FLIPPED = "isFlipped";
|
|
||||||
private static final String KEY_IS_ANIMATED = "isAnimated";
|
private static final String KEY_IS_ANIMATED = "isAnimated";
|
||||||
|
private static final String KEY_IS_FLIPPED = "isFlipped";
|
||||||
|
private static final String KEY_ROTATION_DEGREES = "rotationDegrees";
|
||||||
private static final String KEY_LATITUDE = "latitude";
|
private static final String KEY_LATITUDE = "latitude";
|
||||||
private static final String KEY_LONGITUDE = "longitude";
|
private static final String KEY_LONGITUDE = "longitude";
|
||||||
private static final String KEY_VIDEO_ROTATION = "videoRotation";
|
|
||||||
private static final String KEY_XMP_SUBJECTS = "xmpSubjects";
|
private static final String KEY_XMP_SUBJECTS = "xmpSubjects";
|
||||||
private static final String KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription";
|
private static final String KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription";
|
||||||
|
|
||||||
|
@ -166,10 +165,6 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isVideo(@Nullable String mimeType) {
|
|
||||||
return mimeType != null && mimeType.startsWith(MimeTypes.VIDEO);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
|
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String mimeType = call.argument("mimeType");
|
String mimeType = call.argument("mimeType");
|
||||||
Uri uri = Uri.parse(call.argument("uri"));
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
@ -228,7 +223,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
if (MimeTypes.isVideo(mimeType)) {
|
||||||
Map<String, String> videoDir = getVideoAllMetadataByMediaMetadataRetriever(uri);
|
Map<String, String> videoDir = getVideoAllMetadataByMediaMetadataRetriever(uri);
|
||||||
if (!videoDir.isEmpty()) {
|
if (!videoDir.isEmpty()) {
|
||||||
metadataMap.put("Video", videoDir);
|
metadataMap.put("Video", videoDir);
|
||||||
|
@ -277,8 +272,8 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
Uri uri = Uri.parse(call.argument("uri"));
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
String extension = call.argument("extension");
|
String extension = call.argument("extension");
|
||||||
|
|
||||||
Map<String, Object> metadataMap = new HashMap<>(getCatalogMetadataByImageMetadataReader(uri, mimeType, extension));
|
Map<String, Object> metadataMap = new HashMap<>(getCatalogMetadataByMetadataExtractor(uri, mimeType, extension));
|
||||||
if (isVideo(mimeType)) {
|
if (MimeTypes.isVideo(mimeType)) {
|
||||||
metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri));
|
metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,11 +281,12 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> getCatalogMetadataByImageMetadataReader(Uri uri, String mimeType, String extension) {
|
private Map<String, Object> getCatalogMetadataByMetadataExtractor(Uri uri, String mimeType, String extension) {
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
if (!MimeTypes.isSupportedByMetadataExtractor(mimeType)) return metadataMap;
|
boolean foundExif = false;
|
||||||
|
|
||||||
|
if (MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
|
||||||
|
@ -313,9 +309,19 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXIF
|
// EXIF
|
||||||
putDateFromDirectoryTag(metadataMap, KEY_DATE_MILLIS, metadata, ExifSubIFDDirectory.class, ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
|
for (ExifSubIFDDirectory dir : metadata.getDirectoriesOfType(ExifSubIFDDirectory.class)) {
|
||||||
|
putDateFromTag(metadataMap, KEY_DATE_MILLIS, dir, ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
|
||||||
|
}
|
||||||
|
for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) {
|
||||||
|
foundExif = true;
|
||||||
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||||
putDateFromDirectoryTag(metadataMap, KEY_DATE_MILLIS, metadata, ExifIFD0Directory.class, ExifIFD0Directory.TAG_DATETIME);
|
putDateFromTag(metadataMap, KEY_DATE_MILLIS, dir, ExifIFD0Directory.TAG_DATETIME);
|
||||||
|
}
|
||||||
|
if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||||
|
int orientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
||||||
|
metadataMap.put(KEY_IS_FLIPPED, MetadataHelper.isFlippedForExifCode(orientation));
|
||||||
|
metadataMap.put(KEY_ROTATION_DEGREES, MetadataHelper.getRotationDegreesForExifCode(orientation));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GPS
|
// GPS
|
||||||
|
@ -363,6 +369,31 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
} catch (Exception | NoClassDefFoundError e) {
|
} catch (Exception | NoClassDefFoundError e) {
|
||||||
Log.w(LOG_TAG, "failed to get catalog metadata by metadata-extractor for uri=" + uri + ", mimeType=" + mimeType, e);
|
Log.w(LOG_TAG, "failed to get catalog metadata by metadata-extractor for uri=" + uri + ", mimeType=" + mimeType, e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundExif) {
|
||||||
|
// fallback to read EXIF via ExifInterface
|
||||||
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
|
ExifInterface exif = new ExifInterface(is);
|
||||||
|
|
||||||
|
// TODO TLAD get KEY_DATE_MILLIS from ExifInterface.TAG_DATETIME_ORIGINAL/TAG_DATETIME after Kotlin migration
|
||||||
|
if (exif.hasAttribute(ExifInterface.TAG_ORIENTATION)) {
|
||||||
|
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
|
||||||
|
if (orientation != 0) {
|
||||||
|
metadataMap.put(KEY_IS_FLIPPED, exif.isFlipped());
|
||||||
|
metadataMap.put(KEY_ROTATION_DEGREES, exif.getRotationDegrees());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double[] latLong = exif.getLatLong();
|
||||||
|
if (latLong != null && latLong.length == 2) {
|
||||||
|
metadataMap.put(KEY_LATITUDE, latLong[0]);
|
||||||
|
metadataMap.put(KEY_LONGITUDE, latLong[1]);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=" + uri, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return metadataMap;
|
return metadataMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,7 +414,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rotationString != null) {
|
if (rotationString != null) {
|
||||||
metadataMap.put(KEY_VIDEO_ROTATION, Integer.parseInt(rotationString));
|
metadataMap.put(KEY_ROTATION_DEGREES, Integer.parseInt(rotationString));
|
||||||
}
|
}
|
||||||
if (locationString != null) {
|
if (locationString != null) {
|
||||||
Matcher locationMatcher = VIDEO_LOCATION_PATTERN.matcher(locationString);
|
Matcher locationMatcher = VIDEO_LOCATION_PATTERN.matcher(locationString);
|
||||||
|
@ -420,7 +451,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
if (isVideo(mimeType) || !MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
|
if (MimeTypes.isVideo(mimeType) || !MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -462,9 +493,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
long id = ContentUris.parseId(uri);
|
long id = ContentUris.parseId(uri);
|
||||||
Uri contentUri = uri;
|
Uri contentUri = uri;
|
||||||
if (mimeType.startsWith(MimeTypes.IMAGE)) {
|
if (MimeTypes.isImage(mimeType)) {
|
||||||
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
||||||
} else if (mimeType.startsWith(MimeTypes.VIDEO)) {
|
} else if (MimeTypes.isVideo(mimeType)) {
|
||||||
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
@ -536,10 +567,10 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, String> metadataMap = new HashMap<>();
|
Map<String, String> metadataMap = new HashMap<>();
|
||||||
for (Map.Entry<String, Integer> kv : MediaMetadataRetrieverHelper.allKeys.entrySet()) {
|
for (Map.Entry<Integer, String> kv : MediaMetadataRetrieverHelper.allKeys.entrySet()) {
|
||||||
String value = retriever.extractMetadata(kv.getValue());
|
String value = retriever.extractMetadata(kv.getKey());
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
metadataMap.put(kv.getKey(), value);
|
metadataMap.put(kv.getValue(), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
|
@ -625,12 +656,6 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
// convenience methods
|
// convenience methods
|
||||||
|
|
||||||
private static <T extends Directory> void putDateFromDirectoryTag(Map<String, Object> metadataMap, String key, Metadata metadata, Class<T> dirClass, int tag) {
|
|
||||||
for (T dir : metadata.getDirectoriesOfType(dirClass)) {
|
|
||||||
putDateFromTag(metadataMap, key, dir, tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void putDateFromTag(Map<String, Object> metadataMap, String key, Directory dir, int tag) {
|
private static void putDateFromTag(Map<String, Object> metadataMap, String key, Directory dir, int tag) {
|
||||||
if (dir.containsTag(tag)) {
|
if (dir.containsTag(tag)) {
|
||||||
metadataMap.put(key, dir.getDate(tag, null, TimeZone.getDefault()).getTime());
|
metadataMap.put(key, dir.getDate(tag, null, TimeZone.getDefault()).getTime());
|
||||||
|
|
|
@ -16,8 +16,6 @@ import com.bumptech.glide.request.RequestOptions;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail;
|
import deckers.thibault.aves.decoder.VideoThumbnail;
|
||||||
|
@ -34,17 +32,6 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
private EventChannel.EventSink eventSink;
|
private EventChannel.EventSink eventSink;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
|
||||||
private static final List<String> flutterSupportedTypes = Arrays.asList(
|
|
||||||
MimeTypes.JPEG,
|
|
||||||
MimeTypes.PNG,
|
|
||||||
MimeTypes.GIF,
|
|
||||||
MimeTypes.WEBP,
|
|
||||||
MimeTypes.BMP,
|
|
||||||
MimeTypes.WBMP,
|
|
||||||
MimeTypes.ICO,
|
|
||||||
MimeTypes.SVG
|
|
||||||
);
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public ImageByteStreamHandler(Activity activity, Object arguments) {
|
public ImageByteStreamHandler(Activity activity, Object arguments) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
|
@ -84,7 +71,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
||||||
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
||||||
private void getImage() {
|
private void getImage() {
|
||||||
if (mimeType != null && mimeType.startsWith(MimeTypes.VIDEO)) {
|
if (MimeTypes.isVideo(mimeType)) {
|
||||||
RequestOptions options = new RequestOptions()
|
RequestOptions options = new RequestOptions()
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||||
|
@ -108,7 +95,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
} finally {
|
} finally {
|
||||||
Glide.with(activity).clear(target);
|
Glide.with(activity).clear(target);
|
||||||
}
|
}
|
||||||
} else if (!flutterSupportedTypes.contains(mimeType)) {
|
} else if (!MimeTypes.isSupportedByFlutter(mimeType)) {
|
||||||
// we convert the image on platform side first, when Dart Image.memory does not support it
|
// we convert the image on platform side first, when Dart Image.memory does not support it
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
|
|
|
@ -167,9 +167,9 @@ public abstract class ImageProvider {
|
||||||
// newURI is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
// newURI is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
||||||
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
||||||
contentId = ContentUris.parseId(newUri);
|
contentId = ContentUris.parseId(newUri);
|
||||||
if (mimeType.startsWith(MimeTypes.IMAGE)) {
|
if (MimeTypes.isImage(mimeType)) {
|
||||||
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId);
|
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId);
|
||||||
} else if (mimeType.startsWith(MimeTypes.VIDEO)) {
|
} else if (MimeTypes.isVideo(mimeType)) {
|
||||||
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId);
|
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,10 +85,10 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
callback.onSuccess(entry);
|
callback.onSuccess(entry);
|
||||||
};
|
};
|
||||||
NewEntryChecker alwaysValid = (contentId, dateModifiedSecs) -> true;
|
NewEntryChecker alwaysValid = (contentId, dateModifiedSecs) -> true;
|
||||||
if (mimeType.startsWith(MimeTypes.IMAGE)) {
|
if (MimeTypes.isImage(mimeType)) {
|
||||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
||||||
entryCount = fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION);
|
entryCount = fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION);
|
||||||
} else if (mimeType.startsWith(MimeTypes.VIDEO)) {
|
} else if (MimeTypes.isVideo(mimeType)) {
|
||||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
||||||
entryCount = fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION);
|
entryCount = fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION);
|
||||||
}
|
}
|
||||||
|
@ -126,10 +126,6 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
int newEntryCount = 0;
|
int newEntryCount = 0;
|
||||||
final String orderBy = MediaStore.MediaColumns.DATE_MODIFIED + " DESC";
|
final String orderBy = MediaStore.MediaColumns.DATE_MODIFIED + " DESC";
|
||||||
|
|
||||||
// it is reasonable to assume a default orientation when it is missing for videos,
|
|
||||||
// but not so for images, often containing with metadata ignored by the Media Store
|
|
||||||
final boolean needOrientation = projection == IMAGE_PROJECTION;
|
|
||||||
|
|
||||||
final boolean needDuration = projection == VIDEO_PROJECTION;
|
final boolean needDuration = projection == VIDEO_PROJECTION;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -164,18 +160,15 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
int height = cursor.getInt(heightColumn);
|
int height = cursor.getInt(heightColumn);
|
||||||
final long durationMillis = durationColumn != -1 ? cursor.getLong(durationColumn) : 0;
|
final long durationMillis = durationColumn != -1 ? cursor.getLong(durationColumn) : 0;
|
||||||
|
|
||||||
Integer rotationDegrees = null;
|
|
||||||
// check whether the field may be `null` to distinguish it from a legitimate `0`
|
// check whether the field may be `null` to distinguish it from a legitimate `0`
|
||||||
// this can happen for specific formats (e.g. for PNG, WEBP)
|
// this can happen for specific formats (e.g. for PNG, WEBP)
|
||||||
// or for JPEG that were not properly registered
|
// or for JPEG that were not properly registered
|
||||||
if (orientationColumn != -1 && cursor.getType(orientationColumn) == Cursor.FIELD_TYPE_INTEGER) {
|
|
||||||
rotationDegrees = cursor.getInt(orientationColumn);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> entryMap = new HashMap<String, Object>() {{
|
Map<String, Object> entryMap = new HashMap<String, Object>() {{
|
||||||
put("uri", itemUri.toString());
|
put("uri", itemUri.toString());
|
||||||
put("path", path);
|
put("path", path);
|
||||||
put("sourceMimeType", mimeType);
|
put("sourceMimeType", mimeType);
|
||||||
|
put("sourceRotationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0);
|
||||||
put("sizeBytes", cursor.getLong(sizeColumn));
|
put("sizeBytes", cursor.getLong(sizeColumn));
|
||||||
put("title", cursor.getString(titleColumn));
|
put("title", cursor.getString(titleColumn));
|
||||||
put("dateModifiedSecs", dateModifiedSecs);
|
put("dateModifiedSecs", dateModifiedSecs);
|
||||||
|
@ -186,10 +179,8 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
entryMap.put("width", width);
|
entryMap.put("width", width);
|
||||||
entryMap.put("height", height);
|
entryMap.put("height", height);
|
||||||
entryMap.put("durationMillis", durationMillis);
|
entryMap.put("durationMillis", durationMillis);
|
||||||
entryMap.put("rotationDegrees", rotationDegrees != null ? rotationDegrees : 0);
|
|
||||||
|
|
||||||
if (((width <= 0 || height <= 0) && needSize(mimeType))
|
if (((width <= 0 || height <= 0) && needSize(mimeType))
|
||||||
|| (rotationDegrees == null && needOrientation)
|
|
||||||
|| (durationMillis == 0 && needDuration)) {
|
|| (durationMillis == 0 && needDuration)) {
|
||||||
// some images are incorrectly registered in the Media Store,
|
// some images are incorrectly registered in the Media Store,
|
||||||
// they are valid but miss some attributes, such as width, height, orientation
|
// they are valid but miss some attributes, such as width, height, orientation
|
||||||
|
@ -331,7 +322,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, destination.relativePath);
|
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, destination.relativePath);
|
||||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
|
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
|
||||||
String volumeName = destination.volumeNameForMediaStore;
|
String volumeName = destination.volumeNameForMediaStore;
|
||||||
Uri tableUrl = mimeType.startsWith(MimeTypes.VIDEO) ?
|
Uri tableUrl = MimeTypes.isVideo(mimeType) ?
|
||||||
MediaStore.Video.Media.getContentUri(volumeName) :
|
MediaStore.Video.Media.getContentUri(volumeName) :
|
||||||
MediaStore.Images.Media.getContentUri(volumeName);
|
MediaStore.Images.Media.getContentUri(volumeName);
|
||||||
Uri destinationUri = context.getContentResolver().insert(tableUrl, contentValues);
|
Uri destinationUri = context.getContentResolver().insert(tableUrl, contentValues);
|
||||||
|
|
|
@ -26,7 +26,7 @@ class AvesImageEntry(map: Map<String?, Any?>) {
|
||||||
val dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
val dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||||
|
|
||||||
val isVideo: Boolean
|
val isVideo: Boolean
|
||||||
get() = mimeType.startsWith(MimeTypes.VIDEO)
|
get() = MimeTypes.isVideo(mimeType)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// convenience method
|
// convenience method
|
||||||
|
|
|
@ -6,7 +6,7 @@ import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.drew.imaging.ImageMetadataReader
|
import com.drew.imaging.ImageMetadataReader
|
||||||
import com.drew.metadata.avi.AviDirectory
|
import com.drew.metadata.avi.AviDirectory
|
||||||
import com.drew.metadata.exif.ExifIFD0Directory
|
import com.drew.metadata.exif.ExifIFD0Directory
|
||||||
|
@ -14,29 +14,35 @@ import com.drew.metadata.jpeg.JpegDirectory
|
||||||
import com.drew.metadata.mp4.Mp4Directory
|
import com.drew.metadata.mp4.Mp4Directory
|
||||||
import com.drew.metadata.mp4.media.Mp4VideoDirectory
|
import com.drew.metadata.mp4.media.Mp4VideoDirectory
|
||||||
import com.drew.metadata.photoshop.PsdHeaderDirectory
|
import com.drew.metadata.photoshop.PsdHeaderDirectory
|
||||||
|
import deckers.thibault.aves.utils.ExifInterfaceHelper.getSafeDate
|
||||||
|
import deckers.thibault.aves.utils.ExifInterfaceHelper.getSafeInt
|
||||||
|
import deckers.thibault.aves.utils.MediaMetadataRetrieverHelper.getSafeDateMillis
|
||||||
|
import deckers.thibault.aves.utils.MediaMetadataRetrieverHelper.getSafeInt
|
||||||
|
import deckers.thibault.aves.utils.MediaMetadataRetrieverHelper.getSafeLong
|
||||||
|
import deckers.thibault.aves.utils.MediaMetadataRetrieverHelper.getSafeString
|
||||||
|
import deckers.thibault.aves.utils.MetadataExtractorHelper.getSafeDateMillis
|
||||||
|
import deckers.thibault.aves.utils.MetadataExtractorHelper.getSafeInt
|
||||||
|
import deckers.thibault.aves.utils.MetadataExtractorHelper.getSafeLong
|
||||||
import deckers.thibault.aves.utils.MetadataHelper.getRotationDegreesForExifCode
|
import deckers.thibault.aves.utils.MetadataHelper.getRotationDegreesForExifCode
|
||||||
import deckers.thibault.aves.utils.MetadataHelper.isFlippedForExifCode
|
|
||||||
import deckers.thibault.aves.utils.MetadataHelper.parseVideoMetadataDate
|
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class SourceImageEntry {
|
class SourceImageEntry {
|
||||||
val uri: Uri // content or file URI
|
val uri: Uri // content or file URI
|
||||||
var path: String? = null // best effort to get local path
|
var path: String? = null // best effort to get local path
|
||||||
private val sourceMimeType: String
|
private val sourceMimeType: String
|
||||||
var title: String? = null
|
private var title: String? = null
|
||||||
var width: Int? = null
|
var width: Int? = null
|
||||||
var height: Int? = null
|
var height: Int? = null
|
||||||
private var rotationDegrees: Int? = null
|
private var sourceRotationDegrees: Int? = null
|
||||||
private var isFlipped: Boolean? = null
|
private var sizeBytes: Long? = null
|
||||||
var sizeBytes: Long? = null
|
private var dateModifiedSecs: Long? = null
|
||||||
var dateModifiedSecs: Long? = null
|
|
||||||
private var sourceDateTakenMillis: Long? = null
|
private var sourceDateTakenMillis: Long? = null
|
||||||
private var durationMillis: Long? = null
|
private var durationMillis: Long? = null
|
||||||
|
|
||||||
|
private var foundExif: Boolean = false
|
||||||
|
|
||||||
constructor(uri: Uri, sourceMimeType: String) {
|
constructor(uri: Uri, sourceMimeType: String) {
|
||||||
this.uri = uri
|
this.uri = uri
|
||||||
this.sourceMimeType = sourceMimeType
|
this.sourceMimeType = sourceMimeType
|
||||||
|
@ -46,9 +52,9 @@ class SourceImageEntry {
|
||||||
uri = Uri.parse(map["uri"] as String)
|
uri = Uri.parse(map["uri"] as String)
|
||||||
path = map["path"] as String?
|
path = map["path"] as String?
|
||||||
sourceMimeType = map["sourceMimeType"] as String
|
sourceMimeType = map["sourceMimeType"] as String
|
||||||
width = map["width"] as Int
|
width = map["width"] as Int?
|
||||||
height = map["height"] as Int
|
height = map["height"] as Int?
|
||||||
rotationDegrees = map["rotationDegrees"] as Int
|
sourceRotationDegrees = map["sourceRotationDegrees"] as Int?
|
||||||
sizeBytes = toLong(map["sizeBytes"])
|
sizeBytes = toLong(map["sizeBytes"])
|
||||||
title = map["title"] as String?
|
title = map["title"] as String?
|
||||||
dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||||
|
@ -70,8 +76,7 @@ class SourceImageEntry {
|
||||||
"sourceMimeType" to sourceMimeType,
|
"sourceMimeType" to sourceMimeType,
|
||||||
"width" to width,
|
"width" to width,
|
||||||
"height" to height,
|
"height" to height,
|
||||||
"rotationDegrees" to (rotationDegrees ?: 0),
|
"sourceRotationDegrees" to (sourceRotationDegrees ?: 0),
|
||||||
"isFlipped" to (isFlipped ?: false),
|
|
||||||
"sizeBytes" to sizeBytes,
|
"sizeBytes" to sizeBytes,
|
||||||
"title" to title,
|
"title" to title,
|
||||||
"dateModifiedSecs" to dateModifiedSecs,
|
"dateModifiedSecs" to dateModifiedSecs,
|
||||||
|
@ -99,17 +104,14 @@ class SourceImageEntry {
|
||||||
val hasSize: Boolean
|
val hasSize: Boolean
|
||||||
get() = width ?: 0 > 0 && height ?: 0 > 0
|
get() = width ?: 0 > 0 && height ?: 0 > 0
|
||||||
|
|
||||||
private val hasOrientation: Boolean
|
|
||||||
get() = rotationDegrees != null
|
|
||||||
|
|
||||||
private val hasDuration: Boolean
|
private val hasDuration: Boolean
|
||||||
get() = durationMillis ?: 0 > 0
|
get() = durationMillis ?: 0 > 0
|
||||||
|
|
||||||
private val isImage: Boolean
|
private val isImage: Boolean
|
||||||
get() = sourceMimeType.startsWith(MimeTypes.IMAGE)
|
get() = MimeTypes.isImage(sourceMimeType)
|
||||||
|
|
||||||
private val isVideo: Boolean
|
private val isVideo: Boolean
|
||||||
get() = sourceMimeType.startsWith(MimeTypes.VIDEO)
|
get() = MimeTypes.isVideo(sourceMimeType)
|
||||||
|
|
||||||
val isSvg: Boolean
|
val isSvg: Boolean
|
||||||
get() = sourceMimeType == MimeTypes.SVG
|
get() = sourceMimeType == MimeTypes.SVG
|
||||||
|
@ -119,58 +121,32 @@ class SourceImageEntry {
|
||||||
// finds: width, height, orientation/rotation, date, title, duration
|
// finds: width, height, orientation/rotation, date, title, duration
|
||||||
fun fillPreCatalogMetadata(context: Context): SourceImageEntry {
|
fun fillPreCatalogMetadata(context: Context): SourceImageEntry {
|
||||||
if (isSvg) return this
|
if (isSvg) return this
|
||||||
fillByMediaMetadataRetriever(context)
|
if (isVideo) {
|
||||||
if (hasSize && hasOrientation && (!isVideo || hasDuration)) return this
|
fillVideoByMediaMetadataRetriever(context)
|
||||||
|
if (hasSize && hasDuration) return this
|
||||||
|
}
|
||||||
|
if (MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) {
|
||||||
fillByMetadataExtractor(context)
|
fillByMetadataExtractor(context)
|
||||||
|
if (hasSize && foundExif) return this
|
||||||
|
}
|
||||||
|
if (ExifInterface.isSupportedMimeType(sourceMimeType)) {
|
||||||
|
fillByExifInterface(context)
|
||||||
if (hasSize) return this
|
if (hasSize) return this
|
||||||
|
}
|
||||||
fillByBitmapDecode(context)
|
fillByBitmapDecode(context)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// expects entry with: uri, mimeType
|
// finds: width, height, orientation, date, duration, title
|
||||||
// finds: width, height, orientation/rotation, date, title, duration
|
private fun fillVideoByMediaMetadataRetriever(context: Context) {
|
||||||
private fun fillByMediaMetadataRetriever(context: Context) {
|
|
||||||
if (isImage) return
|
|
||||||
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return
|
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return
|
||||||
try {
|
try {
|
||||||
var width: String? = null
|
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) { width = it }
|
||||||
var height: String? = null
|
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) { height = it }
|
||||||
var rotationDegrees: String? = null
|
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
|
||||||
var durationMillis: String? = null
|
retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it }
|
||||||
if (isImage) {
|
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it }
|
||||||
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)
|
|
||||||
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)
|
|
||||||
rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)
|
|
||||||
}
|
|
||||||
} else if (isVideo) {
|
|
||||||
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
|
||||||
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
|
||||||
rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
||||||
durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
||||||
}
|
|
||||||
if (width != null) {
|
|
||||||
this.width = width.toInt()
|
|
||||||
}
|
|
||||||
if (height != null) {
|
|
||||||
this.height = height.toInt()
|
|
||||||
}
|
|
||||||
if (rotationDegrees != null) {
|
|
||||||
this.rotationDegrees = rotationDegrees.toInt()
|
|
||||||
}
|
|
||||||
if (durationMillis != null) {
|
|
||||||
this.durationMillis = durationMillis.toLong()
|
|
||||||
}
|
|
||||||
val dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
|
|
||||||
val dateMillis = parseVideoMetadataDate(dateString)
|
|
||||||
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
|
||||||
if (dateMillis > 0) {
|
|
||||||
sourceDateTakenMillis = dateMillis
|
|
||||||
}
|
|
||||||
val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
|
||||||
if (title != null) {
|
|
||||||
this.title = title
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -179,10 +155,8 @@ class SourceImageEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// expects entry with: uri, mimeType
|
// finds: width, height, orientation, date, duration
|
||||||
// finds: width, height, orientation, date
|
|
||||||
private fun fillByMetadataExtractor(context: Context) {
|
private fun fillByMetadataExtractor(context: Context) {
|
||||||
if (!isSupportedByMetadataExtractor(sourceMimeType)) return
|
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri).use { input ->
|
StorageUtils.openInputStream(context, uri).use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input)
|
||||||
|
@ -191,62 +165,36 @@ class SourceImageEntry {
|
||||||
// (e.g. PNG registered as JPG)
|
// (e.g. PNG registered as JPG)
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {
|
||||||
if (dir.containsTag(AviDirectory.TAG_WIDTH)) {
|
dir.getSafeInt(AviDirectory.TAG_WIDTH) { width = it }
|
||||||
width = dir.getInt(AviDirectory.TAG_WIDTH)
|
dir.getSafeInt(AviDirectory.TAG_HEIGHT) { height = it }
|
||||||
}
|
dir.getSafeLong(AviDirectory.TAG_DURATION) { durationMillis = it }
|
||||||
if (dir.containsTag(AviDirectory.TAG_HEIGHT)) {
|
|
||||||
height = dir.getInt(AviDirectory.TAG_HEIGHT)
|
|
||||||
}
|
|
||||||
if (dir.containsTag(AviDirectory.TAG_DURATION)) {
|
|
||||||
durationMillis = dir.getLong(AviDirectory.TAG_DURATION)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (dir in metadata.getDirectoriesOfType(Mp4VideoDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(Mp4VideoDirectory::class.java)) {
|
||||||
if (dir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
dir.getSafeInt(Mp4VideoDirectory.TAG_WIDTH) { width = it }
|
||||||
width = dir.getInt(Mp4VideoDirectory.TAG_WIDTH)
|
dir.getSafeInt(Mp4VideoDirectory.TAG_HEIGHT) { height = it }
|
||||||
}
|
|
||||||
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
|
||||||
height = dir.getInt(Mp4VideoDirectory.TAG_HEIGHT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (dir in metadata.getDirectoriesOfType(Mp4Directory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(Mp4Directory::class.java)) {
|
||||||
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
|
dir.getSafeInt(Mp4Directory.TAG_ROTATION) { sourceRotationDegrees = it }
|
||||||
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION)
|
dir.getSafeLong(Mp4Directory.TAG_DURATION) { durationMillis = it }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (dir in metadata.getDirectoriesOfType(JpegDirectory::class.java)) {
|
|
||||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) {
|
|
||||||
width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH)
|
|
||||||
}
|
|
||||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
|
|
||||||
height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (dir in metadata.getDirectoriesOfType(PsdHeaderDirectory::class.java)) {
|
|
||||||
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_WIDTH)) {
|
|
||||||
width = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH)
|
|
||||||
}
|
|
||||||
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)) {
|
|
||||||
height = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXIF, if defined, should override metadata found in other directories
|
// EXIF, if defined, should override metadata found in other directories
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_WIDTH)) {
|
foundExif = true
|
||||||
width = dir.getInt(ExifIFD0Directory.TAG_IMAGE_WIDTH)
|
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it }
|
||||||
|
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = it }
|
||||||
|
dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { sourceRotationDegrees = getRotationDegreesForExifCode(it) }
|
||||||
|
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME) { sourceDateTakenMillis = it }
|
||||||
}
|
}
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_HEIGHT)) {
|
|
||||||
height = dir.getInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT)
|
if (!foundExif) {
|
||||||
|
for (dir in metadata.getDirectoriesOfType(JpegDirectory::class.java)) {
|
||||||
|
dir.getSafeInt(JpegDirectory.TAG_IMAGE_WIDTH) { width = it }
|
||||||
|
dir.getSafeInt(JpegDirectory.TAG_IMAGE_HEIGHT) { height = it }
|
||||||
}
|
}
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
for (dir in metadata.getDirectoriesOfType(PsdHeaderDirectory::class.java)) {
|
||||||
val exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION)
|
dir.getSafeInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH) { width = it }
|
||||||
rotationDegrees = getRotationDegreesForExifCode(exifOrientation)
|
dir.getSafeInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT) { height = it }
|
||||||
isFlipped = isFlippedForExifCode(exifOrientation)
|
|
||||||
}
|
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
|
|
||||||
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).time
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,7 +206,22 @@ class SourceImageEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// expects entry with: uri
|
// finds: width, height, orientation, date
|
||||||
|
private fun fillByExifInterface(context: Context) {
|
||||||
|
try {
|
||||||
|
StorageUtils.openInputStream(context, uri).use { input ->
|
||||||
|
val exif = ExifInterface(input)
|
||||||
|
foundExif = true
|
||||||
|
exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it }
|
||||||
|
exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it }
|
||||||
|
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { sourceRotationDegrees = exif.rotationDegrees }
|
||||||
|
exif.getSafeDate(ExifInterface.TAG_DATETIME) { sourceDateTakenMillis = it }
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// finds: width, height
|
// finds: width, height
|
||||||
private fun fillByBitmapDecode(context: Context) {
|
private fun fillByBitmapDecode(context: Context) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -257,11 +257,10 @@ object ExifInterfaceHelper {
|
||||||
private fun fillMetadataExtractorDir(exif: ExifInterface, metadataExtractorDirs: Map<DirType, Directory>, tags: Map<String, TagMapper?>) {
|
private fun fillMetadataExtractorDir(exif: ExifInterface, metadataExtractorDirs: Map<DirType, Directory>, tags: Map<String, TagMapper?>) {
|
||||||
for (kv in tags) {
|
for (kv in tags) {
|
||||||
val exifInterfaceTag: String = kv.key
|
val exifInterfaceTag: String = kv.key
|
||||||
if (exif.hasAttribute(exifInterfaceTag)) {
|
val mapper = kv.value
|
||||||
|
if (exif.hasAttribute(exifInterfaceTag) && mapper != null) {
|
||||||
val value: String? = exif.getAttribute(exifInterfaceTag)
|
val value: String? = exif.getAttribute(exifInterfaceTag)
|
||||||
if (value != null && (value != "0" || !neverNullTags.contains(exifInterfaceTag))) {
|
if (value != null && (value != "0" || !neverNullTags.contains(exifInterfaceTag))) {
|
||||||
val mapper = kv.value
|
|
||||||
if (mapper != null) {
|
|
||||||
val obj: Any? = when (mapper.format) {
|
val obj: Any? = when (mapper.format) {
|
||||||
TagFormat.ASCII, TagFormat.COMMENT, TagFormat.UNDEFINED -> value
|
TagFormat.ASCII, TagFormat.COMMENT, TagFormat.UNDEFINED -> value
|
||||||
TagFormat.BYTE -> value.toByteArray()
|
TagFormat.BYTE -> value.toByteArray()
|
||||||
|
@ -279,7 +278,6 @@ object ExifInterfaceHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun toRational(s: String?): Rational? {
|
private fun toRational(s: String?): Rational? {
|
||||||
s ?: return null
|
s ?: return null
|
||||||
|
@ -314,6 +312,28 @@ object ExifInterfaceHelper {
|
||||||
if (list.isEmpty()) return null
|
if (list.isEmpty()) return null
|
||||||
return list.toTypedArray()
|
return list.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extensions
|
||||||
|
|
||||||
|
fun ExifInterface.getSafeInt(tag: String, acceptZero: Boolean = true, save: (value: Int) -> Unit) {
|
||||||
|
if (this.hasAttribute(tag)) {
|
||||||
|
val value = this.getAttributeInt(tag, 0)
|
||||||
|
if (acceptZero || value != 0) {
|
||||||
|
save(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ExifInterface.getSafeDate(tag: String, save: (value: Long) -> Unit) {
|
||||||
|
if (this.hasAttribute(tag)) {
|
||||||
|
// TODO TLAD parse date with "yyyy:MM:dd HH:mm:ss" or find the original long
|
||||||
|
val formattedDate = this.getAttribute(tag)
|
||||||
|
val value = formattedDate?.toLongOrNull()
|
||||||
|
if (value != null && value > 0) {
|
||||||
|
save(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DirType {
|
enum class DirType {
|
||||||
|
|
|
@ -5,47 +5,71 @@ import android.os.Build
|
||||||
|
|
||||||
object MediaMetadataRetrieverHelper {
|
object MediaMetadataRetrieverHelper {
|
||||||
@JvmField
|
@JvmField
|
||||||
val allKeys: Map<String, Int> = hashMapOf(
|
val allKeys = hashMapOf(
|
||||||
"METADATA_KEY_ALBUM" to MediaMetadataRetriever.METADATA_KEY_ALBUM,
|
MediaMetadataRetriever.METADATA_KEY_ALBUM to "ALBUM",
|
||||||
"METADATA_KEY_ALBUMARTIST" to MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
|
MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST to "ALBUMARTIST",
|
||||||
"METADATA_KEY_ARTIST" to MediaMetadataRetriever.METADATA_KEY_ARTIST,
|
MediaMetadataRetriever.METADATA_KEY_ARTIST to "ARTIST",
|
||||||
"METADATA_KEY_AUTHOR" to MediaMetadataRetriever.METADATA_KEY_AUTHOR,
|
MediaMetadataRetriever.METADATA_KEY_AUTHOR to "AUTHOR",
|
||||||
"METADATA_KEY_BITRATE" to MediaMetadataRetriever.METADATA_KEY_BITRATE,
|
MediaMetadataRetriever.METADATA_KEY_BITRATE to "BITRATE",
|
||||||
"METADATA_KEY_CAPTURE_FRAMERATE" to MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE,
|
MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE to "CAPTURE_FRAMERATE",
|
||||||
"METADATA_KEY_CD_TRACK_NUMBER" to MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER,
|
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER to "CD_TRACK_NUMBER",
|
||||||
"METADATA_KEY_COLOR_RANGE" to MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE,
|
MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE to "COLOR_RANGE",
|
||||||
"METADATA_KEY_COLOR_STANDARD" to MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD,
|
MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD to "COLOR_STANDARD",
|
||||||
"METADATA_KEY_COLOR_TRANSFER" to MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER,
|
MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER to "COLOR_TRANSFER",
|
||||||
"METADATA_KEY_COMPILATION" to MediaMetadataRetriever.METADATA_KEY_COMPILATION,
|
MediaMetadataRetriever.METADATA_KEY_COMPILATION to "COMPILATION",
|
||||||
"METADATA_KEY_COMPOSER" to MediaMetadataRetriever.METADATA_KEY_COMPOSER,
|
MediaMetadataRetriever.METADATA_KEY_COMPOSER to "COMPOSER",
|
||||||
"METADATA_KEY_DATE" to MediaMetadataRetriever.METADATA_KEY_DATE,
|
MediaMetadataRetriever.METADATA_KEY_DATE to "DATE",
|
||||||
"METADATA_KEY_DISC_NUMBER" to MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER,
|
MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER to "DISC_NUMBER",
|
||||||
"METADATA_KEY_DURATION" to MediaMetadataRetriever.METADATA_KEY_DURATION,
|
MediaMetadataRetriever.METADATA_KEY_DURATION to "DURATION",
|
||||||
"METADATA_KEY_EXIF_LENGTH" to MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH,
|
MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH to "EXIF_LENGTH",
|
||||||
"METADATA_KEY_EXIF_OFFSET" to MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET,
|
MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET to "EXIF_OFFSET",
|
||||||
"METADATA_KEY_GENRE" to MediaMetadataRetriever.METADATA_KEY_GENRE,
|
MediaMetadataRetriever.METADATA_KEY_GENRE to "GENRE",
|
||||||
"METADATA_KEY_HAS_AUDIO" to MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO,
|
MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO to "HAS_AUDIO",
|
||||||
"METADATA_KEY_HAS_VIDEO" to MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO,
|
MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO to "HAS_VIDEO",
|
||||||
"METADATA_KEY_LOCATION" to MediaMetadataRetriever.METADATA_KEY_LOCATION,
|
MediaMetadataRetriever.METADATA_KEY_LOCATION to "LOCATION",
|
||||||
"METADATA_KEY_MIMETYPE" to MediaMetadataRetriever.METADATA_KEY_MIMETYPE,
|
MediaMetadataRetriever.METADATA_KEY_MIMETYPE to "MIMETYPE",
|
||||||
"METADATA_KEY_NUM_TRACKS" to MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS,
|
MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS to "NUM_TRACKS",
|
||||||
"METADATA_KEY_TITLE" to MediaMetadataRetriever.METADATA_KEY_TITLE,
|
MediaMetadataRetriever.METADATA_KEY_TITLE to "TITLE",
|
||||||
"METADATA_KEY_VIDEO_HEIGHT" to MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT,
|
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT to "VIDEO_HEIGHT",
|
||||||
"METADATA_KEY_VIDEO_ROTATION" to MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION,
|
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION to "VIDEO_ROTATION",
|
||||||
"METADATA_KEY_VIDEO_WIDTH" to MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH,
|
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH to "VIDEO_WIDTH",
|
||||||
"METADATA_KEY_WRITER" to MediaMetadataRetriever.METADATA_KEY_WRITER,
|
MediaMetadataRetriever.METADATA_KEY_WRITER to "WRITER",
|
||||||
"METADATA_KEY_YEAR" to MediaMetadataRetriever.METADATA_KEY_YEAR,
|
MediaMetadataRetriever.METADATA_KEY_YEAR to "YEAR",
|
||||||
).apply {
|
).apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
putAll(hashMapOf(
|
putAll(hashMapOf(
|
||||||
"METADATA_KEY_HAS_IMAGE" to MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE,
|
MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE to "HAS_IMAGE",
|
||||||
"METADATA_KEY_IMAGE_COUNT" to MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT,
|
MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT to "IMAGE_COUNT",
|
||||||
"METADATA_KEY_IMAGE_HEIGHT" to MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT,
|
MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT to "IMAGE_HEIGHT",
|
||||||
"METADATA_KEY_IMAGE_PRIMARY" to MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY,
|
MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY to "IMAGE_PRIMARY",
|
||||||
"METADATA_KEY_IMAGE_ROTATION" to MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION,
|
MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION to "IMAGE_ROTATION",
|
||||||
"METADATA_KEY_IMAGE_WIDTH" to MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH,
|
MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH to "IMAGE_WIDTH",
|
||||||
"METADATA_KEY_VIDEO_FRAME_COUNT" to MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT,
|
MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT to "VIDEO_FRAME_COUNT",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extensions
|
||||||
|
|
||||||
|
fun MediaMetadataRetriever.getSafeString(tag: Int, save: (value: String) -> Unit) {
|
||||||
|
val value = this.extractMetadata(tag)
|
||||||
|
if (value != null) save(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaMetadataRetriever.getSafeInt(tag: Int, save: (value: Int) -> Unit) {
|
||||||
|
val value = this.extractMetadata(tag)?.toIntOrNull()
|
||||||
|
if (value != null) save(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaMetadataRetriever.getSafeLong(tag: Int, save: (value: Long) -> Unit) {
|
||||||
|
val value = this.extractMetadata(tag)?.toLongOrNull()
|
||||||
|
if (value != null) save(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MediaMetadataRetriever.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) {
|
||||||
|
val dateString = this.extractMetadata(tag)
|
||||||
|
val dateMillis = MetadataHelper.parseVideoMetadataDate(dateString)
|
||||||
|
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
||||||
|
if (dateMillis > 0) save(dateMillis)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package deckers.thibault.aves.utils
|
||||||
|
|
||||||
|
import com.drew.metadata.Directory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object MetadataExtractorHelper {
|
||||||
|
// extensions
|
||||||
|
|
||||||
|
fun Directory.getSafeInt(tag: Int, save: (value: Int) -> Unit) {
|
||||||
|
if (this.containsTag(tag)) save(this.getInt(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Directory.getSafeLong(tag: Int, save: (value: Long) -> Unit) {
|
||||||
|
if (this.containsTag(tag)) save(this.getLong(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Directory.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) {
|
||||||
|
if (this.containsTag(tag)) save(this.getDate(tag, null, TimeZone.getDefault()).time)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,60 +3,91 @@ package deckers.thibault.aves.utils
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object MimeTypes {
|
object MimeTypes {
|
||||||
const val IMAGE = "image"
|
private const val IMAGE = "image"
|
||||||
|
|
||||||
// generic raster
|
// generic raster
|
||||||
const val BMP = "image/bmp"
|
private const val BMP = "image/bmp"
|
||||||
const val GIF = "image/gif"
|
const val GIF = "image/gif"
|
||||||
const val HEIC = "image/heic"
|
private const val HEIC = "image/heic"
|
||||||
const val HEIF = "image/heif"
|
private const val HEIF = "image/heif"
|
||||||
const val ICO = "image/x-icon"
|
private const val ICO = "image/x-icon"
|
||||||
const val JPEG = "image/jpeg"
|
private const val JPEG = "image/jpeg"
|
||||||
const val PCX = "image/x-pcx"
|
private const val PCX = "image/x-pcx"
|
||||||
const val PNG = "image/png"
|
private const val PNG = "image/png"
|
||||||
const val PSD = "image/x-photoshop" // aka "image/vnd.adobe.photoshop"
|
private const val PSD = "image/x-photoshop" // aka "image/vnd.adobe.photoshop"
|
||||||
const val TIFF = "image/tiff"
|
private const val TIFF = "image/tiff"
|
||||||
const val WBMP = "image/vnd.wap.wbmp"
|
private const val WBMP = "image/vnd.wap.wbmp"
|
||||||
const val WEBP = "image/webp"
|
const val WEBP = "image/webp"
|
||||||
|
|
||||||
// raw raster
|
// raw raster
|
||||||
const val ARW = "image/x-sony-arw"
|
private const val ARW = "image/x-sony-arw"
|
||||||
const val CR2 = "image/x-canon-cr2"
|
private const val CR2 = "image/x-canon-cr2"
|
||||||
const val CRW = "image/x-canon-crw"
|
private const val CRW = "image/x-canon-crw"
|
||||||
const val DCR = "image/x-kodak-dcr"
|
private const val DCR = "image/x-kodak-dcr"
|
||||||
const val DNG = "image/x-adobe-dng"
|
private const val DNG = "image/x-adobe-dng"
|
||||||
const val ERF = "image/x-epson-erf"
|
private const val ERF = "image/x-epson-erf"
|
||||||
const val K25 = "image/x-kodak-k25"
|
private const val K25 = "image/x-kodak-k25"
|
||||||
const val KDC = "image/x-kodak-kdc"
|
private const val KDC = "image/x-kodak-kdc"
|
||||||
const val MRW = "image/x-minolta-mrw"
|
private const val MRW = "image/x-minolta-mrw"
|
||||||
const val NEF = "image/x-nikon-nef"
|
private const val NEF = "image/x-nikon-nef"
|
||||||
const val NRW = "image/x-nikon-nrw"
|
private const val NRW = "image/x-nikon-nrw"
|
||||||
const val ORF = "image/x-olympus-orf"
|
private const val ORF = "image/x-olympus-orf"
|
||||||
const val PEF = "image/x-pentax-pef"
|
private const val PEF = "image/x-pentax-pef"
|
||||||
const val RAF = "image/x-fuji-raf"
|
private const val RAF = "image/x-fuji-raf"
|
||||||
const val RAW = "image/x-panasonic-raw"
|
private const val RAW = "image/x-panasonic-raw"
|
||||||
const val RW2 = "image/x-panasonic-rw2"
|
private const val RW2 = "image/x-panasonic-rw2"
|
||||||
const val SR2 = "image/x-sony-sr2"
|
private const val SR2 = "image/x-sony-sr2"
|
||||||
const val SRF = "image/x-sony-srf"
|
private const val SRF = "image/x-sony-srf"
|
||||||
const val SRW = "image/x-samsung-srw"
|
private const val SRW = "image/x-samsung-srw"
|
||||||
const val X3F = "image/x-sigma-x3f"
|
private const val X3F = "image/x-sigma-x3f"
|
||||||
|
|
||||||
// vector
|
// vector
|
||||||
const val SVG = "image/svg+xml"
|
const val SVG = "image/svg+xml"
|
||||||
|
|
||||||
const val VIDEO = "video"
|
private const val VIDEO = "video"
|
||||||
|
|
||||||
const val AVI = "video/avi"
|
private const val AVI = "video/avi"
|
||||||
const val MOV = "video/quicktime"
|
private const val MOV = "video/quicktime"
|
||||||
const val MP2T = "video/mp2t"
|
private const val MP2T = "video/mp2t"
|
||||||
const val MP4 = "video/mp4"
|
private const val MP4 = "video/mp4"
|
||||||
const val WEBM = "video/webm"
|
private const val WEBM = "video/webm"
|
||||||
|
|
||||||
// as of metadata-extractor v2.14.0, the following formats are not supported
|
|
||||||
private val unsupportedMetadataExtractorFormats = listOf(WBMP, MP2T, WEBM)
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isSupportedByMetadataExtractor(mimeType: String) = !unsupportedMetadataExtractorFormats.contains(mimeType)
|
fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO)
|
||||||
|
|
||||||
|
// as of Flutter v1.22.0
|
||||||
|
@JvmStatic
|
||||||
|
fun isSupportedByFlutter(mimeType: String) = when (mimeType) {
|
||||||
|
JPEG, PNG, GIF, WEBP, BMP, WBMP, ICO, SVG -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
// as of metadata-extractor v2.14.0
|
||||||
|
@JvmStatic
|
||||||
|
fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) {
|
||||||
|
WBMP, MP2T, WEBM -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glide automatically applies EXIF orientation when decoding images of known formats
|
||||||
|
// but we need to rotate the decoded bitmap for the other formats
|
||||||
|
@JvmStatic
|
||||||
|
fun needRotationAfterGlide(mimeType: String) = when (mimeType) {
|
||||||
|
DNG, HEIC, HEIF, PNG, WEBP -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnails obtained from the Media Store are automatically rotated
|
||||||
|
// according to EXIF orientation when decoding images of known formats
|
||||||
|
// but we need to rotate the decoded bitmap for the other formats
|
||||||
|
@JvmStatic
|
||||||
|
fun needRotationAfterContentResolverThumbnail(mimeType: String) = when (mimeType) {
|
||||||
|
DNG, PNG -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getMimeTypeForExtension(extension: String?): String? = when (extension?.toLowerCase(Locale.ROOT)) {
|
fun getMimeTypeForExtension(extension: String?): String? = when (extension?.toLowerCase(Locale.ROOT)) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ImageEntry {
|
||||||
final String sourceMimeType;
|
final String sourceMimeType;
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
int rotationDegrees;
|
int sourceRotationDegrees;
|
||||||
final int sizeBytes;
|
final int sizeBytes;
|
||||||
String sourceTitle;
|
String sourceTitle;
|
||||||
int _dateModifiedSecs;
|
int _dateModifiedSecs;
|
||||||
|
@ -42,7 +42,7 @@ class ImageEntry {
|
||||||
this.sourceMimeType,
|
this.sourceMimeType,
|
||||||
@required this.width,
|
@required this.width,
|
||||||
@required this.height,
|
@required this.height,
|
||||||
this.rotationDegrees,
|
this.sourceRotationDegrees,
|
||||||
this.sizeBytes,
|
this.sizeBytes,
|
||||||
this.sourceTitle,
|
this.sourceTitle,
|
||||||
int dateModifiedSecs,
|
int dateModifiedSecs,
|
||||||
|
@ -68,7 +68,7 @@ class ImageEntry {
|
||||||
sourceMimeType: sourceMimeType,
|
sourceMimeType: sourceMimeType,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
rotationDegrees: rotationDegrees,
|
sourceRotationDegrees: sourceRotationDegrees,
|
||||||
sizeBytes: sizeBytes,
|
sizeBytes: sizeBytes,
|
||||||
sourceTitle: sourceTitle,
|
sourceTitle: sourceTitle,
|
||||||
dateModifiedSecs: dateModifiedSecs,
|
dateModifiedSecs: dateModifiedSecs,
|
||||||
|
@ -90,7 +90,7 @@ class ImageEntry {
|
||||||
sourceMimeType: map['sourceMimeType'] as String,
|
sourceMimeType: map['sourceMimeType'] as String,
|
||||||
width: map['width'] as int ?? 0,
|
width: map['width'] as int ?? 0,
|
||||||
height: map['height'] as int ?? 0,
|
height: map['height'] as int ?? 0,
|
||||||
rotationDegrees: map['rotationDegrees'] as int ?? 0,
|
sourceRotationDegrees: map['sourceRotationDegrees'] as int ?? 0,
|
||||||
sizeBytes: map['sizeBytes'] as int,
|
sizeBytes: map['sizeBytes'] as int,
|
||||||
sourceTitle: map['title'] as String,
|
sourceTitle: map['title'] as String,
|
||||||
dateModifiedSecs: map['dateModifiedSecs'] as int,
|
dateModifiedSecs: map['dateModifiedSecs'] as int,
|
||||||
|
@ -108,7 +108,7 @@ class ImageEntry {
|
||||||
'sourceMimeType': sourceMimeType,
|
'sourceMimeType': sourceMimeType,
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
'rotationDegrees': rotationDegrees,
|
'sourceRotationDegrees': sourceRotationDegrees,
|
||||||
'sizeBytes': sizeBytes,
|
'sizeBytes': sizeBytes,
|
||||||
'title': sourceTitle,
|
'title': sourceTitle,
|
||||||
'dateModifiedSecs': dateModifiedSecs,
|
'dateModifiedSecs': dateModifiedSecs,
|
||||||
|
@ -171,10 +171,10 @@ class ImageEntry {
|
||||||
|
|
||||||
bool get isCatalogued => _catalogMetadata != null;
|
bool get isCatalogued => _catalogMetadata != null;
|
||||||
|
|
||||||
bool get isFlipped => _catalogMetadata?.isFlipped ?? false;
|
|
||||||
|
|
||||||
bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
|
bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
|
||||||
|
|
||||||
|
bool get isFlipped => _catalogMetadata?.isFlipped ?? false;
|
||||||
|
|
||||||
bool get canEdit => path != null;
|
bool get canEdit => path != null;
|
||||||
|
|
||||||
bool get canPrint => !isVideo;
|
bool get canPrint => !isVideo;
|
||||||
|
@ -194,7 +194,7 @@ class ImageEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get portrait => ((isVideo && isCatalogued) ? _catalogMetadata.videoRotation : rotationDegrees) % 180 == 90;
|
bool get portrait => ((isVideo && isCatalogued) ? _catalogMetadata.rotationDegrees : rotationDegrees) % 180 == 90;
|
||||||
|
|
||||||
double get displayAspectRatio {
|
double get displayAspectRatio {
|
||||||
if (width == 0 || height == 0) return 1;
|
if (width == 0 || height == 0) return 1;
|
||||||
|
@ -220,6 +220,13 @@ class ImageEntry {
|
||||||
return _bestDate;
|
return _bestDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get rotationDegrees => catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
||||||
|
|
||||||
|
set rotationDegrees(int rotationDegrees) {
|
||||||
|
sourceRotationDegrees = rotationDegrees;
|
||||||
|
catalogMetadata?.rotationDegrees = rotationDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
int get dateModifiedSecs => _dateModifiedSecs;
|
int get dateModifiedSecs => _dateModifiedSecs;
|
||||||
|
|
||||||
set dateModifiedSecs(int dateModifiedSecs) {
|
set dateModifiedSecs(int dateModifiedSecs) {
|
||||||
|
@ -257,7 +264,7 @@ class ImageEntry {
|
||||||
String _bestTitle;
|
String _bestTitle;
|
||||||
|
|
||||||
String get bestTitle {
|
String get bestTitle {
|
||||||
_bestTitle ??= (_catalogMetadata != null && _catalogMetadata.xmpTitleDescription.isNotEmpty) ? _catalogMetadata.xmpTitleDescription : sourceTitle;
|
_bestTitle ??= (isCatalogued && _catalogMetadata.xmpTitleDescription.isNotEmpty) ? _catalogMetadata.xmpTitleDescription : sourceTitle;
|
||||||
return _bestTitle;
|
return _bestTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,10 @@ class DateMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CatalogMetadata {
|
class CatalogMetadata {
|
||||||
final int contentId, dateMillis, videoRotation;
|
final int contentId, dateMillis;
|
||||||
final bool isFlipped, isAnimated;
|
final bool isAnimated;
|
||||||
|
bool isFlipped;
|
||||||
|
int rotationDegrees;
|
||||||
final String mimeType, xmpSubjects, xmpTitleDescription;
|
final String mimeType, xmpSubjects, xmpTitleDescription;
|
||||||
final double latitude, longitude;
|
final double latitude, longitude;
|
||||||
Address address;
|
Address address;
|
||||||
|
@ -38,9 +40,9 @@ class CatalogMetadata {
|
||||||
this.contentId,
|
this.contentId,
|
||||||
this.mimeType,
|
this.mimeType,
|
||||||
this.dateMillis,
|
this.dateMillis,
|
||||||
this.isFlipped,
|
|
||||||
this.isAnimated,
|
this.isAnimated,
|
||||||
this.videoRotation,
|
this.isFlipped,
|
||||||
|
this.rotationDegrees,
|
||||||
this.xmpSubjects,
|
this.xmpSubjects,
|
||||||
this.xmpTitleDescription,
|
this.xmpTitleDescription,
|
||||||
double latitude,
|
double latitude,
|
||||||
|
@ -57,9 +59,9 @@ class CatalogMetadata {
|
||||||
contentId: contentId ?? this.contentId,
|
contentId: contentId ?? this.contentId,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
dateMillis: dateMillis,
|
dateMillis: dateMillis,
|
||||||
isFlipped: isFlipped,
|
|
||||||
isAnimated: isAnimated,
|
isAnimated: isAnimated,
|
||||||
videoRotation: videoRotation,
|
isFlipped: isFlipped,
|
||||||
|
rotationDegrees: rotationDegrees,
|
||||||
xmpSubjects: xmpSubjects,
|
xmpSubjects: xmpSubjects,
|
||||||
xmpTitleDescription: xmpTitleDescription,
|
xmpTitleDescription: xmpTitleDescription,
|
||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
|
@ -68,15 +70,16 @@ class CatalogMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) {
|
factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) {
|
||||||
final isFlipped = map['isFlipped'] ?? (boolAsInteger ? 0 : false);
|
|
||||||
final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false);
|
final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false);
|
||||||
|
final isFlipped = map['isFlipped'] ?? (boolAsInteger ? 0 : false);
|
||||||
return CatalogMetadata(
|
return CatalogMetadata(
|
||||||
contentId: map['contentId'],
|
contentId: map['contentId'],
|
||||||
mimeType: map['mimeType'],
|
mimeType: map['mimeType'],
|
||||||
dateMillis: map['dateMillis'] ?? 0,
|
dateMillis: map['dateMillis'] ?? 0,
|
||||||
isFlipped: boolAsInteger ? isFlipped != 0 : isFlipped,
|
|
||||||
isAnimated: boolAsInteger ? isAnimated != 0 : isAnimated,
|
isAnimated: boolAsInteger ? isAnimated != 0 : isAnimated,
|
||||||
videoRotation: map['videoRotation'] ?? 0,
|
isFlipped: boolAsInteger ? isFlipped != 0 : isFlipped,
|
||||||
|
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0
|
||||||
|
rotationDegrees: map['rotationDegrees'],
|
||||||
xmpSubjects: map['xmpSubjects'] ?? '',
|
xmpSubjects: map['xmpSubjects'] ?? '',
|
||||||
xmpTitleDescription: map['xmpTitleDescription'] ?? '',
|
xmpTitleDescription: map['xmpTitleDescription'] ?? '',
|
||||||
latitude: map['latitude'],
|
latitude: map['latitude'],
|
||||||
|
@ -88,9 +91,9 @@ class CatalogMetadata {
|
||||||
'contentId': contentId,
|
'contentId': contentId,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'dateMillis': dateMillis,
|
'dateMillis': dateMillis,
|
||||||
'isFlipped': boolAsInteger ? (isFlipped ? 1 : 0) : isFlipped,
|
|
||||||
'isAnimated': boolAsInteger ? (isAnimated ? 1 : 0) : isAnimated,
|
'isAnimated': boolAsInteger ? (isAnimated ? 1 : 0) : isAnimated,
|
||||||
'videoRotation': videoRotation,
|
'isFlipped': boolAsInteger ? (isFlipped ? 1 : 0) : isFlipped,
|
||||||
|
'rotationDegrees': rotationDegrees,
|
||||||
'xmpSubjects': xmpSubjects,
|
'xmpSubjects': xmpSubjects,
|
||||||
'xmpTitleDescription': xmpTitleDescription,
|
'xmpTitleDescription': xmpTitleDescription,
|
||||||
'latitude': latitude,
|
'latitude': latitude,
|
||||||
|
@ -99,7 +102,7 @@ class CatalogMetadata {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CatalogMetadata{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isFlipped=$isFlipped, isAnimated=$isAnimated, videoRotation=$videoRotation, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}';
|
return 'CatalogMetadata{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, rotationDegrees=$rotationDegrees, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ class MetadataDb {
|
||||||
', sourceMimeType TEXT'
|
', sourceMimeType TEXT'
|
||||||
', width INTEGER'
|
', width INTEGER'
|
||||||
', height INTEGER'
|
', height INTEGER'
|
||||||
', rotationDegrees INTEGER'
|
', sourceRotationDegrees INTEGER'
|
||||||
', sizeBytes INTEGER'
|
', sizeBytes INTEGER'
|
||||||
', title TEXT'
|
', title TEXT'
|
||||||
', dateModifiedSecs INTEGER'
|
', dateModifiedSecs INTEGER'
|
||||||
|
@ -48,9 +48,9 @@ class MetadataDb {
|
||||||
'contentId INTEGER PRIMARY KEY'
|
'contentId INTEGER PRIMARY KEY'
|
||||||
', mimeType TEXT'
|
', mimeType TEXT'
|
||||||
', dateMillis INTEGER'
|
', dateMillis INTEGER'
|
||||||
', isFlipped INTEGER'
|
|
||||||
', isAnimated INTEGER'
|
', isAnimated INTEGER'
|
||||||
', videoRotation INTEGER'
|
', isFlipped INTEGER'
|
||||||
|
', rotationDegrees INTEGER'
|
||||||
', xmpSubjects TEXT'
|
', xmpSubjects TEXT'
|
||||||
', xmpTitleDescription TEXT'
|
', xmpTitleDescription TEXT'
|
||||||
', latitude REAL'
|
', latitude REAL'
|
||||||
|
@ -74,8 +74,8 @@ class MetadataDb {
|
||||||
// on SQLite <3.25.0, bundled on older Android devices
|
// on SQLite <3.25.0, bundled on older Android devices
|
||||||
while (oldVersion < newVersion) {
|
while (oldVersion < newVersion) {
|
||||||
if (oldVersion == 1) {
|
if (oldVersion == 1) {
|
||||||
|
// rename column 'orientationDegrees' to 'sourceRotationDegrees'
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
// rename column 'orientationDegrees' to 'rotationDegrees'
|
|
||||||
const newEntryTable = '${entryTable}TEMP';
|
const newEntryTable = '${entryTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newEntryTable('
|
await db.execute('CREATE TABLE $newEntryTable('
|
||||||
'contentId INTEGER PRIMARY KEY'
|
'contentId INTEGER PRIMARY KEY'
|
||||||
|
@ -84,20 +84,41 @@ class MetadataDb {
|
||||||
', sourceMimeType TEXT'
|
', sourceMimeType TEXT'
|
||||||
', width INTEGER'
|
', width INTEGER'
|
||||||
', height INTEGER'
|
', height INTEGER'
|
||||||
', rotationDegrees INTEGER'
|
', sourceRotationDegrees INTEGER'
|
||||||
', sizeBytes INTEGER'
|
', sizeBytes INTEGER'
|
||||||
', title TEXT'
|
', title TEXT'
|
||||||
', dateModifiedSecs INTEGER'
|
', dateModifiedSecs INTEGER'
|
||||||
', sourceDateTakenMillis INTEGER'
|
', sourceDateTakenMillis INTEGER'
|
||||||
', durationMillis INTEGER'
|
', durationMillis INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,rotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
||||||
' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
||||||
' FROM $entryTable;');
|
' FROM $entryTable;');
|
||||||
await db.execute('DROP TABLE $entryTable;');
|
await db.execute('DROP TABLE $entryTable;');
|
||||||
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// rename column 'videoRotation' to 'rotationDegrees'
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
const newMetadataTable = '${metadataTable}TEMP';
|
||||||
|
await db.execute('CREATE TABLE $newMetadataTable('
|
||||||
|
'contentId INTEGER PRIMARY KEY'
|
||||||
|
', mimeType TEXT'
|
||||||
|
', dateMillis INTEGER'
|
||||||
|
', isAnimated INTEGER'
|
||||||
|
', rotationDegrees INTEGER'
|
||||||
|
', xmpSubjects TEXT'
|
||||||
|
', xmpTitleDescription TEXT'
|
||||||
|
', latitude REAL'
|
||||||
|
', longitude REAL'
|
||||||
|
')');
|
||||||
|
await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)'
|
||||||
|
' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude'
|
||||||
|
' FROM $metadataTable;');
|
||||||
|
await db.execute('DROP TABLE $metadataTable;');
|
||||||
|
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
||||||
|
});
|
||||||
|
|
||||||
// new column 'isFlipped'
|
// new column 'isFlipped'
|
||||||
await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;');
|
await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;');
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,11 @@ class MetadataService {
|
||||||
// return map with:
|
// return map with:
|
||||||
// 'mimeType': MIME type as reported by metadata extractors, not Media Store (string)
|
// 'mimeType': MIME type as reported by metadata extractors, not Media Store (string)
|
||||||
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
||||||
// 'isFlipped': flipped according to EXIF orientation (bool)
|
|
||||||
// 'isAnimated': animated gif/webp (bool)
|
// 'isAnimated': animated gif/webp (bool)
|
||||||
|
// 'isFlipped': flipped according to EXIF orientation (bool)
|
||||||
|
// 'rotationDegrees': rotation degrees according to EXIF orientation or other metadata (int)
|
||||||
// 'latitude': latitude (double)
|
// 'latitude': latitude (double)
|
||||||
// 'longitude': longitude (double)
|
// 'longitude': longitude (double)
|
||||||
// 'videoRotation': video rotation degrees (int)
|
|
||||||
// 'xmpSubjects': ';' separated XMP subjects (string)
|
// 'xmpSubjects': ';' separated XMP subjects (string)
|
||||||
// 'xmpTitleDescription': XMP title or XMP description (string)
|
// 'xmpTitleDescription': XMP title or XMP description (string)
|
||||||
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
||||||
|
|
|
@ -39,7 +39,8 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initFutures();
|
_loadDatabase();
|
||||||
|
_loadMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -100,6 +101,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'width': '${entry.width}',
|
'width': '${entry.width}',
|
||||||
'height': '${entry.height}',
|
'height': '${entry.height}',
|
||||||
|
'sourceRotationDegrees': '${entry.sourceRotationDegrees}',
|
||||||
'rotationDegrees': '${entry.rotationDegrees}',
|
'rotationDegrees': '${entry.rotationDegrees}',
|
||||||
'portrait': '${entry.portrait}',
|
'portrait': '${entry.portrait}',
|
||||||
'displayAspectRatio': '${entry.displayAspectRatio}',
|
'displayAspectRatio': '${entry.displayAspectRatio}',
|
||||||
|
@ -118,8 +120,8 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
'isPhoto': '${entry.isPhoto}',
|
'isPhoto': '${entry.isPhoto}',
|
||||||
'isVideo': '${entry.isVideo}',
|
'isVideo': '${entry.isVideo}',
|
||||||
'isCatalogued': '${entry.isCatalogued}',
|
'isCatalogued': '${entry.isCatalogued}',
|
||||||
'isFlipped': '${entry.isFlipped}',
|
|
||||||
'isAnimated': '${entry.isAnimated}',
|
'isAnimated': '${entry.isAnimated}',
|
||||||
|
'isFlipped': '${entry.isFlipped}',
|
||||||
'canEdit': '${entry.canEdit}',
|
'canEdit': '${entry.canEdit}',
|
||||||
'canEditExif': '${entry.canEditExif}',
|
'canEditExif': '${entry.canEditExif}',
|
||||||
'canPrint': '${entry.canPrint}',
|
'canPrint': '${entry.canPrint}',
|
||||||
|
@ -165,10 +167,24 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDbTabView() {
|
Widget _buildDbTabView() {
|
||||||
final catalog = entry.catalogMetadata;
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text('DB'),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await metadataDb.removeIds([entry.contentId]);
|
||||||
|
_loadDatabase();
|
||||||
|
},
|
||||||
|
child: Text('Remove from DB'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
FutureBuilder<DateMetadata>(
|
FutureBuilder<DateMetadata>(
|
||||||
future: _dbDateLoader,
|
future: _dbDateLoader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
@ -202,9 +218,9 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'mimeType': '${data.mimeType}',
|
'mimeType': '${data.mimeType}',
|
||||||
'dateMillis': '${data.dateMillis}',
|
'dateMillis': '${data.dateMillis}',
|
||||||
'isFlipped': '${data.isFlipped}',
|
|
||||||
'isAnimated': '${data.isAnimated}',
|
'isAnimated': '${data.isAnimated}',
|
||||||
'videoRotation': '${data.videoRotation}',
|
'isFlipped': '${data.isFlipped}',
|
||||||
|
'rotationDegrees': '${data.rotationDegrees}',
|
||||||
'latitude': '${data.latitude}',
|
'latitude': '${data.latitude}',
|
||||||
'longitude': '${data.longitude}',
|
'longitude': '${data.longitude}',
|
||||||
'xmpSubjects': '${data.xmpSubjects}',
|
'xmpSubjects': '${data.xmpSubjects}',
|
||||||
|
@ -237,21 +253,6 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Divider(),
|
|
||||||
Text('Catalog metadata:${catalog == null ? ' no data' : ''}'),
|
|
||||||
if (catalog != null)
|
|
||||||
InfoRowGroup({
|
|
||||||
'contentId': '${catalog.contentId}',
|
|
||||||
'mimeType': '${catalog.mimeType}',
|
|
||||||
'dateMillis': '${catalog.dateMillis}',
|
|
||||||
'isFlipped': '${catalog.isFlipped}',
|
|
||||||
'isAnimated': '${catalog.isAnimated}',
|
|
||||||
'videoRotation': '${catalog.videoRotation}',
|
|
||||||
'latitude': '${catalog.latitude}',
|
|
||||||
'longitude': '${catalog.longitude}',
|
|
||||||
'xmpSubjects': '${catalog.xmpSubjects}',
|
|
||||||
'xmpTitleDescription': '${catalog.xmpTitleDescription}',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -312,10 +313,14 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initFutures() {
|
void _loadDatabase() {
|
||||||
_dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
|
_dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
|
||||||
_dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
|
_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));
|
_dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null));
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadMetadata() {
|
||||||
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
|
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
|
||||||
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
|
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
|
||||||
_mediaMetadataLoader = MetadataService.getMediaMetadataRetrieverMetadata(entry);
|
_mediaMetadataLoader = MetadataService.getMediaMetadataRetrieverMetadata(entry);
|
||||||
|
|
|
@ -84,7 +84,7 @@ class BasicSection extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> _buildVideoRows() {
|
Map<String, String> _buildVideoRows() {
|
||||||
final rotation = entry.catalogMetadata?.videoRotation;
|
final rotation = entry.catalogMetadata?.rotationDegrees;
|
||||||
return {
|
return {
|
||||||
'Duration': entry.durationText,
|
'Duration': entry.durationText,
|
||||||
if (rotation != null) 'Rotation': '$rotation°',
|
if (rotation != null) 'Rotation': '$rotation°',
|
||||||
|
|
|
@ -80,7 +80,7 @@ class AvesVideoState extends State<AvesVideo> {
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
);
|
);
|
||||||
|
|
||||||
final degree = entry.catalogMetadata?.videoRotation ?? 0;
|
final degree = entry.catalogMetadata?.rotationDegrees ?? 0;
|
||||||
if (degree != 0) {
|
if (degree != 0) {
|
||||||
child = RotatedBox(
|
child = RotatedBox(
|
||||||
quarterTurns: degree ~/ 90,
|
quarterTurns: degree ~/ 90,
|
||||||
|
|
Loading…
Reference in a new issue