safer image streaming

This commit is contained in:
Thibault Deckers 2022-11-07 22:44:54 +01:00
parent 7d05fb6aef
commit 5e622271fd
4 changed files with 47 additions and 24 deletions

View file

@ -12,6 +12,7 @@ import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
import kotlin.math.ceil
@ -29,13 +30,10 @@ class SvgRegionFetcher internal constructor(
imageHeight: Int,
result: MethodChannel.Result,
) {
if (sizeBytes != null && sizeBytes > FILE_SIZE_DANGER_THRESHOLD) {
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
if (sizeBytes > availableHeapSize) {
// opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser`
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, with only $availableHeapSize free bytes, for uri=$uri regionRect=$regionRect", null)
return
}
if (!MemoryUtils.canAllocate(sizeBytes)) {
// opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser`
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, for uri=$uri regionRect=$regionRect", null)
return
}
var currentSvgRef = lastSvgRef
@ -113,9 +111,4 @@ class SvgRegionFetcher internal constructor(
val uri: Uri,
val svg: SVG,
)
companion object {
// arbitrary size to detect files that may yield an OOM
private const val FILE_SIZE_DANGER_THRESHOLD = 10 * (1 shl 20) // MB
}
}

View file

@ -15,6 +15,7 @@ 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.LogUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter
import deckers.thibault.aves.utils.MimeTypes.isHeic
@ -97,18 +98,23 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
}
if (isVideo(mimeType)) {
streamVideoByGlide(uri, mimeType)
streamVideoByGlide(uri, mimeType, sizeBytes)
} else if (!canDecodeWithFlutter(mimeType, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, pageId, mimeType, sizeBytes, rotationDegrees, isFlipped)
} else {
// to be decoded by Flutter
streamImageAsIs(uri, mimeType)
streamImageAsIs(uri, mimeType, sizeBytes)
}
endOfStream()
}
private fun streamImageAsIs(uri: Uri, mimeType: String) {
private fun streamImageAsIs(uri: Uri, mimeType: String, sizeBytes: Long?) {
if (!MemoryUtils.canAllocate(sizeBytes)) {
error("streamImage-image-read-large", "original image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
return
}
try {
StorageUtils.openInputStream(context, uri)?.use { input -> streamBytes(input) }
} catch (e: Exception) {
@ -144,7 +150,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
if (bitmap != null) {
success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false))
val bytes = bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false)
if (MemoryUtils.canAllocate(sizeBytes)) {
success(bytes)
} else {
error("streamImage-image-decode-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
}
} else {
error("streamImage-image-decode-null", "failed to get image for mimeType=$mimeType uri=$uri", null)
}
@ -155,7 +166,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
}
}
private suspend fun streamVideoByGlide(uri: Uri, mimeType: String) {
private suspend fun streamVideoByGlide(uri: Uri, mimeType: String, sizeBytes: Long?) {
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
@ -165,7 +176,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
@Suppress("BlockingMethodInNonBlockingContext")
val bitmap = target.get()
if (bitmap != null) {
success(bitmap.getBytes(canHaveAlpha = false, recycle = false))
val bytes = bitmap.getBytes(canHaveAlpha = false, recycle = false)
if (MemoryUtils.canAllocate(sizeBytes)) {
success(bytes)
} else {
error("streamImage-video-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
}
} else {
error("streamImage-video-null", "failed to get image for mimeType=$mimeType uri=$uri", null)
}

View file

@ -15,7 +15,7 @@ object BitmapUtils {
private const val INITIAL_BUFFER_SIZE = 2 shl 17 // 256kB
// arbitrary size to detect buffer that may yield an OOM
private const val BUFFER_SIZE_DANGER_THRESHOLD = 10 * (1 shl 20) // MB
private const val BUFFER_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
private val freeBaos = ArrayList<ByteArrayOutputStream>()
private val mutex = Mutex()
@ -44,11 +44,8 @@ object BitmapUtils {
if (recycle) this.recycle()
val bufferSize = stream.size()
if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD) {
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
if (bufferSize > availableHeapSize) {
throw Exception("compressed bitmap to $bufferSize bytes, which cannot be allocated to a new byte array, with only $availableHeapSize free bytes")
}
if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD && !MemoryUtils.canAllocate(bufferSize)) {
throw Exception("bitmap compressed to $bufferSize bytes, which cannot be allocated to a new byte array")
}
val byteArray = stream.toByteArray()

View file

@ -0,0 +1,17 @@
package deckers.thibault.aves.utils
import android.util.Log
object MemoryUtils {
private val LOG_TAG = LogUtils.createTag<MemoryUtils>()
fun canAllocate(byteSize: Number?): Boolean {
byteSize ?: return true
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
val danger = byteSize.toLong() > availableHeapSize
if (danger) {
Log.e(LOG_TAG, "trying to handle $byteSize bytes, with only $availableHeapSize free bytes")
}
return !danger
}
}