improved metadata initialization from the media store
flipping (WIP)
This commit is contained in:
parent
8fc0a98579
commit
60d16a3e17
22 changed files with 369 additions and 193 deletions
|
@ -95,17 +95,17 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
String uriString = call.argument("uri");
|
|
||||||
String mimeType = call.argument("mimeType");
|
String mimeType = call.argument("mimeType");
|
||||||
if (uriString == null || mimeType == null) {
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
|
||||||
|
if (uri == null || mimeType == null) {
|
||||||
result.error("getImageEntry-args", "failed because of missing arguments", null);
|
result.error("getImageEntry-args", "failed because of missing arguments", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri uri = Uri.parse(uriString);
|
|
||||||
ImageProvider provider = ImageProviderFactory.getProvider(uri);
|
ImageProvider provider = ImageProviderFactory.getProvider(uri);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
result.error("getImageEntry-provider", "failed to find provider for uri=" + uriString, null);
|
result.error("getImageEntry-provider", "failed to find provider for uri=" + uri, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Throwable throwable) {
|
public void onFailure(Throwable throwable) {
|
||||||
result.error("getImageEntry-failure", "failed to get entry for uri=" + uriString, throwable.getMessage());
|
result.error("getImageEntry-failure", "failed to get entry for uri=" + uri, throwable.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import deckers.thibault.aves.utils.ExifInterfaceHelper;
|
import deckers.thibault.aves.utils.ExifInterfaceHelper;
|
||||||
|
import deckers.thibault.aves.utils.MediaMetadataRetrieverHelper;
|
||||||
import deckers.thibault.aves.utils.MetadataHelper;
|
import deckers.thibault.aves.utils.MetadataHelper;
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
import deckers.thibault.aves.utils.MimeTypes;
|
||||||
import deckers.thibault.aves.utils.StorageUtils;
|
import deckers.thibault.aves.utils.StorageUtils;
|
||||||
|
@ -63,6 +64,7 @@ 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_LATITUDE = "latitude";
|
private static final String KEY_LATITUDE = "latitude";
|
||||||
private static final String KEY_LONGITUDE = "longitude";
|
private static final String KEY_LONGITUDE = "longitude";
|
||||||
|
@ -146,6 +148,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
case "getExifInterfaceMetadata":
|
case "getExifInterfaceMetadata":
|
||||||
new Thread(() -> getExifInterfaceMetadata(call, new MethodResultWrapper(result))).start();
|
new Thread(() -> getExifInterfaceMetadata(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
|
case "getMediaMetadataRetrieverMetadata":
|
||||||
|
new Thread(() -> getMediaMetadataRetrieverMetadata(call, new MethodResultWrapper(result))).start();
|
||||||
|
break;
|
||||||
case "getEmbeddedPictures":
|
case "getEmbeddedPictures":
|
||||||
new Thread(() -> getEmbeddedPictures(call, new MethodResultWrapper(result))).start();
|
new Thread(() -> getEmbeddedPictures(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
|
@ -167,49 +172,50 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
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");
|
||||||
String uriString = call.argument("uri");
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
Uri uri = Uri.parse(uriString);
|
|
||||||
|
|
||||||
Map<String, Map<String, String>> metadataMap = new HashMap<>();
|
Map<String, Map<String, String>> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
boolean foundExif = false;
|
boolean foundExif = false;
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
|
||||||
for (Directory dir : metadata.getDirectories()) {
|
|
||||||
if (dir.getTagCount() > 0 && !(dir instanceof FileTypeDirectory)) {
|
|
||||||
foundExif |= dir instanceof ExifDirectoryBase;
|
|
||||||
|
|
||||||
// directory name
|
if (MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
|
||||||
String dirName = dir.getName();
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
Map<String, String> dirMap = Objects.requireNonNull(metadataMap.getOrDefault(dirName, new HashMap<>()));
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
metadataMap.put(dirName, dirMap);
|
for (Directory dir : metadata.getDirectories()) {
|
||||||
|
if (dir.getTagCount() > 0 && !(dir instanceof FileTypeDirectory)) {
|
||||||
|
foundExif |= dir instanceof ExifDirectoryBase;
|
||||||
|
|
||||||
// tags
|
// directory name
|
||||||
for (Tag tag : dir.getTags()) {
|
String dirName = dir.getName();
|
||||||
dirMap.put(tag.getTagName(), tag.getDescription());
|
Map<String, String> dirMap = Objects.requireNonNull(metadataMap.getOrDefault(dirName, new HashMap<>()));
|
||||||
}
|
metadataMap.put(dirName, dirMap);
|
||||||
if (dir instanceof XmpDirectory) {
|
|
||||||
try {
|
// tags
|
||||||
XmpDirectory xmpDir = (XmpDirectory) dir;
|
for (Tag tag : dir.getTags()) {
|
||||||
XMPMeta xmpMeta = xmpDir.getXMPMeta();
|
dirMap.put(tag.getTagName(), tag.getDescription());
|
||||||
xmpMeta.sort();
|
}
|
||||||
XMPIterator xmpIterator = xmpMeta.iterator();
|
if (dir instanceof XmpDirectory) {
|
||||||
while (xmpIterator.hasNext()) {
|
try {
|
||||||
XMPPropertyInfo prop = (XMPPropertyInfo) xmpIterator.next();
|
XmpDirectory xmpDir = (XmpDirectory) dir;
|
||||||
String xmpPath = prop.getPath();
|
XMPMeta xmpMeta = xmpDir.getXMPMeta();
|
||||||
String xmpValue = prop.getValue();
|
xmpMeta.sort();
|
||||||
if (xmpPath != null && !xmpPath.isEmpty() && xmpValue != null && !xmpValue.isEmpty()) {
|
XMPIterator xmpIterator = xmpMeta.iterator();
|
||||||
dirMap.put(xmpPath, xmpValue);
|
while (xmpIterator.hasNext()) {
|
||||||
|
XMPPropertyInfo prop = (XMPPropertyInfo) xmpIterator.next();
|
||||||
|
String xmpPath = prop.getPath();
|
||||||
|
String xmpValue = prop.getValue();
|
||||||
|
if (xmpPath != null && !xmpPath.isEmpty() && xmpValue != null && !xmpValue.isEmpty()) {
|
||||||
|
dirMap.put(xmpPath, xmpValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (XMPException e) {
|
||||||
|
Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uri, e);
|
||||||
}
|
}
|
||||||
} catch (XMPException e) {
|
|
||||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uriString, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception | NoClassDefFoundError e) {
|
||||||
|
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=" + uri, e);
|
||||||
}
|
}
|
||||||
} catch (Exception | NoClassDefFoundError e) {
|
|
||||||
Log.w(LOG_TAG, "failed to get metadata by ImageMetadataReader for uri=" + uriString, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundExif) {
|
if (!foundExif) {
|
||||||
|
@ -218,27 +224,27 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
ExifInterface exif = new ExifInterface(is);
|
ExifInterface exif = new ExifInterface(is);
|
||||||
metadataMap.putAll(ExifInterfaceHelper.describeAll(exif));
|
metadataMap.putAll(ExifInterfaceHelper.describeAll(exif));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=" + uriString, e);
|
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=" + uri, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
Map<String, String> videoDir = getVideoAllMetadataByMediaMetadataRetriever(uriString);
|
Map<String, String> videoDir = getVideoAllMetadataByMediaMetadataRetriever(uri);
|
||||||
if (!videoDir.isEmpty()) {
|
if (!videoDir.isEmpty()) {
|
||||||
metadataMap.put("Video", videoDir);
|
metadataMap.put("Video", videoDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadataMap.isEmpty()) {
|
if (metadataMap.isEmpty()) {
|
||||||
result.error("getAllMetadata-failure", "failed to get metadata for uri=" + uriString, null);
|
result.error("getAllMetadata-failure", "failed to get metadata for uri=" + uri, null);
|
||||||
} else {
|
} else {
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(String uri) {
|
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(Uri uri) {
|
||||||
Map<String, String> dirMap = new HashMap<>();
|
Map<String, String> dirMap = new HashMap<>();
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||||
if (retriever != null) {
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
for (Map.Entry<Integer, String> kv : VIDEO_MEDIA_METADATA_KEYS.entrySet()) {
|
for (Map.Entry<Integer, String> kv : VIDEO_MEDIA_METADATA_KEYS.entrySet()) {
|
||||||
|
@ -268,7 +274,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) {
|
private void getCatalogMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String mimeType = call.argument("mimeType");
|
String mimeType = call.argument("mimeType");
|
||||||
String uri = 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<>(getCatalogMetadataByImageMetadataReader(uri, mimeType, extension));
|
||||||
|
@ -280,13 +286,12 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> getCatalogMetadataByImageMetadataReader(String uri, String mimeType, String extension) {
|
private Map<String, Object> getCatalogMetadataByImageMetadataReader(Uri uri, String mimeType, String extension) {
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
// as of metadata-extractor v2.14.0, MP2T/WBMP files are not supported
|
if (!MimeTypes.isSupportedByMetadataExtractor(mimeType)) return metadataMap;
|
||||||
if (MimeTypes.MP2T.equals(mimeType) || MimeTypes.WBMP.equals(mimeType)) return metadataMap;
|
|
||||||
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
|
||||||
// File type
|
// File type
|
||||||
|
@ -356,14 +361,14 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception | NoClassDefFoundError e) {
|
} catch (Exception | NoClassDefFoundError e) {
|
||||||
Log.w(LOG_TAG, "failed to get catalog metadata by ImageMetadataReader for uri=" + uri + ", mimeType=" + mimeType, e);
|
Log.w(LOG_TAG, "failed to get catalog metadata by metadata-extractor for uri=" + uri + ", mimeType=" + mimeType, e);
|
||||||
}
|
}
|
||||||
return metadataMap;
|
return metadataMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(String uri) {
|
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(Uri uri) {
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||||
if (retriever != null) {
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
||||||
|
@ -411,16 +416,16 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
|
private void getOverlayMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String mimeType = call.argument("mimeType");
|
String mimeType = call.argument("mimeType");
|
||||||
String uri = call.argument("uri");
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType) || !MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
for (ExifSubIFDDirectory directory : metadata.getDirectoriesOfType(ExifSubIFDDirectory.class)) {
|
for (ExifSubIFDDirectory directory : metadata.getDirectoriesOfType(ExifSubIFDDirectory.class)) {
|
||||||
putDescriptionFromTag(metadataMap, KEY_APERTURE, directory, ExifSubIFDDirectory.TAG_FNUMBER);
|
putDescriptionFromTag(metadataMap, KEY_APERTURE, directory, ExifSubIFDDirectory.TAG_FNUMBER);
|
||||||
|
@ -453,13 +458,8 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
private void getContentResolverMetadata(MethodCall call, MethodChannel.Result result) {
|
private void getContentResolverMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String mimeType = call.argument("mimeType");
|
String mimeType = call.argument("mimeType");
|
||||||
String uriString = call.argument("uri");
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
if (mimeType == null || uriString == null) {
|
|
||||||
result.error("getContentResolverMetadata-args", "failed because of missing arguments", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = Uri.parse(uriString);
|
|
||||||
long id = ContentUris.parseId(uri);
|
long id = ContentUris.parseId(uri);
|
||||||
Uri contentUri = uri;
|
Uri contentUri = uri;
|
||||||
if (mimeType.startsWith(MimeTypes.IMAGE)) {
|
if (mimeType.startsWith(MimeTypes.IMAGE)) {
|
||||||
|
@ -509,13 +509,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getExifInterfaceMetadata(MethodCall call, MethodChannel.Result result) {
|
private void getExifInterfaceMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
String uriString = call.argument("uri");
|
Uri uri = Uri.parse(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))) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
ExifInterface exif = new ExifInterface(is);
|
ExifInterface exif = new ExifInterface(is);
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
for (String tag : ExifInterfaceHelper.allTags.keySet()) {
|
for (String tag : ExifInterfaceHelper.allTags.keySet()) {
|
||||||
|
@ -525,12 +521,39 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
result.success(metadataMap);
|
result.success(metadataMap);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=" + uriString, e.getMessage());
|
result.error("getExifInterfaceMetadata-failure", "failed to get exif for uri=" + uri, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getMediaMetadataRetrieverMetadata(MethodCall call, MethodChannel.Result result) {
|
||||||
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
|
||||||
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||||
|
if (retriever == null) {
|
||||||
|
result.error("getMediaMetadataRetrieverMetadata-null", "failed to open retriever for uri=" + uri, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, String> metadataMap = new HashMap<>();
|
||||||
|
for (Map.Entry<String, Integer> kv : MediaMetadataRetrieverHelper.allKeys.entrySet()) {
|
||||||
|
String value = retriever.extractMetadata(kv.getValue());
|
||||||
|
if (value != null) {
|
||||||
|
metadataMap.put(kv.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(metadataMap);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.error("getMediaMetadataRetrieverMetadata-failure", "failed to extract metadata for uri=" + uri, e.getMessage());
|
||||||
|
} finally {
|
||||||
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
|
retriever.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getEmbeddedPictures(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
private void getEmbeddedPictures(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
Uri uri = Uri.parse(call.argument("uri"));
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
|
||||||
List<byte[]> pictures = new ArrayList<>();
|
List<byte[]> pictures = new ArrayList<>();
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||||
if (retriever != null) {
|
if (retriever != null) {
|
||||||
|
@ -551,6 +574,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
private void getExifThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
private void getExifThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
Uri uri = Uri.parse(call.argument("uri"));
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
|
||||||
List<byte[]> thumbnails = new ArrayList<>();
|
List<byte[]> thumbnails = new ArrayList<>();
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
ExifInterface exif = new ExifInterface(is);
|
ExifInterface exif = new ExifInterface(is);
|
||||||
|
@ -560,33 +584,41 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(LOG_TAG, "failed to extract exif thumbnail with ExifInterface for uri=" + uri, e);
|
Log.w(LOG_TAG, "failed to extract exif thumbnail with ExifInterface for uri=" + uri, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.success(thumbnails);
|
result.success(thumbnails);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getXmpThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
private void getXmpThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
|
String mimeType = call.argument("mimeType");
|
||||||
Uri uri = Uri.parse(call.argument("uri"));
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
|
||||||
|
if (uri == null || mimeType == null) {
|
||||||
|
result.error("getXmpThumbnails-args", "failed because of missing arguments", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<byte[]> thumbnails = new ArrayList<>();
|
List<byte[]> thumbnails = new ArrayList<>();
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
if (MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
for (XmpDirectory dir : metadata.getDirectoriesOfType(XmpDirectory.class)) {
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
XMPMeta xmpMeta = dir.getXMPMeta();
|
for (XmpDirectory dir : metadata.getDirectoriesOfType(XmpDirectory.class)) {
|
||||||
try {
|
XMPMeta xmpMeta = dir.getXMPMeta();
|
||||||
if (xmpMeta.doesPropertyExist(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME)) {
|
try {
|
||||||
int count = xmpMeta.countArrayItems(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME);
|
if (xmpMeta.doesPropertyExist(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME)) {
|
||||||
for (int i = 1; i < count + 1; i++) {
|
int count = xmpMeta.countArrayItems(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME);
|
||||||
XMPProperty image = xmpMeta.getStructField(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME + "[" + i + "]", XMP_IMG_SCHEMA_NS, XMP_THUMBNAIL_IMAGE_PROP_NAME);
|
for (int i = 1; i < count + 1; i++) {
|
||||||
if (image != null) {
|
XMPProperty image = xmpMeta.getStructField(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME + "[" + i + "]", XMP_IMG_SCHEMA_NS, XMP_THUMBNAIL_IMAGE_PROP_NAME);
|
||||||
thumbnails.add(XMPUtils.decodeBase64(image.getValue()));
|
if (image != null) {
|
||||||
|
thumbnails.add(XMPUtils.decodeBase64(image.getValue()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (XMPException e) {
|
||||||
|
Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uri, e);
|
||||||
}
|
}
|
||||||
} catch (XMPException e) {
|
|
||||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uri, e);
|
|
||||||
}
|
}
|
||||||
|
} catch (IOException | ImageProcessingException | NoClassDefFoundError e) {
|
||||||
|
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e);
|
||||||
}
|
}
|
||||||
} catch (IOException | ImageProcessingException | NoClassDefFoundError e) {
|
|
||||||
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e);
|
|
||||||
}
|
}
|
||||||
result.success(thumbnails);
|
result.success(thumbnails);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
private Activity activity;
|
private Activity activity;
|
||||||
private Uri uri;
|
private Uri uri;
|
||||||
private String mimeType;
|
private String mimeType;
|
||||||
private int orientationDegrees;
|
private int rotationDegrees;
|
||||||
private EventChannel.EventSink eventSink;
|
private EventChannel.EventSink eventSink;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
Map<String, Object> argMap = (Map<String, Object>) arguments;
|
Map<String, Object> argMap = (Map<String, Object>) arguments;
|
||||||
this.mimeType = (String) argMap.get("mimeType");
|
this.mimeType = (String) argMap.get("mimeType");
|
||||||
this.uri = Uri.parse((String) argMap.get("uri"));
|
this.uri = Uri.parse((String) argMap.get("uri"));
|
||||||
this.orientationDegrees = (int) argMap.get("orientationDegrees");
|
this.rotationDegrees = (int) argMap.get("rotationDegrees");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
Bitmap bitmap = target.get();
|
Bitmap bitmap = target.get();
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
// TODO TLAD use exif orientation to rotate & flip?
|
// TODO TLAD use exif orientation to rotate & flip?
|
||||||
bitmap = TransformationUtils.rotateImage(bitmap, orientationDegrees);
|
bitmap = TransformationUtils.rotateImage(bitmap, rotationDegrees);
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG
|
// Bitmap.CompressFormat.PNG is slower than JPEG
|
||||||
|
|
|
@ -23,7 +23,7 @@ public class AvesImageEntry {
|
||||||
this.mimeType = (String) map.get("mimeType");
|
this.mimeType = (String) map.get("mimeType");
|
||||||
this.width = (Integer) map.get("width");
|
this.width = (Integer) map.get("width");
|
||||||
this.height = (Integer) map.get("height");
|
this.height = (Integer) map.get("height");
|
||||||
this.rotationDegrees = (Integer) map.get("orientationDegrees");
|
this.rotationDegrees = (Integer) map.get("rotationDegrees");
|
||||||
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
|
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ public class SourceImageEntry {
|
||||||
@Nullable
|
@Nullable
|
||||||
public Integer width, height, rotationDegrees;
|
public Integer width, height, rotationDegrees;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
public Boolean isFlipped;
|
||||||
|
@Nullable
|
||||||
public Long sizeBytes;
|
public Long sizeBytes;
|
||||||
@Nullable
|
@Nullable
|
||||||
public Long dateModifiedSecs;
|
public Long dateModifiedSecs;
|
||||||
|
@ -74,7 +76,8 @@ public class SourceImageEntry {
|
||||||
put("sourceMimeType", sourceMimeType);
|
put("sourceMimeType", sourceMimeType);
|
||||||
put("width", width);
|
put("width", width);
|
||||||
put("height", height);
|
put("height", height);
|
||||||
put("orientationDegrees", rotationDegrees != null ? rotationDegrees : 0);
|
put("rotationDegrees", rotationDegrees != null ? rotationDegrees : 0);
|
||||||
|
put("isFlipped", isFlipped != null ? isFlipped : false);
|
||||||
put("sizeBytes", sizeBytes);
|
put("sizeBytes", sizeBytes);
|
||||||
put("title", title);
|
put("title", title);
|
||||||
put("dateModifiedSecs", dateModifiedSecs);
|
put("dateModifiedSecs", dateModifiedSecs);
|
||||||
|
@ -101,6 +104,10 @@ public class SourceImageEntry {
|
||||||
return width != null && width > 0 && height != null && height > 0;
|
return width != null && width > 0 && height != null && height > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasOrientation() {
|
||||||
|
return rotationDegrees != null;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasDuration() {
|
private boolean hasDuration() {
|
||||||
return durationMillis != null && durationMillis > 0;
|
return durationMillis != null && durationMillis > 0;
|
||||||
}
|
}
|
||||||
|
@ -122,8 +129,9 @@ public class SourceImageEntry {
|
||||||
// expects entry with: uri, mimeType
|
// expects entry with: uri, mimeType
|
||||||
// finds: width, height, orientation/rotation, date, title, duration
|
// finds: width, height, orientation/rotation, date, title, duration
|
||||||
public SourceImageEntry fillPreCatalogMetadata(@NonNull Context context) {
|
public SourceImageEntry fillPreCatalogMetadata(@NonNull Context context) {
|
||||||
|
if (isSvg()) return this;
|
||||||
fillByMediaMetadataRetriever(context);
|
fillByMediaMetadataRetriever(context);
|
||||||
if (hasSize() && (!isVideo() || hasDuration())) return this;
|
if (hasSize() && hasOrientation() && (!isVideo() || hasDuration())) return this;
|
||||||
fillByMetadataExtractor(context);
|
fillByMetadataExtractor(context);
|
||||||
if (hasSize()) return this;
|
if (hasSize()) return this;
|
||||||
fillByBitmapDecode(context);
|
fillByBitmapDecode(context);
|
||||||
|
@ -133,6 +141,8 @@ public class SourceImageEntry {
|
||||||
// expects entry with: uri, mimeType
|
// expects entry with: uri, mimeType
|
||||||
// finds: width, height, orientation/rotation, date, title, duration
|
// finds: width, height, orientation/rotation, date, title, duration
|
||||||
private void fillByMediaMetadataRetriever(@NonNull Context context) {
|
private void fillByMediaMetadataRetriever(@NonNull Context context) {
|
||||||
|
if (isImage()) return;
|
||||||
|
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||||
if (retriever != null) {
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -185,75 +195,72 @@ public class SourceImageEntry {
|
||||||
// expects entry with: uri, mimeType
|
// expects entry with: uri, mimeType
|
||||||
// finds: width, height, orientation, date
|
// finds: width, height, orientation, date
|
||||||
private void fillByMetadataExtractor(@NonNull Context context) {
|
private void fillByMetadataExtractor(@NonNull Context context) {
|
||||||
if (isSvg()) return;
|
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) return;
|
||||||
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
|
||||||
switch (sourceMimeType) {
|
// do not switch on specific mime types, as the reported mime type could be wrong
|
||||||
case MimeTypes.JPEG:
|
// (e.g. PNG registered as JPG)
|
||||||
for (JpegDirectory dir : metadata.getDirectoriesOfType(JpegDirectory.class)) {
|
if (isVideo()) {
|
||||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) {
|
for (AviDirectory dir : metadata.getDirectoriesOfType(AviDirectory.class)) {
|
||||||
width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH);
|
if (dir.containsTag(AviDirectory.TAG_WIDTH)) {
|
||||||
}
|
width = dir.getInt(AviDirectory.TAG_WIDTH);
|
||||||
if (dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
|
|
||||||
height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
if (dir.containsTag(AviDirectory.TAG_HEIGHT)) {
|
||||||
case MimeTypes.MP4:
|
height = dir.getInt(AviDirectory.TAG_HEIGHT);
|
||||||
for (Mp4VideoDirectory dir : metadata.getDirectoriesOfType(Mp4VideoDirectory.class)) {
|
|
||||||
if (dir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
|
||||||
width = dir.getInt(Mp4VideoDirectory.TAG_WIDTH);
|
|
||||||
}
|
|
||||||
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
|
||||||
height = dir.getInt(Mp4VideoDirectory.TAG_HEIGHT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (Mp4Directory dir : metadata.getDirectoriesOfType(Mp4Directory.class)) {
|
if (dir.containsTag(AviDirectory.TAG_DURATION)) {
|
||||||
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
|
durationMillis = dir.getLong(AviDirectory.TAG_DURATION);
|
||||||
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case MimeTypes.AVI:
|
for (Mp4VideoDirectory dir : metadata.getDirectoriesOfType(Mp4VideoDirectory.class)) {
|
||||||
for (AviDirectory dir : metadata.getDirectoriesOfType(AviDirectory.class)) {
|
if (dir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
||||||
if (dir.containsTag(AviDirectory.TAG_WIDTH)) {
|
width = dir.getInt(Mp4VideoDirectory.TAG_WIDTH);
|
||||||
width = dir.getInt(AviDirectory.TAG_WIDTH);
|
|
||||||
}
|
|
||||||
if (dir.containsTag(AviDirectory.TAG_HEIGHT)) {
|
|
||||||
height = dir.getInt(AviDirectory.TAG_HEIGHT);
|
|
||||||
}
|
|
||||||
if (dir.containsTag(AviDirectory.TAG_DURATION)) {
|
|
||||||
durationMillis = dir.getLong(AviDirectory.TAG_DURATION);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
||||||
case MimeTypes.PSD:
|
height = dir.getInt(Mp4VideoDirectory.TAG_HEIGHT);
|
||||||
for (PsdHeaderDirectory dir : metadata.getDirectoriesOfType(PsdHeaderDirectory.class)) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
for (Mp4Directory dir : metadata.getDirectoriesOfType(Mp4Directory.class)) {
|
||||||
|
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
|
||||||
|
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (JpegDirectory dir : metadata.getDirectoriesOfType(JpegDirectory.class)) {
|
||||||
|
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 (PsdHeaderDirectory dir : metadata.getDirectoriesOfType(PsdHeaderDirectory.class)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) {
|
// EXIF, if defined, should override metadata found in other directories
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_WIDTH)) {
|
for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) {
|
||||||
width = dir.getInt(ExifIFD0Directory.TAG_IMAGE_WIDTH);
|
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_WIDTH)) {
|
||||||
}
|
width = dir.getInt(ExifIFD0Directory.TAG_IMAGE_WIDTH);
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_HEIGHT)) {
|
}
|
||||||
height = dir.getInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT);
|
if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_HEIGHT)) {
|
||||||
}
|
height = dir.getInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT);
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
}
|
||||||
int exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||||
rotationDegrees = MetadataHelper.getRotationDegreesForExifCode(exifOrientation);
|
int exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
||||||
}
|
rotationDegrees = MetadataHelper.getRotationDegreesForExifCode(exifOrientation);
|
||||||
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
|
isFlipped = MetadataHelper.isFlippedForExifCode(exifOrientation);
|
||||||
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime();
|
}
|
||||||
|
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
|
||||||
|
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException | ImageProcessingException | MetadataException | NoClassDefFoundError e) {
|
} catch (IOException | ImageProcessingException | MetadataException | NoClassDefFoundError e) {
|
||||||
|
@ -264,8 +271,6 @@ public class SourceImageEntry {
|
||||||
// expects entry with: uri
|
// expects entry with: uri
|
||||||
// finds: width, height
|
// finds: width, height
|
||||||
private void fillByBitmapDecode(@NonNull Context context) {
|
private void fillByBitmapDecode(@NonNull Context context) {
|
||||||
if (isSvg()) return;
|
|
||||||
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
options.inJustDecodeBounds = true;
|
options.inJustDecodeBounds = true;
|
||||||
|
|
|
@ -130,7 +130,8 @@ public abstract class ImageProvider {
|
||||||
// copy the edited temporary file back to the original
|
// copy the edited temporary file back to the original
|
||||||
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(originalDocumentFile);
|
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(originalDocumentFile);
|
||||||
|
|
||||||
newFields.put("orientationDegrees", exif.getRotationDegrees());
|
newFields.put("rotationDegrees", exif.getRotationDegrees());
|
||||||
|
newFields.put("isFlipped", exif.isFlipped());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
callback.onFailure(e);
|
callback.onFailure(e);
|
||||||
return;
|
return;
|
||||||
|
@ -147,7 +148,7 @@ public abstract class ImageProvider {
|
||||||
// values.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
// values.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
||||||
// }
|
// }
|
||||||
// // uses MediaStore.Images.Media instead of MediaStore.MediaColumns for APIs < Q
|
// // uses MediaStore.Images.Media instead of MediaStore.MediaColumns for APIs < Q
|
||||||
// values.put(MediaStore.Images.Media.ORIENTATION, orientationDegrees);
|
// values.put(MediaStore.Images.Media.ORIENTATION, rotationDegrees);
|
||||||
// // TODO TLAD catch RecoverableSecurityException
|
// // TODO TLAD catch RecoverableSecurityException
|
||||||
// int updatedRowCount = contentResolver.update(uri, values, null, null);
|
// int updatedRowCount = contentResolver.update(uri, values, null, null);
|
||||||
// if (updatedRowCount > 0) {
|
// if (updatedRowCount > 0) {
|
||||||
|
|
|
@ -124,9 +124,14 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private int fetchFrom(final Context context, NewEntryChecker newEntryChecker, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) {
|
private int fetchFrom(final Context context, NewEntryChecker newEntryChecker, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) {
|
||||||
int newEntryCount = 0;
|
int newEntryCount = 0;
|
||||||
final boolean needDuration = projection == VIDEO_PROJECTION;
|
|
||||||
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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, orderBy);
|
Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, orderBy);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
|
@ -159,11 +164,18 @@ 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`
|
||||||
|
// this can happen for specific formats (e.g. for PNG, WEBP)
|
||||||
|
// 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("rotationDegrees", 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);
|
||||||
|
@ -174,8 +186,11 @@ 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)) || (durationMillis == 0 && needDuration)) {
|
if (((width <= 0 || height <= 0) && needSize(mimeType))
|
||||||
|
|| (rotationDegrees == null && needOrientation)
|
||||||
|
|| (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
|
||||||
SourceImageEntry entry = new SourceImageEntry(entryMap).fillPreCatalogMetadata(context);
|
SourceImageEntry entry = new SourceImageEntry(entryMap).fillPreCatalogMetadata(context);
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package deckers.thibault.aves.utils
|
||||||
|
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
object MediaMetadataRetrieverHelper {
|
||||||
|
@JvmField
|
||||||
|
val allKeys: Map<String, Int> = hashMapOf(
|
||||||
|
"METADATA_KEY_ALBUM" to MediaMetadataRetriever.METADATA_KEY_ALBUM,
|
||||||
|
"METADATA_KEY_ALBUMARTIST" to MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
|
||||||
|
"METADATA_KEY_ARTIST" to MediaMetadataRetriever.METADATA_KEY_ARTIST,
|
||||||
|
"METADATA_KEY_AUTHOR" to MediaMetadataRetriever.METADATA_KEY_AUTHOR,
|
||||||
|
"METADATA_KEY_BITRATE" to MediaMetadataRetriever.METADATA_KEY_BITRATE,
|
||||||
|
"METADATA_KEY_CAPTURE_FRAMERATE" to MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE,
|
||||||
|
"METADATA_KEY_CD_TRACK_NUMBER" to MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER,
|
||||||
|
"METADATA_KEY_COLOR_RANGE" to MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE,
|
||||||
|
"METADATA_KEY_COLOR_STANDARD" to MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD,
|
||||||
|
"METADATA_KEY_COLOR_TRANSFER" to MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER,
|
||||||
|
"METADATA_KEY_COMPILATION" to MediaMetadataRetriever.METADATA_KEY_COMPILATION,
|
||||||
|
"METADATA_KEY_COMPOSER" to MediaMetadataRetriever.METADATA_KEY_COMPOSER,
|
||||||
|
"METADATA_KEY_DATE" to MediaMetadataRetriever.METADATA_KEY_DATE,
|
||||||
|
"METADATA_KEY_DISC_NUMBER" to MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER,
|
||||||
|
"METADATA_KEY_DURATION" to MediaMetadataRetriever.METADATA_KEY_DURATION,
|
||||||
|
"METADATA_KEY_EXIF_LENGTH" to MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH,
|
||||||
|
"METADATA_KEY_EXIF_OFFSET" to MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET,
|
||||||
|
"METADATA_KEY_GENRE" to MediaMetadataRetriever.METADATA_KEY_GENRE,
|
||||||
|
"METADATA_KEY_HAS_AUDIO" to MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO,
|
||||||
|
"METADATA_KEY_HAS_VIDEO" to MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO,
|
||||||
|
"METADATA_KEY_LOCATION" to MediaMetadataRetriever.METADATA_KEY_LOCATION,
|
||||||
|
"METADATA_KEY_MIMETYPE" to MediaMetadataRetriever.METADATA_KEY_MIMETYPE,
|
||||||
|
"METADATA_KEY_NUM_TRACKS" to MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS,
|
||||||
|
"METADATA_KEY_TITLE" to MediaMetadataRetriever.METADATA_KEY_TITLE,
|
||||||
|
"METADATA_KEY_VIDEO_HEIGHT" to MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT,
|
||||||
|
"METADATA_KEY_VIDEO_ROTATION" to MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION,
|
||||||
|
"METADATA_KEY_VIDEO_WIDTH" to MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH,
|
||||||
|
"METADATA_KEY_WRITER" to MediaMetadataRetriever.METADATA_KEY_WRITER,
|
||||||
|
"METADATA_KEY_YEAR" to MediaMetadataRetriever.METADATA_KEY_YEAR,
|
||||||
|
).apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
putAll(hashMapOf(
|
||||||
|
"METADATA_KEY_HAS_IMAGE" to MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE,
|
||||||
|
"METADATA_KEY_IMAGE_COUNT" to MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT,
|
||||||
|
"METADATA_KEY_IMAGE_HEIGHT" to MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT,
|
||||||
|
"METADATA_KEY_IMAGE_PRIMARY" to MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY,
|
||||||
|
"METADATA_KEY_IMAGE_ROTATION" to MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION,
|
||||||
|
"METADATA_KEY_IMAGE_WIDTH" to MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH,
|
||||||
|
"METADATA_KEY_VIDEO_FRAME_COUNT" to MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,13 @@ object MimeTypes {
|
||||||
const val MOV = "video/quicktime"
|
const val MOV = "video/quicktime"
|
||||||
const val MP2T = "video/mp2t"
|
const val MP2T = "video/mp2t"
|
||||||
const val MP4 = "video/mp4"
|
const val MP4 = "video/mp4"
|
||||||
|
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
|
||||||
|
fun isSupportedByMetadataExtractor(mimeType: String) = !unsupportedMetadataExtractorFormats.contains(mimeType)
|
||||||
|
|
||||||
@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 orientationDegrees;
|
int rotationDegrees;
|
||||||
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.orientationDegrees,
|
this.rotationDegrees,
|
||||||
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,
|
||||||
orientationDegrees: orientationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
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,
|
||||||
orientationDegrees: map['orientationDegrees'] as int ?? 0,
|
rotationDegrees: map['rotationDegrees'] 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,
|
||||||
'orientationDegrees': orientationDegrees,
|
'rotationDegrees': rotationDegrees,
|
||||||
'sizeBytes': sizeBytes,
|
'sizeBytes': sizeBytes,
|
||||||
'title': sourceTitle,
|
'title': sourceTitle,
|
||||||
'dateModifiedSecs': dateModifiedSecs,
|
'dateModifiedSecs': dateModifiedSecs,
|
||||||
|
@ -171,6 +171,8 @@ 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 canEdit => path != null;
|
bool get canEdit => path != null;
|
||||||
|
@ -192,7 +194,7 @@ class ImageEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get portrait => ((isVideo && isCatalogued) ? _catalogMetadata.videoRotation : orientationDegrees) % 180 == 90;
|
bool get portrait => ((isVideo && isCatalogued) ? _catalogMetadata.videoRotation : rotationDegrees) % 180 == 90;
|
||||||
|
|
||||||
double get displayAspectRatio {
|
double get displayAspectRatio {
|
||||||
if (width == 0 || height == 0) return 1;
|
if (width == 0 || height == 0) return 1;
|
||||||
|
@ -369,8 +371,8 @@ class ImageEntry {
|
||||||
if (width is int) this.width = width;
|
if (width is int) this.width = width;
|
||||||
final height = newFields['height'];
|
final height = newFields['height'];
|
||||||
if (height is int) this.height = height;
|
if (height is int) this.height = height;
|
||||||
final orientationDegrees = newFields['orientationDegrees'];
|
final rotationDegrees = newFields['rotationDegrees'];
|
||||||
if (orientationDegrees is int) this.orientationDegrees = orientationDegrees;
|
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
|
||||||
|
|
||||||
imageChangeNotifier.notifyListeners();
|
imageChangeNotifier.notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -29,7 +29,7 @@ class DateMetadata {
|
||||||
|
|
||||||
class CatalogMetadata {
|
class CatalogMetadata {
|
||||||
final int contentId, dateMillis, videoRotation;
|
final int contentId, dateMillis, videoRotation;
|
||||||
final bool isAnimated;
|
final bool isFlipped, isAnimated;
|
||||||
final String mimeType, xmpSubjects, xmpTitleDescription;
|
final String mimeType, xmpSubjects, xmpTitleDescription;
|
||||||
final double latitude, longitude;
|
final double latitude, longitude;
|
||||||
Address address;
|
Address address;
|
||||||
|
@ -38,6 +38,7 @@ class CatalogMetadata {
|
||||||
this.contentId,
|
this.contentId,
|
||||||
this.mimeType,
|
this.mimeType,
|
||||||
this.dateMillis,
|
this.dateMillis,
|
||||||
|
this.isFlipped,
|
||||||
this.isAnimated,
|
this.isAnimated,
|
||||||
this.videoRotation,
|
this.videoRotation,
|
||||||
this.xmpSubjects,
|
this.xmpSubjects,
|
||||||
|
@ -56,6 +57,7 @@ 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,
|
videoRotation: videoRotation,
|
||||||
xmpSubjects: xmpSubjects,
|
xmpSubjects: xmpSubjects,
|
||||||
|
@ -66,11 +68,13 @@ 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);
|
||||||
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,
|
videoRotation: map['videoRotation'] ?? 0,
|
||||||
xmpSubjects: map['xmpSubjects'] ?? '',
|
xmpSubjects: map['xmpSubjects'] ?? '',
|
||||||
|
@ -84,6 +88,7 @@ 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,
|
'videoRotation': videoRotation,
|
||||||
'xmpSubjects': xmpSubjects,
|
'xmpSubjects': xmpSubjects,
|
||||||
|
@ -94,7 +99,7 @@ class CatalogMetadata {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CatalogMetadata{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, videoRotation=$videoRotation, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}';
|
return 'CatalogMetadata{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isFlipped=$isFlipped, isAnimated=$isAnimated, videoRotation=$videoRotation, 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'
|
||||||
', orientationDegrees INTEGER'
|
', rotationDegrees INTEGER'
|
||||||
', sizeBytes INTEGER'
|
', sizeBytes INTEGER'
|
||||||
', title TEXT'
|
', title TEXT'
|
||||||
', dateModifiedSecs INTEGER'
|
', dateModifiedSecs INTEGER'
|
||||||
|
@ -48,6 +48,7 @@ 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'
|
', videoRotation INTEGER'
|
||||||
', xmpSubjects TEXT'
|
', xmpSubjects TEXT'
|
||||||
|
@ -68,7 +69,43 @@ class MetadataDb {
|
||||||
', path TEXT'
|
', path TEXT'
|
||||||
')');
|
')');
|
||||||
},
|
},
|
||||||
version: 1,
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
|
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
|
||||||
|
// on SQLite <3.25.0, bundled on older Android devices
|
||||||
|
while (oldVersion < newVersion) {
|
||||||
|
if (oldVersion == 1) {
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
// rename column 'orientationDegrees' to 'rotationDegrees'
|
||||||
|
const newEntryTable = '${entryTable}TEMP';
|
||||||
|
await db.execute('CREATE TABLE $newEntryTable('
|
||||||
|
'contentId INTEGER PRIMARY KEY'
|
||||||
|
', uri TEXT'
|
||||||
|
', path TEXT'
|
||||||
|
', sourceMimeType TEXT'
|
||||||
|
', width INTEGER'
|
||||||
|
', height INTEGER'
|
||||||
|
', rotationDegrees INTEGER'
|
||||||
|
', sizeBytes INTEGER'
|
||||||
|
', title TEXT'
|
||||||
|
', dateModifiedSecs INTEGER'
|
||||||
|
', sourceDateTakenMillis INTEGER'
|
||||||
|
', durationMillis INTEGER'
|
||||||
|
')');
|
||||||
|
await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,rotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
||||||
|
' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
||||||
|
' FROM $entryTable;');
|
||||||
|
await db.execute('DROP TABLE $entryTable;');
|
||||||
|
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
||||||
|
});
|
||||||
|
|
||||||
|
// new column 'isFlipped'
|
||||||
|
await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;');
|
||||||
|
|
||||||
|
oldVersion++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
version: 2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ImageFileService {
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'width': entry.width,
|
'width': entry.width,
|
||||||
'height': entry.height,
|
'height': entry.height,
|
||||||
'orientationDegrees': entry.orientationDegrees,
|
'rotationDegrees': entry.rotationDegrees,
|
||||||
'dateModifiedSecs': entry.dateModifiedSecs,
|
'dateModifiedSecs': entry.dateModifiedSecs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ class ImageFileService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getImage(String uri, String mimeType, {int orientationDegrees, int expectedContentLength, BytesReceivedCallback onBytesReceived}) {
|
static Future<Uint8List> getImage(String uri, String mimeType, {int rotationDegrees, int expectedContentLength, BytesReceivedCallback onBytesReceived}) {
|
||||||
try {
|
try {
|
||||||
final completer = Completer<Uint8List>.sync();
|
final completer = Completer<Uint8List>.sync();
|
||||||
final sink = _OutputBuffer();
|
final sink = _OutputBuffer();
|
||||||
|
@ -74,7 +74,7 @@ class ImageFileService {
|
||||||
byteChannel.receiveBroadcastStream(<String, dynamic>{
|
byteChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'orientationDegrees': orientationDegrees ?? 0,
|
'rotationDegrees': rotationDegrees ?? 0,
|
||||||
}).listen(
|
}).listen(
|
||||||
(data) {
|
(data) {
|
||||||
final chunk = data as Uint8List;
|
final chunk = data as Uint8List;
|
||||||
|
@ -183,7 +183,7 @@ class ImageFileService {
|
||||||
|
|
||||||
static Future<Map> rotate(ImageEntry entry, {@required bool clockwise}) async {
|
static Future<Map> rotate(ImageEntry entry, {@required bool clockwise}) async {
|
||||||
try {
|
try {
|
||||||
// return map with: 'width' 'height' 'orientationDegrees' (all optional)
|
// return map with: 'width' 'height' 'rotationDegrees' (all optional)
|
||||||
final result = await platform.invokeMethod('rotate', <String, dynamic>{
|
final result = await platform.invokeMethod('rotate', <String, dynamic>{
|
||||||
'entry': _toPlatformEntryMap(entry),
|
'entry': _toPlatformEntryMap(entry),
|
||||||
'clockwise': clockwise,
|
'clockwise': clockwise,
|
||||||
|
|
|
@ -33,6 +33,7 @@ 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)
|
||||||
// 'latitude': latitude (double)
|
// 'latitude': latitude (double)
|
||||||
// 'longitude': longitude (double)
|
// 'longitude': longitude (double)
|
||||||
|
@ -103,6 +104,19 @@ class MetadataService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Map> getMediaMetadataRetrieverMetadata(ImageEntry entry) async {
|
||||||
|
try {
|
||||||
|
// return map with all data available from the MediaMetadataRetriever
|
||||||
|
final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
|
||||||
|
'uri': entry.uri,
|
||||||
|
}) as Map;
|
||||||
|
return result;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getMediaMetadataRetrieverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
||||||
|
@ -127,10 +141,11 @@ class MetadataService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getXmpThumbnails(String uri) async {
|
static Future<List<Uint8List>> getXmpThumbnails(ImageEntry entry) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getXmpThumbnails', <String, dynamic>{
|
final result = await platform.invokeMethod('getXmpThumbnails', <String, dynamic>{
|
||||||
'uri': uri,
|
'mimeType': entry.mimeType,
|
||||||
|
'uri': entry.uri,
|
||||||
});
|
});
|
||||||
return (result as List).cast<Uint8List>();
|
return (result as List).cast<Uint8List>();
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
|
|
@ -138,7 +138,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
final imageProvider = UriImage(
|
final imageProvider = UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
orientationDegrees: entry.orientationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
);
|
);
|
||||||
if (imageCache.statusForKey(imageProvider).keepAlive) {
|
if (imageCache.statusForKey(imageProvider).keepAlive) {
|
||||||
|
|
|
@ -75,13 +75,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
Future<void> _print(ImageEntry entry) async {
|
Future<void> _print(ImageEntry entry) async {
|
||||||
final uri = entry.uri;
|
final uri = entry.uri;
|
||||||
final mimeType = entry.mimeType;
|
final mimeType = entry.mimeType;
|
||||||
final orientationDegrees = entry.orientationDegrees;
|
final rotationDegrees = entry.rotationDegrees;
|
||||||
final documentName = entry.bestTitle ?? 'Aves';
|
final documentName = entry.bestTitle ?? 'Aves';
|
||||||
final doc = pdf.Document(title: documentName);
|
final doc = pdf.Document(title: documentName);
|
||||||
|
|
||||||
PdfImage pdfImage;
|
PdfImage pdfImage;
|
||||||
if (entry.isSvg) {
|
if (entry.isSvg) {
|
||||||
final bytes = await ImageFileService.getImage(uri, mimeType, orientationDegrees: entry.orientationDegrees);
|
final bytes = await ImageFileService.getImage(uri, mimeType, rotationDegrees: entry.rotationDegrees);
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
if (bytes != null && bytes.isNotEmpty) {
|
||||||
final svgRoot = await svg.fromSvgBytes(bytes, uri);
|
final svgRoot = await svg.fromSvgBytes(bytes, uri);
|
||||||
final viewBox = svgRoot.viewport.viewBox;
|
final viewBox = svgRoot.viewport.viewBox;
|
||||||
|
@ -100,7 +100,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
image: UriImage(
|
image: UriImage(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
orientationDegrees: orientationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,14 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
const UriImage({
|
const UriImage({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
@required this.orientationDegrees,
|
@required this.rotationDegrees,
|
||||||
this.expectedContentLength,
|
this.expectedContentLength,
|
||||||
this.scale = 1.0,
|
this.scale = 1.0,
|
||||||
}) : assert(uri != null),
|
}) : assert(uri != null),
|
||||||
assert(scale != null);
|
assert(scale != null);
|
||||||
|
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int orientationDegrees, expectedContentLength;
|
final int rotationDegrees, expectedContentLength;
|
||||||
final double scale;
|
final double scale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -46,7 +46,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
final bytes = await ImageFileService.getImage(
|
final bytes = await ImageFileService.getImage(
|
||||||
uri,
|
uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
orientationDegrees: orientationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
expectedContentLength: expectedContentLength,
|
expectedContentLength: expectedContentLength,
|
||||||
onBytesReceived: (cumulative, total) {
|
onBytesReceived: (cumulative, total) {
|
||||||
chunkEvents.add(ImageChunkEvent(
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
|
|
@ -30,7 +30,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
Future<DateMetadata> _dbDateLoader;
|
Future<DateMetadata> _dbDateLoader;
|
||||||
Future<CatalogMetadata> _dbMetadataLoader;
|
Future<CatalogMetadata> _dbMetadataLoader;
|
||||||
Future<AddressDetails> _dbAddressLoader;
|
Future<AddressDetails> _dbAddressLoader;
|
||||||
Future<Map> _contentResolverMetadataLoader, _exifInterfaceMetadataLoader;
|
Future<Map> _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
@ -89,25 +89,21 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
'sourceTitle': '${entry.sourceTitle}',
|
'sourceTitle': '${entry.sourceTitle}',
|
||||||
'sourceMimeType': '${entry.sourceMimeType}',
|
'sourceMimeType': '${entry.sourceMimeType}',
|
||||||
'mimeType': '${entry.mimeType}',
|
'mimeType': '${entry.mimeType}',
|
||||||
'mimeTypeAnySubtype': '${entry.mimeTypeAnySubtype}',
|
|
||||||
}),
|
}),
|
||||||
Divider(),
|
Divider(),
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000),
|
'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000),
|
||||||
'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis),
|
'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis),
|
||||||
'bestDate': '${entry.bestDate}',
|
'bestDate': '${entry.bestDate}',
|
||||||
'monthTaken': '${entry.monthTaken}',
|
|
||||||
'dayTaken': '${entry.dayTaken}',
|
|
||||||
}),
|
}),
|
||||||
Divider(),
|
Divider(),
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'width': '${entry.width}',
|
'width': '${entry.width}',
|
||||||
'height': '${entry.height}',
|
'height': '${entry.height}',
|
||||||
'orientationDegrees': '${entry.orientationDegrees}',
|
'rotationDegrees': '${entry.rotationDegrees}',
|
||||||
'portrait': '${entry.portrait}',
|
'portrait': '${entry.portrait}',
|
||||||
'displayAspectRatio': '${entry.displayAspectRatio}',
|
'displayAspectRatio': '${entry.displayAspectRatio}',
|
||||||
'displaySize': '${entry.displaySize}',
|
'displaySize': '${entry.displaySize}',
|
||||||
'megaPixels': '${entry.megaPixels}',
|
|
||||||
}),
|
}),
|
||||||
Divider(),
|
Divider(),
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
|
@ -122,8 +118,10 @@ 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}',
|
||||||
'canEdit': '${entry.canEdit}',
|
'canEdit': '${entry.canEdit}',
|
||||||
|
'canEditExif': '${entry.canEditExif}',
|
||||||
'canPrint': '${entry.canPrint}',
|
'canPrint': '${entry.canPrint}',
|
||||||
'canRotate': '${entry.canRotate}',
|
'canRotate': '${entry.canRotate}',
|
||||||
'xmpSubjects': '${entry.xmpSubjects}',
|
'xmpSubjects': '${entry.xmpSubjects}',
|
||||||
|
@ -204,6 +202,7 @@ 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}',
|
'videoRotation': '${data.videoRotation}',
|
||||||
'latitude': '${data.latitude}',
|
'latitude': '${data.latitude}',
|
||||||
|
@ -245,6 +244,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
'contentId': '${catalog.contentId}',
|
'contentId': '${catalog.contentId}',
|
||||||
'mimeType': '${catalog.mimeType}',
|
'mimeType': '${catalog.mimeType}',
|
||||||
'dateMillis': '${catalog.dateMillis}',
|
'dateMillis': '${catalog.dateMillis}',
|
||||||
|
'isFlipped': '${catalog.isFlipped}',
|
||||||
'isAnimated': '${catalog.isAnimated}',
|
'isAnimated': '${catalog.isAnimated}',
|
||||||
'videoRotation': '${catalog.videoRotation}',
|
'videoRotation': '${catalog.videoRotation}',
|
||||||
'latitude': '${catalog.latitude}',
|
'latitude': '${catalog.latitude}',
|
||||||
|
@ -281,7 +281,8 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
return AvesExpansionTile(
|
return AvesExpansionTile(
|
||||||
title: title,
|
title: title,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Container(
|
||||||
|
alignment: AlignmentDirectional.topStart,
|
||||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
data,
|
data,
|
||||||
|
@ -303,6 +304,10 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
future: _exifInterfaceMetadataLoader,
|
future: _exifInterfaceMetadataLoader,
|
||||||
builder: (context, snapshot) => builder(context, snapshot, 'Exif Interface'),
|
builder: (context, snapshot) => builder(context, snapshot, 'Exif Interface'),
|
||||||
),
|
),
|
||||||
|
FutureBuilder<Map>(
|
||||||
|
future: _mediaMetadataLoader,
|
||||||
|
builder: (context, snapshot) => builder(context, snapshot, 'Media Metadata Retriever'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -313,6 +318,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
||||||
_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));
|
||||||
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
|
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
|
||||||
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
|
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
|
||||||
|
_mediaMetadataLoader = MetadataService.getMediaMetadataRetrieverMetadata(entry);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -544,7 +544,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
await UriImage(
|
await UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
orientationDegrees: entry.orientationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
).evict();
|
).evict();
|
||||||
// evict low quality thumbnail (without specified extents)
|
// evict low quality thumbnail (without specified extents)
|
||||||
await ThumbnailProvider(entry: entry).evict();
|
await ThumbnailProvider(entry: entry).evict();
|
||||||
|
|
|
@ -97,12 +97,12 @@ class ImageView extends StatelessWidget {
|
||||||
final uriImage = UriImage(
|
final uriImage = UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
orientationDegrees: entry.orientationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
);
|
);
|
||||||
child = PhotoView(
|
child = PhotoView(
|
||||||
// key includes size and orientation to refresh when the image is rotated
|
// key includes size and orientation to refresh when the image is rotated
|
||||||
key: ValueKey('${entry.orientationDegrees}_${entry.width}_${entry.height}_${entry.path}'),
|
key: ValueKey('${entry.rotationDegrees}_${entry.width}_${entry.height}_${entry.path}'),
|
||||||
imageProvider: uriImage,
|
imageProvider: uriImage,
|
||||||
// when the full image is ready, we use it in the `loadingBuilder`
|
// when the full image is ready, we use it in the `loadingBuilder`
|
||||||
// we still provide a `loadingBuilder` in that case to avoid a black frame after hero animation
|
// we still provide a `loadingBuilder` in that case to avoid a black frame after hero animation
|
||||||
|
|
|
@ -39,7 +39,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||||
_loader = MetadataService.getExifThumbnails(uri);
|
_loader = MetadataService.getExifThumbnails(uri);
|
||||||
break;
|
break;
|
||||||
case MetadataThumbnailSource.xmp:
|
case MetadataThumbnailSource.xmp:
|
||||||
_loader = MetadataService.getXmpThumbnails(uri);
|
_loader = MetadataService.getXmpThumbnails(entry);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||||
future: _loader,
|
future: _loader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
||||||
final turns = (entry.orientationDegrees / 90).round();
|
final turns = (entry.rotationDegrees / 90).round();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
return Container(
|
return Container(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
|
|
|
@ -101,7 +101,7 @@ class AvesVideoState extends State<AvesVideo> {
|
||||||
image: UriImage(
|
image: UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
orientationDegrees: entry.orientationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
),
|
),
|
||||||
width: entry.width.toDouble(),
|
width: entry.width.toDouble(),
|
||||||
|
|
Loading…
Reference in a new issue