Kotlin migration (WIP)
This commit is contained in:
parent
60d16a3e17
commit
46df3e98de
6 changed files with 327 additions and 346 deletions
|
@ -1,41 +0,0 @@
|
||||||
package deckers.thibault.aves.model;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
|
||||||
|
|
||||||
public class AvesImageEntry {
|
|
||||||
public Uri uri; // content or file URI
|
|
||||||
public String path; // best effort to get local path
|
|
||||||
public String mimeType;
|
|
||||||
@Nullable
|
|
||||||
public Integer width, height, rotationDegrees;
|
|
||||||
@Nullable
|
|
||||||
public Long dateModifiedSecs;
|
|
||||||
|
|
||||||
public AvesImageEntry(Map<String, Object> map) {
|
|
||||||
this.uri = Uri.parse((String) map.get("uri"));
|
|
||||||
this.path = (String) map.get("path");
|
|
||||||
this.mimeType = (String) map.get("mimeType");
|
|
||||||
this.width = (Integer) map.get("width");
|
|
||||||
this.height = (Integer) map.get("height");
|
|
||||||
this.rotationDegrees = (Integer) map.get("rotationDegrees");
|
|
||||||
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVideo() {
|
|
||||||
return mimeType.startsWith(MimeTypes.VIDEO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// convenience method
|
|
||||||
|
|
||||||
private static Long toLong(Object o) {
|
|
||||||
if (o == null) return null;
|
|
||||||
if (o instanceof Integer) return Long.valueOf((Integer) o);
|
|
||||||
return (long) o;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,292 +0,0 @@
|
||||||
package deckers.thibault.aves.model;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.media.MediaMetadataRetriever;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.drew.imaging.ImageMetadataReader;
|
|
||||||
import com.drew.imaging.ImageProcessingException;
|
|
||||||
import com.drew.metadata.Metadata;
|
|
||||||
import com.drew.metadata.MetadataException;
|
|
||||||
import com.drew.metadata.avi.AviDirectory;
|
|
||||||
import com.drew.metadata.exif.ExifIFD0Directory;
|
|
||||||
import com.drew.metadata.jpeg.JpegDirectory;
|
|
||||||
import com.drew.metadata.mp4.Mp4Directory;
|
|
||||||
import com.drew.metadata.mp4.media.Mp4VideoDirectory;
|
|
||||||
import com.drew.metadata.photoshop.PsdHeaderDirectory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
import deckers.thibault.aves.utils.MetadataHelper;
|
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils;
|
|
||||||
|
|
||||||
public class SourceImageEntry {
|
|
||||||
public Uri uri; // content or file URI
|
|
||||||
public String path; // best effort to get local path
|
|
||||||
|
|
||||||
public String sourceMimeType;
|
|
||||||
@Nullable
|
|
||||||
public String title;
|
|
||||||
@Nullable
|
|
||||||
public Integer width, height, rotationDegrees;
|
|
||||||
@Nullable
|
|
||||||
public Boolean isFlipped;
|
|
||||||
@Nullable
|
|
||||||
public Long sizeBytes;
|
|
||||||
@Nullable
|
|
||||||
public Long dateModifiedSecs;
|
|
||||||
@Nullable
|
|
||||||
private Long sourceDateTakenMillis;
|
|
||||||
@Nullable
|
|
||||||
private Long durationMillis;
|
|
||||||
|
|
||||||
public SourceImageEntry() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceImageEntry(@NonNull Map<String, Object> map) {
|
|
||||||
this.uri = Uri.parse((String) map.get("uri"));
|
|
||||||
this.path = (String) map.get("path");
|
|
||||||
this.sourceMimeType = (String) map.get("sourceMimeType");
|
|
||||||
this.width = (int) map.get("width");
|
|
||||||
this.height = (int) map.get("height");
|
|
||||||
this.rotationDegrees = (int) map.get("rotationDegrees");
|
|
||||||
this.sizeBytes = toLong(map.get("sizeBytes"));
|
|
||||||
this.title = (String) map.get("title");
|
|
||||||
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));
|
|
||||||
this.sourceDateTakenMillis = toLong(map.get("sourceDateTakenMillis"));
|
|
||||||
this.durationMillis = toLong(map.get("durationMillis"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> toMap() {
|
|
||||||
return new HashMap<String, Object>() {{
|
|
||||||
put("uri", uri.toString());
|
|
||||||
put("path", path);
|
|
||||||
put("sourceMimeType", sourceMimeType);
|
|
||||||
put("width", width);
|
|
||||||
put("height", height);
|
|
||||||
put("rotationDegrees", rotationDegrees != null ? rotationDegrees : 0);
|
|
||||||
put("isFlipped", isFlipped != null ? isFlipped : false);
|
|
||||||
put("sizeBytes", sizeBytes);
|
|
||||||
put("title", title);
|
|
||||||
put("dateModifiedSecs", dateModifiedSecs);
|
|
||||||
put("sourceDateTakenMillis", sourceDateTakenMillis);
|
|
||||||
put("durationMillis", durationMillis);
|
|
||||||
// only for map export
|
|
||||||
put("contentId", getContentId());
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getContentId() {
|
|
||||||
if (uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
|
|
||||||
try {
|
|
||||||
return ContentUris.parseId(uri);
|
|
||||||
} catch (NumberFormatException | UnsupportedOperationException e) {
|
|
||||||
// ignore when the ID is not a number
|
|
||||||
// e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasSize() {
|
|
||||||
return width != null && width > 0 && height != null && height > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasOrientation() {
|
|
||||||
return rotationDegrees != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasDuration() {
|
|
||||||
return durationMillis != null && durationMillis > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isImage() {
|
|
||||||
return sourceMimeType.startsWith(MimeTypes.IMAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSvg() {
|
|
||||||
return sourceMimeType.equals(MimeTypes.SVG);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isVideo() {
|
|
||||||
return sourceMimeType.startsWith(MimeTypes.VIDEO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadata retrieval
|
|
||||||
|
|
||||||
// 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() && hasOrientation() && (!isVideo() || hasDuration())) return this;
|
|
||||||
fillByMetadataExtractor(context);
|
|
||||||
if (hasSize()) return this;
|
|
||||||
fillByBitmapDecode(context);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
String width = null, height = null, rotation = null, durationMillis = null;
|
|
||||||
if (isImage()) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
|
|
||||||
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
|
|
||||||
rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
|
|
||||||
}
|
|
||||||
} else if (isVideo()) {
|
|
||||||
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
|
|
||||||
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
|
|
||||||
rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
|
||||||
durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
||||||
}
|
|
||||||
if (width != null) {
|
|
||||||
this.width = Integer.parseInt(width);
|
|
||||||
}
|
|
||||||
if (height != null) {
|
|
||||||
this.height = Integer.parseInt(height);
|
|
||||||
}
|
|
||||||
if (rotation != null) {
|
|
||||||
this.rotationDegrees = Integer.parseInt(rotation);
|
|
||||||
}
|
|
||||||
if (durationMillis != null) {
|
|
||||||
this.durationMillis = Long.parseLong(durationMillis);
|
|
||||||
}
|
|
||||||
|
|
||||||
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
|
||||||
long dateMillis = MetadataHelper.parseVideoMetadataDate(dateString);
|
|
||||||
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
|
||||||
if (dateMillis > 0) {
|
|
||||||
this.sourceDateTakenMillis = dateMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
|
||||||
if (title != null) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// ignore
|
|
||||||
} finally {
|
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
|
||||||
retriever.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expects entry with: uri, mimeType
|
|
||||||
// finds: width, height, orientation, date
|
|
||||||
private void fillByMetadataExtractor(@NonNull Context context) {
|
|
||||||
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) return;
|
|
||||||
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
|
||||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
if (dir.containsTag(Mp4VideoDirectory.TAG_HEIGHT)) {
|
|
||||||
height = dir.getInt(Mp4VideoDirectory.TAG_HEIGHT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expects entry with: uri
|
|
||||||
// finds: width, height
|
|
||||||
private void fillByBitmapDecode(@NonNull Context context) {
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeStream(is, null, options);
|
|
||||||
width = options.outWidth;
|
|
||||||
height = options.outHeight;
|
|
||||||
} catch (IOException e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convenience method
|
|
||||||
|
|
||||||
private static Long toLong(@Nullable Object o) {
|
|
||||||
if (o == null) return null;
|
|
||||||
if (o instanceof Integer) return Long.valueOf((Integer) o);
|
|
||||||
return (long) o;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,12 +10,9 @@ import deckers.thibault.aves.model.SourceImageEntry;
|
||||||
class ContentImageProvider extends ImageProvider {
|
class ContentImageProvider extends ImageProvider {
|
||||||
@Override
|
@Override
|
||||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||||
SourceImageEntry entry = new SourceImageEntry();
|
SourceImageEntry entry = new SourceImageEntry(uri, mimeType).fillPreCatalogMetadata(context);
|
||||||
entry.uri = uri;
|
|
||||||
entry.sourceMimeType = mimeType;
|
|
||||||
entry.fillPreCatalogMetadata(context);
|
|
||||||
|
|
||||||
if (entry.hasSize() || entry.isSvg()) {
|
if (entry.getHasSize() || entry.isSvg()) {
|
||||||
callback.onSuccess(entry.toMap());
|
callback.onSuccess(entry.toMap());
|
||||||
} else {
|
} else {
|
||||||
callback.onFailure(new Exception("entry has no size"));
|
callback.onFailure(new Exception("entry has no size"));
|
||||||
|
|
|
@ -13,19 +13,14 @@ import deckers.thibault.aves.utils.FileUtils;
|
||||||
class FileImageProvider extends ImageProvider {
|
class FileImageProvider extends ImageProvider {
|
||||||
@Override
|
@Override
|
||||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||||
SourceImageEntry entry = new SourceImageEntry();
|
SourceImageEntry entry = new SourceImageEntry(uri, mimeType);
|
||||||
entry.uri = uri;
|
|
||||||
entry.sourceMimeType = mimeType;
|
|
||||||
|
|
||||||
String path = FileUtils.getPathFromUri(context, uri);
|
String path = FileUtils.getPathFromUri(context, uri);
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
try {
|
try {
|
||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
entry.path = path;
|
entry.initFromFile(path, file.getName(), file.length(), file.lastModified() / 1000);
|
||||||
entry.title = file.getName();
|
|
||||||
entry.sizeBytes = file.length();
|
|
||||||
entry.dateModifiedSecs = file.lastModified() / 1000;
|
|
||||||
}
|
}
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
callback.onFailure(e);
|
callback.onFailure(e);
|
||||||
|
@ -33,7 +28,7 @@ class FileImageProvider extends ImageProvider {
|
||||||
}
|
}
|
||||||
entry.fillPreCatalogMetadata(context);
|
entry.fillPreCatalogMetadata(context);
|
||||||
|
|
||||||
if (entry.hasSize() || entry.isSvg()) {
|
if (entry.getHasSize() || entry.isSvg()) {
|
||||||
callback.onSuccess(entry.toMap());
|
callback.onSuccess(entry.toMap());
|
||||||
} else {
|
} else {
|
||||||
callback.onFailure(new Exception("entry has no size"));
|
callback.onFailure(new Exception("entry has no size"));
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package deckers.thibault.aves.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
|
||||||
|
class AvesImageEntry(map: Map<String?, Any?>) {
|
||||||
|
@JvmField
|
||||||
|
val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val path = map["path"] as String? // best effort to get local path
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val mimeType = map["mimeType"] as String
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val width = map["width"] as Int
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val height = map["height"] as Int
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val rotationDegrees = map["rotationDegrees"] as Int
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||||
|
|
||||||
|
val isVideo: Boolean
|
||||||
|
get() = mimeType.startsWith(MimeTypes.VIDEO)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// convenience method
|
||||||
|
private fun toLong(o: Any?): Long? = when (o) {
|
||||||
|
is Int -> o.toLong()
|
||||||
|
else -> o as? Long
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,284 @@
|
||||||
|
package deckers.thibault.aves.model
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import com.drew.imaging.ImageMetadataReader
|
||||||
|
import com.drew.metadata.avi.AviDirectory
|
||||||
|
import com.drew.metadata.exif.ExifIFD0Directory
|
||||||
|
import com.drew.metadata.jpeg.JpegDirectory
|
||||||
|
import com.drew.metadata.mp4.Mp4Directory
|
||||||
|
import com.drew.metadata.mp4.media.Mp4VideoDirectory
|
||||||
|
import com.drew.metadata.photoshop.PsdHeaderDirectory
|
||||||
|
import deckers.thibault.aves.utils.MetadataHelper.getRotationDegreesForExifCode
|
||||||
|
import deckers.thibault.aves.utils.MetadataHelper.isFlippedForExifCode
|
||||||
|
import deckers.thibault.aves.utils.MetadataHelper.parseVideoMetadataDate
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SourceImageEntry {
|
||||||
|
val uri: Uri // content or file URI
|
||||||
|
var path: String? = null // best effort to get local path
|
||||||
|
private val sourceMimeType: String
|
||||||
|
var title: String? = null
|
||||||
|
var width: Int? = null
|
||||||
|
var height: Int? = null
|
||||||
|
private var rotationDegrees: Int? = null
|
||||||
|
private var isFlipped: Boolean? = null
|
||||||
|
var sizeBytes: Long? = null
|
||||||
|
var dateModifiedSecs: Long? = null
|
||||||
|
private var sourceDateTakenMillis: Long? = null
|
||||||
|
private var durationMillis: Long? = null
|
||||||
|
|
||||||
|
constructor(uri: Uri, sourceMimeType: String) {
|
||||||
|
this.uri = uri
|
||||||
|
this.sourceMimeType = sourceMimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(map: Map<String, Any?>) {
|
||||||
|
uri = Uri.parse(map["uri"] as String)
|
||||||
|
path = map["path"] as String?
|
||||||
|
sourceMimeType = map["sourceMimeType"] as String
|
||||||
|
width = map["width"] as Int
|
||||||
|
height = map["height"] as Int
|
||||||
|
rotationDegrees = map["rotationDegrees"] as Int
|
||||||
|
sizeBytes = toLong(map["sizeBytes"])
|
||||||
|
title = map["title"] as String?
|
||||||
|
dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||||
|
sourceDateTakenMillis = toLong(map["sourceDateTakenMillis"])
|
||||||
|
durationMillis = toLong(map["durationMillis"])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedSecs: Long) {
|
||||||
|
this.path = path
|
||||||
|
this.title = title
|
||||||
|
this.sizeBytes = sizeBytes
|
||||||
|
this.dateModifiedSecs = dateModifiedSecs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toMap(): Map<String, Any?> {
|
||||||
|
return hashMapOf(
|
||||||
|
"uri" to uri.toString(),
|
||||||
|
"path" to path,
|
||||||
|
"sourceMimeType" to sourceMimeType,
|
||||||
|
"width" to width,
|
||||||
|
"height" to height,
|
||||||
|
"rotationDegrees" to (rotationDegrees ?: 0),
|
||||||
|
"isFlipped" to (isFlipped ?: false),
|
||||||
|
"sizeBytes" to sizeBytes,
|
||||||
|
"title" to title,
|
||||||
|
"dateModifiedSecs" to dateModifiedSecs,
|
||||||
|
"sourceDateTakenMillis" to sourceDateTakenMillis,
|
||||||
|
"durationMillis" to durationMillis,
|
||||||
|
// only for map export
|
||||||
|
"contentId" to contentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore when the ID is not a number
|
||||||
|
// e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg
|
||||||
|
private val contentId: Long?
|
||||||
|
get() {
|
||||||
|
if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
|
||||||
|
try {
|
||||||
|
return ContentUris.parseId(uri)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasSize: Boolean
|
||||||
|
get() = width ?: 0 > 0 && height ?: 0 > 0
|
||||||
|
|
||||||
|
private val hasOrientation: Boolean
|
||||||
|
get() = rotationDegrees != null
|
||||||
|
|
||||||
|
private val hasDuration: Boolean
|
||||||
|
get() = durationMillis ?: 0 > 0
|
||||||
|
|
||||||
|
private val isImage: Boolean
|
||||||
|
get() = sourceMimeType.startsWith(MimeTypes.IMAGE)
|
||||||
|
|
||||||
|
private val isVideo: Boolean
|
||||||
|
get() = sourceMimeType.startsWith(MimeTypes.VIDEO)
|
||||||
|
|
||||||
|
val isSvg: Boolean
|
||||||
|
get() = sourceMimeType == MimeTypes.SVG
|
||||||
|
|
||||||
|
// metadata retrieval
|
||||||
|
// expects entry with: uri, mimeType
|
||||||
|
// finds: width, height, orientation/rotation, date, title, duration
|
||||||
|
fun fillPreCatalogMetadata(context: Context): SourceImageEntry {
|
||||||
|
if (isSvg) return this
|
||||||
|
fillByMediaMetadataRetriever(context)
|
||||||
|
if (hasSize && hasOrientation && (!isVideo || hasDuration)) return this
|
||||||
|
fillByMetadataExtractor(context)
|
||||||
|
if (hasSize) return this
|
||||||
|
fillByBitmapDecode(context)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// expects entry with: uri, mimeType
|
||||||
|
// finds: width, height, orientation/rotation, date, title, duration
|
||||||
|
private fun fillByMediaMetadataRetriever(context: Context) {
|
||||||
|
if (isImage) return
|
||||||
|
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return
|
||||||
|
try {
|
||||||
|
var width: String? = null
|
||||||
|
var height: String? = null
|
||||||
|
var rotationDegrees: String? = null
|
||||||
|
var durationMillis: String? = null
|
||||||
|
if (isImage) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)
|
||||||
|
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)
|
||||||
|
rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)
|
||||||
|
}
|
||||||
|
} else if (isVideo) {
|
||||||
|
width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
||||||
|
height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
||||||
|
rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
||||||
|
durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||||
|
}
|
||||||
|
if (width != null) {
|
||||||
|
this.width = width.toInt()
|
||||||
|
}
|
||||||
|
if (height != null) {
|
||||||
|
this.height = height.toInt()
|
||||||
|
}
|
||||||
|
if (rotationDegrees != null) {
|
||||||
|
this.rotationDegrees = rotationDegrees.toInt()
|
||||||
|
}
|
||||||
|
if (durationMillis != null) {
|
||||||
|
this.durationMillis = durationMillis.toLong()
|
||||||
|
}
|
||||||
|
val dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
|
||||||
|
val dateMillis = parseVideoMetadataDate(dateString)
|
||||||
|
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
|
||||||
|
if (dateMillis > 0) {
|
||||||
|
sourceDateTakenMillis = dateMillis
|
||||||
|
}
|
||||||
|
val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||||
|
if (title != null) {
|
||||||
|
this.title = title
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
|
retriever.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expects entry with: uri, mimeType
|
||||||
|
// finds: width, height, orientation, date
|
||||||
|
private fun fillByMetadataExtractor(context: Context) {
|
||||||
|
if (!isSupportedByMetadataExtractor(sourceMimeType)) return
|
||||||
|
try {
|
||||||
|
StorageUtils.openInputStream(context, uri).use { input ->
|
||||||
|
val metadata = ImageMetadataReader.readMetadata(input)
|
||||||
|
|
||||||
|
// do not switch on specific mime types, as the reported mime type could be wrong
|
||||||
|
// (e.g. PNG registered as JPG)
|
||||||
|
if (isVideo) {
|
||||||
|
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {
|
||||||
|
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 (dir in metadata.getDirectoriesOfType(Mp4VideoDirectory::class.java)) {
|
||||||
|
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 (dir in metadata.getDirectoriesOfType(Mp4Directory::class.java)) {
|
||||||
|
if (dir.containsTag(Mp4Directory.TAG_DURATION)) {
|
||||||
|
durationMillis = dir.getLong(Mp4Directory.TAG_DURATION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (dir in metadata.getDirectoriesOfType(JpegDirectory::class.java)) {
|
||||||
|
if (dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) {
|
||||||
|
width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH)
|
||||||
|
}
|
||||||
|
if (dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) {
|
||||||
|
height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (dir in metadata.getDirectoriesOfType(PsdHeaderDirectory::class.java)) {
|
||||||
|
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_WIDTH)) {
|
||||||
|
width = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH)
|
||||||
|
}
|
||||||
|
if (dir.containsTag(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)) {
|
||||||
|
height = dir.getInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIF, if defined, should override metadata found in other directories
|
||||||
|
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||||
|
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)) {
|
||||||
|
val exifOrientation = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION)
|
||||||
|
rotationDegrees = getRotationDegreesForExifCode(exifOrientation)
|
||||||
|
isFlipped = isFlippedForExifCode(exifOrientation)
|
||||||
|
}
|
||||||
|
if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) {
|
||||||
|
sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expects entry with: uri
|
||||||
|
// finds: width, height
|
||||||
|
private fun fillByBitmapDecode(context: Context) {
|
||||||
|
try {
|
||||||
|
StorageUtils.openInputStream(context, uri).use { input ->
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeStream(input, null, options)
|
||||||
|
width = options.outWidth
|
||||||
|
height = options.outHeight
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// convenience method
|
||||||
|
private fun toLong(o: Any?): Long? = when (o) {
|
||||||
|
is Int -> o.toLong()
|
||||||
|
else -> o as? Long
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue