improved metadata initialization from the media store

flipping (WIP)
This commit is contained in:
Thibault Deckers 2020-10-07 13:38:28 +09:00
parent 8fc0a98579
commit 60d16a3e17
22 changed files with 369 additions and 193 deletions

View file

@ -95,17 +95,17 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
}
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String uriString = call.argument("uri");
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);
return;
}
Uri uri = Uri.parse(uriString);
ImageProvider provider = ImageProviderFactory.getProvider(uri);
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;
}
@ -117,7 +117,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
@Override
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());
}
});
}

View file

@ -48,6 +48,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import deckers.thibault.aves.utils.ExifInterfaceHelper;
import deckers.thibault.aves.utils.MediaMetadataRetrieverHelper;
import deckers.thibault.aves.utils.MetadataHelper;
import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.StorageUtils;
@ -63,6 +64,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
// catalog metadata
private static final String KEY_MIME_TYPE = "mimeType";
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_LATITUDE = "latitude";
private static final String KEY_LONGITUDE = "longitude";
@ -146,6 +148,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
case "getExifInterfaceMetadata":
new Thread(() -> getExifInterfaceMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getMediaMetadataRetrieverMetadata":
new Thread(() -> getMediaMetadataRetrieverMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getEmbeddedPictures":
new Thread(() -> getEmbeddedPictures(call, new MethodResultWrapper(result))).start();
break;
@ -167,49 +172,50 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
private void getAllMetadata(MethodCall call, MethodChannel.Result result) {
String mimeType = call.argument("mimeType");
String uriString = call.argument("uri");
Uri uri = Uri.parse(uriString);
Uri uri = Uri.parse(call.argument("uri"));
Map<String, Map<String, String>> metadataMap = new HashMap<>();
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
String dirName = dir.getName();
Map<String, String> dirMap = Objects.requireNonNull(metadataMap.getOrDefault(dirName, new HashMap<>()));
metadataMap.put(dirName, dirMap);
if (MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
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;
// tags
for (Tag tag : dir.getTags()) {
dirMap.put(tag.getTagName(), tag.getDescription());
}
if (dir instanceof XmpDirectory) {
try {
XmpDirectory xmpDir = (XmpDirectory) dir;
XMPMeta xmpMeta = xmpDir.getXMPMeta();
xmpMeta.sort();
XMPIterator xmpIterator = xmpMeta.iterator();
while (xmpIterator.hasNext()) {
XMPPropertyInfo prop = (XMPPropertyInfo) xmpIterator.next();
String xmpPath = prop.getPath();
String xmpValue = prop.getValue();
if (xmpPath != null && !xmpPath.isEmpty() && xmpValue != null && !xmpValue.isEmpty()) {
dirMap.put(xmpPath, xmpValue);
// directory name
String dirName = dir.getName();
Map<String, String> dirMap = Objects.requireNonNull(metadataMap.getOrDefault(dirName, new HashMap<>()));
metadataMap.put(dirName, dirMap);
// tags
for (Tag tag : dir.getTags()) {
dirMap.put(tag.getTagName(), tag.getDescription());
}
if (dir instanceof XmpDirectory) {
try {
XmpDirectory xmpDir = (XmpDirectory) dir;
XMPMeta xmpMeta = xmpDir.getXMPMeta();
xmpMeta.sort();
XMPIterator xmpIterator = xmpMeta.iterator();
while (xmpIterator.hasNext()) {
XMPPropertyInfo prop = (XMPPropertyInfo) xmpIterator.next();
String xmpPath = prop.getPath();
String xmpValue = prop.getValue();
if (xmpPath != null && !xmpPath.isEmpty() && xmpValue != null && !xmpValue.isEmpty()) {
dirMap.put(xmpPath, xmpValue);
}
}
} catch (XMPException e) {
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) {
@ -218,27 +224,27 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
ExifInterface exif = new ExifInterface(is);
metadataMap.putAll(ExifInterfaceHelper.describeAll(exif));
} 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)) {
Map<String, String> videoDir = getVideoAllMetadataByMediaMetadataRetriever(uriString);
Map<String, String> videoDir = getVideoAllMetadataByMediaMetadataRetriever(uri);
if (!videoDir.isEmpty()) {
metadataMap.put("Video", videoDir);
}
}
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 {
result.success(metadataMap);
}
}
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(String uri) {
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(Uri uri) {
Map<String, String> dirMap = new HashMap<>();
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
if (retriever != null) {
try {
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) {
String mimeType = call.argument("mimeType");
String uri = call.argument("uri");
Uri uri = Uri.parse(call.argument("uri"));
String extension = call.argument("extension");
Map<String, Object> metadataMap = new HashMap<>(getCatalogMetadataByImageMetadataReader(uri, mimeType, extension));
@ -280,13 +286,12 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
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<>();
// as of metadata-extractor v2.14.0, MP2T/WBMP files are not supported
if (MimeTypes.MP2T.equals(mimeType) || MimeTypes.WBMP.equals(mimeType)) return metadataMap;
if (!MimeTypes.isSupportedByMetadataExtractor(mimeType)) return metadataMap;
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
// File type
@ -356,14 +361,14 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
}
}
} 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;
}
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(String uri) {
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(Uri uri) {
Map<String, Object> metadataMap = new HashMap<>();
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
if (retriever != null) {
try {
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) {
String mimeType = call.argument("mimeType");
String uri = call.argument("uri");
Uri uri = Uri.parse(call.argument("uri"));
Map<String, Object> metadataMap = new HashMap<>();
if (isVideo(mimeType)) {
if (isVideo(mimeType) || !MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
result.success(metadataMap);
return;
}
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uri))) {
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
for (ExifSubIFDDirectory directory : metadata.getDirectoriesOfType(ExifSubIFDDirectory.class)) {
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) {
String mimeType = call.argument("mimeType");
String uriString = call.argument("uri");
if (mimeType == null || uriString == null) {
result.error("getContentResolverMetadata-args", "failed because of missing arguments", null);
return;
}
Uri uri = Uri.parse(call.argument("uri"));
Uri uri = Uri.parse(uriString);
long id = ContentUris.parseId(uri);
Uri contentUri = uri;
if (mimeType.startsWith(MimeTypes.IMAGE)) {
@ -509,13 +509,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
}
private void getExifInterfaceMetadata(MethodCall call, MethodChannel.Result result) {
String uriString = call.argument("uri");
if (uriString == null) {
result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null);
return;
}
Uri uri = Uri.parse(call.argument("uri"));
try (InputStream is = StorageUtils.openInputStream(context, Uri.parse(uriString))) {
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
ExifInterface exif = new ExifInterface(is);
Map<String, Object> metadataMap = new HashMap<>();
for (String tag : ExifInterfaceHelper.allTags.keySet()) {
@ -525,12 +521,39 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
}
result.success(metadataMap);
} 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) {
Uri uri = Uri.parse(call.argument("uri"));
List<byte[]> pictures = new ArrayList<>();
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
if (retriever != null) {
@ -551,6 +574,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
private void getExifThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Uri uri = Uri.parse(call.argument("uri"));
List<byte[]> thumbnails = new ArrayList<>();
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
ExifInterface exif = new ExifInterface(is);
@ -560,33 +584,41 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
} catch (IOException e) {
Log.w(LOG_TAG, "failed to extract exif thumbnail with ExifInterface for uri=" + uri, e);
}
result.success(thumbnails);
}
private void getXmpThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String mimeType = call.argument("mimeType");
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<>();
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
for (XmpDirectory dir : metadata.getDirectoriesOfType(XmpDirectory.class)) {
XMPMeta xmpMeta = dir.getXMPMeta();
try {
if (xmpMeta.doesPropertyExist(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME)) {
int count = xmpMeta.countArrayItems(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME);
for (int i = 1; i < count + 1; i++) {
XMPProperty image = xmpMeta.getStructField(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME + "[" + i + "]", XMP_IMG_SCHEMA_NS, XMP_THUMBNAIL_IMAGE_PROP_NAME);
if (image != null) {
thumbnails.add(XMPUtils.decodeBase64(image.getValue()));
if (MimeTypes.isSupportedByMetadataExtractor(mimeType)) {
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
for (XmpDirectory dir : metadata.getDirectoriesOfType(XmpDirectory.class)) {
XMPMeta xmpMeta = dir.getXMPMeta();
try {
if (xmpMeta.doesPropertyExist(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME)) {
int count = xmpMeta.countArrayItems(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME);
for (int i = 1; i < count + 1; i++) {
XMPProperty image = xmpMeta.getStructField(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME + "[" + i + "]", XMP_IMG_SCHEMA_NS, XMP_THUMBNAIL_IMAGE_PROP_NAME);
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);
}

View file

@ -30,7 +30,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
private Activity activity;
private Uri uri;
private String mimeType;
private int orientationDegrees;
private int rotationDegrees;
private EventChannel.EventSink eventSink;
private Handler handler;
@ -52,7 +52,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
Map<String, Object> argMap = (Map<String, Object>) arguments;
this.mimeType = (String) argMap.get("mimeType");
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();
if (bitmap != null) {
// TODO TLAD use exif orientation to rotate & flip?
bitmap = TransformationUtils.rotateImage(bitmap, orientationDegrees);
bitmap = TransformationUtils.rotateImage(bitmap, rotationDegrees);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG

View file

@ -23,7 +23,7 @@ public class AvesImageEntry {
this.mimeType = (String) map.get("mimeType");
this.width = (Integer) map.get("width");
this.height = (Integer) map.get("height");
this.rotationDegrees = (Integer) map.get("orientationDegrees");
this.rotationDegrees = (Integer) map.get("rotationDegrees");
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
}

View file

@ -42,6 +42,8 @@ public class SourceImageEntry {
@Nullable
public Integer width, height, rotationDegrees;
@Nullable
public Boolean isFlipped;
@Nullable
public Long sizeBytes;
@Nullable
public Long dateModifiedSecs;
@ -74,7 +76,8 @@ public class SourceImageEntry {
put("sourceMimeType", sourceMimeType);
put("width", width);
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("title", title);
put("dateModifiedSecs", dateModifiedSecs);
@ -101,6 +104,10 @@ public class SourceImageEntry {
return width != null && width > 0 && height != null && height > 0;
}
public boolean hasOrientation() {
return rotationDegrees != null;
}
private boolean hasDuration() {
return durationMillis != null && durationMillis > 0;
}
@ -122,8 +129,9 @@ public class SourceImageEntry {
// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
public SourceImageEntry fillPreCatalogMetadata(@NonNull Context context) {
if (isSvg()) return this;
fillByMediaMetadataRetriever(context);
if (hasSize() && (!isVideo() || hasDuration())) return this;
if (hasSize() && hasOrientation() && (!isVideo() || hasDuration())) return this;
fillByMetadataExtractor(context);
if (hasSize()) return this;
fillByBitmapDecode(context);
@ -133,6 +141,8 @@ public class SourceImageEntry {
// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
private void fillByMediaMetadataRetriever(@NonNull Context context) {
if (isImage()) return;
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
if (retriever != null) {
try {
@ -185,75 +195,72 @@ public class SourceImageEntry {
// expects entry with: uri, mimeType
// finds: width, height, orientation, date
private void fillByMetadataExtractor(@NonNull Context context) {
if (isSvg()) return;
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) return;
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
switch (sourceMimeType) {
case MimeTypes.JPEG:
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);
}
// do not switch on specific mime types, as the reported mime type could be wrong
// (e.g. PNG registered as JPG)
if (isVideo()) {
for (AviDirectory dir : metadata.getDirectoriesOfType(AviDirectory.class)) {
if (dir.containsTag(AviDirectory.TAG_WIDTH)) {
width = dir.getInt(AviDirectory.TAG_WIDTH);
}
break;
case MimeTypes.MP4:
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);
}
if (dir.containsTag(AviDirectory.TAG_HEIGHT)) {
height = dir.getInt(AviDirectory.TAG_HEIGHT);
}
for (Mp4Directory dir : metadata.getDirectoriesOfType(Mp4Directory.class)) {
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION);
}
if (dir.containsTag(AviDirectory.TAG_DURATION)) {
durationMillis = dir.getLong(AviDirectory.TAG_DURATION);
}
break;
case MimeTypes.AVI:
for (AviDirectory dir : metadata.getDirectoriesOfType(AviDirectory.class)) {
if (dir.containsTag(AviDirectory.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);
}
}
for (Mp4VideoDirectory dir : metadata.getDirectoriesOfType(Mp4VideoDirectory.class)) {
if (dir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
width = dir.getInt(Mp4VideoDirectory.TAG_WIDTH);
}
break;
case MimeTypes.PSD:
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);
}
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
height = dir.getInt(Mp4VideoDirectory.TAG_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)) {
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_ORIENTATION)) {
int exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
rotationDegrees = MetadataHelper.getRotationDegreesForExifCode(exifOrientation);
}
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime();
// EXIF, if defined, should override metadata found in other directories
for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) {
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_ORIENTATION)) {
int exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
rotationDegrees = MetadataHelper.getRotationDegreesForExifCode(exifOrientation);
isFlipped = MetadataHelper.isFlippedForExifCode(exifOrientation);
}
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime();
}
}
}
} catch (IOException | ImageProcessingException | MetadataException | NoClassDefFoundError e) {
@ -264,8 +271,6 @@ public class SourceImageEntry {
// expects entry with: uri
// finds: width, height
private void fillByBitmapDecode(@NonNull Context context) {
if (isSvg()) return;
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;

View file

@ -130,7 +130,8 @@ public abstract class ImageProvider {
// copy the edited temporary file back to the original
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) {
callback.onFailure(e);
return;
@ -147,7 +148,7 @@ public abstract class ImageProvider {
// values.put(MediaStore.MediaColumns.IS_PENDING, 0);
// }
// // 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
// int updatedRowCount = contentResolver.update(uri, values, null, null);
// if (updatedRowCount > 0) {

View file

@ -124,9 +124,14 @@ public class MediaStoreImageProvider extends ImageProvider {
@SuppressLint("InlinedApi")
private int fetchFrom(final Context context, NewEntryChecker newEntryChecker, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) {
int newEntryCount = 0;
final boolean needDuration = projection == VIDEO_PROJECTION;
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 {
Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, orderBy);
if (cursor != null) {
@ -159,11 +164,18 @@ public class MediaStoreImageProvider extends ImageProvider {
int height = cursor.getInt(heightColumn);
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>() {{
put("uri", itemUri.toString());
put("path", path);
put("sourceMimeType", mimeType);
put("rotationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0);
put("sizeBytes", cursor.getLong(sizeColumn));
put("title", cursor.getString(titleColumn));
put("dateModifiedSecs", dateModifiedSecs);
@ -174,8 +186,11 @@ public class MediaStoreImageProvider extends ImageProvider {
entryMap.put("width", width);
entryMap.put("height", height);
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,
// they are valid but miss some attributes, such as width, height, orientation
SourceImageEntry entry = new SourceImageEntry(entryMap).fillPreCatalogMetadata(context);

View file

@ -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,
))
}
}
}

View file

@ -50,6 +50,13 @@ object MimeTypes {
const val MOV = "video/quicktime"
const val MP2T = "video/mp2t"
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
fun getMimeTypeForExtension(extension: String?): String? = when (extension?.toLowerCase(Locale.ROOT)) {

View file

@ -23,7 +23,7 @@ class ImageEntry {
final String sourceMimeType;
int width;
int height;
int orientationDegrees;
int rotationDegrees;
final int sizeBytes;
String sourceTitle;
int _dateModifiedSecs;
@ -42,7 +42,7 @@ class ImageEntry {
this.sourceMimeType,
@required this.width,
@required this.height,
this.orientationDegrees,
this.rotationDegrees,
this.sizeBytes,
this.sourceTitle,
int dateModifiedSecs,
@ -68,7 +68,7 @@ class ImageEntry {
sourceMimeType: sourceMimeType,
width: width,
height: height,
orientationDegrees: orientationDegrees,
rotationDegrees: rotationDegrees,
sizeBytes: sizeBytes,
sourceTitle: sourceTitle,
dateModifiedSecs: dateModifiedSecs,
@ -90,7 +90,7 @@ class ImageEntry {
sourceMimeType: map['sourceMimeType'] as String,
width: map['width'] 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,
sourceTitle: map['title'] as String,
dateModifiedSecs: map['dateModifiedSecs'] as int,
@ -108,7 +108,7 @@ class ImageEntry {
'sourceMimeType': sourceMimeType,
'width': width,
'height': height,
'orientationDegrees': orientationDegrees,
'rotationDegrees': rotationDegrees,
'sizeBytes': sizeBytes,
'title': sourceTitle,
'dateModifiedSecs': dateModifiedSecs,
@ -171,6 +171,8 @@ class ImageEntry {
bool get isCatalogued => _catalogMetadata != null;
bool get isFlipped => _catalogMetadata?.isFlipped ?? false;
bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
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 {
if (width == 0 || height == 0) return 1;
@ -369,8 +371,8 @@ class ImageEntry {
if (width is int) this.width = width;
final height = newFields['height'];
if (height is int) this.height = height;
final orientationDegrees = newFields['orientationDegrees'];
if (orientationDegrees is int) this.orientationDegrees = orientationDegrees;
final rotationDegrees = newFields['rotationDegrees'];
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
imageChangeNotifier.notifyListeners();
return true;

View file

@ -29,7 +29,7 @@ class DateMetadata {
class CatalogMetadata {
final int contentId, dateMillis, videoRotation;
final bool isAnimated;
final bool isFlipped, isAnimated;
final String mimeType, xmpSubjects, xmpTitleDescription;
final double latitude, longitude;
Address address;
@ -38,6 +38,7 @@ class CatalogMetadata {
this.contentId,
this.mimeType,
this.dateMillis,
this.isFlipped,
this.isAnimated,
this.videoRotation,
this.xmpSubjects,
@ -56,6 +57,7 @@ class CatalogMetadata {
contentId: contentId ?? this.contentId,
mimeType: mimeType,
dateMillis: dateMillis,
isFlipped: isFlipped,
isAnimated: isAnimated,
videoRotation: videoRotation,
xmpSubjects: xmpSubjects,
@ -66,11 +68,13 @@ class CatalogMetadata {
}
factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) {
final isFlipped = map['isFlipped'] ?? (boolAsInteger ? 0 : false);
final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false);
return CatalogMetadata(
contentId: map['contentId'],
mimeType: map['mimeType'],
dateMillis: map['dateMillis'] ?? 0,
isFlipped: boolAsInteger ? isFlipped != 0 : isFlipped,
isAnimated: boolAsInteger ? isAnimated != 0 : isAnimated,
videoRotation: map['videoRotation'] ?? 0,
xmpSubjects: map['xmpSubjects'] ?? '',
@ -84,6 +88,7 @@ class CatalogMetadata {
'contentId': contentId,
'mimeType': mimeType,
'dateMillis': dateMillis,
'isFlipped': boolAsInteger ? (isFlipped ? 1 : 0) : isFlipped,
'isAnimated': boolAsInteger ? (isAnimated ? 1 : 0) : isAnimated,
'videoRotation': videoRotation,
'xmpSubjects': xmpSubjects,
@ -94,7 +99,7 @@ class CatalogMetadata {
@override
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}';
}
}

View file

@ -33,7 +33,7 @@ class MetadataDb {
', sourceMimeType TEXT'
', width INTEGER'
', height INTEGER'
', orientationDegrees INTEGER'
', rotationDegrees INTEGER'
', sizeBytes INTEGER'
', title TEXT'
', dateModifiedSecs INTEGER'
@ -48,6 +48,7 @@ class MetadataDb {
'contentId INTEGER PRIMARY KEY'
', mimeType TEXT'
', dateMillis INTEGER'
', isFlipped INTEGER'
', isAnimated INTEGER'
', videoRotation INTEGER'
', xmpSubjects TEXT'
@ -68,7 +69,43 @@ class MetadataDb {
', 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,
);
}

View file

@ -23,7 +23,7 @@ class ImageFileService {
'mimeType': entry.mimeType,
'width': entry.width,
'height': entry.height,
'orientationDegrees': entry.orientationDegrees,
'rotationDegrees': entry.rotationDegrees,
'dateModifiedSecs': entry.dateModifiedSecs,
};
}
@ -66,7 +66,7 @@ class ImageFileService {
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 {
final completer = Completer<Uint8List>.sync();
final sink = _OutputBuffer();
@ -74,7 +74,7 @@ class ImageFileService {
byteChannel.receiveBroadcastStream(<String, dynamic>{
'uri': uri,
'mimeType': mimeType,
'orientationDegrees': orientationDegrees ?? 0,
'rotationDegrees': rotationDegrees ?? 0,
}).listen(
(data) {
final chunk = data as Uint8List;
@ -183,7 +183,7 @@ class ImageFileService {
static Future<Map> rotate(ImageEntry entry, {@required bool clockwise}) async {
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>{
'entry': _toPlatformEntryMap(entry),
'clockwise': clockwise,

View file

@ -33,6 +33,7 @@ class MetadataService {
// return map with:
// 'mimeType': MIME type as reported by metadata extractors, not Media Store (string)
// 'dateMillis': date taken in milliseconds since Epoch (long)
// 'isFlipped': flipped according to EXIF orientation (bool)
// 'isAnimated': animated gif/webp (bool)
// 'latitude': latitude (double)
// 'longitude': longitude (double)
@ -103,6 +104,19 @@ class MetadataService {
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 {
try {
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
@ -127,10 +141,11 @@ class MetadataService {
return [];
}
static Future<List<Uint8List>> getXmpThumbnails(String uri) async {
static Future<List<Uint8List>> getXmpThumbnails(ImageEntry entry) async {
try {
final result = await platform.invokeMethod('getXmpThumbnails', <String, dynamic>{
'uri': uri,
'mimeType': entry.mimeType,
'uri': entry.uri,
});
return (result as List).cast<Uint8List>();
} on PlatformException catch (e) {

View file

@ -138,7 +138,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
final imageProvider = UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
orientationDegrees: entry.orientationDegrees,
rotationDegrees: entry.rotationDegrees,
expectedContentLength: entry.sizeBytes,
);
if (imageCache.statusForKey(imageProvider).keepAlive) {

View file

@ -75,13 +75,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
Future<void> _print(ImageEntry entry) async {
final uri = entry.uri;
final mimeType = entry.mimeType;
final orientationDegrees = entry.orientationDegrees;
final rotationDegrees = entry.rotationDegrees;
final documentName = entry.bestTitle ?? 'Aves';
final doc = pdf.Document(title: documentName);
PdfImage pdfImage;
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) {
final svgRoot = await svg.fromSvgBytes(bytes, uri);
final viewBox = svgRoot.viewport.viewBox;
@ -100,7 +100,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
image: UriImage(
uri: uri,
mimeType: mimeType,
orientationDegrees: orientationDegrees,
rotationDegrees: rotationDegrees,
),
);
}

View file

@ -10,14 +10,14 @@ class UriImage extends ImageProvider<UriImage> {
const UriImage({
@required this.uri,
@required this.mimeType,
@required this.orientationDegrees,
@required this.rotationDegrees,
this.expectedContentLength,
this.scale = 1.0,
}) : assert(uri != null),
assert(scale != null);
final String uri, mimeType;
final int orientationDegrees, expectedContentLength;
final int rotationDegrees, expectedContentLength;
final double scale;
@override
@ -46,7 +46,7 @@ class UriImage extends ImageProvider<UriImage> {
final bytes = await ImageFileService.getImage(
uri,
mimeType,
orientationDegrees: orientationDegrees,
rotationDegrees: rotationDegrees,
expectedContentLength: expectedContentLength,
onBytesReceived: (cumulative, total) {
chunkEvents.add(ImageChunkEvent(

View file

@ -30,7 +30,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
Future<DateMetadata> _dbDateLoader;
Future<CatalogMetadata> _dbMetadataLoader;
Future<AddressDetails> _dbAddressLoader;
Future<Map> _contentResolverMetadataLoader, _exifInterfaceMetadataLoader;
Future<Map> _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader;
ImageEntry get entry => widget.entry;
@ -89,25 +89,21 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
'sourceTitle': '${entry.sourceTitle}',
'sourceMimeType': '${entry.sourceMimeType}',
'mimeType': '${entry.mimeType}',
'mimeTypeAnySubtype': '${entry.mimeTypeAnySubtype}',
}),
Divider(),
InfoRowGroup({
'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000),
'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis),
'bestDate': '${entry.bestDate}',
'monthTaken': '${entry.monthTaken}',
'dayTaken': '${entry.dayTaken}',
}),
Divider(),
InfoRowGroup({
'width': '${entry.width}',
'height': '${entry.height}',
'orientationDegrees': '${entry.orientationDegrees}',
'rotationDegrees': '${entry.rotationDegrees}',
'portrait': '${entry.portrait}',
'displayAspectRatio': '${entry.displayAspectRatio}',
'displaySize': '${entry.displaySize}',
'megaPixels': '${entry.megaPixels}',
}),
Divider(),
InfoRowGroup({
@ -122,8 +118,10 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
'isPhoto': '${entry.isPhoto}',
'isVideo': '${entry.isVideo}',
'isCatalogued': '${entry.isCatalogued}',
'isFlipped': '${entry.isFlipped}',
'isAnimated': '${entry.isAnimated}',
'canEdit': '${entry.canEdit}',
'canEditExif': '${entry.canEditExif}',
'canPrint': '${entry.canPrint}',
'canRotate': '${entry.canRotate}',
'xmpSubjects': '${entry.xmpSubjects}',
@ -204,6 +202,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
InfoRowGroup({
'mimeType': '${data.mimeType}',
'dateMillis': '${data.dateMillis}',
'isFlipped': '${data.isFlipped}',
'isAnimated': '${data.isAnimated}',
'videoRotation': '${data.videoRotation}',
'latitude': '${data.latitude}',
@ -245,6 +244,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
'contentId': '${catalog.contentId}',
'mimeType': '${catalog.mimeType}',
'dateMillis': '${catalog.dateMillis}',
'isFlipped': '${catalog.isFlipped}',
'isAnimated': '${catalog.isAnimated}',
'videoRotation': '${catalog.videoRotation}',
'latitude': '${catalog.latitude}',
@ -281,7 +281,8 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
return AvesExpansionTile(
title: title,
children: [
Padding(
Container(
alignment: AlignmentDirectional.topStart,
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(
data,
@ -303,6 +304,10 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
future: _exifInterfaceMetadataLoader,
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));
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
_mediaMetadataLoader = MetadataService.getMediaMetadataRetrieverMetadata(entry);
setState(() {});
}
}

View file

@ -544,7 +544,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
await UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
orientationDegrees: entry.orientationDegrees,
rotationDegrees: entry.rotationDegrees,
).evict();
// evict low quality thumbnail (without specified extents)
await ThumbnailProvider(entry: entry).evict();

View file

@ -97,12 +97,12 @@ class ImageView extends StatelessWidget {
final uriImage = UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
orientationDegrees: entry.orientationDegrees,
rotationDegrees: entry.rotationDegrees,
expectedContentLength: entry.sizeBytes,
);
child = PhotoView(
// 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,
// 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

View file

@ -39,7 +39,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
_loader = MetadataService.getExifThumbnails(uri);
break;
case MetadataThumbnailSource.xmp:
_loader = MetadataService.getXmpThumbnails(uri);
_loader = MetadataService.getXmpThumbnails(entry);
break;
}
}
@ -50,7 +50,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
future: _loader,
builder: (context, snapshot) {
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;
return Container(
alignment: AlignmentDirectional.topStart,

View file

@ -101,7 +101,7 @@ class AvesVideoState extends State<AvesVideo> {
image: UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
orientationDegrees: entry.orientationDegrees,
rotationDegrees: entry.rotationDegrees,
expectedContentLength: entry.sizeBytes,
),
width: entry.width.toDouble(),