diff --git a/android/app/src/main/java/deckers/thibault/aves/model/AvesImageEntry.java b/android/app/src/main/java/deckers/thibault/aves/model/AvesImageEntry.java deleted file mode 100644 index f1cc8ded1..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/AvesImageEntry.java +++ /dev/null @@ -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 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; - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/SourceImageEntry.java b/android/app/src/main/java/deckers/thibault/aves/model/SourceImageEntry.java deleted file mode 100644 index ce6dace73..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/SourceImageEntry.java +++ /dev/null @@ -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 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 toMap() { - return new HashMap() {{ - 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; - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java index d6c0d3541..b7912001b 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java @@ -10,12 +10,9 @@ import deckers.thibault.aves.model.SourceImageEntry; class ContentImageProvider extends ImageProvider { @Override public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) { - SourceImageEntry entry = new SourceImageEntry(); - entry.uri = uri; - entry.sourceMimeType = mimeType; - entry.fillPreCatalogMetadata(context); + SourceImageEntry entry = new SourceImageEntry(uri, mimeType).fillPreCatalogMetadata(context); - if (entry.hasSize() || entry.isSvg()) { + if (entry.getHasSize() || entry.isSvg()) { callback.onSuccess(entry.toMap()); } else { callback.onFailure(new Exception("entry has no size")); diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java index 21d5d2ff3..a00e780dc 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java @@ -13,19 +13,14 @@ import deckers.thibault.aves.utils.FileUtils; class FileImageProvider extends ImageProvider { @Override public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) { - SourceImageEntry entry = new SourceImageEntry(); - entry.uri = uri; - entry.sourceMimeType = mimeType; + SourceImageEntry entry = new SourceImageEntry(uri, mimeType); String path = FileUtils.getPathFromUri(context, uri); if (path != null) { try { File file = new File(path); if (file.exists()) { - entry.path = path; - entry.title = file.getName(); - entry.sizeBytes = file.length(); - entry.dateModifiedSecs = file.lastModified() / 1000; + entry.initFromFile(path, file.getName(), file.length(), file.lastModified() / 1000); } } catch (SecurityException e) { callback.onFailure(e); @@ -33,7 +28,7 @@ class FileImageProvider extends ImageProvider { } entry.fillPreCatalogMetadata(context); - if (entry.hasSize() || entry.isSvg()) { + if (entry.getHasSize() || entry.isSvg()) { callback.onSuccess(entry.toMap()); } else { callback.onFailure(new Exception("entry has no size")); diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt new file mode 100644 index 000000000..ccbbd280e --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt @@ -0,0 +1,38 @@ +package deckers.thibault.aves.model + +import android.net.Uri +import deckers.thibault.aves.utils.MimeTypes + +class AvesImageEntry(map: Map) { + @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 + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt new file mode 100644 index 000000000..8c7e5cd55 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt @@ -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) { + 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 { + 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 + } + } +} \ No newline at end of file