From 2f29a970daa1587b0da8530161199d8587590ab2 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 21 Sep 2020 12:48:53 +0900 Subject: [PATCH] improved support for psd and other unrecognized formats --- .../streams/ImageByteStreamHandler.java | 68 ++++++------- .../thibault/aves/model/SourceImageEntry.java | 96 ++++++++++++------- .../provider/MediaStoreImageProvider.java | 15 +-- .../thibault/aves/utils/MimeTypes.java | 1 + lib/model/filters/mime.dart | 10 +- 5 files changed, 108 insertions(+), 82 deletions(-) diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java index 572e0f361..31cb5c924 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java @@ -66,6 +66,10 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { handler.post(() -> eventSink.endOfStream()); } + // Supported image formats: + // - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP + // - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats + // - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java private void getImage() { if (mimeType != null && mimeType.startsWith(MimeTypes.VIDEO)) { RequestOptions options = new RequestOptions() @@ -91,42 +95,40 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { } finally { Glide.with(activity).clear(target); } + } else if (MimeTypes.DNG.equals(mimeType) || MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType)) { + // as of Flutter v1.20, Dart Image.memory cannot decode DNG/HEIC/HEIF images + // so we convert the image on platform side first + FutureTarget target = Glide.with(activity) + .asBitmap() + .load(uri) + .submit(); + try { + Bitmap bitmap = target.get(); + if (bitmap != null) { + bitmap = TransformationUtils.rotateImage(bitmap, orientationDegrees); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + // we compress the bitmap because Dart Image.memory cannot decode the raw bytes + // Bitmap.CompressFormat.PNG is slower than JPEG + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); + success(stream.toByteArray()); + } else { + error("getImage-image-decode-null", "failed to get image from uri=" + uri, null); + } + } catch (Exception e) { + error("getImage-image-decode-exception", "failed to get image from uri=" + uri, e.getMessage()); + } finally { + Glide.with(activity).clear(target); + } } else { ContentResolver cr = activity.getContentResolver(); - if (MimeTypes.DNG.equals(mimeType) || MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType)) { - // as of Flutter v1.20, Dart Image.memory cannot decode DNG/HEIC/HEIF images - // so we convert the image on platform side first - FutureTarget target = Glide.with(activity) - .asBitmap() - .load(uri) - .submit(); - try { - Bitmap bitmap = target.get(); - if (bitmap != null) { - bitmap = TransformationUtils.rotateImage(bitmap, orientationDegrees); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); - success(stream.toByteArray()); - } else { - error("getImage-image-decode-null", "failed to get image from uri=" + uri, null); - } - } catch (Exception e) { - error("getImage-image-decode-exception", "failed to get image from uri=" + uri, e.getMessage()); - } finally { - Glide.with(activity).clear(target); - } - } else { - try (InputStream is = cr.openInputStream(uri)) { - if (is != null) { - streamBytes(is); - } else { - error("getImage-image-read-null", "failed to get image from uri=" + uri, null); - } - } catch (IOException e) { - error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage()); + try (InputStream is = cr.openInputStream(uri)) { + if (is != null) { + streamBytes(is); + } else { + error("getImage-image-read-null", "failed to get image from uri=" + uri, null); } + } catch (IOException e) { + error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage()); } } endOfStream(); 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 index 50efa6b54..be3742986 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/SourceImageEntry.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/SourceImageEntry.java @@ -20,6 +20,7 @@ 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; @@ -191,48 +192,69 @@ public class SourceImageEntry { try (InputStream is = StorageUtils.openInputStream(context, uri)) { Metadata metadata = ImageMetadataReader.readMetadata(is); - if (MimeTypes.JPEG.equals(sourceMimeType)) { - for (JpegDirectory dir : metadata.getDirectoriesOfType(JpegDirectory.class)) { - if (dir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) { - width = dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH); + 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); + } } - if (dir.containsTag(JpegDirectory.TAG_IMAGE_HEIGHT)) { - height = dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT); + 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); + } } + for (Mp4Directory dir : metadata.getDirectoriesOfType(Mp4Directory.class)) { + if (dir.containsTag(Mp4Directory.TAG_DURATION)) { + durationMillis = dir.getLong(Mp4Directory.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); + } + } + 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); + } + } + break; + } + + for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) { + if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_WIDTH)) { + width = dir.getInt(ExifIFD0Directory.TAG_IMAGE_WIDTH); } - for (ExifIFD0Directory dir : metadata.getDirectoriesOfType(ExifIFD0Directory.class)) { - if (dir.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { - orientationDegrees = getOrientationDegreesForExifCode(dir.getInt(ExifIFD0Directory.TAG_ORIENTATION)); - } - if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) { - sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime(); - } + if (dir.containsTag(ExifIFD0Directory.TAG_IMAGE_HEIGHT)) { + height = dir.getInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT); } - } else if (MimeTypes.MP4.equals(sourceMimeType)) { - 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(ExifIFD0Directory.TAG_ORIENTATION)) { + orientationDegrees = getOrientationDegreesForExifCode(dir.getInt(ExifIFD0Directory.TAG_ORIENTATION)); } - for (Mp4Directory dir : metadata.getDirectoriesOfType(Mp4Directory.class)) { - if (dir.containsTag(Mp4Directory.TAG_DURATION)) { - durationMillis = dir.getLong(Mp4Directory.TAG_DURATION); - } - } - } else if (MimeTypes.AVI.equals(sourceMimeType)) { - 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); - } + if (dir.containsTag(ExifIFD0Directory.TAG_DATETIME)) { + sourceDateTakenMillis = dir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime(); } } } catch (IOException | ImageProcessingException | MetadataException | NoClassDefFoundError e) { diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java index 475cd67b3..121533ccc 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java @@ -180,20 +180,13 @@ public class MediaStoreImageProvider extends ImageProvider { // they are valid but miss some attributes, such as width, height, orientation SourceImageEntry entry = new SourceImageEntry(entryMap).fillPreCatalogMetadata(context); entryMap = entry.toMap(); - width = entry.width != null ? entry.width : 0; - height = entry.height != null ? entry.height : 0; } - if ((width <= 0 || height <= 0) && needSize(mimeType)) { - // this is probably not a real image, like "/storage/emulated/0", so we skip it - Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path + ", mimeType=" + mimeType); - } else { - newEntryHandler.handleEntry(entryMap); - if (newEntryCount % 30 == 0) { - Thread.sleep(10); - } - newEntryCount++; + newEntryHandler.handleEntry(entryMap); + if (newEntryCount % 30 == 0) { + Thread.sleep(10); } + newEntryCount++; } } cursor.close(); diff --git a/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java b/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java index 5c6f1833e..676c28217 100644 --- a/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java +++ b/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java @@ -8,6 +8,7 @@ public class MimeTypes { public static final String HEIF = "image/heif"; public static final String JPEG = "image/jpeg"; public static final String PNG = "image/png"; + public static final String PSD = "image/x-photoshop"; public static final String SVG = "image/svg+xml"; public static final String WEBP = "image/webp"; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 82baeb3e8..cd1000439 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -50,7 +50,15 @@ class MimeFilter extends CollectionFilter { }; static String displayType(String mime) { - return mime.toUpperCase().replaceFirst(RegExp('.*/(X-)?'), '').replaceFirst('+XML', '').replaceFirst('VND.', ''); + final patterns = [ + RegExp('.*/'), // remove type, keep subtype + RegExp('(X-|VND.)'), // noisy prefixes + '+XML', // noisy suffix + RegExp('ADOBE[-\.]'), // for DNG, PSD... + ]; + mime = mime.toUpperCase(); + patterns.forEach((pattern) => mime = mime.replaceFirst(pattern, '')); + return mime; } @override