reviewed Bitmap byte compression & recycling
This commit is contained in:
parent
b42201dec0
commit
02095dfb56
7 changed files with 45 additions and 74 deletions
|
@ -6,20 +6,19 @@ import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.LogUtils.createTag
|
import deckers.thibault.aves.utils.LogUtils.createTag
|
||||||
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 kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
@ -134,12 +133,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
.submit(size, size)
|
.submit(size, size)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bitmap = target.get()
|
data = target.get()?.getBytes(canHaveAlpha = true, recycle = false)
|
||||||
if (bitmap != null) {
|
|
||||||
val stream = ByteArrayOutputStream()
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
|
||||||
data = stream.toByteArray()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
|
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ 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.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -46,6 +45,7 @@ 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.BitmapUtils
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
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
|
||||||
|
@ -58,7 +58,6 @@ import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
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
|
||||||
|
@ -548,14 +547,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val exif = ExifInterface(input)
|
val exif = ExifInterface(input)
|
||||||
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||||
exif.thumbnailBitmap?.let {
|
exif.thumbnailBitmap?.let { bitmap ->
|
||||||
val bitmap = TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), it, orientation)
|
TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
|
||||||
if (bitmap != null) {
|
thumbnails.add(it.getBytes(canHaveAlpha = false, recycle = false))
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
package deckers.thibault.aves.channel.calls
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.BitmapRegionDecoder
|
import android.graphics.BitmapRegionDecoder
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class RegionFetcher internal constructor(
|
class RegionFetcher internal constructor(
|
||||||
|
@ -61,19 +60,9 @@ class RegionFetcher internal constructor(
|
||||||
regionRect
|
regionRect
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = decoder.decodeRegion(effectiveRect, options)?.let {
|
val bitmap = decoder.decodeRegion(effectiveRect, options)
|
||||||
val stream = ByteArrayOutputStream()
|
if (bitmap != null) {
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
|
|
||||||
if (MimeTypes.canHaveAlpha(mimeType)) {
|
|
||||||
it.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
|
||||||
} else {
|
|
||||||
it.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
|
||||||
}
|
|
||||||
stream.toByteArray()
|
|
||||||
}
|
|
||||||
if (data != null) {
|
|
||||||
result.success(data)
|
|
||||||
} else {
|
} else {
|
||||||
result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
|
result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,12 @@ import com.bumptech.glide.request.RequestOptions
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail
|
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
|
|
||||||
class ThumbnailFetcher internal constructor(
|
class ThumbnailFetcher internal constructor(
|
||||||
private val activity: Activity,
|
private val activity: Activity,
|
||||||
|
@ -39,6 +40,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
|
|
||||||
fun fetch() {
|
fun fetch() {
|
||||||
var bitmap: Bitmap? = null
|
var bitmap: Bitmap? = null
|
||||||
|
var recycle = true
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
|
||||||
// fetch low quality thumbnails when size is not specified
|
// fetch low quality thumbnails when size is not specified
|
||||||
|
@ -58,25 +60,21 @@ class ThumbnailFetcher internal constructor(
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
try {
|
try {
|
||||||
bitmap = getByGlide()
|
bitmap = getByGlide()
|
||||||
|
recycle = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
exception = e
|
exception = e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val stream = ByteArrayOutputStream()
|
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = recycle))
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
} else {
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG
|
var errorDetails: String? = exception?.message
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
|
if (errorDetails?.isNotEmpty() == true) {
|
||||||
result.success(stream.toByteArray())
|
errorDetails = errorDetails.split("\n".toRegex(), 2).first()
|
||||||
return
|
}
|
||||||
|
result.error("getThumbnail-null", "failed to get thumbnail for uri=$uri", errorDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorDetails: String? = exception?.message
|
|
||||||
if (errorDetails?.isNotEmpty() == true) {
|
|
||||||
errorDetails = errorDetails.split("\n".toRegex(), 2).first()
|
|
||||||
}
|
|
||||||
result.error("getThumbnail-null", "failed to get thumbnail for uri=$uri", errorDetails)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package deckers.thibault.aves.channel.streams
|
package deckers.thibault.aves.channel.streams
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
@ -11,7 +10,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail
|
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||||
import deckers.thibault.aves.utils.MimeTypes.canHaveAlpha
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
|
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
||||||
|
@ -20,7 +20,6 @@ import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@ -102,15 +101,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
|
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
|
||||||
}
|
}
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val stream = ByteArrayOutputStream()
|
success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false))
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
|
|
||||||
if (canHaveAlpha(mimeType)) {
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
|
||||||
} else {
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
|
||||||
}
|
|
||||||
success(stream.toByteArray())
|
|
||||||
} else {
|
} else {
|
||||||
error("streamImage-image-decode-null", "failed to get image from uri=$uri", null)
|
error("streamImage-image-decode-null", "failed to get image from uri=$uri", null)
|
||||||
}
|
}
|
||||||
|
@ -134,11 +125,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
try {
|
try {
|
||||||
val bitmap = target.get()
|
val bitmap = target.get()
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val stream = ByteArrayOutputStream()
|
success(bitmap.getBytes(canHaveAlpha = false, recycle = false))
|
||||||
// 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, 100, stream)
|
|
||||||
success(stream.toByteArray())
|
|
||||||
} else {
|
} else {
|
||||||
error("streamImage-video-null", "failed to get image from uri=$uri", null)
|
error("streamImage-video-null", "failed to get image from uri=$uri", null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package deckers.thibault.aves.decoder
|
package deckers.thibault.aves.decoder
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
|
@ -16,9 +15,9 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
import com.bumptech.glide.module.LibraryGlideModule
|
import com.bumptech.glide.module.LibraryGlideModule
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
|
@ -49,16 +48,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
||||||
val retriever = openMetadataRetriever(model.context, model.uri)
|
val retriever = openMetadataRetriever(model.context, model.uri)
|
||||||
if (retriever != null) {
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
var picture = retriever.embeddedPicture
|
val picture = retriever.embeddedPicture ?: retriever.frameAtTime?.getBytes(canHaveAlpha = false, recycle = false)
|
||||||
if (picture == null) {
|
if (picture != null) {
|
||||||
// not ideal: bitmap -> byte[] -> bitmap
|
callback.onDataReady(ByteArrayInputStream(picture))
|
||||||
// but simple fallback and we cache result
|
} else {
|
||||||
val stream = ByteArrayOutputStream()
|
callback.onLoadFailed(Exception("failed to get embedded picture or any frame"))
|
||||||
val bitmap = retriever.frameAtTime
|
|
||||||
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
|
||||||
picture = stream.toByteArray()
|
|
||||||
}
|
}
|
||||||
callback.onDataReady(ByteArrayInputStream(picture))
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
callback.onLoadFailed(e)
|
callback.onLoadFailed(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -5,8 +5,22 @@ import android.graphics.Bitmap
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import deckers.thibault.aves.metadata.Metadata.getExifCode
|
import deckers.thibault.aves.metadata.Metadata.getExifCode
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
object BitmapUtils {
|
object BitmapUtils {
|
||||||
|
fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean = true): ByteArray {
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
// we compress the bitmap because Flutter cannot decode the raw bytes
|
||||||
|
// `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency
|
||||||
|
if (canHaveAlpha) {
|
||||||
|
this.compress(Bitmap.CompressFormat.PNG, quality, stream)
|
||||||
|
} else {
|
||||||
|
this.compress(Bitmap.CompressFormat.JPEG, quality, stream)
|
||||||
|
}
|
||||||
|
if (recycle) this.recycle()
|
||||||
|
return stream.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? {
|
fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? {
|
||||||
if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap
|
if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap
|
||||||
if (rotationDegrees == 0 && !isFlipped) return bitmap
|
if (rotationDegrees == 0 && !isFlipped) return bitmap
|
||||||
|
|
Loading…
Reference in a new issue