reviewed Bitmap byte compression & recycling

This commit is contained in:
Thibault Deckers 2020-11-10 21:09:23 +09:00
parent b42201dec0
commit 02095dfb56
7 changed files with 45 additions and 74 deletions

View file

@ -6,20 +6,19 @@ import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import androidx.core.content.FileProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils.createTag
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
@ -134,12 +133,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
.submit(size, size)
try {
val bitmap = target.get()
if (bitmap != null) {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
data = stream.toByteArray()
}
data = target.get()?.getBytes(canHaveAlpha = true, recycle = false)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
}

View file

@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
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.getSafeLocalizedText
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
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 kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
import kotlin.math.roundToLong
@ -548,14 +547,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
StorageUtils.openInputStream(context, uri)?.use { input ->
val exif = ExifInterface(input)
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())
exif.thumbnailBitmap?.let { bitmap ->
TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
thumbnails.add(it.getBytes(canHaveAlpha = false, recycle = false))
}
}
}

View file

@ -1,16 +1,15 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.net.Uri
import android.util.Size
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
import kotlin.math.roundToInt
class RegionFetcher internal constructor(
@ -61,19 +60,9 @@ class RegionFetcher internal constructor(
regionRect
}
val data = decoder.decodeRegion(effectiveRect, options)?.let {
val stream = ByteArrayOutputStream()
// 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 (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)
val bitmap = decoder.decodeRegion(effectiveRect, options)
if (bitmap != null) {
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
} else {
result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
}

View file

@ -15,11 +15,12 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.decoder.VideoThumbnail
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.needRotationAfterContentResolverThumbnail
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
class ThumbnailFetcher internal constructor(
private val activity: Activity,
@ -39,6 +40,7 @@ class ThumbnailFetcher internal constructor(
fun fetch() {
var bitmap: Bitmap? = null
var recycle = true
var exception: Exception? = null
// fetch low quality thumbnails when size is not specified
@ -58,25 +60,21 @@ class ThumbnailFetcher internal constructor(
if (bitmap == null) {
try {
bitmap = getByGlide()
recycle = false
} catch (e: Exception) {
exception = e
}
}
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)
result.success(stream.toByteArray())
return
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = recycle))
} else {
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)
}
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)

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.channel.streams
import android.app.Activity
import android.graphics.Bitmap
import android.net.Uri
import android.os.Handler
import android.os.Looper
@ -11,7 +10,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.VideoThumbnail
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.isVideo
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 kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
@ -102,15 +101,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
}
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, 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())
success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false))
} else {
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 {
val bitmap = target.get()
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, 100, stream)
success(stream.toByteArray())
success(bitmap.getBytes(canHaveAlpha = false, recycle = false))
} else {
error("streamImage-video-null", "failed to get image from uri=$uri", null)
}

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.decoder
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import com.bumptech.glide.Glide
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.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
@GlideModule
@ -49,16 +48,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
val retriever = openMetadataRetriever(model.context, model.uri)
if (retriever != null) {
try {
var picture = retriever.embeddedPicture
if (picture == null) {
// not ideal: bitmap -> byte[] -> bitmap
// but simple fallback and we cache result
val stream = ByteArrayOutputStream()
val bitmap = retriever.frameAtTime
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream)
picture = stream.toByteArray()
val picture = retriever.embeddedPicture ?: retriever.frameAtTime?.getBytes(canHaveAlpha = false, recycle = false)
if (picture != null) {
callback.onDataReady(ByteArrayInputStream(picture))
} else {
callback.onLoadFailed(Exception("failed to get embedded picture or any frame"))
}
callback.onDataReady(ByteArrayInputStream(picture))
} catch (e: Exception) {
callback.onLoadFailed(e)
} finally {

View file

@ -5,8 +5,22 @@ import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import deckers.thibault.aves.metadata.Metadata.getExifCode
import java.io.ByteArrayOutputStream
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? {
if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap
if (rotationDegrees == 0 && !isFlipped) return bitmap