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 com.caverock.androidsvg.SVGParseException
|
||||||
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
|
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import deckers.thibault.aves.utils.MemoryUtils
|
||||||
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 kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
@ -29,13 +30,10 @@ class SvgRegionFetcher internal constructor(
|
||||||
imageHeight: Int,
|
imageHeight: Int,
|
||||||
result: MethodChannel.Result,
|
result: MethodChannel.Result,
|
||||||
) {
|
) {
|
||||||
if (sizeBytes != null && sizeBytes > FILE_SIZE_DANGER_THRESHOLD) {
|
if (!MemoryUtils.canAllocate(sizeBytes)) {
|
||||||
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
|
// opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser`
|
||||||
if (sizeBytes > availableHeapSize) {
|
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, for uri=$uri regionRect=$regionRect", null)
|
||||||
// opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser`
|
return
|
||||||
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, with only $availableHeapSize free bytes, for uri=$uri regionRect=$regionRect", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentSvgRef = lastSvgRef
|
var currentSvgRef = lastSvgRef
|
||||||
|
@ -113,9 +111,4 @@ class SvgRegionFetcher internal constructor(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val svg: SVG,
|
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.applyExifOrientation
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
import deckers.thibault.aves.utils.MemoryUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter
|
import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isHeic
|
import deckers.thibault.aves.utils.MimeTypes.isHeic
|
||||||
|
@ -97,18 +98,23 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
streamVideoByGlide(uri, mimeType)
|
streamVideoByGlide(uri, mimeType, sizeBytes)
|
||||||
} else if (!canDecodeWithFlutter(mimeType, rotationDegrees, isFlipped)) {
|
} else if (!canDecodeWithFlutter(mimeType, rotationDegrees, isFlipped)) {
|
||||||
// decode exotic format on platform side, then encode it in portable format for Flutter
|
// decode exotic format on platform side, then encode it in portable format for Flutter
|
||||||
streamImageByGlide(uri, pageId, mimeType, sizeBytes, rotationDegrees, isFlipped)
|
streamImageByGlide(uri, pageId, mimeType, sizeBytes, rotationDegrees, isFlipped)
|
||||||
} else {
|
} else {
|
||||||
// to be decoded by Flutter
|
// to be decoded by Flutter
|
||||||
streamImageAsIs(uri, mimeType)
|
streamImageAsIs(uri, mimeType, sizeBytes)
|
||||||
}
|
}
|
||||||
endOfStream()
|
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 {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input -> streamBytes(input) }
|
StorageUtils.openInputStream(context, uri)?.use { input -> streamBytes(input) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -144,7 +150,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
|
||||||
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
||||||
}
|
}
|
||||||
if (bitmap != null) {
|
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 {
|
} else {
|
||||||
error("streamImage-image-decode-null", "failed to get image for mimeType=$mimeType uri=$uri", null)
|
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)
|
val target = Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(glideOptions)
|
.apply(glideOptions)
|
||||||
|
@ -165,7 +176,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
val bitmap = target.get()
|
val bitmap = target.get()
|
||||||
if (bitmap != null) {
|
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 {
|
} else {
|
||||||
error("streamImage-video-null", "failed to get image for mimeType=$mimeType uri=$uri", null)
|
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
|
private const val INITIAL_BUFFER_SIZE = 2 shl 17 // 256kB
|
||||||
|
|
||||||
// arbitrary size to detect buffer that may yield an OOM
|
// 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 freeBaos = ArrayList<ByteArrayOutputStream>()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
@ -44,11 +44,8 @@ object BitmapUtils {
|
||||||
if (recycle) this.recycle()
|
if (recycle) this.recycle()
|
||||||
|
|
||||||
val bufferSize = stream.size()
|
val bufferSize = stream.size()
|
||||||
if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD) {
|
if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD && !MemoryUtils.canAllocate(bufferSize)) {
|
||||||
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
|
throw Exception("bitmap compressed to $bufferSize bytes, which cannot be allocated to a new byte array")
|
||||||
if (bufferSize > availableHeapSize) {
|
|
||||||
throw Exception("compressed bitmap to $bufferSize bytes, which cannot be allocated to a new byte array, with only $availableHeapSize free bytes")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = stream.toByteArray()
|
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