From 24f9bc1b812b201a9d7859785bc78b3606a4dc57 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 15 Oct 2020 11:40:29 +0900 Subject: [PATCH] fixed Glide loading options, exif thumbnail orientation --- .../aves/channel/calls/AppAdapterHandler.java | 17 ++++++--------- .../aves/channel/calls/ImageDecodeTask.java | 10 ++++----- .../streams/ImageByteStreamHandler.java | 18 ++++++++++++---- .../aves/decoder/AvesAppGlideModule.java | 4 ---- .../aves/channel/calls/MetadataHandler.kt | 21 +++++++++++++++---- .../thibault/aves/utils/BitmapUtils.kt | 3 ++- .../deckers/thibault/aves/utils/MimeTypes.kt | 8 +++++++ lib/services/metadata_service.dart | 1 - .../fullscreen/info/metadata_thumbnail.dart | 11 +++------- 9 files changed, 54 insertions(+), 39 deletions(-) diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java index 2ca2aa107..0c586276a 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java @@ -18,10 +18,9 @@ import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.signature.ObjectKey; import java.io.ByteArrayOutputStream; import java.io.File; @@ -173,25 +172,21 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler { .path(String.valueOf(iconResourceId)) .build(); - // add signature to ignore cache for images which got modified but kept the same URI - Key signature = new ObjectKey(packageName + size); RequestOptions options = new RequestOptions() - .signature(signature) + .format(DecodeFormat.PREFER_RGB_565) + .centerCrop() .override(size, size); - FutureTarget target = Glide.with(context) .asBitmap() .apply(options) - .centerCrop() .load(uri) - .signature(signature) .submit(size, size); try { - Bitmap bmp = target.get(); - if (bmp != null) { + Bitmap bitmap = target.get(); + if (bitmap != null) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bmp.compress(Bitmap.CompressFormat.PNG, 100, stream); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream); data = stream.toByteArray(); } } catch (Exception e) { diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java index 51c6ff97f..ed8edb79c 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java @@ -17,7 +17,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.RequestOptions; @@ -166,10 +166,10 @@ public class ImageDecodeTask extends AsyncTask target; @@ -179,14 +179,12 @@ public class ImageDecodeTask extends AsyncTask target = Glide.with(activity) .asBitmap() .apply(options) @@ -101,6 +106,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { // we convert the image on platform side first, when Dart Image.memory does not support it FutureTarget target = Glide.with(activity) .asBitmap() + .apply(options) .load(uri) .submit(); try { @@ -111,8 +117,12 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { if (bitmap != null) { 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); + // Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency + if (MimeTypes.canHaveAlpha(mimeType)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + } else { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); + } success(stream.toByteArray()); } else { error("getImage-image-decode-null", "failed to get image from uri=" + uri, null); diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java b/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java index 07b804415..55dac0d89 100644 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java +++ b/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java @@ -9,17 +9,13 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser; import com.bumptech.glide.module.AppGlideModule; -import com.bumptech.glide.request.RequestOptions; @GlideModule public class AvesAppGlideModule extends AppGlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { - builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565)); - // hide noisy warning (e.g. for images that can't be decoded) builder.setLogLevel(Log.ERROR); } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 6a34c5376..8f158f27e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -4,6 +4,7 @@ import android.content.ContentResolver import android.content.ContentUris import android.content.Context import android.database.Cursor +import android.graphics.Bitmap import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build @@ -13,6 +14,7 @@ import androidx.exifinterface.media.ExifInterface import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPUtils import com.adobe.internal.xmp.properties.XMPPropertyInfo +import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.drew.imaging.ImageMetadataReader import com.drew.imaging.ImageProcessingException import com.drew.lang.Rational @@ -43,6 +45,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText +import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage @@ -52,6 +55,7 @@ import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import java.io.ByteArrayOutputStream import java.io.IOException import java.util.* import kotlin.math.roundToLong @@ -175,13 +179,12 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } - val extension = call.argument("extension") if (mimeType == null || uri == null) { result.error("getCatalogMetadata-args", "failed because of missing arguments", null) return } - val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, extension)) + val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType)) if (isVideo(mimeType)) { metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri)) } @@ -190,7 +193,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.success(metadataMap) } - private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, extension: String?): Map { + private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String): Map { val metadataMap = HashMap() var foundExif = false @@ -518,7 +521,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { try { StorageUtils.openInputStream(context, uri)?.use { input -> val exif = ExifInterface(input) - exif.thumbnailBytes?.let { thumbnails.add(it) } + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + exif.thumbnailBitmap?.let { + val bitmap = TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), it, orientation) + if (bitmap != null) { + val stream = 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) + thumbnails.add(stream.toByteArray()) + } + } } } catch (e: Exception) { // ExifInterface initialization can fail with a RuntimeException diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt index c1e4434f4..8c34b6ed9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt @@ -21,5 +21,6 @@ object BitmapUtils { return TransformationUtils.centerCrop(getBitmapPool(context), bitmap, size, size) } - private fun getBitmapPool(context: Context) = Glide.get(context).bitmapPool + @JvmStatic + fun getBitmapPool(context: Context) = Glide.get(context).bitmapPool } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 146eb27a9..93532cf2e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -32,6 +32,14 @@ object MimeTypes { @JvmStatic fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) + @JvmStatic + // returns whether the specified MIME type represents + // a raster image format that allows an alpha channel + fun canHaveAlpha(mimeType: String?) = when (mimeType) { + BMP, GIF, ICO, PNG, TIFF, WEBP -> true + else -> false + } + // as of Flutter v1.22.0 @JvmStatic fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) { diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 366beb497..c99b1c9fe 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -43,7 +43,6 @@ class MetadataService { final result = await platform.invokeMethod('getCatalogMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, - 'extension': entry.extension, }) as Map; result['contentId'] = entry.contentId; return CatalogMetadata.fromMap(result); diff --git a/lib/widgets/fullscreen/info/metadata_thumbnail.dart b/lib/widgets/fullscreen/info/metadata_thumbnail.dart index da9d4507b..5a13d63dd 100644 --- a/lib/widgets/fullscreen/info/metadata_thumbnail.dart +++ b/lib/widgets/fullscreen/info/metadata_thumbnail.dart @@ -50,20 +50,15 @@ class _MetadataThumbnailsState extends State { future: _loader, builder: (context, snapshot) { if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) { - // TODO TLAD apply the rotation to Exif thumbnail only, on Android side - final turns = (entry.rotationDegrees / 90).round(); final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; return Container( alignment: AlignmentDirectional.topStart, padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4), child: Wrap( children: snapshot.data.map((bytes) { - return RotatedBox( - quarterTurns: turns, - child: Image.memory( - bytes, - scale: devicePixelRatio, - ), + return Image.memory( + bytes, + scale: devicePixelRatio, ); }).toList(), ),