fixed Glide loading options, exif thumbnail orientation
This commit is contained in:
parent
de6cecace6
commit
24f9bc1b81
9 changed files with 54 additions and 39 deletions
|
@ -18,10 +18,9 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
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.FutureTarget;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -173,25 +172,21 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||||
.path(String.valueOf(iconResourceId))
|
.path(String.valueOf(iconResourceId))
|
||||||
.build();
|
.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()
|
RequestOptions options = new RequestOptions()
|
||||||
.signature(signature)
|
.format(DecodeFormat.PREFER_RGB_565)
|
||||||
|
.centerCrop()
|
||||||
.override(size, size);
|
.override(size, size);
|
||||||
|
|
||||||
FutureTarget<Bitmap> target = Glide.with(context)
|
FutureTarget<Bitmap> target = Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.centerCrop()
|
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.signature(signature)
|
|
||||||
.submit(size, size);
|
.submit(size, size);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Bitmap bmp = target.get();
|
Bitmap bitmap = target.get();
|
||||||
if (bmp != null) {
|
if (bitmap != null) {
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream);
|
||||||
data = stream.toByteArray();
|
data = stream.toByteArray();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
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.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
@ -166,10 +166,10 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
int width = params.width;
|
int width = params.width;
|
||||||
int height = params.height;
|
int height = params.height;
|
||||||
|
|
||||||
// add signature to ignore cache for images which got modified but kept the same URI
|
|
||||||
Key signature = new ObjectKey("" + dateModifiedSecs + rotationDegrees + isFlipped + width);
|
|
||||||
RequestOptions options = new RequestOptions()
|
RequestOptions options = new RequestOptions()
|
||||||
.signature(signature)
|
.format(DecodeFormat.PREFER_RGB_565)
|
||||||
|
// add signature to ignore cache for images which got modified but kept the same URI
|
||||||
|
.signature(new ObjectKey("" + dateModifiedSecs + rotationDegrees + isFlipped + width))
|
||||||
.override(width, height);
|
.override(width, height);
|
||||||
|
|
||||||
FutureTarget<Bitmap> target;
|
FutureTarget<Bitmap> target;
|
||||||
|
@ -179,14 +179,12 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(new VideoThumbnail(activity, uri))
|
.load(new VideoThumbnail(activity, uri))
|
||||||
.signature(signature)
|
|
||||||
.submit(width, height);
|
.submit(width, height);
|
||||||
} else {
|
} else {
|
||||||
target = Glide.with(activity)
|
target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.signature(signature)
|
|
||||||
.submit(width, height);
|
.submit(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.DecodeFormat;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
@ -73,9 +74,13 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
// - 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
|
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
||||||
private void getImage() {
|
private void getImage() {
|
||||||
|
// request a fresh image with the highest quality format
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true);
|
||||||
|
|
||||||
if (MimeTypes.isVideo(mimeType)) {
|
if (MimeTypes.isVideo(mimeType)) {
|
||||||
RequestOptions options = new RequestOptions()
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
|
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.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
|
// we convert the image on platform side first, when Dart Image.memory does not support it
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.submit();
|
.submit();
|
||||||
try {
|
try {
|
||||||
|
@ -111,8 +117,12 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG
|
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
if (MimeTypes.canHaveAlpha(mimeType)) {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||||
|
} else {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
|
||||||
|
}
|
||||||
success(stream.toByteArray());
|
success(stream.toByteArray());
|
||||||
} else {
|
} else {
|
||||||
error("getImage-image-decode-null", "failed to get image from uri=" + uri, null);
|
error("getImage-image-decode-null", "failed to get image from uri=" + uri, null);
|
||||||
|
|
|
@ -9,17 +9,13 @@ import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.GlideBuilder;
|
import com.bumptech.glide.GlideBuilder;
|
||||||
import com.bumptech.glide.Registry;
|
import com.bumptech.glide.Registry;
|
||||||
import com.bumptech.glide.annotation.GlideModule;
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
import com.bumptech.glide.load.DecodeFormat;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser;
|
import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser;
|
||||||
import com.bumptech.glide.module.AppGlideModule;
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
public class AvesAppGlideModule extends AppGlideModule {
|
public class AvesAppGlideModule extends AppGlideModule {
|
||||||
@Override
|
@Override
|
||||||
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
|
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)
|
// hide noisy warning (e.g. for images that can't be decoded)
|
||||||
builder.setLogLevel(Log.ERROR);
|
builder.setLogLevel(Log.ERROR);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -13,6 +14,7 @@ import androidx.exifinterface.media.ExifInterface
|
||||||
import com.adobe.internal.xmp.XMPException
|
import com.adobe.internal.xmp.XMPException
|
||||||
import com.adobe.internal.xmp.XMPUtils
|
import com.adobe.internal.xmp.XMPUtils
|
||||||
import com.adobe.internal.xmp.properties.XMPPropertyInfo
|
import com.adobe.internal.xmp.properties.XMPPropertyInfo
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import com.drew.imaging.ImageMetadataReader
|
import com.drew.imaging.ImageMetadataReader
|
||||||
import com.drew.imaging.ImageProcessingException
|
import com.drew.imaging.ImageProcessingException
|
||||||
import com.drew.lang.Rational
|
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.MetadataExtractorHelper.getSafeString
|
||||||
import deckers.thibault.aves.metadata.XMP
|
import deckers.thibault.aves.metadata.XMP
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
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.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
@ -175,13 +179,12 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val extension = call.argument<String>("extension")
|
|
||||||
if (mimeType == null || uri == null) {
|
if (mimeType == null || uri == null) {
|
||||||
result.error("getCatalogMetadata-args", "failed because of missing arguments", null)
|
result.error("getCatalogMetadata-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType, extension))
|
val metadataMap = HashMap(getCatalogMetadataByMetadataExtractor(uri, mimeType))
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri))
|
metadataMap.putAll(getVideoCatalogMetadataByMediaMetadataRetriever(uri))
|
||||||
}
|
}
|
||||||
|
@ -190,7 +193,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, extension: String?): Map<String, Any> {
|
private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String): Map<String, Any> {
|
||||||
val metadataMap = HashMap<String, Any>()
|
val metadataMap = HashMap<String, Any>()
|
||||||
|
|
||||||
var foundExif = false
|
var foundExif = false
|
||||||
|
@ -518,7 +521,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val exif = ExifInterface(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) {
|
} catch (e: Exception) {
|
||||||
// ExifInterface initialization can fail with a RuntimeException
|
// ExifInterface initialization can fail with a RuntimeException
|
||||||
|
|
|
@ -21,5 +21,6 @@ object BitmapUtils {
|
||||||
return TransformationUtils.centerCrop(getBitmapPool(context), bitmap, size, size)
|
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
|
||||||
}
|
}
|
|
@ -32,6 +32,14 @@ object MimeTypes {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO)
|
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
|
// as of Flutter v1.22.0
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
|
fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
|
||||||
|
|
|
@ -43,7 +43,6 @@ class MetadataService {
|
||||||
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
'extension': entry.extension,
|
|
||||||
}) as Map;
|
}) as Map;
|
||||||
result['contentId'] = entry.contentId;
|
result['contentId'] = entry.contentId;
|
||||||
return CatalogMetadata.fromMap(result);
|
return CatalogMetadata.fromMap(result);
|
||||||
|
|
|
@ -50,20 +50,15 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||||
future: _loader,
|
future: _loader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
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;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
return Container(
|
return Container(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4),
|
padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: snapshot.data.map((bytes) {
|
children: snapshot.data.map((bytes) {
|
||||||
return RotatedBox(
|
return Image.memory(
|
||||||
quarterTurns: turns,
|
bytes,
|
||||||
child: Image.memory(
|
scale: devicePixelRatio,
|
||||||
bytes,
|
|
||||||
scale: devicePixelRatio,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue