fixed raw preview region decoding sample size

This commit is contained in:
Thibault Deckers 2024-06-08 18:49:39 +02:00
parent 1346e8867b
commit da4a7ae38f
7 changed files with 64 additions and 37 deletions

View file

@ -17,6 +17,10 @@ All notable changes to this project will be documented in this file.
- support for Android KitKat (API 19)
### Fixed
- crash when cataloguing large images
## <a id="v1.11.1"></a>[v1.11.1] - 2024-05-03
### Added

View file

@ -14,10 +14,12 @@ import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MathUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
import kotlin.math.max
import kotlin.math.roundToInt
// As of Android 14 (API 34), `BitmapRegionDecoder` documentation states
@ -60,10 +62,6 @@ class RegionFetcher internal constructor(
return
}
val options = BitmapFactory.Options().apply {
inSampleSize = sampleSize
}
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.uri != uri) {
currentDecoderRef = null
@ -85,27 +83,35 @@ class RegionFetcher internal constructor(
// with raw images, the known image size may not match the decoded image size
// so we scale the requested region accordingly
val effectiveRect = if (imageWidth != decoder.width || imageHeight != decoder.height) {
var effectiveRect = regionRect
var effectiveSampleSize = sampleSize
if (imageWidth != decoder.width || imageHeight != decoder.height) {
val xf = decoder.width.toDouble() / imageWidth
val yf = decoder.height.toDouble() / imageHeight
Rect(
effectiveRect = Rect(
(regionRect.left * xf).roundToInt(),
(regionRect.top * yf).roundToInt(),
(regionRect.right * xf).roundToInt(),
(regionRect.bottom * yf).roundToInt(),
)
} else {
regionRect
val factor = MathUtils.highestPowerOf2((1 / max(xf, yf)).roundToInt())
if (factor > 1) {
effectiveSampleSize = max(1, effectiveSampleSize / factor)
}
}
// use `Long` as rect size could be unexpectedly large and go beyond `Int` max
val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * effectiveRect.width() * effectiveRect.height() / sampleSize
val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * effectiveRect.width() * effectiveRect.height() / effectiveSampleSize
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
// decoding a region that large would yield an OOM when creating the bitmap
result.error("fetch-large-region", "Region too large for uri=$uri regionRect=$regionRect", null)
return
}
val options = BitmapFactory.Options().apply {
inSampleSize = effectiveSampleSize
}
val bitmap = decoder.decodeRegion(effectiveRect, options)
if (bitmap != null) {
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))

View file

@ -134,16 +134,8 @@ object Metadata {
private val previewFiles = HashMap<Uri, File>()
private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri {
return when (mimeType) {
// formats known to yield OOM for large files
MimeTypes.DNG,
MimeTypes.DNG_ADOBE,
MimeTypes.HEIC,
MimeTypes.HEIF,
MimeTypes.MP4,
MimeTypes.PSD_VND,
MimeTypes.PSD_X,
MimeTypes.TIFF -> {
return if ((MimeTypes.isImage(mimeType) || mimeType == MimeTypes.MP4)) {
if (isDangerouslyLarge(sizeBytes)) {
// make a preview from the beginning of the file,
// hoping the metadata is accessible in the copied chunk
@ -157,9 +149,9 @@ object Metadata {
// small enough to be safe as it is
uri
}
}
} else {
// *probably* safe
else -> uri
uri
}
}

View file

@ -122,7 +122,6 @@ object Helper {
val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, streamLength)
val metadata = com.drew.metadata.Metadata()
val handler = SafeExifTiffHandler(metadata, null, 0)
Log.d(LOG_TAG, "safeReadTiff: availableHeapSize=${MemoryUtils.getAvailableHeapSize()}")
TiffReader().processTiff(reader, handler, 0)
return metadata
}

View file

@ -0,0 +1,9 @@
package deckers.thibault.aves.utils
import kotlin.math.log2
import kotlin.math.pow
object MathUtils {
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
}

View file

@ -10,9 +10,9 @@ class ImageProviderTest {
@Test
fun imageProvider_CorrectEmailSimple_ReturnsTrue() {
val date = LocalDate.of(1990, Month.FEBRUARY, 11).toEpochDay()
assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Europe/Paris"), date), "+01:00")
assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("UTC"), date), "+00:00")
assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Asia/Kolkata"), date), "+05:30")
assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("America/Chicago"), date), "-06:00")
assertEquals("+01:00", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Europe/Paris"), date))
assertEquals("+00:00", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("UTC"), date))
assertEquals("+05:30", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Asia/Kolkata"), date))
assertEquals("-06:00", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("America/Chicago"), date))
}
}

View file

@ -0,0 +1,17 @@
package deckers.thibault.aves.utils
import deckers.thibault.aves.utils.MathUtils.highestPowerOf2
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class MathUtilsTest {
@Test
fun mathUtils_highestPowerOf2() {
assertEquals(1024, highestPowerOf2(1024))
assertEquals(32, highestPowerOf2(42))
assertEquals(0, highestPowerOf2(0))
assertEquals(0, highestPowerOf2(-42))
assertEquals(0, highestPowerOf2(.5))
assertEquals(1, highestPowerOf2(1.5))
}
}