safer image streaming
This commit is contained in:
parent
7d05fb6aef
commit
5e622271fd
4 changed files with 47 additions and 24 deletions
|
@ -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,14 +30,11 @@ 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) {
|
||||
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, with only $availableHeapSize free bytes, for uri=$uri regionRect=$regionRect", null)
|
||||
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, for uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var currentSvgRef = lastSvgRef
|
||||
if (currentSvgRef != null && currentSvgRef.uri != uri) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue